public override async Task <IReadOnlyList <string> > TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken)
        {
            if (textBuffer is null)
            {
                throw new ArgumentNullException(nameof(textBuffer));
            }

            if (!_fileUriProvider.TryGet(textBuffer, out var documentUri))
            {
                // Not an addressable Razor document. Do not allow expression resolution here. In practice this shouldn't happen, just being defensive.
                return(null);
            }

            if (!_documentManager.TryGetDocument(documentUri, out var documentSnapshot))
            {
                // No associated Razor document. Do not resolve expressions here. In practice this shouldn't happen, just being defensive.
                return(null);
            }

            var cacheKey = new CacheKey(documentSnapshot.Uri, documentSnapshot.Version, lineIndex, characterIndex);

            if (_cache.TryGetValue(cacheKey, out var cachedExpressions))
            {
                // We've seen this request before, no need to go async.
                return(cachedExpressions);
            }

            var lspPosition      = new Position(lineIndex, characterIndex);
            var projectionResult = await _projectionProvider.GetProjectionAsync(documentSnapshot, lspPosition, cancellationToken).ConfigureAwait(false);

            if (projectionResult == null)
            {
                // Can't map the position, invalid breakpoint location.
                return(null);
            }

            cancellationToken.ThrowIfCancellationRequested();

            if (projectionResult.LanguageKind != RazorLanguageKind.CSharp)
            {
                // We only allow proximity expressions in C#
                return(null);
            }

            if (!documentSnapshot.TryGetVirtualDocument <CSharpVirtualDocumentSnapshot>(out var virtualDocument))
            {
                Debug.Fail($"Somehow there's no C# document associated with the host Razor document {documentUri.OriginalString} when retrieving proximity expressions.");
                return(null);
            }

            var syntaxTree = await virtualDocument.GetCSharpSyntaxTreeAsync(_workspace, cancellationToken).ConfigureAwait(false);

            var proximityExpressions = RazorCSharpProximityExpressionResolverService.GetProximityExpressions(syntaxTree, projectionResult.PositionIndex, cancellationToken)?.ToList();

            // Cache range so if we're asked again for this document/line/character we don't have to go async.
            _cache.Set(cacheKey, proximityExpressions);

            return(proximityExpressions);
        }
        public async Task <RazorProximityExpressionsResponse?> Handle(RazorProximityExpressionsParams request, CancellationToken cancellationToken)
        {
            var documentSnapshot = await TryGetDocumentSnapshotAndVersionAsync(request.Uri.GetAbsoluteOrUNCPath(), cancellationToken).ConfigureAwait(false);

            if (documentSnapshot is null)
            {
                return(null);
            }

            var codeDocument = await documentSnapshot.GetGeneratedOutputAsync();

            var sourceText = await documentSnapshot.GetTextAsync();

            var linePosition      = new LinePosition(request.Position.Line, request.Position.Character);
            var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition);

            if (codeDocument.IsUnsupported())
            {
                return(null);
            }

            var projectedIndex = hostDocumentIndex;
            var languageKind   = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex);

            // If we're in C#, then map to the right position in the generated document
            if (languageKind == RazorLanguageKind.CSharp &&
                !_documentMappingService.TryMapToProjectedDocumentPosition(codeDocument, hostDocumentIndex, out _, out projectedIndex))
            {
                return(null);
            }
            // Otherwise see if there is more C# on the line to map to
            else if (languageKind == RazorLanguageKind.Html &&
                     !_documentMappingService.TryMapToProjectedDocumentOrNextCSharpPosition(codeDocument, hostDocumentIndex, out _, out projectedIndex))
            {
                return(null);
            }
            else if (languageKind == RazorLanguageKind.Razor)
            {
                return(null);
            }

            // Now ask Roslyn to adjust the breakpoint to a valid location in the code
            var csharpDocument = codeDocument.GetCSharpDocument();
            var syntaxTree     = CSharpSyntaxTree.ParseText(csharpDocument.GeneratedCode, cancellationToken: cancellationToken);
            var expressions    = RazorCSharpProximityExpressionResolverService.GetProximityExpressions(syntaxTree, projectedIndex, cancellationToken)?.ToList();

            if (expressions == null)
            {
                return(null);
            }

            _logger.LogTrace($"Proximity expressions request for ({request.Position.Line}, {request.Position.Character}) yielded {expressions.Count} results.");

            return(new RazorProximityExpressionsResponse
            {
                Expressions = expressions,
            });
        }
        public override async Task <IReadOnlyList <string> > TryResolveProximityExpressionsAsync(ITextBuffer textBuffer, int lineIndex, int characterIndex, CancellationToken cancellationToken)
        {
            if (textBuffer is null)
            {
                throw new ArgumentNullException(nameof(textBuffer));
            }

            if (!_fileUriProvider.TryGet(textBuffer, out var documentUri))
            {
                // Not an addressable Razor document. Do not allow expression resolution here. In practice this shouldn't happen, just being defensive.
                return(null);
            }

            if (!_documentManager.TryGetDocument(documentUri, out var documentSnapshot))
            {
                // No associated Razor document. Do not resolve expressions here. In practice this shouldn't happen, just being defensive.
                return(null);
            }

            if (!documentSnapshot.TryGetVirtualDocument <CSharpVirtualDocumentSnapshot>(out var virtualDocument))
            {
                Debug.Fail($"Some how there's no C# document associated with the host Razor document {documentUri.OriginalString} when validating breakpoint locations.");
                return(null);
            }

            if (virtualDocument.HostDocumentSyncVersion != documentSnapshot.Version)
            {
                // C# document isn't up-to-date with the Razor document. Because VS' debugging tech is synchronous on the UI thread we have to bail. Ideally we'd wait
                // for the C# document to become "updated"; however, that'd require the UI thread to see that the C# buffer is updated. Because this call path blocks
                // the UI thread the C# document will never update until this path has exited. This means as a user types around the point of interest data may get stale
                // but will re-adjust later.
                return(null);
            }

            var cacheKey = new CacheKey(documentSnapshot.Uri, documentSnapshot.Version, lineIndex, characterIndex);

            if (_cache.TryGetValue(cacheKey, out var cachedExpressions))
            {
                // We've seen this request before, no need to go async.
                return(cachedExpressions);
            }

            var lspPosition      = new Position(lineIndex, characterIndex);
            var projectionResult = await _projectionProvider.GetProjectionAsync(documentSnapshot, lspPosition, cancellationToken).ConfigureAwait(false);

            if (projectionResult == null)
            {
                // Can't map the position, invalid breakpoint location.
                return(null);
            }

            cancellationToken.ThrowIfCancellationRequested();

            if (projectionResult.LanguageKind != RazorLanguageKind.CSharp)
            {
                // We only allow proximity expressions in C#
                return(null);
            }

            var syntaxTree = await virtualDocument.GetCSharpSyntaxTreeAsync(_workspace, cancellationToken).ConfigureAwait(false);

            var proximityExpressions = RazorCSharpProximityExpressionResolverService.GetProximityExpressions(syntaxTree, projectionResult.PositionIndex, cancellationToken)?.ToList();

            // Cache range so if we're asked again for this document/line/character we don't have to go async.
            _cache.Set(cacheKey, proximityExpressions);

            return(proximityExpressions);
        }