From 55746a95fa5c365a0e2437adadcaa063a06a4a36 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Sat, 7 Dec 2024 13:47:57 -0800 Subject: [PATCH] Add f1 item for global usings --- .../CSharpHelpContextService.cs | 838 +++++++++--------- .../CSharp/Test/F1Help/F1HelpTests.cs | 9 + 2 files changed, 427 insertions(+), 420 deletions(-) diff --git a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs index 20e4dd353019b..bace004b3665d 100644 --- a/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs +++ b/src/VisualStudio/CSharp/Impl/LanguageService/CSharpHelpContextService.cs @@ -23,568 +23,566 @@ using Microsoft.VisualStudio.LanguageServices.Implementation.F1Help; using Roslyn.Utilities; -namespace Microsoft.VisualStudio.LanguageServices.CSharp.LanguageService +namespace Microsoft.VisualStudio.LanguageServices.CSharp.LanguageService; + +[ExportLanguageService(typeof(IHelpContextService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpHelpContextService() : AbstractHelpContextService { - [ExportLanguageService(typeof(IHelpContextService), LanguageNames.CSharp), Shared] - internal class CSharpHelpContextService : AbstractHelpContextService + // This redirects to https://docs.microsoft.com/visualstudio/ide/not-in-toc/default, indicating nothing is found. + private const string NotFoundHelpTerm = "vs.texteditor"; + + public override string Language => "csharp"; + public override string Product => "csharp"; + + private static string Keyword(string text) + => text + "_CSharpKeyword"; + + public override async Task GetHelpTermAsync(Document document, TextSpan span, CancellationToken cancellationToken) { - // This redirects to https://docs.microsoft.com/visualstudio/ide/not-in-toc/default, indicating nothing is found. - private const string NotFoundHelpTerm = "vs.texteditor"; + // For now, find the token under the start of the selection. + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var token = await syntaxTree.GetTouchingTokenAsync(span.Start, cancellationToken, findInsideTrivia: true).ConfigureAwait(false); - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpHelpContextService() + if (token.Span.IntersectsWith(span)) { - } + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(span, cancellationToken).ConfigureAwait(false); - public override string Language => "csharp"; - public override string Product => "csharp"; + var result = TryGetText(token, semanticModel, document, cancellationToken); + if (result is null) + { + var previousToken = token.GetPreviousToken(); + if (previousToken.Span.IntersectsWith(span)) + result = TryGetText(previousToken, semanticModel, document, cancellationToken); + } - private static string Keyword(string text) - => text + "_CSharpKeyword"; + return result ?? NotFoundHelpTerm; + } - public override async Task GetHelpTermAsync(Document document, TextSpan span, CancellationToken cancellationToken) + var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var trivia = root.FindTrivia(span.Start, findInsideTrivia: true); + if (trivia.Span.IntersectsWith(span) && trivia.Kind() == SyntaxKind.PreprocessingMessageTrivia && + trivia.Token.GetAncestor() != null) { - // For now, find the token under the start of the selection. - var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var token = await syntaxTree.GetTouchingTokenAsync(span.Start, cancellationToken, findInsideTrivia: true).ConfigureAwait(false); + return "#region"; + } - if (token.Span.IntersectsWith(span)) - { - var semanticModel = await document.ReuseExistingSpeculativeModelAsync(span, cancellationToken).ConfigureAwait(false); + if (trivia.IsRegularOrDocComment()) + { + // just find the first "word" that intersects with our position + var text = await syntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + var start = span.Start; + var end = span.Start; - var result = TryGetText(token, semanticModel, document, cancellationToken); - if (result is null) - { - var previousToken = token.GetPreviousToken(); - if (previousToken.Span.IntersectsWith(span)) - result = TryGetText(previousToken, semanticModel, document, cancellationToken); - } + var syntaxFacts = document.GetRequiredLanguageService(); + while (start > 0 && syntaxFacts.IsIdentifierPartCharacter(text[start - 1])) + start--; - return result ?? NotFoundHelpTerm; - } + while (end < text.Length - 1 && syntaxFacts.IsIdentifierPartCharacter(text[end])) + end++; - var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var trivia = root.FindTrivia(span.Start, findInsideTrivia: true); - if (trivia.Span.IntersectsWith(span) && trivia.Kind() == SyntaxKind.PreprocessingMessageTrivia && - trivia.Token.GetAncestor() != null) - { - return "#region"; - } + return text.GetSubText(TextSpan.FromBounds(start, end)).ToString(); + } - if (trivia.IsRegularOrDocComment()) - { - // just find the first "word" that intersects with our position - var text = await syntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); - var start = span.Start; - var end = span.Start; + return NotFoundHelpTerm; + } - var syntaxFacts = document.GetRequiredLanguageService(); - while (start > 0 && syntaxFacts.IsIdentifierPartCharacter(text[start - 1])) - start--; + private string? TryGetText(SyntaxToken token, SemanticModel semanticModel, Document document, CancellationToken cancellationToken) + { + if (TryGetTextForSpecialCharacters(token, out var text) || + TryGetTextForContextualKeyword(token, out text) || + TryGetTextForCombinationKeyword(token, out text) || + TryGetTextForPreProcessor(token, out text) || + TryGetTextForKeyword(token, out text) || + TryGetTextForOperator(token, document, out text) || + TryGetTextForSymbol(token, semanticModel, document, cancellationToken, out text)) + { + return text; + } - while (end < text.Length - 1 && syntaxFacts.IsIdentifierPartCharacter(text[end])) - end++; + return null; + } - return text.GetSubText(TextSpan.FromBounds(start, end)).ToString(); - } + private static bool TryGetTextForSpecialCharacters(SyntaxToken token, [NotNullWhen(true)] out string? text) + { + if (token.Kind() + is SyntaxKind.InterpolatedStringStartToken + or SyntaxKind.InterpolatedStringEndToken + or SyntaxKind.InterpolatedRawStringEndToken + or SyntaxKind.InterpolatedStringTextToken + or SyntaxKind.InterpolatedSingleLineRawStringStartToken + or SyntaxKind.InterpolatedMultiLineRawStringStartToken) + { + text = Keyword("$"); + return true; + } - return NotFoundHelpTerm; + if (token.IsVerbatimStringLiteral()) + { + text = Keyword("@"); + return true; } - private string? TryGetText(SyntaxToken token, SemanticModel semanticModel, Document document, CancellationToken cancellationToken) + if (token.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken)) { - if (TryGetTextForSpecialCharacters(token, out var text) || - TryGetTextForContextualKeyword(token, out text) || - TryGetTextForCombinationKeyword(token, out text) || - TryGetTextForPreProcessor(token, out text) || - TryGetTextForKeyword(token, out text) || - TryGetTextForOperator(token, document, out text) || - TryGetTextForSymbol(token, semanticModel, document, cancellationToken, out text)) - { - return text; - } + text = Keyword("@$"); + return true; + } - return null; + if (token.Kind() + is SyntaxKind.Utf8StringLiteralToken + or SyntaxKind.Utf8SingleLineRawStringLiteralToken + or SyntaxKind.Utf8MultiLineRawStringLiteralToken) + { + text = Keyword("Utf8StringLiteral"); + return true; } - private static bool TryGetTextForSpecialCharacters(SyntaxToken token, [NotNullWhen(true)] out string? text) + if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken) { - if (token.Kind() - is SyntaxKind.InterpolatedStringStartToken - or SyntaxKind.InterpolatedStringEndToken - or SyntaxKind.InterpolatedRawStringEndToken - or SyntaxKind.InterpolatedStringTextToken - or SyntaxKind.InterpolatedSingleLineRawStringStartToken - or SyntaxKind.InterpolatedMultiLineRawStringStartToken) - { - text = Keyword("$"); - return true; - } + text = Keyword("RawStringLiteral"); + return true; + } - if (token.IsVerbatimStringLiteral()) - { - text = Keyword("@"); - return true; - } + text = null; + return false; + } - if (token.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken)) - { - text = Keyword("@$"); - return true; - } + private bool TryGetTextForSymbol( + SyntaxToken token, SemanticModel semanticModel, Document document, CancellationToken cancellationToken, + [NotNullWhen(true)] out string? text) + { + ISymbol? symbol = null; + if (token.Parent is TypeArgumentListSyntax) + { + var genericName = token.GetAncestor(); + if (genericName != null) + symbol = semanticModel.GetSymbolInfo(genericName, cancellationToken).Symbol ?? semanticModel.GetTypeInfo(genericName, cancellationToken).Type; + } + else if (token.Parent is NullableTypeSyntax && token.IsKind(SyntaxKind.QuestionToken)) + { + text = "System.Nullable`1"; + return true; + } + else + { + symbol = semanticModel.GetSemanticInfo(token, document.Project.Solution.Services, cancellationToken) + .GetAnySymbol(includeType: true); - if (token.Kind() - is SyntaxKind.Utf8StringLiteralToken - or SyntaxKind.Utf8SingleLineRawStringLiteralToken - or SyntaxKind.Utf8MultiLineRawStringLiteralToken) + if (symbol == null) { - text = Keyword("Utf8StringLiteral"); - return true; + var bindableParent = document.GetRequiredLanguageService().TryGetBindableParent(token); + var overloads = bindableParent != null ? semanticModel.GetMemberGroup(bindableParent) : ImmutableArray.Empty; + symbol = overloads.FirstOrDefault(); } + } - if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken) - { - text = Keyword("RawStringLiteral"); - return true; - } + // Local: return the name if it's the declaration, otherwise the type + if (symbol is ILocalSymbol localSymbol && !symbol.DeclaringSyntaxReferences.Any(static (d, token) => d.GetSyntax().DescendantTokens().Contains(token), token)) + { + symbol = localSymbol.Type; + } + + // Range variable: use the type + if (symbol is IRangeVariableSymbol) + { + var info = semanticModel.GetTypeInfo(token.GetRequiredParent(), cancellationToken); + symbol = info.Type; + } + // Just use syntaxfacts for operators + if (symbol is IMethodSymbol method && method.MethodKind == MethodKind.BuiltinOperator) + { text = null; return false; } - private bool TryGetTextForSymbol( - SyntaxToken token, SemanticModel semanticModel, Document document, CancellationToken cancellationToken, - [NotNullWhen(true)] out string? text) + if (symbol is IDiscardSymbol) { - ISymbol? symbol = null; - if (token.Parent is TypeArgumentListSyntax) - { - var genericName = token.GetAncestor(); - if (genericName != null) - symbol = semanticModel.GetSymbolInfo(genericName, cancellationToken).Symbol ?? semanticModel.GetTypeInfo(genericName, cancellationToken).Type; - } - else if (token.Parent is NullableTypeSyntax && token.IsKind(SyntaxKind.QuestionToken)) - { - text = "System.Nullable`1"; - return true; - } - else - { - symbol = semanticModel.GetSemanticInfo(token, document.Project.Solution.Services, cancellationToken) - .GetAnySymbol(includeType: true); - - if (symbol == null) - { - var bindableParent = document.GetRequiredLanguageService().TryGetBindableParent(token); - var overloads = bindableParent != null ? semanticModel.GetMemberGroup(bindableParent) : ImmutableArray.Empty; - symbol = overloads.FirstOrDefault(); - } - } - - // Local: return the name if it's the declaration, otherwise the type - if (symbol is ILocalSymbol localSymbol && !symbol.DeclaringSyntaxReferences.Any(static (d, token) => d.GetSyntax().DescendantTokens().Contains(token), token)) - { - symbol = localSymbol.Type; - } - - // Range variable: use the type - if (symbol is IRangeVariableSymbol) - { - var info = semanticModel.GetTypeInfo(token.GetRequiredParent(), cancellationToken); - symbol = info.Type; - } - - // Just use syntaxfacts for operators - if (symbol is IMethodSymbol method && method.MethodKind == MethodKind.BuiltinOperator) - { - text = null; - return false; - } + text = Keyword("discard"); + return true; + } - if (symbol is IDiscardSymbol) - { - text = Keyword("discard"); - return true; - } + if (symbol is IPreprocessingSymbol) + { + Debug.Fail("We should have handled that in the preprocessor directive."); + } - if (symbol is IPreprocessingSymbol) - { - Debug.Fail("We should have handled that in the preprocessor directive."); - } + text = FormatSymbol(symbol); + return text != null; + } - text = FormatSymbol(symbol); - return text != null; + private static bool TryGetTextForOperator(SyntaxToken token, Document document, [NotNullWhen(true)] out string? text) + { + if (token.IsKind(SyntaxKind.ExclamationToken) && + token.Parent.IsKind(SyntaxKind.SuppressNullableWarningExpression)) + { + text = Keyword("nullForgiving"); + return true; } - private static bool TryGetTextForOperator(SyntaxToken token, Document document, [NotNullWhen(true)] out string? text) + var syntaxFacts = document.GetRequiredLanguageService(); + if (syntaxFacts.IsOperator(token)) { - if (token.IsKind(SyntaxKind.ExclamationToken) && - token.Parent.IsKind(SyntaxKind.SuppressNullableWarningExpression)) - { - text = Keyword("nullForgiving"); - return true; - } - - var syntaxFacts = document.GetRequiredLanguageService(); - if (syntaxFacts.IsOperator(token)) - { - text = Keyword(syntaxFacts.GetText(token.RawKind)); - return true; - } + text = Keyword(syntaxFacts.GetText(token.RawKind)); + return true; + } - if (token.IsKind(SyntaxKind.ColonColonToken)) - { - text = Keyword("::"); - return true; - } + if (token.IsKind(SyntaxKind.ColonColonToken)) + { + text = Keyword("::"); + return true; + } - if (token.IsKind(SyntaxKind.ColonToken) && token.Parent is NameColonSyntax) - { - text = Keyword("namedParameter"); - return true; - } + if (token.IsKind(SyntaxKind.ColonToken) && token.Parent is NameColonSyntax) + { + text = Keyword("namedParameter"); + return true; + } - if (token.IsKind(SyntaxKind.EqualsToken)) + if (token.IsKind(SyntaxKind.EqualsToken)) + { + if (token.Parent.IsKind(SyntaxKind.EqualsValueClause)) { - if (token.Parent.IsKind(SyntaxKind.EqualsValueClause)) + if (token.Parent.Parent.IsKind(SyntaxKind.Parameter)) { - if (token.Parent.Parent.IsKind(SyntaxKind.Parameter)) - { - text = Keyword("optionalParameter"); - return true; - } - else if (token.Parent.Parent.IsKind(SyntaxKind.PropertyDeclaration)) - { - text = Keyword("propertyInitializer"); - return true; - } - else if (token.Parent.Parent.IsKind(SyntaxKind.EnumMemberDeclaration)) - { - text = Keyword("enum"); - return true; - } - else if (token.Parent.Parent.IsKind(SyntaxKind.VariableDeclarator)) - { - text = Keyword("="); - return true; - } + text = Keyword("optionalParameter"); + return true; } - else if (token.Parent.IsKind(SyntaxKind.NameEquals)) + else if (token.Parent.Parent.IsKind(SyntaxKind.PropertyDeclaration)) { - if (token.Parent.Parent.IsKind(SyntaxKind.AnonymousObjectMemberDeclarator)) - { - text = Keyword("anonymousObject"); - return true; - } - else if (token.Parent.Parent.IsKind(SyntaxKind.UsingDirective)) - { - text = Keyword("using"); - return true; - } - else if (token.Parent.Parent.IsKind(SyntaxKind.AttributeArgument)) - { - text = Keyword("attributeNamedArgument"); - return true; - } + text = Keyword("propertyInitializer"); + return true; } - else if (token.Parent.IsKind(SyntaxKind.LetClause)) + else if (token.Parent.Parent.IsKind(SyntaxKind.EnumMemberDeclaration)) { - text = Keyword("let"); + text = Keyword("enum"); return true; } - else if (token.Parent is XmlAttributeSyntax) + else if (token.Parent.Parent.IsKind(SyntaxKind.VariableDeclarator)) { - // redirects to https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags - text = "see"; + text = Keyword("="); return true; } - - // EqualsToken in assignment expression is handled by syntaxFacts.IsOperator call above. - // Here we try to handle other contexts of EqualsToken. - // If we hit this assert, there is a context of the EqualsToken that's not handled. - // In this case, we currently fallback to https://docs.microsoft.com/dotnet/csharp/language-reference/operators/assignment-operator - Debug.Fail("Falling back to F1 keyword for assignment token."); - text = Keyword("="); - return true; } - - if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.GreaterThanToken) + else if (token.Parent.IsKind(SyntaxKind.NameEquals)) { - if (token.Parent.IsKind(SyntaxKind.FunctionPointerParameterList)) + if (token.Parent.Parent.IsKind(SyntaxKind.AnonymousObjectMemberDeclarator)) + { + text = Keyword("anonymousObject"); + return true; + } + else if (token.Parent.Parent.IsKind(SyntaxKind.UsingDirective)) { - text = Keyword("functionPointer"); + text = Keyword("using"); + return true; + } + else if (token.Parent.Parent.IsKind(SyntaxKind.AttributeArgument)) + { + text = Keyword("attributeNamedArgument"); return true; } } - - if (token.IsKind(SyntaxKind.QuestionToken) && token.Parent is ConditionalExpressionSyntax) + else if (token.Parent.IsKind(SyntaxKind.LetClause)) { - text = Keyword("?"); + text = Keyword("let"); return true; } - - if (token.IsKind(SyntaxKind.EqualsGreaterThanToken)) + else if (token.Parent is XmlAttributeSyntax) { - text = Keyword("=>"); + // redirects to https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags + text = "see"; return true; } - if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.GreaterThanToken && - token.Parent is (kind: SyntaxKind.TypeParameterList or SyntaxKind.TypeArgumentList)) + // EqualsToken in assignment expression is handled by syntaxFacts.IsOperator call above. + // Here we try to handle other contexts of EqualsToken. + // If we hit this assert, there is a context of the EqualsToken that's not handled. + // In this case, we currently fallback to https://docs.microsoft.com/dotnet/csharp/language-reference/operators/assignment-operator + Debug.Fail("Falling back to F1 keyword for assignment token."); + text = Keyword("="); + return true; + } + + if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.GreaterThanToken) + { + if (token.Parent.IsKind(SyntaxKind.FunctionPointerParameterList)) { - text = Keyword("generics"); + text = Keyword("functionPointer"); return true; } - - text = null; - return false; } - private static bool TryGetTextForPreProcessor(SyntaxToken token, [NotNullWhen(true)] out string? text) + if (token.IsKind(SyntaxKind.QuestionToken) && token.Parent is ConditionalExpressionSyntax) { - var syntaxFacts = CSharpSyntaxFacts.Instance; + text = Keyword("?"); + return true; + } - // Several keywords are both normal keywords and preprocessor keywords. So only consider this token a - // pp-keyword if we're actually in a directive. - var directive = token.GetAncestor(); - if (directive != null) - { - if (token.IsKind(SyntaxKind.DefaultKeyword) && token.Parent is LineDirectiveTriviaSyntax) - { - text = Keyword("defaultline"); - return true; - } + if (token.IsKind(SyntaxKind.EqualsGreaterThanToken)) + { + text = Keyword("=>"); + return true; + } - if (syntaxFacts.IsPreprocessorKeyword(token)) - { - text = $"#{token.Text}"; - return true; - } + if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.GreaterThanToken && + token.Parent is (kind: SyntaxKind.TypeParameterList or SyntaxKind.TypeArgumentList)) + { + text = Keyword("generics"); + return true; + } - if (token.Kind() is SyntaxKind.IdentifierToken or SyntaxKind.EndOfDirectiveToken) - { - text = $"#{directive.HashToken.GetNextToken(includeDirectives: true).Text}"; - return true; - } - } + text = null; + return false; + } - text = null; - return false; - } + private static bool TryGetTextForPreProcessor(SyntaxToken token, [NotNullWhen(true)] out string? text) + { + var syntaxFacts = CSharpSyntaxFacts.Instance; - private static bool TryGetTextForContextualKeyword(SyntaxToken token, [NotNullWhen(true)] out string? text) + // Several keywords are both normal keywords and preprocessor keywords. So only consider this token a + // pp-keyword if we're actually in a directive. + var directive = token.GetAncestor(); + if (directive != null) { - if (token.Text == "nameof") + if (token.IsKind(SyntaxKind.DefaultKeyword) && token.Parent is LineDirectiveTriviaSyntax) { - text = Keyword("nameof"); + text = Keyword("defaultline"); return true; } - if (token.IsContextualKeyword()) + if (syntaxFacts.IsPreprocessorKeyword(token)) { - switch (token.Kind()) - { - case SyntaxKind.PartialKeyword: - if (token.Parent.GetAncestorOrThis() != null) - { - text = Keyword("partialmethod"); - return true; - } - else if (token.Parent.GetAncestorOrThis() != null) - { - text = Keyword("partialtype"); - return true; - } - - break; - - case SyntaxKind.WhereKeyword: - text = token.Parent.GetAncestorOrThis() != null - ? Keyword("whereconstraint") - : Keyword("whereclause"); - - return true; - - case SyntaxKind.RequiredKeyword: - text = Keyword("required"); - return true; - } + text = $"#{token.Text}"; + return true; } - else if (token.ValueText is "notnull" or "unmanaged") + + if (token.Kind() is SyntaxKind.IdentifierToken or SyntaxKind.EndOfDirectiveToken) { - if (token.Parent is IdentifierNameSyntax { Parent: TypeConstraintSyntax { Parent: TypeParameterConstraintClauseSyntax } }) - { - text = Keyword(token.ValueText); - return true; - } + text = $"#{directive.HashToken.GetNextToken(includeDirectives: true).Text}"; + return true; } + } - text = null; - return false; + text = null; + return false; + } + + private static bool TryGetTextForContextualKeyword(SyntaxToken token, [NotNullWhen(true)] out string? text) + { + if (token.Text == "nameof") + { + text = Keyword("nameof"); + return true; } - private static bool TryGetTextForCombinationKeyword(SyntaxToken token, [NotNullWhen(true)] out string? text) + + if (token.IsContextualKeyword()) { switch (token.Kind()) { - case SyntaxKind.PrivateKeyword when ModifiersContains(token, SyntaxKind.ProtectedKeyword): - case SyntaxKind.ProtectedKeyword when ModifiersContains(token, SyntaxKind.PrivateKeyword): - text = Keyword("privateprotected"); - return true; + case SyntaxKind.PartialKeyword: + if (token.Parent.GetAncestorOrThis() != null) + { + text = Keyword("partialmethod"); + return true; + } + else if (token.Parent.GetAncestorOrThis() != null) + { + text = Keyword("partialtype"); + return true; + } - case SyntaxKind.ProtectedKeyword when ModifiersContains(token, SyntaxKind.InternalKeyword): - case SyntaxKind.InternalKeyword when ModifiersContains(token, SyntaxKind.ProtectedKeyword): - text = Keyword("protectedinternal"); - return true; + break; + + case SyntaxKind.WhereKeyword: + text = token.Parent.GetAncestorOrThis() != null + ? Keyword("whereconstraint") + : Keyword("whereclause"); - case SyntaxKind.UsingKeyword when token.Parent is UsingDirectiveSyntax: - text = token.GetNextToken().IsKind(SyntaxKind.StaticKeyword) - ? Keyword("using-static") - : Keyword("using"); - return true; - case SyntaxKind.StaticKeyword when token.Parent is UsingDirectiveSyntax: - text = Keyword("using-static"); return true; - case SyntaxKind.ReturnKeyword when token.Parent.IsKind(SyntaxKind.YieldReturnStatement): - case SyntaxKind.BreakKeyword when token.Parent.IsKind(SyntaxKind.YieldBreakStatement): - text = Keyword("yield"); + + case SyntaxKind.RequiredKeyword: + text = Keyword("required"); return true; } - - text = null; - return false; - - static bool ModifiersContains(SyntaxToken token, SyntaxKind kind) + } + else if (token.ValueText is "notnull" or "unmanaged") + { + if (token.Parent is IdentifierNameSyntax { Parent: TypeConstraintSyntax { Parent: TypeParameterConstraintClauseSyntax } }) { - return CSharpSyntaxFacts.Instance.GetModifiers(token.Parent).Any(t => t.IsKind(kind)); + text = Keyword(token.ValueText); + return true; } } - private static bool TryGetTextForKeyword(SyntaxToken token, [NotNullWhen(true)] out string? text) + text = null; + return false; + } + private static bool TryGetTextForCombinationKeyword(SyntaxToken token, [NotNullWhen(true)] out string? text) + { + switch (token.Kind()) { - if (token.IsKind(SyntaxKind.InKeyword)) - { - if (token.GetAncestor() != null) - { - text = Keyword("from"); - return true; - } + case SyntaxKind.PrivateKeyword when ModifiersContains(token, SyntaxKind.ProtectedKeyword): + case SyntaxKind.ProtectedKeyword when ModifiersContains(token, SyntaxKind.PrivateKeyword): + text = Keyword("privateprotected"); + return true; - if (token.GetAncestor() != null) - { - text = Keyword("join"); - return true; - } - } + case SyntaxKind.ProtectedKeyword when ModifiersContains(token, SyntaxKind.InternalKeyword): + case SyntaxKind.InternalKeyword when ModifiersContains(token, SyntaxKind.ProtectedKeyword): + text = Keyword("protectedinternal"); + return true; - if (token.IsKind(SyntaxKind.DefaultKeyword)) - { - if (token.Parent is DefaultConstraintSyntax) - { - text = Keyword("defaultconstraint"); - return true; - } + case SyntaxKind.UsingKeyword when token.Parent is UsingDirectiveSyntax: + text = token.GetNextToken().IsKind(SyntaxKind.StaticKeyword) + ? Keyword("using-static") + : Keyword("using"); + return true; + case SyntaxKind.StaticKeyword when token.Parent is UsingDirectiveSyntax: + text = Keyword("using-static"); + return true; + case SyntaxKind.GlobalKeyword when token.Parent is UsingDirectiveSyntax: + text = Keyword("global-using"); + return true; + case SyntaxKind.ReturnKeyword when token.Parent.IsKind(SyntaxKind.YieldReturnStatement): + case SyntaxKind.BreakKeyword when token.Parent.IsKind(SyntaxKind.YieldBreakStatement): + text = Keyword("yield"); + return true; + } - if (token.Parent is DefaultSwitchLabelSyntax or GotoStatementSyntax) - { - text = Keyword("defaultcase"); - return true; - } - } + text = null; + return false; - if (token.IsKind(SyntaxKind.ClassKeyword) && token.Parent is ClassOrStructConstraintSyntax) - { - text = Keyword("classconstraint"); - return true; - } + static bool ModifiersContains(SyntaxToken token, SyntaxKind kind) + { + return CSharpSyntaxFacts.Instance.GetModifiers(token.Parent).Any(t => t.IsKind(kind)); + } + } - if (token.IsKind(SyntaxKind.StructKeyword) && token.Parent is ClassOrStructConstraintSyntax) + private static bool TryGetTextForKeyword(SyntaxToken token, [NotNullWhen(true)] out string? text) + { + if (token.IsKind(SyntaxKind.InKeyword)) + { + if (token.GetAncestor() != null) { - text = Keyword("structconstraint"); + text = Keyword("from"); return true; } - if (token.IsKind(SyntaxKind.UsingKeyword) && token.Parent is UsingStatementSyntax or LocalDeclarationStatementSyntax) + if (token.GetAncestor() != null) { - text = Keyword("using-statement"); + text = Keyword("join"); return true; } + } - if (token.IsKind(SyntaxKind.SwitchKeyword) && token.Parent is SwitchExpressionSyntax) + if (token.IsKind(SyntaxKind.DefaultKeyword)) + { + if (token.Parent is DefaultConstraintSyntax) { - text = Keyword("switch-expression"); + text = Keyword("defaultconstraint"); return true; } - if (token.IsKeyword()) + if (token.Parent is DefaultSwitchLabelSyntax or GotoStatementSyntax) { - text = Keyword(token.Text); + text = Keyword("defaultcase"); return true; } + } - if (token.ValueText == "var" && token.IsKind(SyntaxKind.IdentifierToken) && - token.Parent?.Parent is VariableDeclarationSyntax declaration && token.Parent == declaration.Type) - { - text = Keyword("var"); - return true; - } + if (token.IsKind(SyntaxKind.ClassKeyword) && token.Parent is ClassOrStructConstraintSyntax) + { + text = Keyword("classconstraint"); + return true; + } - if (token.IsTypeNamedDynamic()) - { - text = Keyword("dynamic"); - return true; - } + if (token.IsKind(SyntaxKind.StructKeyword) && token.Parent is ClassOrStructConstraintSyntax) + { + text = Keyword("structconstraint"); + return true; + } - text = null; - return false; + if (token.IsKind(SyntaxKind.UsingKeyword) && token.Parent is UsingStatementSyntax or LocalDeclarationStatementSyntax) + { + text = Keyword("using-statement"); + return true; } - private static string FormatNamespaceOrTypeSymbol(INamespaceOrTypeSymbol symbol) + if (token.IsKind(SyntaxKind.SwitchKeyword) && token.Parent is SwitchExpressionSyntax) { - var displayString = symbol.ToDisplayString(TypeFormat); + text = Keyword("switch-expression"); + return true; + } - if (symbol is ITypeSymbol type && type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return "System.Nullable`1"; - } + if (token.IsKeyword()) + { + text = Keyword(token.Text); + return true; + } - if (symbol.GetTypeArguments().Any()) - { - return $"{displayString}`{symbol.GetTypeArguments().Length}"; - } + if (token.ValueText == "var" && token.IsKind(SyntaxKind.IdentifierToken) && + token.Parent?.Parent is VariableDeclarationSyntax declaration && token.Parent == declaration.Type) + { + text = Keyword("var"); + return true; + } - return displayString; + if (token.IsTypeNamedDynamic()) + { + text = Keyword("dynamic"); + return true; } - public override string? FormatSymbol(ISymbol? symbol) + text = null; + return false; + } + + private static string FormatNamespaceOrTypeSymbol(INamespaceOrTypeSymbol symbol) + { + var displayString = symbol.ToDisplayString(TypeFormat); + + if (symbol is ITypeSymbol type && type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { - if (symbol == null) - return null; + return "System.Nullable`1"; + } - if (symbol is ITypeSymbol or INamespaceSymbol) - { - return FormatNamespaceOrTypeSymbol((INamespaceOrTypeSymbol)symbol); - } + if (symbol.GetTypeArguments().Any()) + { + return $"{displayString}`{symbol.GetTypeArguments().Length}"; + } - if (symbol.MatchesKind(SymbolKind.Alias, SymbolKind.Local, SymbolKind.Parameter)) - { - return FormatSymbol(symbol.GetSymbolType()); - } + return displayString; + } - var containingType = FormatNamespaceOrTypeSymbol(symbol.ContainingType); - var name = symbol.ToDisplayString(NameFormat); + public override string? FormatSymbol(ISymbol? symbol) + { + if (symbol == null) + return null; - if (symbol.IsConstructor()) - { - return $"{containingType}.#ctor"; - } + if (symbol is ITypeSymbol or INamespaceSymbol) + { + return FormatNamespaceOrTypeSymbol((INamespaceOrTypeSymbol)symbol); + } - if (symbol.GetTypeArguments().Any()) - { - return $"{containingType}.{name}``{symbol.GetTypeArguments().Length}"; - } + if (symbol.MatchesKind(SymbolKind.Alias, SymbolKind.Local, SymbolKind.Parameter)) + { + return FormatSymbol(symbol.GetSymbolType()); + } - return $"{containingType}.{name}"; + var containingType = FormatNamespaceOrTypeSymbol(symbol.ContainingType); + var name = symbol.ToDisplayString(NameFormat); + + if (symbol.IsConstructor()) + { + return $"{containingType}.#ctor"; } + + if (symbol.GetTypeArguments().Any()) + { + return $"{containingType}.{name}``{symbol.GetTypeArguments().Length}"; + } + + return $"{containingType}.{name}"; } } diff --git a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs index fc030c088c745..c4ce52b975ca0 100644 --- a/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs +++ b/src/VisualStudio/CSharp/Test/F1Help/F1HelpTests.cs @@ -2300,4 +2300,13 @@ await TestAsync( #pragma warning dis[||]able CS0312 """, "#disable"); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/68009")] + public async Task TestGlobalUsing1() + { + await Test_KeywordAsync( + """ + [||]global using System; + """, "global-using"); + } }