Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,66 @@ 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
};
}

// These are explicitly ignored by the client when lineFoldingOnly is true, so no need to serialize them.
updatedRange.StartCharacter = null;
updatedRange.EndCharacter = null;

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,11 +65,90 @@ 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
{
TextDocument = new LSP.TextDocumentClientCapabilities
{
FoldingRange = new LSP.FoldingRangeSetting
{
LineFoldingOnly = lineFoldingOnly
}
}
};

var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities);
var expected = testLspServer.GetLocations()
.SelectMany(kvp => kvp.Value.Select(location => CreateFoldingRange(kvp.Key, location.Range, collapsedText ?? "...")))
.SelectMany(kvp => kvp.Value.Select(location => CreateFoldingRange(kvp.Key, location.Range, collapsedText ?? "...", lineFoldingOnly)))
.OrderByDescending(range => range.StartLine)
.ThenByDescending(range => range.StartCharacter)
.ToArray();
Expand All @@ -89,7 +169,7 @@ private async Task AssertFoldingRanges(bool mutatingLspWorkspace, string markup,
request, CancellationToken.None);
}

private static LSP.FoldingRange CreateFoldingRange(string kind, LSP.Range range, string collapsedText)
private static LSP.FoldingRange CreateFoldingRange(string kind, LSP.Range range, string collapsedText, bool lineFoldingOnly)
=> new()
{
Kind = kind switch
Expand All @@ -98,8 +178,8 @@ private static LSP.FoldingRange CreateFoldingRange(string kind, LSP.Range range,
null => null,
_ => new(kind)
},
StartCharacter = range.Start.Character,
EndCharacter = range.End.Character,
StartCharacter = lineFoldingOnly ? null : range.Start.Character,
EndCharacter = lineFoldingOnly ? null : range.End.Character,
StartLine = range.Start.Line,
EndLine = range.End.Line,
CollapsedText = collapsedText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
Expand All @@ -12,12 +13,18 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;

internal static class FoldingRanges
{
[Obsolete("Use GetFoldingRangesAsync(Document, bool, CancellationToken) instead", error: true)]
public static Task<FoldingRange[]> GetFoldingRangesAsync(Document document, CancellationToken cancellationToken)
{
return GetFoldingRangesAsync(document, lineFoldingOnly: false, cancellationToken);
}

public static Task<FoldingRange[]> GetFoldingRangesAsync(Document document, bool lineFoldingOnly, CancellationToken cancellationToken)
{
// We need to manually get the IGlobalOptionsService out of the Mef composition, because Razor has its own
// 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: lineFoldingOnly, cancellationToken);
}
}
Loading