Skip to content

Commit

Permalink
Adds LSP sample using Rust analyzer (#281)
Browse files Browse the repository at this point in the history
* Added Rust LSP server example
  • Loading branch information
javierdlg authored Nov 1, 2023
1 parent f5b5c36 commit c3b9e14
Show file tree
Hide file tree
Showing 6 changed files with 441 additions and 201 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"RustLspExtension.RustLanguageServerProvider.DisplayName": "Rust Analyzer LSP server"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: Rust Language Server Provider Sample Reference
description: Language server provider that runs the rust analyzer when a matching document is opened.
date: 2023-10-31
---

# Walkthrough: Rust Language Server Provider Sample

This sample creates a Rust Language Server Provider extension that serves intellisense and tooltips when a rust file is opened in Visual Studio.

## Prerequisites
This sample requires installing [Rust](https://www.rust-lang.org/tools/install) and copying [rust-analyzer.exe](https://github.com/rust-lang/rust-analyzer) found under releases named "rust-analyzer-x86_64-pc-windows-msvc.zip" into the project folder.

## Language Server Provider definition

The extension contains a code file that defines a language server provider and its properties starting with the `VisualStudioContribution` class attribute which makes the server available to Visual Studio:

```csharp
[VisualStudioContribution]
internal class RustLanguageServerProvider : LanguageServerProvider
{
```

The `LanguageServerProviderConfiguration` property defines information about the server that is available to Visual Studio even before the extension is loaded:

```csharp
public override LanguageServerProviderConfiguration LanguageServerProviderConfiguration => new(
"%RustLspExtension.RustLanguageServerProvider.DisplayName%",
new[]
{
DocumentFilter.FromDocumentType(RustDocumentType)
});
```

This configuration object allows setting the display name for the server and specifying one or more document filters. You can also specify a localized string as a display name from [string-resoures.json](./.vsextension/string-resources.json):

```json
{
"RustLspExtension.RustLanguageServerProvider.DisplayName": "Rust Analyzer LSP server"
}
```

## Activating the Language Server

Once a document that has a matching document type is opened, Visual Studio calls into `CreateServerConnectionAsync` and requests an `IDuplexPipe` that will be used to communicate with the language server.

In our sample, the rust-analyzer executable is launched and a duplex pipe is used to communicate with the process.

```csharp
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);
}
```

## Disabling the server

`LanguageServerProvider` contains a public Boolean property `Enabled`, which is set to `true` by default. This property defines if Visual Studio should activate your server when a matching document type is opened. When `Enabled` is set to `False`, all currently running servers will be stopped and no new instances will be activated until re-enabled.

## Capturing and handling activation failures

After `CreateServerConnectionAsync` completes, Visual Studio will attempt to initialize the server via the provided duplex pipe following standard LSP protocol. Once this step is done, `OnServerInitializationResultAsync` is called where `ServerInitializationResult` denotes if the server succeeded or failed to initialize, and if it failed `LanguageServerInitializationFailureInfo` will contain the exception and message provided by the language server.

```csharp
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);
}
```

## Usage

Once deployed, the language server will automatically start once an applicable document type is opened.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.Extensibility;

/// <summary>
/// Extension entry point for the VisualStudio.Extensibility extension.
/// </summary>
[VisualStudioContribution]
internal class RustExtension : Extension
{
/// <inheritdoc/>
public override ExtensionConfiguration ExtensionConfiguration => new()
{
Metadata = new(
id: "RustLspExtension.003741dc-9931-47c3-ad95-8804705cfbb9",
version: this.ExtensionAssemblyVersion,
publisherName: "Microsoft",
displayName: "RustLspExtension",
description: "Rust Language Server Extension"),
};

/// <inheritdoc />
protected override void InitializeServices(IServiceCollection serviceCollection)
{
base.InitializeServices(serviceCollection);

// You can configure dependency injection here by adding services to the serviceCollection.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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.Pipelines;
using System.Reflection;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Editor;
using Microsoft.VisualStudio.Extensibility.LanguageServer;
using Microsoft.VisualStudio.RpcContracts.LanguageServerProvider;
using Nerdbank.Streams;

/// <inheritdoc/>
[VisualStudioContribution]
internal class RustLanguageServerProvider : LanguageServerProvider
{
/// <summary>
/// Gets the document type for rust code files.
/// </summary>
[VisualStudioContribution]
public static DocumentTypeConfiguration RustDocumentType => new("rust")
{
FileExtensions = new[] { ".rs", ".rust" },
BaseDocumentType = LanguageServerBaseDocumentType,
};

/// <inheritdoc/>
public override LanguageServerProviderConfiguration LanguageServerProviderConfiguration => new(
"%RustLspExtension.RustLanguageServerProvider.DisplayName%",
new[]
{
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;

#pragma warning disable CA2000 // The process is disposed after Visual Studio sends the stop command.
Process process = new Process();
#pragma warning restore CA2000 // Dispose objects before losing scope.
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>
<NoWarn>$(NoWarn);VSEXTAPI0001;</NoWarn>

<!-- The VisualStudio.Extensibility preview packages are available from the azure-public/vside/msft_consumption feed -->
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/azure-public/vside/_packaging/msft_consumption/nuget/v3/index.json;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup Condition="Exists('.\rust-analyzer.exe')">
<Content Include="rust-analyzer.exe" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Sdk" Version="17.9.42-preview-1" />
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Build" Version="17.9.42-preview-1" />
</ItemGroup>
</Project>
Loading

0 comments on commit c3b9e14

Please sign in to comment.