diff --git a/src/Compilers/CSharp/Portable/Syntax/DirectiveTriviaSyntax.cs b/src/Compilers/CSharp/Portable/Syntax/DirectiveTriviaSyntax.cs index 3bd5919728659..a55dd5f264b5f 100644 --- a/src/Compilers/CSharp/Portable/Syntax/DirectiveTriviaSyntax.cs +++ b/src/Compilers/CSharp/Portable/Syntax/DirectiveTriviaSyntax.cs @@ -52,6 +52,8 @@ public SyntaxToken DirectiveNameToken return ((LoadDirectiveTriviaSyntax)this).LoadKeyword; case SyntaxKind.ShebangDirectiveTrivia: return ((ShebangDirectiveTriviaSyntax)this).ExclamationToken; + case SyntaxKind.NullableDirectiveTrivia: + return ((NullableDirectiveTriviaSyntax)this).NullableKeyword; default: throw ExceptionUtilities.UnexpectedValue(this.Kind()); } diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs index bf4212806ec05..2dd079d104aa6 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs @@ -247,6 +247,7 @@ public static bool IsPreprocessorDirective(SyntaxKind kind) case SyntaxKind.LoadDirectiveTrivia: case SyntaxKind.BadDirectiveTrivia: case SyntaxKind.ShebangDirectiveTrivia: + case SyntaxKind.NullableDirectiveTrivia: return true; default: return false; diff --git a/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/PreprocessorTests.cs b/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/PreprocessorTests.cs index 96365c11efc94..22fd1c778ec4e 100644 --- a/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/PreprocessorTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/PreprocessorTests.cs @@ -260,6 +260,9 @@ private void VerifyDirectivesSpecial(CSharpSyntaxNode node, params DirectiveInfo Assert.Equal(exp.Text, setting.ValueText); Assert.True(setting.Kind() == SyntaxKind.EnableKeyword || setting.Kind() == SyntaxKind.DisableKeyword); } + Assert.Equal(SyntaxKind.NullableKeyword, nn.DirectiveNameToken.Kind()); + Assert.True(SyntaxFacts.IsPreprocessorDirective(SyntaxKind.NullableDirectiveTrivia)); + Assert.True(SyntaxFacts.IsPreprocessorKeyword(SyntaxKind.NullableKeyword)); break; default: if (null != exp.Text) diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/DisableKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/DisableKeywordRecommenderTests.cs index 59f81c31c6884..7900df221b675 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/DisableKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/DisableKeywordRecommenderTests.cs @@ -2,12 +2,31 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations { public class DisableKeywordRecommenderTests : KeywordRecommenderTests { + [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + [WorkItem(31130, "https://github.com/dotnet/roslyn/issues/31130")] + public async Task TestAfterNullable() + { + await VerifyKeywordAsync(@"#nullable $$"); + } + + [Fact] + [WorkItem(31130, "https://github.com/dotnet/roslyn/issues/31130")] + public async Task TestNotAfterNullableAndNewline() + { + await VerifyAbsenceAsync(@" +#nullable +$$ +"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] public async Task TestNotAtRoot_Interactive() { diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/EnableKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/EnableKeywordRecommenderTests.cs new file mode 100644 index 0000000000000..3115199d09f91 --- /dev/null +++ b/src/EditorFeatures/CSharpTest2/Recommendations/EnableKeywordRecommenderTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations +{ + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public class EnableKeywordRecommenderTests : KeywordRecommenderTests + { + [Fact] + [WorkItem(31130, "https://github.com/dotnet/roslyn/issues/31130")] + public async Task TestAfterNullable() + { + await VerifyKeywordAsync(@"#nullable $$"); + } + + [Fact] + [WorkItem(31130, "https://github.com/dotnet/roslyn/issues/31130")] + public async Task TestNotAfterNullableAndNewline() + { + await VerifyAbsenceAsync(@" +#nullable +$$ +"); + } + + [Fact] + [WorkItem(31130, "https://github.com/dotnet/roslyn/issues/31130")] + public async Task TestNotAfterHash() + { + await VerifyAbsenceAsync(@"#$$"); + } + + [Fact] + public async Task TestNotAtRoot_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, @"$$"); + } + + [Fact] + public async Task TestNotAfterClass_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"class C { } +$$"); + } + + [Fact] + public async Task TestNotAfterGlobalStatement_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"System.Console.WriteLine(); +$$"); + } + + [Fact] + public async Task TestNotAfterGlobalVariableDeclaration_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"int i = 0; +$$"); + } + + [Fact] + public async Task TestNotInUsingAlias() + { + await VerifyAbsenceAsync(@"using Goo = $$"); + } + + [Fact] + public async Task TestNotInEmptyStatement() + { + await VerifyAbsenceAsync(AddInsideMethod(@"$$")); + } + + [Fact] + public async Task TestNotAfterPragma() + { + await VerifyAbsenceAsync(@"#pragma $$"); + } + + [Fact] + public async Task TestNotAfterPragmaWarning() + { + await VerifyAbsenceAsync(@"#pragma warning $$"); + } + } +} diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/NullableKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/NullableKeywordRecommenderTests.cs new file mode 100644 index 0000000000000..29a085bb8ced4 --- /dev/null +++ b/src/EditorFeatures/CSharpTest2/Recommendations/NullableKeywordRecommenderTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations +{ + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public class NullableKeywordRecommenderTests : KeywordRecommenderTests + { + [Fact] + public async Task TestNotAtRoot_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"$$"); + } + + [Fact] + public async Task TestNotAfterClass_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"class C { } +$$"); + } + + [Fact] + public async Task TestNotAfterGlobalStatement_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"System.Console.WriteLine(); +$$"); + } + + [Fact] + public async Task TestNotAfterGlobalVariableDeclaration_Interactive() + { + await VerifyAbsenceAsync(SourceCodeKind.Script, +@"int i = 0; +$$"); + } + + [Fact] + public async Task TestNotInUsingAlias() + { + await VerifyAbsenceAsync( +@"using Goo = $$"); + } + + [Fact] + public async Task TestNotInEmptyStatement() + { + await VerifyAbsenceAsync(AddInsideMethod( +@"$$")); + } + + [Fact] + public async Task TestAfterHash() + { + await VerifyKeywordAsync( +@"#$$"); + } + + [Fact] + public async Task TestAfterHashAndSpace() + { + await VerifyKeywordAsync( +@"# $$"); + } + + [Fact] + public async Task TestNotAfterHashAndNullable() + { + await VerifyAbsenceAsync( +@"#nullable $$"); + } + } +} diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs index 1f44608e59297..9c9ffa8bf1244 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs @@ -56,6 +56,7 @@ private static ImmutableArray> GetKeywo new DynamicKeywordRecommender(), new ElifKeywordRecommender(), new ElseKeywordRecommender(), + new EnableKeywordRecommender(), new EndIfKeywordRecommender(), new EndRegionKeywordRecommender(), new EnumKeywordRecommender(), @@ -96,6 +97,7 @@ private static ImmutableArray> GetKeywo new NameOfKeywordRecommender(), new NamespaceKeywordRecommender(), new NewKeywordRecommender(), + new NullableKeywordRecommender(), new NullKeywordRecommender(), new ObjectKeywordRecommender(), new OnKeywordRecommender(), diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs index 212da7c7f7bc1..a7148f26b18d7 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/DisableKeywordRecommender.cs @@ -14,12 +14,20 @@ public DisableKeywordRecommender() protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) { - // # pragma warning | - // # pragma warning d| var previousToken1 = context.TargetToken; var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); var previousToken3 = previousToken2.GetPreviousToken(includeSkipped: true); + if (previousToken1.Kind() == SyntaxKind.NullableKeyword && + previousToken2.Kind() == SyntaxKind.HashToken) + { + // # nullable | + // # nullable d| + return true; + } + + // # pragma warning | + // # pragma warning d| return previousToken1.Kind() == SyntaxKind.WarningKeyword && previousToken2.Kind() == SyntaxKind.PragmaKeyword && diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs new file mode 100644 index 0000000000000..4e6b59ec0d600 --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/EnableKeywordRecommender.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +{ + internal class EnableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + { + public EnableKeywordRecommender() + : base(SyntaxKind.EnableKeyword, isValidInPreprocessorContext: true) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + var previousToken1 = context.TargetToken; + var previousToken2 = previousToken1.GetPreviousToken(includeSkipped: true); + + // # nullable | + // # nullable e| + return previousToken1.Kind() == SyntaxKind.NullableKeyword && + previousToken2.Kind() == SyntaxKind.HashToken; + } + } +} diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs new file mode 100644 index 0000000000000..274c19c440a11 --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/NullableKeywordRecommender.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders +{ + internal class NullableKeywordRecommender : AbstractSyntacticSingleKeywordRecommender + { + public NullableKeywordRecommender() + : base(SyntaxKind.NullableKeyword, isValidInPreprocessorContext: true) + { + } + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + => context.IsPreProcessorKeywordContext; + } +}