File: SpellCheck\SpellCheckTests.cs
Web Access
Project: src\src\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SpellCheck;
 
public sealed class SpellCheckTests(ITestOutputHelper testOutputHelper)
    : AbstractLanguageServerProtocolTests(testOutputHelper)
{
 
    #region Document
 
    [Theory, CombinatorialData]
    public async Task TestNoDocumentResultsForClosedFiles(bool mutatingLspWorkspace)
    {
        var markup =
@"class A
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        Assert.Empty(results);
    }
 
    [Theory, CombinatorialData]
    public async Task TestDocumentResultsForOpenFiles(bool mutatingLspWorkspace)
    {
        var markup =
@"class {|Identifier:A|}
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var testDocument = testLspServer.TestWorkspace.Documents.Single();
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(testDocument.AnnotatedSpans),
        });
    }
 
    [Theory, CombinatorialData]
    public async Task TestLotsOfResults(bool mutatingLspWorkspace)
    {
        // Produce an 'interesting' large string, with varying length identifiers, and varying distances between the spans. 
        var random = new Random(Seed: 0);
        var markup = string.Join(Environment.NewLine, Enumerable.Range(0, 5500).Select(v =>
$$"""
class {|Identifier:A{{v}}|}
{
}
{{string.Join(Environment.NewLine, Enumerable.Repeat("", random.Next() % 5))}}
"""));
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var testDocument = testLspServer.TestWorkspace.Documents.Single();
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        var sourceText = await document.GetTextAsync();
        Assert.True(results.Length == 6);
 
        var allRanges = GetRanges(testDocument.AnnotatedSpans);
        for (var i = 0; i < results.Length; i++)
        {
            AssertJsonEquals(results[i], new VSInternalSpellCheckableRangeReport
            {
                ResultId = "DocumentSpellCheckHandler:1",
                Ranges = [.. allRanges.Skip(3 * i * 1000).Take(3 * 1000)],
            });
        }
    }
 
    [Theory, CombinatorialData]
    public async Task TestDocumentResultsForRemovedDocument(bool mutatingLspWorkspace)
    {
        var markup =
@"class {|Identifier:A|}
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
        var workspace = testLspServer.TestWorkspace;
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        // Get the diagnostics for the solution containing the doc.
        var solution = document.Project.Solution;
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI()).ConfigureAwait(false);
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(workspace.Documents.Single().AnnotatedSpans),
        });
 
        // Now remove the doc.
        workspace.OnDocumentRemoved(workspace.Documents.Single().Id);
        await CloseDocumentAsync(testLspServer, document);
 
        results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI(), results.Single().ResultId).ConfigureAwait(false);
 
        Assert.Null(results.Single().Ranges);
        Assert.Null(results.Single().ResultId);
    }
 
    [Theory, CombinatorialData]
    public async Task TestNoChangeIfDocumentResultsCalledTwice(bool mutatingLspWorkspace)
    {
        var markup =
@"class {|Identifier:A|}
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans),
        });
 
        var resultId = results.Single().ResultId;
        results = await RunGetDocumentSpellCheckSpansAsync(
            testLspServer, document.GetURI(), previousResultId: resultId);
 
        Assert.Null(results.Single().Ranges);
        Assert.Equal(resultId, results.Single().ResultId);
    }
 
    [Theory, CombinatorialData]
    public async Task TestDocumentResultChangedAfterEntityAdded(bool mutatingLspWorkspace)
    {
        var markup =
@"class {|Identifier:A|}
{
}
 
";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans),
        });
 
        await InsertTextAsync(testLspServer, document, sourceText.Length, "// comment");
 
        var (_, lspSolution) = await testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None).ConfigureAwait(false);
        document = lspSolution!.Projects.Single().Documents.Single();
        results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI(), results.Single().ResultId);
 
        MarkupTestFile.GetSpans(
@"class {|Identifier:A|}
{
}
 
{|Comment:// comment|}", out _, out IDictionary<string, ImmutableArray<TextSpan>> annotatedSpans);
 
        sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:2",
            Ranges = GetRanges(annotatedSpans),
        });
    }
 
    [Theory, CombinatorialData]
    public async Task TestDocumentResultIdSameAfterIrrelevantEdit(bool mutatingLspWorkspace)
    {
        var markup =
@"class {|Identifier:A|}
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans),
        });
 
        await InsertTextAsync(testLspServer, document, sourceText.Length, text: " ");
 
        results = await RunGetDocumentSpellCheckSpansAsync(
            testLspServer, document.GetURI(),
            previousResultId: results[0].ResultId);
 
        sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
        });
    }
 
    [Theory, CombinatorialData]
    public async Task TestDocumentResultsAreNotMapped(bool mutatingLspWorkspace)
    {
        var markup =
@"#line 4 ""test.txt""
class {|Identifier:A|}
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI());
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans),
        });
    }
 
    [Theory, CombinatorialData]
    public async Task TestStreamingDocumentDiagnostics(bool mutatingLspWorkspace)
    {
        var markup =
@"class {|Identifier:A|}
{
}";
        await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
        var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single();
 
        await OpenDocumentAsync(testLspServer, document);
 
        var results = await RunGetDocumentSpellCheckSpansAsync(testLspServer, document.GetURI(), useProgress: true);
 
        var sourceText = await document.GetTextAsync();
        Assert.Single(results);
        AssertJsonEquals(results.Single(), new VSInternalSpellCheckableRangeReport
        {
            ResultId = "DocumentSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.Single().AnnotatedSpans),
        });
    }
 
    #endregion
 
    #region Workspace
 
    [Theory, CombinatorialData]
    public async Task TestWorkspaceResultsForClosedFiles(bool mutatingLspWorkspace)
    {
        var markup1 =
@"class {|Identifier:A|}
{
}";
        var markup2 = "";
        await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace);
 
        var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
        Assert.Equal(2, results.Length);
 
        var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First();
        var sourceText = await document.GetTextAsync();
        AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport
        {
            TextDocument = CreateTextDocumentIdentifier(document.GetURI()),
            ResultId = "WorkspaceSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans),
        });
        AssertEx.Empty(results[1].Ranges);
    }
 
    [Theory, CombinatorialData]
    public async Task TestNoWorkspaceDiagnosticsForClosedFilesInProjectsWithIncorrectLanguage(bool mutatingLspWorkspace)
    {
        var csharpMarkup =
@"class A {";
        var typeScriptMarkup = "???";
 
        var workspaceXml =
@$"<Workspace>
            <Project Language=""C#"" CommonReferences=""true"" AssemblyName=""CSProj1"">
                <Document FilePath=""C:\C.cs"">{csharpMarkup}</Document>
            </Project>
            <Project Language=""TypeScript"" CommonReferences=""true"" AssemblyName=""TypeScriptProj"">
                <Document FilePath=""C:\T.ts"">{typeScriptMarkup}</Document>
            </Project>
        </Workspace>";
 
        await using var testLspServer = await CreateXmlTestLspServerAsync(workspaceXml, mutatingLspWorkspace);
 
        var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
        Assert.True(results.All(r => r.TextDocument!.Uri.LocalPath == "C:\\C.cs"));
    }
 
    //        [Fact]
    //        public async Task TestWorkspaceDiagnosticsForSourceGeneratedFiles()
    //        {
    //            var markup1 =
    //@"class A {";
    //            var markup2 = "";
    //            await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync(
    //                markups: Array.Empty<string>(),
    //                sourceGeneratedMarkups: new[] { markup1, markup2 },
    //                BackgroundAnalysisScope.FullSolution,
    //                useVSDiagnostics);
 
    //            var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
    //            // Project.GetSourceGeneratedDocumentsAsync may not return documents in a deterministic order, so we sort
    //            // the results here to ensure subsequent assertions are not dependent on the order of items provided by the
    //            // project.
    //            results = results.Sort((x, y) => x.Uri.ToString().CompareTo(y.Uri.ToString()));
 
    //            Assert.Equal(2, results.Length);
    //            Assert.Equal("CS1513", results[0].Diagnostics.Single().Code);
    //            Assert.Empty(results[1].Diagnostics);
    //        }
 
    [Theory, CombinatorialData]
    public async Task TestWorkspaceResultsForRemovedDocument(bool mutatingLspWorkspace)
    {
        var markup1 =
@"class {|Identifier:A|}
{
}";
        var markup2 = "";
        await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace);
 
        var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
        Assert.Equal(2, results.Length);
 
        var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First();
        var sourceText = await document.GetTextAsync();
        AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport
        {
            TextDocument = CreateTextDocumentIdentifier(document.GetURI()),
            ResultId = "WorkspaceSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans),
        });
        AssertEx.Empty(results[1].Ranges);
 
        testLspServer.TestWorkspace.OnDocumentRemoved(testLspServer.TestWorkspace.Documents.First().Id);
 
        var results2 = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, previousResults: CreateParamsFromPreviousReports(results));
 
        // First doc should show up as removed.
        Assert.Equal(2, results2.Length);
        Assert.Null(results2[0].Ranges);
        Assert.Null(results2[0].ResultId);
 
        // Second doc should be unchanged
        AssertEx.Empty(results[1].Ranges);
        Assert.Equal(results[1].ResultId, results2[1].ResultId);
    }
 
    [Theory, CombinatorialData]
    public async Task TestNoChangeIfWorkspaceResultsCalledTwice(bool mutatingLspWorkspace)
    {
        var markup1 =
@"class {|Identifier:A|}
{
}";
        var markup2 = "";
        await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace);
 
        var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
        Assert.Equal(2, results.Length);
 
        var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First();
        var sourceText = await document.GetTextAsync();
        AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport
        {
            TextDocument = CreateTextDocumentIdentifier(document.GetURI()),
            ResultId = "WorkspaceSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans),
        });
        AssertEx.Empty(results[1].Ranges);
 
        var results2 = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, previousResults: CreateParamsFromPreviousReports(results));
 
        Assert.Equal(2, results2.Length);
        Assert.Null(results2[0].Ranges);
        Assert.Null(results2[1].Ranges);
 
        Assert.Equal(results[0].ResultId, results2[0].ResultId);
        Assert.Equal(results[1].ResultId, results2[1].ResultId);
    }
 
    [Theory, CombinatorialData]
    public async Task TestWorkspaceResultUpdatedAfterEdit(bool mutatingLspWorkspace)
    {
        var markup1 =
@"class {|Identifier:A|}
{
}
 
";
        var markup2 = "";
        await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace);
 
        var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
        Assert.Equal(2, results.Length);
 
        var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First();
        var sourceText = await document.GetTextAsync();
        AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport
        {
            TextDocument = CreateTextDocumentIdentifier(document.GetURI()),
            ResultId = "WorkspaceSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans),
        });
        AssertEx.Empty(results[1].Ranges);
 
        await PullDiagnosticTests.InsertInClosedDocumentAsync(testLspServer, document.Id, "// comment");
 
        var results2 = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, previousResults: CreateParamsFromPreviousReports(results));
 
        Assert.Equal(2, results2.Length);
        var (_, lspSolution) = await testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None).ConfigureAwait(false);
        document = lspSolution!.Projects.Single().Documents.First();
        sourceText = await document.GetTextAsync();
 
        MarkupTestFile.GetSpans(
@"class {|Identifier:A|}
{
}
 
{|Comment:// comment|}", out _, out IDictionary<string, ImmutableArray<TextSpan>> annotatedSpans);
 
        AssertJsonEquals(results2[0], new VSInternalWorkspaceSpellCheckableReport
        {
            TextDocument = CreateTextDocumentIdentifier(document.GetURI()),
            ResultId = "WorkspaceSpellCheckHandler:3",
            Ranges = GetRanges(annotatedSpans),
        });
        Assert.Null(results2[1].Ranges);
 
        Assert.NotEqual(results[0].ResultId, results2[0].ResultId);
        Assert.Equal(results[1].ResultId, results2[1].ResultId);
    }
 
    [Theory, CombinatorialData]
    public async Task TestStreamingWorkspaceResults(bool mutatingLspWorkspace)
    {
        var markup1 =
@"class {|Identifier:A|}
{
}";
        var markup2 = "";
        await using var testLspServer = await CreateTestLspServerAsync([markup1, markup2], mutatingLspWorkspace);
 
        var results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer);
 
        Assert.Equal(2, results.Length);
 
        var document = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.First();
        var sourceText = await document.GetTextAsync();
        AssertJsonEquals(results[0], new VSInternalWorkspaceSpellCheckableReport
        {
            TextDocument = CreateTextDocumentIdentifier(document.GetURI()),
            ResultId = "WorkspaceSpellCheckHandler:1",
            Ranges = GetRanges(testLspServer.TestWorkspace.Documents.First().AnnotatedSpans),
        });
        AssertEx.Empty(results[1].Ranges);
 
        results = await RunGetWorkspaceSpellCheckSpansAsync(testLspServer, CreateParamsFromPreviousReports(results), useProgress: true);
 
        Assert.Equal(2, results.Length);
        Assert.Null(results[0].Ranges);
        Assert.Null(results[1].Ranges);
    }
 
    #endregion
 
    private static int[] GetRanges(IDictionary<string, ImmutableArray<TextSpan>> annotatedSpans)
    {
        var allSpans = annotatedSpans
            .SelectMany(kvp => kvp.Value.Select(textSpan => (kind: kvp.Key, textSpan))
            .OrderBy(t => t.textSpan.Start))
            .ToImmutableArray();
 
        var ranges = new int[allSpans.Length * 3];
        var index = 0;
        var lastSpanEnd = 0;
 
        foreach (var (kind, span) in allSpans)
        {
            ranges[index++] = (int)Convert(kind);
            ranges[index++] = span.Start - lastSpanEnd;
            ranges[index++] = span.Length;
 
            lastSpanEnd = span.End;
        }
 
        return ranges;
    }
 
    private static VSInternalSpellCheckableRangeKind Convert(string kind)
        => kind switch
        {
            "String" => VSInternalSpellCheckableRangeKind.String,
            "Comment" => VSInternalSpellCheckableRangeKind.Comment,
            "Identifier" => VSInternalSpellCheckableRangeKind.Identifier,
            _ => throw ExceptionUtilities.UnexpectedValue(kind),
        };
 
    private static Task OpenDocumentAsync(TestLspServer testLspServer, Document document)
        => testLspServer.OpenDocumentAsync(document.GetURI());
 
    private static Task CloseDocumentAsync(TestLspServer testLspServer, Document document)
        => testLspServer.CloseDocumentAsync(document.GetURI());
 
    private static async Task<VSInternalSpellCheckableRangeReport[]> RunGetDocumentSpellCheckSpansAsync(
        TestLspServer testLspServer,
        Uri uri,
        string? previousResultId = null,
        bool useProgress = false)
    {
        BufferedProgress<VSInternalSpellCheckableRangeReport[]>? progress = useProgress
            ? BufferedProgress.Create<VSInternalSpellCheckableRangeReport[]>(null) : null;
        var spans = await testLspServer.ExecuteRequestAsync<VSInternalDocumentSpellCheckableParams, VSInternalSpellCheckableRangeReport[]>(
            VSInternalMethods.TextDocumentSpellCheckableRangesName,
            CreateDocumentParams(uri, previousResultId, progress),
            CancellationToken.None).ConfigureAwait(false);
 
        if (useProgress)
        {
            Assert.Null(spans);
            spans = progress!.Value.GetFlattenedValues();
        }
 
        AssertEx.NotNull(spans);
        return spans;
    }
 
    private static async Task<VSInternalWorkspaceSpellCheckableReport[]> RunGetWorkspaceSpellCheckSpansAsync(
        TestLspServer testLspServer,
        ImmutableArray<(string resultId, Uri uri)>? previousResults = null,
        bool useProgress = false)
    {
        BufferedProgress<VSInternalWorkspaceSpellCheckableReport[]>? progress = useProgress ? BufferedProgress.Create<VSInternalWorkspaceSpellCheckableReport[]>(null) : null;
        var spans = await testLspServer.ExecuteRequestAsync<VSInternalWorkspaceSpellCheckableParams, VSInternalWorkspaceSpellCheckableReport[]>(
            VSInternalMethods.WorkspaceSpellCheckableRangesName,
            CreateWorkspaceParams(previousResults, progress),
            CancellationToken.None).ConfigureAwait(false);
 
        if (useProgress)
        {
            Assert.Null(spans);
            spans = progress!.Value.GetFlattenedValues();
        }
 
        AssertEx.NotNull(spans);
        return spans;
    }
 
    private static async Task InsertTextAsync(
        TestLspServer testLspServer,
        Document document,
        int position,
        string text)
    {
        var sourceText = await document.GetTextAsync();
        var lineInfo = sourceText.Lines.GetLinePositionSpan(new TextSpan(position, 0));
 
        await testLspServer.InsertTextAsync(document.GetURI(), (lineInfo.Start.Line, lineInfo.Start.Character, text));
    }
 
    private static VSInternalDocumentSpellCheckableParams CreateDocumentParams(
        Uri uri,
        string? previousResultId = null,
        IProgress<VSInternalSpellCheckableRangeReport[]>? progress = null)
    {
        return new VSInternalDocumentSpellCheckableParams
        {
            TextDocument = new TextDocumentIdentifier { Uri = uri },
            PreviousResultId = previousResultId,
            PartialResultToken = progress,
        };
    }
 
    private static VSInternalWorkspaceSpellCheckableParams CreateWorkspaceParams(
        ImmutableArray<(string resultId, Uri uri)>? previousResults = null,
        IProgress<VSInternalWorkspaceSpellCheckableReport[]>? progress = null)
    {
        return new VSInternalWorkspaceSpellCheckableParams
        {
            PreviousResults = previousResults?.Select(r => new VSInternalStreamingParams { PreviousResultId = r.resultId, TextDocument = new TextDocumentIdentifier { Uri = r.uri } }).ToArray(),
            PartialResultToken = progress,
        };
    }
 
    private static ImmutableArray<(string resultId, Uri uri)> CreateParamsFromPreviousReports(VSInternalWorkspaceSpellCheckableReport[] results)
    {
        return [.. results.Select(r => (r.ResultId!, r.TextDocument.Uri))];
    }
}