I've finally managed to successfully embed projection buffers in a tool window and hook them up to C#'s language services. One caveat: this approach only works for Visual Studio using Roslyn. I've published a Github project you can use as well as an accompanying blog post.
The answer to your question is long and involves so many moving pieces that it doesn't lend itself to the StackOverflow style of Q&A very well. That being said, I'll summarize the necessary steps and include some relevant code.
The following sample creates a projection buffer of a file comprised of the first 100 characters of the file.
We first create an IVsInvisibleEditor
for a given filepath and create a code window for it. We set the contents of this code window to be the IVsTextLines
of the IVsInvisibleEditor
.
We then set a custom role "CustomProjectionRole"
on the text buffer of this code window. This role allows us to customize the text buffer via a MEF exported ITextViewModelProvider
.
public IWpfTextViewHost CreateEditor(string filePath, int start = 0, int end = 100)
{
//IVsInvisibleEditors are in-memory represenations of typical Visual Studio editors.
//Language services, highlighting and error squiggles are hooked up to these editors
//for us once we convert them to WpfTextViews.
var invisibleEditor = GetInvisibleEditor(filePath);
var docDataPointer = IntPtr.Zero;
Guid guidIVsTextLines = typeof(IVsTextLines).GUID;
ErrorHandler.ThrowOnFailure(invisibleEditor.GetDocData(
fEnsureWritable: 1
, riid: ref guidIVsTextLines
, ppDocData: out docDataPointer));
IVsTextLines docData = (IVsTextLines)Marshal.GetObjectForIUnknown(docDataPointer);
//Create a code window adapter
var codeWindow = _editorAdapter.CreateVsCodeWindowAdapter(VisualStudioServices.OLEServiceProvider);
ErrorHandler.ThrowOnFailure(codeWindow.SetBuffer(docData));
//Get a text view for our editor which we will then use to get the WPF control for that editor.
IVsTextView textView;
ErrorHandler.ThrowOnFailure(codeWindow.GetPrimaryView(out textView));
//We add our own role to this text view. Later this will allow us to selectively modify
//this editor without getting in the way of Visual Studio's normal editors.
var roles = _editorFactoryService.DefaultRoles.Concat(new string[] { "CustomProjectionRole" });
var vsTextBuffer = docData as IVsTextBuffer;
var textBuffer = _editorAdapter.GetDataBuffer(vsTextBuffer);
textBuffer.Properties.AddProperty("StartPosition", start);
textBuffer.Properties.AddProperty("EndPosition", end);
var guid = VSConstants.VsTextBufferUserDataGuid.VsTextViewRoles_guid;
((IVsUserData)codeWindow).SetData(ref guid, _editorFactoryService.CreateTextViewRoleSet(roles).ToString());
_currentlyFocusedTextView = textView;
var textViewHost = _editorAdapter.GetWpfTextViewHost(textView);
return textViewHost;
}
We now create an IVsTextViewModelProvider
that creates and returns a ProjectionTextViewModel
. This ProjectionTextViewModel
saves a projection buffer within its Visual Buffer. This means that when this buffer is displayed, the projection buffer is what is shown. However, the language services of the backing data buffer operate correctly.
[Export(typeof(ITextViewModelProvider)), ContentType("CSharp"), TextViewRole("CustomProjectionRole")]
internal class ProjectionTextViewModelProvider : ITextViewModelProvider
{
public ITextViewModel CreateTextViewModel(ITextDataModel dataModel, ITextViewRoleSet roles)
{
//Create a projection buffer based on the specified start and end position.
var projectionBuffer = CreateProjectionBuffer(dataModel);
//Display this projection buffer in the visual buffer, while still maintaining
//the full file buffer as the underlying data buffer.
var textViewModel = new ProjectionTextViewModel(dataModel, projectionBuffer);
return textViewModel;
}
public IProjectionBuffer CreateProjectionBuffer(ITextDataModel dataModel)
{
//retrieve start and end position that we saved in MyToolWindow.CreateEditor()
var startPosition = (int)dataModel.DataBuffer.Properties.GetProperty("StartPosition");
var endPosition = (int)dataModel.DataBuffer.Properties.GetProperty("EndPosition");
var length = endPosition - startPosition;
//Take a snapshot of the text within these indices.
var textSnapshot = dataModel.DataBuffer.CurrentSnapshot;
var trackingSpan = textSnapshot.CreateTrackingSpan(startPosition, length, SpanTrackingMode.EdgeExclusive);
//Create the actual projection buffer
var projectionBuffer = ProjectionBufferFactory.CreateProjectionBuffer(
null
, new List<object>() { trackingSpan }
, ProjectionBufferOptions.None
);
return projectionBuffer;
}
[Import]
public IProjectionBufferFactoryService ProjectionBufferFactory { get; set; }
}
Hopefully this gets any future visitors off to a good start.