From f1616c36b6df7410565944cf57e73337e48988aa Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 23 Jun 2025 14:04:49 -0700 Subject: [PATCH 01/10] porting folding --- internal/ls/folding.go | 502 ++++++++++++++++++++++++++++++++++ internal/lsp/server.go | 20 ++ internal/printer/printer.go | 2 +- internal/printer/utilities.go | 10 +- internal/scanner/scanner.go | 21 ++ 5 files changed, 549 insertions(+), 6 deletions(-) create mode 100644 internal/ls/folding.go diff --git a/internal/ls/folding.go b/internal/ls/folding.go new file mode 100644 index 0000000000..bd995c9018 --- /dev/null +++ b/internal/ls/folding.go @@ -0,0 +1,502 @@ +package ls + +import ( + "context" + "regexp" + "sort" + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "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.FoldingRange { + _, sourceFile := l.getProgramAndFile(documentURI) + res := l.addNodeOutliningSpans(sourceFile) + res = append(res, l.addRegionOutliningSpans(sourceFile)...) + sort.Slice(res, func(i, j int) bool { + if res[i] == nil && res[j] == nil { + return false + } + if res[i] == nil { + return false + } + if res[j] == nil { + return true + } + return res[i].StartLine < res[j].StartLine + }) + return res +} + +func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { + depthRemaining := 40 + current := 0 + // Includes the EOF Token so that comments which aren't attached to statements are included + statements := sourceFile.Statements //!!! sourceFile.endOfFileToken + n := len(statements.Nodes) + foldingRange := make([]*lsproto.FoldingRange, n) + for current < n { + for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) { + foldingRange = append(foldingRange, visitNode(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(statements.Nodes[current], depthRemaining, sourceFile, l)...) + current++ + } + lastImport := current - 1 + if lastImport != firstImport { + foldingRangeKind := lsproto.FoldingRangeKindImports + foldingRange = append(foldingRange, createFoldingRangeFromBounds( + astnav.GetStartOfNode(findChildOfKind(statements.Nodes[firstImport], + ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/), + statements.Nodes[lastImport].End(), + &foldingRangeKind, + sourceFile, + l)) + } + } + return foldingRange +} + +func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { + regions := []*lsproto.FoldingRange{} + lineStarts := scanner.GetLineStarts(sourceFile) + for _, currentLineStart := range lineStarts { + lineEnd := scanner.GetLineEndOfPosition(sourceFile, int(currentLineStart)) + lineText := sourceFile.Text()[currentLineStart:lineEnd] + result := parseRegionDelimiter(lineText) + if result == nil || isInComment(sourceFile, int(currentLineStart), nil) != nil { + continue + } + if result.isStart { + span := l.createLspRangeFromBounds(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//"), lineEnd, sourceFile) + foldingRangeKindRegion := lsproto.FoldingRangeKindRegion + collapsedTest := "#region" + if result.name != "" { + collapsedTest = result.name + } + regions = append(regions, createFoldingRange(span, &foldingRangeKindRegion, nil, collapsedTest)) + } else { + // if len(regions) > 0 { + // region := regions[len(regions)-1] + // regions = regions[:len(regions)-1] + // if region != nil { + // region.StartLine = uint32(lineEnd - int(region.StartLine)) // !!! test + // region.EndLine = lineEnd - region.HintSpan.Start + // out = append(out, region) + // } + // } + } + } + return regions +} + +func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { + if depthRemaining == 0 { + return nil + } + // cancellationToken.throwIfCancellationRequested(); + var foldingRange []*lsproto.FoldingRange + if ast.IsDeclaration(n) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(n, sourceFile)...) + } + 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(n.Parent.AsBinaryExpression().Left, sourceFile)...) + } + if ast.IsBlock(n) || ast.IsModuleBlock(n) { + statements := n.Statements() + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(statements[len(statements)-1].End(), sourceFile)...) + } + if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) { + members := n.Members() + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(members[len(members)-1].End(), sourceFile)...) + } + + span := getOutliningSpanForNode(n, sourceFile, l) + if span != nil { + foldingRange = append(foldingRange, span) + } + + depthRemaining-- + if ast.IsCallExpression(n) { + depthRemaining++ + expressionNodes := visitNode(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(arg, depthRemaining, sourceFile, l)...) + } + } + typeArguments := n.TypeArguments() + for _, typeArg := range typeArguments { + if typeArg != nil { + foldingRange = append(foldingRange, visitNode(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(n.Expression(), depthRemaining, sourceFile, l) + if expressionNodes != nil { + foldingRange = append(foldingRange, expressionNodes...) + } + thenNode := visitNode(ifStatement.ThenStatement, depthRemaining, sourceFile, l) + if thenNode != nil { + foldingRange = append(foldingRange, thenNode...) + } + depthRemaining++ + elseNode := visitNode(ifStatement.ElseStatement, depthRemaining, sourceFile, l) + if elseNode != nil { + foldingRange = append(foldingRange, elseNode...) + } + depthRemaining-- + } else { + var visit func(node *ast.Node) bool + visit = func(node *ast.Node) bool { + childNode := visitNode(node, depthRemaining, sourceFile, l) + if childNode != nil { + foldingRange = append(foldingRange, childNode...) + } + return false + } + n.ForEachChild(visit) + } + depthRemaining++ + return foldingRange +} + +func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { + if ast.IsJsxText(n) { + return nil + } + return addOutliningForLeadingCommentsForPos(n.Pos(), sourceFile) +} + +func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { + c := &printer.EmitContext{} + comments := scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(c).NodeFactory, sourceFile.Text(), pos) + if comments == nil { + return nil + } + + foldingRange := []*lsproto.FoldingRange{} + 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, nil) + } + return nil + } + + sourceText := sourceFile.Text() + for comment := range comments { + pos := comment.Pos() + end := comment.End() + // cancellationToken.throwIfCancellationRequested(); + switch comment.Kind { + case ast.KindSingleLineCommentTrivia: + // never fold region delimiters into single-line comment regions + commentText := sourceText[pos:end] + 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 = pos + } + lastSingleLineCommentEnd = end + singleLineCommentCount++ + break + case ast.KindMultiLineCommentTrivia: + comments := combineAndAddMultipleSingleLineComments() + if comments != nil { + foldingRange = append(foldingRange, comments) + } + foldingRange = append(foldingRange, createFoldingRangeFromBounds(pos, end, &foldingRangeKindComment, sourceFile, nil)) + singleLineCommentCount = 0 + break + default: + // Debug.assertNever(kind); + } + } + return foldingRange +} + +var regionDelimiterRegExp = regexp.MustCompile(`^#(end)?region(.*)\r?$`) + +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.TrimLeft(lineText, " \t") + if !strings.HasPrefix(lineText, "//") { + return nil + } + lineText = strings.TrimLeft(lineText[2:], " \t") + result := regionDelimiterRegExp.FindStringSubmatch(lineText) + if result != nil { + return ®ionDelimiterResult{ + isStart: result[1] == "", + name: strings.TrimSpace(result[2]), + } + } + return nil +} + +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, n.Parent, 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, n.Parent, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + } else if tryStatement.FinallyBlock == n { + node := findChildOfKind(n.Parent, ast.KindFinallyKeyword, sourceFile) + if node != nil { + return spanForNode(n, node, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + } + } + 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), nil, nil, "") + } + case ast.KindModuleBlock: + return spanForNode(n, n.Parent, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindCaseBlock, ast.KindTypeLiteral, ast.KindObjectBindingPattern: + return spanForNode(n, n, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + case ast.KindTupleType: + return spanForNode(n, 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, n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart */, sourceFile, l) + case ast.KindArrayLiteralExpression: + return spanForNode(n, 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, 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 { + if len(node.Elements()) == 0 { + return nil + } + openToken := findChildOfKind(node, ast.KindOpenBraceToken, sourceFile) + closeToken := findChildOfKind(node, ast.KindCloseBraceToken, sourceFile) + if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.End(), sourceFile) { + return nil + } + return spanBetweenTokens(openToken, closeToken, node, 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, nil, l.createLspRangeFromNode(node, sourceFile), "") +} + +func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + if node.AsCallExpression().Arguments == nil { + return nil + } + openToken := findChildOfKind(node, ast.KindOpenParenToken, sourceFile) + closeToken := findChildOfKind(node, ast.KindCloseParenToken, sourceFile) + if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.End(), sourceFile) { + return nil + } + + return spanBetweenTokens(openToken, closeToken, node, 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, nil, l.createLspRangeFromNode(node, sourceFile), "") +} + +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(), nil, sourceFile, l) +} + +func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + var openingElement *ast.Node + if node.Kind == ast.KindJsxElement { + openingElement = node.AsJsxElement().OpeningElement + } else { + openingElement = node.AsJsxFragment().OpeningFragment + } + textRange := l.createLspRangeFromBounds(openingElement.Pos(), openingElement.End(), sourceFile) + tagName := openingElement.TagName().Text() + var bannerText strings.Builder + if node.Kind == ast.KindJsxElement { + bannerText.WriteString("<") + bannerText.WriteString(tagName) + bannerText.WriteString(">...") + } else { + bannerText.WriteString("<>...") + } + + return createFoldingRange(textRange, nil, nil, bannerText.String()) +} + +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(), nil, 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), nil, nil, "") + } + return nil +} + +func spanForNode(node *ast.Node, hintSpanNode *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + close := ast.KindCloseBraceToken + if open != ast.KindOpenBraceToken { + close = ast.KindCloseBracketToken + } + openToken := findChildOfKind(node, open, sourceFile) + closeToken := findChildOfKind(node, close, sourceFile) + if openToken != nil && closeToken != nil { + return spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, useFullStart, l) + } + return nil +} + +func spanBetweenTokens(openToken *ast.Node, closeToken *ast.Node, hintSpanNode *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, nil, l.createLspRangeFromNode(hintSpanNode, sourceFile), "") +} + +func createFoldingRange(textRange *lsproto.Range, foldingRangeKind *lsproto.FoldingRangeKind, hintRange *lsproto.Range, collapsedText string) *lsproto.FoldingRange { + if hintRange == nil { + hintRange = textRange + } + if collapsedText == "" { + defaultText := "..." + collapsedText = defaultText + } + return &lsproto.FoldingRange{ + StartLine: textRange.Start.Line, + EndLine: textRange.End.Line, // !!! needs to be adjusted for in vscode repo + Kind: foldingRangeKind, + CollapsedText: &collapsedText, + } +} + +// func adjustFoldingRange(textRange lsproto.Range, sourceFile *ast.SourceFile) { +// if textRange.End.Character > 0 { +// foldEndCharacter := sourceFile.Text()[textRange.End.Line : textRange.End] +// } +// } + +func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind *lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + return createFoldingRange(l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, nil, "") +} + +func functionSpan(node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + openToken := tryGetFunctionOpenToken(node, body, sourceFile) + closeToken := findChildOfKind(body, ast.KindCloseBraceToken, sourceFile) + if openToken != nil && closeToken != nil { + return spanBetweenTokens(openToken, closeToken, node, 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 := findChildOfKind(node, ast.KindOpenParenToken, sourceFile) + if openParenToken != nil { + return openParenToken + } + } + return 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) +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 25c42458f8..b646b5cbf0 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -412,6 +412,9 @@ func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.R return s.handleCompletion(ctx, req) case *lsproto.SignatureHelpParams: return s.handleSignatureHelp(ctx, req) + case *lsproto.FoldingRangeParams: + return s.handleFoldingRange(ctx, req) + default: switch req.Method { case lsproto.MethodShutdown: @@ -476,6 +479,13 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) { SignatureHelpProvider: &lsproto.SignatureHelpOptions{ TriggerCharacters: &[]string{"(", ","}, }, + FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{ + FoldingRangeOptions: &lsproto.FoldingRangeOptions{ + WorkDoneProgressOptions: lsproto.WorkDoneProgressOptions{ + WorkDoneProgress: ptrTo(true), + }, + }, + }, }, }) } @@ -570,6 +580,16 @@ func (s *Server) handleSignatureHelp(ctx context.Context, req *lsproto.RequestMe return nil } +func (s *Server) handleFoldingRange(ctx context.Context, req *lsproto.RequestMessage) error { + params := req.Params.(*lsproto.FoldingRangeParams) + project := s.projectService.EnsureDefaultProjectForURI(params.TextDocument.Uri) + languageService, done := project.GetLanguageServiceForRequest(ctx) + defer done() + foldingRanges := languageService.ProvideFoldingRange(ctx, params.TextDocument.Uri) + s.sendResult(req.ID, foldingRanges) + return nil +} + func (s *Server) handleDefinition(ctx context.Context, req *lsproto.RequestMessage) error { params := req.Params.(*lsproto.DefinitionParams) project := s.projectService.EnsureDefaultProjectForURI(params.TextDocument.Uri) diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 4f9c719793..bcf864f294 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -5028,7 +5028,7 @@ func (p *Printer) emitCommentsBeforeToken(token ast.Kind, pos int, contextNode * if contextNode.Pos() != startPos { indentLeading := flags&tefIndentLeadingComments != 0 - needsIndent := indentLeading && p.currentSourceFile != nil && !positionsAreOnSameLine(startPos, pos, p.currentSourceFile) + needsIndent := indentLeading && p.currentSourceFile != nil && !PositionsAreOnSameLine(startPos, pos, p.currentSourceFile) p.increaseIndentIf(needsIndent) p.emitLeadingComments(startPos, false /*elided*/) p.decreaseIndentIf(needsIndent) diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 695e27aeb1..3c569637d3 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -335,7 +335,7 @@ func rangeIsOnSingleLine(r core.TextRange, sourceFile *ast.SourceFile) bool { } func rangeStartPositionsAreOnSameLine(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine( + return PositionsAreOnSameLine( getStartPositionOfRange(range1, sourceFile, false /*includeComments*/), getStartPositionOfRange(range2, sourceFile, false /*includeComments*/), sourceFile, @@ -343,15 +343,15 @@ func rangeStartPositionsAreOnSameLine(range1 core.TextRange, range2 core.TextRan } func rangeEndPositionsAreOnSameLine(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine(range1.End(), range2.End(), sourceFile) + return PositionsAreOnSameLine(range1.End(), range2.End(), sourceFile) } func rangeStartIsOnSameLineAsRangeEnd(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile, false /*includeComments*/), range2.End(), sourceFile) + return PositionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile, false /*includeComments*/), range2.End(), sourceFile) } func rangeEndIsOnSameLineAsRangeStart(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine(range1.End(), getStartPositionOfRange(range2, sourceFile, false /*includeComments*/), sourceFile) + return PositionsAreOnSameLine(range1.End(), getStartPositionOfRange(range2, sourceFile, false /*includeComments*/), sourceFile) } func getStartPositionOfRange(r core.TextRange, sourceFile *ast.SourceFile, includeComments bool) int { @@ -361,7 +361,7 @@ func getStartPositionOfRange(r core.TextRange, sourceFile *ast.SourceFile, inclu return scanner.SkipTriviaEx(sourceFile.Text(), r.Pos(), &scanner.SkipTriviaOptions{StopAtComments: includeComments}) } -func positionsAreOnSameLine(pos1 int, pos2 int, sourceFile *ast.SourceFile) bool { +func PositionsAreOnSameLine(pos1 int, pos2 int, sourceFile *ast.SourceFile) bool { return getLinesBetweenPositions(sourceFile, pos1, pos2) == 0 } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index d691bc08e1..a99b76f6bb 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2323,6 +2323,27 @@ func GetLineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) (line return } +func GetLineEndOfPosition(sourceFile ast.SourceFileLike, pos int) int { + line, _ := GetLineAndCharacterOfPosition(sourceFile, pos) + lineStarts := GetLineStarts(sourceFile) + + var lastCharPos int + if line+1 >= len(lineStarts) { + lineMap := sourceFile.LineMap() + lastCharPos = int(lineMap[len(lineMap)-1]) + } else { + lastCharPos = int(lineStarts[line+1] - 1) + } + + fullText := sourceFile.Text() + // if the new line is "\r\n", we should return the last non-new-line-character position + if len(fullText) > 0 && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { + return lastCharPos - 1 + } else { + return lastCharPos + } +} + func GetEndLinePosition(sourceFile *ast.SourceFile, line int) int { pos := int(GetLineStarts(sourceFile)[line]) for { From c691b7d78cf1b6d337cd065cf69e85acf73af852 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 30 Jun 2025 10:11:44 -0700 Subject: [PATCH 02/10] adding all tests --- internal/ast/utilities.go | 45 +- internal/ls/folding.go | 183 +++-- internal/ls/folding_test.go | 1445 +++++++++++++++++++++++++++++++++++ internal/ls/utilities.go | 2 +- internal/scanner/scanner.go | 5 +- 5 files changed, 1609 insertions(+), 71 deletions(-) create mode 100644 internal/ls/folding_test.go diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 248db74ab3..fcb0206b25 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -663,6 +663,49 @@ func isDeclarationStatementKind(kind Kind) bool { return false } +func isDeclarationKind(kind Kind) bool { + switch kind { + case KindArrowFunction, + KindBindingElement, + KindClassDeclaration, + KindClassExpression, + KindClassStaticBlockDeclaration, + KindConstructor, + KindEnumDeclaration, + KindEnumMember, + KindExportSpecifier, + KindFunctionDeclaration, + KindFunctionExpression, + KindGetAccessor, + KindImportClause, + KindImportEqualsDeclaration, + KindImportSpecifier, + KindInterfaceDeclaration, + KindJsxAttribute, + KindMethodDeclaration, + KindMethodSignature, + KindModuleDeclaration, + KindNamespaceExportDeclaration, + KindNamespaceExport, + KindNamespaceImport, + KindParameter, + KindPropertyAssignment, + KindPropertyDeclaration, + KindPropertySignature, + KindSetAccessor, + KindShorthandPropertyAssignment, + KindTypeAliasDeclaration, + KindTypeParameter, + KindVariableDeclaration, + KindJSDocTypedefTag, + KindJSDocCallbackTag, + KindJSDocPropertyTag, + KindNamedTupleMember: + return true + } + return false +} + // Determines whether a node is a DeclarationStatement. Ideally this does not use Parent pointers, but it may use them // to rule out a Block node that is part of `try` or `catch` or is the Block-like body of a function. // @@ -1266,7 +1309,7 @@ func IsDeclaration(node *Node) bool { if node.Kind == KindTypeParameter { return node.Parent != nil } - return IsDeclarationNode(node) + return isDeclarationKind(node.Kind) } // True if `name` is the name of a declaration node diff --git a/internal/ls/folding.go b/internal/ls/folding.go index bd995c9018..88788b9bdb 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -18,16 +18,15 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l res := l.addNodeOutliningSpans(sourceFile) res = append(res, l.addRegionOutliningSpans(sourceFile)...) sort.Slice(res, func(i, j int) bool { - if res[i] == nil && res[j] == nil { - return false - } - if res[i] == nil { - return false + if res[i].StartLine != res[j].StartLine { + return res[i].StartLine < res[j].StartLine } - if res[j] == nil { - return true + if res[i].EndLine != res[j].EndLine { + return res[i].EndLine < res[j].EndLine + } else if res[i].StartCharacter != nil && res[j].StartCharacter != nil { + return *res[i].StartCharacter < *res[j].StartCharacter } - return res[i].StartLine < res[j].StartLine + return *res[i].EndCharacter < *res[j].EndCharacter }) return res } @@ -36,9 +35,22 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l depthRemaining := 40 current := 0 // Includes the EOF Token so that comments which aren't attached to statements are included - statements := sourceFile.Statements //!!! sourceFile.endOfFileToken + statements := sourceFile.Statements + var curr *ast.Node + currentTokenEnd := 0 + if statements != nil && statements.Nodes != nil { + curr = statements.Nodes[len(statements.Nodes)-1] + currentTokenEnd = curr.End() + } + scanner := scanner.GetScannerForSourceFile(sourceFile, currentTokenEnd) + eof := scanner.Token() + tokenFullStart := scanner.TokenFullStart() + tokenEnd := scanner.TokenEnd() + k := sourceFile.GetOrCreateToken(eof, tokenFullStart, tokenEnd, curr) + statements.Nodes = append(statements.Nodes, k) + n := len(statements.Nodes) - foldingRange := make([]*lsproto.FoldingRange, n) + var foldingRange []*lsproto.FoldingRange for current < n { for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) { foldingRange = append(foldingRange, visitNode(statements.Nodes[current], depthRemaining, sourceFile, l)...) @@ -59,7 +71,7 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l astnav.GetStartOfNode(findChildOfKind(statements.Nodes[firstImport], ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/), statements.Nodes[lastImport].End(), - &foldingRangeKind, + foldingRangeKind, sourceFile, l)) } @@ -68,7 +80,8 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l } func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { - regions := []*lsproto.FoldingRange{} + var regions []*lsproto.FoldingRange + var out []*lsproto.FoldingRange lineStarts := scanner.GetLineStarts(sourceFile) for _, currentLineStart := range lineStarts { lineEnd := scanner.GetLineEndOfPosition(sourceFile, int(currentLineStart)) @@ -78,26 +91,37 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) [] continue } if result.isStart { - span := l.createLspRangeFromBounds(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//"), lineEnd, sourceFile) + // span := l.createLspRangeFromBounds(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//"), lineEnd, sourceFile) + i := strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//") + span := l.createLspPosition(i+int(currentLineStart), sourceFile) foldingRangeKindRegion := lsproto.FoldingRangeKindRegion collapsedTest := "#region" if result.name != "" { collapsedTest = result.name - } - regions = append(regions, createFoldingRange(span, &foldingRangeKindRegion, nil, collapsedTest)) + } // createFoldingRange(span, foldingRangeKindRegion, nil, collapsedTest) + regions = append(regions, &lsproto.FoldingRange{ + StartLine: span.Line, + StartCharacter: &span.Character, + Kind: &foldingRangeKindRegion, + CollapsedText: &collapsedTest, + }) } else { - // if len(regions) > 0 { - // region := regions[len(regions)-1] - // regions = regions[:len(regions)-1] - // if region != nil { - // region.StartLine = uint32(lineEnd - int(region.StartLine)) // !!! test - // region.EndLine = lineEnd - region.HintSpan.Start - // out = append(out, region) - // } - // } + if len(regions) > 0 { + region := regions[len(regions)-1] + regions = regions[:len(regions)-1] + if region != nil { + if out == nil { + out = []*lsproto.FoldingRange{} + } + endingPosition := l.createLspPosition(lineEnd, sourceFile) + region.EndLine = endingPosition.Line + region.EndCharacter = &endingPosition.Character + out = append(out, region) + } + } } } - return regions + return out } func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { @@ -107,18 +131,35 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L // cancellationToken.throwIfCancellationRequested(); var foldingRange []*lsproto.FoldingRange if ast.IsDeclaration(n) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(n, sourceFile)...) + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(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(n.Parent.AsBinaryExpression().Left, sourceFile)...) + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(n.Parent.AsBinaryExpression().Left, sourceFile, l)...) + } + if ast.IsBlock(n) { + statements := n.AsBlock().Statements + if statements != nil { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(statements.End(), sourceFile, l)...) + } } - if ast.IsBlock(n) || ast.IsModuleBlock(n) { - statements := n.Statements() - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(statements[len(statements)-1].End(), sourceFile)...) + if ast.IsModuleBlock(n) { + statements := n.AsModuleBlock().Statements + if statements != nil { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(statements.End(), sourceFile, l)...) + } } if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) { - members := n.Members() - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(members[len(members)-1].End(), sourceFile)...) + 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(members.End(), sourceFile, l)...) + } } span := getOutliningSpanForNode(n, sourceFile, l) @@ -177,21 +218,18 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L return foldingRange } -func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { +func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { if ast.IsJsxText(n) { return nil } - return addOutliningForLeadingCommentsForPos(n.Pos(), sourceFile) + return addOutliningForLeadingCommentsForPos(n.Pos(), sourceFile, l) } -func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { +func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { c := &printer.EmitContext{} comments := scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(c).NodeFactory, sourceFile.Text(), pos) - if comments == nil { - return nil - } - foldingRange := []*lsproto.FoldingRange{} + var foldingRange []*lsproto.FoldingRange firstSingleLineCommentStart := -1 lastSingleLineCommentEnd := -1 singleLineCommentCount := 0 @@ -202,13 +240,13 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile) [ if singleLineCommentCount > 1 { return createFoldingRangeFromBounds( - firstSingleLineCommentStart, lastSingleLineCommentEnd, &foldingRangeKindComment, sourceFile, nil) + firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l) } return nil } sourceText := sourceFile.Text() - for comment := range comments { + comments(func(comment ast.CommentRange) bool { pos := comment.Pos() end := comment.End() // cancellationToken.throwIfCancellationRequested(); @@ -238,12 +276,17 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile) [ if comments != nil { foldingRange = append(foldingRange, comments) } - foldingRange = append(foldingRange, createFoldingRangeFromBounds(pos, end, &foldingRangeKindComment, sourceFile, nil)) + foldingRange = append(foldingRange, createFoldingRangeFromBounds(pos, end, foldingRangeKindComment, sourceFile, l)) singleLineCommentCount = 0 break default: // Debug.assertNever(kind); } + return true + }) + addedComments := combineAndAddMultipleSingleLineComments() + if addedComments != nil { + foldingRange = append(foldingRange, addedComments) } return foldingRange } @@ -299,7 +342,7 @@ 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), nil, nil, "") + return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", nil, "") } case ast.KindModuleBlock: return spanForNode(n, n.Parent, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) @@ -334,12 +377,20 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag } func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { - if len(node.Elements()) == 0 { + var elements *ast.NodeList + if node.Kind == ast.KindNamedImports { + elements = node.AsNamedImports().Elements + } else if node.Kind == ast.KindNamedExports { + elements = node.AsNamedExports().Elements + } else if node.Kind == ast.KindImportAttributes { + elements = node.AsImportAttributes().Attributes + } + if elements == nil { return nil } openToken := findChildOfKind(node, ast.KindOpenBraceToken, sourceFile) closeToken := findChildOfKind(node, ast.KindCloseBraceToken, sourceFile) - if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.End(), sourceFile) { + if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } return spanBetweenTokens(openToken, closeToken, node, sourceFile, false /*useFullStart*/, l) @@ -351,7 +402,7 @@ func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, return nil } textRange := l.createLspRangeFromBounds(start, node.End(), sourceFile) - return createFoldingRange(textRange, nil, l.createLspRangeFromNode(node, sourceFile), "") + return createFoldingRange(textRange, "", l.createLspRangeFromNode(node, sourceFile), "") } func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -360,7 +411,7 @@ func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *Langua } openToken := findChildOfKind(node, ast.KindOpenParenToken, sourceFile) closeToken := findChildOfKind(node, ast.KindCloseParenToken, sourceFile) - if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.End(), sourceFile) { + if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } @@ -373,14 +424,14 @@ func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *Languag return nil } textRange := l.createLspRangeFromBounds(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) - return createFoldingRange(textRange, nil, l.createLspRangeFromNode(node, sourceFile), "") + return createFoldingRange(textRange, "", l.createLspRangeFromNode(node, sourceFile), "") } 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(), nil, sourceFile, l) + return createFoldingRangeFromBounds(astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) } func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -390,7 +441,7 @@ func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageSe } else { openingElement = node.AsJsxFragment().OpeningFragment } - textRange := l.createLspRangeFromBounds(openingElement.Pos(), openingElement.End(), sourceFile) + textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(openingElement, sourceFile, false /*includeJSDoc*/), openingElement.End(), sourceFile) tagName := openingElement.TagName().Text() var bannerText strings.Builder if node.Kind == ast.KindJsxElement { @@ -403,7 +454,7 @@ func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageSe bannerText.WriteString("<>...") } - return createFoldingRange(textRange, nil, nil, bannerText.String()) + return createFoldingRange(textRange, "", nil, bannerText.String()) } func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -416,12 +467,12 @@ 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(), nil, sourceFile, l) + 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), nil, nil, "") + return createFoldingRange(l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", nil, "") } return nil } @@ -446,10 +497,10 @@ func spanBetweenTokens(openToken *ast.Node, closeToken *ast.Node, hintSpanNode * } else { textRange = l.createLspRangeFromBounds(astnav.GetStartOfNode(openToken, sourceFile, false /*includeJSDoc*/), closeToken.End(), sourceFile) } - return createFoldingRange(textRange, nil, l.createLspRangeFromNode(hintSpanNode, sourceFile), "") + return createFoldingRange(textRange, "", l.createLspRangeFromNode(hintSpanNode, sourceFile), "") } -func createFoldingRange(textRange *lsproto.Range, foldingRangeKind *lsproto.FoldingRangeKind, hintRange *lsproto.Range, collapsedText string) *lsproto.FoldingRange { +func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, hintRange *lsproto.Range, collapsedText string) *lsproto.FoldingRange { if hintRange == nil { hintRange = textRange } @@ -457,21 +508,21 @@ func createFoldingRange(textRange *lsproto.Range, foldingRangeKind *lsproto.Fold defaultText := "..." collapsedText = defaultText } + var kind *lsproto.FoldingRangeKind + if foldingRangeKind != "" { + kind = &foldingRangeKind + } return &lsproto.FoldingRange{ - StartLine: textRange.Start.Line, - EndLine: textRange.End.Line, // !!! needs to be adjusted for in vscode repo - Kind: foldingRangeKind, - CollapsedText: &collapsedText, + StartLine: textRange.Start.Line, + StartCharacter: &textRange.Start.Character, + EndLine: textRange.End.Line, // !!! needs to be adjusted for in vscode repo + EndCharacter: &textRange.End.Character, + Kind: kind, + CollapsedText: &collapsedText, } } -// func adjustFoldingRange(textRange lsproto.Range, sourceFile *ast.SourceFile) { -// if textRange.End.Character > 0 { -// foldEndCharacter := sourceFile.Text()[textRange.End.Line : textRange.End] -// } -// } - -func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind *lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { +func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { return createFoldingRange(l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, nil, "") } diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go new file mode 100644 index 0000000000..e0a529a9aa --- /dev/null +++ b/internal/ls/folding_test.go @@ -0,0 +1,1445 @@ +package ls_test + +import ( + "sort" + "testing" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func runFoldingRangeTest(t *testing.T, input string) { + testData := fourslash.ParseTestData(t, input, "/file1.ts") + markerPositions := testData.Ranges + ctx := projecttestutil.WithRequestID(t.Context()) + service, done := createLanguageService(ctx, testData.Files[0].FileName(), map[string]any{ + testData.Files[0].FileName(): testData.Files[0].Content, + }) + defer done() + + foldingRanges := service.ProvideFoldingRange(ctx, ls.FileNameToDocumentURI("/file1.ts")) + if len(foldingRanges) != len(markerPositions) { + t.Fatalf("Expected %d folding ranges, got %d", len(markerPositions), len(foldingRanges)) + } + sort.Slice(markerPositions, func(i, j int) bool { + if markerPositions[i].LSRange.Start.Line != markerPositions[j].LSRange.Start.Line { + return markerPositions[i].LSRange.Start.Line < markerPositions[j].LSRange.Start.Line + } + if markerPositions[i].LSRange.End.Line != markerPositions[j].LSRange.End.Line { + return markerPositions[i].LSRange.End.Line < markerPositions[j].LSRange.End.Line + } else if markerPositions[i].LSRange.Start.Character != markerPositions[j].LSRange.Start.Character { + return markerPositions[i].LSRange.Start.Character < markerPositions[j].LSRange.Start.Character + } + return markerPositions[i].LSRange.End.Character < markerPositions[j].LSRange.End.Character + }) + for i, marker := range markerPositions { + assert.DeepEqual(t, marker.LSRange.Start.Line, foldingRanges[i].StartLine) + assert.DeepEqual(t, marker.LSRange.End.Line, foldingRanges[i].EndLine) + assert.DeepEqual(t, marker.LSRange.Start.Character, *foldingRanges[i].StartCharacter) + assert.DeepEqual(t, marker.LSRange.End.Character, *foldingRanges[i].EndCharacter) + } +} + +func TestFolding(t *testing.T) { + t.Parallel() + + testCases := []struct { + title string + input string + expectedLocations map[string]*collections.Set[string] + }{ + { + title: "getOutliningSpansForRegionsNoSingleLineFolds", + input: `[|//#region +function foo()[| { + +}|] +[|//these +//should|] +//#endregion not you|] +[|// be +// together|] + +[|//#region bla bla bla + +function bar()[| { }|] + +//#endregion|]`, + }, + { + title: "getOutliningSpansForComments", + input: `[|/* + Block comment at the beginning of the file before module: + line one of the comment + line two of the comment + line three + line four + line five +*/|] +declare module "m"; +[|// Single line comments at the start of the file +// line 2 +// line 3 +// line 4|] +declare module "n";`, + }, + { + title: "getOutliningSpansForRegions", + input: `// region without label + [|// #region + + // #endregion|] + + // region without label with trailing spaces + [|// #region + + // #endregion|] + + // region with label + [|// #region label1 + + // #endregion|] + + // region with extra whitespace in all valid locations + [|// #region label2 label3 + + // #endregion|] + + // No space before directive + [|//#region label4 + + //#endregion|] + + // Nested regions + [|// #region outer + + [|// #region inner + + // #endregion inner|] + + // #endregion outer|] + + // region delimiters not valid when there is preceding text on line + test // #region invalid1 + + test // #endregion`, + }, + { + title: "outliningSpansSwitchCases", + input: `switch (undefined)[| { +case 0:[| + console.log(1) + console.log(2) + break; + console.log(3);|] +case 1:[| + break;|] +case 2:[| + break; + console.log(3);|] +case 3:[| + console.log(4);|] + +case 4: +case 5: +case 6:[| + + + console.log(5);|] + +case 7:[| console.log(6);|] + +case 8:[| [|{ + console.log(8); + break; +}|] +console.log(8);|] + +default:[| + console.log(7); + console.log(8);|] +}|]`, + }, + { + title: "outliningSpansForParenthesizedExpression", + input: `const a = [|( + true + ? true + : false + ? true + : false +)|]; + +const b = ( 1 ); + +const c = [|( + 1 +)|]; + +( 1 ); + +[|( + [|( + [|( + 1 + )|] + )|] +)|]; + +[|( + [|( + ( 1 ) + )|] +)|];`, + }, + { + title: "outliningSpansForInportsAndExports", + input: `import { a1, a2 } from "a"; +; +import { +} from "a"; +; +import [|{ + b1, + b2, +}|] from "b"; +; +import j1 from "./j" assert { type: "json" }; +; +import j2 from "./j" assert { +}; +; +import j3 from "./j" assert [|{ + type: "json" +}|]; +; +[|import { a5, a6 } from "a"; +import [|{ + a7, + a8, +}|] from "a";|] +export { a1, a2 }; +; +export { a3, a4 } from "a"; +; +export { +}; +; +export [|{ + b1, + b2, +}|]; +; +export { +} from "b"; +; +export [|{ + b3, + b4, +}|] from "b"; +;`, + }, + { + title: "outliningSpansForImportAndExportAttributes", + input: `import { a1, a2 } from "a"; +; +import { +} from "a"; +; +import [|{ + b1, + b2, +}|] from "b"; +; +import j1 from "./j" with { type: "json" }; +; +import j2 from "./j" with { +}; +; +import j3 from "./j" with [|{ + type: "json" +}|]; +; +[|import { a5, a6 } from "a"; +import [|{ + a7, + a8, +}|] from "a";|] +export { a1, a2 }; +; +export { a3, a4 } from "a"; +; +export { +}; +; +export [|{ + b1, + b2, +}|]; +; +export { +} from "b"; +; +export [|{ + b3, + b4, +}|] from "b"; +;`, + }, + { + title: "outliningSpansForFunction", + input: `[|( + a: number, + b: number +) => { + return a + b; +}|]; + +(a: number, b: number) =>[| { + return a + b; +}|] + +const f1 = function[| ( + a: number + b: number +) { + return a + b; +}|] + +const f2 = function (a: number, b: number)[| { + return a + b; +}|] + +function f3[| ( + a: number + b: number +) { + return a + b; +}|] + +function f4(a: number, b: number)[| { + return a + b; +}|] + +class Foo[| { + constructor[|( + a: number, + b: number + ) { + this.a = a; + this.b = b; + }|] + + m1[|( + a: number, + b: number + ) { + return a + b; + }|] + + m1(a: number, b: number)[| { + return a + b; + }|] +}|] + +declare function foo(props: any): void; +foo[|( + a =>[| { + + }|] +)|] + +foo[|( + (a) =>[| { + + }|] +)|] + +foo[|( + (a, b, c) =>[| { + + }|] +)|] + +foo[|([| + (a, + b, + c) => { + + }|] +)|]`, + }, + { + title: "outliningSpansForArrowFunctionBody", + input: `() => 42; +() => ( 42 ); +() =>[| { + 42 +}|]; +() => [|( + 42 +)|]; +() =>[| "foo" + + "bar" + + "baz"|];`, + }, + { + title: "outliningSpansForArguments", + input: `console.log(123, 456)l; +console.log( +); +console.log[|( + 123, 456 +)|]; +console.log[|( + 123, + 456 +)|]; +() =>[| console.log[|( + 123, + 456 +)|]|];`, + }, + { + title: "outliningForNonCompleteInterfaceDeclaration", + input: `interface I`, + }, + { + title: "incrementalParsingWithJsDoc", + input: `[|import a from 'a/aaaaaaa/aaaaaaa/aaaaaa/aaaaaaa'; +import b from 'b'; +import c from 'c';|] + +[|/** @internal */|] +export class LanguageIdentifier[| { }|]`, + }, + { + title: "incrementalParsingWithJsDoc_2", + input: `[|import a from 'a/aaaaaaa/aaaaaaa/aaaaaa/aaaaaaa'; +/**/import b from 'b'; +import c from 'c';|] + +[|/** @internal */|] +export class LanguageIdentifier[| { }|]`, + }, + { + title: "getOutliningSpansForUnbalancedRegion", + input: `// top-heavy region balance +// #region unmatched + +[|// #region matched + +// #endregion matched|]`, + }, + { + title: "getOutliningSpansForTemplateLiteral", + input: "declare function tag(...args: any[]): void\nconst a = [|`signal line`|]\nconst b = [|`multi\nline`|]\nconst c = tag[|`signal line`|]\nconst d = tag[|`multi\nline`|]\nconst e = [|`signal ${1} line`|]\nconst f = [|`multi\n${1}\nline`|]\nconst g = tag[|`signal ${1} line`|]\nconst h = tag[|`multi\n${1}\nline`|]\nconst i = ``", + }, + { + title: "getOutliningSpansForImports", + input: `[|import * as ns from "mod"; + +import d from "mod"; +import { a, b, c } from "mod"; + +import r = require("mod");|] + +// statement +var x = 0; + +// another set of imports +[|import * as ns from "mod"; +import d from "mod"; +import { a, b, c } from "mod"; +import r = require("mod");|]`, + }, + { + title: "getOutliningSpansDepthElseIf", + input: `if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else[| { + 1; +}|]`, + }, + { + title: "getOutliningSpansDepthChainedCalls", + input: `declare var router: any; +router + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|]`, + }, + { + title: "getOutliningSpans", + input: `// interface +interface IFoo[| { + getDist(): number; +}|] + +// class members +class Foo[| { + constructor()[| { + }|] + + public foo(): number[| { + return 0; + }|] + + public get X()[| { + return 1; + }|] + + public set X(v: number)[| { + }|] + + public member = function f()[| { + + }|] +}|] +// class expressions +[|(new class[| { + bla()[| { + + }|] +}|])|] +switch(1)[| { +case 1:[| break;|] +}|] + +var array =[| [ + 1, + 2 +]|] + +// modules +module m1[| { + module m2[| { }|] + module m3[| { + function foo()[| { + + }|] + + interface IFoo2[| { + + }|] + + class foo2 implements IFoo2[| { + + }|] + }|] +}|] + +// function declaration +function foo(): number[| { + return 0; +}|] + +// function expressions +[|(function f()[| { + +}|])|] + +// trivia handeling +class ClassFooWithTrivia[| /* some comments */ + /* more trivia */ { + + + [|/*some trailing trivia */|] +}|] /* even more */ + +// object literals +var x =[|{ + a:1, + b:2, + get foo()[| { + return 1; + }|] +}|] +//outline with deep nesting +var nest =[| [[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[ + [|[ + [ + [ + [ + [ + 1,2,3 + ] + ] + ] + ] + ]|] +]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]; + +//outline after a deeply nested node +class AfterNestedNodes[| { +}|] +// function arguments +function f(x: number[], y: number[])[| { + return 3; +}|] +f[|( +// single line array literal span won't render in VS + [|[0]|], + [|[ + 1, + 2 + ]|] +)|]; + +class C[| { + foo: T; +}|] + +class D extends C[| { + constructor(x)[| { + super(x); + }|] +}|]`, + }, + { + title: "getOutliningForTypeLiteral", + input: `type A =[| { + a: number; +}|] + +type B =[| { + a:[| { + a1:[| { + a2:[| { + x: number; + y: number; + }|] + }|] + }|], + b:[| { + x: number; + }|], + c:[| { + x: number; + }|] +}|]`, + }, + { + title: "getOutliningForTupleType", + input: `type A =[| [ + number, + number, + number +]|] + +type B =[| [ + [|[ + [|[ + number, + number, + number + ]|] + ]|] +]|]`, + }, + { + title: "getOutliningForSingleLineComments", + input: `[|// Single line comments at the start of the file + // line 2 + // line 3 + // line 4|] + module Sayings[| { + + [|/* + */|] + [|// A sequence of + // single line|] + [|/* + and block + */|] + [|// comments + //|] + export class Sample[| { + }|] + }|] + + interface IFoo[| { + [|// all consecutive single line comments should be in one block regardless of their number or empty lines/spaces inbetween + + // comment 2 + // comment 3 + + //comment 4 + /// comment 5 + ///// comment 6 + + //comment 7 + ///comment 8 + // comment 9 + // //comment 10 + + // // //comment 11 + // comment 12 + // comment 13 + // comment 14 + // comment 15 + + // comment 16 + // comment 17 + // comment 18 + // comment 19 + // comment 20 + // comment 21|] + + getDist(): number; // One single line comment should not be collapsed + }|] + + // One single line comment should not be collapsed + class WithOneSingleLineComment[| { + }|] + + function Foo()[| { + [|// comment 1 + // comment 2|] + this.method = function (param)[| { + }|] + + [|// comment 1 + // comment 2|] + function method(param)[| { + }|] + }|]`, + }, + { + title: "getOutliningForObjectsInArray", + input: `// objects in x should generate outlining spans that do not render in VS +const x =[| [ + [|{ a: 0 }|], + [|{ b: 1 }|], + [|{ c: 2 }|] +]|]; +// objects in y should generate outlining spans that render as expected +const y =[| [ + [|{ + a: 0 + }|], + [|{ + b: 1 + }|], + [|{ + c: 2 + }|] +]|]; +// same behavior for nested arrays +const w =[| [ + [|[ 0 ]|], + [|[ 1 ]|], + [|[ 2 ]|] +]|]; + +const z =[| [ + [|[ + 0 + ]|], + [|[ + 1 + ]|], + [|[ + 2 + ]|] +]|]; +// multiple levels of nesting work as expected +const z =[| [ + [|[ + [|{ hello: 0 }|] + ]|], + [|[ + [|{ hello: 3 }|] + ]|], + [|[ + [|{ hello: 5 }|], + [|{ hello: 7 }|] + ]|] +]|];`, + }, + { + title: "getOutliningForObjectDestructuring", + input: `const[| { + a, + b, + c +}|] =[| { + a: 1, + b: 2, + c: 3 +}|] + +const[| { + a:[| { + a_1, + a_2, + a_3:[| { + a_3_1, + a_3_2, + a_3_3, + }|], + }|], + b, + c +}|] =[| { + a:[| { + a_1: 1, + a_2: 2, + a_3:[| { + a_3_1: 1, + a_3_2: 1, + a_3_3: 1 + }|], + }|], + b: 2, + c: 3 +}|]`, + }, + { + title: "getOutliningForBlockComments", + input: `[|/* + Block comment at the beginning of the file before module: + line one of the comment + line two of the comment + line three + line four + line five + */|] + module Sayings[| { + [|/* + Comment before class: + line one of the comment + line two of the comment + line three + line four + line five + */|] + export class Greeter[| { + [|/* + Comment before a string identifier + line two of the comment + */|] + greeting: string; + [|/* + constructor + parameter message as a string + */|] + + [|/* + Multiple comments should be collapsed individually + */|] + constructor(message: string /* do not collapse this */)[| { + this.greeting = message; + }|] + [|/* + method of a class + */|] + greet()[| { + return "Hello, " + this.greeting; + }|] + }|] + }|] + + [|/* + Block comment for interface. The ending can be on the same line as the declaration. + */|]interface IFoo[| { + [|/* + Multiple block comments + */|] + + [|/* + should be collapsed + */|] + + [|/* + individually + */|] + + [|/* + this comment has trailing space before /* and after *-/ signs + */|] + + [|/** + * + * + * + */|] + + [|/* + */|] + + [|/* + */|] + // single line comments in the middle should not have an effect + [|/* + */|] + + [|/* + */|] + + [|/* + this block comment ends + on the same line */|] [|/* where the following comment starts + should be collapsed separately + */|] + + getDist(): number; + }|] + + var x =[|{ + a:1, + b: 2, + [|/* + Over a function in an object literal + */|] + get foo()[| { + return 1; + }|] + }|] + + // Over a function expression assigned to a variable + [|/** + * Return a sum + * @param {Number} y + * @param {Number} z + * @returns {Number} the sum of y and z + */|] + const sum2 = (y, z) =>[| { + return y + z; + }|]; + + // Over a variable + [|/** + * foo + */|] + const foo = null; + + function Foo()[| { + [|/** + * Description + * + * @param {string} param + * @returns + */|] + this.method = function (param)[| { + }|] + + [|/** + * Description + * + * @param {string} param + * @returns + */|] + function method(param)[| { + }|] + }|] + + function fn1()[| { + [|/** + * comment + */|] + }|] + function fn2()[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + function fn3()[| { + const x = 1; + + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + function fn4()[| { + [|/** + * comment + */|] + const x = 1; + + [|/** + * comment + */|] + }|] + function fn5()[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + return 1; + }|] + function fn6()[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + const x = 1; + }|] + + [|/* + comment + */|] + + f6(); + + class C1[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + class C2[| { + private prop = 1; + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + class C3[| { + [|/** + * comment + */|] + + private prop = 1; + [|/** + * comment + */|] + }|] + class C4[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + private prop = 1; + }|] + + [|/* + comment + */|] + new C4(); + + module M1[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + module M2[| { + export const a = 1; + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + module M3[| { + [|/** + * comment + */|] + export const a = 1; + + [|/** + * comment + */|] + }|] + module M4[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + export const a = 1; + }|] + interface I1[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + interface I2[| { + x: number; + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|] + interface I3[| { + [|/** + * comment + */|] + x: number; + + [|/** + * comment + */|] + }|] + interface I4[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + x: number; + }|] + [|{ + [|/** + * comment + */|] + + [|/** + * comment + */|] + }|]`, + }, + { + title: "getOutliningForArrayDestructuring", + input: `const[| [ + a, + b, + c +]|] =[| [ + 1, + 2, + 3 +]|]; + +const[| [ + [|[ + [|[ + [|[ + a, + b, + c + ]|] + ]|] + ]|], + [|[ + a1, + b1, + c1 + ]|] +]|] =[| [ + [|[ + [|[ + [|[ + 1, + 2, + 3 + ]|] + ]|] + ]|], + [|[ + 1, + 2, + 3 + ]|] +]|]`, + }, + // { + // title: "getJSXOutliningSpans", + // input: `import React, { Component } from 'react'; + + // export class Home extends Component[| { + // render()[| { + // return [|( + // [|
+ // [|

Hello, world!

|] + // [||] + //
+ // + // [|<> + // text + // |] + //
|] + // )|]; + // }|] + // }|]`, + // }, + { + title: "corruptedTryExpressionsDontCrashGettingOutlineSpans", + input: `try[| { + var x = [ + {% try %}|]{% except %} + ] +} catch (e)[| { + +}|]`, + }, + { + title: "outliningSpansForFunctions", + input: `namespace NS[| { + function f(x: number, y: number)[| { + return x + y; + }|] + + function g[|( + x: number, + y: number, + ): number { + return x + y; + }|] +}|]`, + }, + { + title: "outliningSpansTrailingBlockCmmentsAfterStatements", + input: `console.log(0); +[|/* +/ * Some text + */|]`, + }, + { + title: "outlineSpansBlockCommentsWithoutStatements", + input: `[|/* +/ * Some text + */|]`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.title, func(t *testing.T) { + t.Parallel() + runFoldingRangeTest(t, testCase.input) + }) + } +} diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 5fc1f0cb74..8a0d7bfdd1 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -390,7 +390,7 @@ func isInRightSideOfInternalImportEqualsDeclaration(node *ast.Node) bool { } func (l *LanguageService) createLspRangeFromNode(node *ast.Node, file *ast.SourceFile) *lsproto.Range { - return l.createLspRangeFromBounds(node.Pos(), node.End(), file) + return l.createLspRangeFromBounds(astnav.GetStartOfNode(node, file, false /*includeJSDoc*/), node.End(), file) } func (l *LanguageService) createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range { diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 4dacb01f05..43b5475baf 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2303,15 +2303,14 @@ func GetLineEndOfPosition(sourceFile ast.SourceFileLike, pos int) int { var lastCharPos int if line+1 >= len(lineStarts) { - lineMap := sourceFile.LineMap() - lastCharPos = int(lineMap[len(lineMap)-1]) + lastCharPos = len(sourceFile.Text()) } else { lastCharPos = int(lineStarts[line+1] - 1) } fullText := sourceFile.Text() // if the new line is "\r\n", we should return the last non-new-line-character position - if len(fullText) > 0 && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { + if len(fullText) > 0 && len(fullText) != lastCharPos && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { return lastCharPos - 1 } else { return lastCharPos From 1938da6b21d7eb0bfa8f76dcf7961f2e4a706d52 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 30 Jun 2025 12:13:10 -0700 Subject: [PATCH 03/10] code clean up --- internal/ls/folding.go | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 88788b9bdb..0d8b8356da 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -34,8 +34,9 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { depthRemaining := 40 current := 0 - // Includes the EOF Token so that comments which aren't attached to statements are included + statements := sourceFile.Statements + // Includes the EOF Token so that comments which aren't attached to statements are included var curr *ast.Node currentTokenEnd := 0 if statements != nil && statements.Nodes != nil { @@ -43,11 +44,7 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l currentTokenEnd = curr.End() } scanner := scanner.GetScannerForSourceFile(sourceFile, currentTokenEnd) - eof := scanner.Token() - tokenFullStart := scanner.TokenFullStart() - tokenEnd := scanner.TokenEnd() - k := sourceFile.GetOrCreateToken(eof, tokenFullStart, tokenEnd, curr) - statements.Nodes = append(statements.Nodes, k) + statements.Nodes = append(statements.Nodes, sourceFile.GetOrCreateToken(scanner.Token(), scanner.TokenFullStart(), scanner.TokenEnd(), curr)) n := len(statements.Nodes) var foldingRange []*lsproto.FoldingRange @@ -91,14 +88,12 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) [] continue } if result.isStart { - // span := l.createLspRangeFromBounds(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//"), lineEnd, sourceFile) - i := strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//") - span := l.createLspPosition(i+int(currentLineStart), sourceFile) + span := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile) foldingRangeKindRegion := lsproto.FoldingRangeKindRegion collapsedTest := "#region" if result.name != "" { collapsedTest = result.name - } // createFoldingRange(span, foldingRangeKindRegion, nil, collapsedTest) + } regions = append(regions, &lsproto.FoldingRange{ StartLine: span.Line, StartCharacter: &span.Character, @@ -204,8 +199,7 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L } depthRemaining-- } else { - var visit func(node *ast.Node) bool - visit = func(node *ast.Node) bool { + visit := func(node *ast.Node) bool { childNode := visitNode(node, depthRemaining, sourceFile, l) if childNode != nil { foldingRange = append(foldingRange, childNode...) @@ -226,8 +220,8 @@ func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFi } func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { - c := &printer.EmitContext{} - comments := scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(c).NodeFactory, sourceFile.Text(), pos) + p := &printer.EmitContext{} + comments := scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceFile.Text(), pos) var foldingRange []*lsproto.FoldingRange firstSingleLineCommentStart := -1 @@ -393,7 +387,7 @@ func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l * if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } - return spanBetweenTokens(openToken, closeToken, node, sourceFile, false /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, node, sourceFile, false /*useFullStart*/, l) } func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -415,7 +409,7 @@ func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *Langua return nil } - return spanBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) } func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -485,12 +479,12 @@ func spanForNode(node *ast.Node, hintSpanNode *ast.Node, open ast.Kind, useFullS openToken := findChildOfKind(node, open, sourceFile) closeToken := findChildOfKind(node, close, sourceFile) if openToken != nil && closeToken != nil { - return spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, useFullStart, l) + return rangeBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, useFullStart, l) } return nil } -func spanBetweenTokens(openToken *ast.Node, closeToken *ast.Node, hintSpanNode *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange { +func rangeBetweenTokens(openToken *ast.Node, closeToken *ast.Node, hintSpanNode *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) @@ -515,7 +509,7 @@ func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.Foldi return &lsproto.FoldingRange{ StartLine: textRange.Start.Line, StartCharacter: &textRange.Start.Character, - EndLine: textRange.End.Line, // !!! needs to be adjusted for in vscode repo + EndLine: textRange.End.Line, EndCharacter: &textRange.End.Character, Kind: kind, CollapsedText: &collapsedText, @@ -530,7 +524,7 @@ func functionSpan(node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l openToken := tryGetFunctionOpenToken(node, body, sourceFile) closeToken := findChildOfKind(body, ast.KindCloseBraceToken, sourceFile) if openToken != nil && closeToken != nil { - return spanBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) } return nil } From 86df1387fd31ff47616189a43e5a12c822cbbc0a Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 30 Jun 2025 16:18:31 -0700 Subject: [PATCH 04/10] cleanup --- internal/ast/utilities.go | 45 +---------------------- internal/ls/folding.go | 71 +++++++++++++++++-------------------- internal/ls/folding_test.go | 10 +----- internal/lsp/server.go | 6 +--- 4 files changed, 36 insertions(+), 96 deletions(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index a1924538fa..5c5fd2314d 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -663,49 +663,6 @@ func isDeclarationStatementKind(kind Kind) bool { return false } -func isDeclarationKind(kind Kind) bool { - switch kind { - case KindArrowFunction, - KindBindingElement, - KindClassDeclaration, - KindClassExpression, - KindClassStaticBlockDeclaration, - KindConstructor, - KindEnumDeclaration, - KindEnumMember, - KindExportSpecifier, - KindFunctionDeclaration, - KindFunctionExpression, - KindGetAccessor, - KindImportClause, - KindImportEqualsDeclaration, - KindImportSpecifier, - KindInterfaceDeclaration, - KindJsxAttribute, - KindMethodDeclaration, - KindMethodSignature, - KindModuleDeclaration, - KindNamespaceExportDeclaration, - KindNamespaceExport, - KindNamespaceImport, - KindParameter, - KindPropertyAssignment, - KindPropertyDeclaration, - KindPropertySignature, - KindSetAccessor, - KindShorthandPropertyAssignment, - KindTypeAliasDeclaration, - KindTypeParameter, - KindVariableDeclaration, - KindJSDocTypedefTag, - KindJSDocCallbackTag, - KindJSDocPropertyTag, - KindNamedTupleMember: - return true - } - return false -} - // Determines whether a node is a DeclarationStatement. Ideally this does not use Parent pointers, but it may use them // to rule out a Block node that is part of `try` or `catch` or is the Block-like body of a function. // @@ -1308,7 +1265,7 @@ func IsDeclaration(node *Node) bool { if node.Kind == KindTypeParameter { return node.Parent != nil } - return isDeclarationKind(node.Kind) + return IsDeclarationNode(node) } // True if `name` is the name of a declaration node diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 0d8b8356da..c5520a944d 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -232,22 +232,20 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l 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(firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l) } return nil } sourceText := sourceFile.Text() comments(func(comment ast.CommentRange) bool { - pos := comment.Pos() - end := comment.End() + commentPos := comment.Pos() + commentEnd := comment.End() // cancellationToken.throwIfCancellationRequested(); switch comment.Kind { case ast.KindSingleLineCommentTrivia: // never fold region delimiters into single-line comment regions - commentText := sourceText[pos:end] + commentText := sourceText[commentPos:commentEnd] if parseRegionDelimiter(commentText) != nil { comments := combineAndAddMultipleSingleLineComments() if comments != nil { @@ -260,9 +258,9 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l // 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 = pos + firstSingleLineCommentStart = commentPos } - lastSingleLineCommentEnd = end + lastSingleLineCommentEnd = commentEnd singleLineCommentCount++ break case ast.KindMultiLineCommentTrivia: @@ -270,7 +268,7 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l if comments != nil { foldingRange = append(foldingRange, comments) } - foldingRange = append(foldingRange, createFoldingRangeFromBounds(pos, end, foldingRangeKindComment, sourceFile, l)) + foldingRange = append(foldingRange, createFoldingRangeFromBounds(commentPos, commentEnd, foldingRangeKindComment, sourceFile, l)) singleLineCommentCount = 0 break default: @@ -321,35 +319,35 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag // 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, n.Parent, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + 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, n.Parent, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) } else if tryStatement.FinallyBlock == n { node := findChildOfKind(n.Parent, ast.KindFinallyKeyword, sourceFile) if node != nil { - return spanForNode(n, node, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) } } 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), "", nil, "") + return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", "") } case ast.KindModuleBlock: - return spanForNode(n, n.Parent, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + 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, n, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) case ast.KindTupleType: - return spanForNode(n, n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart */, sourceFile, l) + 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, n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart */, sourceFile, l) case ast.KindArrayLiteralExpression: - return spanForNode(n, n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart */, sourceFile, l) + 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: @@ -357,7 +355,7 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral: return spanForTemplateLiteral(n, sourceFile, l) case ast.KindArrayBindingPattern: - return spanForNode(n, n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart */, sourceFile, l) case ast.KindArrowFunction: return spanForArrowFunction(n, sourceFile, l) case ast.KindCallExpression: @@ -387,7 +385,7 @@ 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, node, sourceFile, false /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, sourceFile, false /*useFullStart*/, l) } func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -396,7 +394,7 @@ func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, return nil } textRange := l.createLspRangeFromBounds(start, node.End(), sourceFile) - return createFoldingRange(textRange, "", l.createLspRangeFromNode(node, sourceFile), "") + return createFoldingRange(textRange, "", "") } func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -409,7 +407,7 @@ func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *Langua return nil } - return rangeBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) } func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -418,7 +416,7 @@ func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *Languag return nil } textRange := l.createLspRangeFromBounds(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) - return createFoldingRange(textRange, "", l.createLspRangeFromNode(node, sourceFile), "") + return createFoldingRange(textRange, "", "") } func spanForTemplateLiteral(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -448,7 +446,7 @@ func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageSe bannerText.WriteString("<>...") } - return createFoldingRange(textRange, "", nil, bannerText.String()) + return createFoldingRange(textRange, "", bannerText.String()) } func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -466,38 +464,35 @@ func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *Languag 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), "", nil, "") + return createFoldingRange(l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "") } return nil } -func spanForNode(node *ast.Node, hintSpanNode *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { - close := ast.KindCloseBraceToken +func spanForNode(node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + closeBrace := ast.KindCloseBraceToken if open != ast.KindOpenBraceToken { - close = ast.KindCloseBracketToken + closeBrace = ast.KindCloseBracketToken } openToken := findChildOfKind(node, open, sourceFile) - closeToken := findChildOfKind(node, close, sourceFile) + closeToken := findChildOfKind(node, closeBrace, sourceFile) if openToken != nil && closeToken != nil { - return rangeBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, useFullStart, l) + return rangeBetweenTokens(openToken, closeToken, sourceFile, useFullStart, l) } return nil } -func rangeBetweenTokens(openToken *ast.Node, closeToken *ast.Node, hintSpanNode *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange { +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, "", l.createLspRangeFromNode(hintSpanNode, sourceFile), "") + return createFoldingRange(textRange, "", "") } -func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, hintRange *lsproto.Range, collapsedText string) *lsproto.FoldingRange { - if hintRange == nil { - hintRange = textRange - } +func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange { if collapsedText == "" { defaultText := "..." collapsedText = defaultText @@ -517,14 +512,14 @@ func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.Foldi } func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { - return createFoldingRange(l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, nil, "") + 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 := findChildOfKind(body, ast.KindCloseBraceToken, sourceFile) if openToken != nil && closeToken != nil { - return rangeBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) } return nil } diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go index e0a529a9aa..f8c2d2009e 100644 --- a/internal/ls/folding_test.go +++ b/internal/ls/folding_test.go @@ -845,7 +845,7 @@ type B =[| [ }|] interface IFoo[| { - [|// all consecutive single line comments should be in one block regardless of their number or empty lines/spaces inbetween + [|// all consecutive single line comments should be in one block regardless of their number or empty lines/spaces in between // comment 2 // comment 3 @@ -880,8 +880,6 @@ type B =[| [ }|] function Foo()[| { - [|// comment 1 - // comment 2|] this.method = function (param)[| { }|] @@ -1101,12 +1099,6 @@ const[| { const foo = null; function Foo()[| { - [|/** - * Description - * - * @param {string} param - * @returns - */|] this.method = function (param)[| { }|] diff --git a/internal/lsp/server.go b/internal/lsp/server.go index dc6584f6d7..76992c26af 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -577,11 +577,7 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) { MoreTriggerCharacter: &[]string{"}", ";", "\n"}, }, FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{ - FoldingRangeOptions: &lsproto.FoldingRangeOptions{ - WorkDoneProgressOptions: lsproto.WorkDoneProgressOptions{ - WorkDoneProgress: ptrTo(true), - }, - }, + Boolean: ptrTo(true), }, }, }) From 18a273762163c00df45d790284a6a69e22882531 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 30 Jun 2025 17:35:51 -0700 Subject: [PATCH 05/10] adding check for binary expression --- internal/ls/folding.go | 3 ++- internal/ls/folding_test.go | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index c5520a944d..6c81cd0493 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -125,7 +125,8 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L } // cancellationToken.throwIfCancellationRequested(); var foldingRange []*lsproto.FoldingRange - if ast.IsDeclaration(n) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { + // !!! remove !ast.IsBinaryExpression(n) after JSDoc implementation + if (!ast.IsBinaryExpression(n) && ast.IsDeclaration(n)) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(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) { diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go index f8c2d2009e..98771e631c 100644 --- a/internal/ls/folding_test.go +++ b/internal/ls/folding_test.go @@ -388,7 +388,7 @@ foo[|([| }, { title: "outliningSpansForArguments", - input: `console.log(123, 456)l; + input: `console.log(123, 456); console.log( ); console.log[|( @@ -880,6 +880,8 @@ type B =[| [ }|] function Foo()[| { + [|// comment 1 + // comment 2|] this.method = function (param)[| { }|] @@ -1099,6 +1101,12 @@ const[| { const foo = null; function Foo()[| { + [|/** + * Description + * + * @param {string} param + * @returns + */|] this.method = function (param)[| { }|] From b7b020f256913374c2834601975c81912cda8562 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 3 Jul 2025 10:30:09 -0700 Subject: [PATCH 06/10] addressing pr comments --- internal/ls/folding.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 6c81cd0493..01ec411446 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -36,16 +36,6 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l current := 0 statements := sourceFile.Statements - // Includes the EOF Token so that comments which aren't attached to statements are included - var curr *ast.Node - currentTokenEnd := 0 - if statements != nil && statements.Nodes != nil { - curr = statements.Nodes[len(statements.Nodes)-1] - currentTokenEnd = curr.End() - } - scanner := scanner.GetScannerForSourceFile(sourceFile, currentTokenEnd) - statements.Nodes = append(statements.Nodes, sourceFile.GetOrCreateToken(scanner.Token(), scanner.TokenFullStart(), scanner.TokenEnd(), curr)) - n := len(statements.Nodes) var foldingRange []*lsproto.FoldingRange for current < n { @@ -73,6 +63,16 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l l)) } } + + // Includes the EOF Token so that comments which aren't attached to statements are included + var curr *ast.Node + currentTokenEnd := 0 + if statements != nil && statements.Nodes != nil { + curr = statements.Nodes[len(statements.Nodes)-1] + currentTokenEnd = curr.End() + } + scanner := scanner.GetScannerForSourceFile(sourceFile, currentTokenEnd) + foldingRange = append(foldingRange, visitNode(sourceFile.GetOrCreateToken(scanner.Token(), scanner.TokenFullStart(), scanner.TokenEnd(), curr), depthRemaining, sourceFile, l)...) return foldingRange } From 74357261223a0aef6744ea836d5fd7223b23f031 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 3 Jul 2025 10:52:33 -0700 Subject: [PATCH 07/10] fixing test --- internal/ls/folding_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go index 98771e631c..631441fb81 100644 --- a/internal/ls/folding_test.go +++ b/internal/ls/folding_test.go @@ -15,7 +15,7 @@ func runFoldingRangeTest(t *testing.T, input string) { testData := fourslash.ParseTestData(t, input, "/file1.ts") markerPositions := testData.Ranges ctx := projecttestutil.WithRequestID(t.Context()) - service, done := createLanguageService(ctx, testData.Files[0].FileName(), map[string]any{ + service, done := createLanguageService(ctx, testData.Files[0].FileName(), map[string]string{ testData.Files[0].FileName(): testData.Files[0].Content, }) defer done() From 3527adba77e7e0230b87edb0d0d3b50ad201a9f2 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 3 Jul 2025 11:11:01 -0700 Subject: [PATCH 08/10] using sourceFile.endOfFileToken --- internal/ls/folding.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 01ec411446..707af3649c 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -64,15 +64,8 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l } } - // Includes the EOF Token so that comments which aren't attached to statements are included - var curr *ast.Node - currentTokenEnd := 0 - if statements != nil && statements.Nodes != nil { - curr = statements.Nodes[len(statements.Nodes)-1] - currentTokenEnd = curr.End() - } - scanner := scanner.GetScannerForSourceFile(sourceFile, currentTokenEnd) - foldingRange = append(foldingRange, visitNode(sourceFile.GetOrCreateToken(scanner.Token(), scanner.TokenFullStart(), scanner.TokenEnd(), curr), depthRemaining, sourceFile, l)...) + // Visit the EOF Token so that comments which aren't attached to statements are included. + foldingRange = append(foldingRange, visitNode(sourceFile.EndOfFileToken, depthRemaining, sourceFile, l)...) return foldingRange } From 08fbeda94c1963c596d9f0d10189bfd32ef2dba9 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 7 Jul 2025 11:03:58 -0700 Subject: [PATCH 09/10] addressing pr comments --- internal/ls/folding.go | 38 ++++++++++++++++++------------------- internal/ls/folding_test.go | 5 ++--- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 707af3649c..1d7794a926 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -20,13 +20,14 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l sort.Slice(res, func(i, j int) bool { if res[i].StartLine != res[j].StartLine { return res[i].StartLine < res[j].StartLine - } - if res[i].EndLine != res[j].EndLine { - return res[i].EndLine < res[j].EndLine } else if res[i].StartCharacter != nil && res[j].StartCharacter != nil { return *res[i].StartCharacter < *res[j].StartCharacter + } else if res[i].EndLine != res[j].EndLine { + return res[i].EndLine < res[j].EndLine + } else if res[i].EndCharacter != nil && res[j].EndCharacter != nil { + return *res[i].EndCharacter < *res[j].EndCharacter } - return *res[i].EndCharacter < *res[j].EndCharacter + return false }) return res } @@ -80,32 +81,31 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) [] if result == nil || isInComment(sourceFile, int(currentLineStart), nil) != nil { continue } + if result.isStart { - span := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile) + commentStart := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile) foldingRangeKindRegion := lsproto.FoldingRangeKindRegion - collapsedTest := "#region" + collapsedText := "#region" if result.name != "" { - collapsedTest = 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: span.Line, - StartCharacter: &span.Character, + StartLine: commentStart.Line, + StartCharacter: &commentStart.Character, Kind: &foldingRangeKindRegion, - CollapsedText: &collapsedTest, + CollapsedText: &collapsedText, }) } else { if len(regions) > 0 { region := regions[len(regions)-1] regions = regions[:len(regions)-1] - if region != nil { - if out == nil { - out = []*lsproto.FoldingRange{} - } - endingPosition := l.createLspPosition(lineEnd, sourceFile) - region.EndLine = endingPosition.Line - region.EndCharacter = &endingPosition.Character - out = append(out, region) - } + endingPosition := l.createLspPosition(lineEnd, sourceFile) + region.EndLine = endingPosition.Line + region.EndCharacter = &endingPosition.Character + out = append(out, region) } } } diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go index 631441fb81..d39f982b4a 100644 --- a/internal/ls/folding_test.go +++ b/internal/ls/folding_test.go @@ -27,11 +27,10 @@ func runFoldingRangeTest(t *testing.T, input string) { sort.Slice(markerPositions, func(i, j int) bool { if markerPositions[i].LSRange.Start.Line != markerPositions[j].LSRange.Start.Line { return markerPositions[i].LSRange.Start.Line < markerPositions[j].LSRange.Start.Line - } - if markerPositions[i].LSRange.End.Line != markerPositions[j].LSRange.End.Line { - return markerPositions[i].LSRange.End.Line < markerPositions[j].LSRange.End.Line } else if markerPositions[i].LSRange.Start.Character != markerPositions[j].LSRange.Start.Character { return markerPositions[i].LSRange.Start.Character < markerPositions[j].LSRange.Start.Character + } else if markerPositions[i].LSRange.End.Line != markerPositions[j].LSRange.End.Line { + return markerPositions[i].LSRange.End.Line < markerPositions[j].LSRange.End.Line } return markerPositions[i].LSRange.End.Character < markerPositions[j].LSRange.End.Character }) From 30f9df788c6e7e05b21f2d73a655df785c8a4cc8 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 7 Jul 2025 17:08:05 -0700 Subject: [PATCH 10/10] addressing comments --- internal/ls/folding.go | 31 ++++++++++++++++++------------- internal/ls/folding_test.go | 19 ++++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 1d7794a926..f2b8affe7f 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -1,10 +1,12 @@ package ls import ( + "cmp" "context" "regexp" - "sort" + "slices" "strings" + "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" @@ -17,17 +19,20 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l _, sourceFile := l.getProgramAndFile(documentURI) res := l.addNodeOutliningSpans(sourceFile) res = append(res, l.addRegionOutliningSpans(sourceFile)...) - sort.Slice(res, func(i, j int) bool { - if res[i].StartLine != res[j].StartLine { - return res[i].StartLine < res[j].StartLine - } else if res[i].StartCharacter != nil && res[j].StartCharacter != nil { - return *res[i].StartCharacter < *res[j].StartCharacter - } else if res[i].EndLine != res[j].EndLine { - return res[i].EndLine < res[j].EndLine - } else if res[i].EndCharacter != nil && res[j].EndCharacter != nil { - return *res[i].EndCharacter < *res[j].EndCharacter + slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int { + if a.StartLine != b.StartLine { + return cmp.Compare(a.StartLine, b.StartLine) } - return false + if a.StartCharacter != nil && b.StartCharacter != nil { + return cmp.Compare(*a.StartCharacter, *b.StartCharacter) + } + if a.EndLine != b.EndLine { + return cmp.Compare(a.EndLine, b.EndLine) + } + if a.EndCharacter != nil && b.EndCharacter != nil { + return cmp.Compare(*a.EndCharacter, *b.EndCharacter) + } + return 0 }) return res } @@ -287,11 +292,11 @@ type regionDelimiterResult struct { 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.TrimLeft(lineText, " \t") + lineText = strings.TrimLeftFunc(lineText, unicode.IsSpace) if !strings.HasPrefix(lineText, "//") { return nil } - lineText = strings.TrimLeft(lineText[2:], " \t") + lineText = strings.TrimSpace(lineText[2:]) result := regionDelimiterRegExp.FindStringSubmatch(lineText) if result != nil { return ®ionDelimiterResult{ diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go index d39f982b4a..1bb7222624 100644 --- a/internal/ls/folding_test.go +++ b/internal/ls/folding_test.go @@ -1,7 +1,8 @@ package ls_test import ( - "sort" + "cmp" + "slices" "testing" "github.com/microsoft/typescript-go/internal/collections" @@ -24,15 +25,15 @@ func runFoldingRangeTest(t *testing.T, input string) { if len(foldingRanges) != len(markerPositions) { t.Fatalf("Expected %d folding ranges, got %d", len(markerPositions), len(foldingRanges)) } - sort.Slice(markerPositions, func(i, j int) bool { - if markerPositions[i].LSRange.Start.Line != markerPositions[j].LSRange.Start.Line { - return markerPositions[i].LSRange.Start.Line < markerPositions[j].LSRange.Start.Line - } else if markerPositions[i].LSRange.Start.Character != markerPositions[j].LSRange.Start.Character { - return markerPositions[i].LSRange.Start.Character < markerPositions[j].LSRange.Start.Character - } else if markerPositions[i].LSRange.End.Line != markerPositions[j].LSRange.End.Line { - return markerPositions[i].LSRange.End.Line < markerPositions[j].LSRange.End.Line + slices.SortFunc(markerPositions, func(a, b *fourslash.RangeMarker) int { + if a.LSRange.Start.Line != b.LSRange.Start.Line { + return cmp.Compare(a.LSRange.Start.Line, b.LSRange.Start.Line) + } else if a.LSRange.Start.Character != b.LSRange.Start.Character { + return cmp.Compare(a.LSRange.Start.Character, b.LSRange.Start.Character) + } else if a.LSRange.End.Line != b.LSRange.End.Line { + return cmp.Compare(a.LSRange.End.Line, b.LSRange.End.Line) } - return markerPositions[i].LSRange.End.Character < markerPositions[j].LSRange.End.Character + return cmp.Compare(a.LSRange.End.Character, b.LSRange.End.Character) }) for i, marker := range markerPositions { assert.DeepEqual(t, marker.LSRange.Start.Line, foldingRanges[i].StartLine)