private bool IsSimpleImplicitExpression(CompletionParams request, LSPDocumentSnapshot documentSnapshot, TextExtent?wordExtent) { if (string.Equals(request.Context.TriggerCharacter, "@", StringComparison.Ordinal)) { // Completion was triggered with `@` this is always a simple implicit expression return(true); } if (wordExtent == null) { return(false); } if (!wordExtent.Value.IsSignificant) { // Word is only whitespace, definitely not an implicit expresison return(false); } // We need to look at the item before the word because `@` at the beginning of a word is not encapsulated in that word. var leadingWordCharacterIndex = Math.Max(0, wordExtent.Value.Span.Start.Position - 1); var leadingWordCharacter = documentSnapshot.Snapshot[leadingWordCharacterIndex]; if (leadingWordCharacter == '@') { // This means that completion was requested at something like @for|e and the word was "fore" with the previous character index being "@" return(true); } return(false); }
private async Task <CompletionItem> PostProcessCompletionItemAsync( CompletionItem preResolveCompletionItem, CompletionItem resolvedCompletionItem, CompletionRequestContext requestContext, LSPDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) { // This is a special contract between the Visual Studio LSP platform and language servers where if insert text and text edit's are not present // then the "resolve" endpoint is guaranteed to run prior to a completion item's content being comitted. This gives language servers the // opportunity to lazily evaluate text edits which in turn we need to remap. Given text edits generated through this mechanism tend to be // more exntensive we do a full remapping gesture which includes formatting of said text-edits. var shouldRemapTextEdits = preResolveCompletionItem.InsertText is null && preResolveCompletionItem.TextEdit is null; if (!shouldRemapTextEdits) { _logger.LogInformation("No TextEdit remap required."); return(resolvedCompletionItem); } _logger.LogInformation("Start formatting text edit."); var formattingOptions = _formattingOptionsProvider.GetOptions(documentSnapshot); if (resolvedCompletionItem.TextEdit != null) { var containsSnippet = resolvedCompletionItem.InsertTextFormat == InsertTextFormat.Snippet; var remappedEdits = await _documentMappingProvider.RemapFormattedTextEditsAsync( requestContext.ProjectedDocumentUri, new[] { resolvedCompletionItem.TextEdit }, formattingOptions, containsSnippet, cancellationToken).ConfigureAwait(false); // We only passed in a single edit to be remapped var remappedEdit = remappedEdits.Single(); resolvedCompletionItem.TextEdit = remappedEdit; _logger.LogInformation("Formatted text edit."); } if (resolvedCompletionItem.AdditionalTextEdits != null) { var remappedEdits = await _documentMappingProvider.RemapFormattedTextEditsAsync( requestContext.ProjectedDocumentUri, resolvedCompletionItem.AdditionalTextEdits, formattingOptions, containsSnippet : false, // Additional text edits can't contain snippets cancellationToken).ConfigureAwait(false); resolvedCompletionItem.AdditionalTextEdits = remappedEdits; _logger.LogInformation("Formatted additional text edit."); } return(resolvedCompletionItem); }
static CompletionItem[] RemoveDesignTimeItems(LSPDocumentSnapshot documentSnapshot, TextExtent?wordExtent, CompletionItem[] items) { var filteredItems = items.Except(DesignTimeHelpersCompletionItems, CompletionItemComparer.Instance).ToArray(); // If the current identifier starts with "__", only trim out common design time helpers from the list. // In all other cases, trim out both common design time helpers and all completion items starting with "__". if (RemoveAllDesignTimeItems(documentSnapshot, wordExtent)) { filteredItems = filteredItems.Where(item => item.Label != null && !item.Label.StartsWith("__", StringComparison.Ordinal)).ToArray(); } return(filteredItems);
private bool IsSimpleImplicitExpression(LSPDocumentSnapshot documentSnapshot, CompletionParams request, LanguageServerKind serverKind) { if (serverKind != LanguageServerKind.CSharp) { return(false); } if (string.Equals(request.Context.TriggerCharacter, "@", StringComparison.Ordinal)) { // Completion was triggered with `@` this is always a simple implicit expression return(true); } var snapshot = documentSnapshot.Snapshot; var navigator = _textStructureNavigator.GetTextStructureNavigator(snapshot.TextBuffer); var line = snapshot.GetLineFromLineNumber(request.Position.Line); var absoluteIndex = line.Start + request.Position.Character; if (absoluteIndex > snapshot.Length) { Debug.Fail("This should never happen when resolving C# polyfills given we're operating on snapshots."); return(false); } // Lets walk backwards to the character that caused completion (if one triggered it) to ensure that the "GetExtentOfWord" returns // the word we care about and not whitespace following it. For instance: // // @Date|\r\n // // Will return the \r\n as the "word" which is incorrect; however, this is actually fixed in newer VS CoreEditor builds but is behind // the "Use word pattern in LSP completion" preview feature. Once this preview feature flag is the default we can remove this -1. var completionCharacterIndex = Math.Max(0, absoluteIndex - 1); var completionSnapshotPoint = new SnapshotPoint(documentSnapshot.Snapshot, completionCharacterIndex); var wordExtent = navigator.GetExtentOfWord(completionSnapshotPoint); if (!wordExtent.IsSignificant) { // Word is only whitespace, definitely not an implicit expresison return(false); } // We need to look at the item before the word because `@` at the beginning of a word is not encapsulated in that word. var leadingWordCharacterIndex = Math.Max(0, wordExtent.Span.Start.Position - 1); var leadingWordCharacter = snapshot[leadingWordCharacterIndex]; if (leadingWordCharacter == '@') { // This means that completion was requested at something like @for|e and the word was "fore" with the previous character index being "@" return(true); } return(false); }
private async Task <RazorLanguageKind?> GetTriggerCharacterLanguageKindAsync(LSPDocumentSnapshot documentSnapshot, Position positionAfterTriggerChar, string triggerCharacter, CancellationToken cancellationToken) { // request.Character will point to the position after the character that was inserted. // For onTypeFormatting, it makes more sense to look up the projection of the character that was inserted. var line = documentSnapshot.Snapshot.GetLineFromLineNumber(positionAfterTriggerChar.Line); var position = line.Start.Position + positionAfterTriggerChar.Character; var point = new SnapshotPoint(documentSnapshot.Snapshot, position); // Subtract the trigger character length to go back to the position of the trigger character var triggerCharacterPoint = point.Subtract(triggerCharacter.Length); var triggerCharacterLine = documentSnapshot.Snapshot.GetLineFromPosition(triggerCharacterPoint.Position); var triggerCharacterPosition = new Position(triggerCharacterLine.LineNumber, triggerCharacterPoint.Position - triggerCharacterLine.Start.Position); var triggerCharacterProjectionResult = await _projectionProvider.GetProjectionAsync(documentSnapshot, triggerCharacterPosition, cancellationToken).ConfigureAwait(false); return(triggerCharacterProjectionResult?.LanguageKind); }
// We should remove Razor design time helpers from C#'s completion list. If the current identifier being targeted does not start with a double // underscore, we trim out all items starting with "__" from the completion list. If the current identifier does start with a double underscore // (e.g. "__ab[||]"), we only trim out common design time helpers from the completion list. private SumType <CompletionItem[], CompletionList> RemoveDesignTimeItems( LSPDocumentSnapshot documentSnapshot, TextExtent?wordExtent, SumType <CompletionItem[], CompletionList> completionResult) { var result = completionResult.Match <SumType <CompletionItem[], CompletionList> >( items => { items = RemoveDesignTimeItems(documentSnapshot, wordExtent, items); return(items); }, list => { list.Items = RemoveDesignTimeItems(documentSnapshot, wordExtent, list.Items); return(list); }); return(result);
private SumType <CompletionItem[], CompletionList>?MassageCSharpCompletions( CompletionParams request, LSPDocumentSnapshot documentSnapshot, SumType <CompletionItem[], CompletionList> result) { var updatedResult = result; var wordExtent = documentSnapshot.Snapshot.GetWordExtent(request.Position.Line, request.Position.Character, _textStructureNavigator); if (IsSimpleImplicitExpression(request, documentSnapshot, wordExtent)) { updatedResult = DoNotPreselect(updatedResult); updatedResult = IncludeCSharpKeywords(updatedResult); } updatedResult = RemoveDesignTimeItems(documentSnapshot, wordExtent, updatedResult); return(updatedResult); }
public override Task <Range> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) { if (documentSnapshot.Uri != _documentUri) { return(Task.FromResult((Range)null)); } foreach (var mapping in _mappings.OrderBy(d => d.Key)) { if (mapping.Key.Line == position.Line && mapping.Key.Character >= position.Character) { return(Task.FromResult(mapping.Value)); } } return(Task.FromResult((Range)null)); }
public async override Task <Range?> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) { if (documentSnapshot is null) { throw new ArgumentNullException(nameof(documentSnapshot)); } if (position is null) { throw new ArgumentNullException(nameof(position)); } // We initialize the logger here instead of the constructor as the breakpoint span provider is constructed // *before* the language server. Thus, the log hub has yet to be initialized, thus we would be unable to // create the logger at that time. await InitializeLogHubAsync(cancellationToken).ConfigureAwait(false); var languageQueryParams = new RazorBreakpointSpanParams() { Position = position, Uri = documentSnapshot.Uri }; var response = await _requestInvoker.ReinvokeRequestOnServerAsync <RazorBreakpointSpanParams, RazorBreakpointSpanResponse>( documentSnapshot.Snapshot.TextBuffer, LanguageServerConstants.RazorBreakpointSpanEndpoint, RazorLSPConstants.RazorLanguageServerName, CheckRazorBreakpointSpanCapability, languageQueryParams, cancellationToken).ConfigureAwait(false); var languageResponse = response?.Response; if (languageResponse is null) { _logHubLogger?.LogInformation("The breakpoint position could not be mapped to a valid range."); return(null); } return(languageResponse.Range); }
static bool RemoveAllDesignTimeItems(LSPDocumentSnapshot documentSnapshot, TextExtent?wordExtent) { if (!wordExtent.HasValue) { return(true); } var wordSpan = wordExtent.Value.Span; if (wordSpan.Length < 2) { return(true); } var snapshot = documentSnapshot.Snapshot; var startIndex = wordSpan.Start.Position; if (snapshot[startIndex] == '_' && snapshot[startIndex + 1] == '_') { return(false); } return(true); }
public override Task <ProjectionResult> GetProjectionForCompletionAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) => GetProjectionAsync(documentSnapshot, position, cancellationToken);
public abstract void Changed( LSPDocumentSnapshot old, LSPDocumentSnapshot @new, VirtualDocumentSnapshot virtualOld, VirtualDocumentSnapshot virtualNew, LSPDocumentChangeKind kind);
internal async Task <(bool, SumType <CompletionItem[], CompletionList>?)> TryGetProvisionalCompletionsAsync(CompletionParams request, LSPDocumentSnapshot documentSnapshot, ProjectionResult projection, CancellationToken cancellationToken) { SumType <CompletionItem[], CompletionList>?result = null; if (projection.LanguageKind != RazorLanguageKind.Html || request.Context.TriggerKind != CompletionTriggerKind.TriggerCharacter || request.Context.TriggerCharacter != ".") { return(false, result); } if (projection.Position.Character == 0) { // We're at the start of line. Can't have provisional completions here. return(false, result); } var previousCharacterPosition = new Position(projection.Position.Line, projection.Position.Character - 1); var previousCharacterProjection = await _projectionProvider.GetProjectionAsync(documentSnapshot, previousCharacterPosition, cancellationToken).ConfigureAwait(false); if (previousCharacterProjection == null || previousCharacterProjection.LanguageKind != RazorLanguageKind.CSharp) { return(false, result); } if (!(_documentManager is TrackingLSPDocumentManager trackingDocumentManager)) { return(false, result); } // Edit the CSharp projected document to contain a '.'. This allows C# completion to provide valid // completion items for moments when a user has typed a '.' that's typically interpreted as Html. var addProvisionalDot = new VisualStudioTextChange(previousCharacterProjection.PositionIndex, 0, "."); await _joinableTaskFactory.SwitchToMainThreadAsync(); trackingDocumentManager.UpdateVirtualDocument <CSharpVirtualDocument>(documentSnapshot.Uri, new[] { addProvisionalDot }, previousCharacterProjection.HostDocumentVersion); var provisionalCompletionParams = new CompletionParams() { Context = request.Context, Position = new Position(previousCharacterProjection.Position.Line, previousCharacterProjection.Position.Character + 1), TextDocument = new TextDocumentIdentifier() { Uri = previousCharacterProjection.Uri } }; result = await _requestInvoker.ReinvokeRequestOnServerAsync <CompletionParams, SumType <CompletionItem[], CompletionList>?>( Methods.TextDocumentCompletionName, RazorLSPConstants.CSharpContentTypeName, provisionalCompletionParams, cancellationToken).ConfigureAwait(true); // We have now obtained the necessary completion items. We no longer need the provisional change. Revert. var removeProvisionalDot = new VisualStudioTextChange(previousCharacterProjection.PositionIndex, 1, string.Empty); trackingDocumentManager.UpdateVirtualDocument <CSharpVirtualDocument>(documentSnapshot.Uri, new[] { removeProvisionalDot }, previousCharacterProjection.HostDocumentVersion); return(true, result); }
public override async Task <ProjectionResult> GetProjectionAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken) { if (documentSnapshot is null) { throw new ArgumentNullException(nameof(documentSnapshot)); } if (position is null) { throw new ArgumentNullException(nameof(position)); } var languageQueryParams = new RazorLanguageQueryParams() { Position = new Position(position.Line, position.Character), Uri = documentSnapshot.Uri }; var languageResponse = await _requestInvoker.RequestServerAsync <RazorLanguageQueryParams, RazorLanguageQueryResponse>( LanguageServerConstants.RazorLanguageQueryEndpoint, LanguageServerKind.Razor, languageQueryParams, cancellationToken).ConfigureAwait(false); VirtualDocumentSnapshot virtualDocument; if (languageResponse.Kind == RazorLanguageKind.CSharp && documentSnapshot.TryGetVirtualDocument <CSharpVirtualDocumentSnapshot>(out var csharpDoc)) { virtualDocument = csharpDoc; } else if (languageResponse.Kind == RazorLanguageKind.Html && documentSnapshot.TryGetVirtualDocument <HtmlVirtualDocumentSnapshot>(out var htmlDoc)) { virtualDocument = htmlDoc; } else { return(null); } if (languageResponse.HostDocumentVersion == UndefinedDocumentVersion) { // There should always be a document version attached to an open document. // Log it and move on as if it was synchronized. _logger.LogVerbose($"Could not find a document version associated with the document '{documentSnapshot.Uri}'"); } else { var synchronized = await _documentSynchronizer.TrySynchronizeVirtualDocumentAsync(documentSnapshot.Version, virtualDocument, cancellationToken).ConfigureAwait(false); if (!synchronized) { // Could not synchronize return(null); } } var result = new ProjectionResult() { Uri = virtualDocument.Uri, Position = languageResponse.Position, PositionIndex = languageResponse.PositionIndex, LanguageKind = languageResponse.Kind, HostDocumentVersion = languageResponse.HostDocumentVersion }; return(result); }
public abstract Task <Range?> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken);
public override FormattingOptions GetOptions(LSPDocumentSnapshot documentSnapshot) { return(new FormattingOptions()); }