Good thing to know when making a custom editor
By Matthew Manela (Developer Solution Intern)
Here’s a walkthrough to a problem I ran into when I was trying to create a custom editor that lets you change what language the code window is in on the fly.
Here is the situation: You are developing a VSIP package for Visual Studio. In this package you want to create a custom editor that will open a simple xml file called simple.myxml which contains data like this:
<Title>My Loaded File</Title>
string word = "editor";
foreach(char letter in word.ToCharArray())
Your editor will read this xml file and parse it and populate two fields in an editor control. One is the title field which is just a text box that will contain the text between the Title tags. The other is a control which contains an IVsCodeWindow with an IVsTextBuffer attached to it. This text buffer is completely in memory and has no association with a file on disk. The code windows language service will be set based upon the text in the Language tags and the text in the code window will be loaded with the text between the Code tags. The finished result after loaded simple.sml is something that looks like this:
This all seems to have worked well. The file loaded was parsed correctly and was loaded into my editor. The color coding worked so we have nice C# color coding. At this point you think “wow, so easy”. Then you load your second file that has different code and you see this:
It seems now only the text color coded correctly and nothing else did. But what went wrong. If you try this with a J# language service you see the same problem, but you don’t see it with the VB language service.
The reason this isn’t working has to do with how the C# and J# language services figure out if two text buffers are the same. They check something called the BufferMoniker. Usually if your text buffer is directly correlated with a file on disk the buffer moniker will be the filename. However, since our text buffer is completely in memory our buffer moniker is an empty string. This caused a problem only when you open your second file. The first file color codes fine but when you open the second the language service will make a check and say “Is the current buffer moniker the same as the previous one then don’t refresh the color coding lexical data”. In this case it will check is empty string equal to empty string, then don’t refresh lexical data.
This problem is hard to figure out but the solution isn’t hard. You need to set a unique buffer moniker for each of your text buffers. You can set the buffer moniker uniquely using a generated Guid and your IVsTextBuffer object.
IVsTextBuffer myTextBuffer;//defined elsewhere
IVsUserData userData = (IVsUserData) myTextBuffer;
string uniqueMoniker = Guid.NewGuid().ToString();
Guid bufferMonikerGuid = typeof(IVsUserData).GUID;
userData.SetData(ref bufferMonikerGuid, uniqueMoniker);
That is all you need. Before your load your doc data just set the buffer moniker for the text buffer you created and now color coding will work for more than one window.
Not so Fast … We are not done yet!
Depending on how you are inserting text into your code window you can run into more problems. Let’s say you use the following method to insert text into the code window:
void SetText(string text)
vsTextLines.CreateEditPoint(0, 0, out oObj);
EditPoint editPoint = (EditPoint)oObj;
This worked fine until you added the buffer moniker. The problem here is that the automation model (which contains EditPoint’s) makes an assumption. If a buffer moniker is not empty that means there is a file on disk. Since your buffer moniker isn’t a file on disk then CreateEditPoint will fail and oObj will be null. So the wonderfully simple solution above has broken your code.
The answer to this problem is: don’t use the automation model. Ideally you would be able to use the automation model to edit the text buffer but given that limitation you can still get SetText to work. You can use ReplaceLines in IVsTextLines to replace the content in the text buffer with other text.
public void SetText(string text)
int endLine, endCol;
textLines.GetLastLineIndex(out endLine, out endCol);
int len = (text == null) ? 0 : newText.Length;
//fix location of the string
IntPtr pText = Marshal.StringToCoTaskMemAuto(text);
textLines.ReplaceLines(0, 0, endLine, endCol, pText, len, null);
Now your custom editor control works. Often a problem that seems impossible has a simple solution that’s really hard to find.