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 !) });