Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 additions & 0 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
139 changes: 73 additions & 66 deletions internal/ls/folding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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)
Expand All @@ -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]
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -307,67 +311,67 @@ 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
}
}
fallthrough
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:
Expand All @@ -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
}
Expand All @@ -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 + ">...</" + 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
Expand All @@ -450,67 +454,70 @@ 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
}
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
}
Expand Down