diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index ddfc4449da288..aaa624648b592 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1542,6 +1542,85 @@ public async Task ShebangNotAsFirstCommentInScript(TestHost testHost) await TestAsync(code, code, testHost, Options.Script, expected); } + [Theory, CombinatorialData] + public async Task IgnoredDirective_01(TestHost testHost) + { + await TestAsync(""" + #:unknown // comment + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("unknown"), + String(" // comment"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + } + + [Theory, CombinatorialData] + public async Task IgnoredDirective_02(TestHost testHost) + { + await TestAsync(""" + #:sdk Test 2.1.0 + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + String(" Test 2.1.0"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + } + + [Theory, CombinatorialData] + public async Task IgnoredDirective_03(TestHost testHost) + { + await TestAsync(""" + #:no-space + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("no-space"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + } + + [Theory, CombinatorialData] + public async Task IgnoredDirective_04(TestHost testHost) + { + await TestAsync($""" + #:sdk{'\t'}Test 2.1.0 + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + String("\tTest 2.1.0"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + } + [Theory, CombinatorialData] public async Task CommentAsMethodBodyContent(TestHost testHost) { diff --git a/src/EditorFeatures/TestUtilities/Classification/FormattedClassification.cs b/src/EditorFeatures/TestUtilities/Classification/FormattedClassification.cs index 0e8032a83013e..45db1c6f8133b 100644 --- a/src/EditorFeatures/TestUtilities/Classification/FormattedClassification.cs +++ b/src/EditorFeatures/TestUtilities/Classification/FormattedClassification.cs @@ -202,6 +202,9 @@ public override string ToString() case "string - escape character": return $"Escape(\"{Text}\")"; + case "preprocessor keyword": + return $"""{nameof(FormattedClassifications.PPKeyword)}("{Text}")"""; + default: var trimmedClassification = ClassificationName; if (trimmedClassification.EndsWith(" name")) diff --git a/src/Workspaces/CSharp/Portable/Classification/Worker.cs b/src/Workspaces/CSharp/Portable/Classification/Worker.cs index 9603852601d07..283c9431f1a19 100644 --- a/src/Workspaces/CSharp/Portable/Classification/Worker.cs +++ b/src/Workspaces/CSharp/Portable/Classification/Worker.cs @@ -249,6 +249,7 @@ private void ClassifyTrivia(SyntaxTrivia trivia, SyntaxTriviaList triviaList) case SyntaxKind.PragmaChecksumDirectiveTrivia: case SyntaxKind.ReferenceDirectiveTrivia: case SyntaxKind.LoadDirectiveTrivia: + case SyntaxKind.IgnoredDirectiveTrivia: case SyntaxKind.NullableDirectiveTrivia: case SyntaxKind.BadDirectiveTrivia: ClassifyPreprocessorDirective((DirectiveTriviaSyntax)trivia.GetStructure()!); diff --git a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs index 6d2efd5fc182f..7ecaaf744ced3 100644 --- a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs +++ b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.Classification; @@ -69,6 +70,9 @@ private void ClassifyPreprocessorDirective(DirectiveTriviaSyntax node) case SyntaxKind.LoadDirectiveTrivia: ClassifyLoadDirective((LoadDirectiveTriviaSyntax)node); break; + case SyntaxKind.IgnoredDirectiveTrivia: + ClassifyIgnoredDirective((IgnoredDirectiveTriviaSyntax)node); + break; case SyntaxKind.NullableDirectiveTrivia: ClassifyNullableDirective((NullableDirectiveTriviaSyntax)node); break; @@ -324,6 +328,29 @@ private void ClassifyLoadDirective(LoadDirectiveTriviaSyntax node) ClassifyDirectiveTrivia(node); } + private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) + { + AddClassification(node.HashToken, ClassificationTypeNames.PreprocessorKeyword); + AddClassification(node.ColonToken, ClassificationTypeNames.PreprocessorKeyword); + + // The first part (separated by whitespace) of content is a "keyword", e.g., 'sdk' in '#:sdk Test'. + // We only recognize some whitespace characters here for simplicity and performance. + if (node.Content.Text.IndexOfAny([' ', '\t']) is > 0 and var firstSpaceIndex) + { + var keywordSpan = new TextSpan(node.Content.SpanStart, firstSpaceIndex); + var stringLiteralSpan = TextSpan.FromBounds(node.Content.SpanStart + firstSpaceIndex, node.Content.FullSpan.End); + + AddClassification(keywordSpan, ClassificationTypeNames.PreprocessorKeyword); + AddClassification(stringLiteralSpan, ClassificationTypeNames.StringLiteral); + } + else + { + AddClassification(node.Content, ClassificationTypeNames.PreprocessorKeyword); + } + + ClassifyDirectiveTrivia(node); + } + private void ClassifyNullableDirective(NullableDirectiveTriviaSyntax node) { AddClassification(node.HashToken, ClassificationTypeNames.PreprocessorKeyword);