Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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
LanguageServerBaseDocumentTypeas the base for custom document types. - Return a valid
IDuplexPipefromCreateServerConnectionAsync— Visual Studio communicates with your server exclusively through this pipe. - Set
this.Enabled = falseinOnServerInitializationResultAsyncwhen 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 throughtextDocument/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. |