VisualStudio.Extensibility Documentation

repository·main·Indexed 19 days ago

https://github.com/microsoft/vsextensibility

A modern, out-of-process framework for building Visual Studio extensions designed to improve IDE stability and performance. This documentation covers the asynchronous API, language configuration via JSON and TextMate Grammar, and integration with VSSDK components such as AsyncPackage and MEF. It includes guidance on implementing brokered services, text classification using ITextViewTaggerProvider, and creating invokable Code Lens providers.

Tokens
38.6K
Snippets
107
Records
144
Agent score
64%

What's inside VisualStudio.Extensibility

  1. Explore Visual Studio Out-of-Process Extensibility Samples

    main

    The New_Extensibility_Model/Samples directory contains various implementation examples for the Visual Studio out-of-process extensibility model. These samples cover a wide range of capabilities, from basic command handling to advanced UI integration and debugger visualizers.

    Available Sample Scenarios

    SampleDescription
    Simple command handlerBasics of working with commands.
    Insert guidInserting text/code in the editor, configuring command activation conditions, and using resource files for localization.
    Command parentingAuthoring commands that can be parented to different aspects of the IDE.
    Document selectorCreating editor extensions applicable only to files matching specific path patterns.
    Output windowBasic usage of the [Output Window API].
    Tool windowCreating and populating a tool window.
    User promptDisplaying prompts to gather simple user input.
    DialogDisplaying a dialog with custom UI.
    Word count marginCreating an editor margin extension (e.g., for displaying word counts).
    Markdown linterA complex extension demonstrating multiple interacting components for a specific file type.
    Project QueryPerforming various project system queries.
    Comment removerConsuming Visual Studio SDK services via .NET dependency injection alongside VisualStudio.Extensibility APIs.
    RegexMatchDebugVisualizerUsing Remote UI to create a modal [Debugger Visualizer].
    MemoryStreamDebugVisualizerCreating a [Debugger Visualizer] that launches in a non-modal tool window.
  2. What is the VisualStudio.Extensibility model?

    main

    VisualStudio.Extensibility is a modern framework for developing Visual Studio extensions that run out-of-process from the IDE. This architecture provides several key benefits:

    • Increased reliability: Because extensions run in a separate process, a crash or hang in your extension will not cause the Visual Studio IDE to crash or become unresponsive.
    • Improved performance: The out-of-process model helps maintain IDE responsiveness.
    • Hot-loading: Extensions can be installed without requiring a restart of Visual Studio.
    • Modern API: Features a streamlined, asynchronous API designed for developer productivity.

    In-process extensions: If you need functionality that is not yet available in the VisualStudio.Extensibility SDK, you can develop an in-process extension by leveraging VisualStudio.Extensibility APIs while relying on the traditional VSSDK to cover feature gaps.

  3. Implement a Tagger

    main

    A tagger is responsible for reacting to requests for tags (RequestTagsAsync) and document changes (TextViewChangedAsync). When triggered, the tagger calculates the relevant ranges and returns the tags using UpdateTagsAsync.

    For high-performance taggers, it is recommended to work on small subsets of the document (e.g., only modified lines) to avoid complex synchronization logic and ensure quick updates.

  4. Use `TransferData` for large object collections

    main

    When dealing with potentially large objects (like a MatchCollection), implement TransferData in your VisualizerObjectSource. This allows the client to request specific pieces of data via an index, preventing timeout exceptions that occur if GetData attempts to transfer the entire collection at once.

    In the TransferData implementation:

    • Deserialize the index from incomingData.
    • Retrieve the specific item from the target object.
    • Serialize the result to outgoingData.
    • Return null (via serialization) when the index is out of bounds to signal the end of the collection.
    public override void TransferData(object target, Stream incomingData, Stream outgoingData)
    {
        var index = (int)DeserializeFromJson(incomingData, typeof(int))!;
        if (target is MatchCollection matchCollection && index < matchCollection.Count)
        {
            var result = RegexMatchObjectSource.Convert(matchCollection[index]);
            result.Name = $"[{index}]";
            SerializeAsJson(outgoingData, result);
        }
        else
        {
            SerializeAsJson(outgoingData, null);
        }
    }
  5. Monitor settings changes using Observers

    main

    When GenerateObserverClass is set to true on a SettingCategory, Visual Studio generates an observer class (e.g., [CategoryName]Observer) under the Settings child namespace of your extension.

    To use the observer:

    1. Register: Call serviceCollection.AddSettingsObservers(); in your extension setup.
    2. Inject: Request the generated observer in your class constructor (e.g., a Tool Window).
    3. Listen: Subscribe to the Changed event. The event provides a snapshot of the current settings.

    Note: The Changed event is invoked at least once with the current values upon subscription, so you do not need separate logic for the initial read.

    // In your tool window constructor
    public MyToolWindowData(VisualStudioExtensibility extensibility, SettingsSampleCategoryObserver settingsObserver)
    {
        this.settingsObserver = Requires.NotNull(settingsObserver);
        settingsObserver.Changed += this.SettingsObserver_ChangedAsync;
    }
    
    // Event handler for changes
    private Task SettingsObserver_ChangedAsync(Settings.SettingsSampleCategorySnapshot settingsSnapshot)
    {
        // Access values via the snapshot
        this.ManualUpdate = !settingsSnapshot.AutoUpdateSetting.ValueOrDefault(defaultValue: true);
        return Task.CompletedTask;
    }
  6. Configure Output Window Channel display names via Resource Files

    main

    The VisualStudio.Extensibility Output Window API requires that the display name for an Output Window Channel be stored in a Resource File (such as a .resx file).

    To ensure GetChannelAsync() can resolve the display name, you must associate the resource file with your Extension instance by overriding the ResourceManager property in your extension class.

  7. Implement a custom extension class with local services

    main

    To implement an extension that requires both commands and shared local services, inherit from ExtensionWithCommand.

    Key implementation details include:

    • ResourceManager: Use this property to point to a resource dictionary containing localized entries (e.g., for output window panes).
    • InitializeServices: Use this method to add local services to the dependency injection graph. For example, a scoped service like MarkdownDiagnosticsService can be registered here to allow injection of the VisualStudioExtensibility object.
  8. How to use Experimental APIs

    main

    VisualStudio.Extensibility uses the [Experimental] attribute on certain types and members to signal that they are likely to be modified or removed in future versions. This is common for new feature areas, partially completed features, or APIs being refined based on user feedback.

    By default, using an experimental API will trigger a build error to prevent unintentional usage of unstable code. These errors follow the pattern VSEXTPREVIEW_<FEATURE_NAME> (e.g., VSEXTPREVIEW_OUTPUTWINDOW).

  9. How to query by name using OutputGroupsByName

    main

    If a metadata collection supports it, you can use ByName methods to filter for specific items. When using OutputGroupsByName, the Project System Query API will include valid output groups requested and automatically skip any invalid names provided in the arguments.

    var result = await this.Extensibility.Workspaces().QueryProjectsAsync(
    	project => project.With(p => p.Name)
    		.With(p => p.ActiveConfigurations
    		.With(c => c.Name)
    		.With(c => c.OutputGroupsByName("Built", "XmlSerializer", "SourceFiles", "RandomNameShouldntBePickedUp")
    		.With(g => g.Name))),
    	cancellationToken);
  10. Consume Visual Studio SDK services via Dependency Injection

    main

    In-proc extensions can consume traditional Visual Studio SDK services (like DTE or IVsTextManager) using .NET dependency injection.

    To inject these services into a command, add them to the command's constructor using the following wrappers:

    • AsyncServiceProviderInjection<TService, TLegacyService>: For services available via the AsyncServiceProvider (e.g., DTE).
    • MefInjection<TService>: For services available via MEF (e.g., IBufferTagAggregatorFactoryService).

    Note: While VisualStudio.Extensibility APIs are async, some Visual Studio SDK services are restricted to the UI thread. Use await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); to switch to the UI thread when interacting with them.

    public RemoveAllComments(
        TraceSource traceSource,
        AsyncServiceProviderInjection<DTE, DTE2> dte,
        MefInjection<IBufferTagAggregatorFactoryService> bufferTagAggregatorFactoryService,
        MefInjection<IVsEditorAdaptersFactoryService> editorAdaptersFactoryService,
        AsyncServiceProviderInjection<SVsTextManager, IVsTextManager> textManager)
        : base(traceSource, dte, bufferTagAggregatorFactoryService, editorAdaptersFactoryService, textManager)
    {
    }
  11. Perform nested queries with AsQueryable

    main

    When a query result contains complex objects (like OutputGroups) and you need to retrieve additional metadata for those objects that weren't part of the initial query, use the .AsQueryable() method. This allows you to initiate a new, asynchronous query on the existing objects.

    // Assuming 'group' is an object from a previous query result
    var newResult = await group.AsQueryable()
    	.With(g => g.Name)
    	.ExecuteQueryAsync();