From 9e5ebf0c5724033df2ecf24f72cdd6be7ee65574 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 5 May 2025 16:17:42 +0200 Subject: [PATCH 1/6] Fix PPKeyword text that appears in failed tests --- .../TestUtilities/Classification/FormattedClassification.cs | 3 +++ 1 file changed, 3 insertions(+) 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")) From 81ff498f290bf5be5eaf8beae00f9433827cb841 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 5 May 2025 16:21:38 +0200 Subject: [PATCH 2/6] Add basic syntax highlighting --- .../SyntacticClassifierTests.cs | 38 +++++++++++++++++++ .../CSharp/Portable/Classification/Worker.cs | 1 + .../Classification/Worker_Preprocesser.cs | 11 ++++++ 3 files changed, 50 insertions(+) diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index ddfc4449da288..b0ad4fedf5a97 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1542,6 +1542,44 @@ 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(":"), + String("unknown // 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(":"), + String("sdk Test 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/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..3d9440021b9a5 100644 --- a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs +++ b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs @@ -69,6 +69,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 +327,14 @@ private void ClassifyLoadDirective(LoadDirectiveTriviaSyntax node) ClassifyDirectiveTrivia(node); } + private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) + { + AddClassification(node.HashToken, ClassificationTypeNames.PreprocessorKeyword); + AddClassification(node.ColonToken, ClassificationTypeNames.PreprocessorKeyword); + AddClassification(node.Content, ClassificationTypeNames.StringLiteral); + ClassifyDirectiveTrivia(node); + } + private void ClassifyNullableDirective(NullableDirectiveTriviaSyntax node) { AddClassification(node.HashToken, ClassificationTypeNames.PreprocessorKeyword); From a250fb0428884d2f2fc7b8cb5a61bdb13dc196c9 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 6 May 2025 12:57:24 +0200 Subject: [PATCH 3/6] Split classification if possible --- .../SyntacticClassifierTests.cs | 25 +++++++++++++++++-- .../Classification/Worker_Preprocesser.cs | 17 ++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index b0ad4fedf5a97..17e6fdddd8763 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1552,7 +1552,8 @@ await TestAsync(""" testHost, PPKeyword("#"), PPKeyword(":"), - String("unknown // comment"), + PPKeyword("unknown"), + String("// comment"), Identifier("Console"), Operators.Dot, Identifier("Write"), @@ -1571,7 +1572,27 @@ await TestAsync(""" testHost, PPKeyword("#"), PPKeyword(":"), - String("sdk Test 2.1.0"), + 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"), diff --git a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs index 3d9440021b9a5..a4e3d7979a9a6 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; @@ -331,7 +332,21 @@ private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) { AddClassification(node.HashToken, ClassificationTypeNames.PreprocessorKeyword); AddClassification(node.ColonToken, ClassificationTypeNames.PreprocessorKeyword); - AddClassification(node.Content, ClassificationTypeNames.StringLiteral); + + // The first part (separated by whitespace) of content is a "keyword", e.g., 'sdk' in '#:sdk Test'. + if (node.Content.ValueText.IndexOf(' ') is > 0 and var firstSpaceIndex) + { + var keywordSpan = new TextSpan(node.Content.SpanStart, firstSpaceIndex); + var stringLiteralSpan = new TextSpan(node.Content.SpanStart + firstSpaceIndex + 1, node.Content.Span.Length - firstSpaceIndex - 1); + + AddClassification(keywordSpan, ClassificationTypeNames.PreprocessorKeyword); + AddClassification(stringLiteralSpan, ClassificationTypeNames.StringLiteral); + } + else + { + AddClassification(node.Content, ClassificationTypeNames.PreprocessorKeyword); + } + ClassifyDirectiveTrivia(node); } From 0d911c9fb48d42b33ae22688d9f6b0328706e81c Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 6 May 2025 13:10:47 +0200 Subject: [PATCH 4/6] Allow any white space --- .../SyntacticClassifierTests.cs | 20 +++++++++++++++++++ .../Classification/Worker_Preprocesser.cs | 17 +++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index 17e6fdddd8763..6d1ff2b975757 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1601,6 +1601,26 @@ await TestAsync(""" 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("Test 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/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs index a4e3d7979a9a6..c58e8c6fc3577 100644 --- a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs +++ b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs @@ -334,7 +334,7 @@ private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) AddClassification(node.ColonToken, ClassificationTypeNames.PreprocessorKeyword); // The first part (separated by whitespace) of content is a "keyword", e.g., 'sdk' in '#:sdk Test'. - if (node.Content.ValueText.IndexOf(' ') is > 0 and var firstSpaceIndex) + if (TryFindWhitespace(node.Content.ValueText, out var firstSpaceIndex)) { var keywordSpan = new TextSpan(node.Content.SpanStart, firstSpaceIndex); var stringLiteralSpan = new TextSpan(node.Content.SpanStart + firstSpaceIndex + 1, node.Content.Span.Length - firstSpaceIndex - 1); @@ -348,6 +348,21 @@ private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) } ClassifyDirectiveTrivia(node); + + static bool TryFindWhitespace(string text, out int index) + { + for (var i = 0; i < text.Length; i++) + { + if (SyntaxFacts.IsWhitespace(text[i])) + { + index = i; + return true; + } + } + + index = -1; + return false; + } } private void ClassifyNullableDirective(NullableDirectiveTriviaSyntax node) From 6be720a7e5a25b2d3009e29a88ffb6c1c8e5eebb Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 7 May 2025 14:34:28 +0200 Subject: [PATCH 5/6] Simplify --- .../SyntacticClassifierTests.cs | 6 +++--- .../Classification/Worker_Preprocesser.cs | 20 +++---------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index 6d1ff2b975757..aaa624648b592 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1553,7 +1553,7 @@ await TestAsync(""" PPKeyword("#"), PPKeyword(":"), PPKeyword("unknown"), - String("// comment"), + String(" // comment"), Identifier("Console"), Operators.Dot, Identifier("Write"), @@ -1573,7 +1573,7 @@ await TestAsync(""" PPKeyword("#"), PPKeyword(":"), PPKeyword("sdk"), - String("Test 2.1.0"), + String(" Test 2.1.0"), Identifier("Console"), Operators.Dot, Identifier("Write"), @@ -1612,7 +1612,7 @@ await TestAsync($""" PPKeyword("#"), PPKeyword(":"), PPKeyword("sdk"), - String("Test 2.1.0"), + String("\tTest 2.1.0"), Identifier("Console"), Operators.Dot, Identifier("Write"), diff --git a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs index c58e8c6fc3577..1e77732d02aba 100644 --- a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs +++ b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs @@ -334,10 +334,11 @@ private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) AddClassification(node.ColonToken, ClassificationTypeNames.PreprocessorKeyword); // The first part (separated by whitespace) of content is a "keyword", e.g., 'sdk' in '#:sdk Test'. - if (TryFindWhitespace(node.Content.ValueText, out var firstSpaceIndex)) + // 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 = new TextSpan(node.Content.SpanStart + firstSpaceIndex + 1, node.Content.Span.Length - firstSpaceIndex - 1); + var stringLiteralSpan = new TextSpan(node.Content.SpanStart + firstSpaceIndex, node.Content.Span.Length - firstSpaceIndex); AddClassification(keywordSpan, ClassificationTypeNames.PreprocessorKeyword); AddClassification(stringLiteralSpan, ClassificationTypeNames.StringLiteral); @@ -348,21 +349,6 @@ private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) } ClassifyDirectiveTrivia(node); - - static bool TryFindWhitespace(string text, out int index) - { - for (var i = 0; i < text.Length; i++) - { - if (SyntaxFacts.IsWhitespace(text[i])) - { - index = i; - return true; - } - } - - index = -1; - return false; - } } private void ClassifyNullableDirective(NullableDirectiveTriviaSyntax node) From b43fd2d67fc996944cd575e23a7e97d88dccafb3 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 7 May 2025 14:36:24 +0200 Subject: [PATCH 6/6] Simplify further --- .../CSharp/Portable/Classification/Worker_Preprocesser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs index 1e77732d02aba..7ecaaf744ced3 100644 --- a/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs +++ b/src/Workspaces/CSharp/Portable/Classification/Worker_Preprocesser.cs @@ -338,7 +338,7 @@ private void ClassifyIgnoredDirective(IgnoredDirectiveTriviaSyntax node) if (node.Content.Text.IndexOfAny([' ', '\t']) is > 0 and var firstSpaceIndex) { var keywordSpan = new TextSpan(node.Content.SpanStart, firstSpaceIndex); - var stringLiteralSpan = new TextSpan(node.Content.SpanStart + firstSpaceIndex, node.Content.Span.Length - firstSpaceIndex); + var stringLiteralSpan = TextSpan.FromBounds(node.Content.SpanStart + firstSpaceIndex, node.Content.FullSpan.End); AddClassification(keywordSpan, ClassificationTypeNames.PreprocessorKeyword); AddClassification(stringLiteralSpan, ClassificationTypeNames.StringLiteral);