Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ public FoldingRangesHandler(IGlobalOptionService globalOptions)
if (document is null)
return SpecializedTasks.Null<FoldingRange[]>();

return SpecializedTasks.AsNullable(GetFoldingRangesAsync(_globalOptions, document, cancellationToken));
var lineFoldingOnly = context.GetRequiredClientCapabilities().TextDocument?.FoldingRange?.LineFoldingOnly == true;
return SpecializedTasks.AsNullable(GetFoldingRangesAsync(_globalOptions, document, lineFoldingOnly, cancellationToken));
}

internal static Task<FoldingRange[]> GetFoldingRangesAsync(
IGlobalOptionService globalOptions,
Document document,
bool lineFoldingOnly,
CancellationToken cancellationToken)
{
var options = globalOptions.GetBlockStructureOptions(document.Project) with
Expand All @@ -59,7 +61,7 @@ internal static Task<FoldingRange[]> GetFoldingRangesAsync(
ShowBlockStructureGuidesForCodeLevelConstructs = true
};

return GetFoldingRangesAsync(document, options, cancellationToken);
return GetFoldingRangesAsync(document, options, lineFoldingOnly, cancellationToken);
}

/// <summary>
Expand All @@ -68,6 +70,7 @@ internal static Task<FoldingRange[]> GetFoldingRangesAsync(
public static async Task<FoldingRange[]> GetFoldingRangesAsync(
Document document,
BlockStructureOptions options,
bool lineFoldingOnly,
CancellationToken cancellationToken)
{
var blockStructureService = document.GetRequiredLanguageService<BlockStructureService>();
Expand All @@ -76,10 +79,10 @@ public static async Task<FoldingRange[]> GetFoldingRangesAsync(
return [];

var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
return GetFoldingRanges(blockStructure, text);
return GetFoldingRanges(blockStructure, text, lineFoldingOnly);
}

private static FoldingRange[] GetFoldingRanges(BlockStructure blockStructure, SourceText text)
private static FoldingRange[] GetFoldingRanges(BlockStructure blockStructure, SourceText text, bool lineFoldingOnly)
{
if (blockStructure.Spans.IsEmpty)
{
Expand All @@ -103,8 +106,6 @@ private static FoldingRange[] GetFoldingRanges(BlockStructure blockStructure, So
continue;
}

// TODO - Figure out which blocks should be returned as a folding range (and what kind).
// https://github.com/dotnet/roslyn/projects/45#card-20049168
FoldingRangeKind? foldingRangeKind = span.Type switch
{
BlockTypes.Comment => FoldingRangeKind.Comment,
Expand All @@ -123,8 +124,62 @@ private static FoldingRange[] GetFoldingRanges(BlockStructure blockStructure, So
Kind = foldingRangeKind,
CollapsedText = span.BannerText
});

if (lineFoldingOnly)
{
foldingRanges = AdjustToEnsureNonOverlappingLines(foldingRanges);
}
}

return foldingRanges.ToArray();

static ArrayBuilder<FoldingRange> AdjustToEnsureNonOverlappingLines(ArrayBuilder<FoldingRange> foldingRanges)
{
using var _ = PooledDictionary<int, FoldingRange>.GetInstance(out var startLineToFoldingRange);

// Spans are sorted in descending order by start position (the span starting closer to the end of the file is first).
foreach (var foldingRange in foldingRanges)
{
var updatedRange = foldingRange;
// Check if another span starts on the same line.
if (startLineToFoldingRange.ContainsKey(foldingRange.StartLine))
{
// There's already a span that starts on this line. We want to keep the innermost span, which is the one
// we already have in the dictionary (as it started later in the file). Skip this one.
continue;
}

var endLine = foldingRange.EndLine;

// Check if this span ends on the same line another span starts.
// Since we're iterating bottom up, if there is a span that starts on this end line, it will be in the dictionary.
if (startLineToFoldingRange.ContainsKey(endLine))
{
// The end line of this span overlaps with the start line of another span - attempt to adjust this one
// to the prior line.
var adjustedEndLine = endLine - 1;

// If the adjusted end line is now at or before the start line, there's no folding range possible without line overlapping another span.
if (adjustedEndLine <= foldingRange.StartLine)
{
continue;
}

updatedRange = new FoldingRange
{
StartLine = foldingRange.StartLine,
StartCharacter = foldingRange.StartCharacter,
EndLine = adjustedEndLine,
EndCharacter = foldingRange.EndCharacter,
Kind = foldingRange.Kind,
CollapsedText = foldingRange.CollapsedText
};
}

startLineToFoldingRange[foldingRange.StartLine] = updatedRange;
}

return [.. startLineToFoldingRange.Values];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#nullable disable

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -64,9 +65,89 @@ private Action<int> Foo(){|implementation: => i =>{|foldingRange:
}|}
""");

private async Task AssertFoldingRanges(bool mutatingLspWorkspace, string markup, string collapsedText = null)
[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/7974")]
public Task TestGetFoldingRangeAsync_LineFoldingOnly_NoOverlappingRanges(bool mutatingLspWorkspace)
=> AssertFoldingRanges(mutatingLspWorkspace, """
class C{|foldingRange:
{
public void M1(){|implementation:
{
var x = 1;
}|}
public void M2(){|implementation:
{
var y = 2;
}|}
}|}
""", lineFoldingOnly: true);

[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/7974")]
public Task TestGetFoldingRangeAsync_LineFoldingOnly_StartLineOverlapsChoosesInner(bool mutatingLspWorkspace)
=> AssertFoldingRanges(mutatingLspWorkspace, """
class C { public void M1() {|implementation:{
var x = 1;
}|}
}
""", lineFoldingOnly: true);

[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/7974")]
public Task TestGetFoldingRangeAsync_LineFoldingOnly_EndLineOverlaps(bool mutatingLspWorkspace)
=> AssertFoldingRanges(mutatingLspWorkspace, """
class C{|foldingRange:
{
void M(){|implementation:
{
void Local(){|foldingRange:
{
}|}}|}}|}
""", lineFoldingOnly: true);

[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/7974")]
public Task TestGetFoldingRangeAsync_LineFoldingOnly_EndLineOverlapsStartLine(bool mutatingLspWorkspace)
=> AssertFoldingRanges(mutatingLspWorkspace, """
class C{|foldingRange:
{
void M(){|implementation:
{
if (true){|foldingRange:
{|}
} else{|foldingRange: {
}|}
}|}
}|}
""", lineFoldingOnly: true);

[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/7974")]
public Task TestGetFoldingRangeAsync_WithoutLineFoldingOnly_AllowsRangesOnSameLine(bool mutatingLspWorkspace)
=> AssertFoldingRanges(mutatingLspWorkspace, """
class C {|foldingRange:{ public void M1() {|implementation:{
if (true){|foldingRange:
{
}|} else{|foldingRange: {
}|}
}|}
}|}
""", lineFoldingOnly: false);

private async Task AssertFoldingRanges(
bool mutatingLspWorkspace,
[StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string markup,
string collapsedText = null,
bool lineFoldingOnly = false)
{
var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
var clientCapabilities = new LSP.ClientCapabilities();
if (lineFoldingOnly)
{
clientCapabilities.TextDocument = new LSP.TextDocumentClientCapabilities
{
FoldingRange = new LSP.FoldingRangeSetting
{
LineFoldingOnly = true
}
};
}

var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities);
var expected = testLspServer.GetLocations()
.SelectMany(kvp => kvp.Value.Select(location => CreateFoldingRange(kvp.Key, location.Range, collapsedText ?? "...")))
.OrderByDescending(range => range.StartLine)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ public static Task<FoldingRange[]> GetFoldingRangesAsync(Document document, Canc
// composition so can't import it (and its internal anyway)
var globalOptions = document.Project.Solution.Services.ExportProvider.GetService<IGlobalOptionService>();

return FoldingRangesHandler.GetFoldingRangesAsync(globalOptions, document, cancellationToken);
return FoldingRangesHandler.GetFoldingRangesAsync(globalOptions, document, lineFoldingOnly: false, cancellationToken);
}
}
Loading