Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you use a projection buffer to support embedded languages in the Visual Studio editor

At the end of the first paragraph in this link it states:

The Visual Studio text outlining feature is implemented by using a projection buffer to hide the collapsed text, and the Visual Studio editor for ASP.NET pages uses projection to support embedded languages such as Visual Basic and C#.

I have searched and searched but have not found any examples or documentation at all to accomplish this, does anyone have any idea how this is done? I have gotten classification working and created a projection buffer of the spans I want to be classified as C# code. I set the buffers context type as "CSharp" but the spans never get classified. I have also tried to base my content type from "projection" but that does now work either.

like image 702
Frank Avatar asked Apr 14 '14 12:04

Frank


Video Answer


2 Answers

Projection buffers in Visual Studio were primarily created to handle scenarios where one language region is embedded in another language. Classical examples are CSS and Javascript inside HTML. Similarly, C# or VB in ASP.NET or Razor. In fact, HTML editor handles many languages and its projection buffer architecture is quite extensible (I wrote big part of it). This way all functionality inside style block is handled by the CSS editor and HTML editor doesn't have to do much.

Projection buffer is not as complicated when you get how it works. Projection buffers form a graph and top level buffer is presented in the view. Projection buffer does not have its own content, it consists of projection spans which, in turn are either tracking spans (ITrackingSpan) or inert regions (strings).

Consider style block inside HTML. First, you need to create projection buffer with content type of "projection" or another content type that is derived from "projection". Then you create projection buffer that will hold CSS with the content type of "CSS". File as read from disk is located in a text buffer with content type "HTMLX" ("HTML" content type is reserved for classic Web Forms editor). HTML editor parses the file and extracts style block content as well as inline styles into a separate string. Inline style fragments are decorated into classes so they appear well formed to the CSS editor.

Now projection mappings are constructed. First CSS projection buffer is populated with inert strings (they represent CSS not visible to the user such as decorations of inline styles) as well as tracking spans created off disk buffer (HTML) that define regions visible to the user - specifically, contents of style block(s).

Then projections for the view (top-level) buffer are constructed. These projections are a list of tracking spans which is combination of tracking spans created off CSS editor projection buffer (NOT off HTML disk buffer) and tracking spans created off the HTML disk buffer that represent HTML parts of the view.

The graph looks roughly like this

  View Buffer [ContentType = "projection"]
    |      \
    |     CSS Projection [ContentType = CSS]
    |      /
  Disk Buffer [ContentType = HTMLX]

Edits made to HTML parts of the view buffer are reflected to the disk buffer and HTML language services provides completion, syntax check, etc. Edits made in style blocks go to the CSS project buffer and CSS editor provides completion and syntax check. They also get reflected to the disk buffer via second level of projections.

Now, forwarding commands down to the embedded language (such as context menu invoke) and maintaining proper breakpoint mapping for Javascript or C# is a separate code. Projections only help with view-related things, chain of controllers and debugger operations have to be handled separately. HTML editor command controller is aware of embedded languages and depending on the caret position forwards commands down to the respective language service.

like image 94
Mikhail Arkhipov - MSFT Avatar answered Oct 30 '22 11:10

Mikhail Arkhipov - MSFT


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.

like image 37
JoshVarty Avatar answered Oct 30 '22 09:10

JoshVarty