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
33 changes: 33 additions & 0 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,39 @@ func (f *FourslashTest) VerifyOutliningSpans(t *testing.T, foldingRangeKind ...l
}
}

// FoldingRangeLineExpected represents expected start and end lines for a folding range.
type FoldingRangeLineExpected struct {
StartLine uint32
EndLine uint32
}

// VerifyFoldingRangeLines verifies folding ranges by comparing only start and end lines.
// This is useful for testing with lineFoldingOnly where character positions are ignored.
func (f *FourslashTest) VerifyFoldingRangeLines(t *testing.T, expected []FoldingRangeLineExpected) {
params := &lsproto.FoldingRangeParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
}
result := sendRequest(t, f, lsproto.TextDocumentFoldingRangeInfo, params)
if result.FoldingRanges == nil {
t.Fatalf("Nil response received for folding range request")
}

actualRanges := *result.FoldingRanges
if len(actualRanges) != len(expected) {
t.Fatalf("verifyFoldingRangeLines failed - expected %d ranges, got %d", len(expected), len(actualRanges))
}

for i, exp := range expected {
got := actualRanges[i]
if got.StartLine != exp.StartLine || got.EndLine != exp.EndLine {
t.Errorf("verifyFoldingRangeLines failed - range %d: expected (startLine=%d, endLine=%d), got (startLine=%d, endLine=%d)",
i, exp.StartLine, exp.EndLine, got.StartLine, got.EndLine)
}
}
}

func (f *FourslashTest) VerifyBaselineHover(t *testing.T) {
markersAndItems := core.MapFiltered(f.Markers(), func(marker *Marker) (markerAndItem[*lsproto.Hover], bool) {
if marker.Name == nil {
Expand Down
114 changes: 114 additions & 0 deletions internal/fourslash/tests/foldingRangeLineFoldingOnly_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestFoldingRangeLineFoldingOnly(t *testing.T) {
t.Parallel()

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `if (EMPTY_TAGs.has(tag)) {
output += "/>";
} else {
output += ">";

if (!html && kidcount > 0) {
//
}
}

export function use<T>(ctx: any): T | undefined {
//
}`
ptrTrue := true
capabilities := &lsproto.ClientCapabilities{
TextDocument: &lsproto.TextDocumentClientCapabilities{
FoldingRange: &lsproto.FoldingRangeClientCapabilities{
LineFoldingOnly: &ptrTrue,
FoldingRange: &lsproto.ClientFoldingRangeOptions{
CollapsedText: &ptrTrue,
},
},
},
}
f, done := fourslash.NewFourslash(t, capabilities, content)
defer done()

// With lineFoldingOnly, end lines should be adjusted so closing brackets stay visible.
// Line 0: if (EMPTY_TAGs.has(tag)) {
// Line 1: output += "/>";
// Line 2: } else {
// Line 3: output += ">";
// Line 4:
// Line 5: if (!html && kidcount > 0) {
// Line 6: //
// Line 7: }
// Line 8: }
// Line 9:
// Line 10: export function use<T>(ctx: any): T | undefined {
// Line 11: //
// Line 12: }
f.VerifyFoldingRangeLines(t, []fourslash.FoldingRangeLineExpected{
{StartLine: 0, EndLine: 1}, // if block: end adjusted from line 2 to 1
{StartLine: 2, EndLine: 7}, // else block: end adjusted from line 8 to 7
{StartLine: 5, EndLine: 6}, // inner if block: end adjusted from line 7 to 6
{StartLine: 10, EndLine: 11}, // function: end adjusted from line 12 to 11
})
}

func TestFoldingRangeLineFoldingOnlyWithRegions(t *testing.T) {
t.Parallel()

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// #region MyRegion
const x = 1;
function foo() {
return x;
}
// #endregion

// #region Outer
const y = 2;
// #region Inner
const z = 3;
// #endregion
// #endregion`
ptrTrue := true
capabilities := &lsproto.ClientCapabilities{
TextDocument: &lsproto.TextDocumentClientCapabilities{
FoldingRange: &lsproto.FoldingRangeClientCapabilities{
LineFoldingOnly: &ptrTrue,
FoldingRange: &lsproto.ClientFoldingRangeOptions{
CollapsedText: &ptrTrue,
},
},
},
}
f, done := fourslash.NewFourslash(t, capabilities, content)
defer done()

// Line 0: // #region MyRegion
// Line 1: const x = 1;
// Line 2: function foo() {
// Line 3: return x;
// Line 4: }
// Line 5: // #endregion
// Line 6:
// Line 7: // #region Outer
// Line 8: const y = 2;
// Line 9: // #region Inner
// Line 10: const z = 3;
// Line 11: // #endregion
// Line 12: // #endregion
f.VerifyFoldingRangeLines(t, []fourslash.FoldingRangeLineExpected{
{StartLine: 0, EndLine: 5}, // #region MyRegion: NOT adjusted (ends with "n", not a closing pair)
{StartLine: 2, EndLine: 3}, // function foo() block: end adjusted from line 4 to 3
{StartLine: 7, EndLine: 12}, // #region Outer: NOT adjusted
{StartLine: 9, EndLine: 11}, // #region Inner: NOT adjusted
})
}
30 changes: 30 additions & 0 deletions internal/ls/folding.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l
_, sourceFile := l.getProgramAndFile(documentURI)
res := l.addNodeOutliningSpans(ctx, sourceFile)
res = append(res, l.addRegionOutliningSpans(ctx, sourceFile)...)
if lsproto.GetClientCapabilities(ctx).TextDocument.FoldingRange.LineFoldingOnly {
res = l.adjustFoldingEnd(res, sourceFile)
}
slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int {
if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 {
return c
Expand All @@ -28,6 +31,33 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l
return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil
}

// adjustFoldingEnd adjusts the end line of folding ranges when the client signals lineFoldingOnly.
// This mirrors the behavior of VS Code's built-in TypeScript extension (workaround for vscode#47240).
// When lineFoldingOnly is true, we hide lines from startLine+1 to endLine. And to keep closing
// brackets/braces visible, we subtract 1 from endLine when the range ends with a closing pair character.
func (l *LanguageService) adjustFoldingEnd(ranges []*lsproto.FoldingRange, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
sourceText := sourceFile.Text()
result := make([]*lsproto.FoldingRange, 0, len(ranges))
for _, r := range ranges {
if r.EndCharacter != nil && *r.EndCharacter > 0 {
endOffset := int(l.converters.LineAndCharacterToPosition(sourceFile, lsproto.Position{
Line: r.EndLine,
Character: *r.EndCharacter,
}))
if endOffset > 0 && endOffset <= len(sourceText) {
foldEndChar := sourceText[endOffset-1]
if foldEndChar == '}' || foldEndChar == ']' || foldEndChar == ')' || foldEndChar == '`' || foldEndChar == '>' {
if r.EndLine > r.StartLine {
r.EndLine--
}
}
}
}
result = append(result, r)
}
return result
}

func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
depthRemaining := 40
current := 0
Expand Down