diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs index 78697b62e2eb5..0f1753257a44f 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs @@ -19,6 +19,7 @@ using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities; using Nerdbank.Streams; using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; @@ -31,12 +32,11 @@ internal abstract partial class AbstractInProcLanguageClient( ILspServiceLoggerFactory lspLoggerFactory, IThreadingContext threadingContext, ExportProvider exportProvider, - AbstractLanguageClientMiddleLayer? middleLayer = null) : ILanguageClient, ILanguageServerFactory, ICapabilitiesProvider, ILanguageClientCustomMessage2 + AbstractLanguageClientMiddleLayer? middleLayer = null) + : ILanguageClient, ILanguageServerFactory, ICapabilitiesProvider, ILanguageClientCustomMessage2, IPropertyOwner { private readonly IThreadingContext _threadingContext = threadingContext; -#pragma warning disable CS0618 // Type or member is obsolete - blocked on Razor switching to new APIs for STJ - https://github.com/dotnet/roslyn/issues/73317 - private readonly ILanguageClientMiddleLayer? _middleLayer = middleLayer; -#pragma warning restore CS0618 // Type or member is obsolete + private readonly ILanguageClientMiddleLayer2? _middleLayer = middleLayer; private readonly ILspServiceLoggerFactory _lspLoggerFactory = lspLoggerFactory; private readonly ExportProvider _exportProvider = exportProvider; @@ -102,6 +102,12 @@ internal abstract partial class AbstractInProcLanguageClient( /// public IEnumerable? FilesToWatch { get; } + /// + /// Property collection used by the client. + /// This is where we set the property to enable the use of client side System.Text.Json serialization. + /// + public PropertyCollection Properties { get; } = CreateStjPropertyCollection(); + public event AsyncEventHandler? StartAsync; /// @@ -267,6 +273,14 @@ public virtual AbstractLanguageServer Create( /// public Task AttachForCustomMessageAsync(JsonRpc rpc) => Task.CompletedTask; + private static PropertyCollection CreateStjPropertyCollection() + { + var collection = new PropertyCollection(); + // These are well known property names used by the LSP client to enable STJ client side serialization. + collection.AddProperty("lsp-serialization", "stj"); + return collection; + } + internal TestAccessor GetTestAccessor() { return new TestAccessor(this); diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs b/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs index 1ecbf95264cf7..434fc34126665 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractLanguageClientMiddleLayer.cs @@ -3,19 +3,17 @@ // See the LICENSE file in the project root for more information. using System; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.VisualStudio.LanguageServer.Client; -using Newtonsoft.Json.Linq; namespace Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; -#pragma warning disable CS0618 // Type or member is obsolete - blocked on Razor switching to new APIs for STJ - https://github.com/dotnet/roslyn/issues/73317 -internal abstract class AbstractLanguageClientMiddleLayer : ILanguageClientMiddleLayer -#pragma warning restore CS0618 // Type or member is obsolete +internal abstract class AbstractLanguageClientMiddleLayer : ILanguageClientMiddleLayer2 { public abstract bool CanHandle(string methodName); - public abstract Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification); + public abstract Task HandleNotificationAsync(string methodName, JsonElement methodParam, Func sendNotification); - public abstract Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest); + public abstract Task HandleRequestAsync(string methodName, JsonElement methodParam, Func> sendRequest); } diff --git a/src/Tools/ExternalAccess/Razor/RazorCSharpInterceptionMiddleLayer.cs b/src/Tools/ExternalAccess/Razor/RazorCSharpInterceptionMiddleLayer.cs index 5b274b59a765f..5330a42dd0147 100644 --- a/src/Tools/ExternalAccess/Razor/RazorCSharpInterceptionMiddleLayer.cs +++ b/src/Tools/ExternalAccess/Razor/RazorCSharpInterceptionMiddleLayer.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor { @@ -27,10 +27,17 @@ public RazorCSharpInterceptionMiddleLayerWrapper(IRazorCSharpInterceptionMiddleL public override bool CanHandle(string methodName) => _razorCSharpInterceptionMiddleLayer.CanHandle(methodName); - public override Task HandleNotificationAsync(string methodName, JToken methodParam, Func sendNotification) - => _razorCSharpInterceptionMiddleLayer.HandleNotificationAsync(methodName, methodParam, sendNotification); + public override Task HandleNotificationAsync(string methodName, JsonElement methodParam, Func sendNotification) + { + // Razor only ever looks at the method name, so it is safe to pass null for all the Newtonsoft JToken params. + return _razorCSharpInterceptionMiddleLayer.HandleNotificationAsync(methodName, null!, null!); + } - public override Task HandleRequestAsync(string methodName, JToken methodParam, Func> sendRequest) - => _razorCSharpInterceptionMiddleLayer.HandleRequestAsync(methodName, methodParam, sendRequest); + public override Task HandleRequestAsync(string methodName, JsonElement methodParam, Func> sendRequest) + { + // Razor only implements a middlelayer for semantic tokens refresh, which is a notification. + // Cohosting makes all this unnecessary, so keeping this as minimal as possible until then. + throw new NotImplementedException(); + } } } diff --git a/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTestsBase.cs b/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTestsBase.cs index ece1c3379fd94..e1a5fe73e0390 100644 --- a/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTestsBase.cs +++ b/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTestsBase.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Editor.UnitTests; +using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.UnitTests; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; @@ -43,7 +44,7 @@ protected class DocumentOutlineTestMocks : IAsyncDisposable private readonly IAsyncDisposable _disposable; internal DocumentOutlineTestMocks( - LanguageServiceBrokerCallback languageServiceBrokerCallback, + LanguageServiceBrokerCallback languageServiceBrokerCallback, IThreadingContext threadingContext, EditorTestWorkspace workspace, IAsyncDisposable disposable) @@ -55,7 +56,7 @@ internal DocumentOutlineTestMocks( TextBuffer = workspace.Documents.Single().GetTextBuffer(); } - internal LanguageServiceBrokerCallback LanguageServiceBrokerCallback { get; } + internal LanguageServiceBrokerCallback LanguageServiceBrokerCallback { get; } internal IThreadingContext ThreadingContext { get; } @@ -78,30 +79,22 @@ protected async Task CreateMocksAsync(string code) var workspace = EditorTestWorkspace.CreateCSharp(code, composition: s_composition); var threadingContext = workspace.GetService(); - var testLspServer = await CreateTestLspServerAsync(workspace, new InitializationOptions - { - // Set the message formatter to use newtonsoft on the client side to match real behavior. - // Also avoid calling initialize / initialized as the test harness uses types only compatible with STJ. - // TODO - switch back to STJ with https://github.com/dotnet/roslyn/issues/73317 - ClientMessageFormatter = new JsonMessageFormatter(), - CallInitialize = false, - CallInitialized = false - }); + var testLspServer = await CreateTestLspServerAsync(workspace); var mocks = new DocumentOutlineTestMocks(RequestAsync, threadingContext, workspace, testLspServer); return mocks; - async Task RequestAsync(Request request, CancellationToken cancellationToken) + async Task RequestAsync(Request request, CancellationToken cancellationToken) { - var docRequest = (DocumentRequest)request; + var docRequest = (DocumentRequest)request; var parameters = docRequest.ParameterFactory(docRequest.TextBuffer.CurrentSnapshot); - var response = await testLspServer.ExecuteRequestAsync(request.Method, parameters, cancellationToken); + var response = await testLspServer.ExecuteRequestAsync(request.Method, parameters, cancellationToken); return response; } } - private async Task CreateTestLspServerAsync(EditorTestWorkspace workspace, InitializationOptions initializationOptions) + private async Task CreateTestLspServerAsync(EditorTestWorkspace workspace) { var solution = workspace.CurrentSolution; @@ -132,13 +125,7 @@ private async Task CreateTestLspServerAsync(EditorTestWorkspace w var workspaceWaiter = operations.GetWaiter(FeatureAttribute.Workspace); await workspaceWaiter.ExpeditedWaitAsync(); - var server = await TestLspServer.CreateAsync(workspace, initializationOptions, _logger); - - // We disable the default test initialize call because the default test harness intialize types only support STJ (not newtonsoft). - // We only care that initialize has been called with some capability, so call with simple objects. - // TODO - remove with switch to STJ in https://github.com/dotnet/roslyn/issues/73317 - await server.ExecuteRequestAsync(Roslyn.LanguageServer.Protocol.Methods.InitializeName, new NewtonsoftInitializeParams() { Capabilities = new object() }, CancellationToken.None); - + var server = await TestLspServer.CreateAsync(workspace, new InitializationOptions(), _logger); return server; } diff --git a/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs b/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs index 4874c749f4682..0559d1c49b659 100644 --- a/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs +++ b/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.PatternMatching; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; @@ -25,15 +26,15 @@ internal sealed partial class DocumentOutlineViewModel /// Makes an LSP document symbol request and returns the response and the text snapshot used at /// the time the LSP client sends the request to the server. /// - public static async Task<(DocumentSymbolNewtonsoft.NewtonsoftRoslynDocumentSymbol[] response, ITextSnapshot snapshot)?> DocumentSymbolsRequestAsync( + public static async Task<(RoslynDocumentSymbol[] response, ITextSnapshot snapshot)?> DocumentSymbolsRequestAsync( ITextBuffer textBuffer, - LanguageServiceBrokerCallback callbackAsync, + LanguageServiceBrokerCallback callbackAsync, string textViewFilePath, CancellationToken cancellationToken) { ITextSnapshot? requestSnapshot = null; - var request = new DocumentRequest() + var request = new DocumentRequest() { Method = Methods.TextDocumentDocumentSymbolName, LanguageServerName = WellKnownLspServerKinds.AlwaysActiveVSLspServer.ToUserVisibleString(), @@ -41,9 +42,14 @@ internal sealed partial class DocumentOutlineViewModel ParameterFactory = (snapshot) => { requestSnapshot = snapshot; - return new DocumentSymbolNewtonsoft.NewtonsoftRoslynDocumentSymbolParams( - new DocumentSymbolNewtonsoft.NewtonsoftTextDocumentIdentifier(ProtocolConversions.CreateAbsoluteUri(textViewFilePath)), - UseHierarchicalSymbols: true); + return new RoslynDocumentSymbolParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = ProtocolConversions.CreateAbsoluteUri(textViewFilePath), + }, + UseHierarchicalSymbols = true + }; } }; @@ -88,7 +94,7 @@ internal sealed partial class DocumentOutlineViewModel /// ] /// } /// ] - public static ImmutableArray CreateDocumentSymbolData(DocumentSymbolNewtonsoft.NewtonsoftRoslynDocumentSymbol[] documentSymbols, ITextSnapshot textSnapshot) + public static ImmutableArray CreateDocumentSymbolData(RoslynDocumentSymbol[] documentSymbols, ITextSnapshot textSnapshot) { // Obtain a flat list of all the document symbols sorted by location in the document. var allSymbols = documentSymbols @@ -108,7 +114,7 @@ public static ImmutableArray CreateDocumentSymbolData(Docume // Returns the symbol in the list at index start (the parent symbol) with the following symbols in the list // (descendants) appropriately nested into the parent. - DocumentSymbolData NestDescendantSymbols(ImmutableArray allSymbols, int start, out int newStart) + DocumentSymbolData NestDescendantSymbols(ImmutableArray allSymbols, int start, out int newStart) { var currentParent = allSymbols[start]; start++; @@ -141,18 +147,20 @@ DocumentSymbolData NestDescendantSymbols(ImmutableArray parentRange.Start && childRange.End <= parentRange.End; - static LinePositionSpan RangeToLinePositionSpan(DocumentSymbolNewtonsoft.NewtonsoftRange range) - => new(new LinePosition(range.Start.Line, range.Start.Character), new LinePosition(range.End.Line, range.End.Character)); + static LinePositionSpan RangeToLinePositionSpan(Range range) + { + return new(new LinePosition(range.Start.Line, range.Start.Character), new LinePosition(range.End.Line, range.End.Character)); + } } // Converts a Document Symbol Range to a SnapshotSpan within the text snapshot used for the LSP request. - SnapshotSpan GetSymbolRangeSpan(DocumentSymbolNewtonsoft.NewtonsoftRange symbolRange) + SnapshotSpan GetSymbolRangeSpan(Range symbolRange) { var originalStartPosition = textSnapshot.GetLineFromLineNumber(symbolRange.Start.Line).Start.Position + symbolRange.Start.Character; var originalEndPosition = textSnapshot.GetLineFromLineNumber(symbolRange.End.Line).Start.Position + symbolRange.End.Character; diff --git a/src/VisualStudio/Core/Def/DocumentOutline/DocumentSymbolNewtonsoft.cs b/src/VisualStudio/Core/Def/DocumentOutline/DocumentSymbolNewtonsoft.cs deleted file mode 100644 index 962a451c2c5a5..0000000000000 --- a/src/VisualStudio/Core/Def/DocumentOutline/DocumentSymbolNewtonsoft.cs +++ /dev/null @@ -1,127 +0,0 @@ -// 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.Globalization; -using System.Runtime.Serialization; -using Microsoft.CodeAnalysis.LanguageServer; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.VisualStudio.LanguageServices.DocumentOutline; - -/// -/// These are very temporary types that we need in order to serialize document symbol data -/// using Newtonsoft instead of System.Text.Json -/// -/// We currently must support Newtonsoft serialization here because we have not yet opted into using STJ -/// in the VS language server client (and so the client will serialize the request using Newtonsoft). -/// -/// https://github.com/dotnet/roslyn/pull/72675 tracks opting in the client to STJ. -/// TODO - everything in this type should be deleted once the client side is using STJ. -/// -internal class DocumentSymbolNewtonsoft -{ - private class NewtonsoftDocumentUriConverter : JsonConverter - { - /// - public override bool CanConvert(Type objectType) - { - return true; - } - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - reader = reader ?? throw new ArgumentNullException(nameof(reader)); - if (reader.TokenType == JsonToken.String) - { - var token = JToken.ReadFrom(reader); - var uri = new Uri(token.ToObject()); - - return uri; - } - else if (reader.TokenType == JsonToken.Null) - { - return null; - } - - throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, LanguageServerProtocolResources.DocumentUriSerializationError, reader.Value)); - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer = writer ?? throw new ArgumentNullException(nameof(writer)); - - if (value is Uri uri) - { - var token = JToken.FromObject(uri.AbsoluteUri); - token.WriteTo(writer); - } - else - { - throw new ArgumentException($"{nameof(value)} must be of type {nameof(Uri)}"); - } - } - } - - [DataContract] - internal record NewtonsoftTextDocumentIdentifier([property: DataMember(Name = "uri"), JsonConverter(typeof(NewtonsoftDocumentUriConverter))] Uri Uri); - - [DataContract] - internal record NewtonsoftRoslynDocumentSymbolParams( - [property: DataMember(Name = "textDocument")] NewtonsoftTextDocumentIdentifier TextDocument, - [property: DataMember(Name = "useHierarchicalSymbols"), JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] bool UseHierarchicalSymbols); - - [DataContract] - internal record NewtonsoftRoslynDocumentSymbol( - [property: DataMember(IsRequired = true, Name = "name")] string Name, - [property: DataMember(Name = "detail")][property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Detail, - [property: DataMember(Name = "kind")] NewtonsoftSymbolKind Kind, - [property: DataMember(Name = "deprecated")][property: JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] bool Deprecated, - [property: DataMember(IsRequired = true, Name = "range")] NewtonsoftRange Range, - [property: DataMember(IsRequired = true, Name = "selectionRange")] NewtonsoftRange SelectionRange, - [property: DataMember(Name = "children")][property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] NewtonsoftRoslynDocumentSymbol[]? Children, - [property: DataMember(Name = "glyph")] int Glyph); - - [DataContract] - internal record NewtonsoftRange( - [property: DataMember(Name = "start"), JsonProperty(Required = Required.Always)] NewtonsoftPosition Start, - [property: DataMember(Name = "end"), JsonProperty(Required = Required.Always)] NewtonsoftPosition End); - - [DataContract] - internal record NewtonsoftPosition([property: DataMember(Name = "line")] int Line, [property: DataMember(Name = "character")] int Character); - - [DataContract] - internal enum NewtonsoftSymbolKind - { - File = 1, - Module = 2, - Namespace = 3, - Package = 4, - Class = 5, - Method = 6, - Property = 7, - Field = 8, - Constructor = 9, - Enum = 10, - Interface = 11, - Function = 12, - Variable = 13, - Constant = 14, - String = 15, - Number = 16, - Boolean = 17, - Array = 18, - Object = 19, - Key = 20, - Null = 21, - EnumMember = 22, - Struct = 23, - Event = 24, - Operator = 25, - TypeParameter = 26, - } -}