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

4

13

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.

Marshamarshal answered 14/4, 2014 at 12:18 Comment(5)
Are you getting any language services to work within your projection buffer? Syntax highlighting, auto-complete etc?Aberration
No, it will not recognize that I am tagging the buffer as C# at all. I didn't realize it but the only other post on here that helped me was yours. I tagged my post with similar tags in hopes that Jason might answer mine as well. I think the only way to get an answer is going to be a support call with Microsoft, seems this is not an area the general development population is interested in.Marshamarshal
Yeah, there is next to no info on this stuff out there. If you figure it out, I'd love if you posted your solution here as I'll be watching for it. If I manage to get it working, I'll make sure to post here as well.Aberration
Agreed, if I save up enough pennies for a support call to Microsoft and get an answer I will definitely share it here.Marshamarshal
I've opened a support call, but haven't made much progress. I've also opened a uservoice issue here: visualstudio.uservoice.com/forums/121579-visual-studio/…Aberration
M
1

A working example can be found here but as per the warning comments it is not a simple task.

// Abandon all hope ye who enters here.
// https://twitter.com/Schabse/status/393092191356076032
// https://twitter.com/jasonmalinowski/status/393094145398407168

// Based on decompiled code from Microsoft.VisualStudio.Html.ContainedLanguage.Server
// Thanks to Jason Malinowski for helping me navigate this mess.
// All of this can go away when the Roslyn editor ships.
Marshamarshal answered 1/6, 2014 at 12:21 Comment(0)
D
6

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.

Durance answered 18/5, 2016 at 5:12 Comment(0)
A
5

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.

Aberration answered 2/8, 2014 at 0:28 Comment(0)
M
1

A working example can be found here but as per the warning comments it is not a simple task.

// Abandon all hope ye who enters here.
// https://twitter.com/Schabse/status/393092191356076032
// https://twitter.com/jasonmalinowski/status/393094145398407168

// Based on decompiled code from Microsoft.VisualStudio.Html.ContainedLanguage.Server
// Thanks to Jason Malinowski for helping me navigate this mess.
// All of this can go away when the Roslyn editor ships.
Marshamarshal answered 1/6, 2014 at 12:21 Comment(0)
L
0

The comment you left seems to be talking about outline but not to support embedded languages. If you are trying to hide text you can take a look at this IntraText Code.

If you download and install it or debug it by setting the startup project to devenv and the command args to /RootSuffix Exp you can then open a text file and type #CCCCCC or any other hex web color. The code will collapse and show you a simple graphic.

As far as I know about supporting embedded languages, is that you have to to write your own custom Language Service. I have wanted to be able to support existing language services intellisense features. However, it seems that you must reinvent the wheel when it comes to this sort of behavior within visual studio.

Lamanna answered 31/5, 2014 at 8:46 Comment(1)
I'm fairly certain that you don't have to do that if you're using the Roslyn compiler. You should be able to project from an IVsInvisibleEditor's buffer and inherit the language services. This worked with the Roslyn 2012 CTP + Visual Studio 2012, but doesn't work with the new release.Aberration

© 2022 - 2024 — McMap. All rights reserved.