diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 034c2ec4ca..d8b74259f7 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -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 { diff --git a/internal/fourslash/tests/foldingRangeLineFoldingOnly_test.go b/internal/fourslash/tests/foldingRangeLineFoldingOnly_test.go new file mode 100644 index 0000000000..54b6d9e65c --- /dev/null +++ b/internal/fourslash/tests/foldingRangeLineFoldingOnly_test.go @@ -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(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(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 + }) +} diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 08d2bef34b..e78adc663e 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -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 @@ -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