diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs
index abe00f37ee641..b22dfda918ede 100644
--- a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs
+++ b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs
@@ -63,7 +63,7 @@ internal abstract partial class AbstractInProcLanguageClient(
/// Unused, implementing .
/// Gets the optional target object for receiving custom messages not covered by the language server protocol.
///
- public object? CustomMessageTarget => null;
+ public virtual object? CustomMessageTarget => null;
///
/// An enum representing this server instance.
@@ -256,5 +256,22 @@ public virtual AbstractLanguageServer Create(
/// This method is called after the language server has been activated, but connection has not been established.
///
public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask;
+
+ internal TestAccessor GetTestAccessor()
+ {
+ return new TestAccessor(this);
+ }
+
+ internal readonly struct TestAccessor
+ {
+ private readonly AbstractInProcLanguageClient _instance;
+
+ internal TestAccessor(AbstractInProcLanguageClient instance)
+ {
+ _instance = instance;
+ }
+
+ public AbstractLanguageServer? LanguageServer => _instance._languageServer;
+ }
}
}
diff --git a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
index 8c7d44ae3356d..7924e0bd7f73d 100644
--- a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
+++ b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
@@ -79,6 +79,7 @@
+
diff --git a/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj b/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj
index dc006033f6ca1..478f9de53cc99 100644
--- a/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj
+++ b/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj
@@ -92,6 +92,7 @@
+
diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs
index 7196583560a2c..408b4f1cee7f5 100644
--- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs
+++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs
@@ -353,6 +353,11 @@ internal TestAccessor(AbstractLanguageServer server)
return null;
}
+ internal Task ExecuteRequestAsync(string methodName, TRequest request, CancellationToken cancellationToken)
+ {
+ return _server._queue.Value.ExecuteAsync(request, methodName, _server._lspServices.Value, cancellationToken);
+ }
+
internal JsonRpc GetServerRpc() => _server._jsonRpc;
internal bool HasShutdownStarted()
diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj
index 8c0513a6b0158..3c304c0719c78 100644
--- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj
+++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.csproj
@@ -19,5 +19,6 @@
+
diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs
index 130f3014c414e..31e88ccc68d83 100644
--- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs
+++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs
@@ -76,6 +76,21 @@ public static ImmutableArray GetDocuments(this Solution solution, stri
return documents;
}
+ ///
+ /// Get all regular and additional s for the given .
+ ///
+ public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri)
+ {
+ var documentIds = GetDocumentIds(solution, documentUri);
+
+ var documents = documentIds
+ .Select(solution.GetDocument)
+ .Concat(documentIds.Select(solution.GetAdditionalDocument))
+ .WhereNotNull()
+ .ToImmutableArray();
+ return documents;
+ }
+
public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri)
=> solution.GetDocumentIdsWithFilePath(ProtocolConversions.GetDocumentFilePathFromUri(documentUri));
diff --git a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs
index 3fb63ca69e04d..1c1e201e3698d 100644
--- a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs
@@ -35,19 +35,20 @@ public GetTextDocumentWithContextHandler()
Contract.ThrowIfNull(context.Workspace);
Contract.ThrowIfNull(context.Solution);
- // We specifically don't use context.Document here because we want multiple
- var documents = context.Solution.GetDocuments(request.TextDocument.Uri);
+ // We specifically don't use context.Document here because we want multiple. We also don't need
+ // all of the document info, just the Id is enough
+ var documentIds = context.Solution.GetDocumentIds(request.TextDocument.Uri);
- if (!documents.Any())
+ if (!documentIds.Any())
{
return SpecializedTasks.Null();
}
var contexts = new List();
- foreach (var document in documents)
+ foreach (var documentId in documentIds)
{
- var project = document.Project;
+ var project = context.Solution.GetRequiredProject(documentId.ProjectId);
var projectContext = ProtocolConversions.ProjectToProjectContext(project);
contexts.Add(projectContext);
}
@@ -57,13 +58,13 @@ public GetTextDocumentWithContextHandler()
// GetDocumentIdInCurrentContext will just return the same ID back, which means we're going to pick the first
// ID in GetDocumentIdsWithFilePath, but there's really nothing we can do since we don't have contexts for
// close documents anyways.
- var openDocument = documents.First();
- var currentContextDocumentId = context.Workspace.GetDocumentIdInCurrentContext(openDocument.Id);
+ var openDocumentId = documentIds.First();
+ var currentContextDocumentId = context.Workspace.GetDocumentIdInCurrentContext(openDocumentId);
return Task.FromResult(new VSProjectContextList
{
ProjectContexts = contexts.ToArray(),
- DefaultIndex = documents.IndexOf(d => d.Id == currentContextDocumentId)
+ DefaultIndex = documentIds.IndexOf(d => d == currentContextDocumentId)
});
}
}
diff --git a/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs b/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs
index e1a1678d30a49..f85b0291b3bb8 100644
--- a/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs
+++ b/src/Features/LanguageServer/Protocol/Handler/RequestContext.cs
@@ -53,7 +53,7 @@ internal readonly struct RequestContext
///
/// This field is only initialized for handlers that request solution context.
///
- private readonly StrongBox<(Workspace Workspace, Solution Solution, Document? Document)>? _lspSolution;
+ private readonly StrongBox<(Workspace Workspace, Solution Solution, TextDocument? Document)>? _lspSolution;
///
/// The workspace this request is for, if applicable. This will be present if is
@@ -116,6 +116,43 @@ public Document? Document
throw new InvalidOperationException();
}
+ if (_lspSolution.Value.Document is null)
+ {
+ return null;
+ }
+
+ if (_lspSolution.Value.Document is Document document)
+ {
+ return document;
+ }
+
+ // Explicitly throw for attempts to get a Document when only a TextDocument is available.
+ throw new InvalidOperationException($"Attempted to retrieve a Document but a TextDocument was found instead.");
+ }
+ }
+
+ ///
+ /// The text document that the request is for, if applicable. This comes from the returned from the handler itself via a call to
+ /// .
+ ///
+ public TextDocument? TextDocument
+ {
+ get
+ {
+ if (_lspSolution is null)
+ {
+ // This request context never had a solution instance
+ return null;
+ }
+
+ // The solution is available unless it has been cleared by a call to ClearSolutionContext. Explicitly throw
+ // for attempts to access this property after it has been manually cleared. Note that we can't rely on
+ // Document being null for this check, because it is not always provided as part of the solution context.
+ if (_lspSolution.Value.Workspace is null)
+ {
+ throw new InvalidOperationException();
+ }
+
return _lspSolution.Value.Document;
}
}
@@ -149,7 +186,7 @@ public RequestContext(
string method,
ClientCapabilities? clientCapabilities,
WellKnownLspServerKinds serverKind,
- Document? document,
+ TextDocument? document,
IDocumentChangeTracker documentChangeTracker,
ImmutableDictionary trackedDocuments,
ImmutableArray supportedLanguages,
@@ -159,7 +196,7 @@ public RequestContext(
if (workspace is not null)
{
RoslynDebug.Assert(solution is not null);
- _lspSolution = new StrongBox<(Workspace Workspace, Solution Solution, Document? Document)>((workspace, solution, document));
+ _lspSolution = new StrongBox<(Workspace Workspace, Solution Solution, TextDocument? Document)>((workspace, solution, document));
}
else
{
@@ -228,7 +265,7 @@ public static async Task CreateAsync(
{
Workspace? workspace = null;
Solution? solution = null;
- Document? document = null;
+ TextDocument? document = null;
if (textDocument is not null)
{
// we were given a request associated with a document. Find the corresponding roslyn document for this.
diff --git a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj
index c01c28c61523f..6bfd8f20ad908 100644
--- a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj
+++ b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj
@@ -74,6 +74,7 @@
+
diff --git a/src/Features/LanguageServer/Protocol/ProtocolConstants.cs b/src/Features/LanguageServer/Protocol/ProtocolConstants.cs
index cd072452be53e..04e1a666c9f0f 100644
--- a/src/Features/LanguageServer/Protocol/ProtocolConstants.cs
+++ b/src/Features/LanguageServer/Protocol/ProtocolConstants.cs
@@ -15,5 +15,7 @@ internal class ProtocolConstants
public const string RoslynLspLanguagesContract = "RoslynLspLanguages";
public const string TypeScriptLanguageContract = "TypeScriptLspLanguage";
+
+ public const string RazorCohostContract = "RazorLanguageServer";
}
}
diff --git a/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs b/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs
index a99f84b743393..da3f8390d0ac8 100644
--- a/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs
+++ b/src/Features/LanguageServer/Protocol/WellKnownLspServerKinds.cs
@@ -8,6 +8,11 @@ namespace Microsoft.CodeAnalysis.LanguageServer;
internal enum WellKnownLspServerKinds
{
+ ///
+ /// Razor LSP server for Razor document requests (.razor and .cshtml files)
+ ///
+ RazorCohostServer,
+
///
/// Roslyn LSP server for razor c# requests.
///
@@ -52,6 +57,7 @@ public static string ToUserVisibleString(this WellKnownLspServerKinds server)
{
return server switch
{
+ WellKnownLspServerKinds.RazorCohostServer => "Razor Cohost Language Server Client",
WellKnownLspServerKinds.RazorLspServer => "Razor C# Language Server Client",
WellKnownLspServerKinds.LiveShareLspServer => "Live Share C#/Visual Basic Language Server Client",
WellKnownLspServerKinds.AlwaysActiveVSLspServer => "Roslyn Language Server Client",
@@ -69,6 +75,8 @@ public static string ToTelemetryString(this WellKnownLspServerKinds server)
{
return server switch
{
+ WellKnownLspServerKinds.RazorCohostServer => "RazorCohostLanguageClient",
+
// Telemetry was previously reported as RazorInProcLanguageClient.GetType().Name
WellKnownLspServerKinds.RazorLspServer => "RazorInProcLanguageClient",
@@ -96,6 +104,7 @@ public static string GetContractName(this WellKnownLspServerKinds server)
{
return server switch
{
+ WellKnownLspServerKinds.RazorCohostServer => ProtocolConstants.RazorCohostContract,
WellKnownLspServerKinds.RazorLspServer => ProtocolConstants.RoslynLspLanguagesContract,
WellKnownLspServerKinds.LiveShareLspServer => ProtocolConstants.RoslynLspLanguagesContract,
WellKnownLspServerKinds.AlwaysActiveVSLspServer => ProtocolConstants.RoslynLspLanguagesContract,
diff --git a/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs b/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs
index d6d04e0edfd8e..15cf5611badc9 100644
--- a/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs
+++ b/src/Features/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs
@@ -213,7 +213,7 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText)
///
/// This is always called serially in the when creating the .
///
- public async Task<(Workspace?, Solution?, Document?)> GetLspDocumentInfoAsync(TextDocumentIdentifier textDocumentIdentifier, CancellationToken cancellationToken)
+ public async Task<(Workspace?, Solution?, TextDocument?)> GetLspDocumentInfoAsync(TextDocumentIdentifier textDocumentIdentifier, CancellationToken cancellationToken)
{
// Get the LSP view of all the workspace solutions.
var uri = textDocumentIdentifier.Uri;
@@ -222,10 +222,10 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText)
// Find the matching document from the LSP solutions.
foreach (var (workspace, lspSolution, isForked) in lspSolutions)
{
- var documents = lspSolution.GetDocuments(textDocumentIdentifier.Uri);
+ var documents = lspSolution.GetTextDocuments(textDocumentIdentifier.Uri);
if (documents.Any())
{
- var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredDocument(id));
+ var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id));
// Record metadata on how we got this document.
var workspaceKind = document.Project.Solution.WorkspaceKind;
@@ -378,7 +378,7 @@ await workspace.TryOnDocumentOpenedAsync(
///
/// Given a set of documents from the workspace current solution, verify that the LSP text is the same as the document contents.
///
- private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken)
+ private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDictionary> documentsInWorkspace, CancellationToken cancellationToken)
{
foreach (var (uriInWorkspace, documentsForUri) in documentsInWorkspace)
{
@@ -396,7 +396,7 @@ private async Task DoesAllTextMatchWorkspaceSolutionAsync(ImmutableDiction
return true;
}
- private static async ValueTask AreChecksumsEqualAsync(Document document, SourceText lspText, CancellationToken cancellationToken)
+ private static async ValueTask AreChecksumsEqualAsync(TextDocument document, SourceText lspText, CancellationToken cancellationToken)
{
var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
if (documentText == lspText)
@@ -410,12 +410,12 @@ private static async ValueTask AreChecksumsEqualAsync(Document document, S
///
/// Using the workspace's current solutions, find the matching documents in for each URI.
///
- private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution)
+ private static ImmutableDictionary> GetDocumentsForUris(ImmutableArray trackedDocuments, Solution workspaceCurrentSolution)
{
- using var _ = PooledDictionary>.GetInstance(out var documentsInSolution);
+ using var _ = PooledDictionary>.GetInstance(out var documentsInSolution);
foreach (var trackedDoc in trackedDocuments)
{
- var documents = workspaceCurrentSolution.GetDocuments(trackedDoc);
+ var documents = workspaceCurrentSolution.GetTextDocuments(trackedDoc);
if (documents.Any())
{
documentsInSolution[trackedDoc] = documents;
diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs
index 4357dedb32d48..8ee5c538f8e53 100644
--- a/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs
+++ b/src/Features/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs
@@ -710,7 +710,7 @@ private static bool IsWorkspaceRegistered(Workspace workspace, TestLspServer tes
private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(Uri uri, TestLspServer testLspServer)
{
var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false);
- return (workspace, document);
+ return (workspace, document as Document);
}
private static Task<(Workspace?, Solution?)> GetLspHostWorkspaceAndSolutionAsync(TestLspServer testLspServer)
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs
new file mode 100644
index 0000000000000..e0fa705ee48b1
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorCohostDocumentRequestHandler.cs
@@ -0,0 +1,43 @@
+// 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 Microsoft.CommonLanguageServerProtocol.Framework;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+internal abstract class AbstractRazorCohostDocumentRequestHandler : AbstractRazorCohostRequestHandler, ITextDocumentIdentifierHandler
+{
+ TextDocumentIdentifier? ITextDocumentIdentifierHandler.GetTextDocumentIdentifier(TRequestType request)
+ {
+ var razorIdentifier = GetRazorTextDocumentIdentifier(request);
+ if (razorIdentifier == null)
+ {
+ return null;
+ }
+
+ var textDocumentIdentifier = new VSTextDocumentIdentifier
+ {
+ Uri = razorIdentifier.Value.Uri,
+ };
+
+ if (razorIdentifier.Value.ProjectContextId != null)
+ {
+ textDocumentIdentifier.ProjectContext = new VSProjectContext
+ {
+ Id = razorIdentifier.Value.ProjectContextId
+ };
+ }
+
+ return textDocumentIdentifier;
+ }
+
+ protected abstract RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TRequestType request);
+}
+
+///
+/// Custom type containing information in a to avoid coupling LSP protocol versions.
+///
+internal record struct RazorTextDocumentIdentifier(Uri Uri, string? ProjectContextId);
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorLspService.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorLspService.cs
new file mode 100644
index 0000000000000..fe2161116ec4b
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorLspService.cs
@@ -0,0 +1,15 @@
+// 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 Microsoft.CodeAnalysis.LanguageServer;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+///
+/// Base class for services that need to live in Razor but be exported using
+/// since those services must implement but the Razor code doesn't have IVT to it.
+///
+internal abstract class AbstractRazorLspService : ILspService
+{
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorRequestHandler.cs b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorRequestHandler.cs
new file mode 100644
index 0000000000000..33492c7b05fa7
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/AbstractRazorRequestHandler.cs
@@ -0,0 +1,35 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+internal abstract class AbstractRazorCohostRequestHandler : ILspServiceRequestHandler
+{
+ bool IMethodHandler.MutatesSolutionState => MutatesSolutionState;
+
+ bool ISolutionRequiredHandler.RequiresLSPSolution => RequiresLSPSolution;
+
+ Task IRequestHandler.HandleRequestAsync(TRequestType request, RequestContext context, CancellationToken cancellationToken)
+ {
+ // We have to wrap the RequestContext in order to expose it to Roslyn. We could create our own (by exporting
+ // and IRequestContextFactory) but that would not be possible if/when we live in the same server as Roslyn
+ // so may as well deal with it now.
+ // This does mean we can't nicely pass through the original Uri, which would have ProjectContext info, but
+ // we get the Project so that will have to do.
+
+ var razorRequestContext = new RazorCohostRequestContext(context);
+ return HandleRequestAsync(request, razorRequestContext, cancellationToken);
+ }
+
+ protected abstract bool MutatesSolutionState { get; }
+
+ protected abstract bool RequiresLSPSolution { get; }
+
+ protected abstract Task HandleRequestAsync(TRequestType request, RazorCohostRequestContext context, CancellationToken cancellationToken);
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs b/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs
new file mode 100644
index 0000000000000..520faed5e9018
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs
@@ -0,0 +1,17 @@
+// 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.Collections.Immutable;
+using Microsoft.CodeAnalysis.LanguageServer;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+internal static class Constants
+{
+ public const string RazorLSPContentType = "Razor";
+
+ public const string RazorLanguageContract = ProtocolConstants.RazorCohostContract;
+
+ public static readonly ImmutableArray RazorLanguage = ImmutableArray.Create("Razor");
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorLspServiceFactoryAttribute.cs b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorLspServiceFactoryAttribute.cs
new file mode 100644
index 0000000000000..8c23bcce2509c
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorLspServiceFactoryAttribute.cs
@@ -0,0 +1,14 @@
+// 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 Microsoft.CodeAnalysis.LanguageServer;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+[AttributeUsage(AttributeTargets.Class), MetadataAttribute]
+internal class ExportRazorLspServiceFactoryAttribute(Type handlerType) : ExportLspServiceFactoryAttribute(handlerType, Constants.RazorLanguageContract, WellKnownLspServerKinds.RazorCohostServer)
+{
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorStatelessLspServiceAttribute.cs b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorStatelessLspServiceAttribute.cs
new file mode 100644
index 0000000000000..3728932cce877
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/ExportRazorStatelessLspServiceAttribute.cs
@@ -0,0 +1,14 @@
+// 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 Microsoft.CodeAnalysis.LanguageServer;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+[AttributeUsage(AttributeTargets.Class), MetadataAttribute]
+internal class ExportRazorStatelessLspServiceAttribute(Type handlerType) : ExportStatelessLspServiceAttribute(handlerType, Constants.RazorLanguageContract, WellKnownLspServerKinds.RazorCohostServer)
+{
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostCapabilitiesProvider.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostCapabilitiesProvider.cs
new file mode 100644
index 0000000000000..1679c16c61651
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostCapabilitiesProvider.cs
@@ -0,0 +1,10 @@
+// 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.
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+internal interface IRazorCohostCapabilitiesProvider
+{
+ string GetCapabilities(string clientCapabilities);
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostClientLanguageServerManager.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostClientLanguageServerManager.cs
new file mode 100644
index 0000000000000..40f56f01e0c85
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostClientLanguageServerManager.cs
@@ -0,0 +1,19 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.LanguageServer;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost
+{
+ internal interface IRazorCohostClientLanguageServerManager : ILspService
+ {
+ ValueTask SendNotificationAsync(string methodName, CancellationToken cancellationToken);
+ ValueTask SendNotificationAsync(string methodName, TParams @params, CancellationToken cancellationToken);
+ ValueTask SendRequestAsync(string methodName, CancellationToken cancellationToken);
+ Task SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken);
+ ValueTask SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostLanguageClientActivationService.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostLanguageClientActivationService.cs
new file mode 100644
index 0000000000000..4b1a3fc34067e
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCohostLanguageClientActivationService.cs
@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+internal interface IRazorCohostLanguageClientActivationService
+{
+ ///
+ /// Returns whether the Razor cohost server should activate
+ ///
+ bool ShouldActivateCohostServer();
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/IRazorCustomMessageTarget.cs b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCustomMessageTarget.cs
new file mode 100644
index 0000000000000..28e11a4a5cad4
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/IRazorCustomMessageTarget.cs
@@ -0,0 +1,9 @@
+// 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.
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+internal interface IRazorCustomMessageTarget
+{
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostClientLanguageServerManagerFactory.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostClientLanguageServerManagerFactory.cs
new file mode 100644
index 0000000000000..4f5b630e193c7
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostClientLanguageServerManagerFactory.cs
@@ -0,0 +1,43 @@
+// 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.Threading.Tasks;
+using System.Threading;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.LanguageServer;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+[ExportRazorLspServiceFactory(typeof(IRazorCohostClientLanguageServerManager)), Shared]
+[method: ImportingConstructor]
+[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+internal class RazorCohostClientLanguageServerManagerFactory() : ILspServiceFactory
+{
+ public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind)
+ {
+ var notificationManager = lspServices.GetRequiredService();
+
+ return new RazorCohostClientLanguageServerManager(notificationManager);
+ }
+
+ internal class RazorCohostClientLanguageServerManager(IClientLanguageServerManager clientLanguageServerManager) : IRazorCohostClientLanguageServerManager
+ {
+ public Task SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken)
+ => clientLanguageServerManager.SendRequestAsync(methodName, @params, cancellationToken);
+
+ public ValueTask SendRequestAsync(string methodName, CancellationToken cancellationToken)
+ => clientLanguageServerManager.SendRequestAsync(methodName, cancellationToken);
+
+ public ValueTask SendRequestAsync(string methodName, TParams @params, CancellationToken cancellationToken)
+ => clientLanguageServerManager.SendRequestAsync(methodName, @params, cancellationToken);
+
+ public ValueTask SendNotificationAsync(string methodName, CancellationToken cancellationToken)
+ => clientLanguageServerManager.SendNotificationAsync(methodName, cancellationToken);
+
+ public ValueTask SendNotificationAsync(string methodName, TParams @params, CancellationToken cancellationToken)
+ => clientLanguageServerManager.SendNotificationAsync(methodName, @params, cancellationToken);
+ }
+}
diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidChangeEndpoint.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidChangeEndpoint.cs
new file mode 100644
index 0000000000000..1a7d0d17ca431
--- /dev/null
+++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorCohostDidChangeEndpoint.cs
@@ -0,0 +1,56 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.LanguageServer;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CommonLanguageServerProtocol.Framework;
+using Microsoft.VisualStudio.LanguageServer.Protocol;
+
+namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
+
+[LanguageServerEndpoint(Methods.TextDocumentDidChangeName)]
+[ExportRazorStatelessLspService(typeof(RazorCohostDidChangeEndpoint)), Shared]
+[method: ImportingConstructor]
+[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+internal sealed class RazorCohostDidChangeEndpoint([Import(AllowDefault = true)] IRazorCohostDidChangeHandler? didChangeHandler) : ILspServiceDocumentRequestHandler
+{
+ public bool MutatesSolutionState => true;
+ public bool RequiresLSPSolution => false;
+
+ public TextDocumentIdentifier GetTextDocumentIdentifier(DidChangeTextDocumentParams request)
+ => request.TextDocument;
+
+ public async Task