569 lines
23 KiB
Go
569 lines
23 KiB
Go
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(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
|
|
}
|
|
return cmp.Compare(*a.StartCharacter, *b.StartCharacter)
|
|
})
|
|
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
|
|
|
|
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(
|
|
ctx,
|
|
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(ctx context.Context, 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
|
|
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, region)
|
|
} 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 n.Flags&ast.NodeFlagsReparsed != 0 || depthRemaining == 0 || 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(ctx, 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(ctx, 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(ctx, 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 ®ionDelimiterResult{
|
|
isStart: isStart,
|
|
name: strings.TrimSpace(lineText),
|
|
}
|
|
}
|
|
|
|
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(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(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(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
|
|
} else if tryStatement.FinallyBlock == n {
|
|
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(ctx, l.createLspRangeFromNode(n, sourceFile), "", "")
|
|
}
|
|
case ast.KindModuleBlock:
|
|
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(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
|
|
case ast.KindTupleType:
|
|
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l)
|
|
case ast.KindCaseClause, ast.KindDefaultClause:
|
|
return spanForNodeArray(ctx, n.AsCaseOrDefaultClause().Statements, sourceFile, l)
|
|
case ast.KindObjectLiteralExpression:
|
|
return spanForNode(ctx, n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l)
|
|
case ast.KindArrayLiteralExpression:
|
|
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l)
|
|
case ast.KindJsxElement, ast.KindJsxFragment:
|
|
return spanForJSXElement(ctx, n, sourceFile, l)
|
|
case ast.KindJsxSelfClosingElement, ast.KindJsxOpeningElement:
|
|
return spanForJSXAttributes(ctx, n, sourceFile, l)
|
|
case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral:
|
|
return spanForTemplateLiteral(ctx, n, sourceFile, l)
|
|
case ast.KindArrayBindingPattern:
|
|
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l)
|
|
case ast.KindArrowFunction:
|
|
return spanForArrowFunction(ctx, n, sourceFile, l)
|
|
case ast.KindCallExpression:
|
|
return spanForCallExpression(ctx, n, sourceFile, l)
|
|
case ast.KindParenthesizedExpression:
|
|
return spanForParenthesizedExpression(ctx, n, sourceFile, l)
|
|
case ast.KindNamedImports, ast.KindNamedExports, ast.KindImportAttributes:
|
|
return spanForImportExportElements(ctx, n, sourceFile, l)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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:
|
|
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(ctx, openToken, closeToken, sourceFile, false /*useFullStart*/, l)
|
|
}
|
|
|
|
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(ctx, textRange, "", "")
|
|
}
|
|
|
|
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
|
|
}
|
|
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(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l)
|
|
}
|
|
|
|
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(ctx, textRange, "", "")
|
|
}
|
|
|
|
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(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l)
|
|
}
|
|
|
|
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(ctx, textRange, "", bannerText)
|
|
}
|
|
// JsxFragment
|
|
jsxFragment := node.AsJsxFragment()
|
|
textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile)
|
|
return createFoldingRange(ctx, textRange, "", "<>...</>")
|
|
}
|
|
|
|
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
|
|
} else {
|
|
attributes = node.AsJsxOpeningElement().Attributes
|
|
}
|
|
if len(attributes.Properties()) == 0 {
|
|
return nil
|
|
}
|
|
return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l)
|
|
}
|
|
|
|
func spanForNodeArray(ctx context.Context, statements *ast.NodeList, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
|
if statements != nil && len(statements.Nodes) != 0 {
|
|
return createFoldingRange(ctx, l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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(ctx, openToken, closeToken, sourceFile, useFullStart, l)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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(ctx, textRange, "", "")
|
|
}
|
|
|
|
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
|
|
}
|
|
result := &lsproto.FoldingRange{
|
|
StartLine: textRange.Start.Line,
|
|
StartCharacter: &textRange.Start.Character,
|
|
EndLine: textRange.End.Line,
|
|
EndCharacter: &textRange.End.Character,
|
|
Kind: kind,
|
|
}
|
|
if collapsedText != "" && supportsCollapsedText(ctx) {
|
|
result.CollapsedText = &collapsedText
|
|
}
|
|
return result
|
|
}
|
|
|
|
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(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(ctx, 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)
|
|
}
|