internal/ls/folding.go (479 lines of code) (raw):

package ls import ( "cmp" "context" "slices" "strings" "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" ) 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)...) slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int { if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 { return c } return cmp.Compare(*a.StartCharacter, *b.StartCharacter) }) return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil } func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { depthRemaining := 40 current := 0 statements := sourceFile.Statements n := len(statements.Nodes) foldingRange := make([]*lsproto.FoldingRange, 0, 40) for current < n { for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) { foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...) current++ } if current == n { break } firstImport := current for current < n && ast.IsAnyImportSyntax(statements.Nodes[current]) { foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...) current++ } lastImport := current - 1 if lastImport != firstImport { foldingRangeKind := lsproto.FoldingRangeKindImports foldingRange = append(foldingRange, createFoldingRangeFromBounds( astnav.GetStartOfNode(astnav.FindChildOfKind(statements.Nodes[firstImport], ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/), statements.Nodes[lastImport].End(), foldingRangeKind, sourceFile, l)) } } // Visit the EOF Token so that comments which aren't attached to statements are included. foldingRange = append(foldingRange, visitNode(ctx, sourceFile.EndOfFileToken, depthRemaining, sourceFile, l)...) return foldingRange } func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { regions := make([]*lsproto.FoldingRange, 0, 40) out := make([]*lsproto.FoldingRange, 0, 40) lineStarts := scanner.GetECMALineStarts(sourceFile) for _, currentLineStart := range lineStarts { lineEnd := getLineEndOfPosition(sourceFile, int(currentLineStart)) lineText := sourceFile.Text()[currentLineStart:lineEnd] result := parseRegionDelimiter(lineText) if result == nil || isInComment(sourceFile, int(currentLineStart), astnav.GetTokenAtPosition(sourceFile, int(currentLineStart))) != nil { continue } 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 } // 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, }) } else { if len(regions) > 0 { region := regions[len(regions)-1] regions = regions[:len(regions)-1] endingPosition := l.createLspPosition(lineEnd, sourceFile) region.EndLine = endingPosition.Line region.EndCharacter = &endingPosition.Character out = append(out, region) } } } return out } func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { if depthRemaining == 0 { return nil } if ctx.Err() != nil { return nil } foldingRange := make([]*lsproto.FoldingRange, 0, 40) if (!ast.IsBinaryExpression(n) && ast.IsDeclaration(n)) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n, sourceFile, l)...) } if ast.IsFunctionLike(n) && n.Parent != nil && ast.IsBinaryExpression(n.Parent) && n.Parent.AsBinaryExpression().Left != nil && ast.IsPropertyAccessExpression(n.Parent.AsBinaryExpression().Left) { foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n.Parent.AsBinaryExpression().Left, sourceFile, l)...) } if ast.IsBlock(n) { statements := n.AsBlock().Statements if statements != nil { foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...) } } if ast.IsModuleBlock(n) { statements := n.AsModuleBlock().Statements if statements != nil { foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...) } } if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) { var members *ast.NodeList if ast.IsClassDeclaration(n) { members = n.AsClassDeclaration().Members } else if ast.IsClassExpression(n) { members = n.AsClassExpression().Members } else { members = n.AsInterfaceDeclaration().Members } if members != nil { foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, members.End(), sourceFile, l)...) } } span := getOutliningSpanForNode(n, sourceFile, l) if span != nil { foldingRange = append(foldingRange, span) } depthRemaining-- if ast.IsCallExpression(n) { depthRemaining++ expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l) if expressionNodes != nil { foldingRange = append(foldingRange, expressionNodes...) } depthRemaining-- for _, arg := range n.Arguments() { if arg != nil { foldingRange = append(foldingRange, visitNode(ctx, arg, depthRemaining, sourceFile, l)...) } } typeArguments := n.TypeArguments() for _, typeArg := range typeArguments { if typeArg != nil { foldingRange = append(foldingRange, visitNode(ctx, typeArg, depthRemaining, sourceFile, l)...) } } } else if ast.IsIfStatement(n) && n.AsIfStatement().ElseStatement != nil && ast.IsIfStatement(n.AsIfStatement().ElseStatement) { // Consider an 'else if' to be on the same depth as the 'if'. ifStatement := n.AsIfStatement() expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l) if expressionNodes != nil { foldingRange = append(foldingRange, expressionNodes...) } thenNode := visitNode(ctx, ifStatement.ThenStatement, depthRemaining, sourceFile, l) if thenNode != nil { foldingRange = append(foldingRange, thenNode...) } depthRemaining++ elseNode := visitNode(ctx, ifStatement.ElseStatement, depthRemaining, sourceFile, l) if elseNode != nil { foldingRange = append(foldingRange, elseNode...) } depthRemaining-- } else { visit := func(node *ast.Node) bool { childNode := visitNode(ctx, node, depthRemaining, sourceFile, l) if childNode != nil { foldingRange = append(foldingRange, childNode...) } return false } n.ForEachChild(visit) } depthRemaining++ return foldingRange } func addOutliningForLeadingCommentsForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { if ast.IsJsxText(n) { return nil } return addOutliningForLeadingCommentsForPos(ctx, n.Pos(), sourceFile, l) } func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { p := &printer.EmitContext{} foldingRange := make([]*lsproto.FoldingRange, 0, 40) firstSingleLineCommentStart := -1 lastSingleLineCommentEnd := -1 singleLineCommentCount := 0 foldingRangeKindComment := lsproto.FoldingRangeKindComment 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 nil } sourceText := sourceFile.Text() for comment := range scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceText, pos) { commentPos := comment.Pos() commentEnd := comment.End() if ctx.Err() != nil { return nil } switch comment.Kind { case ast.KindSingleLineCommentTrivia: // never fold region delimiters into single-line comment regions commentText := sourceText[commentPos:commentEnd] if parseRegionDelimiter(commentText) != nil { comments := combineAndAddMultipleSingleLineComments() if comments != nil { foldingRange = append(foldingRange, comments) } singleLineCommentCount = 0 break } // For single line comments, combine consecutive ones (2 or more) into // a single span from the start of the first till the end of the last if singleLineCommentCount == 0 { firstSingleLineCommentStart = commentPos } lastSingleLineCommentEnd = commentEnd singleLineCommentCount++ break case ast.KindMultiLineCommentTrivia: comments := combineAndAddMultipleSingleLineComments() if comments != nil { foldingRange = append(foldingRange, comments) } foldingRange = append(foldingRange, createFoldingRangeFromBounds(commentPos, commentEnd, foldingRangeKindComment, sourceFile, l)) singleLineCommentCount = 0 break default: debug.AssertNever(comment.Kind) } } addedComments := combineAndAddMultipleSingleLineComments() if addedComments != nil { foldingRange = append(foldingRange, addedComments) } return foldingRange } type regionDelimiterResult struct { isStart bool name string } func parseRegionDelimiter(lineText string) *regionDelimiterResult { // We trim the leading whitespace and // without the regex since the // multiple potential whitespace matches can make for some gnarly backtracking behavior lineText = strings.TrimLeftFunc(lineText, unicode.IsSpace) if !strings.HasPrefix(lineText, "//") { return nil } lineText = strings.TrimSpace(lineText[2:]) lineText = strings.TrimSuffix(lineText, "\r") if !strings.HasPrefix(lineText, "#") { return nil } lineText = lineText[1:] isStart := true if strings.HasPrefix(lineText, "end") { isStart = false lineText = lineText[3:] } if !strings.HasPrefix(lineText, "region") { return nil } lineText = lineText[6:] return &regionDelimiterResult{ isStart: isStart, name: strings.TrimSpace(lineText), } } func getOutliningSpanForNode(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) } // 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) 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) } else if tryStatement.FinallyBlock == n { if span := spanForNode(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), "", "") } case ast.KindModuleBlock: return spanForNode(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) case ast.KindTupleType: return spanForNode(n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l) case ast.KindCaseClause, ast.KindDefaultClause: return spanForNodeArray(n.AsCaseOrDefaultClause().Statements, sourceFile, l) case ast.KindObjectLiteralExpression: return spanForNode(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) case ast.KindJsxElement, ast.KindJsxFragment: return spanForJSXElement(n, sourceFile, l) case ast.KindJsxSelfClosingElement, ast.KindJsxOpeningElement: return spanForJSXAttributes(n, sourceFile, l) case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral: return spanForTemplateLiteral(n, sourceFile, l) case ast.KindArrayBindingPattern: return spanForNode(n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l) case ast.KindArrowFunction: return spanForArrowFunction(n, sourceFile, l) case ast.KindCallExpression: return spanForCallExpression(n, sourceFile, l) case ast.KindParenthesizedExpression: return spanForParenthesizedExpression(n, sourceFile, l) case ast.KindNamedImports, ast.KindNamedExports, ast.KindImportAttributes: return spanForImportExportElements(n, sourceFile, l) } return nil } func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { var elements *ast.NodeList switch node.Kind { case ast.KindNamedImports: elements = node.AsNamedImports().Elements case ast.KindNamedExports: elements = node.AsNamedExports().Elements case ast.KindImportAttributes: elements = node.AsImportAttributes().Attributes } if elements == nil || len(elements.Nodes) == 0 { return nil } openToken := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, sourceFile) closeToken := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, sourceFile) if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } return rangeBetweenTokens(openToken, closeToken, sourceFile, false /*useFullStart*/, l) } func spanForParenthesizedExpression(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, "", "") } func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { if node.AsCallExpression().Arguments == nil || len(node.AsCallExpression().Arguments.Nodes) == 0 { return nil } openToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile) closeToken := astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile) if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) } func spanForArrowFunction(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, "", "") } func spanForTemplateLiteral(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) } func spanForJSXElement(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 := jsxElement.OpeningElement.TagName().Text() bannerText := "<" + tagName + ">...</" + tagName + ">" return createFoldingRange(textRange, "", bannerText) } // JsxFragment jsxFragment := node.AsJsxFragment() textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile) return createFoldingRange(textRange, "", "<>...</>") } func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { var attributes *ast.JsxAttributesNode if node.Kind == ast.KindJsxSelfClosingElement { attributes = node.AsJsxSelfClosingElement().Attributes } else { attributes = node.AsJsxOpeningElement().Attributes } if len(attributes.Properties()) == 0 { return nil } return createFoldingRangeFromBounds(astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) } func spanForNodeArray(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 nil } func spanForNode(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 nil } func rangeBetweenTokens(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, "", "") } func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange { if collapsedText == "" { defaultText := "..." collapsedText = defaultText } var kind *lsproto.FoldingRangeKind if foldingRangeKind != "" { kind = &foldingRangeKind } return &lsproto.FoldingRange{ StartLine: textRange.Start.Line, StartCharacter: &textRange.Start.Character, EndLine: textRange.End.Line, EndCharacter: &textRange.End.Character, Kind: kind, CollapsedText: &collapsedText, } } 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 functionSpan(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 nil } func tryGetFunctionOpenToken(node *ast.SignatureDeclaration, body *ast.Node, sourceFile *ast.SourceFile) *ast.Node { if isNodeArrayMultiLine(node.Parameters(), sourceFile) { openParenToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile) if openParenToken != nil { return openParenToken } } return astnav.FindChildOfKind(body, ast.KindOpenBraceToken, sourceFile) } func isNodeArrayMultiLine(list []*ast.Node, sourceFile *ast.SourceFile) bool { if len(list) == 0 { return false } return !printer.PositionsAreOnSameLine(list[0].Pos(), list[len(list)-1].End(), sourceFile) }