Edit

Language Server Provider

Add IntelliSense, diagnostics, formatting, and other language features to Visual Studio by implementing a Language Server Protocol (LSP) provider with VisualStudio.Extensibility.

Note

This guide uses the VisualStudio.Extensibility SDK. See Choose the right extensibility model for model comparison.

Full example

The complete implementation:

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace RustLanguageServerProviderExtension;

using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Editor;
using Microsoft.VisualStudio.Extensibility.LanguageServer;
using Microsoft.VisualStudio.RpcContracts.LanguageServerProvider;
using Nerdbank.Streams;

/// <inheritdoc/>
#pragma warning disable VSEXTPREVIEW_LSP // Type is for evaluation purposes only and is subject to change or removal in future updates.
[VisualStudioContribution]
internal class RustLanguageServerProvider : LanguageServerProvider
{
    /// <summary>
    /// Gets the document type for rust code files.
    /// </summary>
    [VisualStudioContribution]
    public static DocumentTypeConfiguration RustDocumentType => new("rust")
    {
        FileExtensions = [".rs", ".rust"],
        BaseDocumentType = LanguageServerBaseDocumentType,
    };

    /// <inheritdoc/>
    public override LanguageServerProviderConfiguration LanguageServerProviderConfiguration => new(
        "%RustLspExtension.RustLanguageServerProvider.DisplayName%",
        [DocumentFilter.FromDocumentType(RustDocumentType)]);

    /// <inheritdoc/>
    public override Task<IDuplexPipe?> CreateServerConnectionAsync(CancellationToken cancellationToken)
    {
        ProcessStartInfo info = new ProcessStartInfo();
        info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, @"rust-analyzer.exe");
        info.RedirectStandardInput = true;
        info.RedirectStandardOutput = true;
        info.UseShellExecute = false;
        info.CreateNoWindow = true;

        Process process = new Process();
        process.StartInfo = info;

        if (process.Start())
        {
            return Task.FromResult<IDuplexPipe?>(new DuplexPipe(
                PipeReader.Create(process.StandardOutput.BaseStream),
                PipeWriter.Create(process.StandardInput.BaseStream)));
        }

        return Task.FromResult<IDuplexPipe?>(null);
    }

    /// <inheritdoc/>
    public override Task OnServerInitializationResultAsync(ServerInitializationResult serverInitializationResult, LanguageServerInitializationFailureInfo? initializationFailureInfo, CancellationToken cancellationToken)
    {
        if (serverInitializationResult == ServerInitializationResult.Failed)
        {
            // Log telemetry for failure and disable the server from being activated again.
            this.Enabled = false;
        }

        return base.OnServerInitializationResultAsync(serverInitializationResult, initializationFailureInfo, cancellationToken);
    }
}
#pragma warning restore VSEXTPREVIEW_LSP // Type is for evaluation purposes only and is subject to change or removal in future updates.

Tip

See the full working sample: RustLanguageServerProvider ↗

Related concepts: Contributions and configurations — how [VisualStudioContribution] and document type configurations work

Understand the example

What you'll build

A LanguageServerProvider extension that connects an LSP-compliant language server to Visual Studio, enabling rich language support for custom file types.

Implementation

Step 1: Define custom document types

Register file extensions your language server handles:

[VisualStudioContribution]
internal static DocumentTypeConfiguration RustDocumentType => new("rust")
{
    FileExtensions = new[] { ".rs", ".rust" },
    BaseDocumentType = LanguageServerBaseDocumentType,
};

Step 2: Create the language server provider

Extend LanguageServerProvider and apply the [VisualStudioContribution] attribute:

[VisualStudioContribution]
public class RustLanguageServerProvider : LanguageServerProvider
{
    public RustLanguageServerProvider(
        ExtensionCore container,
        VisualStudioExtensibility extensibilityObject,
        TraceSource traceSource)
        : base(container, extensibilityObject)
    {
    }

    public override LanguageServerProviderConfiguration LanguageServerProviderConfiguration =>
        new("Rust Language Server",
            new[]
            {
                DocumentFilter.FromDocumentType(RustDocumentType),
            });
}

Step 3: Start the language server connection

Override CreateServerConnectionAsync to launch your server and return a duplex pipe:

public override Task<IDuplexPipe?> CreateServerConnectionAsync(CancellationToken cancellationToken)
{
    (Stream PipeToServer, Stream PipeToVS) = FullDuplexStream.CreatePair();

    // Launch or connect your language server process using PipeToServer

    return Task.FromResult<IDuplexPipe?>(
        new DuplexPipe(PipeToVS.UsePipeReader(), PipeToVS.UsePipeWriter()));
}

Step 4: Handle initialization results

Override OnServerInitializationResultAsync to respond to startup success or failure:

public override Task OnServerInitializationResultAsync(
    ServerInitializationResult startState,
    LanguageServerInitializationFailureInfo? initializationFailureInfo,
    CancellationToken cancellationToken)
{
    if (startState == ServerInitializationResult.Failed)
    {
        // Disable the server to prevent further activation attempts
        this.Enabled = false;
    }

    return Task.CompletedTask;
}

Step 5: Send initialization options (optional)

Pass custom data to the server with the LSP initialize message:

public RustLanguageServerProvider(
    ExtensionCore container,
    VisualStudioExtensibility extensibilityObject,
    TraceSource traceSource)
    : base(container, extensibilityObject)
{
    this.LanguageServerOptions.InitializationOptions =
        JToken.Parse(@"[{""server"":""initialize""}]");
}

Step 6: Publish diagnostics

Your language server sends textDocument/publishDiagnostics notifications to surface errors in the Visual Studio Error List:

{
  "method": "textDocument/publishDiagnostics",
  "params": {
    "uri": "file:///c:/project/main.rs",
    "diagnostics": [
      {
        "range": { "start": { "line": 2, "character": 0 }, "end": { "line": 2, "character": 10 } },
        "severity": 1,
        "code": "E0425",
        "source": "rustc",
        "message": "cannot find value `foo` in this scope"
      }
    ]
  }
}

Severity values: 1 = Error, 2 = Warning, 3 = Information, 4 = Hint.

Tips

✅ DO

  • Use LanguageServerBaseDocumentType as the base for custom document types.
  • Return a valid IDuplexPipe from CreateServerConnectionAsync — Visual Studio communicates with your server exclusively through this pipe.
  • Set this.Enabled = false in OnServerInitializationResultAsync when the server fails to start, to prevent repeated activation attempts.
  • Use DocumentFilter.FromDocumentType() to scope activation to specific file types.

❌ DON'T

  • Don't use LSP for languages already supported by Visual Studio (C#, VB, C++) — use Roslyn analyzers instead.
  • Don't block CreateServerConnectionAsync — launch your server process asynchronously and return the pipe.
  • Don't forget to declare capabilities in your server's InitializeResult — missing capabilities cause the client to disconnect.
  • Don't use ErrorListProvider (VSSDK) when you have an LSP server — diagnostics flow automatically through textDocument/publishDiagnostics.

🔧 Troubleshooting

Symptom Fix
Language server doesn't start Verify CreateServerConnectionAsync returns a valid connection. Check the Output window for errors.
Diagnostics don't appear in Error List Ensure your server sends textDocument/publishDiagnostics. Verify file URIs use the file:///c:/... format.
Server crashes on startup Check that all required capabilities are declared in InitializeResult.
Custom file types not recognized Confirm DocumentTypeConfiguration file extensions match (include the leading .).
Server keeps restarting Check that OnServerInitializationResultAsync sets this.Enabled = false on failure.

Next steps