From 8b2620940255d3280108be01a98052936b370186 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:24:35 -0800 Subject: [PATCH 1/2] Don't set folding range collapsed text if client doesn't want it --- internal/fourslash/fourslash.go | 14 ++++ internal/ls/folding.go | 139 +++++++++++++++++--------------- 2 files changed, 87 insertions(+), 66 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 488c0beff8..568972cb3b 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -492,6 +492,20 @@ var ( defaultDocumentSymbolCapabilities = &lsproto.DocumentSymbolClientCapabilities{ HierarchicalDocumentSymbolSupport: ptrTrue, } + defaultFoldingRangeCapabilities = &lsproto.FoldingRangeClientCapabilities{ + RangeLimit: ptrTo[uint32](5000), + // LineFoldingOnly: ptrTrue, + FoldingRangeKind: &lsproto.ClientFoldingRangeKindOptions{ + ValueSet: &[]lsproto.FoldingRangeKind{ + lsproto.FoldingRangeKindComment, + lsproto.FoldingRangeKindImports, + lsproto.FoldingRangeKindRegion, + }, + }, + FoldingRange: &lsproto.ClientFoldingRangeOptions{ + CollapsedText: ptrTrue, // Unused by our testing, but set to exercise the code. + }, + } ) func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lsproto.ClientCapabilities { diff --git a/internal/ls/folding.go b/internal/ls/folding.go index ecd819d635..445ca4f9d5 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -18,7 +18,7 @@ import ( func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.FoldingRangeResponse, error) { _, sourceFile := l.getProgramAndFile(documentURI) res := l.addNodeOutliningSpans(ctx, sourceFile) - res = append(res, l.addRegionOutliningSpans(sourceFile)...) + res = append(res, l.addRegionOutliningSpans(ctx, sourceFile)...) slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int { if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 { return c @@ -52,6 +52,7 @@ func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile if lastImport != firstImport { foldingRangeKind := lsproto.FoldingRangeKindImports foldingRange = append(foldingRange, createFoldingRangeFromBounds( + ctx, astnav.GetStartOfNode(astnav.FindChildOfKind(statements.Nodes[firstImport], ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/), statements.Nodes[lastImport].End(), @@ -66,7 +67,7 @@ func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile return foldingRange } -func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { +func (l *LanguageService) addRegionOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { regions := make([]*lsproto.FoldingRange, 0, 40) out := make([]*lsproto.FoldingRange, 0, 40) lineStarts := scanner.GetECMALineStarts(sourceFile) @@ -81,19 +82,22 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) [] if result.isStart { commentStart := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile) foldingRangeKindRegion := lsproto.FoldingRangeKindRegion - collapsedText := "#region" - if result.name != "" { - collapsedText = result.name + region := &lsproto.FoldingRange{ + StartLine: commentStart.Line, + StartCharacter: &commentStart.Character, + Kind: &foldingRangeKindRegion, + } + if supportsCollapsedText(ctx) { + collapsedText := "#region" + if result.name != "" { + collapsedText = result.name + } + region.CollapsedText = &collapsedText } // Our spans start out with some initial data. // On every `#endregion`, we'll come back to these `FoldingRange`s // and fill in their EndLine/EndCharacter. - regions = append(regions, &lsproto.FoldingRange{ - StartLine: commentStart.Line, - StartCharacter: &commentStart.Character, - Kind: &foldingRangeKindRegion, - CollapsedText: &collapsedText, - }) + regions = append(regions, region) } else { if len(regions) > 0 { region := regions[len(regions)-1] @@ -148,7 +152,7 @@ func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile } } - span := getOutliningSpanForNode(n, sourceFile, l) + span := getOutliningSpanForNode(ctx, n, sourceFile, l) if span != nil { foldingRange = append(foldingRange, span) } @@ -221,7 +225,7 @@ func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFi combineAndAddMultipleSingleLineComments := func() *lsproto.FoldingRange { // Only outline spans of two or more consecutive single line comments if singleLineCommentCount > 1 { - return createFoldingRangeFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l) + return createFoldingRangeFromBounds(ctx, firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l) } return nil } @@ -260,7 +264,7 @@ func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFi if comments != nil { foldingRange = append(foldingRange, comments) } - foldingRange = append(foldingRange, createFoldingRangeFromBounds(commentPos, commentEnd, foldingRangeKindComment, sourceFile, l)) + foldingRange = append(foldingRange, createFoldingRangeFromBounds(ctx, commentPos, commentEnd, foldingRangeKindComment, sourceFile, l)) singleLineCommentCount = 0 break default: @@ -307,25 +311,25 @@ func parseRegionDelimiter(lineText string) *regionDelimiterResult { } } -func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func getOutliningSpanForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { switch n.Kind { case ast.KindBlock: if ast.IsFunctionLike(n.Parent) { - return functionSpan(n.Parent, n, sourceFile, l) + return functionSpan(ctx, n.Parent, n, sourceFile, l) } // Check if the block is standalone, or 'attached' to some parent statement. // If the latter, we want to collapse the block, but consider its hint span // to be the entire span of the parent. switch n.Parent.Kind { case ast.KindDoStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindForStatement, ast.KindIfStatement, ast.KindWhileStatement, ast.KindWithStatement, ast.KindCatchClause: - return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) case ast.KindTryStatement: // Could be the try-block, or the finally-block. tryStatement := n.Parent.AsTryStatement() if tryStatement.TryBlock == n { - return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) } else if tryStatement.FinallyBlock == n { - if span := spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l); span != nil { + if span := spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l); span != nil { return span } } @@ -333,41 +337,41 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag default: // Block was a standalone block. In this case we want to only collapse // the span of the block, independent of any parent span. - return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", "") + return createFoldingRange(ctx, l.createLspRangeFromNode(n, sourceFile), "", "") } case ast.KindModuleBlock: - return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindCaseBlock, ast.KindTypeLiteral, ast.KindObjectBindingPattern: - return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) case ast.KindTupleType: - return spanForNode(n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l) case ast.KindCaseClause, ast.KindDefaultClause: - return spanForNodeArray(n.AsCaseOrDefaultClause().Statements, sourceFile, l) + return spanForNodeArray(ctx, n.AsCaseOrDefaultClause().Statements, sourceFile, l) case ast.KindObjectLiteralExpression: - return spanForNode(n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l) case ast.KindArrayLiteralExpression: - return spanForNode(n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l) case ast.KindJsxElement, ast.KindJsxFragment: - return spanForJSXElement(n, sourceFile, l) + return spanForJSXElement(ctx, n, sourceFile, l) case ast.KindJsxSelfClosingElement, ast.KindJsxOpeningElement: - return spanForJSXAttributes(n, sourceFile, l) + return spanForJSXAttributes(ctx, n, sourceFile, l) case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral: - return spanForTemplateLiteral(n, sourceFile, l) + return spanForTemplateLiteral(ctx, n, sourceFile, l) case ast.KindArrayBindingPattern: - return spanForNode(n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l) + return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l) case ast.KindArrowFunction: - return spanForArrowFunction(n, sourceFile, l) + return spanForArrowFunction(ctx, n, sourceFile, l) case ast.KindCallExpression: - return spanForCallExpression(n, sourceFile, l) + return spanForCallExpression(ctx, n, sourceFile, l) case ast.KindParenthesizedExpression: - return spanForParenthesizedExpression(n, sourceFile, l) + return spanForParenthesizedExpression(ctx, n, sourceFile, l) case ast.KindNamedImports, ast.KindNamedExports, ast.KindImportAttributes: - return spanForImportExportElements(n, sourceFile, l) + return spanForImportExportElements(ctx, n, sourceFile, l) } return nil } -func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForImportExportElements(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { var elements *ast.NodeList switch node.Kind { case ast.KindNamedImports: @@ -385,19 +389,19 @@ func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l * if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } - return rangeBetweenTokens(openToken, closeToken, sourceFile, false /*useFullStart*/, l) + return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, false /*useFullStart*/, l) } -func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForParenthesizedExpression(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/) if printer.PositionsAreOnSameLine(start, node.End(), sourceFile) { return nil } textRange := l.createLspRangeFromBounds(start, node.End(), sourceFile) - return createFoldingRange(textRange, "", "") + return createFoldingRange(ctx, textRange, "", "") } -func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForCallExpression(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { if node.AsCallExpression().Arguments == nil || len(node.AsCallExpression().Arguments.Nodes) == 0 { return nil } @@ -407,40 +411,40 @@ func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *Langua return nil } - return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l) } -func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForArrowFunction(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { arrowFunctionNode := node.AsArrowFunction() if ast.IsBlock(arrowFunctionNode.Body) || ast.IsParenthesizedExpression(arrowFunctionNode.Body) || printer.PositionsAreOnSameLine(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) { return nil } textRange := l.createLspRangeFromBounds(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) - return createFoldingRange(textRange, "", "") + return createFoldingRange(ctx, textRange, "", "") } -func spanForTemplateLiteral(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForTemplateLiteral(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { if node.Kind == ast.KindNoSubstitutionTemplateLiteral && len(node.Text()) == 0 { return nil } - return createFoldingRangeFromBounds(astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) + return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) } -func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForJSXElement(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { if node.Kind == ast.KindJsxElement { jsxElement := node.AsJsxElement() textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxElement.OpeningElement, sourceFile, false /*includeJSDoc*/), jsxElement.ClosingElement.End(), sourceFile) tagName := scanner.GetTextOfNode(jsxElement.OpeningElement.TagName()) bannerText := "<" + tagName + ">..." - return createFoldingRange(textRange, "", bannerText) + return createFoldingRange(ctx, textRange, "", bannerText) } // JsxFragment jsxFragment := node.AsJsxFragment() textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile) - return createFoldingRange(textRange, "", "<>...") + return createFoldingRange(ctx, textRange, "", "<>...") } -func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForJSXAttributes(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { var attributes *ast.JsxAttributesNode if node.Kind == ast.KindJsxSelfClosingElement { attributes = node.AsJsxSelfClosingElement().Attributes @@ -450,17 +454,17 @@ func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *Languag if len(attributes.Properties()) == 0 { return nil } - return createFoldingRangeFromBounds(astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) + return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) } -func spanForNodeArray(statements *ast.NodeList, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForNodeArray(ctx context.Context, statements *ast.NodeList, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { if statements != nil && len(statements.Nodes) != 0 { - return createFoldingRange(l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "") + return createFoldingRange(ctx, l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "") } return nil } -func spanForNode(node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func spanForNode(ctx context.Context, node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { closeBrace := ast.KindCloseBraceToken if open != ast.KindOpenBraceToken { closeBrace = ast.KindCloseBracketToken @@ -468,49 +472,52 @@ func spanForNode(node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *a openToken := astnav.FindChildOfKind(node, open, sourceFile) closeToken := astnav.FindChildOfKind(node, closeBrace, sourceFile) if openToken != nil && closeToken != nil { - return rangeBetweenTokens(openToken, closeToken, sourceFile, useFullStart, l) + return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, useFullStart, l) } return nil } -func rangeBetweenTokens(openToken *ast.Node, closeToken *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange { +func rangeBetweenTokens(ctx context.Context, openToken *ast.Node, closeToken *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange { var textRange *lsproto.Range if useFullStart { textRange = l.createLspRangeFromBounds(openToken.Pos(), closeToken.End(), sourceFile) } else { textRange = l.createLspRangeFromBounds(astnav.GetStartOfNode(openToken, sourceFile, false /*includeJSDoc*/), closeToken.End(), sourceFile) } - return createFoldingRange(textRange, "", "") + return createFoldingRange(ctx, textRange, "", "") } -func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange { - if collapsedText == "" { - defaultText := "..." - collapsedText = defaultText - } +func supportsCollapsedText(ctx context.Context) bool { + return lsproto.GetClientCapabilities(ctx).TextDocument.FoldingRange.FoldingRange.CollapsedText +} + +func createFoldingRange(ctx context.Context, textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange { var kind *lsproto.FoldingRangeKind if foldingRangeKind != "" { kind = &foldingRangeKind } - return &lsproto.FoldingRange{ + result := &lsproto.FoldingRange{ StartLine: textRange.Start.Line, StartCharacter: &textRange.Start.Character, EndLine: textRange.End.Line, EndCharacter: &textRange.End.Character, Kind: kind, - CollapsedText: &collapsedText, } + if collapsedText != "" && supportsCollapsedText(ctx) { + result.CollapsedText = &collapsedText + } + return result } -func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { - return createFoldingRange(l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, "") +func createFoldingRangeFromBounds(ctx context.Context, pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + return createFoldingRange(ctx, l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, "") } -func functionSpan(node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func functionSpan(ctx context.Context, node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { openToken := tryGetFunctionOpenToken(node, body, sourceFile) closeToken := astnav.FindChildOfKind(body, ast.KindCloseBraceToken, sourceFile) if openToken != nil && closeToken != nil { - return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l) } return nil } From dfe11d92e287d49f5e02ab19f36fb65ddd335040 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:30:06 -0800 Subject: [PATCH 2/2] oops --- internal/fourslash/fourslash.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 568972cb3b..ab9ddec985 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -568,6 +568,9 @@ func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lspr if capabilitiesWithDefaults.TextDocument.DocumentSymbol == nil { capabilitiesWithDefaults.TextDocument.DocumentSymbol = defaultDocumentSymbolCapabilities } + if capabilitiesWithDefaults.TextDocument.FoldingRange == nil { + capabilitiesWithDefaults.TextDocument.FoldingRange = defaultFoldingRangeCapabilities + } return &capabilitiesWithDefaults }