File: Snippets\RoslynLSPSnippetConvertTests.cs
Web Access
Project: src\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.UnitTests)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
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]
[Trait(Traits.Feature, Traits.Features.RoslynLSPSnippetConverter)]
public class RoslynLSPSnippetConvertTests
{
    #region Edgecase extend TextChange tests
 
    [Fact]
    public Task TestExtendSnippetTextChangeForwardsForCaret()
    {
        var markup =
@"[|if ({|placeholder:true|})
{
}|] $$";
 
        var expectedLSPSnippet =
@"if (${1:true})
{
} $0";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeBackwardsForCaret()
    {
        var markup =
@"$$ [|if ({|placeholder:true|})
{
}|]";
 
        var expectedLSPSnippet =
@"$0 if (${1:true})
{
}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeForwardsForPlaceholder()
    {
        var markup =
@"[|if (true)
{$$
}|] {|placeholder:test|}";
 
        var expectedLSPSnippet =
@"if (true)
{$0
} ${1:test}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeBackwardsForPlaceholder()
    {
        var markup =
@"{|placeholder:test|} [|if (true)
{$$
}|]";
 
        var expectedLSPSnippet =
@"${1:test} if (true)
{$0
}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeForwardsForPlaceholderThenCaret()
    {
        var markup =
@"[|if (true)
{
}|] {|placeholder:test|} $$";
 
        var expectedLSPSnippet =
@"if (true)
{
} ${1:test} $0";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeForwardsForCaretThenPlaceholder()
    {
        var markup =
@"[|if (true)
{
}|] $$ {|placeholder:test|}";
 
        var expectedLSPSnippet =
@"if (true)
{
} $0 ${1:test}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeBackwardsForPlaceholderThenCaret()
    {
        var markup =
@"{|placeholder:test|} $$ [|if (true)
{
}|]";
 
        var expectedLSPSnippet =
@"${1:test} $0 if (true)
{
}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeBackwardsForCaretThenPlaceholder()
    {
        var markup =
@"$$ {|placeholder:test|} [|if (true)
{
}|]";
 
        var expectedLSPSnippet =
@"$0 ${1:test} if (true)
{
}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeBackwardsForCaretForwardsForPlaceholder()
    {
        var markup =
@"$$ [|if (true)
{
}|] {|placeholder:test|}";
 
        var expectedLSPSnippet =
@"$0 if (true)
{
} ${1:test}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeBackwardsForPlaceholderForwardsForCaret()
    {
        var markup =
@"{|placeholder:test|} [|if (true)
{
}|] $$";
 
        var expectedLSPSnippet =
@"${1:test} if (true)
{
} $0";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodForwardsForCaret()
    {
        var markup =
@"public void Method()
{
    [|if ({|placeholder:true|})
    {
    }|] $$
}";
 
        var expectedLSPSnippet =
@"if (${1:true})
    {
    } $0";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodBackwardsForCaret()
    {
        var markup =
@"public void Method()
{
    $$ [|if ({|placeholder:true|})
    {
    }|]
}";
 
        var expectedLSPSnippet =
@"$0 if (${1:true})
    {
    }";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodForwardsForPlaceholder()
    {
        var markup =
@"public void Method()
{
    [|if (true)
     {$$
     }|] {|placeholder:test|}
}";
 
        var expectedLSPSnippet =
@"if (true)
     {$0
     } ${1:test}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodBackwardsForPlaceholder()
    {
        var markup =
@"public void Method()
{
    {|placeholder:test|} [|if (true)
    {$$
    }|]";
 
        var expectedLSPSnippet =
@"${1:test} if (true)
    {$0
    }";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodForwardsForPlaceholderThenCaret()
    {
        var markup =
@"public void Method()
{
    [|if (true)
    {
    }|] {|placeholder:test|} $$
}";
 
        var expectedLSPSnippet =
@"if (true)
    {
    } ${1:test} $0";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodForwardsForCaretThenPlaceholder()
    {
        var markup =
@"public void Method()
{
    [|if (true)
    {
    }|] $$ {|placeholder:test|}
}";
 
        var expectedLSPSnippet =
@"if (true)
    {
    } $0 ${1:test}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodBackwardsForPlaceholderThenCaret()
    {
        var markup =
@"public void Method()
{
    {|placeholder:test|} $$ [|if (true)
    {
    }|]
}";
 
        var expectedLSPSnippet =
@"${1:test} $0 if (true)
    {
    }";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodBackwardsForCaretThenPlaceholder()
    {
        var markup =
@"public void Method()
{
    $$ {|placeholder:test|} [|if (true)
    {
    }|]
}";
 
        var expectedLSPSnippet =
@"$0 ${1:test} if (true)
    {
    }";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodBackwardsForCaretForwardsForPlaceholder()
    {
        var markup =
@"public void Method()
{
    $$ [|if (true)
    {
    }|] {|placeholder:test|}
}";
 
        var expectedLSPSnippet =
@"$0 if (true)
    {
    } ${1:test}";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    public Task TestExtendSnippetTextChangeInMethodBackwardsForPlaceholderForwardsForCaret()
    {
        var markup =
@"public void Method()
{
    {|placeholder:test|} [|if (true)
    {
    }|] $$
}";
 
        var expectedLSPSnippet =
@"${1:test} if (true)
    {
    } $0";
 
        return TestAsync(markup, expectedLSPSnippet);
    }
 
    [Fact]
    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]
    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, caretPosition: 12,
            ImmutableArray<SnippetPlaceholder>.Empty, new TextChange(new TextSpan(8, 0), "quux"), triggerLocation: 12, CancellationToken.None).Result;
        AssertEx.EqualOrDiff("quux$0", lspSnippetString);
    }
 
    [Fact]
    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, caretPosition: 12,
            ImmutableArray<SnippetPlaceholder>.Empty, new TextChange(new TextSpan(4, 4), "bar quux"), triggerLocation: 12, CancellationToken.None).Result;
        AssertEx.EqualOrDiff("bar quux$0", lspSnippetString);
    }
 
    #endregion
 
    #region LSP Snippet generation tests
 
    [Fact]
    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]
    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]
    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<string, ImmutableArray<TextSpan>> 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, stringSpan.Start, CancellationToken.None).ConfigureAwait(false);
        AssertEx.EqualOrDiff(output, lspSnippetString);
    }
 
    private static ImmutableArray<SnippetPlaceholder> GetSnippetPlaceholders(string text, IDictionary<string, ImmutableArray<TextSpan>> placeholderDictionary)
    {
        using var _ = ArrayBuilder<SnippetPlaceholder>.GetInstance(out var arrayBuilder);
        foreach (var kvp in placeholderDictionary)
        {
            if (kvp.Key.Length > 0)
            {
                var spans = kvp.Value;
                var placeholderText = text.Substring(spans[0].Start, spans[0].Length);
                var placeholders = spans.Select(span => span.Start).ToImmutableArray();
                arrayBuilder.Add(new SnippetPlaceholder(placeholderText, placeholders));
            }
        }
 
        return arrayBuilder.ToImmutableAndClear();
    }
}