diff --git a/src/Compilers/Test/Core/Traits/Traits.cs b/src/Compilers/Test/Core/Traits/Traits.cs index 942c762f2247a..de4093efe2aee 100644 --- a/src/Compilers/Test/Core/Traits/Traits.cs +++ b/src/Compilers/Test/Core/Traits/Traits.cs @@ -290,6 +290,7 @@ public static class Features public const string RemoveUnnecessaryLineContinuation = nameof(RemoveUnnecessaryLineContinuation); public const string Rename = nameof(Rename); public const string RenameTracking = nameof(RenameTracking); + public const string RoslynLSPSnippetConverter = nameof(RoslynLSPSnippetConverter); public const string SignatureHelp = nameof(SignatureHelp); public const string Simplification = nameof(Simplification); public const string SmartIndent = nameof(SmartIndent); diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/AbstractCSharpSnippetCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/AbstractCSharpSnippetCompletionProviderTests.cs new file mode 100644 index 0000000000000..f4681a9ad43ae --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/AbstractCSharpSnippetCompletionProviderTests.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Completion.CompletionProviders.Snippets; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Test.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders.Snippets +{ + public abstract class AbstractCSharpSnippetCompletionProviderTests : AbstractCSharpCompletionProviderTests + { + protected abstract string ItemToCommit { get; } + + protected override TestComposition GetComposition() + => base.GetComposition() + .AddExcludedPartTypes(typeof(IRoslynLSPSnippetExpander)) + .AddParts(typeof(TestRoslynLanguageServerSnippetExpander)); + + internal override Type GetCompletionProviderType() + => typeof(CSharpSnippetCompletionProvider); + } +} diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpConsoleSnippetCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpConsoleSnippetCompletionProviderTests.cs index 5d93fc25af3bc..6ffacd38dfa24 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpConsoleSnippetCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpConsoleSnippetCompletionProviderTests.cs @@ -11,12 +11,9 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders.Snippets { - public class CSharpConsoleSnippetCompletionProviderTests : AbstractCSharpCompletionProviderTests + public class CSharpConsoleSnippetCompletionProviderTests : AbstractCSharpSnippetCompletionProviderTests { - private static readonly string s_itemToCommit = FeaturesResources.Write_to_the_console; - - internal override Type GetCompletionProviderType() - => typeof(CSharpSnippetCompletionProvider); + protected override string ItemToCommit => FeaturesResources.Write_to_the_console; [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] public async Task InsertConsoleSnippetInMethodTest() @@ -40,7 +37,7 @@ public void Method() Console.WriteLine($$); } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -65,7 +62,7 @@ public async Task MethodAsync() await Console.Out.WriteLineAsync($$); } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -84,13 +81,14 @@ public async Task MethodAsync() @"using System; Console.WriteLine($$); + class Program { public async Task MethodAsync() { } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -108,7 +106,7 @@ public async Task MethodAsync() } } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -125,7 +123,7 @@ public async Task MethodAsync() } } "; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -152,7 +150,7 @@ public Program() Console.WriteLine($$); } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } /// @@ -189,7 +187,7 @@ void LocalMethod() } } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } /// @@ -222,7 +220,7 @@ static void Main(string[] args) }; }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } /// @@ -249,7 +247,7 @@ public async Task InsertConsoleSnippetInParenthesizedLambdaExpressionTest() global::System.Console.WriteLine($$); return x == y; };"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -272,7 +270,7 @@ public void Method() }; } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -286,7 +284,7 @@ public void Method() Func f = x => $$; } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -301,7 +299,7 @@ public void Method() } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -326,7 +324,7 @@ public Test(string val) } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -340,7 +338,7 @@ public void Method(int x, $$) } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -354,7 +352,7 @@ public async Task NoConsoleSnippetInRecordDeclarationTest() public string LastName { get; init; } = default!; };"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -369,7 +367,7 @@ public void Method() } }"; - await VerifyItemIsAbsentAsync(markupBeforeCommit, s_itemToCommit); + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -394,7 +392,7 @@ public void Method() Console.WriteLine($$); } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] @@ -419,7 +417,7 @@ public void Method() Console.WriteLine($$); } }"; - await VerifyCustomCommitProviderAsync(markupBeforeCommit, s_itemToCommit, expectedCodeAfterCommit); + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); } } } diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpIfSnippetCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpIfSnippetCompletionProviderTests.cs new file mode 100644 index 0000000000000..3f8ce882649c9 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/CSharpIfSnippetCompletionProviderTests.cs @@ -0,0 +1,380 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.CSharp.Completion.CompletionProviders.Snippets; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders.Snippets +{ + public class CSharpIfSnippetCompletionProviderTests : AbstractCSharpSnippetCompletionProviderTests + { + protected override string ItemToCommit => FeaturesResources.Insert_an_if_statement; + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetInMethodTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + Ins$$ + } +}"; + + var expectedCodeAfterCommit = +@"class Program +{ + public void Method() + { + if (true) + {$$ + } + } +}"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetInGlobalContextTest() + { + var markupBeforeCommit = +@"Ins$$ +"; + + var expectedCodeAfterCommit = +@"if (true) +{$$ +} +"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInBlockNamespaceTest() + { + var markupBeforeCommit = +@" +namespace Namespace +{ + $$ + class Program + { + public async Task MethodAsync() + { + } + } +}"; + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInFileScopedNamespaceTest() + { + var markupBeforeCommit = +@" +namespace Namespace; +$$ +class Program +{ + public async Task MethodAsync() + { + } +} +"; + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetInConstructorTest() + { + var markupBeforeCommit = +@"class Program +{ + public Program() + { + var x = 5; + $$ + } +}"; + + var expectedCodeAfterCommit = +@"class Program +{ + public Program() + { + var x = 5; + if (true) + {$$ + } + } +}"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippettInLocalFunctionTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + var x = 5; + void LocalMethod() + { + $$ + } + } +}"; + + var expectedCodeAfterCommit = +@"class Program +{ + public void Method() + { + var x = 5; + void LocalMethod() + { + if (true) + {$$ + } + } + } +}"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetInAnonymousFunctionTest() + { + var markupBeforeCommit = +@"public delegate void Print(int value); + +static void Main(string[] args) +{ + Print print = delegate(int val) { + $$ + }; + +}"; + + var expectedCodeAfterCommit = +@"public delegate void Print(int value); + +static void Main(string[] args) +{ + Print print = delegate(int val) { + if (true) + {$$ + } + }; + +}"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetInParenthesizedLambdaExpressionTest() + { + var markupBeforeCommit = +@"Func testForEquality = (x, y) => +{ + $$ + return x == y; +};"; + + var expectedCodeAfterCommit = +@"Func testForEquality = (x, y) => +{ + if (true) + {$$ + } + + return x == y; +};"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInSwitchExpression() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + var operation = 2; + + var result = operation switch + { + $$ + 1 => ""Case 1"", + 2 => ""Case 2"", + 3 => ""Case 3"", + 4 => ""Case 4"", + }; + } +}"; + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInSingleLambdaExpression() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + Func f = x => $$; + } +}"; + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInStringTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + var str = ""$$""; + } +}"; + + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInObjectInitializerTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + var str = new Test($$); + } +} + +class Test +{ + private string val; + + public Test(string val) + { + this.val = val; + } +}"; + + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInParameterListTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method(int x, $$) + { + } +}"; + + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInRecordDeclarationTest() + { + var markupBeforeCommit = +@"public record Person +{ + $$ + public string FirstName { get; init; } = default!; + public string LastName { get; init; } = default!; +};"; + + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task NoIfSnippetInVariableDeclarationTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + var x = $$ + } +}"; + + await VerifyItemIsAbsentAsync(markupBeforeCommit, ItemToCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetWithInvocationBeforeAndAfterCursorTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + Wr$$Blah + } +}"; + + var expectedCodeAfterCommit = +@"class Program +{ + public void Method() + { + if (true) + {$$ + } + } +}"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.Completion)] + public async Task InsertIfSnippetWithInvocationUnderscoreBeforeAndAfterCursorTest() + { + var markupBeforeCommit = +@"class Program +{ + public void Method() + { + _Wr$$Blah_ + } +}"; + + var expectedCodeAfterCommit = +@"class Program +{ + public void Method() + { + if (true) + {$$ + } + } +}"; + await VerifyCustomCommitProviderAsync(markupBeforeCommit, ItemToCommit, expectedCodeAfterCommit); + } + } +} diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/TestRoslynLanguageServerSnippetExpander.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/TestRoslynLanguageServerSnippetExpander.cs new file mode 100644 index 0000000000000..8b687e0331897 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/Snippets/TestRoslynLanguageServerSnippetExpander.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Composition; +using System.Text; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Snippets; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders.Snippets +{ + [Export(typeof(IRoslynLSPSnippetExpander))] + [Shared] + internal class TestRoslynLanguageServerSnippetExpander : IRoslynLSPSnippetExpander + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public TestRoslynLanguageServerSnippetExpander() + { + } + + public bool CanExpandSnippet() + { + return true; + } + } +} diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs index 037b1f4a82e31..8318cc717c83d 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManager.cs @@ -9,13 +9,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Completion.Providers.Snippets; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Indentation; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; @@ -38,6 +39,7 @@ internal sealed class CommitManager : IAsyncCompletionCommitManager private readonly ITextView _textView; private readonly IGlobalOptionService _globalOptions; private readonly IThreadingContext _threadingContext; + private readonly RoslynLSPSnippetExpander _roslynLSPSnippetExpander; public IEnumerable PotentialCommitCharacters { @@ -59,12 +61,14 @@ internal CommitManager( ITextView textView, RecentItemsManager recentItemsManager, IGlobalOptionService globalOptions, - IThreadingContext threadingContext) + IThreadingContext threadingContext, + RoslynLSPSnippetExpander roslynLSPSnippetExpander) { _globalOptions = globalOptions; _threadingContext = threadingContext; _recentItemsManager = recentItemsManager; _textView = textView; + _roslynLSPSnippetExpander = roslynLSPSnippetExpander; } /// @@ -237,6 +241,16 @@ private AsyncCompletionData.CommitResult Commit( return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); } + // Specifically for snippets, we use reflection to try and invoke the LanguageServerSnippetExpander's + // TryExpand method and determine if it succeeded or not. + if (SnippetCompletionItem.IsSnippet(roslynItem)) + { + var lspSnippetText = change.Properties[SnippetCompletionItem.LSPSnippetKey]; + + _roslynLSPSnippetExpander.Expand(change.TextChange.Span, lspSnippetText, _textView, triggerSnapshot); + return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None); + } + var textChange = change.TextChange; var triggerSnapshotSpan = new SnapshotSpan(triggerSnapshot, textChange.Span.ToSpan()); var mappedSpan = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive); diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs index 5300bc40eb59a..317c19d9953ea 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CommitManagerProvider.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Snippets; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; @@ -22,17 +23,20 @@ internal class CommitManagerProvider : IAsyncCompletionCommitManagerProvider private readonly IThreadingContext _threadingContext; private readonly RecentItemsManager _recentItemsManager; private readonly IGlobalOptionService _globalOptions; + private readonly RoslynLSPSnippetExpander _roslynLSPSnippetExpander; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CommitManagerProvider( IThreadingContext threadingContext, RecentItemsManager recentItemsManager, - IGlobalOptionService globalOptions) + IGlobalOptionService globalOptions, + RoslynLSPSnippetExpander roslynLSPSnippetExpander) { _threadingContext = threadingContext; _recentItemsManager = recentItemsManager; _globalOptions = globalOptions; + _roslynLSPSnippetExpander = roslynLSPSnippetExpander; } IAsyncCompletionCommitManager? IAsyncCompletionCommitManagerProvider.GetOrCreate(ITextView textView) @@ -42,7 +46,7 @@ public CommitManagerProvider( return null; } - return new CommitManager(textView, _recentItemsManager, _globalOptions, _threadingContext); + return new CommitManager(textView, _recentItemsManager, _globalOptions, _threadingContext, _roslynLSPSnippetExpander); } } } diff --git a/src/EditorFeatures/Core/Snippets/RoslynLSPSnippetExpander.cs b/src/EditorFeatures/Core/Snippets/RoslynLSPSnippetExpander.cs new file mode 100644 index 0000000000000..49bc122b5ab37 --- /dev/null +++ b/src/EditorFeatures/Core/Snippets/RoslynLSPSnippetExpander.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Snippets +{ + [Export(typeof(IRoslynLSPSnippetExpander))] + [Export(typeof(RoslynLSPSnippetExpander))] + internal class RoslynLSPSnippetExpander : IRoslynLSPSnippetExpander + { + private readonly object? _lspSnippetExpander; + private readonly Type? _expanderType; + private readonly MethodInfo? _expanderMethodInfo; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RoslynLSPSnippetExpander( + [Import("Microsoft.VisualStudio.LanguageServer.Client.Snippets.LanguageServerSnippetExpander", AllowDefault = true)] object? languageServerSnippetExpander) + { + _lspSnippetExpander = languageServerSnippetExpander; + + if (_lspSnippetExpander is not null) + { + _expanderType = _lspSnippetExpander.GetType(); + _expanderMethodInfo = _expanderType.GetMethod("TryExpand"); + } + } + + public void Expand(TextSpan textSpan, string lspSnippetText, ITextView textView, ITextSnapshot textSnapshot) + { + Contract.ThrowIfFalse(CanExpandSnippet()); + + var textEdit = new TextEdit() + { + Range = ProtocolConversions.TextSpanToRange(textSpan, textSnapshot.AsText()), + NewText = lspSnippetText + }; + + try + { + // ExpanderMethodInfo should not be null at this point. + var expandMethodResult = _expanderMethodInfo!.Invoke(_lspSnippetExpander, new object[] { textEdit, textView, textSnapshot }); + if (expandMethodResult is not bool resultValue) + { + throw new Exception("The result of the invoked LSP snippet expander was not a boolean."); + } + + if (!resultValue) + { + throw new Exception("The invoked LSP snippet expander came back as false."); + } + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + } + } + + public bool CanExpandSnippet() + { + return _expanderMethodInfo is not null; + } + } +} diff --git a/src/EditorFeatures/Test/Snippets/RoslynLSPSnippetConvertTests.cs b/src/EditorFeatures/Test/Snippets/RoslynLSPSnippetConvertTests.cs new file mode 100644 index 0000000000000..78e98d814e17a --- /dev/null +++ b/src/EditorFeatures/Test/Snippets/RoslynLSPSnippetConvertTests.cs @@ -0,0 +1,518 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.UnitTests.Snippets +{ + [UseExportProvider] + public class RoslynLSPSnippetConvertTests + { + #region Edgecase extend TextChange tests + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeForwardsForCaret() + { + var markup = +@"[|if ({|placeholder:true|}) +{ +}|] $$"; + + var expectedLSPSnippet = +@"if (${1:true}) +{ +} $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeBackwardsForCaret() + { + var markup = +@"$$ [|if ({|placeholder:true|}) +{ +}|]"; + + var expectedLSPSnippet = +@"$0 if (${1:true}) +{ +}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeForwardsForPlaceholder() + { + var markup = +@"[|if (true) +{$$ +}|] {|placeholder:test|}"; + + var expectedLSPSnippet = +@"if (true) +{$0 +} ${1:test}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeBackwardsForPlaceholder() + { + var markup = +@"{|placeholder:test|} [|if (true) +{$$ +}|]"; + + var expectedLSPSnippet = +@"${1:test} if (true) +{$0 +}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeForwardsForPlaceholderThenCaret() + { + var markup = +@"[|if (true) +{ +}|] {|placeholder:test|} $$"; + + var expectedLSPSnippet = +@"if (true) +{ +} ${1:test} $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeForwardsForCaretThenPlaceholder() + { + var markup = +@"[|if (true) +{ +}|] $$ {|placeholder:test|}"; + + var expectedLSPSnippet = +@"if (true) +{ +} $0 ${1:test}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeBackwardsForPlaceholderThenCaret() + { + var markup = +@"{|placeholder:test|} $$ [|if (true) +{ +}|]"; + + var expectedLSPSnippet = +@"${1:test} $0 if (true) +{ +}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeBackwardsForCaretThenPlaceholder() + { + var markup = +@"$$ {|placeholder:test|} [|if (true) +{ +}|]"; + + var expectedLSPSnippet = +@"$0 ${1:test} if (true) +{ +}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeBackwardsForCaretForwardsForPlaceholder() + { + var markup = +@"$$ [|if (true) +{ +}|] {|placeholder:test|}"; + + var expectedLSPSnippet = +@"$0 if (true) +{ +} ${1:test}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeBackwardsForPlaceholderForwardsForCaret() + { + var markup = +@"{|placeholder:test|} [|if (true) +{ +}|] $$"; + + var expectedLSPSnippet = +@"${1:test} if (true) +{ +} $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodForwardsForCaret() + { + var markup = +@"public void Method() +{ + [|if ({|placeholder:true|}) + { + }|] $$ +}"; + + var expectedLSPSnippet = +@"if (${1:true}) + { + } $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodBackwardsForCaret() + { + var markup = +@"public void Method() +{ + $$ [|if ({|placeholder:true|}) + { + }|] +}"; + + var expectedLSPSnippet = +@"$0 if (${1:true}) + { + }"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodForwardsForPlaceholder() + { + var markup = +@"public void Method() +{ + [|if (true) + {$$ + }|] {|placeholder:test|} +}"; + + var expectedLSPSnippet = +@"if (true) + {$0 + } ${1:test}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodBackwardsForPlaceholder() + { + var markup = +@"public void Method() +{ + {|placeholder:test|} [|if (true) + {$$ + }|]"; + + var expectedLSPSnippet = +@"${1:test} if (true) + {$0 + }"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodForwardsForPlaceholderThenCaret() + { + var markup = +@"public void Method() +{ + [|if (true) + { + }|] {|placeholder:test|} $$ +}"; + + var expectedLSPSnippet = +@"if (true) + { + } ${1:test} $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodForwardsForCaretThenPlaceholder() + { + var markup = +@"public void Method() +{ + [|if (true) + { + }|] $$ {|placeholder:test|} +}"; + + var expectedLSPSnippet = +@"if (true) + { + } $0 ${1:test}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodBackwardsForPlaceholderThenCaret() + { + var markup = +@"public void Method() +{ + {|placeholder:test|} $$ [|if (true) + { + }|] +}"; + + var expectedLSPSnippet = +@"${1:test} $0 if (true) + { + }"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodBackwardsForCaretThenPlaceholder() + { + var markup = +@"public void Method() +{ + $$ {|placeholder:test|} [|if (true) + { + }|] +}"; + + var expectedLSPSnippet = +@"$0 ${1:test} if (true) + { + }"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodBackwardsForCaretForwardsForPlaceholder() + { + var markup = +@"public void Method() +{ + $$ [|if (true) + { + }|] {|placeholder:test|} +}"; + + var expectedLSPSnippet = +@"$0 if (true) + { + } ${1:test}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodBackwardsForPlaceholderForwardsForCaret() + { + var markup = +@"public void Method() +{ + {|placeholder:test|} [|if (true) + { + }|] $$ +}"; + + var expectedLSPSnippet = +@"${1:test} if (true) + { + } $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestExtendSnippetTextChangeInMethodWithCodeBeforeAndAfterBackwardsForPlaceholderForwardsForCaret() + { + var markup = +@"public void Method() +{ + var x = 5; + {|placeholder:test|} [|if (true) + { + }|] $$ + + x = 3; +}"; + + var expectedLSPSnippet = +@"${1:test} if (true) + { + } $0"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public void TestExtendTextChangeInsertion() + { + var testString = "foo bar quux baz"; + using var workspace = CreateWorkspaceFromCode(testString); + var document = workspace.CurrentSolution.GetRequiredDocument(workspace.Documents.First().Id); + var lspSnippetString = RoslynLSPSnippetConverter.GenerateLSPSnippetAsync(document, 12, ImmutableArray.Empty, new TextChange(new TextSpan(8, 0), "quux"), CancellationToken.None).Result; + AssertEx.EqualOrDiff("quux$0", lspSnippetString); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public void TestExtendTextChangeReplacement() + { + var testString = "foo bar quux baz"; + using var workspace = CreateWorkspaceFromCode(testString); + var document = workspace.CurrentSolution.GetRequiredDocument(workspace.Documents.First().Id); + var lspSnippetString = RoslynLSPSnippetConverter.GenerateLSPSnippetAsync(document, 12, ImmutableArray.Empty, new TextChange(new TextSpan(4, 4), "bar quux"), CancellationToken.None).Result; + AssertEx.EqualOrDiff("bar quux$0", lspSnippetString); + } + + #endregion + + #region LSP Snippet generation tests + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestForLoopSnippet() + { + var markup = +@"[|for (var {|placeholder1:i|} = 0; {|placeholder1:i|} < {|placeholder2:length|}; {|placeholder1:i|}++) +{$$ +}|]"; + + var expectedLSPSnippet = +@"for (var ${1:i} = 0; ${1:i} < ${2:length}; ${1:i}++) +{$0 +}"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestIfSnippetSamePlaceholderCursorLocation() + { + var markup = +@"public void Method() +{ + var x = 5; + [|if ({|placeholder:true|}$$) + { + }|] + + x = 3; +}"; + + var expectedLSPSnippet = +@"if (${1:true}$0) + { + }"; + + return TestAsync(markup, expectedLSPSnippet); + } + + [Fact, Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)] + public Task TestIfSnippetSameCursorPlaceholderLocation() + { + var markup = +@"public void Method() +{ + var x = 5; + [|if ($${|placeholder:true|}) + { + }|] + + x = 3; +}"; + + var expectedLSPSnippet = +@"if ($0${1:true}) + { + }"; + + return TestAsync(markup, expectedLSPSnippet); + } + + #endregion + + protected static TestWorkspace CreateWorkspaceFromCode(string code) + => TestWorkspace.CreateCSharp(code); + + private static async Task TestAsync(string markup, string output) + { + MarkupTestFile.GetPositionAndSpans(markup, out var text, out var cursorPosition, out IDictionary> placeholderDictionary); + var stringSpan = placeholderDictionary[""].First(); + var textChange = new TextChange(new TextSpan(stringSpan.Start, 0), text.Substring(stringSpan.Start, stringSpan.Length)); + var placeholders = GetSnippetPlaceholders(text, placeholderDictionary); + using var workspace = CreateWorkspaceFromCode(markup); + var document = workspace.CurrentSolution.GetRequiredDocument(workspace.Documents.First().Id); + + var lspSnippetString = await RoslynLSPSnippetConverter.GenerateLSPSnippetAsync(document, cursorPosition!.Value, placeholders, textChange, CancellationToken.None).ConfigureAwait(false); + AssertEx.EqualOrDiff(output, lspSnippetString); + } + + private static ImmutableArray GetSnippetPlaceholders(string text, IDictionary> placeholderDictionary) + { + using var _ = ArrayBuilder.GetInstance(out var arrayBuilder); + foreach (var kvp in placeholderDictionary) + { + if (kvp.Key.Length > 0) + { + var spans = kvp.Value; + var identifier = text.Substring(spans[0].Start, spans[0].Length); + var placeholders = spans.Select(span => span.Start).ToImmutableArray(); + arrayBuilder.Add(new SnippetPlaceholder(identifier, placeholders)); + } + } + + return arrayBuilder.ToImmutable(); + } + } +} diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs index 0212dd8fe8c1f..000e66025c4d7 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/Snippets/CSharpSnippetCompletionProvider.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Completion.Providers.Snippets; using Microsoft.CodeAnalysis.CSharp.Completion.Providers; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Snippets; namespace Microsoft.CodeAnalysis.CSharp.Completion.CompletionProviders.Snippets { @@ -18,7 +19,8 @@ internal class CSharpSnippetCompletionProvider : AbstractSnippetCompletionProvid { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpSnippetCompletionProvider() + public CSharpSnippetCompletionProvider(IRoslynLSPSnippetExpander roslynLSPSnippetExpander) + : base(roslynLSPSnippetExpander) { } } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs index 7dcce9be665b7..1ac7d20c24337 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpConsoleSnippetProvider.cs @@ -19,6 +19,7 @@ using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; diff --git a/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs new file mode 100644 index 0000000000000..d9893eac487ae --- /dev/null +++ b/src/Features/CSharp/Portable/Snippets/CSharpIfSnippetProvider.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Snippets +{ + [ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] + internal class CSharpIfSnippetProvider : AbstractIfSnippetProvider + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpIfSnippetProvider() + { + } + + protected override void GetIfStatementConditionAndCursorPosition(SyntaxNode node, out SyntaxNode condition, out int cursorPositionNode) + { + var ifStatement = (IfStatementSyntax)node; + condition = ifStatement.Condition; + cursorPositionNode = ifStatement.Statement.SpanStart + 1; + } + } +} diff --git a/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs b/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs index 8546174899ff5..6f4f796207d72 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpSnippetService.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; diff --git a/src/Features/Core/Portable/Completion/CompletionChange.cs b/src/Features/Core/Portable/Completion/CompletionChange.cs index 4c7f456992d04..1e59751452d00 100644 --- a/src/Features/Core/Portable/Completion/CompletionChange.cs +++ b/src/Features/Core/Portable/Completion/CompletionChange.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.Text; @@ -40,8 +41,16 @@ public sealed class CompletionChange /// public bool IncludesCommitCharacter { get; } + internal ImmutableDictionary Properties { get; } + private CompletionChange( TextChange textChange, ImmutableArray textChanges, int? newPosition, bool includesCommitCharacter) + : this(textChange, textChanges, newPosition, includesCommitCharacter, ImmutableDictionary.Empty) + { + } + + private CompletionChange( + TextChange textChange, ImmutableArray textChanges, int? newPosition, bool includesCommitCharacter, ImmutableDictionary properties) { TextChange = textChange; NewPosition = newPosition; @@ -49,6 +58,7 @@ private CompletionChange( TextChanges = textChanges.NullToEmpty(); if (TextChanges.IsEmpty) TextChanges = ImmutableArray.Create(textChange); + Properties = properties; } /// @@ -97,6 +107,16 @@ public static CompletionChange Create( return new CompletionChange(textChange, textChanges, newPosition, includesCommitCharacter); } + internal static CompletionChange Create( + TextChange textChange, + ImmutableArray textChanges, + ImmutableDictionary properties, + int? newPosition, + bool includesCommitCharacter) + { + return new CompletionChange(textChange, textChanges, newPosition, includesCommitCharacter, properties); + } + /// /// Creates a copy of this with the property changed. /// diff --git a/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs index f88cc061d1a59..6787d2b7f3b61 100644 --- a/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/Snippets/AbstractSnippetCompletionProvider.cs @@ -3,16 +3,28 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ConvertToInterpolatedString; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Snippets; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Completion.Providers.Snippets { internal abstract class AbstractSnippetCompletionProvider : CompletionProvider { + private readonly IRoslynLSPSnippetExpander _roslynLSPSnippetExpander; + + public AbstractSnippetCompletionProvider(IRoslynLSPSnippetExpander roslynLSPSnippetExpander) + { + _roslynLSPSnippetExpander = roslynLSPSnippetExpander; + } + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) { // This retrieves the document without the text used to invoke completion @@ -35,34 +47,43 @@ public override async Task GetChangeAsync(Document document, C var allTextChanges = await allChangesDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); var change = Utilities.Collapse(allChangesText, allTextChanges.AsImmutable()); - return CompletionChange.Create(change, allTextChanges.AsImmutable(), newPosition: snippet.CursorPosition, includesCommitCharacter: true); + + // Converts the snippet to an LSP formatted snippet string. + var lspSnippet = await RoslynLSPSnippetConverter.GenerateLSPSnippetAsync(allChangesDocument, snippet.CursorPosition, snippet.Placeholders, change, cancellationToken).ConfigureAwait(false); + var props = ImmutableDictionary.Empty + .Add(SnippetCompletionItem.LSPSnippetKey, lspSnippet); + + return CompletionChange.Create(change, allTextChanges.AsImmutable(), properties: props, snippet.CursorPosition, includesCommitCharacter: true); } public override async Task ProvideCompletionsAsync(CompletionContext context) { - var document = context.Document; - var cancellationToken = context.CancellationToken; - var position = context.Position; - var service = document.GetLanguageService(); - - if (service == null) + if (_roslynLSPSnippetExpander.CanExpandSnippet()) { - return; - } + var document = context.Document; + var cancellationToken = context.CancellationToken; + var position = context.Position; + var service = document.GetLanguageService(); - var (strippedDocument, newPosition) = await GetDocumentWithoutInvokingTextAsync(document, position, cancellationToken).ConfigureAwait(false); + if (service == null) + { + return; + } - var snippets = await service.GetSnippetsAsync(strippedDocument, newPosition, cancellationToken).ConfigureAwait(false); + var (strippedDocument, newPosition) = await GetDocumentWithoutInvokingTextAsync(document, position, cancellationToken).ConfigureAwait(false); - foreach (var snippetData in snippets) - { - var completionItem = SnippetCompletionItem.Create( - displayText: snippetData.DisplayName, - displayTextSuffix: "", - position: position, - snippetIdentifier: snippetData.SnippetIdentifier, - glyph: Glyph.Snippet); - context.AddItem(completionItem); + var snippets = await service.GetSnippetsAsync(strippedDocument, newPosition, cancellationToken).ConfigureAwait(false); + + foreach (var snippetData in snippets) + { + var completionItem = SnippetCompletionItem.Create( + displayText: snippetData.DisplayName, + displayTextSuffix: "", + position: position, + snippetIdentifier: snippetData.SnippetIdentifier, + glyph: Glyph.Snippet); + context.AddItem(completionItem); + } } } diff --git a/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs index edb4a3d156c7f..1ef95d39a95f6 100644 --- a/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/Snippets/SnippetCompletionItem.cs @@ -10,6 +10,9 @@ namespace Microsoft.CodeAnalysis.Completion.Providers.Snippets { internal class SnippetCompletionItem { + public static string LSPSnippetKey = "LSPSnippet"; + public static string SnippetIdentifierKey = "SnippetIdentifier"; + public static CompletionItem Create( string displayText, string displayTextSuffix, @@ -19,7 +22,7 @@ public static CompletionItem Create( { var props = ImmutableDictionary.Empty .Add("Position", position.ToString()) - .Add("SnippetIdentifier", snippetIdentifier); + .Add(SnippetIdentifierKey, snippetIdentifier); return CommonCompletionItem.Create( displayText: displayText, @@ -32,7 +35,7 @@ public static CompletionItem Create( public static string GetSnippetIdentifier(CompletionItem item) { - Contract.ThrowIfFalse(item.Properties.TryGetValue("SnippetIdentifier", out var text)); + Contract.ThrowIfFalse(item.Properties.TryGetValue(SnippetIdentifierKey, out var text)); return text; } @@ -42,5 +45,10 @@ public static int GetInvocationPosition(CompletionItem item) Contract.ThrowIfFalse(int.TryParse(text, out var num)); return num; } + + public static bool IsSnippet(CompletionItem item) + { + return item.Properties.TryGetValue(SnippetIdentifierKey, out var _); + } } } diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 9be2c67622603..651e147fd9dc2 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -3132,6 +3132,9 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Sort Imports or usings + + Insert an 'if' statement + Directives from '{0}' diff --git a/src/Features/Core/Portable/Snippets/AbstractSnippetService.cs b/src/Features/Core/Portable/Snippets/AbstractSnippetService.cs index 1208d21bb0e1e..3cf776e009557 100644 --- a/src/Features/Core/Portable/Snippets/AbstractSnippetService.cs +++ b/src/Features/Core/Portable/Snippets/AbstractSnippetService.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Snippets diff --git a/src/Features/Core/Portable/Snippets/ExportSnippetProviderAttribute.cs b/src/Features/Core/Portable/Snippets/ExportSnippetProviderAttribute.cs index ea2ad2e53d6ae..d20fbfefef59b 100644 --- a/src/Features/Core/Portable/Snippets/ExportSnippetProviderAttribute.cs +++ b/src/Features/Core/Portable/Snippets/ExportSnippetProviderAttribute.cs @@ -4,6 +4,7 @@ using System; using System.Composition; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; namespace Microsoft.CodeAnalysis.Snippets { diff --git a/src/Features/Core/Portable/Snippets/IRoslynLSPSnippetExpander.cs b/src/Features/Core/Portable/Snippets/IRoslynLSPSnippetExpander.cs new file mode 100644 index 0000000000000..7b0763207e898 --- /dev/null +++ b/src/Features/Core/Portable/Snippets/IRoslynLSPSnippetExpander.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Snippets +{ + internal interface IRoslynLSPSnippetExpander + { + bool CanExpandSnippet(); + } +} diff --git a/src/Features/Core/Portable/Snippets/ISnippetService.cs b/src/Features/Core/Portable/Snippets/ISnippetService.cs index 30f6c4d86968a..f005325da09da 100644 --- a/src/Features/Core/Portable/Snippets/ISnippetService.cs +++ b/src/Features/Core/Portable/Snippets/ISnippetService.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Snippets diff --git a/src/Features/Core/Portable/Snippets/RoslynLSPSnippetConverter.cs b/src/Features/Core/Portable/Snippets/RoslynLSPSnippetConverter.cs new file mode 100644 index 0000000000000..3fe52bf86914f --- /dev/null +++ b/src/Features/Core/Portable/Snippets/RoslynLSPSnippetConverter.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Snippets +{ + internal static class RoslynLSPSnippetConverter + { + /// + /// Extends the TextChange to encompass all placeholder positions as well as caret position. + /// Generates a LSP formatted snippet from a TextChange, list of placeholders, and caret position. + /// + public static async Task GenerateLSPSnippetAsync(Document document, int caretPosition, ImmutableArray placeholders, TextChange textChange, CancellationToken cancellationToken) + { + var extendedTextChange = await ExtendSnippetTextChangeAsync(document, textChange, placeholders, caretPosition, cancellationToken).ConfigureAwait(false); + return ConvertToLSPSnippetString(extendedTextChange, placeholders, caretPosition); + } + + /// + /// Iterates through every index in the snippet string and determines where the + /// LSP formatted chunks should be inserted for each placeholder. + /// + private static string ConvertToLSPSnippetString(TextChange textChange, ImmutableArray placeholders, int caretPosition) + { + var textChangeStart = textChange.Span.Start; + var textChangeText = textChange.NewText; + Contract.ThrowIfNull(textChangeText); + + using var _1 = PooledStringBuilder.GetInstance(out var lspSnippetString); + using var _2 = PooledDictionary.GetInstance(out var dictionary); + PopulateMapOfSpanStartsToLSPStringItem(dictionary, placeholders, textChangeStart); + + // Need to go through the length + 1 since caret postions occur before and after the + // character position. + // If there is a caret at the end of the line, then it's position + // will be equivalent to the length of the TextChange. + for (var i = 0; i < textChange.Span.Length + 1;) + { + if (i == caretPosition - textChangeStart) + { + lspSnippetString.Append("$0"); + } + + //Tries to see if a value exists at that position in the map, and if so it + // generates a string that is LSP formatted. + if (dictionary.TryGetValue(i, out var placeholderInfo)) + { + var str = $"${{{placeholderInfo.priority}:{placeholderInfo.identifier}}}"; + lspSnippetString.Append(str); + + // Skip past the entire identifier in the TextChange text + i += placeholderInfo.identifier.Length; + } + else + { + if (i < textChangeText.Length) + { + lspSnippetString.Append(textChangeText[i]); + i++; + } + else + { + break; + } + } + } + + return lspSnippetString.ToString(); + } + + /// + /// Preprocesses the list of placeholders into a dictionary that maps the insertion position + /// in the string to the placeholder's identifier and the priority associated with it. + /// + private static void PopulateMapOfSpanStartsToLSPStringItem(Dictionary dictionary, ImmutableArray placeholders, int textChangeStart) + { + for (var i = 0; i < placeholders.Length; i++) + { + var placeholder = placeholders[i]; + foreach (var position in placeholder.PlaceHolderPositions) + { + // i + 1 since the placeholder priority is set according to the index in the + // placeholders array, starting at 1. + // We should never be adding two placeholders in the same position since identifiers + // must have a length greater than 0. + dictionary.Add(position - textChangeStart, (placeholder.Identifier, i + 1)); + } + } + } + + /// + /// We need to extend the snippet's TextChange if any of the placeholders or + /// if the caret position comes before or after the span of the TextChange. + /// If so, then find the new string that encompasses all of the placeholders + /// and caret position. + /// This is important for the cases in which the document does not determine the TextChanges from + /// the original document accurately. + /// + private static async Task ExtendSnippetTextChangeAsync(Document document, TextChange textChange, ImmutableArray placeholders, int caretPosition, CancellationToken cancellationToken) + { + var extendedSpan = GetUpdatedTextSpan(textChange, placeholders, caretPosition); + var documentText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newString = documentText.ToString(extendedSpan); + var newTextChange = new TextChange(extendedSpan, newString); + + return newTextChange; + } + + /// + /// Iterates through the placeholders and determines if any of the positions + /// come before or after what is indicated by the snippet's TextChange. + /// If so, adjust the starting and ending position accordingly. + /// + private static TextSpan GetUpdatedTextSpan(TextChange textChange, ImmutableArray placeholders, int caretPosition) + { + var textChangeText = textChange.NewText; + Contract.ThrowIfNull(textChangeText); + + var startPosition = textChange.Span.Start; + var endPosition = textChange.Span.Start + textChangeText.Length; + + if (placeholders.Length > 0) + { + startPosition = Math.Min(startPosition, placeholders.Min(placeholder => placeholder.PlaceHolderPositions.Min())); + endPosition = Math.Max(endPosition, placeholders.Max(placeholder => placeholder.PlaceHolderPositions.Max())); + } + + startPosition = Math.Min(startPosition, caretPosition); + endPosition = Math.Max(endPosition, caretPosition); + + return TextSpan.FromBounds(startPosition, endPosition); + } + } +} diff --git a/src/Features/Core/Portable/Snippets/SnippetChange.cs b/src/Features/Core/Portable/Snippets/SnippetChange.cs index 136e10d07b761..584d9a7aea2d8 100644 --- a/src/Features/Core/Portable/Snippets/SnippetChange.cs +++ b/src/Features/Core/Portable/Snippets/SnippetChange.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; @@ -21,19 +22,27 @@ internal readonly struct SnippetChange /// /// The position that the cursor should end up on /// - public readonly int? CursorPosition; + public readonly int CursorPosition; + + /// + /// The items that we will want to rename as well as the ordering + /// in which to visit those items. + /// + public readonly ImmutableArray Placeholders; public SnippetChange( ImmutableArray textChanges, - int? cursorPosition) + int cursorPosition, + ImmutableArray placeholders) { if (textChanges.IsEmpty) { - throw new ArgumentException($"{ textChanges.Length } must not be empty"); + throw new ArgumentException($"{nameof(textChanges)} must not be empty."); } TextChanges = textChanges; CursorPosition = cursorPosition; + Placeholders = placeholders; } } } diff --git a/src/Features/Core/Portable/Snippets/SnippetPlaceholder.cs b/src/Features/Core/Portable/Snippets/SnippetPlaceholder.cs new file mode 100644 index 0000000000000..5932bdfeff17d --- /dev/null +++ b/src/Features/Core/Portable/Snippets/SnippetPlaceholder.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Snippets +{ + internal readonly struct SnippetPlaceholder + { + /// + /// The identifier in the snippet that needs to be renamed. + /// + public readonly string Identifier; + + /// + /// The positions associated with the identifier that will need to + /// be converted into LSP formatted strings. + /// + public readonly ImmutableArray PlaceHolderPositions; + + /// + /// + /// For loop would have two placeholders: + /// + /// for (var {1:i} = 0; {1:i} < {2:length}; {1:i}++) + /// + /// Identifier: i, 3 associated positions
+ /// Identifier: length, 1 associated position
+ ///
+ ///
+ public SnippetPlaceholder(string identifier, ImmutableArray placeholderPositions) + { + if (identifier.Length == 0) + { + throw new ArgumentException($"{nameof(identifier)} must not be an empty string."); + } + + Identifier = identifier; + PlaceHolderPositions = placeholderPositions; + } + } +} diff --git a/src/Features/Core/Portable/Snippets/AbstractConsoleSnippetProvider.cs b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractConsoleSnippetProvider.cs similarity index 81% rename from src/Features/Core/Portable/Snippets/AbstractConsoleSnippetProvider.cs rename to src/Features/Core/Portable/Snippets/SnippetProviders/AbstractConsoleSnippetProvider.cs index 440dd4defdb22..51ca5a533c985 100644 --- a/src/Features/Core/Portable/Snippets/AbstractConsoleSnippetProvider.cs +++ b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractConsoleSnippetProvider.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Linq; @@ -73,18 +74,22 @@ private async Task GenerateSnippetTextChangeAsync(Document document, return new TextChange(TextSpan.FromBounds(position, position), expressionStatement.NormalizeWhitespace().ToFullString()); } - protected override int? GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget) + /// + /// Tries to get the location after the open parentheses in the argument list. + /// If it can't, then we default to the end of the snippet's span. + /// + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget) { var invocationExpression = caretTarget.DescendantNodes().Where(syntaxFacts.IsInvocationExpression).FirstOrDefault(); if (invocationExpression is null) { - return null; + return caretTarget.Span.End; } var argumentListNode = syntaxFacts.GetArgumentListOfInvocationExpression(invocationExpression); if (argumentListNode is null) { - return null; + return caretTarget.Span.End; } syntaxFacts.GetPartsOfArgumentList(argumentListNode, out var openParenToken, out _, out _); @@ -96,7 +101,7 @@ protected override async Task AnnotateNodesToReformatAsync(Document { var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var syntaxFacts = document.GetRequiredLanguageService(); - var snippetExpressionNode = GetConsoleExpressionStatement(syntaxFacts, root, position); + var snippetExpressionNode = FindAddedSnippetSyntaxNode(root, position, syntaxFacts); if (snippetExpressionNode is null) { return root; @@ -108,7 +113,31 @@ protected override async Task AnnotateNodesToReformatAsync(Document return root.ReplaceNode(snippetExpressionNode, reformatSnippetNode); } - private static SyntaxNode? GetConsoleExpressionStatement(ISyntaxFactsService syntaxFacts, SyntaxNode root, int position) + protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + return ImmutableArray.Empty; + } + + private static SyntaxToken? GetOpenParenToken(SyntaxNode node, ISyntaxFacts syntaxFacts) + { + var invocationExpression = node.DescendantNodes().Where(syntaxFacts.IsInvocationExpression).FirstOrDefault(); + if (invocationExpression is null) + { + return null; + } + + var argumentListNode = syntaxFacts.GetArgumentListOfInvocationExpression(invocationExpression); + if (argumentListNode is null) + { + return null; + } + + syntaxFacts.GetPartsOfArgumentList(argumentListNode, out var openParenToken, out _, out _); + + return openParenToken; + } + + protected override SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, ISyntaxFacts syntaxFacts) { var closestNode = root.FindNode(TextSpan.FromBounds(position, position)); var nearestExpressionStatement = closestNode.FirstAncestorOrSelf(syntaxFacts.IsExpressionStatement); diff --git a/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractIfSnippetProvider.cs b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractIfSnippetProvider.cs new file mode 100644 index 0000000000000..8d9a4e2eb9e6a --- /dev/null +++ b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractIfSnippetProvider.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Snippets +{ + internal abstract class AbstractIfSnippetProvider : AbstractSnippetProvider + { + public override string SnippetIdentifier => "if"; + + public override string SnippetDisplayName => FeaturesResources.Insert_an_if_statement; + + protected abstract void GetIfStatementConditionAndCursorPosition(SyntaxNode node, out SyntaxNode condition, out int cursorPositionNode); + + protected override async Task IsValidSnippetLocationAsync(Document document, int position, CancellationToken cancellationToken) + { + var semanticModel = await document.ReuseExistingSpeculativeModelAsync(position, cancellationToken).ConfigureAwait(false); + + var syntaxContext = document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); + return syntaxContext.IsStatementContext || syntaxContext.IsGlobalStatementContext; + } + + protected override Task> GenerateSnippetTextChangesAsync(Document document, int position, CancellationToken cancellationToken) + { + var snippetTextChange = GenerateSnippetTextChange(document, position); + return Task.FromResult(ImmutableArray.Create(snippetTextChange)); + } + + private static TextChange GenerateSnippetTextChange(Document document, int position) + { + var generator = SyntaxGenerator.GetGenerator(document); + var ifStatement = generator.IfStatement(generator.TrueLiteralExpression(), Array.Empty()); + + return new TextChange(TextSpan.FromBounds(position, position), ifStatement.ToFullString()); + } + + protected override int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget) + { + GetIfStatementConditionAndCursorPosition(caretTarget, out _, out var cursorPosition); + + // Place at the end of the node specified for cursor position. + // Is the statement node in C# and the "Then" keyword + return cursorPosition; + } + + protected override async Task AnnotateNodesToReformatAsync(Document document, + SyntaxAnnotation findSnippetAnnotation, SyntaxAnnotation cursorAnnotation, int position, CancellationToken cancellationToken) + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var syntaxFacts = document.GetRequiredLanguageService(); + var snippetExpressionNode = FindAddedSnippetSyntaxNode(root, position, syntaxFacts); + if (snippetExpressionNode is null) + { + return root; + } + + var reformatSnippetNode = snippetExpressionNode.WithAdditionalAnnotations(findSnippetAnnotation, cursorAnnotation, Simplifier.Annotation, Formatter.Annotation); + return root.ReplaceNode(snippetExpressionNode, reformatSnippetNode); + } + + protected override ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var arrayBuilder); + GetIfStatementConditionAndCursorPosition(node, out var condition, out var unusedVariable); + arrayBuilder.Add(new SnippetPlaceholder(identifier: condition.ToString(), placeholderPositions: ImmutableArray.Create(condition.SpanStart))); + + return arrayBuilder.ToImmutableArray(); + } + + protected override SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, ISyntaxFacts syntaxFacts) + { + var closestNode = root.FindNode(TextSpan.FromBounds(position, position), getInnermostNodeForTie: true); + + var nearestStatement = closestNode.DescendantNodesAndSelf(syntaxFacts.IsIfStatement).FirstOrDefault(); + + if (nearestStatement is null) + { + return null; + } + + // Checking to see if that expression statement that we found is + // starting at the same position as the position we inserted + // the if statement. + if (nearestStatement.SpanStart != position) + { + return null; + } + + return nearestStatement; + } + } +} diff --git a/src/Features/Core/Portable/Snippets/AbstractSnippetProvider.cs b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractSnippetProvider.cs similarity index 51% rename from src/Features/Core/Portable/Snippets/AbstractSnippetProvider.cs rename to src/Features/Core/Portable/Snippets/SnippetProviders/AbstractSnippetProvider.cs index 0242f76ed37ff..0a758e1a0fa58 100644 --- a/src/Features/Core/Portable/Snippets/AbstractSnippetProvider.cs +++ b/src/Features/Core/Portable/Snippets/SnippetProviders/AbstractSnippetProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; @@ -11,10 +12,14 @@ using Microsoft.CodeAnalysis.CodeCleanup; using Microsoft.CodeAnalysis.EditAndContinue.Contracts; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.ExtractMethod; using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.NavigateTo; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Snippets @@ -42,7 +47,17 @@ internal abstract class AbstractSnippetProvider : ISnippetProvider /// Method for each snippet to locate the inserted SyntaxNode to reformat ///
protected abstract Task AnnotateNodesToReformatAsync(Document document, SyntaxAnnotation reformatAnnotation, SyntaxAnnotation cursorAnnotation, int position, CancellationToken cancellationToken); - protected abstract int? GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget); + protected abstract int GetTargetCaretPosition(ISyntaxFactsService syntaxFacts, SyntaxNode caretTarget); + + /// + /// Every SnippetProvider will need a method to retrieve the "main" snippet syntax once it has been inserted as a TextChange. + /// + protected abstract SyntaxNode? FindAddedSnippetSyntaxNode(SyntaxNode root, int position, ISyntaxFacts syntaxFacts); + + /// + /// Method to find the locations that must be renamed and where tab stops must be inserted into the snippet. + /// + protected abstract ImmutableArray GetPlaceHolderLocationsList(SyntaxNode node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken); /// /// Determines if the location is valid for a snippet, @@ -73,17 +88,60 @@ internal abstract class AbstractSnippetProvider : ISnippetProvider public async Task GetSnippetAsync(Document document, int position, CancellationToken cancellationToken) { var syntaxFacts = document.GetRequiredLanguageService(); + + // Generates the snippet as a list of textchanges var textChanges = await GenerateSnippetTextChangesAsync(document, position, cancellationToken).ConfigureAwait(false); + + // Applies the snippet textchanges to the document var snippetDocument = await GetDocumentWithSnippetAsync(document, textChanges, cancellationToken).ConfigureAwait(false); - var formatAnnotatedSnippetDocument = await AddFormatAnnotationAsync(snippetDocument, position, cancellationToken).ConfigureAwait(false); + // Finds the inserted snippet and replaces the node in the document with a node that has added trivia + // since all trivia is removed when converted to a TextChange. + var snippetWithTriviaDocument = await GetDocumentWithSnippetAndTriviaAsync(snippetDocument, position, syntaxFacts, cancellationToken).ConfigureAwait(false); + + // Adds annotations to inserted snippet to be formatted, simplified, add imports if needed, etc. + var formatAnnotatedSnippetDocument = await AddFormatAnnotationAsync(snippetWithTriviaDocument, position, cancellationToken).ConfigureAwait(false); + + // Goes through and calls upon the formatting engines that the previous step annotated. var reformattedDocument = await CleanupDocumentAsync(formatAnnotatedSnippetDocument, cancellationToken).ConfigureAwait(false); + var reformattedRoot = await reformattedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var caretTarget = reformattedRoot.GetAnnotatedNodes(_cursorAnnotation).SingleOrDefault(); + var caretTarget = reformattedRoot.GetAnnotatedNodes(_cursorAnnotation).FirstOrDefault(); + var mainChangeNode = reformattedRoot.GetAnnotatedNodes(_findSnippetAnnotation).FirstOrDefault(); + + // All the TextChanges from the original document. Will include any imports (if necessary) and all snippet associated + // changes after having been formatted. var changes = await reformattedDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false); + + // Gets a listing of the identifiers that need to be found in the snippet TextChange + // and their associated TextSpan so they can later be converted into an LSP snippet format. + var placeholders = GetPlaceHolderLocationsList(mainChangeNode, syntaxFacts, cancellationToken); + + // All the changes from the original document to the most updated. Will later be + // collpased into one collapsed TextChange. + var changesArray = changes.ToImmutableArray(); return new SnippetChange( - textChanges: changes.ToImmutableArray(), - cursorPosition: GetTargetCaretPosition(syntaxFacts, caretTarget)); + textChanges: changesArray, + cursorPosition: GetTargetCaretPosition(syntaxFacts, caretTarget), + placeholders: placeholders); + } + + /// + /// Descends into the inserted snippet to add back trivia on every token. + /// + private static SyntaxNode? GenerateElasticTriviaForSyntax(ISyntaxFacts syntaxFacts, SyntaxNode? node) + { + if (node is null) + { + return null; + } + + var nodeWithTrivia = node.ReplaceTokens(node.DescendantTokens(descendIntoTrivia: true), + (oldtoken, _) => oldtoken.WithAdditionalAnnotations(SyntaxAnnotation.ElasticAnnotation) + .WithAppendedTrailingTrivia(syntaxFacts.ElasticMarker) + .WithPrependedLeadingTrivia(syntaxFacts.ElasticMarker)); + + return nodeWithTrivia; } private async Task CleanupDocumentAsync( @@ -91,23 +149,50 @@ private async Task CleanupDocumentAsync( { if (document.SupportsSyntaxTree) { - var options = await CodeCleanupOptions.FromDocumentAsync(document, fallbackOptions: null, cancellationToken).ConfigureAwait(false); + var addImportPlacementOptions = await document.GetAddImportPlacementOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); + var simplifierOptions = await document.GetSimplifierOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); + var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions: null, cancellationToken).ConfigureAwait(false); document = await ImportAdder.AddImportsFromSymbolAnnotationAsync( - document, _findSnippetAnnotation, options.AddImportOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + document, _findSnippetAnnotation, addImportPlacementOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - document = await Simplifier.ReduceAsync(document, _findSnippetAnnotation, options.SimplifierOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + document = await Simplifier.ReduceAsync(document, _findSnippetAnnotation, simplifierOptions, cancellationToken: cancellationToken).ConfigureAwait(false); // format any node with explicit formatter annotation - document = await Formatter.FormatAsync(document, _findSnippetAnnotation, options.FormattingOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + document = await Formatter.FormatAsync(document, _findSnippetAnnotation, syntaxFormattingOptions, cancellationToken: cancellationToken).ConfigureAwait(false); // format any elastic whitespace - document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation, options.FormattingOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation, syntaxFormattingOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } return document; } + /// + /// Locates the snippet that was inserted. Generates trivia for every token in that syntaxnode. + /// Replaces the SyntaxNodes and gets back the new document. + /// + private async Task GetDocumentWithSnippetAndTriviaAsync(Document snippetDocument, int position, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken) + { + var root = await snippetDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var nearestStatement = FindAddedSnippetSyntaxNode(root, position, syntaxFacts); + + if (nearestStatement is null) + { + return snippetDocument; + } + + var nearestStatementWithTrivia = GenerateElasticTriviaForSyntax(syntaxFacts, nearestStatement); + + if (nearestStatementWithTrivia is null) + { + return snippetDocument; + } + + root = root.ReplaceNode(nearestStatement, nearestStatementWithTrivia); + return snippetDocument.WithSyntaxRoot(root); + } + private static async Task GetDocumentWithSnippetAsync(Document document, ImmutableArray snippets, CancellationToken cancellationToken) { var originalText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/Snippets/ISnippetProvider.cs b/src/Features/Core/Portable/Snippets/SnippetProviders/ISnippetProvider.cs similarity index 95% rename from src/Features/Core/Portable/Snippets/ISnippetProvider.cs rename to src/Features/Core/Portable/Snippets/SnippetProviders/ISnippetProvider.cs index 5ffeaf1d2452b..ec5dfa51c773c 100644 --- a/src/Features/Core/Portable/Snippets/ISnippetProvider.cs +++ b/src/Features/Core/Portable/Snippets/SnippetProviders/ISnippetProvider.cs @@ -10,7 +10,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Snippets +namespace Microsoft.CodeAnalysis.Snippets.SnippetProviders { internal interface ISnippetProvider { diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index 8c147bda9edf8..d1fdfdcd3c35f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -940,6 +940,11 @@ Ujistěte se, že specifikátor tt použijete pro jazyky, pro které je nezbytn Inline refaktoring metody {0} se zachováním deklarace + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Nedostatek šestnáctkových číslic diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index d4ed497f717e6..c439e030c6682 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -940,6 +940,11 @@ Stellen Sie sicher, dass Sie den Bezeichner "tt" für Sprachen verwenden, für d "{0}" inline einbinden und beibehalten + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Nicht genügend Hexadezimalziffern. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index b4880c59c2521..3f59776cec1df 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -940,6 +940,11 @@ Asegúrese de usar el especificador "tt" para los idiomas para los que es necesa Alinear y mantener "{0}" + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Insuficientes dígitos hexadecimales diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index bcc35d64dd2ba..87ab946945c88 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -940,6 +940,11 @@ Veillez à utiliser le spécificateur "tt" pour les langues où il est nécessai Inline et conserver '{0}' + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Chiffres hexadécimaux insuffisants diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index da5d1d38ca153..593ec623f48a9 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -940,6 +940,11 @@ Assicurarsi di usare l'identificatore "tt" per le lingue per le quali è necessa Imposta come inline e mantieni '{0}' + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Cifre esadecimali insufficienti diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index b1c0ae21250de..1c8d9569c924f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -940,6 +940,11 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma インラインおよび '{0}' の保持 + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits 16 進数の数字が正しくありません diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index 34902bb0ae8f7..7e538b61cacb7 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -940,6 +940,11 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma '{0}'을(를) 인라인으로 지정 및 유지 + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits 16진수가 부족합니다. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 2bde53c93039e..0f70bdf794e5d 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -940,6 +940,11 @@ Pamiętaj, aby nie używać specyfikatora „tt” dla wszystkich języków, w k Wstaw środwierszowo i zachowaj metodę „{0}” + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Zbyt mało cyfr szesnastkowych diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index 1dbe371c883b3..2a2fc6f832f1c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -940,6 +940,11 @@ Verifique se o especificador "tt" foi usado para idiomas para os quais é necess Embutir e manter '{0}' + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Dígitos hexadecimais insuficientes diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 18f314686dede..f25589dbd1f21 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -940,6 +940,11 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma Сделать "{0}" встроенным и сохранить его + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Недостаточно шестнадцатеричных цифр diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index 05dd581a2fc97..52853f1a84ea6 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -940,6 +940,11 @@ AM ve PM arasındaki farkın korunmasının gerekli olduğu diller için "tt" be '{0}' öğesini satır içine al ve koru + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits Yetersiz onaltılık basamak diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index 678612aec02fc..1d412d3fba271 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -940,6 +940,11 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 内联并保留“{0}” + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits 无效的十六进制数字 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index 7c735724a7c41..e3606c83c4b4b 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -940,6 +940,11 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 內嵌並保留 '{0}' + + Insert an 'if' statement + Insert an 'if' statement + + Insufficient hexadecimal digits 十六進位數位不足 diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs index 55fd4486126bf..85721afca1226 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs @@ -125,6 +125,16 @@ private static void AddInitializerSuppressOperations(List lis return null; } + // Special case for formatting if-statements blocks on new lines + if (CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(previousToken, currentToken) && + currentToken.IsKind(SyntaxKind.OpenBraceToken) && + currentToken.Parent.IsParentKind(SyntaxKind.IfStatement)) + { + var num = LineBreaksAfter(previousToken, currentToken); + + return CreateAdjustNewLinesOperation(num, AdjustNewLinesOption.ForceLinesIfOnSingleLine); + } + // if operation is already forced, return as it is. if (operation.Option == AdjustNewLinesOption.ForceLines) return operation; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs index 8a75d07297e83..60e4e1d31caec 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxKinds.cs @@ -96,6 +96,7 @@ public TSyntaxKind Convert(int kind) where TSyntaxKind : struct public int ExpressionStatement => (int)SyntaxKind.ExpressionStatement; public int ForEachStatement => (int)SyntaxKind.ForEachStatement; + public int IfStatement => (int)SyntaxKind.IfStatement; public int LocalDeclarationStatement => (int)SyntaxKind.LocalDeclarationStatement; public int? LocalFunctionStatement => (int)SyntaxKind.LocalFunctionStatement; public int LockStatement => (int)SyntaxKind.LockStatement; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs index de1745f2a9170..3e1f4a414b123 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs @@ -807,6 +807,9 @@ public static bool IsExpressionStatement(this ISyntaxFacts syntaxFacts, [NotNull public static bool IsForEachStatement(this ISyntaxFacts syntaxFacts, [NotNullWhen(true)] SyntaxNode? node) => node?.RawKind == syntaxFacts.SyntaxKinds.ForEachStatement; + public static bool IsIfStatement(this ISyntaxFacts syntaxFacts, [NotNullWhen(true)] SyntaxNode? node) + => node?.RawKind == syntaxFacts.SyntaxKinds.IfStatement; + public static bool IsLocalDeclarationStatement(this ISyntaxFacts syntaxFacts, [NotNullWhen(true)] SyntaxNode? node) => node?.RawKind == syntaxFacts.SyntaxKinds.LocalDeclarationStatement; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs index 724be53fe366e..51b0cf521d9bf 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxKinds.cs @@ -139,6 +139,7 @@ internal interface ISyntaxKinds int ExpressionStatement { get; } int ForEachStatement { get; } + int IfStatement { get; } int LocalDeclarationStatement { get; } int? LocalFunctionStatement { get; } int LockStatement { get; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index 9998934b60735..c0b518ef9e0e2 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -1954,7 +1954,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServices Return initializer.Initializer.Initializers End Function - #End Region End Class End Namespace diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb index 74eca068b2f95..5131c37c627db 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxKinds.vb @@ -99,6 +99,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServices Public ReadOnly Property ExpressionStatement As Integer = SyntaxKind.ExpressionStatement Implements ISyntaxKinds.ExpressionStatement Public ReadOnly Property ForEachStatement As Integer = SyntaxKind.ForEachStatement Implements ISyntaxKinds.ForEachStatement + Public ReadOnly Property IfStatement As Integer = SyntaxKind.IfStatement Implements ISyntaxKinds.IfStatement Public ReadOnly Property LocalDeclarationStatement As Integer = SyntaxKind.LocalDeclarationStatement Implements ISyntaxKinds.LocalDeclarationStatement Public ReadOnly Property LocalFunctionStatement As Integer? = Nothing Implements ISyntaxKinds.LocalFunctionStatement Public ReadOnly Property LockStatement As Integer = SyntaxKind.SyncLockStatement Implements ISyntaxKinds.LockStatement