/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/ProtocolUnitTests/UriTests.cs
462 строки
27 KB
David Barbet
Refactor LSP server logging to remove singleton lsp instance (#84001)
16 июн 2026, 05:04
Не верифицирован
16 июн 2026, 05:04
82e81e5
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Composition; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.UnitTests.MiscellaneousFiles; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; public sealed class UriTests : AbstractLanguageServerProtocolTests { public UriTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } protected override TestComposition Composition => base.Composition.AddParts( typeof(CustomResolveHandler), typeof(LanguageSpecificHandler), typeof(TestLspMiscellaneousFilesWorkspaceProviderFactory)); [ConditionalTheory(typeof(WindowsOnly), Reason = "Uses Windows paths and Unicode encoding differs across platforms")] [CombinatorialData] [WorkItem("https://github.com/dotnet/runtime/issues/89538")] public async Task TestMiscDocument_WithFileScheme(bool mutatingLspWorkspace) { var filePath = "C:\\\ud86d\udeac\ue25b.txt"; // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file with a file URI. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); await testLspServer.OpenDocumentAsync(looseFileUri, """ class A { void M() { } } """, languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); Assert.NotNull(document); Assert.True(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(looseFileUri, document.GetURI()); Assert.Equal(filePath, document.FilePath); } [Theory, CombinatorialData] public async Task TestMiscDocument_WithOtherScheme(bool mutatingLspWorkspace) { // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file that hasn't been saved with a name. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); await testLspServer.OpenDocumentAsync(looseFileUri, """ class A { void M() { } } """, languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); Assert.NotNull(document); Assert.True(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(looseFileUri, document.GetURI()); Assert.Equal(looseFileUri.UriString, document.FilePath); } [Theory, CombinatorialData] public async Task TestWorkspaceDocument_WithFileScheme(bool mutatingLspWorkspace) { var documentFilePath = TestHelpers.GetRootedPath("A.cs"); var projectFilePath = TestHelpers.GetRootedPath("CSProj1.csproj"); var markup = $$""" <Workspace> <Project Language="C#" Name="CSProj1" CommonReferences="true" FilePath="{{projectFilePath}}"> <Document FilePath="{{documentFilePath}}"> public class A { } </Document> </Project> </Workspace> """; await using var testLspServer = await CreateXmlTestLspServerAsync(markup, mutatingLspWorkspace, initializationOptions: new() { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var workspaceDocument = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single(); var expectedDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath); await testLspServer.OpenDocumentAsync(expectedDocumentUri).ConfigureAwait(false); // Verify file is not added to the misc file workspace. { var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = expectedDocumentUri }, CancellationToken.None); Assert.NotNull(document); Assert.False(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(expectedDocumentUri, document.GetURI()); Assert.Equal(documentFilePath, document.FilePath); } // Try again, this time with a uri with different case sensitivity. This is supported, and is needed by Xaml. { var lowercaseUri = ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath.ToLowerInvariant()); Assert.NotEqual(expectedDocumentUri.GetRequiredParsedUri().AbsolutePath, lowercaseUri.GetRequiredParsedUri().AbsolutePath); var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = lowercaseUri }, CancellationToken.None); Assert.NotNull(document); Assert.False(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(expectedDocumentUri, document.GetURI()); Assert.Equal(documentFilePath, document.FilePath); } } [Theory, CombinatorialData] public async Task TestWorkspaceDocument_WithFileAndGitScheme(bool mutatingLspWorkspace) { // Start with an empty workspace. await using var testLspServer = await CreateTestLspServerAsync( "Initial Disk Contents", mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var fileDocumentUri = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single().GetURI(); var fileDocumentText = "FileText"; await testLspServer.OpenDocumentAsync(fileDocumentUri, fileDocumentText); // Add a git version of this document. Instead of "file://FILEPATH" the uri is "git://FILEPATH" var gitDocumentUri = new DocumentUri(fileDocumentUri.ToString().Replace("file", "git")); var gitDocumentText = "GitText"; await testLspServer.OpenDocumentAsync(gitDocumentUri, gitDocumentText); // Verify file is added to the workspace and the text matches the file document var (workspace, _, fileDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = fileDocumentUri }, CancellationToken.None); Assert.NotNull(fileDocument); var fileTextResult = await fileDocument.GetTextAsync(); Assert.Equal(fileDocumentUri, fileDocument.GetURI()); Assert.Equal(fileDocumentText, fileTextResult.ToString()); // Verify file is added to the workspace and the text matches the git document var (gitWorkspace, _, gitDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = gitDocumentUri }, CancellationToken.None); Assert.NotNull(gitDocument); var gitText = await gitDocument.GetTextAsync(); Assert.Equal(gitDocumentUri, gitDocument.GetURI()); Assert.Equal(gitDocumentText, gitText.ToString()); } [Theory, CombinatorialData] public async Task TestFindsExistingDocumentWhenUriHasDifferentEncodingAsync(bool mutatingLspWorkspace) { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Execute the request as JSON directly to avoid the test client serializing System.Uri using the encoded Uri to send to the server. var requestJson = """ { "textDocument": { "uri": "git:/c:/Users/dabarbet/source/repos/ConsoleApp10/ConsoleApp10/Program.cs?{{\"path\":\"c:\\\\Users\\\\dabarbet\\\\source\\\\repos\\\\ConsoleApp10\\\\ConsoleApp10\\\\Program.cs\",\"ref\":\"~\"}}", "languageId": "csharp", "text": "LSP text" } } """; var jsonDocument = JsonDocument.Parse(requestJson); await testLspServer.ExecutePreSerializedRequestAsync(LSP.Methods.TextDocumentDidOpenName, jsonDocument); // Retrieve the URI from the json - this is the unencoded (and not JSON escaped) version of the URI. var unencodedUri = JsonSerializer.Deserialize<LSP.DidOpenTextDocumentParams>(jsonDocument, JsonSerializerOptions)!.TextDocument.DocumentUri; // Access the document using the unencoded URI to make sure we find it in the C# misc files. var (workspace, _, lspDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = unencodedUri }, CancellationToken.None).ConfigureAwait(false); Assert.NotNull(lspDocument); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind); Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language); var originalText = await lspDocument.GetTextAsync(CancellationToken.None); Assert.Equal("LSP text", originalText.ToString()); // Now make a request using the encoded document to ensure the server is able to find the document in misc C# files. var encodedUriString = @"git:/c:/Users/dabarbet/source/repos/ConsoleApp10/ConsoleApp10/Program.cs?%7B%7B%22path%22:%22c:%5C%5CUsers%5C%5Cdabarbet%5C%5Csource%5C%5Crepos%5C%5CConsoleApp10%5C%5CConsoleApp10%5C%5CProgram.cs%22,%22ref%22:%22~%22%7D%7D"; var encodedUri = new DocumentUri(encodedUriString); var info = await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = encodedUri }), CancellationToken.None); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind); Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language); var (encodedWorkspace, _, encodedDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = encodedUri }, CancellationToken.None).ConfigureAwait(false); Assert.Same(workspace, encodedWorkspace); Assert.NotNull(encodedDocument); Assert.Equal(LanguageNames.CSharp, encodedDocument.Project.Language); var encodedText = await encodedDocument.GetTextAsync(CancellationToken.None); Assert.Equal("LSP text", encodedText.ToString()); // The text we get back should be the exact same instance that was originally saved by the unencoded request. Assert.Same(originalText, encodedText); } [Theory, CombinatorialData, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2208409")] public async Task TestFindsExistingDocumentWhenUriHasDifferentCasingForCaseInsensitiveUriAsync(bool mutatingLspWorkspace) { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var upperCaseUri = new DocumentUri(@"file:///C:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs"); var lowerCaseUri = new DocumentUri(@"file:///c:/Users/dabarbet/source/repos/XUnitApp1/UnitTest1.cs"); // Execute the request as JSON directly to avoid the test client serializing System.Uri. var requestJson = $$$""" { "textDocument": { "uri": "{{{upperCaseUri.UriString}}}", "languageId": "csharp", "text": "LSP text" } } """; var jsonDocument = JsonDocument.Parse(requestJson); await testLspServer.ExecutePreSerializedRequestAsync(LSP.Methods.TextDocumentDidOpenName, jsonDocument); // Access the document using the upper case to make sure we find it in the C# misc files. var (workspace, _, lspDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = upperCaseUri }, CancellationToken.None).ConfigureAwait(false); Assert.NotNull(lspDocument); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind); Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language); var originalText = await lspDocument.GetTextAsync(CancellationToken.None); Assert.Equal("LSP text", originalText.ToString()); // Now make a request using different case. var info = await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = lowerCaseUri }), CancellationToken.None); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind); Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language); var (lowerCaseWorkspace, _, lowerCaseDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = lowerCaseUri }, CancellationToken.None).ConfigureAwait(false); Assert.Same(workspace, lowerCaseWorkspace); Assert.NotNull(lowerCaseDocument); Assert.Equal(LanguageNames.CSharp, lowerCaseDocument.Project.Language); var lowerCaseText = await lowerCaseDocument.GetTextAsync(CancellationToken.None); Assert.Equal("LSP text", lowerCaseText.ToString()); // The text we get back should be the exact same instance that was originally saved by the unencoded request. Assert.Same(originalText, lowerCaseText); } [Theory, CombinatorialData, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2208409")] public async Task TestUsesDifferentDocumentForDifferentCaseWithNonUncUriAsync(bool mutatingLspWorkspace) { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var upperCaseUri = new DocumentUri(@"git:/Blah"); var lowerCaseUri = new DocumentUri(@"git:/blah"); // Execute the request as JSON directly to avoid the test client serializing System.Uri. var requestJson = $$$""" { "textDocument": { "uri": "{{{upperCaseUri.UriString}}}", "languageId": "csharp", "text": "LSP text" } } """; var jsonDocument = JsonDocument.Parse(requestJson); await testLspServer.ExecutePreSerializedRequestAsync(LSP.Methods.TextDocumentDidOpenName, jsonDocument); // Access the document using the upper case to make sure we find it in the C# misc files. var (workspace, _, lspDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = upperCaseUri }, CancellationToken.None).ConfigureAwait(false); Assert.NotNull(lspDocument); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace?.Kind); Assert.Equal(LanguageNames.CSharp, lspDocument.Project.Language); var originalText = await lspDocument.GetTextAsync(CancellationToken.None); Assert.Equal("LSP text", originalText.ToString()); // Now make a request using different case. This should throw since we have not opened a document with the URI with different case (and not UNC). await Assert.ThrowsAnyAsync<Exception>(async () => await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = lowerCaseUri }), CancellationToken.None)); } [Theory, CombinatorialData] public async Task TestDoesNotCrashIfUnableToDetermineLanguageInfo(bool mutatingLspWorkspace) { // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open an empty loose file that hasn't been saved with a name. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); await testLspServer.OpenDocumentAsync(looseFileUri, "hello", languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); Assert.NotNull(document); Assert.True(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(looseFileUri, document.GetURI()); Assert.Equal(looseFileUri.UriString, document.FilePath); // Close the document (deleting the saved language information) await testLspServer.CloseDocumentAsync(looseFileUri); // Assert that the request throws but the server does not crash. await Assert.ThrowsAnyAsync<Exception>(async () => await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }), CancellationToken.None)); Assert.False(testLspServer.GetServerAccessor().HasShutdownStarted()); Assert.False(testLspServer.GetQueueAccessor()!.Value.IsComplete()); } [Theory] // Invalid URIs [InlineData(true, "file://invalid^uri")] [InlineData(false, "file://invalid^uri")] [InlineData(true, "perforce://%239/some/file/here/source.cs")] [InlineData(false, "perforce://%239/some/file/here/source.cs")] // Valid URI, but System.Uri cannot parse it. [InlineData(true, "vscode-notebook-cell://dev-container+7b2/workspaces/devkit-crash/notebook.ipynb")] [InlineData(false, "vscode-notebook-cell://dev-container+7b2/workspaces/devkit-crash/notebook.ipynb")] // Valid URI, but System.Uri cannot parse it. [InlineData(true, "perforce://@=1454483/some/file/here/source.cs")] [InlineData(false, "perforce://@=1454483/some/file/here/source.cs")] public async Task TestOpenDocumentWithInvalidUri(bool mutatingLspWorkspace, string uriString) { // Create a server that supports LSP misc files await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open file with a URI System.Uri cannot parse. This should not crash the server. var invalidUri = new DocumentUri(uriString); // ParsedUri should be null as System.Uri cannot parse it. Assert.Null(invalidUri.ParsedUri); await testLspServer.OpenDocumentAsync(invalidUri, string.Empty, languageId: "csharp").ConfigureAwait(false); // Verify requests succeed and that the file is in misc. var info = await testLspServer.ExecuteRequestAsync<CustomResolveParams, ResolvedDocumentInfo>(CustomResolveHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = invalidUri }), CancellationToken.None); Assert.Equal(WorkspaceKind.MiscellaneousFiles, info!.WorkspaceKind); Assert.Equal(LanguageNames.CSharp, info.ProjectLanguage); // Verify we can modify the document in misc. await testLspServer.InsertTextAsync(invalidUri, (0, 0, "hello")); var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = invalidUri }, CancellationToken.None); Assert.Equal("hello", (await document!.GetTextAsync()).ToString()); } [Theory, CombinatorialData] public async Task TestUnparseableUri_FindsLanguageSpecificHandler_WhenDocumentIsOpen(bool mutatingLspWorkspace) { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Use a URI that System.Uri cannot parse. var unparseableUri = new DocumentUri("_claude_vscode_fs_right:/c:/Projects/MyApp/Pages/Component.razor"); Assert.Null(unparseableUri.ParsedUri); // Open the document with the unparseable URI and "razor" language ID. // The language ID should be saved and used to route subsequent requests to the Razor-specific handler. await testLspServer.OpenDocumentAsync(unparseableUri, "<div>hello</div>", languageId: "razor"); // Send a request to the language-specific handler - should route to the Razor handler successfully. var response = await testLspServer.ExecuteRequestAsync<CustomResolveParams, LanguageSpecificResponse>( LanguageSpecificHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = unparseableUri }), CancellationToken.None); Assert.NotNull(response); Assert.True(response.HandlerCalled); } [Theory, CombinatorialData] public async Task TestUnparseableUri_WithNoDefaultHandler_DoesNotCrashServer_WhenDocumentIsClosed(bool mutatingLspWorkspace) { await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Use a URI that System.Uri cannot parse. var unparseableUri = new DocumentUri("_claude_vscode_fs_right:/c:/Projects/MyApp/Pages/Component.razor"); Assert.Null(unparseableUri.ParsedUri); // Send a request to a handler that is ONLY registered for "Razor" language (no default handler). // Language lookup fails (document closed, URI unparseable) so there is no language to route to. // The server should gracefully fail the request, not crash. await Assert.ThrowsAnyAsync<Exception>(async () => await testLspServer.ExecuteRequestAsync<CustomResolveParams, LanguageSpecificResponse>( LanguageSpecificHandler.MethodName, new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = unparseableUri }), CancellationToken.None)); Assert.False(testLspServer.GetServerAccessor().HasShutdownStarted()); Assert.False(testLspServer.GetQueueAccessor()!.Value.IsComplete()); } [Theory] [InlineData(true, null, null)] [InlineData(false, "file://c:\\valid", null)] [InlineData(false, null, "file://c:\\valid")] [InlineData(true, "file://c:\\valid", "file://c:\\valid")] [InlineData(true, "file://c:\\valid", "file:///c:/valid")] [InlineData(true, "file://c:\\valid", "file://c:\\VALID")] [InlineData(false, "file://c:\\valid", "file://c:\\valid2")] public void TestUriEquality(bool areEqual, string? uriString1, string? uriString2) { var documentUri1 = uriString1 != null ? new DocumentUri(uriString1) : null; var documentUri2 = uriString2 != null ? new DocumentUri(uriString2) : null; Assert.True(areEqual == (documentUri1 == documentUri2)); Assert.True(areEqual != (documentUri1 != documentUri2)); } private sealed record class ResolvedDocumentInfo(string WorkspaceKind, string ProjectLanguage); private sealed record class CustomResolveParams([property: JsonPropertyName("textDocument")] LSP.TextDocumentIdentifier TextDocument); private sealed record class LanguageSpecificResponse(bool HandlerCalled); [ExportCSharpVisualBasicStatelessLspService(typeof(CustomResolveHandler)), PartNotDiscoverable, Shared] [LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] private sealed class CustomResolveHandler() : ILspServiceDocumentRequestHandler<CustomResolveParams, ResolvedDocumentInfo> { public const string MethodName = nameof(CustomResolveHandler); public bool MutatesSolutionState => false; public bool RequiresLSPSolution => true; public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(CustomResolveParams request) => request.TextDocument; public async Task<ResolvedDocumentInfo> HandleRequestAsync(CustomResolveParams request, RequestContext context, CancellationToken cancellationToken) { return new ResolvedDocumentInfo(context.Workspace!.Kind!, context.GetRequiredDocument().Project.Language); } } /// <summary> /// Test handler that is only registered for the "Razor" language, with no default fallback. /// This simulates handlers like textDocument/documentColor that are dynamically registered by Razor. /// </summary> [ExportCSharpVisualBasicStatelessLspService(typeof(LanguageSpecificHandler)), PartNotDiscoverable, Shared] [LanguageServerEndpoint(MethodName, "Razor")] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] private sealed class LanguageSpecificHandler() : ILspServiceRequestHandler<CustomResolveParams, LanguageSpecificResponse> { public const string MethodName = nameof(LanguageSpecificHandler); public bool MutatesSolutionState => false; public bool RequiresLSPSolution => false; public Task<LanguageSpecificResponse> HandleRequestAsync(CustomResolveParams request, RequestContext context, CancellationToken cancellationToken) { return Task.FromResult(new LanguageSpecificResponse(true)); } } }