Beispiel #1
0
        public override bool TryMapToProjectedDocumentPosition(RazorCodeDocument codeDocument, int absoluteIndex, out Position projectedPosition, out int projectedIndex)
        {
            if (codeDocument is null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            var csharpDoc = codeDocument.GetCSharpDocument();

            foreach (var mapping in csharpDoc.SourceMappings)
            {
                var originalSpan          = mapping.OriginalSpan;
                var originalAbsoluteIndex = originalSpan.AbsoluteIndex;
                if (originalAbsoluteIndex <= absoluteIndex)
                {
                    // Treat the mapping as owning the edge at its end (hence <= originalSpan.Length),
                    // otherwise we wouldn't handle the cursor being right after the final C# char
                    var distanceIntoOriginalSpan = absoluteIndex - originalAbsoluteIndex;
                    if (distanceIntoOriginalSpan <= originalSpan.Length)
                    {
                        var generatedSource = codeDocument.GetCSharpSourceText();
                        projectedIndex = mapping.GeneratedSpan.AbsoluteIndex + distanceIntoOriginalSpan;
                        var generatedLinePosition = generatedSource.Lines.GetLinePosition(projectedIndex);
                        projectedPosition = new Position(generatedLinePosition.Line, generatedLinePosition.Character);
                        return(true);
                    }
                }
            }

            projectedPosition = default;
            projectedIndex    = default;
            return(false);
        }
        internal static async Task <Range?> TryGetPropertyRangeAsync(RazorCodeDocument codeDocument, string propertyName, RazorDocumentMappingService documentMappingService, ILogger logger, CancellationToken cancellationToken)
        {
            // Parse the C# file and find the property that matches the name.
            // We don't worry about parameter attributes here for two main reasons:
            //   1. We don't have symbolic information, so the best we could do would be checking for any
            //      attribute named Parameter, regardless of which namespace. It also means we would have
            //      to do more checks for all of the various ways that the attribute could be specified
            //      (eg fully qualified, aliased, etc.)
            //   2. Since C# doesn't allow multiple properties with the same name, and we're doing a case
            //      sensitive search, we know the property we find is the one the user is trying to encode in a
            //      tag helper attribute. If they don't have the [Parameter] attribute then the Razor compiler
            //      will error, but allowing them to Go To Def on that property regardless, actually helps
            //      them fix the error.
            var csharpText = codeDocument.GetCSharpSourceText();
            var syntaxTree = CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken);
            var root       = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);

            // Since we know how the compiler generates the C# source we can be a little specific here, and avoid
            // long tree walks. If the compiler ever changes how they generate their code, the tests for this will break
            // so we'll know about it.
            if (root is CompilationUnitSyntax compilationUnit &&
                compilationUnit.Members[0] is NamespaceDeclarationSyntax namespaceDeclaration &&
                namespaceDeclaration.Members[0] is ClassDeclarationSyntax classDeclaration)
            {
                var property = classDeclaration
                               .Members
                               .OfType <PropertyDeclarationSyntax>()
                               .Where(p => p.Identifier.ValueText.Equals(propertyName, StringComparison.Ordinal))
                               .FirstOrDefault();

                if (property is null)
                {
                    // The property probably exists in a partial class
                    logger.LogInformation("Could not find property in the generated source. Comes from partial?");
                    return(null);
                }

                var range = property.Identifier.Span.AsRange(csharpText);
                if (documentMappingService.TryMapFromProjectedDocumentRange(codeDocument, range, out var originalRange))
                {
                    return(originalRange);
                }

                logger.LogInformation("Property found but couldn't map its location.");
            }

            logger.LogInformation("Generated C# was not in expected shape (CompilationUnit -> Namespace -> Class)");

            return(null);
        }
        private static Document GetCSharpDocument(RazorCodeDocument codeDocument, FormattingOptions options)
        {
            var adhocWorkspace = new AdhocWorkspace();
            var csharpOptions  = adhocWorkspace.Options
                                 .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.TabSize, LanguageNames.CSharp, (int)options.TabSize)
                                 .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.IndentationSize, LanguageNames.CSharp, (int)options.TabSize)
                                 .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.UseTabs, LanguageNames.CSharp, !options.InsertSpaces);

            adhocWorkspace.TryApplyChanges(adhocWorkspace.CurrentSolution.WithOptions(csharpOptions));

            var project          = adhocWorkspace.AddProject("TestProject", LanguageNames.CSharp);
            var csharpSourceText = codeDocument.GetCSharpSourceText();
            var csharpDocument   = adhocWorkspace.AddDocument(project.Id, "TestDocument", csharpSourceText);

            return(csharpDocument);
        }
        // Internal for testing only
        internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Range razorRange, [NotNullWhen(true)] out Range?csharpRange)
        {
            SourceSpan?minGeneratedSpan = null;
            SourceSpan?maxGeneratedSpan = null;

            var sourceText = codeDocument.GetSourceText();
            var textSpan   = razorRange.AsTextSpan(sourceText);
            var csharpDoc  = codeDocument.GetCSharpDocument();

            // We want to find the min and max C# source mapping that corresponds with our Razor range.
            foreach (var mapping in csharpDoc.SourceMappings)
            {
                var mappedTextSpan = mapping.OriginalSpan.AsTextSpan();

                if (textSpan.OverlapsWith(mappedTextSpan))
                {
                    if (minGeneratedSpan is null || mapping.GeneratedSpan.AbsoluteIndex < minGeneratedSpan.Value.AbsoluteIndex)
                    {
                        minGeneratedSpan = mapping.GeneratedSpan;
                    }

                    var mappingEndIndex = mapping.GeneratedSpan.AbsoluteIndex + mapping.GeneratedSpan.Length;
                    if (maxGeneratedSpan is null || mappingEndIndex > maxGeneratedSpan.Value.AbsoluteIndex + maxGeneratedSpan.Value.Length)
                    {
                        maxGeneratedSpan = mapping.GeneratedSpan;
                    }
                }
            }

            // Create a new projected range based on our calculated min/max source spans.
            if (minGeneratedSpan is not null && maxGeneratedSpan is not null)
            {
                var csharpSourceText = codeDocument.GetCSharpSourceText();
                var startRange       = minGeneratedSpan.Value.AsTextSpan().AsRange(csharpSourceText);
                var endRange         = maxGeneratedSpan.Value.AsTextSpan().AsRange(csharpSourceText);

                csharpRange = new Range {
                    Start = startRange.Start, End = endRange.End
                };
                Debug.Assert(csharpRange.Start <= csharpRange.End, "Range.Start should not be larger than Range.End");

                return(true);
            }

            csharpRange = null;
            return(false);
        }
        public bool TrySetOutput(
            DefaultDocumentSnapshot document,
            RazorCodeDocument codeDocument,
            VersionStamp inputVersion,
            VersionStamp outputCSharpVersion,
            VersionStamp outputHtmlVersion)
        {
            lock (_setOutputLock)
            {
                if (_inputVersion.HasValue &&
                    _inputVersion.Value != inputVersion &&
                    _inputVersion == _inputVersion.Value.GetNewerVersion(inputVersion))
                {
                    // Latest document is newer than the provided document.
                    return(false);
                }

                if (!document.TryGetText(out var source))
                {
                    Debug.Fail("The text should have already been evaluated.");
                    return(false);
                }

                _source              = source;
                _inputVersion        = inputVersion;
                _outputCSharpVersion = outputCSharpVersion;
                _outputHtmlVersion   = outputHtmlVersion;
                _outputCSharp        = codeDocument.GetCSharpDocument();
                _outputHtml          = codeDocument.GetHtmlDocument();
                _latestDocument      = document;
                var csharpSourceText = codeDocument.GetCSharpSourceText();
                _csharpTextContainer.SetText(csharpSourceText);
                var htmlSourceText = codeDocument.GetHtmlSourceText();
                _htmlTextContainer.SetText(htmlSourceText);

                return(true);
            }
        }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

        public override TextEdit[] GetProjectedDocumentEdits(RazorCodeDocument codeDocument, TextEdit[] edits)
        {
            var projectedEdits         = new List <TextEdit>();
            var csharpSourceText       = codeDocument.GetCSharpSourceText();
            var lastNewLineAddedToLine = 0;

            foreach (var edit in edits)
            {
                var range = edit.Range;
                if (!IsRangeWithinDocument(range, csharpSourceText))
                {
                    continue;
                }

                var startSync = range.Start.TryGetAbsoluteIndex(csharpSourceText, _logger, out var startIndex);
                var endSync   = range.End.TryGetAbsoluteIndex(csharpSourceText, _logger, out var endIndex);
                if (startSync is false || endSync is false)
                {
                    break;
                }

                var mappedStart = TryMapFromProjectedDocumentPosition(codeDocument, startIndex, out var hostDocumentStart, out _);
                var mappedEnd   = TryMapFromProjectedDocumentPosition(codeDocument, endIndex, out var hostDocumentEnd, out _);

                // Ideal case, both start and end can be mapped so just return the edit
                if (mappedStart && mappedEnd)
                {
                    // If the previous edit was on the same line, and added a newline, then we need to add a space
                    // between this edit and the previous one, because the normalization will have swallowed it. See
                    // below for a more info.
                    var newText = (lastNewLineAddedToLine == range.Start.Line ? " " : "") + edit.NewText;
                    projectedEdits.Add(new TextEdit()
                    {
                        NewText = newText,
                        Range   = new Range(hostDocumentStart !, hostDocumentEnd !)
                    });