未验证 提交 c7fcad34 编写于 作者: M msftbot[bot] 提交者: GitHub

Merge pull request #47948 from dibarbet/merges/merge-features/lspClassifications-to-master

merge features/lspClassifications into master
......@@ -341,6 +341,13 @@ private protected static LanguageServerProtocol GetLanguageServer(Solution solut
return workspace.ExportProvider.GetExportedValue<LanguageServerProtocol>();
}
private protected static RequestExecutionQueue CreateRequestQueue(Solution solution)
{
var workspace = (TestWorkspace)solution.Workspace;
var solutionProvider = workspace.ExportProvider.GetExportedValue<ILspSolutionProvider>();
return new RequestExecutionQueue(solutionProvider);
}
private static string GetDocumentFilePathFromName(string documentName)
=> "C:\\" + documentName;
}
......
......@@ -21,7 +21,9 @@ internal abstract class AbstractRequestHandlerProvider
private readonly ImmutableDictionary<string, Lazy<IRequestHandler, IRequestHandlerMetadata>> _requestHandlers;
public AbstractRequestHandlerProvider(IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers, string? languageName = null)
=> _requestHandlers = CreateMethodToHandlerMap(requestHandlers.Where(rh => rh.Metadata.LanguageName == languageName));
{
_requestHandlers = CreateMethodToHandlerMap(requestHandlers.Where(rh => rh.Metadata.LanguageName == languageName));
}
private static ImmutableDictionary<string, Lazy<IRequestHandler, IRequestHandlerMetadata>> CreateMethodToHandlerMap(IEnumerable<Lazy<IRequestHandler, IRequestHandlerMetadata>> requestHandlers)
{
......@@ -34,20 +36,21 @@ public AbstractRequestHandlerProvider(IEnumerable<Lazy<IRequestHandler, IRequest
return requestHandlerDictionary.ToImmutable();
}
public Task<ResponseType> ExecuteRequestAsync<RequestType, ResponseType>(string methodName, RequestType request, LSP.ClientCapabilities clientCapabilities,
public Task<ResponseType> ExecuteRequestAsync<RequestType, ResponseType>(RequestExecutionQueue queue, string methodName, RequestType request, LSP.ClientCapabilities clientCapabilities,
string? clientName, CancellationToken cancellationToken) where RequestType : class
{
Contract.ThrowIfNull(request);
Contract.ThrowIfTrue(string.IsNullOrEmpty(methodName), "Invalid method name");
var handler = (IRequestHandler<RequestType, ResponseType>?)_requestHandlers[methodName]?.Value;
var handlerEntry = _requestHandlers[methodName];
Contract.ThrowIfNull(handlerEntry, string.Format("Request handler entry not found for method {0}", methodName));
var mutatesSolutionState = handlerEntry.Metadata.MutatesSolutionState;
var handler = (IRequestHandler<RequestType, ResponseType>?)handlerEntry.Value;
Contract.ThrowIfNull(handler, string.Format("Request handler not found for method {0}", methodName));
var context = CreateContext(clientCapabilities, clientName);
return handler.HandleRequestAsync(request, context, cancellationToken);
return queue.ExecuteAsync(mutatesSolutionState, handler, request, clientCapabilities, clientName, cancellationToken);
}
private static RequestContext CreateContext(LSP.ClientCapabilities clientCapabilities, string? clientName)
=> new RequestContext(clientCapabilities, clientName);
}
}
......@@ -11,6 +11,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
......@@ -50,6 +51,20 @@ public static ImmutableArray<TextDocument> GetTextDocuments(this Solution soluti
return documentIds.SelectAsArray(id => getDocument(solution, id));
}
public static (Document?, Solution) GetDocumentAndSolution(this ILspSolutionProvider provider, TextDocumentIdentifier? textDocument, string? clientName)
{
var solution = provider.GetCurrentSolutionForMainWorkspace();
if (textDocument != null)
{
var document = provider.GetDocument(textDocument, clientName);
var solutionOfDocument = document?.Project.Solution;
return (document, solutionOfDocument ?? solution);
}
return (null, solution);
}
public static ImmutableArray<Document> GetDocuments(this ILspSolutionProvider solutionProvider, Uri uri, string? clientName)
{
return GetDocuments<Document>(solutionProvider, uri, (s, u, c) => s.GetDocuments(u), clientName);
......
......@@ -30,8 +30,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
/// complex data, such as edits and commands, to be computed only when necessary
/// (i.e. when hovering/previewing a code action).
/// </summary>
[ExportLspMethod(MSLSPMethods.TextDocumentCodeActionResolveName), Shared]
internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeAction, LSP.VSCodeAction>
[ExportLspMethod(MSLSPMethods.TextDocumentCodeActionResolveName, mutatesSolutionState: false), Shared]
internal class CodeActionResolveHandler : IRequestHandler<LSP.VSCodeAction, LSP.VSCodeAction>
{
private readonly CodeActionsCache _codeActionsCache;
private readonly ICodeFixService _codeFixService;
......@@ -42,21 +42,22 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
public CodeActionResolveHandler(
CodeActionsCache codeActionsCache,
ICodeFixService codeFixService,
ICodeRefactoringService codeRefactoringService,
ILspSolutionProvider solutionProvider)
: base(solutionProvider)
ICodeRefactoringService codeRefactoringService)
{
_codeActionsCache = codeActionsCache;
_codeFixService = codeFixService;
_codeRefactoringService = codeRefactoringService;
}
public override async Task<LSP.VSCodeAction> HandleRequestAsync(LSP.VSCodeAction codeAction, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(VSCodeAction request)
=> ((JToken)request.Data).ToObject<CodeActionResolveData>().TextDocument;
public async Task<LSP.VSCodeAction> HandleRequestAsync(LSP.VSCodeAction codeAction, RequestContext context, CancellationToken cancellationToken)
{
var data = ((JToken)codeAction.Data).ToObject<CodeActionResolveData>();
var document = SolutionProvider.GetDocument(data.TextDocument, context.ClientName);
var document = context.Document;
Contract.ThrowIfNull(document);
var data = ((JToken)codeAction.Data).ToObject<CodeActionResolveData>();
var codeActions = await CodeActionHelpers.GetCodeActionsAsync(
_codeActionsCache,
document,
......
......@@ -22,8 +22,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
/// of the returned VSCodeActions blank, as these properties should be populated by the
/// CodeActionsResolveHandler only when the user requests them.
/// </summary>
[ExportLspMethod(LSP.Methods.TextDocumentCodeActionName), Shared]
internal class CodeActionsHandler : AbstractRequestHandler<LSP.CodeActionParams, LSP.VSCodeAction[]>
[ExportLspMethod(LSP.Methods.TextDocumentCodeActionName, mutatesSolutionState: false), Shared]
internal class CodeActionsHandler : IRequestHandler<LSP.CodeActionParams, LSP.VSCodeAction[]>
{
private readonly CodeActionsCache _codeActionsCache;
private readonly ICodeFixService _codeFixService;
......@@ -36,18 +36,18 @@ internal class CodeActionsHandler : AbstractRequestHandler<LSP.CodeActionParams,
public CodeActionsHandler(
CodeActionsCache codeActionsCache,
ICodeFixService codeFixService,
ICodeRefactoringService codeRefactoringService,
ILspSolutionProvider solutionProvider)
: base(solutionProvider)
ICodeRefactoringService codeRefactoringService)
{
_codeActionsCache = codeActionsCache;
_codeFixService = codeFixService;
_codeRefactoringService = codeRefactoringService;
}
public override async Task<LSP.VSCodeAction[]> HandleRequestAsync(LSP.CodeActionParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(CodeActionParams request) => request.TextDocument;
public async Task<LSP.VSCodeAction[]> HandleRequestAsync(LSP.CodeActionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return Array.Empty<VSCodeAction>();
......
......@@ -12,12 +12,13 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler.Commands;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.WorkspaceExecuteCommandName)]
[ExportLspMethod(LSP.Methods.WorkspaceExecuteCommandName, mutatesSolutionState: false)]
internal class ExecuteWorkspaceCommandHandler : IRequestHandler<LSP.ExecuteCommandParams, object>
{
private readonly ImmutableDictionary<string, Lazy<IExecuteWorkspaceCommandHandler, IExecuteWorkspaceCommandHandlerMetadata>> _executeCommandHandlers;
......@@ -39,6 +40,8 @@ public ExecuteWorkspaceCommandHandler([ImportMany] IEnumerable<Lazy<IExecuteWork
return requestHandlerDictionary.ToImmutable();
}
public virtual TextDocumentIdentifier? GetTextDocumentIdentifier(ExecuteCommandParams request) => null;
/// <summary>
/// Handles an <see cref="LSP.Methods.WorkspaceExecuteCommand"/>
/// by delegating to a handler for the specific command requested.
......
......@@ -23,18 +23,20 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
/// Handle a completion request.
/// </summary>
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentCompletionName)]
internal class CompletionHandler : AbstractRequestHandler<LSP.CompletionParams, LSP.CompletionList?>
[ExportLspMethod(LSP.Methods.TextDocumentCompletionName, mutatesSolutionState: false)]
internal class CompletionHandler : IRequestHandler<LSP.CompletionParams, LSP.CompletionList?>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompletionHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public CompletionHandler()
{
}
public override async Task<LSP.CompletionList?> HandleRequestAsync(LSP.CompletionParams request, RequestContext context, CancellationToken cancellationToken)
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.CompletionParams request) => request.TextDocument;
public async Task<LSP.CompletionList?> HandleRequestAsync(LSP.CompletionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return null;
......
......@@ -22,33 +22,30 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
/// Handle a completion resolve request to add description.
/// </summary>
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentCompletionResolveName)]
internal class CompletionResolveHandler : AbstractRequestHandler<LSP.CompletionItem, LSP.CompletionItem>
[ExportLspMethod(LSP.Methods.TextDocumentCompletionResolveName, mutatesSolutionState: false)]
internal class CompletionResolveHandler : IRequestHandler<LSP.CompletionItem, LSP.CompletionItem>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompletionResolveHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public CompletionResolveHandler()
{
}
public override async Task<LSP.CompletionItem> HandleRequestAsync(LSP.CompletionItem completionItem, RequestContext context, CancellationToken cancellationToken)
{
CompletionResolveData data;
if (completionItem.Data is CompletionResolveData)
{
data = (CompletionResolveData)completionItem.Data;
}
else
{
data = ((JToken)completionItem.Data).ToObject<CompletionResolveData>();
}
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.CompletionItem request)
=> GetCompletionResolveData(request).TextDocument;
var document = SolutionProvider.GetDocument(data.TextDocument, context.ClientName);
private static CompletionResolveData GetCompletionResolveData(LSP.CompletionItem completionItem)
=> completionItem.Data as CompletionResolveData ?? ((JToken)completionItem.Data).ToObject<CompletionResolveData>();
public async Task<LSP.CompletionItem> HandleRequestAsync(LSP.CompletionItem completionItem, RequestContext context, CancellationToken cancellationToken)
{
var document = context.Document;
if (document == null)
{
return completionItem;
}
var data = GetCompletionResolveData(completionItem);
var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(data.Position), cancellationToken).ConfigureAwait(false);
var completionService = document.Project.LanguageServices.GetRequiredService<CompletionService>();
......
......@@ -19,18 +19,21 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal abstract class AbstractGoToDefinitionHandler<RequestType, ResponseType> : AbstractRequestHandler<RequestType, ResponseType>
internal abstract class AbstractGoToDefinitionHandler : IRequestHandler<LSP.TextDocumentPositionParams, LSP.Location[]>
{
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
public AbstractGoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileService, ILspSolutionProvider solutionProvider) : base(solutionProvider)
public AbstractGoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileService)
=> _metadataAsSourceFileService = metadataAsSourceFileService;
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.TextDocumentPositionParams request) => request.TextDocument;
public abstract Task<LSP.Location[]> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken);
protected async Task<LSP.Location[]> GetDefinitionAsync(LSP.TextDocumentPositionParams request, bool typeOnly, RequestContext context, CancellationToken cancellationToken)
{
var locations = ArrayBuilder<LSP.Location>.GetInstance();
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return locations.ToArrayAndFree();
......
......@@ -15,13 +15,13 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentDefinitionName)]
internal class GoToDefinitionHandler : AbstractGoToDefinitionHandler<LSP.TextDocumentPositionParams, LSP.Location[]>
[ExportLspMethod(LSP.Methods.TextDocumentDefinitionName, mutatesSolutionState: false)]
internal class GoToDefinitionHandler : AbstractGoToDefinitionHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileService, ILspSolutionProvider solutionProvider)
: base(metadataAsSourceFileService, solutionProvider)
public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileService)
: base(metadataAsSourceFileService)
{
}
......
......@@ -15,13 +15,13 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentTypeDefinitionName)]
internal class GoToTypeDefinitionHandler : AbstractGoToDefinitionHandler<LSP.TextDocumentPositionParams, LSP.Location[]>
[ExportLspMethod(LSP.Methods.TextDocumentTypeDefinitionName, mutatesSolutionState: false)]
internal class GoToTypeDefinitionHandler : AbstractGoToDefinitionHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public GoToTypeDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileService, ILspSolutionProvider solutionProvider)
: base(metadataAsSourceFileService, solutionProvider)
public GoToTypeDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileService)
: base(metadataAsSourceFileService)
{
}
......
......@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Composition;
......@@ -15,9 +17,16 @@ internal class ExportLspMethodAttribute : ExportAttribute, IRequestHandlerMetada
{
public string MethodName { get; }
public string LanguageName { get; }
public string? LanguageName { get; }
/// <summary>
/// Whether or not handling this method results in changes to the current solution state.
/// Mutating requests will block all subsequent requests from starting until after they have
/// completed and mutations have been applied. See <see cref="RequestExecutionQueue"/>.
/// </summary>
public bool MutatesSolutionState { get; }
public ExportLspMethodAttribute(string methodName, string languageName = null) : base(typeof(IRequestHandler))
public ExportLspMethodAttribute(string methodName, bool mutatesSolutionState, string? languageName = null) : base(typeof(IRequestHandler))
{
if (string.IsNullOrEmpty(methodName))
{
......@@ -25,6 +34,7 @@ public ExportLspMethodAttribute(string methodName, string languageName = null) :
}
MethodName = methodName;
MutatesSolutionState = mutatesSolutionState;
LanguageName = languageName;
}
}
......
......@@ -16,20 +16,22 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentFoldingRangeName)]
internal class FoldingRangesHandler : AbstractRequestHandler<FoldingRangeParams, FoldingRange[]>
[ExportLspMethod(Methods.TextDocumentFoldingRangeName, mutatesSolutionState: false)]
internal class FoldingRangesHandler : IRequestHandler<FoldingRangeParams, FoldingRange[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FoldingRangesHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FoldingRangesHandler()
{
}
public override async Task<FoldingRange[]> HandleRequestAsync(FoldingRangeParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(FoldingRangeParams request) => request.TextDocument;
public async Task<FoldingRange[]> HandleRequestAsync(FoldingRangeParams request, RequestContext context, CancellationToken cancellationToken)
{
var foldingRanges = ArrayBuilder<FoldingRange>.GetInstance();
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return foldingRanges.ToArrayAndFree();
......
......@@ -11,20 +11,17 @@
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal abstract class AbstractFormatDocumentHandlerBase<RequestType, ResponseType> : AbstractRequestHandler<RequestType, ResponseType>
internal abstract class AbstractFormatDocumentHandlerBase<RequestType, ResponseType> : IRequestHandler<RequestType, ResponseType>
{
protected AbstractFormatDocumentHandlerBase(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
protected async Task<LSP.TextEdit[]> GetTextEditsAsync(LSP.TextDocumentIdentifier documentIdentifier, RequestContext context, CancellationToken cancellationToken, LSP.Range? range = null)
protected async Task<LSP.TextEdit[]> GetTextEditsAsync(RequestContext context, CancellationToken cancellationToken, LSP.Range? range = null)
{
var edits = new ArrayBuilder<LSP.TextEdit>();
var document = SolutionProvider.GetDocument(documentIdentifier, context.ClientName);
var document = context.Document;
if (document != null)
{
......@@ -45,5 +42,7 @@ protected async Task<LSP.TextEdit[]> GetTextEditsAsync(LSP.TextDocumentIdentifie
protected virtual Task<IList<TextChange>> GetFormattingChangesAsync(IEditorFormattingService formattingService, Document document, TextSpan? textSpan, CancellationToken cancellationToken)
=> formattingService.GetFormattingChangesAsync(document, textSpan, cancellationToken);
public abstract TextDocumentIdentifier? GetTextDocumentIdentifier(RequestType request);
public abstract Task<ResponseType> HandleRequestAsync(RequestType request, RequestContext context, CancellationToken cancellationToken);
}
}
......@@ -14,16 +14,18 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentFormattingName)]
[ExportLspMethod(LSP.Methods.TextDocumentFormattingName, mutatesSolutionState: false)]
internal class FormatDocumentHandler : AbstractFormatDocumentHandlerBase<LSP.DocumentFormattingParams, LSP.TextEdit[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FormatDocumentHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FormatDocumentHandler()
{
}
public override LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.DocumentFormattingParams request) => request.TextDocument;
public override Task<LSP.TextEdit[]> HandleRequestAsync(LSP.DocumentFormattingParams request, RequestContext context, CancellationToken cancellationToken)
=> GetTextEditsAsync(request.TextDocument, context, cancellationToken);
=> GetTextEditsAsync(context, cancellationToken);
}
}
......@@ -20,19 +20,21 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentOnTypeFormattingName)]
internal class FormatDocumentOnTypeHandler : AbstractRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]>
[ExportLspMethod(Methods.TextDocumentOnTypeFormattingName, mutatesSolutionState: false)]
internal class FormatDocumentOnTypeHandler : IRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FormatDocumentOnTypeHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FormatDocumentOnTypeHandler()
{
}
public override async Task<TextEdit[]> HandleRequestAsync(DocumentOnTypeFormattingParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(DocumentOnTypeFormattingParams request) => request.TextDocument;
public async Task<TextEdit[]> HandleRequestAsync(DocumentOnTypeFormattingParams request, RequestContext context, CancellationToken cancellationToken)
{
var edits = new ArrayBuilder<TextEdit>();
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document != null)
{
var formattingService = document.Project.LanguageServices.GetRequiredService<IEditorFormattingService>();
......
......@@ -14,16 +14,18 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentRangeFormattingName)]
[ExportLspMethod(Methods.TextDocumentRangeFormattingName, mutatesSolutionState: false)]
internal class FormatDocumentRangeHandler : AbstractFormatDocumentHandlerBase<DocumentRangeFormattingParams, TextEdit[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FormatDocumentRangeHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FormatDocumentRangeHandler()
{
}
public override TextDocumentIdentifier? GetTextDocumentIdentifier(DocumentRangeFormattingParams request) => request.TextDocument;
public override Task<TextEdit[]> HandleRequestAsync(DocumentRangeFormattingParams request, RequestContext context, CancellationToken cancellationToken)
=> GetTextEditsAsync(request.TextDocument, context, cancellationToken, range: request.Range);
=> GetTextEditsAsync(context, cancellationToken, range: request.Range);
}
}
......@@ -17,18 +17,20 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentDocumentHighlightName)]
internal class DocumentHighlightsHandler : AbstractRequestHandler<TextDocumentPositionParams, DocumentHighlight[]>
[ExportLspMethod(Methods.TextDocumentDocumentHighlightName, mutatesSolutionState: false)]
internal class DocumentHighlightsHandler : IRequestHandler<TextDocumentPositionParams, DocumentHighlight[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DocumentHighlightsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public DocumentHighlightsHandler()
{
}
public override async Task<DocumentHighlight[]> HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(TextDocumentPositionParams request) => request.TextDocument;
public async Task<DocumentHighlight[]> HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return Array.Empty<DocumentHighlight>();
......
......@@ -17,18 +17,20 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentHoverName)]
internal class HoverHandler : AbstractRequestHandler<TextDocumentPositionParams, Hover?>
[ExportLspMethod(Methods.TextDocumentHoverName, mutatesSolutionState: false)]
internal class HoverHandler : IRequestHandler<TextDocumentPositionParams, Hover?>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public HoverHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public HoverHandler()
{
}
public override async Task<Hover?> HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(TextDocumentPositionParams request) => request.TextDocument;
public async Task<Hover?> HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return null;
......
......@@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
......@@ -19,9 +20,14 @@ internal interface IRequestHandler
internal interface IRequestHandler<RequestType, ResponseType> : IRequestHandler
{
/// <summary>
/// Handles an LSP request.
/// Gets the TestDocumentIdentifier from the request, if the request provides one.
/// </summary>
/// <param name="context">The LSP request context.</param>
TextDocumentIdentifier? GetTextDocumentIdentifier(RequestType request);
/// <summary>
/// Handles an LSP request in the context of the supplied document and/or solutuion.
/// </summary>
/// <param name="context">The LSP request context, which should have been filled in with document information from <see cref="GetTextDocumentIdentifier(RequestType)"/> if applicable.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the request processing.</param>
/// <returns>The LSP response.</returns>
Task<ResponseType> HandleRequestAsync(RequestType request, RequestContext context, CancellationToken cancellationToken);
......
......@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal interface IRequestHandlerMetadata
......@@ -14,6 +16,13 @@ internal interface IRequestHandlerMetadata
/// <summary>
/// Name of the language for LSP method to handle (optional).
/// </summary>
string LanguageName { get; }
string? LanguageName { get; }
/// <summary>
/// Whether or not handling this method results in changes to the current solution state.
/// Mutating requests will block all subsequent requests from starting until after they have
/// completed and mutations have been applied. See <see cref="RequestExecutionQueue"/>.
/// </summary>
bool MutatesSolutionState { get; }
}
}
......@@ -14,13 +14,14 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.InitializeName)]
[ExportLspMethod(LSP.Methods.InitializeName, mutatesSolutionState: false)]
internal class InitializeHandler : IRequestHandler<LSP.InitializeParams, LSP.InitializeResult>
{
private readonly ImmutableArray<Lazy<CompletionProvider, Completion.Providers.CompletionProviderMetadata>> _completionProviders;
......@@ -34,6 +35,8 @@ public InitializeHandler([ImportMany] IEnumerable<Lazy<CompletionProvider, Compl
.ToImmutableArray();
}
public TextDocumentIdentifier? GetTextDocumentIdentifier(InitializeParams request) => null;
public Task<LSP.InitializeResult> HandleRequestAsync(LSP.InitializeParams request, RequestContext context, CancellationToken cancellationToken)
{
var triggerCharacters = _completionProviders.SelectMany(lz => GetTriggerCharacters(lz.Value)).Distinct().Select(c => c.ToString()).ToArray();
......@@ -58,6 +61,16 @@ public Task<LSP.InitializeResult> HandleRequestAsync(LSP.InitializeParams reques
DocumentHighlightProvider = true,
ReferencesProvider = true,
ProjectContextProvider = true,
SemanticTokensOptions = new LSP.SemanticTokensOptions
{
DocumentProvider = new LSP.SemanticTokensDocumentProviderOptions { Edits = true },
RangeProvider = true,
Legend = new LSP.SemanticTokensLegend
{
TokenTypes = LSP.SemanticTokenTypes.AllTypes.Concat(SemanticTokensHelpers.RoslynCustomTokenTypes).ToArray(),
TokenModifiers = new string[] { LSP.SemanticTokenModifiers.Static }
}
},
ExecuteCommandProvider = new LSP.ExecuteCommandOptions(),
TextDocumentSync = new LSP.TextDocumentSyncOptions
{
......
......@@ -17,20 +17,22 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.MSLSPMethods.OnAutoInsertName)]
internal class OnAutoInsertHandler : AbstractRequestHandler<LSP.DocumentOnAutoInsertParams, LSP.DocumentOnAutoInsertResponseItem[]>
[ExportLspMethod(LSP.MSLSPMethods.OnAutoInsertName, mutatesSolutionState: false)]
internal class OnAutoInsertHandler : IRequestHandler<LSP.DocumentOnAutoInsertParams, LSP.DocumentOnAutoInsertResponseItem[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public OnAutoInsertHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public OnAutoInsertHandler()
{
}
public override async Task<LSP.DocumentOnAutoInsertResponseItem[]> HandleRequestAsync(LSP.DocumentOnAutoInsertParams autoInsertParams, RequestContext context, CancellationToken cancellationToken)
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.DocumentOnAutoInsertParams request) => request.TextDocument;
public async Task<LSP.DocumentOnAutoInsertResponseItem[]> HandleRequestAsync(LSP.DocumentOnAutoInsertParams autoInsertParams, RequestContext context, CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<LSP.DocumentOnAutoInsertResponseItem>.GetInstance(out var response);
var document = SolutionProvider.GetDocument(autoInsertParams.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
......
......@@ -18,18 +18,23 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(MSLSPMethods.ProjectContextsName)]
internal class GetTextDocumentWithContextHandler : AbstractRequestHandler<GetTextDocumentWithContextParams, ActiveProjectContexts?>
[ExportLspMethod(MSLSPMethods.ProjectContextsName, mutatesSolutionState: false)]
internal class GetTextDocumentWithContextHandler : IRequestHandler<GetTextDocumentWithContextParams, ActiveProjectContexts?>
{
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public GetTextDocumentWithContextHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public GetTextDocumentWithContextHandler(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
}
public override Task<ActiveProjectContexts?> HandleRequestAsync(GetTextDocumentWithContextParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(GetTextDocumentWithContextParams request) => null;
public Task<ActiveProjectContexts?> HandleRequestAsync(GetTextDocumentWithContextParams request, RequestContext context, CancellationToken cancellationToken)
{
var documents = SolutionProvider.GetDocuments(request.TextDocument.Uri, context.ClientName);
var documents = _solutionProvider.GetDocuments(request.TextDocument.Uri, context.ClientName);
if (!documents.Any())
{
......
......@@ -19,23 +19,25 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[ExportLspMethod(LSP.Methods.TextDocumentReferencesName), Shared]
internal class FindAllReferencesHandler : AbstractRequestHandler<LSP.ReferenceParams, LSP.VSReferenceItem[]>
[ExportLspMethod(LSP.Methods.TextDocumentReferencesName, mutatesSolutionState: false), Shared]
internal class FindAllReferencesHandler : IRequestHandler<LSP.ReferenceParams, LSP.VSReferenceItem[]>
{
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FindAllReferencesHandler(IMetadataAsSourceFileService metadataAsSourceFileService, ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FindAllReferencesHandler(IMetadataAsSourceFileService metadataAsSourceFileService)
{
_metadataAsSourceFileService = metadataAsSourceFileService;
}
public override async Task<LSP.VSReferenceItem[]> HandleRequestAsync(ReferenceParams referenceParams, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(ReferenceParams request) => request.TextDocument;
public async Task<LSP.VSReferenceItem[]> HandleRequestAsync(ReferenceParams referenceParams, RequestContext context, CancellationToken cancellationToken)
{
Debug.Assert(context.ClientCapabilities.HasVisualStudioLspCapability());
var document = SolutionProvider.GetDocument(referenceParams.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return Array.Empty<LSP.VSReferenceItem>();
......
......@@ -16,20 +16,22 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentImplementationName)]
internal class FindImplementationsHandler : AbstractRequestHandler<LSP.TextDocumentPositionParams, LSP.Location[]>
[ExportLspMethod(LSP.Methods.TextDocumentImplementationName, mutatesSolutionState: false)]
internal class FindImplementationsHandler : IRequestHandler<LSP.TextDocumentPositionParams, LSP.Location[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FindImplementationsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FindImplementationsHandler()
{
}
public override async Task<LSP.Location[]> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.TextDocumentPositionParams request) => request.TextDocument;
public async Task<LSP.Location[]> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
{
var locations = ArrayBuilder<LSP.Location>.GetInstance();
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return locations.ToArrayAndFree();
......
......@@ -19,19 +19,21 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[ExportLspMethod(LSP.Methods.TextDocumentRenameName), Shared]
internal class RenameHandler : AbstractRequestHandler<LSP.RenameParams, WorkspaceEdit?>
[ExportLspMethod(LSP.Methods.TextDocumentRenameName, mutatesSolutionState: false), Shared]
internal class RenameHandler : IRequestHandler<LSP.RenameParams, WorkspaceEdit?>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RenameHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public RenameHandler()
{
}
public override async Task<WorkspaceEdit?> HandleRequestAsync(RenameParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(RenameParams request) => request.TextDocument;
public async Task<WorkspaceEdit?> HandleRequestAsync(RenameParams request, RequestContext context, CancellationToken cancellationToken)
{
WorkspaceEdit? workspaceEdit = null;
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document != null)
{
var oldSolution = document.Project.Solution;
......
......@@ -2,11 +2,13 @@
// 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 Microsoft.VisualStudio.LanguageServer.Protocol;
#nullable enable
using System;
using System.Diagnostics;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
/// <summary>
......@@ -14,20 +16,44 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler
/// </summary>
internal readonly struct RequestContext
{
private readonly Action<Solution>? _solutionUpdater;
/// <summary>
/// The solution state that the request should operate on.
/// </summary>
public readonly Solution Solution;
/// <summary>
/// The client capabilities for the request.
/// </summary>
public ClientCapabilities ClientCapabilities { get; }
public readonly ClientCapabilities ClientCapabilities;
/// <summary>
/// The LSP client making the request
/// </summary>
public string? ClientName { get; }
public readonly string? ClientName;
public RequestContext(ClientCapabilities clientCapabilities, string? clientName)
/// <summary>
/// The document that the request is for, if applicable. This comes from the <see cref="TextDocumentIdentifier"/> returned from the handler itself via a call to <see cref="IRequestHandler{RequestType, ResponseType}.GetTextDocumentIdentifier(RequestType)"/>.
/// </summary>
public readonly Document? Document;
public RequestContext(Solution solution, ClientCapabilities clientCapabilities, string? clientName, Document? document, Action<Solution>? solutionUpdater)
{
Document = document;
Solution = solution;
_solutionUpdater = solutionUpdater;
ClientCapabilities = clientCapabilities;
ClientName = clientName;
}
/// <summary>
/// Allows a mutating request to provide a new solution snapshot that all subsequent requests should use.
/// </summary>
public void UpdateSolution(Solution solution)
{
Contract.ThrowIfNull(_solutionUpdater, "Mutating solution not allowed in a non-mutating request handler");
_solutionUpdater.Invoke(solution);
}
}
}
// 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.
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal partial class RequestExecutionQueue
{
private readonly struct QueueItem
{
/// <summary>
/// Processes the queued request, and signals back to the called whether the handler ran to completion.
/// </summary>
/// <remarks>A return value of true does not imply that the request was handled successfully, only that no exception was thrown and the task wasn't cancelled.</remarks>
public readonly Func<RequestContext, CancellationToken, Task<bool>> CallbackAsync;
/// <inheritdoc cref="ExportLspMethodAttribute.MutatesSolutionState" />
public readonly bool MutatesSolutionState;
/// <inheritdoc cref="RequestContext.ClientName" />
public readonly string? ClientName;
/// <inheritdoc cref="RequestContext.ClientCapabilities" />
public readonly ClientCapabilities ClientCapabilities;
/// <summary>
/// The document identifier that will be used to find the solution and document for this request. This comes from the <see cref="TextDocumentIdentifier"/> returned from the handler itself via a call to <see cref="IRequestHandler{RequestType, ResponseType}.GetTextDocumentIdentifier(RequestType)"/>.
/// </summary>
public readonly TextDocumentIdentifier? TextDocument;
/// <summary>
/// A cancellation token that will cancel the handing of this request. The request could also be cancelled by the queue shutting down.
/// </summary>
public readonly CancellationToken CancellationToken;
public QueueItem(bool mutatesSolutionState, ClientCapabilities clientCapabilities, string? clientName, TextDocumentIdentifier? textDocument, Func<RequestContext, CancellationToken, Task<bool>> callbackAsync, CancellationToken cancellationToken)
{
MutatesSolutionState = mutatesSolutionState;
ClientCapabilities = clientCapabilities;
ClientName = clientName;
TextDocument = textDocument;
CallbackAsync = callbackAsync;
CancellationToken = cancellationToken;
}
}
}
}
// 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.
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
/// <summary>
/// Coordinates the exectution of LSP messages to ensure correct results are sent back.
/// </summary>
/// <remarks>
/// <para>
/// When a request comes in for some data the handler must be able to access a solution state that is correct
/// at the time of the request, that takes into account any text change requests that have come in previously
/// (via textDocument/didChange for example).
/// </para>
/// <para>
/// This class acheives this by distinguishing between mutating and non-mutating requests, and ensuring that
/// when a mutating request comes in, its processing blocks all subsequent requests. As each request comes in
/// it is added to a queue, and a queue item will not be retreived while a mutating request is running. Before
/// any request is handled the solution state is created by merging workspace solution state, which could have
/// changes from non-LSP means (eg, adding a project reference), with the current "mutated" state.
/// When a non-mutating work item is retrieved from the queue, it is given the current solution state, but then
/// run in a fire-and-forget fashion.
/// </para>
/// <para>
/// Regardless of whether a request is mutating or not, or blocking or not, is an implementation detail of this class
/// and any consumers observing the results of the task returned from <see cref="ExecuteAsync{TRequestType, TResponseType}(bool, IRequestHandler{TRequestType, TResponseType}, TRequestType, ClientCapabilities, string?, CancellationToken)"/>
/// will see the results of the handling of the request, whenever it occurred.
/// </para>
/// <para>
/// Exceptions in the handling of non-mutating requests are sent back to callers. Exceptions in the processing of
/// the queue will close the LSP connection so that the client can reconnect. Exceptions in the handling of mutating
/// requests will also close the LSP connection, as at that point the mutated solution is in an unknown state.
/// </para>
/// <para>
/// After shutdown is called, or an error causes the closing of the connection, the queue will not accept any
/// more messages, and a new queue will need to be created.
/// </para>
/// </remarks>
internal partial class RequestExecutionQueue
{
private readonly ILspSolutionProvider _solutionProvider;
private readonly AsyncQueue<QueueItem> _queue;
private readonly CancellationTokenSource _cancelSource;
/// <summary>
/// Raised when the execution queue has failed, or the solution state its tracking is in an unknown state
/// and so the only course of action is to shutdown the server so that the client re-connects and we can
/// start over again.
/// </summary>
/// <remarks>
/// Once this event has been fired all currently active and pending work items in the queue will be cancelled.
/// </remarks>
public event EventHandler<RequestShutdownEventArgs>? RequestServerShutdown;
public RequestExecutionQueue(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
_queue = new AsyncQueue<QueueItem>();
_cancelSource = new CancellationTokenSource();
// Start the queue processing
_ = ProcessQueueAsync();
}
/// <summary>
/// Shuts down the queue, stops accepting new messages, and cancels any in-progress or queued tasks. Calling
/// this multiple times won't cause any issues.
/// </summary>
public void Shutdown()
{
_cancelSource.Cancel();
DrainQueue();
}
/// <summary>
/// Queues a request to be handled by the specified handler, with mutating requests blocking subsequent requests
/// from starting until the mutation is complete.
/// </summary>
/// <param name="mutatesSolutionState">Whether or not handling this method results in changes to the current solution state.
/// Mutating requests will block all subsequent requests from starting until after they have
/// completed and mutations have been applied.</param>
/// <param name="handler">The handler that will handle the request.</param>
/// <param name="request">The request to handle.</param>
/// <param name="clientCapabilities">The client capabilities.</param>
/// <param name="clientName">The client name.</param>
/// <param name="requestCancellationToken">A cancellation token that will cancel the handing of this request.
/// The request could also be cancelled by the queue shutting down.</param>
/// <returns>A task that can be awaited to observe the results of the handing of this request.</returns>
public Task<TResponseType> ExecuteAsync<TRequestType, TResponseType>(
bool mutatesSolutionState,
IRequestHandler<TRequestType, TResponseType> handler,
TRequestType request,
ClientCapabilities clientCapabilities,
string? clientName,
CancellationToken requestCancellationToken) where TRequestType : class
{
// Create a task completion source that will represent the processing of this request to the caller
var completion = new TaskCompletionSource<TResponseType>();
// Note: If the queue is not accepting any more items then TryEnqueue below will fail.
var textDocument = handler.GetTextDocumentIdentifier(request);
var item = new QueueItem(mutatesSolutionState, clientCapabilities, clientName, textDocument,
callbackAsync: async (context, cancellationToken) =>
{
// Check if cancellation was requested while this was waiting in the queue
if (cancellationToken.IsCancellationRequested)
{
completion.SetCanceled();
// Tell the queue to ignore any mutations from this request, not that we've given it a chance
// to make any
return false;
}
try
{
var result = await handler.HandleRequestAsync(request, context, cancellationToken).ConfigureAwait(false);
completion.SetResult(result);
// Tell the queue that this was successful so that mutations (if any) can be applied
return true;
}
catch (OperationCanceledException ex)
{
completion.TrySetCanceled(ex.CancellationToken);
}
catch (Exception exception)
{
// Pass the exception to the task completion source, so the caller of the ExecuteAsync method can observe but
// don't let it escape from this callback, so it doesn't affect the queue processing.
completion.SetException(exception);
}
// Tell the queue to ignore any mutations from this request
return false;
}, requestCancellationToken);
var didEnqueue = _queue.TryEnqueue(item);
// If the queue has been shut down the enqueue will fail, so we just fault the task immediately.
// The queue itself is threadsafe (_queue.TryEnqueue and _queue.Complete use the same lock).
if (!didEnqueue)
{
completion.SetException(new InvalidOperationException("Server was requested to shut down."));
}
return completion.Task;
}
private async Task ProcessQueueAsync()
{
// Keep track of solution state modifications made by LSP requests
Solution? lastMutatedSolution = null;
try
{
while (!_cancelSource.IsCancellationRequested)
{
var work = await _queue.DequeueAsync().ConfigureAwait(false);
// Create a linked cancellation token to cancel any requests in progress when this shuts down
var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelSource.Token, work.CancellationToken).Token;
// The "current" solution can be updated by non-LSP actions, so we need it, but we also need
// to merge in the changes from any mutations that have been applied to open documents
var (document, solution) = _solutionProvider.GetDocumentAndSolution(work.TextDocument, work.ClientName);
solution = MergeChanges(solution, lastMutatedSolution);
if (work.MutatesSolutionState)
{
Solution? mutatedSolution = null;
var context = new RequestContext(solution, work.ClientCapabilities, work.ClientName, document, s => mutatedSolution = s);
// Mutating requests block other requests from starting to ensure an up to date snapshot is used.
var ranToCompletion = false;
try
{
ranToCompletion = await work.CallbackAsync(context, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Since the cancellationToken passed to the callback is a linked source it could be cancelled
// without the queue token being cancelled, but ranToCompletion will be false so no need to
// do anything special here.
RoslynDebug.Assert(ranToCompletion == false);
}
// If the handling of the request failed, the exception will bubble back up to the caller, but we
// still need to react to it here by throwing away solution updates
if (ranToCompletion)
{
lastMutatedSolution = mutatedSolution ?? lastMutatedSolution;
}
else
{
OnRequestServerShutdown($"An error occured processing a mutating request and the solution is in an invalid state. Check LSP client logs for any error information.");
break;
}
}
else
{
var context = new RequestContext(solution, work.ClientCapabilities, work.ClientName, document, null);
// Non mutating are fire-and-forget because they are by definition readonly. Any errors
// will be sent back to the client but we can still capture errors in queue processing
// via NFW, though these errors don't put us into a bad state as far as the rest of the queue goes.
_ = work.CallbackAsync(context, cancellationToken).ReportNonFatalErrorAsync();
}
}
}
catch (OperationCanceledException e) when (e.CancellationToken == _cancelSource.Token)
{
// If the queue is asked to shut down between the start of the while loop, and the Dequeue call
// we could end up here, but we don't want to report an error. The Shutdown call will take care of things.
}
catch (Exception e) when (FatalError.ReportWithoutCrash(e))
{
OnRequestServerShutdown($"Error occured processing queue: {e.Message}.");
}
}
private void OnRequestServerShutdown(string message)
{
RequestServerShutdown?.Invoke(this, new RequestShutdownEventArgs(message));
Shutdown();
}
/// <summary>
/// Cancels all requests in the queue and stops the queue from accepting any more requests. After this method
/// is called this queue is essentially useless.
/// </summary>
private void DrainQueue()
{
// Tell the queue not to accept any more items
_queue.Complete();
// Spin through the queue and pass in our cancelled token, so that the waiting tasks are cancelled.
// NOTE: This only really works because the first thing that CallbackAsync does is check for cancellation
// but generics make it annoying to store the TaskCompletionSource<TResult> on the QueueItem so this
// is the best we can do for now. Ideally we would manipulate the TaskCompletionSource directly here
// and just call SetCanceled
while (_queue.TryDequeue(out var item))
{
_ = item.CallbackAsync(default, new CancellationToken(true));
}
}
private static Solution MergeChanges(Solution solution, Solution? mutatedSolution)
{
// TODO: Merge in changes to the solution that have been received from didChange LSP methods
// https://github.com/dotnet/roslyn/issues/45427
return mutatedSolution ?? solution;
}
}
}
......@@ -2,22 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Threading;
using System.Threading.Tasks;
using System;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
internal abstract class AbstractRequestHandler<RequestType, ResponseType> : IRequestHandler<RequestType, ResponseType>
internal class RequestShutdownEventArgs : EventArgs
{
protected readonly ILspSolutionProvider SolutionProvider;
public string Message { get; }
protected AbstractRequestHandler(ILspSolutionProvider solutionProvider)
public RequestShutdownEventArgs(string message)
{
SolutionProvider = solutionProvider;
this.Message = message;
}
public abstract Task<ResponseType> HandleRequestAsync(RequestType request, RequestContext context, CancellationToken cancellationToken);
}
}
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
{
/// <summary>
/// Caches the semantic token information that needs to be preserved between multiple
/// semantic token requests.
/// Multiple token sets can be cached per document. The number of token sets cached
/// per document is determined by the _maxCachesPerDoc field.
/// </summary>
[Export(typeof(SemanticTokensCache)), Shared]
internal class SemanticTokensCache
{
/// <summary>
/// Maps an LSP token type to the index LSP associates with the token.
/// Required since we report tokens back to LSP as a series of ints,
/// and LSP needs a way to decipher them.
/// </summary>
public static readonly Dictionary<string, int> TokenTypeToIndex;
/// <summary>
/// Number of cached token sets we store per document. Must be >= 1.
/// </summary>
private readonly int _maxCachesPerDoc = 5;
/// <summary>
/// The next resultId available to use. Atomically incremented with Interlocked,
/// so this doesn't need to be protected by _semaphore.
/// </summary>
private long _nextResultId;
/// <summary>
/// Multiple cache requests or updates may be received concurrently.
/// We need this sempahore to ensure that we aren't making concurrent
/// modifications to the _tokens dictionary.
/// </summary>
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
#region protected by _semaphore
/// <summary>
/// Maps a document URI to its n most recently cached token sets.
/// </summary>
private readonly Dictionary<Uri, List<LSP.SemanticTokens>> _tokens =
new Dictionary<Uri, List<LSP.SemanticTokens>>();
#endregion
static SemanticTokensCache()
{
// Computes the mapping between a LSP token type and its respective index recognized by LSP.
TokenTypeToIndex = new Dictionary<string, int>();
var index = 0;
foreach (var lspTokenType in LSP.SemanticTokenTypes.AllTypes)
{
TokenTypeToIndex.Add(lspTokenType, index);
index++;
}
foreach (var roslynTokenType in SemanticTokensHelpers.RoslynCustomTokenTypes)
{
TokenTypeToIndex.Add(roslynTokenType, index);
index++;
}
}
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SemanticTokensCache()
{
}
/// <summary>
/// Updates the given document's token set cache. Removes old cache results if the document's
/// cache is full.
/// </summary>
public async Task UpdateCacheAsync(
Uri uri,
LSP.SemanticTokens tokens,
CancellationToken cancellationToken)
{
Contract.ThrowIfNull(tokens.ResultId);
using (await _semaphore.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
// Case 1: Document does not currently have any token sets cached. Create a cache
// for the document and return.
if (!_tokens.TryGetValue(uri, out var tokenSets))
{
_tokens.Add(uri, new List<LSP.SemanticTokens> { tokens });
return;
}
// Case 2: Document already has the maximum number of token sets cached. Remove the
// oldest token set from the cache, and then add the new token set (see case 3).
if (tokenSets.Count >= _maxCachesPerDoc)
{
tokenSets.RemoveAt(0);
}
// Case 3: Document has less than the maximum number of token sets cached.
// Add new token set to cache.
tokenSets.Add(tokens);
}
}
/// <summary>
/// Returns the cached tokens data for a given document URI and resultId.
/// Returns null if no match is found.
/// </summary>
public async Task<int[]?> GetCachedTokensDataAsync(
Uri uri,
string resultId,
CancellationToken cancellationToken)
{
using (await _semaphore.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
if (!_tokens.TryGetValue(uri, out var tokenSets))
{
return null;
}
// Return a non-null value only if the document's cache contains a token set with the resultId
// that the user is searching for.
return tokenSets.FirstOrDefault(t => t.ResultId == resultId)?.Data;
}
}
/// <summary>
/// Returns the next available resultId.
/// </summary>
public string GetNextResultId() => Interlocked.Increment(ref _nextResultId).ToString();
}
}
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Differencing;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
{
/// <summary>
/// Computes the semantic tokens edits for a file. An edit request is received every 500ms,
/// or every time an edit is made by the user.
/// </summary>
[ExportLspMethod(LSP.SemanticTokensMethods.TextDocumentSemanticTokensEditsName, mutatesSolutionState: false), Shared]
internal class SemanticTokensEditsHandler : IRequestHandler<LSP.SemanticTokensEditsParams, SumType<LSP.SemanticTokens, LSP.SemanticTokensEdits>>
{
private readonly SemanticTokensCache _tokensCache;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SemanticTokensEditsHandler(SemanticTokensCache tokensCache)
{
_tokensCache = tokensCache;
}
public TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.SemanticTokensEditsParams request) => request.TextDocument;
public async Task<SumType<LSP.SemanticTokens, LSP.SemanticTokensEdits>> HandleRequestAsync(
LSP.SemanticTokensEditsParams request,
RequestContext context,
CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Document, "Document is null.");
Contract.ThrowIfNull(request.TextDocument, "TextDocument is null.");
Contract.ThrowIfNull(request.PreviousResultId, "previousResultId is null.");
// Even though we want to ultimately pass edits back to LSP, we still need to compute all semantic tokens,
// both for caching purposes and in order to have a baseline comparison when computing the edits.
var newSemanticTokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
context.Document, SemanticTokensCache.TokenTypeToIndex,
range: null, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(newSemanticTokensData, "newSemanticTokensData is null.");
var resultId = _tokensCache.GetNextResultId();
var newSemanticTokens = new LSP.SemanticTokens { ResultId = resultId, Data = newSemanticTokensData };
await _tokensCache.UpdateCacheAsync(
request.TextDocument.Uri, newSemanticTokens, cancellationToken).ConfigureAwait(false);
// Getting the cached tokens for the document. If we don't have an applicable cached token set,
// we can't calculate edits, so we must return all semantic tokens instead.
var oldSemanticTokensData = await _tokensCache.GetCachedTokensDataAsync(
request.TextDocument.Uri, request.PreviousResultId, cancellationToken).ConfigureAwait(false);
if (oldSemanticTokensData == null)
{
return newSemanticTokens;
}
var edits = new SemanticTokensEdits
{
Edits = ComputeSemanticTokensEdits(oldSemanticTokensData, newSemanticTokensData),
ResultId = resultId
};
return edits;
}
/// <summary>
/// Compares two sets of SemanticTokens and returns the edits between them.
/// </summary>
private static LSP.SemanticTokensEdit[] ComputeSemanticTokensEdits(
int[] oldSemanticTokens,
int[] newSemanticTokens)
{
if (oldSemanticTokens.SequenceEqual(newSemanticTokens))
{
return Array.Empty<SemanticTokensEdit>();
}
// We use Roslyn's version of the Myers' Diff Algorithm to compute the minimal edits
// between the old and new tokens.
// Edits are computed by token (i.e. in sets of five integers), so if one value in the token
// is changed, the entire token is replaced. We do this instead of directly comparing each
// value in the token individually so that we can potentially save on computation costs, since
// we can return early if we find that one value in the token doesn't match. However, there
// are trade-offs since our insertions/deletions are usually larger.
// Turning arrays into tuples of five ints, each representing one token
var oldGroupedSemanticTokens = ConvertToGroupedSemanticTokens(oldSemanticTokens);
var newGroupedSemanticTokens = ConvertToGroupedSemanticTokens(newSemanticTokens);
var edits = LongestCommonSemanticTokensSubsequence.GetEdits(oldGroupedSemanticTokens, newGroupedSemanticTokens);
return ConvertToSemanticTokenEdits(newGroupedSemanticTokens, edits);
}
private static SemanticTokensEdit[] ConvertToSemanticTokenEdits(SemanticToken[] newGroupedSemanticTokens, IEnumerable<SequenceEdit> edits)
{
// Our goal is to minimize the number of edits we return to LSP. It's possible an index
// may have both an insertion and deletion, in which case we can combine the two into a
// single update. We use the dictionary below to keep track of whether an index contains
// an insertion, deletion, or both.
using var _ = PooledDictionary<int, SemanticTokenEditKind>.GetInstance(out var indexToEditKinds);
foreach (var edit in edits)
{
// We only care about EditKind.Insert and EditKind.Delete, since they encompass all
// changes to the document. All other EditKinds are ignored.
switch (edit.Kind)
{
case EditKind.Insert:
indexToEditKinds.TryGetValue(edit.NewIndex, out var editKindWithoutInsert);
Contract.ThrowIfTrue(editKindWithoutInsert == SemanticTokenEditKind.Insert, "There cannot be two inserts at the same position.");
Contract.ThrowIfTrue(editKindWithoutInsert == SemanticTokenEditKind.Update, "There cannot be an insert and update at the same position.");
indexToEditKinds[edit.NewIndex] = editKindWithoutInsert == SemanticTokenEditKind.None ? SemanticTokenEditKind.Insert : SemanticTokenEditKind.Update;
break;
case EditKind.Delete:
indexToEditKinds.TryGetValue(edit.OldIndex, out var editKindWithoutDelete);
Contract.ThrowIfTrue(editKindWithoutDelete == SemanticTokenEditKind.Delete, "There cannot be two deletions at the same position.");
Contract.ThrowIfTrue(editKindWithoutDelete == SemanticTokenEditKind.Update, "There cannot be a deletion and update at the same position.");
indexToEditKinds[edit.OldIndex] = editKindWithoutDelete == SemanticTokenEditKind.None ? SemanticTokenEditKind.Delete : SemanticTokenEditKind.Update;
break;
}
}
return CombineEditsIfPossible(newGroupedSemanticTokens, indexToEditKinds);
}
private static SemanticTokensEdit[] CombineEditsIfPossible(
SemanticToken[] newGroupedSemanticTokens,
Dictionary<int, SemanticTokenEditKind> indexToEditKinds)
{
// This method combines the edits into the minimal possible edits (for the most part).
// For example, if an index contains both an insertion and deletion, we combine the two
// edits into one.
// We also combine edits if we have consecutive edits of the same types, i.e.
// Delete->Delete, Insert->Insert, and Update->Update.
// Technically, we could combine Update->Insert, and Update->Delete, but those cases have
// special rules and would complicate the logic. They also generally do not result in a
// huge reduction in the total number of edits, so we leave them out for now.
using var _ = ArrayBuilder<LSP.SemanticTokensEdit>.GetInstance(out var semanticTokensEdits);
var editIndices = indexToEditKinds.Keys.ToArray();
// The indices in indexToEdit kinds are not guaranteed to be in chronological order when we
// extract them from the dictionary. We must sort the edit kinds by index since we need to
// know what kind of edits surround a given index in order to potentially combine them into
// one edit.
Array.Sort(editIndices);
// Example to give clarity to currentEditIndex and currentTokenIndex variables defined below:
// Non-grouped semantic tokens: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
// currentEditIndex: 0 1
// currentTokenIndex: 0 1 2
for (var currentEditIndex = 0; currentEditIndex < editIndices.Length; currentEditIndex++)
{
var currentTokenIndex = editIndices[currentEditIndex];
var initialEditKind = indexToEditKinds[currentTokenIndex];
var editStartPosition = currentTokenIndex * 5;
if (initialEditKind == SemanticTokenEditKind.Update)
{
currentEditIndex = AddUpdateEdit(
newGroupedSemanticTokens, indexToEditKinds, semanticTokensEdits, editIndices,
currentEditIndex, editStartPosition);
}
else if (initialEditKind == SemanticTokenEditKind.Insert)
{
currentEditIndex = AddInsertionEdit(
newGroupedSemanticTokens, indexToEditKinds, semanticTokensEdits, editIndices,
currentEditIndex, editStartPosition);
}
else
{
Contract.ThrowIfFalse(initialEditKind == SemanticTokenEditKind.Delete, "Expected initialEditKind to be SemanticTokenEditKind.Delete.");
currentEditIndex = AddDeletionEdit(
indexToEditKinds, semanticTokensEdits, editIndices, currentEditIndex, editStartPosition);
}
}
return semanticTokensEdits.ToArray();
// Local functions
static int AddUpdateEdit(
SemanticToken[] newGroupedSemanticTokens,
Dictionary<int, SemanticTokenEditKind> indexToEditKinds,
ArrayBuilder<SemanticTokensEdit> semanticTokensEdits,
int[] editIndices,
int startEditIndex,
int editStartPosition)
{
var _ = ArrayBuilder<int>.GetInstance(out var tokensToInsert);
// For simplicitly, we only allow an "update" (i.e. a dual insertion/deletion) to be
// combined with other updates.
var endEditIndex = GetCombinedEditEndIndex(
SemanticTokenEditKind.Update, indexToEditKinds, editIndices, startEditIndex);
var deleteCount = 5 * (1 + (endEditIndex - startEditIndex));
for (var i = 0; i <= endEditIndex - startEditIndex; i++)
{
newGroupedSemanticTokens[editIndices[startEditIndex + i]].AddToEnd(tokensToInsert);
}
semanticTokensEdits.Add(
GenerateEdit(start: editStartPosition, deleteCount: deleteCount, data: tokensToInsert.ToArray()));
return endEditIndex;
}
static int AddInsertionEdit(
SemanticToken[] newGroupedSemanticTokens,
Dictionary<int, SemanticTokenEditKind> indexToEditKinds,
ArrayBuilder<SemanticTokensEdit> semanticTokensEdits,
int[] editIndices,
int startEditIndex,
int editStartPosition)
{
var _ = ArrayBuilder<int>.GetInstance(out var tokensToInsert);
// An insert can only be combined with other inserts that directly follow it.
var endEditIndex = GetCombinedEditEndIndex(
SemanticTokenEditKind.Insert, indexToEditKinds, editIndices, startEditIndex);
for (var i = 0; i <= endEditIndex - startEditIndex; i++)
{
newGroupedSemanticTokens[editIndices[startEditIndex + i]].AddToEnd(tokensToInsert);
}
semanticTokensEdits.Add(
GenerateEdit(start: editStartPosition, deleteCount: 0, data: tokensToInsert.ToArray()));
return endEditIndex;
}
static int AddDeletionEdit(
Dictionary<int, SemanticTokenEditKind> indexToEditKinds,
ArrayBuilder<SemanticTokensEdit> semanticTokensEdits,
int[] editIndices,
int startEditIndex,
int editStartPosition)
{
// A deletion can only be combined with other deletions that directly follow it.
var endEditNumber = GetCombinedEditEndIndex(
SemanticTokenEditKind.Delete, indexToEditKinds, editIndices, startEditIndex);
var deleteCount = 5 * (1 + (endEditNumber - startEditIndex));
semanticTokensEdits.Add(
GenerateEdit(start: editStartPosition, deleteCount: deleteCount, data: Array.Empty<int>()));
return endEditNumber;
}
// Returns the updated ordered edit number after we know how many edits we can combine.
static int GetCombinedEditEndIndex(
SemanticTokenEditKind editKind,
Dictionary<int, SemanticTokenEditKind> indexToEditKinds,
int[] editIndices,
int currentEditIndex)
{
// To continue combining edits, we need to ensure:
// 1) There is an edit following the current edit.
// 2) The current and next edits involve tokens that are located right next to
// each other in the file.
// 3) The next edit is the same type as the current edit.
while (currentEditIndex + 1 < editIndices.Length &&
indexToEditKinds[editIndices[currentEditIndex + 1]] == editKind &&
editIndices[currentEditIndex + 1] == editIndices[currentEditIndex] + 1)
{
currentEditIndex++;
}
return currentEditIndex;
}
}
/// <summary>
/// Converts an array of individual semantic token values to an array of values grouped
/// together by semantic token.
/// </summary>
private static SemanticToken[] ConvertToGroupedSemanticTokens(int[] tokens)
{
Contract.ThrowIfTrue(tokens.Length % 5 != 0, $"Tokens length should be divisible by 5. Actual length: {tokens.Length}");
using var _ = ArrayBuilder<SemanticToken>.GetInstance(out var fullTokens);
for (var i = 0; i < tokens.Length; i += 5)
{
fullTokens.Add(new SemanticToken(tokens[i], tokens[i + 1], tokens[i + 2], tokens[i + 3], tokens[i + 4]));
}
return fullTokens.ToArray();
}
internal static LSP.SemanticTokensEdit GenerateEdit(int start, int deleteCount, int[] data)
=> new LSP.SemanticTokensEdit
{
Start = start,
DeleteCount = deleteCount,
Data = data
};
private sealed class LongestCommonSemanticTokensSubsequence : LongestCommonSubsequence<SemanticToken[]>
{
private static readonly LongestCommonSemanticTokensSubsequence s_instance = new LongestCommonSemanticTokensSubsequence();
protected override bool ItemsEqual(
SemanticToken[] oldSemanticTokens, int oldIndex,
SemanticToken[] newSemanticTokens, int newIndex)
=> oldSemanticTokens[oldIndex].Equals(newSemanticTokens[newIndex]);
public static IEnumerable<SequenceEdit> GetEdits(
SemanticToken[] oldSemanticTokens, SemanticToken[] newSemanticTokens)
=> s_instance.GetEdits(oldSemanticTokens, oldSemanticTokens.Length, newSemanticTokens, newSemanticTokens.Length);
}
/// <summary>
/// Stores the values that make up the LSP representation of an individual semantic token.
/// </summary>
#pragma warning disable CA1067 // Override Object.Equals(object) when implementing IEquatable<T>
private readonly struct SemanticToken : IEquatable<SemanticToken>
#pragma warning restore CA1067 // Override Object.Equals(object) when implementing IEquatable<T>
{
private readonly int _deltaLine;
private readonly int _deltaStartCharacter;
private readonly int _length;
private readonly int _tokenType;
private readonly int _tokenModifiers;
public SemanticToken(int deltaLine, int deltaStartCharacter, int length, int tokenType, int tokenModifiers)
{
_deltaLine = deltaLine;
_deltaStartCharacter = deltaStartCharacter;
_length = length;
_tokenType = tokenType;
_tokenModifiers = tokenModifiers;
}
public void AddToEnd(ArrayBuilder<int> tokensToInsert)
{
tokensToInsert.Add(_deltaLine);
tokensToInsert.Add(_deltaStartCharacter);
tokensToInsert.Add(_length);
tokensToInsert.Add(_tokenType);
tokensToInsert.Add(_tokenModifiers);
}
public bool Equals(SemanticToken otherToken)
{
return _deltaLine == otherToken._deltaLine &&
_deltaStartCharacter == otherToken._deltaStartCharacter &&
_length == otherToken._length &&
_tokenType == otherToken._tokenType &&
_tokenModifiers == otherToken._tokenModifiers;
}
}
private enum SemanticTokenEditKind
{
None = 0,
Insert = 1,
Delete = 2,
Update = 3
}
}
}
// 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.
#nullable enable
using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
{
/// <summary>
/// Computes the semantic tokens for a whole document.
/// </summary>
/// <remarks>
/// This handler is invoked when a user opens a file. Depending on the size of the file, the full token set may be
/// slow to compute, so the <see cref="SemanticTokensRangeHandler"/> is also called when a file is opened in order
/// to render UI results quickly until this handler finishes running.
/// Unlike the range handler, the whole document handler may be called again if the LSP client finds an edit that
/// is difficult to correctly apply to their tags cache. This allows for reliable recovery from errors and accounts
/// for limitations in the edits application logic.
/// </remarks>
[ExportLspMethod(LSP.SemanticTokensMethods.TextDocumentSemanticTokensName, mutatesSolutionState: false), Shared]
internal class SemanticTokensHandler : IRequestHandler<LSP.SemanticTokensParams, LSP.SemanticTokens>
{
private readonly SemanticTokensCache _tokensCache;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SemanticTokensHandler(SemanticTokensCache tokensCache)
{
_tokensCache = tokensCache;
}
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.SemanticTokensParams request) => request.TextDocument;
public async Task<LSP.SemanticTokens> HandleRequestAsync(
LSP.SemanticTokensParams request,
RequestContext context,
CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Document, "Document is null.");
Contract.ThrowIfNull(request.TextDocument, "TextDocument is null.");
var resultId = _tokensCache.GetNextResultId();
var tokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
context.Document, SemanticTokensCache.TokenTypeToIndex,
range: null, cancellationToken).ConfigureAwait(false);
var tokens = new LSP.SemanticTokens { ResultId = resultId, Data = tokensData };
await _tokensCache.UpdateCacheAsync(request.TextDocument.Uri, tokens, cancellationToken).ConfigureAwait(false);
return tokens;
}
}
}
// 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.
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
{
internal class SemanticTokensHelpers
{
internal static readonly string[] RoslynCustomTokenTypes =
{
ClassificationTypeNames.ConstantName,
ClassificationTypeNames.ControlKeyword,
ClassificationTypeNames.DelegateName,
ClassificationTypeNames.ExcludedCode,
ClassificationTypeNames.ExtensionMethodName,
ClassificationTypeNames.FieldName,
ClassificationTypeNames.LabelName,
ClassificationTypeNames.LocalName,
ClassificationTypeNames.MethodName,
ClassificationTypeNames.ModuleName,
ClassificationTypeNames.OperatorOverloaded,
// Preprocessor
ClassificationTypeNames.PreprocessorKeyword,
ClassificationTypeNames.PreprocessorText,
ClassificationTypeNames.Punctuation,
// Regex
ClassificationTypeNames.RegexAlternation,
ClassificationTypeNames.RegexAnchor,
ClassificationTypeNames.RegexCharacterClass,
ClassificationTypeNames.RegexComment,
ClassificationTypeNames.RegexGrouping,
ClassificationTypeNames.RegexOtherEscape,
ClassificationTypeNames.RegexQuantifier,
ClassificationTypeNames.RegexSelfEscapedCharacter,
ClassificationTypeNames.RegexText,
ClassificationTypeNames.StringEscapeCharacter,
ClassificationTypeNames.Text,
ClassificationTypeNames.VerbatimStringLiteral,
ClassificationTypeNames.WhiteSpace,
// XML
ClassificationTypeNames.XmlDocCommentAttributeName,
ClassificationTypeNames.XmlDocCommentAttributeQuotes,
ClassificationTypeNames.XmlDocCommentAttributeValue,
ClassificationTypeNames.XmlDocCommentCDataSection,
ClassificationTypeNames.XmlDocCommentComment,
ClassificationTypeNames.XmlDocCommentDelimiter,
ClassificationTypeNames.XmlDocCommentEntityReference,
ClassificationTypeNames.XmlDocCommentName,
ClassificationTypeNames.XmlDocCommentProcessingInstruction,
ClassificationTypeNames.XmlDocCommentText,
ClassificationTypeNames.XmlLiteralAttributeName,
ClassificationTypeNames.XmlLiteralAttributeQuotes,
ClassificationTypeNames.XmlLiteralAttributeValue,
ClassificationTypeNames.XmlLiteralCDataSection,
ClassificationTypeNames.XmlLiteralComment,
ClassificationTypeNames.XmlLiteralDelimiter,
ClassificationTypeNames.XmlLiteralEmbeddedExpression,
ClassificationTypeNames.XmlLiteralEntityReference,
ClassificationTypeNames.XmlLiteralName,
ClassificationTypeNames.XmlLiteralProcessingInstruction,
ClassificationTypeNames.XmlLiteralText
};
// TO-DO: Expand this mapping once support for custom token types is added:
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1085998
private static readonly Dictionary<string, string> s_classificationTypeToSemanticTokenTypeMap =
new Dictionary<string, string>
{
[ClassificationTypeNames.ClassName] = LSP.SemanticTokenTypes.Class,
[ClassificationTypeNames.Comment] = LSP.SemanticTokenTypes.Comment,
[ClassificationTypeNames.EnumMemberName] = LSP.SemanticTokenTypes.EnumMember,
[ClassificationTypeNames.EnumName] = LSP.SemanticTokenTypes.Enum,
[ClassificationTypeNames.EventName] = LSP.SemanticTokenTypes.Event,
[ClassificationTypeNames.Identifier] = LSP.SemanticTokenTypes.Variable,
[ClassificationTypeNames.InterfaceName] = LSP.SemanticTokenTypes.Interface,
[ClassificationTypeNames.Keyword] = LSP.SemanticTokenTypes.Keyword,
[ClassificationTypeNames.NamespaceName] = LSP.SemanticTokenTypes.Namespace,
[ClassificationTypeNames.NumericLiteral] = LSP.SemanticTokenTypes.Number,
[ClassificationTypeNames.Operator] = LSP.SemanticTokenTypes.Operator,
[ClassificationTypeNames.ParameterName] = LSP.SemanticTokenTypes.Parameter,
[ClassificationTypeNames.PropertyName] = LSP.SemanticTokenTypes.Property,
[ClassificationTypeNames.StringLiteral] = LSP.SemanticTokenTypes.String,
[ClassificationTypeNames.StructName] = LSP.SemanticTokenTypes.Struct,
[ClassificationTypeNames.TypeParameterName] = LSP.SemanticTokenTypes.TypeParameter,
};
/// <summary>
/// Returns the semantic tokens data for a given document with an optional range.
/// </summary>
internal static async Task<int[]> ComputeSemanticTokensDataAsync(
Document document,
Dictionary<string, int> tokenTypesToIndex,
LSP.Range? range,
CancellationToken cancellationToken)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
// By default we calculate the tokens for the full document span, although the user
// can pass in a range if they wish.
var textSpan = range == null ? root.FullSpan : ProtocolConversions.RangeToTextSpan(range, text);
var classifiedSpans = await Classifier.GetClassifiedSpansAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(classifiedSpans, "classifiedSpans is null");
// TO-DO: We should implement support for streaming once this LSP bug is fixed:
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1132601
return ComputeTokens(text.Lines, classifiedSpans.ToArray(), tokenTypesToIndex);
}
private static int[] ComputeTokens(
TextLineCollection lines,
ClassifiedSpan[] classifiedSpans,
Dictionary<string, int> tokenTypesToIndex)
{
using var _ = ArrayBuilder<int>.GetInstance(classifiedSpans.Length, out var data);
// We keep track of the last line number and last start character since tokens are
// reported relative to each other.
var lastLineNumber = 0;
var lastStartCharacter = 0;
for (var currentClassifiedSpanIndex = 0; currentClassifiedSpanIndex < classifiedSpans.Length; currentClassifiedSpanIndex++)
{
currentClassifiedSpanIndex = ComputeNextToken(
lines, ref lastLineNumber, ref lastStartCharacter, classifiedSpans,
currentClassifiedSpanIndex, tokenTypesToIndex,
out var deltaLine, out var startCharacterDelta, out var tokenLength,
out var tokenType, out var tokenModifiers);
data.AddRange(deltaLine, startCharacterDelta, tokenLength, tokenType, tokenModifiers);
}
return data.ToArray();
}
private static int ComputeNextToken(
TextLineCollection lines,
ref int lastLineNumber,
ref int lastStartCharacter,
ClassifiedSpan[] classifiedSpans,
int currentClassifiedSpanIndex,
Dictionary<string, int> tokenTypesToIndex,
out int deltaLineOut,
out int startCharacterDeltaOut,
out int tokenLengthOut,
out int tokenTypeOut,
out int tokenModifiersOut)
{
// Each semantic token is represented in LSP by five numbers:
// 1. Token line number delta, relative to the previous token
// 2. Token start character delta, relative to the previous token
// 3. Token length
// 4. Token type (index) - looked up in SemanticTokensLegend.tokenTypes
// 5. Token modifiers - each set bit will be looked up in SemanticTokensLegend.tokenModifiers
var classifiedSpan = classifiedSpans[currentClassifiedSpanIndex];
var originalTextSpan = classifiedSpan.TextSpan;
var linePosition = lines.GetLinePositionSpan(originalTextSpan).Start;
var lineNumber = linePosition.Line;
// 1. Token line number delta, relative to the previous token
var deltaLine = lineNumber - lastLineNumber;
Contract.ThrowIfTrue(deltaLine < 0, $"deltaLine is less than 0: {deltaLine}");
// 2. Token start character delta, relative to the previous token
// (Relative to 0 or the previous token’s start if they're on the same line)
var deltaStartCharacter = linePosition.Character;
if (lastLineNumber == lineNumber)
{
deltaStartCharacter -= lastStartCharacter;
}
lastLineNumber = lineNumber;
lastStartCharacter = linePosition.Character;
// 3. Token length
var tokenLength = originalTextSpan.Length;
// We currently only have one modifier (static). The logic below will need to change in the future if other
// modifiers are added in the future.
var modifierBits = TokenModifiers.None;
var tokenTypeIndex = 0;
// Classified spans with the same text span should be combined into one token.
while (classifiedSpans[currentClassifiedSpanIndex].TextSpan == originalTextSpan)
{
var classificationType = classifiedSpans[currentClassifiedSpanIndex].ClassificationType;
if (classificationType != ClassificationTypeNames.StaticSymbol)
{
// 4. Token type - looked up in SemanticTokensLegend.tokenTypes (language server defined mapping
// from integer to LSP token types).
tokenTypeIndex = GetTokenTypeIndex(classificationType, tokenTypesToIndex);
}
else
{
// 5. Token modifiers - each set bit will be looked up in SemanticTokensLegend.tokenModifiers
modifierBits = TokenModifiers.Static;
}
// Break out of the loop if we have no more classified spans left, or if the next classified span has
// a different text span than our current text span.
if (currentClassifiedSpanIndex + 1 >= classifiedSpans.Length || classifiedSpans[currentClassifiedSpanIndex + 1].TextSpan != originalTextSpan)
{
break;
}
currentClassifiedSpanIndex++;
}
deltaLineOut = deltaLine;
startCharacterDeltaOut = deltaStartCharacter;
tokenLengthOut = tokenLength;
tokenTypeOut = tokenTypeIndex;
tokenModifiersOut = (int)modifierBits;
return currentClassifiedSpanIndex;
}
private static int GetTokenTypeIndex(string classificationType, Dictionary<string, int> tokenTypesToIndex)
{
if (!s_classificationTypeToSemanticTokenTypeMap.TryGetValue(classificationType, out var tokenTypeStr))
{
tokenTypeStr = classificationType;
}
Contract.ThrowIfFalse(tokenTypesToIndex.TryGetValue(tokenTypeStr, out var tokenTypeIndex), "No matching token type index found.");
return tokenTypeIndex;
}
}
}
// 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.
#nullable enable
using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
{
/// <summary>
/// Computes the semantic tokens for a given range.
/// </summary>
/// <remarks>
/// When a user opens a file, it can be beneficial to only compute the semantic tokens for the visible range
/// for faster UI rendering.
/// The range handler is only invoked when a file is opened. When the first whole document request completes
/// via <see cref="SemanticTokensHandler"/>, the range handler is not invoked again for the rest of the session.
/// </remarks>
[ExportLspMethod(LSP.SemanticTokensMethods.TextDocumentSemanticTokensRangeName, mutatesSolutionState: false), Shared]
internal class SemanticTokensRangeHandler : IRequestHandler<LSP.SemanticTokensRangeParams, LSP.SemanticTokens>
{
private readonly SemanticTokensCache _tokensCache;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SemanticTokensRangeHandler(SemanticTokensCache tokensCache)
{
_tokensCache = tokensCache;
}
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.SemanticTokensRangeParams request) => request.TextDocument;
public async Task<LSP.SemanticTokens> HandleRequestAsync(
LSP.SemanticTokensRangeParams request,
RequestContext context,
CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Document, "TextDocument is null.");
var resultId = _tokensCache.GetNextResultId();
// The results from the range handler should not be cached since we don't want to cache
// partial token results. In addition, a range request is only ever called with a whole
// document request, so caching range results is unnecessary since the whole document
// handler will cache the results anyway.
var tokensData = await SemanticTokensHelpers.ComputeSemanticTokensDataAsync(
context.Document, SemanticTokensCache.TokenTypeToIndex,
request.Range, cancellationToken).ConfigureAwait(false);
return new LSP.SemanticTokens { ResultId = resultId, Data = tokensData };
}
}
}
// 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.
#nullable enable
using System;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens
{
/// <summary>
/// The LSP modifiers from <see cref="VisualStudio.LanguageServer.Protocol.SemanticTokenModifiers"/>
/// Roslyn currently supports. Enum is used to signify the modifier(s) that apply to a given token.
/// </summary>
[Flags]
internal enum TokenModifiers
{
None = 0,
Static = 1,
}
}
......@@ -20,20 +20,21 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentSignatureHelpName)]
internal class SignatureHelpHandler : AbstractRequestHandler<LSP.TextDocumentPositionParams, LSP.SignatureHelp>
[ExportLspMethod(LSP.Methods.TextDocumentSignatureHelpName, mutatesSolutionState: false)]
internal class SignatureHelpHandler : IRequestHandler<LSP.TextDocumentPositionParams, LSP.SignatureHelp>
{
private readonly IEnumerable<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> _allProviders;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SignatureHelpHandler([ImportMany] IEnumerable<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> allProviders, ILspSolutionProvider solutionProvider)
: base(solutionProvider)
public SignatureHelpHandler([ImportMany] IEnumerable<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> allProviders)
=> _allProviders = allProviders;
public override async Task<LSP.SignatureHelp> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.TextDocumentPositionParams request) => request.TextDocument;
public async Task<LSP.SignatureHelp> HandleRequestAsync(LSP.TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return new LSP.SignatureHelp();
......
......@@ -20,18 +20,20 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentDocumentSymbolName)]
internal class DocumentSymbolsHandler : AbstractRequestHandler<DocumentSymbolParams, object[]>
[ExportLspMethod(Methods.TextDocumentDocumentSymbolName, mutatesSolutionState: false)]
internal class DocumentSymbolsHandler : IRequestHandler<DocumentSymbolParams, object[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DocumentSymbolsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public DocumentSymbolsHandler()
{
}
public override async Task<object[]> HandleRequestAsync(DocumentSymbolParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentSymbolParams request) => request.TextDocument;
public async Task<object[]> HandleRequestAsync(DocumentSymbolParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return Array.Empty<SymbolInformation>();
......
......@@ -17,18 +17,21 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.WorkspaceSymbolName)]
internal class WorkspaceSymbolsHandler : AbstractRequestHandler<WorkspaceSymbolParams, SymbolInformation[]>
[ExportLspMethod(Methods.WorkspaceSymbolName, mutatesSolutionState: false)]
internal class WorkspaceSymbolsHandler : IRequestHandler<WorkspaceSymbolParams, SymbolInformation[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public WorkspaceSymbolsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public WorkspaceSymbolsHandler()
{
}
public override async Task<SymbolInformation[]> HandleRequestAsync(WorkspaceSymbolParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(WorkspaceSymbolParams request) => null;
public async Task<SymbolInformation[]> HandleRequestAsync(WorkspaceSymbolParams request, RequestContext context, CancellationToken cancellationToken)
{
var solution = SolutionProvider.GetCurrentSolutionForMainWorkspace();
var solution = context.Solution;
var searchTasks = Task.WhenAll(solution.Projects.Select(project => SearchProjectAsync(project, request, cancellationToken)));
return (await searchTasks.ConfigureAwait(false)).SelectMany(s => s).ToArray();
......
......@@ -6,15 +6,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer
{
......
......@@ -110,7 +110,8 @@ void M()
VSCodeAction unresolvedCodeAction,
LSP.ClientCapabilities clientCapabilities = null)
{
var result = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.VSCodeAction, LSP.VSCodeAction>(
var queue = CreateRequestQueue(solution);
var result = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.VSCodeAction, LSP.VSCodeAction>(queue,
LSP.MSLSPMethods.TextDocumentCodeActionResolveName, unresolvedCodeAction,
clientCapabilities, null, CancellationToken.None);
return result;
......
......@@ -192,7 +192,8 @@ void M()
LSP.Location caret,
LSP.ClientCapabilities clientCapabilities = null)
{
var result = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CodeActionParams, LSP.VSCodeAction[]>(
var queue = CreateRequestQueue(solution);
var result = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CodeActionParams, LSP.VSCodeAction[]>(queue,
LSP.Methods.TextDocumentCodeActionName, CreateCodeActionParams(caret),
clientCapabilities, null, CancellationToken.None);
return result;
......
......@@ -41,8 +41,11 @@ void M()
}
private static async Task<object> RunResolveCompletionItemAsync(Solution solution, LSP.CompletionItem completionItem, LSP.ClientCapabilities clientCapabilities = null)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CompletionItem, LSP.CompletionItem>(LSP.Methods.TextDocumentCompletionResolveName,
completionItem, clientCapabilities, null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CompletionItem, LSP.CompletionItem>(queue, LSP.Methods.TextDocumentCompletionResolveName,
completionItem, clientCapabilities, null, CancellationToken.None);
}
private static LSP.VSCompletionItem CreateResolvedCompletionItem(string text, LSP.CompletionItemKind kind, string[] tags, LSP.CompletionParams requestParameters,
ClassifiedTextElement description, string detail, string documentation, string[] commitCharacters = null)
......
......@@ -128,7 +128,8 @@ void M()
private static async Task<LSP.CompletionList> RunGetCompletionsAsync(Solution solution, LSP.CompletionParams completionParams)
{
var clientCapabilities = new LSP.VSClientCapabilities { SupportsVisualStudioExtensions = true };
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CompletionParams, LSP.CompletionList>(LSP.Methods.TextDocumentCompletionName,
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CompletionParams, LSP.CompletionList>(queue, LSP.Methods.TextDocumentCompletionName,
completionParams, clientCapabilities, null, CancellationToken.None);
}
}
......
......@@ -103,7 +103,10 @@ void M()
}
private static async Task<LSP.Location[]> RunGotoDefinitionAsync(Solution solution, LSP.Location caret)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(LSP.Methods.TextDocumentDefinitionName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(queue, LSP.Methods.TextDocumentDefinitionName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
}
}
......@@ -75,7 +75,10 @@ class B
}
private static async Task<LSP.Location[]> RunGotoTypeDefinitionAsync(Solution solution, LSP.Location caret)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(LSP.Methods.TextDocumentTypeDefinitionName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(queue, LSP.Methods.TextDocumentTypeDefinitionName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
}
}
......@@ -69,7 +69,8 @@ private static async Task<LSP.FoldingRange[]> RunGetFoldingRangeAsync(Solution s
TextDocument = CreateTextDocumentIdentifier(new Uri(document.FilePath))
};
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.FoldingRangeParams, LSP.FoldingRange[]>(LSP.Methods.TextDocumentFoldingRangeName,
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.FoldingRangeParams, LSP.FoldingRange[]>(queue, LSP.Methods.TextDocumentFoldingRangeName,
request, new LSP.ClientCapabilities(), null, CancellationToken.None);
}
......
......@@ -45,9 +45,12 @@ void M()
}
private static async Task<LSP.TextEdit[]> RunFormatDocumentOnTypeAsync(Solution solution, string characterTyped, LSP.Location locationTyped)
=> await GetLanguageServer(solution)
.ExecuteRequestAsync<LSP.DocumentOnTypeFormattingParams, LSP.TextEdit[]>(LSP.Methods.TextDocumentOnTypeFormattingName,
CreateDocumentOnTypeFormattingParams(characterTyped, locationTyped), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution)
.ExecuteRequestAsync<LSP.DocumentOnTypeFormattingParams, LSP.TextEdit[]>(queue, LSP.Methods.TextDocumentOnTypeFormattingName,
CreateDocumentOnTypeFormattingParams(characterTyped, locationTyped), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private static LSP.DocumentOnTypeFormattingParams CreateDocumentOnTypeFormattingParams(string characterTyped, LSP.Location locationTyped)
=> new LSP.DocumentOnTypeFormattingParams()
......
......@@ -42,8 +42,11 @@ void M()
}
private static async Task<LSP.TextEdit[]> RunFormatDocumentRangeAsync(Solution solution, LSP.Location location)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentRangeFormattingParams, LSP.TextEdit[]>(LSP.Methods.TextDocumentRangeFormattingName,
CreateDocumentRangeFormattingParams(location), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentRangeFormattingParams, LSP.TextEdit[]>(queue, LSP.Methods.TextDocumentRangeFormattingName,
CreateDocumentRangeFormattingParams(location), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private static LSP.DocumentRangeFormattingParams CreateDocumentRangeFormattingParams(LSP.Location location)
=> new LSP.DocumentRangeFormattingParams()
......
......@@ -43,8 +43,11 @@ void M()
}
private static async Task<LSP.TextEdit[]> RunFormatDocumentAsync(Solution solution, Uri uri)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentFormattingParams, LSP.TextEdit[]>(LSP.Methods.TextDocumentFormattingName,
CreateDocumentFormattingParams(uri), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentFormattingParams, LSP.TextEdit[]>(queue, LSP.Methods.TextDocumentFormattingName,
CreateDocumentFormattingParams(uri), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private static LSP.DocumentFormattingParams CreateDocumentFormattingParams(Uri uri)
=> new LSP.DocumentFormattingParams()
......
......@@ -61,7 +61,8 @@ void M()
private static async Task<LSP.DocumentHighlight[]> RunGetDocumentHighlightAsync(Solution solution, LSP.Location caret)
{
var results = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.DocumentHighlight[]>(LSP.Methods.TextDocumentDocumentHighlightName,
var queue = CreateRequestQueue(solution);
var results = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.DocumentHighlight[]>(queue, LSP.Methods.TextDocumentDocumentHighlightName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
Array.Sort(results, (h1, h2) =>
{
......
......@@ -190,8 +190,11 @@ static void Main(string[] args)
}
private static async Task<LSP.VSHover> RunGetHoverAsync(Solution solution, LSP.Location caret, ProjectId projectContext = null)
=> (LSP.VSHover)await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Hover>(LSP.Methods.TextDocumentHoverName,
CreateTextDocumentPositionParams(caret, projectContext), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return (LSP.VSHover)await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Hover>(queue, LSP.Methods.TextDocumentHoverName,
CreateTextDocumentPositionParams(caret, projectContext), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private void VerifyContent(LSP.VSHover result, string expectedContent)
{
......
......@@ -23,8 +23,11 @@ public async Task TestInitializeAsync()
}
private static async Task<LSP.InitializeResult> RunInitializeAsync(Solution solution, LSP.InitializeParams request)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.InitializeParams, LSP.InitializeResult>(LSP.Methods.InitializeName,
request, new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.InitializeParams, LSP.InitializeResult>(queue, LSP.Methods.InitializeName,
request, new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private static void AssertServerCapabilities(LSP.ServerCapabilities actual)
{
......
......@@ -204,9 +204,11 @@ private async Task VerifyNoResult(string characterTyped, string markup)
}
private static async Task<LSP.DocumentOnAutoInsertResponseItem[]> RunOnAutoInsertAsync(Solution solution, string characterTyped, LSP.Location locationTyped)
=> await GetLanguageServer(solution)
.ExecuteRequestAsync<LSP.DocumentOnAutoInsertParams, LSP.DocumentOnAutoInsertResponseItem[]>(MSLSPMethods.OnAutoInsertName,
CreateDocumentOnAutoInsertParams(characterTyped, locationTyped), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentOnAutoInsertParams, LSP.DocumentOnAutoInsertResponseItem[]>(queue, MSLSPMethods.OnAutoInsertName,
CreateDocumentOnAutoInsertParams(characterTyped, locationTyped), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private static LSP.DocumentOnAutoInsertParams CreateDocumentOnAutoInsertParams(string characterTyped, LSP.Location locationTyped)
=> new LSP.DocumentOnAutoInsertParams()
......
// 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.Handler;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: true), PartNotDiscoverable]
internal class FailingMutatingRequestHandler : IRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(FailingMutatingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FailingMutatingRequestHandler()
{
}
public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequest request) => null;
public async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
// Mutate the solution
var solution = context.Solution;
solution = solution.WithNewWorkspace(solution.Workspace, solution.WorkspaceVersion + 1);
context.UpdateSolution(solution);
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException();
}
}
}
// 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.Handler;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: false), PartNotDiscoverable]
internal class FailingRequestHandler : IRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(FailingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FailingRequestHandler()
{
}
public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequest request) => null;
public async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException();
}
}
}
// 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.Handler;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: true), PartNotDiscoverable]
internal class MutatingRequestHandler : IRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(MutatingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public MutatingRequestHandler()
{
}
public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequest request) => null;
public async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
var response = new TestResponse
{
Solution = context.Solution,
RequestOrder = request.RequestOrder,
StartTime = DateTime.UtcNow
};
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
// Mutate the solution
var solution = context.Solution;
solution = solution.WithNewWorkspace(solution.Workspace, solution.WorkspaceVersion + 1);
context.UpdateSolution(solution);
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
response.EndTime = DateTime.UtcNow;
return response;
}
}
}
// 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.Handler;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
[Shared, ExportLspMethod(MethodName, mutatesSolutionState: false), PartNotDiscoverable]
internal class NonMutatingRequestHandler : IRequestHandler<TestRequest, TestResponse>
{
public const string MethodName = nameof(NonMutatingRequestHandler);
private const int Delay = 100;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public NonMutatingRequestHandler()
{
}
public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequest request) => null;
public async Task<TestResponse> HandleRequestAsync(TestRequest request, RequestContext context, CancellationToken cancellationToken)
{
var response = new TestResponse();
response.Solution = context.Solution;
response.RequestOrder = request.RequestOrder;
response.StartTime = DateTime.UtcNow;
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
// some busy work
response.ToString();
await Task.Delay(Delay, cancellationToken).ConfigureAwait(false);
response.EndTime = DateTime.UtcNow;
return response;
}
}
}
// 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.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
public partial class RequestOrderingTests : AbstractLanguageServerProtocolTests
{
protected override TestComposition Composition => base.Composition
.AddParts(typeof(MutatingRequestHandler))
.AddParts(typeof(NonMutatingRequestHandler))
.AddParts(typeof(FailingRequestHandler))
.AddParts(typeof(FailingMutatingRequestHandler));
[Fact]
public async Task MutatingRequestsDontOverlap()
{
var requests = new[] {
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// Every request should have started at or after the one before it
Assert.True(responses[1].StartTime >= responses[0].EndTime);
Assert.True(responses[2].StartTime >= responses[1].EndTime);
}
[Fact]
public async Task NonMutatingRequestsOverlap()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// Every request should have started immediately, without waiting
Assert.True(responses[1].StartTime < responses[0].EndTime);
Assert.True(responses[2].StartTime < responses[1].EndTime);
}
[Fact]
public async Task NonMutatingWaitsForMutating()
{
var requests = new[] {
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// The non mutating tasks should have waited for the first task to finish
Assert.True(responses[1].StartTime >= responses[0].EndTime);
Assert.True(responses[2].StartTime >= responses[0].EndTime);
// The non mutating requests shouldn't have waited for each other
Assert.True(responses[1].StartTime < responses[2].EndTime);
Assert.True(responses[2].StartTime < responses[1].EndTime);
}
[Fact]
public async Task MutatingDoesntWaitForNonMutating()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// All tasks should start without waiting for any to finish
Assert.True(responses[1].StartTime < responses[0].EndTime);
Assert.True(responses[2].StartTime < responses[0].EndTime);
Assert.True(responses[1].StartTime < responses[2].EndTime);
Assert.True(responses[2].StartTime < responses[1].EndTime);
}
[Fact]
public async Task NonMutatingOperatesOnTheRightSolutions()
{
var requests = new[] {
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var responses = await TestAsync(requests);
// first two tasks should have kicked off without waiting
Assert.True(responses[0].StartTime < responses[1].EndTime);
Assert.True(responses[1].StartTime < responses[0].EndTime);
// The mutating task should have kicked off without waiting for those to finish
Assert.True(responses[2].StartTime < responses[1].EndTime);
Assert.True(responses[2].StartTime < responses[0].EndTime);
// The last two tasks should have waited for the mutating task
Assert.True(responses[3].StartTime >= responses[2].EndTime);
Assert.True(responses[4].StartTime >= responses[2].EndTime);
// The last two should have operated on different solutions than the first three
Assert.NotEqual(responses[3].Solution.WorkspaceVersion, responses[0].Solution.WorkspaceVersion);
Assert.NotEqual(responses[3].Solution.WorkspaceVersion, responses[1].Solution.WorkspaceVersion);
Assert.NotEqual(responses[3].Solution.WorkspaceVersion, responses[2].Solution.WorkspaceVersion);
Assert.Equal(responses[3].Solution.WorkspaceVersion, responses[3].Solution.WorkspaceVersion);
}
[Fact]
public async Task ThrowingTaskDoesntBringDownQueue()
{
var requests = new[] {
new TestRequest(FailingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var waitables = StartTestRun(requests);
// first task should fail
await Assert.ThrowsAsync<InvalidOperationException>(() => waitables[0]);
// remaining tasks should have executed normally
var responses = await Task.WhenAll(waitables.Skip(1));
Assert.Empty(responses.Where(r => r.StartTime == default));
Assert.All(responses, r => Assert.True(r.EndTime > r.StartTime));
}
[Fact]
public async Task FailingMutableTaskShutsDownQueue()
{
// NOTE: A failing task shuts down the queue not due to an exception escaping out of the handler
// but because the solution state would be invalid. This doesn't test the queues exception
// resiliancy.
var requests = new[] {
new TestRequest(FailingMutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
new TestRequest(MutatingRequestHandler.MethodName),
new TestRequest(NonMutatingRequestHandler.MethodName),
};
var waitables = StartTestRun(requests);
// first task should fail
await Assert.ThrowsAsync<InvalidOperationException>(() => waitables[0]);
// remaining tasks should be canceled
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.WhenAll(waitables.Skip(1)));
Assert.All(waitables.Skip(1), w => Assert.True(w.IsCanceled));
}
private async Task<TestResponse[]> TestAsync(TestRequest[] requests)
{
var waitables = StartTestRun(requests);
var responses = await Task.WhenAll(waitables);
// Sanity checks to ensure test handlers aren't doing something wacky, making future checks invalid
Assert.Empty(responses.Where(r => r.StartTime == default));
Assert.All(responses, r => Assert.True(r.EndTime > r.StartTime));
return responses;
}
private List<Task<TestResponse>> StartTestRun(TestRequest[] requests)
{
using var workspace = CreateTestWorkspace("class C { }", out _);
var solution = workspace.CurrentSolution;
var queue = CreateRequestQueue(solution);
var languageServer = GetLanguageServer(solution);
var clientCapabilities = new LSP.ClientCapabilities();
var waitables = new List<Task<TestResponse>>();
var order = 1;
foreach (var request in requests)
{
request.RequestOrder = order++;
waitables.Add(languageServer.ExecuteRequestAsync<TestRequest, TestResponse>(queue, request.MethodName, request, clientCapabilities, null, CancellationToken.None));
}
return waitables;
}
}
}
// 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.LanguageServer.UnitTests.RequestOrdering
{
internal class TestRequest
{
public string MethodName { get; }
public int RequestOrder { get; set; }
public TestRequest(string methodName)
{
MethodName = methodName;
}
}
}
// 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;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
internal class TestResponse
{
public int RequestOrder { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public Solution Solution { get; set; }
}
}
......@@ -97,8 +97,11 @@ public async Task SwitchingContextsChangesDefaultContext()
}
private static async Task<LSP.ActiveProjectContexts?> RunGetProjectContext(Solution solution, Uri uri)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.GetTextDocumentWithContextParams, LSP.ActiveProjectContexts?>(LSP.MSLSPMethods.ProjectContextsName,
CreateGetProjectContextParams(uri), new LSP.ClientCapabilities(), clientName: null, cancellationToken: CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.GetTextDocumentWithContextParams, LSP.ActiveProjectContexts?>(queue, LSP.MSLSPMethods.ProjectContextsName,
CreateGetProjectContextParams(uri), new LSP.ClientCapabilities(), clientName: null, cancellationToken: CancellationToken.None);
}
private static LSP.GetTextDocumentWithContextParams CreateGetProjectContextParams(Uri uri)
=> new LSP.GetTextDocumentWithContextParams()
......
......@@ -129,7 +129,8 @@ private static async Task<LSP.VSReferenceItem[]> RunFindAllReferencesAsync(Solut
SupportsVisualStudioExtensions = true
};
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.ReferenceParams, LSP.VSReferenceItem[]>(LSP.Methods.TextDocumentReferencesName,
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.ReferenceParams, LSP.VSReferenceItem[]>(queue, LSP.Methods.TextDocumentReferencesName,
CreateReferenceParams(caret), vsClientCapabilities, null, CancellationToken.None);
}
......
......@@ -124,7 +124,10 @@ class {|implementation:C|} : A { }";
}
private static async Task<LSP.Location[]> RunFindImplementationAsync(Solution solution, LSP.Location caret)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(LSP.Methods.TextDocumentImplementationName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.Location[]>(queue, LSP.Methods.TextDocumentImplementationName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
}
}
......@@ -125,7 +125,10 @@ private static LSP.RenameParams CreateRenameParams(LSP.Location location, string
};
private static async Task<WorkspaceEdit> RunRenameAsync(Solution solution, LSP.Location renameLocation, string renamevalue)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.RenameParams, LSP.WorkspaceEdit>(LSP.Methods.TextDocumentRenameName,
CreateRenameParams(renameLocation, renamevalue), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.RenameParams, LSP.WorkspaceEdit>(queue, LSP.Methods.TextDocumentRenameName,
CreateRenameParams(renameLocation, renamevalue), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
}
}
// 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.
#nullable enable
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SemanticTokens
{
public abstract class AbstractSemanticTokensTests : AbstractLanguageServerProtocolTests
{
protected static async Task<LSP.SemanticTokens> RunGetSemanticTokensAsync(
Solution solution, LSP.Location caret)
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.SemanticTokensParams, LSP.SemanticTokens>(queue,
LSP.SemanticTokensMethods.TextDocumentSemanticTokensName,
CreateSemanticTokensParams(caret), new LSP.VSClientCapabilities(), null, CancellationToken.None);
}
private static LSP.SemanticTokensParams CreateSemanticTokensParams(LSP.Location caret)
=> new LSP.SemanticTokensParams
{
TextDocument = new LSP.TextDocumentIdentifier { Uri = caret.Uri }
};
protected static async Task<LSP.SemanticTokens> RunGetSemanticTokensRangeAsync(
Solution solution, LSP.Location caret, LSP.Range range)
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.SemanticTokensRangeParams, LSP.SemanticTokens>(queue,
LSP.SemanticTokensMethods.TextDocumentSemanticTokensRangeName,
CreateSemanticTokensRangeParams(caret, range), new LSP.VSClientCapabilities(), null, CancellationToken.None);
}
private static LSP.SemanticTokensRangeParams CreateSemanticTokensRangeParams(LSP.Location caret, LSP.Range range)
=> new LSP.SemanticTokensRangeParams
{
TextDocument = new LSP.TextDocumentIdentifier { Uri = caret.Uri },
Range = range
};
protected static async Task<SumType<LSP.SemanticTokens, LSP.SemanticTokensEdits>> RunGetSemanticTokensEditsAsync(
Solution solution, LSP.Location caret, string previousResultId)
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.SemanticTokensEditsParams, SumType<LSP.SemanticTokens, LSP.SemanticTokensEdits>>(queue,
LSP.SemanticTokensMethods.TextDocumentSemanticTokensEditsName,
CreateSemanticTokensParams(caret, previousResultId), new LSP.VSClientCapabilities(), null, CancellationToken.None);
}
private static LSP.SemanticTokensEditsParams CreateSemanticTokensParams(LSP.Location caret, string previousResultId)
=> new LSP.SemanticTokensEditsParams
{
TextDocument = new LSP.TextDocumentIdentifier { Uri = caret.Uri },
PreviousResultId = previousResultId
};
protected static void UpdateDocumentText(string updatedText, Workspace workspace)
{
var docId = ((TestWorkspace)workspace).Documents.First().Id;
((TestWorkspace)workspace).ChangeDocument(docId, SourceText.From(updatedText));
UpdateSolutionProvider((TestWorkspace)workspace, workspace.CurrentSolution);
}
}
}
// 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.
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SemanticTokens
{
public class SemanticTokensEditsTests : AbstractSemanticTokensTests
{
/*
* Markup for basic test case:
* // Comment
* static class C { }
*/
private static readonly string s_standardCase = @"{|caret:|}// Comment
static class C { }";
/*
* Markup for single line test case:
* // Comment
*/
private static readonly string s_singleLineCase = @"{|caret:|}// Comment";
[Fact]
public async Task TestInsertingNewLineInMiddleOfFile()
{
var updatedText = @"// Comment
static class C { }";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
var results = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
var expectedEdit = SemanticTokensEditsHandler.GenerateEdit(start: 5, deleteCount: 1, data: new int[] { 2 });
Assert.Equal(expectedEdit, ((LSP.SemanticTokensEdits)results).Edits.First());
Assert.Equal("2", ((LSP.SemanticTokensEdits)results).ResultId);
}
/// <summary>
/// Tests making a deletion from the end of the file.
/// </summary>
[Fact]
public async Task TestGetSemanticTokensEdits_EndDeletionAsync()
{
var updatedText =
@"// Comment";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
var results = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
var expectedEdit = SemanticTokensEditsHandler.GenerateEdit(start: 5, deleteCount: 25, data: System.Array.Empty<int>());
Assert.Equal(expectedEdit, ((LSP.SemanticTokensEdits)results).Edits.First());
Assert.Equal("2", ((LSP.SemanticTokensEdits)results).ResultId);
}
/// <summary>
/// Tests making an insertion at the end of the file.
/// </summary>
[Fact]
public async Task TestGetSemanticTokensEdits_EndInsertionAsync()
{
var updatedText =
@"// Comment
static class C { }
// Comment";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
var results = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
var expectedEdit = SemanticTokensEditsHandler.GenerateEdit(
start: 30, deleteCount: 0, data: new int[] { 1, 0, 10, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Comment], 0 });
Assert.Equal(expectedEdit, ((LSP.SemanticTokensEdits)results).Edits.First());
Assert.Equal("2", ((LSP.SemanticTokensEdits)results).ResultId);
}
/// <summary>
/// Tests to make sure we return a minimal number of edits.
/// </summary>
[Fact]
public async Task TestGetSemanticTokensEdits_ReturnMinimalEdits()
{
var updatedText =
@"class
// Comment";
using var workspace = CreateTestWorkspace(s_singleLineCase, out var locations);
var caretLocation = locations["caret"].First();
await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
// Edit text
UpdateDocumentText(updatedText, workspace);
var results = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
// 1. Replaces length of token (10 to 5) and replaces token type (comment to keyword)
// 2. Creates new token for '// Comment'
var expectedEdit = SemanticTokensEditsHandler.GenerateEdit(
start: 0, deleteCount: 5,
data: new int[]
{
0, 0, 5, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0,
1, 0, 10, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Comment], 0
});
Assert.Equal(expectedEdit, ((LSP.SemanticTokensEdits)results).Edits?[0]);
Assert.Equal("2", ((LSP.SemanticTokensEdits)results).ResultId);
}
/// <summary>
/// Tests to make sure that if we don't have a matching semantic token set for the document in the cache,
/// we return the full set of semantic tokens.
/// </summary>
[Fact]
public async Task TestGetSemanticTokensEditsNoCacheAsync()
{
var updatedText =
@"// Comment
static class C { }";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
var results = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "10");
// Make sure we're returned SemanticTokens instead of SemanticTokensEdits.
Assert.True(results.Value is LSP.SemanticTokens);
}
[Fact]
public async Task TestConvertSemanticTokenEditsIntoSemanticTokens_InsertNewlineInMiddleOfFile()
{
var updatedText =
@"// Comment
static class C { }";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
var originalTokens = await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
// Edits to tokens conversion
var edits = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
var editsToTokens = ApplySemanticTokensEdits(originalTokens.Data, (LSP.SemanticTokensEdits)edits);
// Raw tokens
var rawTokens = await RunGetSemanticTokensAsync(workspace.CurrentSolution, locations["caret"].First());
Assert.True(Enumerable.SequenceEqual(rawTokens.Data, editsToTokens));
}
[Fact]
public async Task TestConvertSemanticTokenEditsIntoSemanticTokens_ReplacementEdit()
{
var updatedText =
@"// Comment
internal struct S { }";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
var originalTokens = await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
// Edits to tokens conversion
var edits = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
var editsToTokens = ApplySemanticTokensEdits(originalTokens.Data, (LSP.SemanticTokensEdits)edits);
// Raw tokens
var rawTokens = await RunGetSemanticTokensAsync(workspace.CurrentSolution, locations["caret"].First());
Assert.True(Enumerable.SequenceEqual(rawTokens.Data, editsToTokens));
}
[Fact]
public async Task TestConvertSemanticTokenEditsIntoSemanticTokens_ManyEdits()
{
var updatedText =
@"
// Comment
class C
{
static void M(int x)
{
var v = 1;
}
}";
using var workspace = CreateTestWorkspace(s_standardCase, out var locations);
var caretLocation = locations["caret"].First();
var originalTokens = await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
UpdateDocumentText(updatedText, workspace);
// Edits to tokens conversion
var edits = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "1");
var editsToTokens = ApplySemanticTokensEdits(originalTokens.Data, (LSP.SemanticTokensEdits)edits);
// Raw tokens
var rawTokens = await RunGetSemanticTokensAsync(workspace.CurrentSolution, locations["caret"].First());
Assert.True(Enumerable.SequenceEqual(rawTokens.Data, editsToTokens));
}
private static int[] ApplySemanticTokensEdits(int[]? originalTokens, LSP.SemanticTokensEdits edits)
{
var data = originalTokens.ToList();
if (edits.Edits != null)
{
foreach (var edit in edits.Edits)
{
data.RemoveRange(edit.Start, edit.DeleteCount);
data.InsertRange(edit.Start, edit.Data);
}
}
return data.ToArray();
}
}
}
// 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.
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SemanticTokens
{
public class SemanticTokensRangeTests : AbstractSemanticTokensTests
{
[Fact]
public async Task TestGetSemanticTokensRangeAsync()
{
var markup =
@"{|caret:|}// Comment
static class C { }
";
using var workspace = CreateTestWorkspace(markup, out var locations);
var range = new LSP.Range { Start = new Position(1, 0), End = new Position(2, 0) };
var results = await RunGetSemanticTokensRangeAsync(workspace.CurrentSolution, locations["caret"].First(), range);
var expectedResults = new LSP.SemanticTokens
{
Data = new int[]
{
// Line | Char | Len | Token type | Modifier
1, 0, 6, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'static'
0, 7, 5, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'class'
0, 6, 1, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Class], (int)TokenModifiers.Static, // 'C'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '{'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '}'
},
ResultId = "1"
};
Assert.Equal(expectedResults.Data, results.Data);
Assert.Equal(expectedResults.ResultId, results.ResultId);
}
}
}
// 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.
#nullable enable
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SemanticTokens
{
public class SemanticTokensTests : AbstractSemanticTokensTests
{
[Fact]
public async Task TestGetSemanticTokensAsync()
{
var markup =
@"{|caret:|}// Comment
static class C { }";
using var workspace = CreateTestWorkspace(markup, out var locations);
var results = await RunGetSemanticTokensAsync(workspace.CurrentSolution, locations["caret"].First());
var expectedResults = new LSP.SemanticTokens
{
Data = new int[]
{
// Line | Char | Len | Token type | Modifier
0, 0, 10, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Comment], 0, // '// Comment'
1, 0, 6, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'static'
0, 7, 5, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'class'
0, 6, 1, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Class], (int)TokenModifiers.Static, // 'C'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '{'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '}'
},
ResultId = "1"
};
Assert.Equal(expectedResults.Data, results.Data);
Assert.Equal(expectedResults.ResultId, results.ResultId);
}
/// <summary>
/// Tests all three handlers in succession and makes sure we receive the expected result at each stage.
/// </summary>
[Fact]
public async Task TestAllHandlersAsync()
{
var markup =
@"{|caret:|}// Comment
static class C { }
";
using var workspace = CreateTestWorkspace(markup, out var locations);
var caretLocation = locations["caret"].First();
// 1. Range handler
var range = new LSP.Range { Start = new Position(1, 0), End = new Position(2, 0) };
var rangeResults = await RunGetSemanticTokensRangeAsync(workspace.CurrentSolution, caretLocation, range);
var expectedRangeResults = new LSP.SemanticTokens
{
Data = new int[]
{
// Line | Char | Len | Token type | Modifier
1, 0, 6, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'static'
0, 7, 5, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'class'
0, 6, 1, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Class], (int)TokenModifiers.Static, // 'C'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '{'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '}'
},
ResultId = "1"
};
Assert.Equal(expectedRangeResults.Data, rangeResults.Data);
Assert.Equal(expectedRangeResults.ResultId, rangeResults.ResultId);
// 2. Whole document handler
var wholeDocResults = await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
var expectedWholeDocResults = new LSP.SemanticTokens
{
Data = new int[]
{
// Line | Char | Len | Token type | Modifier
0, 0, 10, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Comment], 0, // '// Comment'
1, 0, 6, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'static'
0, 7, 5, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'class'
0, 6, 1, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Class], (int)TokenModifiers.Static, // 'C'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '{'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '}'
},
ResultId = "2"
};
Assert.Equal(expectedWholeDocResults.Data, wholeDocResults.Data);
Assert.Equal(expectedWholeDocResults.ResultId, wholeDocResults.ResultId);
// 3. Edits handler - insert newline at beginning of file
var newMarkup = @"
// Comment
static class C { }
";
UpdateDocumentText(newMarkup, workspace);
var editResults = await RunGetSemanticTokensEditsAsync(workspace.CurrentSolution, caretLocation, previousResultId: "2");
var expectedEdit = SemanticTokensEditsHandler.GenerateEdit(0, 1, new int[] { 1 });
Assert.Equal(expectedEdit, ((LSP.SemanticTokensEdits)editResults).Edits.First());
Assert.Equal("3", ((LSP.SemanticTokensEdits)editResults).ResultId);
// 4. Re-request whole document handler (may happen if LSP runs into an error)
var wholeDocResults2 = await RunGetSemanticTokensAsync(workspace.CurrentSolution, caretLocation);
var expectedWholeDocResults2 = new LSP.SemanticTokens
{
Data = new int[]
{
// Line | Char | Len | Token type | Modifier
1, 0, 10, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Comment], 0, // '// Comment'
1, 0, 6, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'static'
0, 7, 5, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Keyword], 0, // 'class'
0, 6, 1, SemanticTokensCache.TokenTypeToIndex[LSP.SemanticTokenTypes.Class], (int)TokenModifiers.Static, // 'C'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '{'
0, 2, 1, SemanticTokensCache.TokenTypeToIndex[ClassificationTypeNames.Punctuation], 0, // '}'
},
ResultId = "4"
};
Assert.Equal(expectedWholeDocResults2.Data, wholeDocResults2.Data);
Assert.Equal(expectedWholeDocResults2.ResultId, wholeDocResults2.ResultId);
}
}
}
......@@ -45,8 +45,11 @@ int M2(string a)
}
private static async Task<LSP.SignatureHelp> RunGetSignatureHelpAsync(Solution solution, LSP.Location caret)
=> await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.SignatureHelp>(LSP.Methods.TextDocumentSignatureHelpName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
{
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.TextDocumentPositionParams, LSP.SignatureHelp>(queue, LSP.Methods.TextDocumentSignatureHelpName,
CreateTextDocumentPositionParams(caret), new LSP.ClientCapabilities(), null, CancellationToken.None);
}
private static LSP.SignatureInformation CreateSignatureInformation(string methodLabal, string methodDocumentation, string parameterLabel, string parameterDocumentation)
=> new LSP.SignatureInformation()
......
......@@ -102,7 +102,8 @@ private static async Task<object[]> RunGetDocumentSymbolsAsync(Solution solution
}
};
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentSymbolParams, object[]>(LSP.Methods.TextDocumentDocumentSymbolName,
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.DocumentSymbolParams, object[]>(queue, LSP.Methods.TextDocumentDocumentSymbolName,
request, clientCapabilities, null, CancellationToken.None);
}
......
......@@ -156,7 +156,8 @@ private static async Task<LSP.SymbolInformation[]> RunGetWorkspaceSymbolsAsync(S
Query = query
};
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.WorkspaceSymbolParams, LSP.SymbolInformation[]>(LSP.Methods.WorkspaceSymbolName,
var queue = CreateRequestQueue(solution);
return await GetLanguageServer(solution).ExecuteRequestAsync<LSP.WorkspaceSymbolParams, LSP.SymbolInformation[]>(queue, LSP.Methods.WorkspaceSymbolName,
request, new LSP.ClientCapabilities(), null, CancellationToken.None);
}
}
......
......@@ -26,6 +26,7 @@ internal abstract class AbstractLanguageServerClient : ILanguageClient
private readonly IAsynchronousOperationListenerProvider _listenerProvider;
private readonly AbstractRequestHandlerProvider _requestHandlerProvider;
private readonly Workspace _workspace;
private readonly ILspSolutionProvider _solutionProvider;
private InProcLanguageServer? _languageServer;
/// <summary>
......@@ -63,6 +64,7 @@ internal abstract class AbstractLanguageServerClient : ILanguageClient
VisualStudioWorkspace workspace,
IDiagnosticService diagnosticService,
IAsynchronousOperationListenerProvider listenerProvider,
ILspSolutionProvider solutionProvider,
string? diagnosticsClientName)
{
_requestHandlerProvider = requestHandlerProvider;
......@@ -70,6 +72,7 @@ internal abstract class AbstractLanguageServerClient : ILanguageClient
_diagnosticService = diagnosticService;
_listenerProvider = listenerProvider;
_diagnosticsClientName = diagnosticsClientName;
_solutionProvider = solutionProvider;
}
public Task<Connection> ActivateAsync(CancellationToken token)
......@@ -78,7 +81,7 @@ public Task<Connection> ActivateAsync(CancellationToken token)
var (clientStream, serverStream) = FullDuplexStream.CreatePair();
_languageServer = new InProcLanguageServer(serverStream, serverStream, _requestHandlerProvider, _workspace,
_diagnosticService, _listenerProvider, clientName: _diagnosticsClientName);
_diagnosticService, _listenerProvider, _solutionProvider, clientName: _diagnosticsClientName);
return Task.FromResult(new Connection(clientStream, clientStream));
}
......
......@@ -16,6 +16,8 @@
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
......@@ -40,6 +42,7 @@ internal class InProcLanguageServer
private readonly JsonRpc _jsonRpc;
private readonly AbstractRequestHandlerProvider _requestHandlerProvider;
private readonly CodeAnalysis.Workspace _workspace;
private readonly RequestExecutionQueue _queue;
private VSClientCapabilities _clientCapabilities;
private bool _shuttingDown;
......@@ -50,6 +53,7 @@ internal class InProcLanguageServer
CodeAnalysis.Workspace workspace,
IDiagnosticService diagnosticService,
IAsynchronousOperationListenerProvider listenerProvider,
ILspSolutionProvider solutionProvider,
string? clientName)
{
_requestHandlerProvider = requestHandlerProvider;
......@@ -69,6 +73,9 @@ internal class InProcLanguageServer
_diagnosticService.DiagnosticsUpdated += DiagnosticService_DiagnosticsUpdated;
_clientCapabilities = new VSClientCapabilities();
_queue = new RequestExecutionQueue(solutionProvider);
_queue.RequestServerShutdown += RequestExecutionQueue_Errored;
}
public bool Running => !_shuttingDown && !_jsonRpc.IsDisposed;
......@@ -83,7 +90,7 @@ public async Task<InitializeResult> InitializeAsync(InitializeParams initializeP
{
_clientCapabilities = (VSClientCapabilities)initializeParams.Capabilities;
var serverCapabilities = await _requestHandlerProvider.ExecuteRequestAsync<InitializeParams, InitializeResult>(Methods.InitializeName,
var serverCapabilities = await _requestHandlerProvider.ExecuteRequestAsync<InitializeParams, InitializeResult>(_queue, Methods.InitializeName,
initializeParams, _clientCapabilities, _clientName, cancellationToken).ConfigureAwait(false);
// Always support hover - if any LSP client for a content type advertises support,
......@@ -118,6 +125,8 @@ public Task ShutdownAsync(CancellationToken _)
_shuttingDown = true;
_diagnosticService.DiagnosticsUpdated -= DiagnosticService_DiagnosticsUpdated;
ShutdownRequestQueue();
return Task.CompletedTask;
}
......@@ -141,103 +150,119 @@ public Task ExitAsync(CancellationToken _)
[JsonRpcMethod(Methods.TextDocumentDefinitionName, UseSingleObjectParameterDeserialization = true)]
public Task<LSP.Location[]> GetTextDocumentDefinitionAsync(TextDocumentPositionParams textDocumentPositionParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, LSP.Location[]>(Methods.TextDocumentDefinitionName,
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, LSP.Location[]>(_queue, Methods.TextDocumentDefinitionName,
textDocumentPositionParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentRenameName, UseSingleObjectParameterDeserialization = true)]
public Task<WorkspaceEdit> GetTextDocumentRenameAsync(RenameParams renameParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<RenameParams, WorkspaceEdit>(Methods.TextDocumentRenameName,
=> _requestHandlerProvider.ExecuteRequestAsync<RenameParams, WorkspaceEdit>(_queue, Methods.TextDocumentRenameName,
renameParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentReferencesName, UseSingleObjectParameterDeserialization = true)]
public Task<VSReferenceItem[]> GetTextDocumentReferencesAsync(ReferenceParams referencesParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<ReferenceParams, VSReferenceItem[]>(Methods.TextDocumentReferencesName,
=> _requestHandlerProvider.ExecuteRequestAsync<ReferenceParams, VSReferenceItem[]>(_queue, Methods.TextDocumentReferencesName,
referencesParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentCodeActionName, UseSingleObjectParameterDeserialization = true)]
public Task<VSCodeAction[]> GetTextDocumentCodeActionsAsync(CodeActionParams codeActionParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<CodeActionParams, VSCodeAction[]>(Methods.TextDocumentCodeActionName,
=> _requestHandlerProvider.ExecuteRequestAsync<CodeActionParams, VSCodeAction[]>(_queue, Methods.TextDocumentCodeActionName,
codeActionParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(MSLSPMethods.TextDocumentCodeActionResolveName, UseSingleObjectParameterDeserialization = true)]
public Task<VSCodeAction> ResolveCodeActionAsync(VSCodeAction vsCodeAction, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<VSCodeAction, VSCodeAction>(MSLSPMethods.TextDocumentCodeActionResolveName,
=> _requestHandlerProvider.ExecuteRequestAsync<VSCodeAction, VSCodeAction>(_queue, MSLSPMethods.TextDocumentCodeActionResolveName,
vsCodeAction, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentCompletionName, UseSingleObjectParameterDeserialization = true)]
public async Task<SumType<CompletionList, CompletionItem[]>> GetTextDocumentCompletionAsync(CompletionParams completionParams, CancellationToken cancellationToken)
// Convert to sumtype before reporting to work around https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1107698
=> await _requestHandlerProvider.ExecuteRequestAsync<CompletionParams, CompletionList>(Methods.TextDocumentCompletionName,
=> await _requestHandlerProvider.ExecuteRequestAsync<CompletionParams, CompletionList>(_queue, Methods.TextDocumentCompletionName,
completionParams, _clientCapabilities, _clientName, cancellationToken).ConfigureAwait(false);
[JsonRpcMethod(Methods.TextDocumentCompletionResolveName, UseSingleObjectParameterDeserialization = true)]
public Task<CompletionItem> ResolveCompletionItemAsync(CompletionItem completionItem, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<CompletionItem, CompletionItem>(Methods.TextDocumentCompletionResolveName,
=> _requestHandlerProvider.ExecuteRequestAsync<CompletionItem, CompletionItem>(_queue, Methods.TextDocumentCompletionResolveName,
completionItem, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentFoldingRangeName, UseSingleObjectParameterDeserialization = true)]
public Task<FoldingRange[]> GetTextDocumentFoldingRangeAsync(FoldingRangeParams textDocumentFoldingRangeParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<FoldingRangeParams, FoldingRange[]>(Methods.TextDocumentFoldingRangeName,
=> _requestHandlerProvider.ExecuteRequestAsync<FoldingRangeParams, FoldingRange[]>(_queue, Methods.TextDocumentFoldingRangeName,
textDocumentFoldingRangeParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentDocumentHighlightName, UseSingleObjectParameterDeserialization = true)]
public Task<DocumentHighlight[]> GetTextDocumentDocumentHighlightsAsync(TextDocumentPositionParams textDocumentPositionParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, DocumentHighlight[]>(Methods.TextDocumentDocumentHighlightName,
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, DocumentHighlight[]>(_queue, Methods.TextDocumentDocumentHighlightName,
textDocumentPositionParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentHoverName, UseSingleObjectParameterDeserialization = true)]
public Task<Hover?> GetTextDocumentDocumentHoverAsync(TextDocumentPositionParams textDocumentPositionParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, Hover?>(Methods.TextDocumentHoverName,
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, Hover?>(_queue, Methods.TextDocumentHoverName,
textDocumentPositionParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentDocumentSymbolName, UseSingleObjectParameterDeserialization = true)]
public Task<object[]> GetTextDocumentDocumentSymbolsAsync(DocumentSymbolParams documentSymbolParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentSymbolParams, object[]>(Methods.TextDocumentDocumentSymbolName,
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentSymbolParams, object[]>(_queue, Methods.TextDocumentDocumentSymbolName,
documentSymbolParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentFormattingName, UseSingleObjectParameterDeserialization = true)]
public Task<TextEdit[]> GetTextDocumentFormattingAsync(DocumentFormattingParams documentFormattingParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentFormattingParams, TextEdit[]>(Methods.TextDocumentFormattingName,
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentFormattingParams, TextEdit[]>(_queue, Methods.TextDocumentFormattingName,
documentFormattingParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentOnTypeFormattingName, UseSingleObjectParameterDeserialization = true)]
public Task<TextEdit[]> GetTextDocumentFormattingOnTypeAsync(DocumentOnTypeFormattingParams documentOnTypeFormattingParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentOnTypeFormattingParams, TextEdit[]>(Methods.TextDocumentOnTypeFormattingName,
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentOnTypeFormattingParams, TextEdit[]>(_queue, Methods.TextDocumentOnTypeFormattingName,
documentOnTypeFormattingParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentImplementationName, UseSingleObjectParameterDeserialization = true)]
public Task<LSP.Location[]> GetTextDocumentImplementationsAsync(TextDocumentPositionParams textDocumentPositionParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, LSP.Location[]>(Methods.TextDocumentImplementationName,
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, LSP.Location[]>(_queue, Methods.TextDocumentImplementationName,
textDocumentPositionParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentRangeFormattingName, UseSingleObjectParameterDeserialization = true)]
public Task<TextEdit[]> GetTextDocumentRangeFormattingAsync(DocumentRangeFormattingParams documentRangeFormattingParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentRangeFormattingParams, TextEdit[]>(Methods.TextDocumentRangeFormattingName,
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentRangeFormattingParams, TextEdit[]>(_queue, Methods.TextDocumentRangeFormattingName,
documentRangeFormattingParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.TextDocumentSignatureHelpName, UseSingleObjectParameterDeserialization = true)]
public Task<SignatureHelp> GetTextDocumentSignatureHelpAsync(TextDocumentPositionParams textDocumentPositionParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, SignatureHelp>(Methods.TextDocumentSignatureHelpName,
=> _requestHandlerProvider.ExecuteRequestAsync<TextDocumentPositionParams, SignatureHelp>(_queue, Methods.TextDocumentSignatureHelpName,
textDocumentPositionParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.WorkspaceExecuteCommandName, UseSingleObjectParameterDeserialization = true)]
public Task<object> ExecuteWorkspaceCommandAsync(ExecuteCommandParams executeCommandParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<ExecuteCommandParams, object>(Methods.WorkspaceExecuteCommandName,
=> _requestHandlerProvider.ExecuteRequestAsync<ExecuteCommandParams, object>(_queue, Methods.WorkspaceExecuteCommandName,
executeCommandParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(Methods.WorkspaceSymbolName, UseSingleObjectParameterDeserialization = true)]
public Task<SymbolInformation[]> GetWorkspaceSymbolsAsync(WorkspaceSymbolParams workspaceSymbolParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<WorkspaceSymbolParams, SymbolInformation[]>(Methods.WorkspaceSymbolName,
=> _requestHandlerProvider.ExecuteRequestAsync<WorkspaceSymbolParams, SymbolInformation[]>(_queue, Methods.WorkspaceSymbolName,
workspaceSymbolParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(MSLSPMethods.ProjectContextsName, UseSingleObjectParameterDeserialization = true)]
public Task<ActiveProjectContexts?> GetProjectContextsAsync(GetTextDocumentWithContextParams textDocumentWithContextParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<GetTextDocumentWithContextParams, ActiveProjectContexts?>(MSLSPMethods.ProjectContextsName,
=> _requestHandlerProvider.ExecuteRequestAsync<GetTextDocumentWithContextParams, ActiveProjectContexts?>(_queue, MSLSPMethods.ProjectContextsName,
textDocumentWithContextParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(SemanticTokensMethods.TextDocumentSemanticTokensName, UseSingleObjectParameterDeserialization = true)]
public Task<SemanticTokens> GetTextDocumentSemanticTokensAsync(SemanticTokensParams semanticTokensParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<SemanticTokensParams, SemanticTokens>(_queue, SemanticTokensMethods.TextDocumentSemanticTokensName,
semanticTokensParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(SemanticTokensMethods.TextDocumentSemanticTokensEditsName, UseSingleObjectParameterDeserialization = true)]
public Task<SumType<SemanticTokens, SemanticTokensEdits>> GetTextDocumentSemanticTokensEditsAsync(SemanticTokensEditsParams semanticTokensEditsParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<SemanticTokensEditsParams, SumType<SemanticTokens, SemanticTokensEdits>>(_queue, SemanticTokensMethods.TextDocumentSemanticTokensEditsName,
semanticTokensEditsParams, _clientCapabilities, _clientName, cancellationToken);
// Note: Since a range request is always received in conjunction with a whole document request, we don't need to cache range results.
[JsonRpcMethod(SemanticTokensMethods.TextDocumentSemanticTokensRangeName, UseSingleObjectParameterDeserialization = true)]
public Task<SemanticTokens> GetTextDocumentSemanticTokensRangeAsync(SemanticTokensRangeParams semanticTokensRangeParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<SemanticTokensRangeParams, SemanticTokens>(_queue, SemanticTokensMethods.TextDocumentSemanticTokensRangeName,
semanticTokensRangeParams, _clientCapabilities, _clientName, cancellationToken);
[JsonRpcMethod(MSLSPMethods.OnAutoInsertName, UseSingleObjectParameterDeserialization = true)]
public Task<DocumentOnAutoInsertResponseItem[]> GetDocumentOnAutoInsertAsync(DocumentOnAutoInsertParams autoInsertParams, CancellationToken cancellationToken)
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentOnAutoInsertParams, DocumentOnAutoInsertResponseItem[]>(MSLSPMethods.OnAutoInsertName,
=> _requestHandlerProvider.ExecuteRequestAsync<DocumentOnAutoInsertParams, DocumentOnAutoInsertResponseItem[]>(_queue, MSLSPMethods.OnAutoInsertName,
autoInsertParams, _clientCapabilities, _clientName, cancellationToken);
private void DiagnosticService_DiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e)
......@@ -264,6 +289,35 @@ private void DiagnosticService_DiagnosticsUpdated(object sender, DiagnosticsUpda
}
}
private void ShutdownRequestQueue()
{
_queue.RequestServerShutdown -= RequestExecutionQueue_Errored;
// if the queue requested shutdown via its event, it will have already shut itself down, but this
// won't cause any problems calling it again
_queue.Shutdown();
}
private void RequestExecutionQueue_Errored(object sender, RequestShutdownEventArgs e)
{
// log message and shut down
var message = new LogMessageParams()
{
MessageType = MessageType.Error,
Message = e.Message
};
var asyncToken = _listener.BeginAsyncOperation(nameof(RequestExecutionQueue_Errored));
Task.Run(async () =>
{
await _jsonRpc.NotifyWithParameterObjectAsync(Methods.WindowLogMessageName, message).ConfigureAwait(false);
// The "default" here is the cancellation token, which these methods don't use, hence the discard name
await ShutdownAsync(_: default).ConfigureAwait(false);
await ExitAsync(_: default).ConfigureAwait(false);
}).CompletesAsyncOperation(asyncToken);
}
/// <summary>
/// Stores the last published LSP diagnostics with the Roslyn document that they came from.
/// This is useful in the following scenario. Imagine we have documentA which has contributions to mapped files m1 and m2.
......
......@@ -29,8 +29,9 @@ internal class LiveShareLanguageServerClient : AbstractLanguageServerClient
public LiveShareLanguageServerClient(LanguageServerProtocol languageServerProtocol,
VisualStudioWorkspace workspace,
IDiagnosticService diagnosticService,
IAsynchronousOperationListenerProvider listenerProvider)
: base(languageServerProtocol, workspace, diagnosticService, listenerProvider, diagnosticsClientName: null)
IAsynchronousOperationListenerProvider listenerProvider,
ILspSolutionProvider solutionProvider)
: base(languageServerProtocol, workspace, diagnosticService, listenerProvider, solutionProvider, diagnosticsClientName: null)
{
}
......
......@@ -42,8 +42,9 @@ internal class RazorLanguageClient : AbstractLanguageServerClient
public RazorLanguageClient(LanguageServerProtocol languageServerProtocol,
VisualStudioWorkspace workspace,
IDiagnosticService diagnosticService,
IAsynchronousOperationListenerProvider listenerProvider)
: base(languageServerProtocol, workspace, diagnosticService, listenerProvider, ClientName)
IAsynchronousOperationListenerProvider listenerProvider,
ILspSolutionProvider solutionProvider)
: base(languageServerProtocol, workspace, diagnosticService, listenerProvider, solutionProvider, ClientName)
{
}
}
......
......@@ -379,8 +379,9 @@ static InProcLanguageServer CreateLanguageServer(Stream inputStream, Stream outp
{
var protocol = workspace.ExportProvider.GetExportedValue<LanguageServerProtocol>();
var listenerProvider = workspace.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>();
var solutionProvider = workspace.ExportProvider.GetExportedValue<ILspSolutionProvider>();
var languageServer = new InProcLanguageServer(inputStream, outputStream, protocol, workspace, mockDiagnosticService, listenerProvider, clientName: null);
var languageServer = new InProcLanguageServer(inputStream, outputStream, protocol, workspace, mockDiagnosticService, listenerProvider, solutionProvider, clientName: null);
return languageServer;
}
}
......
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.LiveShare.CustomProtocol;
using Microsoft.VisualStudio.LiveShare.LanguageServices;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare
{
/// <summary>
/// Handler for a request to classify the document. This is used for semantic colorization and only works for C#\VB.
/// TODO - Move once defined as a custom protocol.
/// Note, this must return object instead of ClassificationSpan b/c liveshare uses dynamic to convert handler results.
/// Unfortunately, ClassificationSpan is an internal type and cannot be defined in the external access layer.
/// </summary>
internal abstract class AbstractClassificationsHandler : ILspRequestHandler<ClassificationParams, object[], Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
protected AbstractClassificationsHandler(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
}
protected abstract Task AddClassificationsAsync(IClassificationService classificationService, Document document, TextSpan textSpan, List<ClassifiedSpan> spans, CancellationToken cancellationToken);
public async Task<object[]> HandleAsync(ClassificationParams request, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
{
request.TextDocument.Uri = requestContext.ProtocolConverter.FromProtocolUri(request.TextDocument.Uri);
var document = _solutionProvider.GetDocument(request.TextDocument);
var classificationService = document?.Project.LanguageServices.GetService<IClassificationService>();
if (document == null || classificationService == null)
{
return Array.Empty<ClassificationSpan>();
}
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var textSpan = ProtocolConversions.RangeToTextSpan(request.Range, text);
var spans = new List<ClassifiedSpan>();
await AddClassificationsAsync(classificationService, document, textSpan, spans, cancellationToken).ConfigureAwait(false);
return spans.Select(c => new ClassificationSpan { Classification = c.ClassificationType, Range = ProtocolConversions.TextSpanToRange(c.TextSpan, text) }).ToArray();
}
}
}
// 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.Collections.Generic;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.LiveShare.CustomProtocol;
using Microsoft.VisualStudio.LiveShare.LanguageServices;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare
{
/// <summary>
/// Handler for a request to classify the document. This is used for semantic colorization and only works for C#\VB.
/// TODO - Move once defined as a custom protocol.
/// Note, this must return object instead of ClassificationSpan b/c liveshare uses dynamic to convert handler results.
/// Unfortunately, ClassificationSpan is an internal type and cannot be defined in the external access layer.
/// </summary>
internal class ClassificationsHandler : AbstractClassificationsHandler
{
public ClassificationsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
protected override async Task AddClassificationsAsync(IClassificationService classificationService, Document document, TextSpan textSpan, List<ClassifiedSpan> spans, CancellationToken cancellationToken)
=> await classificationService.AddSemanticClassificationsAsync(document, textSpan, spans, cancellationToken).ConfigureAwait(false);
}
[ExportLspRequestHandler(LiveShareConstants.RoslynContractName, RoslynMethods.ClassificationsName)]
[Obsolete("Used for backwards compatibility with old liveshare clients.")]
internal class RoslynClassificationsHandler : ClassificationsHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RoslynClassificationsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
}
[ExportLspRequestHandler(LiveShareConstants.CSharpContractName, RoslynMethods.ClassificationsName)]
internal class CSharpClassificationsHandler : ClassificationsHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpClassificationsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
}
[ExportLspRequestHandler(LiveShareConstants.VisualBasicContractName, RoslynMethods.ClassificationsName)]
internal class VisualBasicClassificationsHandler : ClassificationsHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VisualBasicClassificationsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
}
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, RoslynMethods.ClassificationsName)]
internal class TypeScriptClassificationsHandler : ClassificationsHandler
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptClassificationsHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
}
}
// 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;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CSharp.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.VisualBasic.Classification;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare.Client.Classification
{
[ExportLanguageServiceFactory(typeof(ISyntaxClassificationService), LanguageNames.CSharp, WorkspaceKind.CloudEnvironmentClientWorkspace), Shared]
internal class CSharpLspEditorClassificationFactory : RoslynSyntaxClassificationServiceFactory
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpLspEditorClassificationFactory(CSharpLspClientServiceFactory csharpLspClientServiceFactory,
ClassificationTypeMap classificationTypeMap, IThreadingContext threadingContext)
: base(csharpLspClientServiceFactory, classificationTypeMap, threadingContext)
{
}
[Obsolete(MefConstruction.FactoryMethodMessage, error: true)]
protected override ISyntaxClassificationService GetOriginalSyntaxClassificationService(HostLanguageServices languageServices)
=> new CSharpSyntaxClassificationService(languageServices);
}
[ExportLanguageServiceFactory(typeof(ISyntaxClassificationService), LanguageNames.VisualBasic, WorkspaceKind.CloudEnvironmentClientWorkspace), Shared]
internal class VBLspEditorClassificationServiceFactory : RoslynSyntaxClassificationServiceFactory
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VBLspEditorClassificationServiceFactory(VisualBasicLspClientServiceFactory vbLspClientServiceFactory,
ClassificationTypeMap classificationTypeMap, IThreadingContext threadingContext)
: base(vbLspClientServiceFactory, classificationTypeMap, threadingContext)
{
}
[Obsolete(MefConstruction.FactoryMethodMessage, error: true)]
protected override ISyntaxClassificationService GetOriginalSyntaxClassificationService(HostLanguageServices languageServices)
=> new VisualBasicSyntaxClassificationService(languageServices);
}
}
// 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.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Classification.Classifiers;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.LiveShare.CustomProtocol;
using LS = Microsoft.VisualStudio.LiveShare.LanguageServices;
using Task = System.Threading.Tasks.Task;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare.Client.Classification
{
internal class RoslynSyntaxClassificationService : ISyntaxClassificationService
{
private readonly AbstractLspClientServiceFactory _roslynLspClientServiceFactory;
private readonly ISyntaxClassificationService _originalService;
private readonly ClassificationTypeMap _classificationTypeMap;
private readonly IThreadingContext _threadingContext;
public RoslynSyntaxClassificationService(AbstractLspClientServiceFactory roslynLspClientServiceFactory, ISyntaxClassificationService originalService,
ClassificationTypeMap classificationTypeMap, IThreadingContext threadingContext)
{
_roslynLspClientServiceFactory = roslynLspClientServiceFactory;
_originalService = originalService;
_classificationTypeMap = classificationTypeMap;
_threadingContext = threadingContext;
}
public void AddLexicalClassifications(SourceText text, TextSpan textSpan, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
=> _originalService.AddLexicalClassifications(text, textSpan, result, cancellationToken);
public void AddSemanticClassifications(SemanticModel semanticModel, TextSpan textSpan, CodeAnalysis.Workspace workspace, Func<SyntaxNode, ImmutableArray<ISyntaxClassifier>> getNodeClassifiers, Func<SyntaxToken, ImmutableArray<ISyntaxClassifier>> getTokenClassifiers, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
{
_threadingContext.JoinableTaskFactory.Run(async () =>
{
var sourceText = await semanticModel.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false);
await AddRemoteClassificationsAsync(RoslynMethods.ClassificationsName, semanticModel.SyntaxTree.FilePath, sourceText, textSpan, result.Add, cancellationToken).ConfigureAwait(false);
});
}
public async Task AddSemanticClassificationsAsync(Document document, TextSpan textSpan, Func<SyntaxNode, ImmutableArray<ISyntaxClassifier>> getNodeClassifiers, Func<SyntaxToken, ImmutableArray<ISyntaxClassifier>> getTokenClassifiers, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
{
// If we are in the preview workspace, the request is to try to colorize a lightbulb preview. The document would have changed
// in this workspace and we currently don't support requests for anything but the Workspace.CurrentSolution.
if (document.Project.Solution.Workspace.Kind == WorkspaceKind.Preview)
{
return;
}
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
await AddRemoteClassificationsAsync(RoslynMethods.ClassificationsName, document.FilePath, sourceText, textSpan, result.Add, cancellationToken).ConfigureAwait(false);
}
public void AddSyntacticClassifications(SyntaxTree syntaxTree, TextSpan textSpan, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
=> _originalService.AddSyntacticClassifications(syntaxTree, textSpan, result, cancellationToken);
public ClassifiedSpan FixClassification(SourceText text, ClassifiedSpan classifiedSpan)
=> _originalService.FixClassification(text, classifiedSpan);
public ImmutableArray<ISyntaxClassifier> GetDefaultSyntaxClassifiers()
=> _originalService.GetDefaultSyntaxClassifiers();
public async Task AddRemoteClassificationsAsync(string classificationsServiceName, string filePath, SourceText sourceText, TextSpan textSpan, Action<ClassifiedSpan> tagAdder, CancellationToken cancellationToken)
{
var lspClient = _roslynLspClientServiceFactory.ActiveLanguageServerClient;
if (lspClient == null)
{
return;
}
var classificationParams = new ClassificationParams
{
TextDocument = new TextDocumentIdentifier { Uri = lspClient.ProtocolConverter.ToProtocolUri(new Uri(filePath)) },
Range = ProtocolConversions.TextSpanToRange(textSpan, sourceText)
};
var request = new LS.LspRequest<ClassificationParams, ClassificationSpan[]>(classificationsServiceName);
var classificationSpans = await lspClient.RequestAsync(request, classificationParams, cancellationToken).ConfigureAwait(false);
if (classificationSpans == null)
{
return;
}
foreach (var classificationSpan in classificationSpans)
{
// The host may return more classifications than are supported by the guest. As an example, 15.7 added classifications for type members which wouldnt be understood by a 15.6 guest.
// Check with the classificationTypeMap to see if this is a known classification.
var classification = classificationSpan.Classification;
if (_classificationTypeMap.GetClassificationType(classification) == null)
{
classification = ClassificationTypeNames.Identifier;
}
var span = ProtocolConversions.RangeToTextSpan(classificationSpan.Range, sourceText);
if (span.End <= sourceText.Length)
{
tagAdder(new ClassifiedSpan(classification, span));
}
}
}
}
}
// 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.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare.Client.Classification
{
internal abstract class RoslynSyntaxClassificationServiceFactory : ILanguageServiceFactory
{
private readonly AbstractLspClientServiceFactory _roslynLspClientServiceFactory;
private readonly ClassificationTypeMap _classificationTypeMap;
private readonly IThreadingContext _threadingContext;
public RoslynSyntaxClassificationServiceFactory(AbstractLspClientServiceFactory roslynLspClientServiceFactory,
ClassificationTypeMap classificationTypeMap, IThreadingContext threadingContext)
{
_roslynLspClientServiceFactory = roslynLspClientServiceFactory ?? throw new ArgumentNullException(nameof(roslynLspClientServiceFactory));
_classificationTypeMap = classificationTypeMap ?? throw new ArgumentNullException(nameof(classificationTypeMap));
_threadingContext = threadingContext ?? throw new ArgumentNullException(nameof(threadingContext));
}
[Obsolete(MefConstruction.FactoryMethodMessage, error: true)]
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new RoslynSyntaxClassificationService(_roslynLspClientServiceFactory, GetOriginalSyntaxClassificationService(languageServices), _classificationTypeMap, _threadingContext);
}
[Obsolete(MefConstruction.FactoryMethodMessage, error: true)]
protected abstract ISyntaxClassificationService GetOriginalSyntaxClassificationService(HostLanguageServices languageServices);
}
}
// 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.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare.CustomProtocol
{
// TODO - Move this to LSP extensions or elsewhere.
// Temporary internal copy so we can implement these custom extensions
// until these types are available elsewhere.
internal class ClassificationParams
{
/// <summary>
/// The document for which classification is requested.
/// </summary>
public TextDocumentIdentifier TextDocument { get; set; }
/// <summary>
/// The range for which classification is requested.
/// </summary>
public Range Range { get; set; }
}
internal class ClassificationSpan
{
/// <summary>
/// The range being classified.
/// </summary>
public Range Range { get; set; }
/// <summary>
/// The classification of the span.
/// </summary>
public string Classification { get; set; }
}
}
......@@ -48,10 +48,13 @@ internal class TypeScriptCompletionHandlerShim : CompletionHandler, ILspRequestH
}
});
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptCompletionHandlerShim(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public TypeScriptCompletionHandlerShim(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
}
public Task<LanguageServer.Protocol.CompletionList?> HandleAsync(object input, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
......@@ -60,7 +63,7 @@ public TypeScriptCompletionHandlerShim(ILspSolutionProvider solutionProvider) :
// However, this works through liveshare on the LSP client, but not the LSP extension.
// see https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1107682 for tracking.
var request = ((JObject)input).ToObject<CompletionParams>(s_jsonSerializer);
var context = new LSP.RequestContext(requestContext.GetClientCapabilities(), null);
var context = this.CreateRequestContext(request, _solutionProvider, requestContext.GetClientCapabilities());
return base.HandleRequestAsync(request, context, cancellationToken);
}
}
......@@ -68,23 +71,32 @@ public TypeScriptCompletionHandlerShim(ILspSolutionProvider solutionProvider) :
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.TextDocumentCompletionResolveName)]
internal class TypeScriptCompletionResolverHandlerShim : CompletionResolveHandler, ILspRequestHandler<LanguageServer.Protocol.CompletionItem, LanguageServer.Protocol.CompletionItem, Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptCompletionResolverHandlerShim(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public TypeScriptCompletionResolverHandlerShim(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
}
public Task<LanguageServer.Protocol.CompletionItem> HandleAsync(LanguageServer.Protocol.CompletionItem param, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
=> base.HandleRequestAsync(param, new LSP.RequestContext(requestContext.GetClientCapabilities(), null), cancellationToken);
{
var context = this.CreateRequestContext(param, _solutionProvider, requestContext.GetClientCapabilities());
return base.HandleRequestAsync(param, context, cancellationToken);
}
}
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.TextDocumentDocumentSymbolName)]
internal class TypeScriptDocumentSymbolsHandlerShim : DocumentSymbolsHandler, ILspRequestHandler<DocumentSymbolParams, SymbolInformation[], Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptDocumentSymbolsHandlerShim(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public TypeScriptDocumentSymbolsHandlerShim(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
}
public async Task<SymbolInformation[]> HandleAsync(DocumentSymbolParams param, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
......@@ -96,7 +108,7 @@ public async Task<SymbolInformation[]> HandleAsync(DocumentSymbolParams param, R
clientCapabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = false;
}
var context = new LSP.RequestContext(clientCapabilities, null);
var context = this.CreateRequestContext(param, _solutionProvider, clientCapabilities);
var response = await base.HandleRequestAsync(param, context, cancellationToken).ConfigureAwait(false);
// Since hierarchicalSupport will never be true, it is safe to cast the response to SymbolInformation[]
......@@ -107,15 +119,22 @@ public async Task<SymbolInformation[]> HandleAsync(DocumentSymbolParams param, R
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.TextDocumentImplementationName)]
internal class TypeScriptFindImplementationsHandlerShim : FindImplementationsHandler, ILspRequestHandler<TextDocumentPositionParams, LanguageServer.Protocol.Location[], Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
private readonly IThreadingContext _threadingContext;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptFindImplementationsHandlerShim(ILspSolutionProvider solutionProvider, IThreadingContext threadingContext) : base(solutionProvider)
=> _threadingContext = threadingContext;
public TypeScriptFindImplementationsHandlerShim(ILspSolutionProvider solutionProvider, IThreadingContext threadingContext)
{
_threadingContext = threadingContext;
_solutionProvider = solutionProvider;
}
public Task<LanguageServer.Protocol.Location[]> HandleAsync(TextDocumentPositionParams request, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
=> base.HandleRequestAsync(request, new LSP.RequestContext(requestContext.GetClientCapabilities(), null), cancellationToken);
{
var context = this.CreateRequestContext(request, _solutionProvider, requestContext.GetClientCapabilities());
return base.HandleRequestAsync(request, context, cancellationToken);
}
protected override async Task FindImplementationsAsync(IFindUsagesService findUsagesService, Document document, int position, SimpleFindUsagesContext context)
{
......@@ -128,42 +147,72 @@ protected override async Task FindImplementationsAsync(IFindUsagesService findUs
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.InitializeName)]
internal class TypeScriptInitializeHandlerShim : InitializeHandler, ILspRequestHandler<InitializeParams, InitializeResult, Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptInitializeHandlerShim([ImportMany] IEnumerable<Lazy<CompletionProvider, CompletionProviderMetadata>> completionProviders,
ILspSolutionProvider solutionProvider) : base(completionProviders)
{
_solutionProvider = solutionProvider;
}
public Task<InitializeResult> HandleAsync(InitializeParams param, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
=> base.HandleRequestAsync(param, new LSP.RequestContext(requestContext.GetClientCapabilities(), null), cancellationToken);
=> base.HandleRequestAsync(param, this.CreateRequestContext(param, _solutionProvider, requestContext.GetClientCapabilities()), cancellationToken);
}
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.TextDocumentSignatureHelpName)]
internal class TypeScriptSignatureHelpHandlerShim : SignatureHelpHandler, ILspRequestHandler<TextDocumentPositionParams, SignatureHelp, Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptSignatureHelpHandlerShim([ImportMany] IEnumerable<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> allProviders,
ILspSolutionProvider solutionProvider) : base(allProviders, solutionProvider)
ILspSolutionProvider solutionProvider) : base(allProviders)
{
_solutionProvider = solutionProvider;
}
public Task<SignatureHelp> HandleAsync(TextDocumentPositionParams param, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
=> base.HandleRequestAsync(param, new LSP.RequestContext(requestContext.GetClientCapabilities(), null), cancellationToken);
{
var context = this.CreateRequestContext(param, _solutionProvider, requestContext.GetClientCapabilities());
return base.HandleRequestAsync(param, context, cancellationToken);
}
}
[ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.WorkspaceSymbolName)]
internal class TypeScriptWorkspaceSymbolsHandlerShim : WorkspaceSymbolsHandler, ILspRequestHandler<WorkspaceSymbolParams, SymbolInformation[], Solution>
{
private readonly ILspSolutionProvider _solutionProvider;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TypeScriptWorkspaceSymbolsHandlerShim(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public TypeScriptWorkspaceSymbolsHandlerShim(ILspSolutionProvider solutionProvider)
{
_solutionProvider = solutionProvider;
}
[JsonRpcMethod(UseSingleObjectParameterDeserialization = true)]
public Task<SymbolInformation[]> HandleAsync(WorkspaceSymbolParams request, RequestContext<Solution> requestContext, CancellationToken cancellationToken)
=> base.HandleRequestAsync(request, new LSP.RequestContext(requestContext.GetClientCapabilities(), null), cancellationToken);
{
var context = this.CreateRequestContext(request, _solutionProvider, requestContext.GetClientCapabilities());
return base.HandleRequestAsync(request, context, cancellationToken);
}
}
/// <summary>
/// Helper methods only used by the above, that can be deleted along with the above
/// </summary>
internal static class Extensions
{
public static LSP.RequestContext CreateRequestContext<TRequest, TResponse>(this IRequestHandler<TRequest, TResponse> requestHandler, TRequest request, ILspSolutionProvider provider, ClientCapabilities clientCapabilities, string? clientName = null)
{
var textDocument = requestHandler.GetTextDocumentIdentifier(request);
var (document, solution) = provider.GetDocumentAndSolution(textDocument, clientName);
return new LSP.RequestContext(solution, clientCapabilities, clientName, document, solutionUpdater: null);
}
}
}
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.LiveShare.CustomProtocol;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.LiveShare.UnitTests
{
public class ClassificationsHandlerTests : AbstractLiveShareRequestHandlerTests
{
[Fact]
public async Task TestClassificationsAsync()
{
var markup =
@"class A
{
void M()
{
{|classify:var|} i = 1;
}
}";
using var workspace = CreateTestWorkspace(markup, out var locations);
var classifyLocation = locations["classify"].First();
var results = await TestHandleAsync<ClassificationParams, object[]>(workspace.CurrentSolution, CreateClassificationParams(classifyLocation), RoslynMethods.ClassificationsName);
AssertJsonEquals(new ClassificationSpan[] { CreateClassificationSpan("keyword", classifyLocation.Range) }, results);
}
[Fact]
public async Task TestClassificationsAsync_WithMappedPath()
{
var markup =
@"class A
{
void M()
{
{|classify:var|} i = 1;
}
}";
using var workspace = CreateTestWorkspace(markup, out var locations);
var classifyLocation = locations["classify"].First();
var guestUri = new Uri("vsls://guestUri");
var results = await TestHandleAsync<ClassificationParams, object[]>(workspace.CurrentSolution,
CreateClassificationParams(classifyLocation, guestUri), RoslynMethods.ClassificationsName, ConversionFunction);
AssertJsonEquals(new ClassificationSpan[] { CreateClassificationSpan("keyword", classifyLocation.Range) }, results);
Uri ConversionFunction(Uri uri) => uri == guestUri ? classifyLocation.Uri : null;
}
private static ClassificationSpan CreateClassificationSpan(string classification, LanguageServer.Protocol.Range range)
=> new ClassificationSpan()
{
Classification = classification,
Range = range
};
private static ClassificationParams CreateClassificationParams(Location location)
=> CreateClassificationParams(location, location.Uri);
private static ClassificationParams CreateClassificationParams(Location location, Uri originalUri)
=> new ClassificationParams()
{
Range = location.Range,
TextDocument = CreateTextDocumentIdentifier(originalUri)
};
}
}
......@@ -9,6 +9,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.LanguageServer.Client;
using Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
......@@ -24,8 +25,8 @@ internal class XamlLanguageServerClient : AbstractLanguageServerClient
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, true)]
public XamlLanguageServerClient(XamlLanguageServerProtocol languageServerProtocol, VisualStudioWorkspace workspace, IDiagnosticService diagnosticService, IAsynchronousOperationListenerProvider listenerProvider)
: base(languageServerProtocol, workspace, diagnosticService, listenerProvider, diagnosticsClientName: null)
public XamlLanguageServerClient(XamlLanguageServerProtocol languageServerProtocol, VisualStudioWorkspace workspace, IDiagnosticService diagnosticService, IAsynchronousOperationListenerProvider listenerProvider, ILspSolutionProvider solutionProvider)
: base(languageServerProtocol, workspace, diagnosticService, listenerProvider, solutionProvider, diagnosticsClientName: null)
{
}
......
......@@ -25,18 +25,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
/// Handle a completion request.
/// </summary>
[Shared]
[ExportLspMethod(Methods.TextDocumentCompletionName, StringConstants.XamlLanguageName)]
internal class CompletionHandler : AbstractRequestHandler<CompletionParams, CompletionItem[]>
[ExportLspMethod(Methods.TextDocumentCompletionName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class CompletionHandler : IRequestHandler<CompletionParams, CompletionItem[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompletionHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public CompletionHandler()
{
}
public override async Task<CompletionItem[]> HandleRequestAsync(CompletionParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request) => request.TextDocument;
public async Task<CompletionItem[]> HandleRequestAsync(CompletionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetTextDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return CreateErrorItem($"Cannot find document in solution!", request.TextDocument.Uri.ToString());
......
......@@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.Xaml.Features.Completion;
using Microsoft.VisualStudio.LanguageServices.Xaml.Implementation.LanguageServer.Extensions;
using Microsoft.VisualStudio.Text.Adornments;
......@@ -26,16 +27,18 @@ namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
/// Handle a completion resolve request to add description.
/// </summary>
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentCompletionResolveName, StringConstants.XamlLanguageName)]
internal class CompletionResolveHandler : AbstractRequestHandler<LSP.CompletionItem, LSP.CompletionItem>
[ExportLspMethod(LSP.Methods.TextDocumentCompletionResolveName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class CompletionResolveHandler : IRequestHandler<LSP.CompletionItem, LSP.CompletionItem>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompletionResolveHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public CompletionResolveHandler()
{
}
public override async Task<LSP.CompletionItem> HandleRequestAsync(LSP.CompletionItem completionItem, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(CompletionItem request) => null;
public async Task<LSP.CompletionItem> HandleRequestAsync(LSP.CompletionItem completionItem, RequestContext context, CancellationToken cancellationToken)
{
if (!(completionItem.Data is CompletionResolveData data))
{
......@@ -43,7 +46,7 @@ public override async Task<LSP.CompletionItem> HandleRequestAsync(LSP.Completion
}
var documentId = DocumentId.CreateFromSerialized(ProjectId.CreateFromSerialized(data.ProjectGuid), data.DocumentGuid);
var document = SolutionProvider.GetCurrentSolutionForMainWorkspace().GetAdditionalDocument(documentId);
var document = context.Solution.GetAdditionalDocument(documentId);
if (document == null)
{
return completionItem;
......
......@@ -19,20 +19,22 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentFoldingRangeName, StringConstants.XamlLanguageName)]
internal class FoldingRangesHandler : AbstractRequestHandler<FoldingRangeParams, FoldingRange[]>
[ExportLspMethod(Methods.TextDocumentFoldingRangeName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class FoldingRangesHandler : IRequestHandler<FoldingRangeParams, FoldingRange[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FoldingRangesHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FoldingRangesHandler()
{
}
public override async Task<FoldingRange[]> HandleRequestAsync(FoldingRangeParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(FoldingRangeParams request) => request.TextDocument;
public async Task<FoldingRange[]> HandleRequestAsync(FoldingRangeParams request, RequestContext context, CancellationToken cancellationToken)
{
var foldingRanges = ArrayBuilder<FoldingRange>.GetInstance();
var document = SolutionProvider.GetTextDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return foldingRanges.ToArrayAndFree();
......
......@@ -16,17 +16,16 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
internal abstract class AbstractFormatDocumentHandlerBase<RequestType, ResponseType> : AbstractRequestHandler<RequestType, ResponseType>
internal abstract class AbstractFormatDocumentHandlerBase<RequestType, ResponseType> : IRequestHandler<RequestType, ResponseType>
{
protected AbstractFormatDocumentHandlerBase(ILspSolutionProvider solutionProvider) : base(solutionProvider)
{
}
public abstract LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(RequestType request);
public abstract Task<ResponseType> HandleRequestAsync(RequestType request, RequestContext context, CancellationToken cancellationToken);
protected async Task<LSP.TextEdit[]> GetTextEditsAsync(LSP.TextDocumentIdentifier documentIdentifier, LSP.FormattingOptions formattingOptions, RequestContext context, CancellationToken cancellationToken, LSP.Range? range = null)
{
using var _ = ArrayBuilder<LSP.TextEdit>.GetInstance(out var edits);
var document = SolutionProvider.GetTextDocument(documentIdentifier, context.ClientName);
var document = context.Document;
var formattingService = document?.Project.LanguageServices.GetService<IXamlFormattingService>();
if (document != null && formattingService != null)
......
......@@ -17,15 +17,17 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.Methods.TextDocumentFormattingName, StringConstants.XamlLanguageName)]
[ExportLspMethod(LSP.Methods.TextDocumentFormattingName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class FormatDocumentHandler : AbstractFormatDocumentHandlerBase<LSP.DocumentFormattingParams, LSP.TextEdit[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FormatDocumentHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FormatDocumentHandler()
{
}
public override LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.DocumentFormattingParams request) => request.TextDocument;
public override Task<LSP.TextEdit[]> HandleRequestAsync(LSP.DocumentFormattingParams request, RequestContext context, CancellationToken cancellationToken)
=> GetTextEditsAsync(request.TextDocument, request.Options, context, cancellationToken);
}
......
......@@ -22,16 +22,18 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentOnTypeFormattingName, StringConstants.XamlLanguageName)]
internal class FormatDocumentOnTypeHandler : AbstractRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]>
[ExportLspMethod(Methods.TextDocumentOnTypeFormattingName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class FormatDocumentOnTypeHandler : IRequestHandler<DocumentOnTypeFormattingParams, TextEdit[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FormatDocumentOnTypeHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FormatDocumentOnTypeHandler()
{
}
public override async Task<TextEdit[]> HandleRequestAsync(DocumentOnTypeFormattingParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(DocumentOnTypeFormattingParams request) => request.TextDocument;
public async Task<TextEdit[]> HandleRequestAsync(DocumentOnTypeFormattingParams request, RequestContext context, CancellationToken cancellationToken)
{
var edits = new ArrayBuilder<TextEdit>();
if (string.IsNullOrEmpty(request.Character))
......@@ -39,7 +41,7 @@ public override async Task<TextEdit[]> HandleRequestAsync(DocumentOnTypeFormatti
return edits.ToArrayAndFree();
}
var document = SolutionProvider.GetTextDocument(request.TextDocument, context.ClientName);
var document = context.Document;
var formattingService = document?.Project.LanguageServices.GetService<IXamlFormattingService>();
if (document != null && formattingService != null)
{
......
......@@ -17,15 +17,17 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentRangeFormattingName, StringConstants.XamlLanguageName)]
[ExportLspMethod(Methods.TextDocumentRangeFormattingName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class FormatDocumentRangeHandler : AbstractFormatDocumentHandlerBase<DocumentRangeFormattingParams, TextEdit[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public FormatDocumentRangeHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public FormatDocumentRangeHandler()
{
}
public override TextDocumentIdentifier? GetTextDocumentIdentifier(DocumentRangeFormattingParams request) => request.TextDocument;
public override Task<TextEdit[]> HandleRequestAsync(DocumentRangeFormattingParams request, RequestContext context, CancellationToken cancellationToken)
=> GetTextEditsAsync(request.TextDocument, request.Options, context, cancellationToken, range: request.Range);
}
......
......@@ -24,18 +24,20 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.TextDocumentHoverName, StringConstants.XamlLanguageName)]
internal class HoverHandler : AbstractRequestHandler<TextDocumentPositionParams, Hover?>
[ExportLspMethod(Methods.TextDocumentHoverName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class HoverHandler : IRequestHandler<TextDocumentPositionParams, Hover?>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public HoverHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public HoverHandler()
{
}
public override async Task<Hover?> HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
public TextDocumentIdentifier? GetTextDocumentIdentifier(TextDocumentPositionParams request) => request.TextDocument;
public async Task<Hover?> HandleRequestAsync(TextDocumentPositionParams request, RequestContext context, CancellationToken cancellationToken)
{
var document = SolutionProvider.GetTextDocument(request.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return null;
......
......@@ -16,7 +16,7 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(Methods.InitializeName, StringConstants.XamlLanguageName)]
[ExportLspMethod(Methods.InitializeName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class InitializeHandler : IRequestHandler<InitializeParams, InitializeResult>
{
[ImportingConstructor]
......@@ -25,6 +25,8 @@ public InitializeHandler()
{
}
public TextDocumentIdentifier? GetTextDocumentIdentifier(InitializeParams request) => null;
public Task<InitializeResult> HandleRequestAsync(InitializeParams request, RequestContext context, CancellationToken cancellationToken)
{
......
......@@ -20,20 +20,22 @@
namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer.Handler
{
[Shared]
[ExportLspMethod(LSP.MSLSPMethods.OnAutoInsertName, StringConstants.XamlLanguageName)]
internal class OnAutoInsertHandler : AbstractRequestHandler<LSP.DocumentOnAutoInsertParams, LSP.DocumentOnAutoInsertResponseItem[]>
[ExportLspMethod(LSP.MSLSPMethods.OnAutoInsertName, mutatesSolutionState: false, StringConstants.XamlLanguageName)]
internal class OnAutoInsertHandler : IRequestHandler<LSP.DocumentOnAutoInsertParams, LSP.DocumentOnAutoInsertResponseItem[]>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public OnAutoInsertHandler(ILspSolutionProvider solutionProvider) : base(solutionProvider)
public OnAutoInsertHandler()
{
}
public override async Task<LSP.DocumentOnAutoInsertResponseItem[]> HandleRequestAsync(LSP.DocumentOnAutoInsertParams autoInsertParams, RequestContext context, CancellationToken cancellationToken)
public LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(LSP.DocumentOnAutoInsertParams request) => request.TextDocument;
public async Task<LSP.DocumentOnAutoInsertResponseItem[]> HandleRequestAsync(LSP.DocumentOnAutoInsertParams autoInsertParams, RequestContext context, CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<LSP.DocumentOnAutoInsertResponseItem>.GetInstance(out var response);
var document = SolutionProvider.GetTextDocument(autoInsertParams.TextDocument, context.ClientName);
var document = context.Document;
if (document == null)
{
return response.ToArray();
......
......@@ -13,6 +13,10 @@ public static class ClassificationTypeNames
/// <summary>
/// Additive classifications types supply additional context to other classifications.
/// </summary>
/// <remarks>
/// NOTE: Any updates to this class should also be reflected in LSP - see
/// Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs
/// </remarks>
public static ImmutableArray<string> AdditiveTypeNames { get; } = ImmutableArray.Create(StaticSymbol);
public const string Comment = "comment";
......
......@@ -28,6 +28,14 @@ public static class Classifier
return GetClassifiedSpans(semanticModel, textSpan, document.Project.Solution.Workspace, cancellationToken);
}
/// <summary>
/// Returns classified spans in ascending <see cref="ClassifiedSpan"/> order.
/// <see cref="ClassifiedSpan"/>s may have the same <see cref="ClassifiedSpan.TextSpan"/>. This occurs when there are multiple
/// <see cref="ClassifiedSpan.ClassificationType"/>s for the same region of code. For example, a reference to a static method
/// will have two spans, one that designates it as a method, and one that designates it as static.
/// <see cref="ClassifiedSpan"/>s may also have overlapping <see cref="ClassifiedSpan.TextSpan"/>s. This occurs when there are
/// strings containing regex and/or escape characters.
/// </summary>
public static IEnumerable<ClassifiedSpan> GetClassifiedSpans(
SemanticModel semanticModel,
TextSpan textSpan,
......
......@@ -30,7 +30,7 @@ public static void RemoveDocument(this Workspace workspace, DocumentId documentI
workspace.TryApplyChanges(newSolution);
}
public static void Updatedocument(this Workspace workspace, DocumentId documentId, SourceText newText)
public static void UpdateDocument(this Workspace workspace, DocumentId documentId, SourceText newText)
{
var oldSolution = workspace.CurrentSolution;
var newSolution = oldSolution.WithDocumentText(documentId, newText);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册