From 7ef666b08638a8fb7eae92732c2bfa71ffffb3b3 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 27 Jul 2020 14:54:09 +1000 Subject: [PATCH 1/4] F1 help for private protected and protected internal go to their expected places --- .../CSharpHelpContextService.cs | 28 ++++++ .../CSharp/Test/F1Help/F1HelpTests.cs | 79 +++++++++++++++++ src/VisualStudio/Core/Test/Help/HelpTests.vb | 86 +++++++++++++++++++ .../VisualBasic/Impl/Help/HelpKeywords.vb | 2 + .../VisualBasicHelpContextService.Visitor.vb | 36 +++++++- 5 files changed, 228 insertions(+), 3 deletions(-) diff --git a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs index f3a3bc1d18da8..5b0f791bf1aab 100644 --- a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs +++ b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs @@ -115,6 +115,7 @@ private bool IsValid(SyntaxToken token, TextSpan span) private string TryGetText(SyntaxToken token, SemanticModel semanticModel, Document document, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken) { if (TryGetTextForContextualKeyword(token, out var text) || + TryGetTextForCombinationKeyword(token, out text) || TryGetTextForKeyword(token, syntaxFacts, out text) || TryGetTextForPreProcessor(token, syntaxFacts, out text) || TryGetTextForSymbol(token, semanticModel, document, cancellationToken, out text) || @@ -286,6 +287,33 @@ private bool TryGetTextForContextualKeyword(SyntaxToken token, out string text) text = null; return false; } + private bool TryGetTextForCombinationKeyword(SyntaxToken token, out string text) + { + // Contextual keywords can appear in any order, and the user could initiate help from either keyword + // so to keep the actual checks simple we just check all 4 combinations + return TryGetTextForCombinationKeyword(token, token.GetPreviousToken(), out text) || + TryGetTextForCombinationKeyword(token, token.GetNextToken(), out text) || + TryGetTextForCombinationKeyword(token.GetPreviousToken(), token, out text) || + TryGetTextForCombinationKeyword(token.GetNextToken(), token, out text); + } + + private bool TryGetTextForCombinationKeyword(SyntaxToken token1, SyntaxToken token2, out string text) + { + if (token1.Kind() == SyntaxKind.PrivateKeyword && token2.Kind() == SyntaxKind.ProtectedKeyword) + { + text = "privateprotected_CSharpKeyword"; + return true; + } + + if (token1.Kind() == SyntaxKind.ProtectedKeyword && token2.Kind() == SyntaxKind.InternalKeyword) + { + text = "protectedinternal_CSharpKeyword"; + return true; + } + + text = null; + return false; + } private bool TryGetTextForKeyword(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) { diff --git a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs index 79cd1b5b7e941..ff4031079ce56 100644 --- a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs +++ b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs @@ -39,6 +39,85 @@ private async Task Test_KeywordAsync(string markup, string expectedText) await TestAsync(markup, expectedText + "_CSharpKeyword"); } + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestInternal() + { + await Test_KeywordAsync( +@"intern[||]al class C +{ +}", "internal"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestProtected() + { + await Test_KeywordAsync( +@"public class C +{ + protec[||]ted void goo(); +}", "protected"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestProtectedInternal1() + { + await Test_KeywordAsync( +@"public class C +{ + internal protec[||]ted void goo(); +}", "protectedinternal"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestProtectedInternal2() + { + await Test_KeywordAsync( +@"public class C +{ + protec[||]ted internal void goo(); +}", "protectedinternal"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestPrivateProtected1() + { + await Test_KeywordAsync( +@"public class C +{ + private protec[||]ted void goo(); +}", "privateprotected"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestPrivateProtected2() + { + await Test_KeywordAsync( +@"public class C +{ + priv[||]ate protected void goo(); +}", "privateprotected"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestPrivateProtected3() + { + await Test_KeywordAsync( +@"public class C +{ + protected priv[||]ate void goo(); +}", "privateprotected"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestPrivateProtected4() + { + await Test_KeywordAsync( +@"public class C +{ + prot[||]ected private void goo(); +}", "privateprotected"); + } + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] public async Task TestVoid() { diff --git a/src/VisualStudio/Core/Test/Help/HelpTests.vb b/src/VisualStudio/Core/Test/Help/HelpTests.vb index caaa789ed06ca..3402169072527 100644 --- a/src/VisualStudio/Core/Test/Help/HelpTests.vb +++ b/src/VisualStudio/Core/Test/Help/HelpTests.vb @@ -21,6 +21,92 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Help End Using End Function + + Public Async Function TestFriend() As Task + Dim text = +Fri[||]end Class G +End Class + + Await TestAsync(text.Value, "vb.Friend") + End Function + + + Public Async Function TestProtected() As Task + Dim text = +Public Class G + Protec[||]ted Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.Protected") + End Function + + + Public Async Function TestProtectedFriend1() As Task + Dim text = +Public Class G + Protec[||]ted Friend Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.ProtectedFriend") + End Function + + + Public Async Function TestProtectedFriend2() As Task + Dim text = +Public Class G + Friend Protec[||]ted Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.ProtectedFriend") + End Function + + + Public Async Function TestPrivateProtected1() As Task + Dim text = +Public Class G + Private Protec[||]ted Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.PrivateProtected") + End Function + + + Public Async Function TestPrivateProtected2() As Task + Dim text = +Public Class G + Priv[||]ate Protected Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.PrivateProtected") + End Function + + + Public Async Function TestPrivateProtected3() As Task + Dim text = +Public Class G + Protected Priv[||]ate Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.PrivateProtected") + End Function + + + Public Async Function TestPrivateProtected4() As Task + Dim text = +Public Class G + Protec[||]ted Private Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.PrivateProtected") + End Function + Public Async Function TestAddHandler1() As Task Dim text = diff --git a/src/VisualStudio/VisualBasic/Impl/Help/HelpKeywords.vb b/src/VisualStudio/VisualBasic/Impl/Help/HelpKeywords.vb index ec8d447132d09..f83ea56ea2d8a 100644 --- a/src/VisualStudio/VisualBasic/Impl/Help/HelpKeywords.vb +++ b/src/VisualStudio/VisualBasic/Impl/Help/HelpKeywords.vb @@ -91,6 +91,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help Friend Const Iterator As String = "vb.Iterator" Friend Const Await As String = "vb.Await" Friend Const Yield As String = "vb.Yield" + Friend Const PrivateProtected As String = "vb.PrivateProtected" + Friend Const ProtectedFriend As String = "vb.ProtectedFriend" End Class diff --git a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb index d2f9581a53729..b4792a3a35222 100644 --- a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb +++ b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb @@ -765,9 +765,39 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help End Sub Private Function SelectModifier(list As SyntaxTokenList) As Boolean - Dim modifier = list.FirstOrDefault(Function(t) t.Span.IntersectsWith(_span)) - If modifier <> Nothing Then - result = Keyword(modifier.Text) + Dim previousToken As SyntaxToken + For i As Integer = 0 To list.Count - 1 + Dim modifier = list(i) + If modifier.Span.IntersectsWith(_span) Then + ' For combination tokens we check current vs previous in any order + If SelectCombinationModifier(previousToken, modifier) OrElse SelectCombinationModifier(modifier, previousToken) Then + Return True + Else + If i < list.Count - 1 Then + Dim nextToken = list(i + 1) + ' Now check current vs next in any order + If SelectCombinationModifier(nextToken, modifier) OrElse SelectCombinationModifier(modifier, nextToken) Then + Return True + End If + End If + + ' Not a combination token, just normal keyword help + result = Keyword(modifier.Text) + Return True + End If + End If + previousToken = modifier + Next + + Return False + End Function + + Private Function SelectCombinationModifier(token1 As SyntaxToken, token2 As SyntaxToken) As Boolean + If token1.Kind() = SyntaxKind.ProtectedKeyword AndAlso token2.Kind() = SyntaxKind.FriendKeyword Then + result = HelpKeywords.ProtectedFriend + Return True + ElseIf token1.Kind() = SyntaxKind.PrivateKeyword AndAlso token2.Kind() = SyntaxKind.ProtectedKeyword Then + result = HelpKeywords.PrivateProtected Return True End If From e79db7f57c0277e33e81a8db5c58b4b16155b20f Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 27 Jul 2020 15:02:42 +1000 Subject: [PATCH 2/4] Clean up code analysis messages --- .../CSharpHelpContextService.cs | 20 +++++++++---------- .../CSharp/Test/F1Help/F1HelpTests.cs | 4 ++-- src/VisualStudio/Core/Test/Help/HelpTests.vb | 2 +- .../VisualBasicHelpContextService.Visitor.vb | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs index 5b0f791bf1aab..42f54602e8914 100644 --- a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs +++ b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs @@ -106,7 +106,7 @@ public override async Task GetHelpTermAsync(Document document, TextSpan return string.Empty; } - private bool IsValid(SyntaxToken token, TextSpan span) + private static bool IsValid(SyntaxToken token, TextSpan span) { // If the token doesn't actually intersect with our position, give up return token.Kind() == SyntaxKind.EndIfDirectiveTrivia || token.Span.IntersectsWith(span); @@ -154,9 +154,9 @@ private bool TryGetTextForSymbol(SyntaxToken token, SemanticModel semanticModel, } // Local: return the name if it's the declaration, otherwise the type - if (symbol is ILocalSymbol && !symbol.DeclaringSyntaxReferences.Any(d => d.GetSyntax().DescendantTokens().Contains(token))) + if (symbol is ILocalSymbol localSymbol && !symbol.DeclaringSyntaxReferences.Any(d => d.GetSyntax().DescendantTokens().Contains(token))) { - symbol = ((ILocalSymbol)symbol).Type; + symbol = localSymbol.Type; } // Range variable: use the type @@ -177,7 +177,7 @@ private bool TryGetTextForSymbol(SyntaxToken token, SemanticModel semanticModel, return symbol != null; } - private bool TryGetTextForOperator(SyntaxToken token, Document document, out string text) + private static bool TryGetTextForOperator(SyntaxToken token, Document document, out string text) { var syntaxFacts = document.GetLanguageService(); if (syntaxFacts.IsOperator(token) || syntaxFacts.IsPredefinedOperator(token) || SyntaxFacts.IsAssignmentExpressionOperatorToken(token.Kind())) @@ -226,7 +226,7 @@ private bool TryGetTextForOperator(SyntaxToken token, Document document, out str return false; } - private bool TryGetTextForPreProcessor(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) + private static bool TryGetTextForPreProcessor(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) { if (syntaxFacts.IsPreprocessorKeyword(token)) { @@ -244,7 +244,7 @@ private bool TryGetTextForPreProcessor(SyntaxToken token, ISyntaxFactsService sy return false; } - private bool TryGetTextForContextualKeyword(SyntaxToken token, out string text) + private static bool TryGetTextForContextualKeyword(SyntaxToken token, out string text) { if (token.Text == "nameof") { @@ -287,7 +287,7 @@ private bool TryGetTextForContextualKeyword(SyntaxToken token, out string text) text = null; return false; } - private bool TryGetTextForCombinationKeyword(SyntaxToken token, out string text) + private static bool TryGetTextForCombinationKeyword(SyntaxToken token, out string text) { // Contextual keywords can appear in any order, and the user could initiate help from either keyword // so to keep the actual checks simple we just check all 4 combinations @@ -297,7 +297,7 @@ private bool TryGetTextForCombinationKeyword(SyntaxToken token, out string text) TryGetTextForCombinationKeyword(token.GetNextToken(), token, out text); } - private bool TryGetTextForCombinationKeyword(SyntaxToken token1, SyntaxToken token2, out string text) + private static bool TryGetTextForCombinationKeyword(SyntaxToken token1, SyntaxToken token2, out string text) { if (token1.Kind() == SyntaxKind.PrivateKeyword && token2.Kind() == SyntaxKind.ProtectedKeyword) { @@ -315,7 +315,7 @@ private bool TryGetTextForCombinationKeyword(SyntaxToken token1, SyntaxToken tok return false; } - private bool TryGetTextForKeyword(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) + private static bool TryGetTextForKeyword(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) { if (token.Kind() == SyntaxKind.InKeyword) { @@ -339,7 +339,7 @@ private bool TryGetTextForKeyword(SyntaxToken token, ISyntaxFactsService syntaxF } if (token.ValueText == "var" && token.IsKind(SyntaxKind.IdentifierToken) && - token.Parent.Parent is VariableDeclarationSyntax && token.Parent == ((VariableDeclarationSyntax)token.Parent.Parent).Type) + token.Parent.Parent is VariableDeclarationSyntax declaration && token.Parent == declaration.Type) { text = "var_CSharpKeyword"; return true; diff --git a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs index ff4031079ce56..cb4820f0a3942 100644 --- a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs +++ b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs @@ -24,7 +24,7 @@ public class F1HelpTests private static readonly ComposableCatalog s_catalog = TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic.WithPart(typeof(CSharpHelpContextService)); private static readonly IExportProviderFactory s_exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory(s_catalog); - private async Task TestAsync(string markup, string expectedText) + private static async Task TestAsync(string markup, string expectedText) { using var workspace = TestWorkspace.CreateCSharp(markup, exportProvider: s_exportProviderFactory.CreateExportProvider()); var caret = workspace.Documents.First().CursorPosition; @@ -34,7 +34,7 @@ private async Task TestAsync(string markup, string expectedText) Assert.Equal(expectedText, actualText); } - private async Task Test_KeywordAsync(string markup, string expectedText) + private static async Task Test_KeywordAsync(string markup, string expectedText) { await TestAsync(markup, expectedText + "_CSharpKeyword"); } diff --git a/src/VisualStudio/Core/Test/Help/HelpTests.vb b/src/VisualStudio/Core/Test/Help/HelpTests.vb index 3402169072527..6c4b737c4b029 100644 --- a/src/VisualStudio/Core/Test/Help/HelpTests.vb +++ b/src/VisualStudio/Core/Test/Help/HelpTests.vb @@ -13,7 +13,7 @@ Imports Roslyn.Utilities Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Help <[UseExportProvider]> Public Class HelpTests - Public Async Function TestAsync(markup As String, expected As String) As Tasks.Task + Public Shared Async Function TestAsync(markup As String, expected As String) As Tasks.Task Using workspace = TestWorkspace.CreateVisualBasic(markup) Dim caret = workspace.Documents.First().CursorPosition Dim service = New VisualBasicHelpContextService() diff --git a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb index b4792a3a35222..684fb33e3890f 100644 --- a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb +++ b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb @@ -15,7 +15,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help Private Class Visitor Inherits VisualBasicSyntaxVisitor - Public result As String = Nothing + Public result As String Private ReadOnly _span As TextSpan Private ReadOnly _semanticModel As SemanticModel Private ReadOnly _service As VisualBasicHelpContextService @@ -30,11 +30,11 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help Me._cancellationToken = cancellationToken End Sub - Private Function Keyword(text As String) As String + Private Shared Function Keyword(text As String) As String Return "vb." + text End Function - Private Function Keyword(kind As SyntaxKind) As String + Private Shared Function Keyword(kind As SyntaxKind) As String Return Keyword(kind.GetText()) End Function From ac7ad93d09ee96f258bec40385ec4eb68f8a9b8d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 27 Jul 2020 15:26:53 +1000 Subject: [PATCH 3/4] Simplify and extract logic --- .../CSharpHelpContextService.cs | 10 +++-- .../VisualBasicHelpContextService.Visitor.vb | 43 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs index 42f54602e8914..68605521b236d 100644 --- a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs +++ b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs @@ -291,10 +291,12 @@ private static bool TryGetTextForCombinationKeyword(SyntaxToken token, out strin { // Contextual keywords can appear in any order, and the user could initiate help from either keyword // so to keep the actual checks simple we just check all 4 combinations - return TryGetTextForCombinationKeyword(token, token.GetPreviousToken(), out text) || - TryGetTextForCombinationKeyword(token, token.GetNextToken(), out text) || - TryGetTextForCombinationKeyword(token.GetPreviousToken(), token, out text) || - TryGetTextForCombinationKeyword(token.GetNextToken(), token, out text); + var previousToken = token.GetPreviousToken(); + var nextToken = token.GetNextToken(); + return TryGetTextForCombinationKeyword(token, previousToken, out text) || + TryGetTextForCombinationKeyword(token, nextToken, out text) || + TryGetTextForCombinationKeyword(previousToken, token, out text) || + TryGetTextForCombinationKeyword(nextToken, token, out text); } private static bool TryGetTextForCombinationKeyword(SyntaxToken token1, SyntaxToken token2, out string text) diff --git a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb index 684fb33e3890f..3cbb8d622b062 100644 --- a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb +++ b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb @@ -765,33 +765,46 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help End Sub Private Function SelectModifier(list As SyntaxTokenList) As Boolean - Dim previousToken As SyntaxToken For i As Integer = 0 To list.Count - 1 Dim modifier = list(i) If modifier.Span.IntersectsWith(_span) Then - ' For combination tokens we check current vs previous in any order - If SelectCombinationModifier(previousToken, modifier) OrElse SelectCombinationModifier(modifier, previousToken) Then - Return True - Else - If i < list.Count - 1 Then - Dim nextToken = list(i + 1) - ' Now check current vs next in any order - If SelectCombinationModifier(nextToken, modifier) OrElse SelectCombinationModifier(modifier, nextToken) Then - Return True - End If - End If - ' Not a combination token, just normal keyword help - result = Keyword(modifier.Text) + If SelectCombinationModifier(modifier, i, list) Then Return True End If + + ' Not a combination token, just normal keyword help + result = Keyword(modifier.Text) + Return True End If - previousToken = modifier Next Return False End Function + Private Function SelectCombinationModifier(token As SyntaxToken, index As Integer, list As SyntaxTokenList) As Boolean + Dim previousToken As SyntaxToken + Dim nextToken As SyntaxToken + + If index > 0 Then + previousToken = list(index - 1) + End If + + If index < list.Count - 1 Then + nextToken = list(index + 1) + End If + + ' For combination tokens we check current vs previous in any order + If SelectCombinationModifier(previousToken, token) OrElse + SelectCombinationModifier(token, previousToken) OrElse + SelectCombinationModifier(nextToken, token) OrElse + SelectCombinationModifier(token, nextToken) Then + Return True + End If + + Return False + End Function + Private Function SelectCombinationModifier(token1 As SyntaxToken, token2 As SyntaxToken) As Boolean If token1.Kind() = SyntaxKind.ProtectedKeyword AndAlso token2.Kind() = SyntaxKind.FriendKeyword Then result = HelpKeywords.ProtectedFriend From 3c89329b3e59563c8b15ec6258994d1efe79cb8f Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 28 Jul 2020 15:55:01 +1000 Subject: [PATCH 4/4] Allow for modifiers in any order with other modifiers in between them because some people are monsters --- .../CSharpHelpContextService.cs | 38 ++++++++---------- .../CSharp/Test/F1Help/F1HelpTests.cs | 22 ++++++++++ src/VisualStudio/Core/Test/Help/HelpTests.vb | 21 ++++++++++ .../VisualBasicHelpContextService.Visitor.vb | 40 ++++++------------- 4 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs index 68605521b236d..810a26486f13f 100644 --- a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs +++ b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs @@ -115,7 +115,7 @@ private static bool IsValid(SyntaxToken token, TextSpan span) private string TryGetText(SyntaxToken token, SemanticModel semanticModel, Document document, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken) { if (TryGetTextForContextualKeyword(token, out var text) || - TryGetTextForCombinationKeyword(token, out text) || + TryGetTextForCombinationKeyword(token, syntaxFacts, out text) || TryGetTextForKeyword(token, syntaxFacts, out text) || TryGetTextForPreProcessor(token, syntaxFacts, out text) || TryGetTextForSymbol(token, semanticModel, document, cancellationToken, out text) || @@ -287,34 +287,28 @@ private static bool TryGetTextForContextualKeyword(SyntaxToken token, out string text = null; return false; } - private static bool TryGetTextForCombinationKeyword(SyntaxToken token, out string text) + private static bool TryGetTextForCombinationKeyword(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) { - // Contextual keywords can appear in any order, and the user could initiate help from either keyword - // so to keep the actual checks simple we just check all 4 combinations - var previousToken = token.GetPreviousToken(); - var nextToken = token.GetNextToken(); - return TryGetTextForCombinationKeyword(token, previousToken, out text) || - TryGetTextForCombinationKeyword(token, nextToken, out text) || - TryGetTextForCombinationKeyword(previousToken, token, out text) || - TryGetTextForCombinationKeyword(nextToken, token, out text); - } - - private static bool TryGetTextForCombinationKeyword(SyntaxToken token1, SyntaxToken token2, out string text) - { - if (token1.Kind() == SyntaxKind.PrivateKeyword && token2.Kind() == SyntaxKind.ProtectedKeyword) + switch (token.Kind()) { - text = "privateprotected_CSharpKeyword"; - return true; - } + case SyntaxKind.PrivateKeyword when ModifiersContains(token, syntaxFacts, SyntaxKind.ProtectedKeyword): + case SyntaxKind.ProtectedKeyword when ModifiersContains(token, syntaxFacts, SyntaxKind.PrivateKeyword): + text = "privateprotected_CSharpKeyword"; + return true; - if (token1.Kind() == SyntaxKind.ProtectedKeyword && token2.Kind() == SyntaxKind.InternalKeyword) - { - text = "protectedinternal_CSharpKeyword"; - return true; + case SyntaxKind.ProtectedKeyword when ModifiersContains(token, syntaxFacts, SyntaxKind.InternalKeyword): + case SyntaxKind.InternalKeyword when ModifiersContains(token, syntaxFacts, SyntaxKind.ProtectedKeyword): + text = "protectedinternal_CSharpKeyword"; + return true; } text = null; return false; + + static bool ModifiersContains(SyntaxToken token, ISyntaxFactsService syntaxFacts, SyntaxKind kind) + { + return syntaxFacts.GetModifiers(token.Parent).Any(t => t.IsKind(kind)); + } } private static bool TryGetTextForKeyword(SyntaxToken token, ISyntaxFactsService syntaxFacts, out string text) diff --git a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs index cb4820f0a3942..6aa1fdf8730f1 100644 --- a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs +++ b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs @@ -118,6 +118,28 @@ await Test_KeywordAsync( }", "privateprotected"); } + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestModifierSoup() + { + await Test_KeywordAsync( + @"public class C +{ + private new prot[||]ected static unsafe void foo() + { + } +}", "privateprotected"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] + public async Task TestModifierSoupField() + { + await Test_KeywordAsync( + @"public class C +{ + new prot[||]ected static unsafe private goo; +}", "privateprotected"); + } + [Fact, Trait(Traits.Feature, Traits.Features.F1Help)] public async Task TestVoid() { diff --git a/src/VisualStudio/Core/Test/Help/HelpTests.vb b/src/VisualStudio/Core/Test/Help/HelpTests.vb index 6c4b737c4b029..d51d201e855cb 100644 --- a/src/VisualStudio/Core/Test/Help/HelpTests.vb +++ b/src/VisualStudio/Core/Test/Help/HelpTests.vb @@ -107,6 +107,27 @@ End Class Await TestAsync(text.Value, "vb.PrivateProtected") End Function + + Public Async Function TestModifierSoup() As Task + Dim text = +Public Class G + Protec[||]ted Async Shared Private Sub M() + End Sub +End Class + + Await TestAsync(text.Value, "vb.PrivateProtected") + End Function + + + Public Async Function TestModifierSoupField() As Task + Dim text = +Public Class G + Private Shadows Shared Prot[||]ected foo as Boolean +End Class + + Await TestAsync(text.Value, "vb.PrivateProtected") + End Function + Public Async Function TestAddHandler1() As Task Dim text = diff --git a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb index 3cbb8d622b062..3c7b4c3e60236 100644 --- a/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb +++ b/src/VisualStudio/VisualBasic/Impl/Help/VisualBasicHelpContextService.Visitor.vb @@ -245,11 +245,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help End Sub Public Overrides Sub VisitFieldDeclaration(node As FieldDeclarationSyntax) - Dim modifier = node.Modifiers.FirstOrDefault(Function(m) m.Span.IntersectsWith(_span)) - - If modifier <> Nothing Then - result = Keyword(modifier.Text) - End If + SelectModifier(node.Modifiers) End Sub Public Overrides Sub VisitForEachStatement(node As ForEachStatementSyntax) @@ -769,7 +765,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help Dim modifier = list(i) If modifier.Span.IntersectsWith(_span) Then - If SelectCombinationModifier(modifier, i, list) Then + If SelectCombinationModifier(modifier, list) Then Return True End If @@ -782,41 +778,31 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Help Return False End Function - Private Function SelectCombinationModifier(token As SyntaxToken, index As Integer, list As SyntaxTokenList) As Boolean - Dim previousToken As SyntaxToken - Dim nextToken As SyntaxToken - - If index > 0 Then - previousToken = list(index - 1) - End If - - If index < list.Count - 1 Then - nextToken = list(index + 1) + Private Function SelectCombinationModifier(token As SyntaxToken, list As SyntaxTokenList) As Boolean + If SelectCombinationModifier(token, list, SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, HelpKeywords.PrivateProtected) Then + Return True End If - ' For combination tokens we check current vs previous in any order - If SelectCombinationModifier(previousToken, token) OrElse - SelectCombinationModifier(token, previousToken) OrElse - SelectCombinationModifier(nextToken, token) OrElse - SelectCombinationModifier(token, nextToken) Then + If SelectCombinationModifier(token, list, SyntaxKind.ProtectedKeyword, SyntaxKind.FriendKeyword, HelpKeywords.ProtectedFriend) Then Return True End If Return False End Function - Private Function SelectCombinationModifier(token1 As SyntaxToken, token2 As SyntaxToken) As Boolean - If token1.Kind() = SyntaxKind.ProtectedKeyword AndAlso token2.Kind() = SyntaxKind.FriendKeyword Then - result = HelpKeywords.ProtectedFriend + Private Function SelectCombinationModifier(token As SyntaxToken, list As SyntaxTokenList, kind1 As SyntaxKind, kind2 As SyntaxKind, helpKeyword As String) As Boolean + If token.IsKind(kind1) AndAlso list.Any(Function(t) t.IsKind(kind2)) Then + result = helpKeyword Return True - ElseIf token1.Kind() = SyntaxKind.PrivateKeyword AndAlso token2.Kind() = SyntaxKind.ProtectedKeyword Then - result = HelpKeywords.PrivateProtected + End If + + If token.IsKind(kind2) AndAlso list.Any(Function(t) t.IsKind(kind1)) Then + result = helpKeyword Return True End If Return False End Function - Public Overrides Sub VisitVariableDeclarator(node As VariableDeclaratorSyntax) Dim bestName = node.Names.FirstOrDefault(Function(n) n.Span.IntersectsWith(_span)) If bestName Is Nothing Then