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);
        }
Exemplo n.º 2
0
        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);
        }
Exemplo n.º 5
0
        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);
        }
Exemplo n.º 15
0
 public abstract Task <Range?> GetBreakpointSpanAsync(LSPDocumentSnapshot documentSnapshot, Position position, CancellationToken cancellationToken);
 public override FormattingOptions GetOptions(LSPDocumentSnapshot documentSnapshot)
 {
     return(new FormattingOptions());
 }