using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using OutputWindow = IdesLspPoc.Output.OutputWindow; using Process = System.Diagnostics.Process; namespace IdesLspPoc.LspClient { // NOTE : We use the SDK and will need to split code/references across major VS versions // Design thoughts - one of these for each distinct LSP we manage. We could have multiple ContentType declarations. public abstract class ToolkitLspClient : ILanguageClient { [Import] protected OutputWindow _outputWindow; public event AsyncEventHandler StartAsync; public event AsyncEventHandler StopAsync; /// /// Name of Language Client; displayed to user /// For example, if the LSP writes logs to an output window, this is where they will appear /// public abstract string Name { get; } /// /// Used if we set up a JSON that drives some behavior through settings /// https://learn.microsoft.com/en-us/visualstudio/extensibility/adding-an-lsp-extension?view=vs-2022#settings /// public IEnumerable ConfigurationSections { get { // see IdesLspPoc\LspClientSettings.json for configuration values (like enabling tracing) // see lsp.pkgdef for bundling the config into the extension yield return "ideslsp"; } } /// /// Payload sent to LSP server in the "initialize" message /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize /// /// public virtual object InitializationOptions { get; } = null; public IEnumerable FilesToWatch { get; } = new List() { // "**/*.buildspec", // "**/*buildspec*.yml", // "**/*buildspec*.yaml", // "**/buildspec/**/*.yml", // "**/buildspec/**/*.yaml", // "build.yml", // "**/*build.yml", // "**/*build.yaml", }; public bool ShowNotificationOnInitializeFailed { get; } = true; // Just exploring how to query for VS what file types it knows about // [Import] // internal IContentTypeRegistryService ContentTypeRegistryService { get; set; } /// /// VS Calls this when the extension has loaded /// /// public virtual async Task OnLoadedAsync() { // Just exploring how to query for VS what file types it knows about // var x = ContentTypeRegistryService.ContentTypes.ToList(); // var ttt = x.Select(q => q.DisplayName).ToList(); // Design thoughts - we might start downloading the LSP in the background here. // We would conclude then call StartAsync when the download completes, in order to trigger ActivateAsync. await _outputWindow.InitializeAsync(CancellationToken.None); await StartAsync.InvokeAsync(this, EventArgs.Empty); } /// /// VS Calls this to start up the Language Server and get the communications streams /// /// public async Task ActivateAsync(CancellationToken token) { await Task.Yield(); _outputWindow.WriteLine("Launching Language Server"); await TaskScheduler.Default; var lspProcess = CreateLspProcess(); if (!lspProcess.Start()) { // null indicates the server cannot be started return null; } await OnBeforeLspConnectionStartsAsync(lspProcess); return new Connection(lspProcess.StandardOutput.BaseStream, lspProcess.StandardInput.BaseStream); } /// /// Used by implementing classes that need to interact with the language server process prior to /// starting up the LSP protocol. /// /// Language server process protected virtual Task OnBeforeLspConnectionStartsAsync(Process lspProcess) { return Task.CompletedTask; } /// /// VS calls this after successfully making initialization calls with the language server /// /// public Task OnServerInitializedAsync() { _outputWindow.WriteLine("Language Server is initialized"); return Task.CompletedTask; } /// /// VS calls this if it was not successful in making initialization calls with the language server /// /// public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState) { // Design notes - perform any cleanup work (if any) here _outputWindow.WriteLine("Language Server failed: "); _outputWindow.WriteLine($"- Status Message: {initializationState.StatusMessage}"); _outputWindow.WriteLine($"- Exception: {initializationState.InitializationException?.Message}"); _outputWindow.WriteLine($"- Initialized: {initializationState.IsInitialized}"); _outputWindow.WriteLine($"- Status: {initializationState.Status}"); var failureInfo = new InitializationFailureContext() { FailureMessage = initializationState.StatusMessage ?? $"Unknown initialization failure, exception: {initializationState.InitializationException?.Message}", }; return Task.FromResult(failureInfo); } private Process CreateLspProcess() { ProcessStartInfo info = new ProcessStartInfo { WorkingDirectory = GetServerWorkingDir(), FileName = GetServerPath(), Arguments = string.Join(" ", GetLspProcessArgs()), RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, }; Process process = new Process { StartInfo = info, }; return process; } protected virtual IEnumerable GetLspProcessArgs() { return new[] { "--stdio" }; } protected abstract string GetServerWorkingDir(); protected abstract string GetServerPath(); } }