diff --git a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs index ee4ae67f0d5ed..f91bac9193f8f 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs @@ -123,16 +123,28 @@ private static async Task TryClassifyContainingMemberSpanAsync( return false; } - var subTextSpan = service.GetMemberBodySpanForSpeculativeBinding(member); - if (subTextSpan.IsEmpty) + var memberBodySpan = service.GetMemberBodySpanForSpeculativeBinding(member); + if (memberBodySpan.IsEmpty) { // Wasn't a member we could reclassify independently. return false; } - var subSpanToTag = new SnapshotSpan( - snapshotSpan.Snapshot, - subTextSpan.Contains(changedSpan) ? subTextSpan.ToSpan() : member.FullSpan.ToSpan()); + // TODO(cyrusn): Unclear what this logic is for. It looks like it's just trying to narrow the span down + // slightly from the full member, just to its body. Unclear if this provides any substantive benefits. But + // keeping for now to preserve long standing logic. + var memberSpanToClassify = memberBodySpan.Contains(changedSpan) + ? memberBodySpan.ToSpan() + : member.FullSpan.ToSpan(); + + // Take the subspan we know we want to classify, and intersect that with the actual span being asked for. + // That way if we're only asking for a portion of a method, we still only classify that, and not the whole + // method. + var finalSpanToClassify = memberSpanToClassify.Intersection(snapshotSpan.Span); + if (finalSpanToClassify is null) + return false; + + var subSpanToTag = new SnapshotSpan(snapshotSpan.Snapshot, finalSpanToClassify.Value); // re-classify only the member we're inside. await ClassifySpansAsync( diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs index f47ac8bd42e69..a000da86c48d9 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/AbstractEmbeddedLanguageClassificationService.cs @@ -106,9 +106,12 @@ public void VisitTokens(SyntaxNode node) private void ProcessToken(SyntaxToken token) { _cancellationToken.ThrowIfCancellationRequested(); - ProcessTriviaList(token.LeadingTrivia); + + // Directives need to be processes as they can contain strings, which then have escapes in them. + if (token.ContainsDirectives) + ProcessTriviaList(token.LeadingTrivia); + ClassifyToken(token); - ProcessTriviaList(token.TrailingTrivia); } private void ClassifyToken(SyntaxToken token) @@ -116,7 +119,7 @@ private void ClassifyToken(SyntaxToken token) if (token.Span.IntersectsWith(_textSpan) && _owner.SyntaxTokenKinds.Contains(token.RawKind)) { var context = new EmbeddedLanguageClassificationContext( - _solutionServices, _project, _semanticModel, token, _options, _owner.Info.VirtualCharService, _result, _cancellationToken); + _solutionServices, _project, _semanticModel, token, _textSpan, _options, _owner.Info.VirtualCharService, _result, _cancellationToken); var classifiers = _owner.GetServices(_semanticModel, token, _cancellationToken); foreach (var classifier in classifiers) @@ -146,7 +149,7 @@ private void ProcessTriviaList(SyntaxTriviaList triviaList) private void ProcessTrivia(SyntaxTrivia trivia) { - if (trivia.HasStructure && trivia.FullSpan.IntersectsWith(_textSpan)) + if (trivia.IsDirective && trivia.FullSpan.IntersectsWith(_textSpan)) VisitTokens(trivia.GetStructure()!); } } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs b/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs index e19607c9f30be..629b8dd02be56 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/Classification/EmbeddedLanguageClassifierContext.cs @@ -8,52 +8,63 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Classification +namespace Microsoft.CodeAnalysis.Classification; + +internal readonly struct EmbeddedLanguageClassificationContext { - internal readonly struct EmbeddedLanguageClassificationContext + internal readonly SolutionServices SolutionServices; + + private readonly SegmentedList _result; + + /// + /// The portion of the string or character token to classify. + /// + private readonly TextSpan _spanToClassify; + + public Project Project { get; } + + /// + /// The string or character token to classify. + /// + public SyntaxToken SyntaxToken { get; } + + /// + /// SemanticModel that is contained in. + /// + public SemanticModel SemanticModel { get; } + + public CancellationToken CancellationToken { get; } + + internal readonly ClassificationOptions Options; + internal readonly IVirtualCharService VirtualCharService; + + internal EmbeddedLanguageClassificationContext( + SolutionServices solutionServices, + Project project, + SemanticModel semanticModel, + SyntaxToken syntaxToken, + TextSpan spanToClassify, + ClassificationOptions options, + IVirtualCharService virtualCharService, + SegmentedList result, + CancellationToken cancellationToken) + { + SolutionServices = solutionServices; + Project = project; + SemanticModel = semanticModel; + SyntaxToken = syntaxToken; + _spanToClassify = spanToClassify; + Options = options; + VirtualCharService = virtualCharService; + _result = result; + CancellationToken = cancellationToken; + } + + public void AddClassification(string classificationType, TextSpan span) { - internal readonly SolutionServices SolutionServices; - - private readonly SegmentedList _result; - - public Project Project { get; } - - /// - /// The string or character token to classify. - /// - public SyntaxToken SyntaxToken { get; } - - /// - /// SemanticModel that is contained in. - /// - public SemanticModel SemanticModel { get; } - - public CancellationToken CancellationToken { get; } - - internal readonly ClassificationOptions Options; - internal readonly IVirtualCharService VirtualCharService; - - internal EmbeddedLanguageClassificationContext( - SolutionServices solutionServices, - Project project, - SemanticModel semanticModel, - SyntaxToken syntaxToken, - ClassificationOptions options, - IVirtualCharService virtualCharService, - SegmentedList result, - CancellationToken cancellationToken) - { - SolutionServices = solutionServices; - Project = project; - SemanticModel = semanticModel; - SyntaxToken = syntaxToken; - Options = options; - VirtualCharService = virtualCharService; - _result = result; - CancellationToken = cancellationToken; - } - - public void AddClassification(string classificationType, TextSpan span) - => _result.Add(new ClassifiedSpan(classificationType, span)); + // Ignore characters that don't intersect with the requested span. That avoids potentially adding lots of + // classifications for portions of a large string that are out of view. + if (span.IntersectsWith(_spanToClassify)) + _result.Add(new ClassifiedSpan(classificationType, span)); } }