From f1616c36b6df7410565944cf57e73337e48988aa Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 23 Jun 2025 14:04:49 -0700 Subject: [PATCH 01/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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) From 356383f9aa5e388fc8bcb9898741306c1ec90ed5 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 15 Jul 2025 14:48:36 -0700 Subject: [PATCH 11/19] addressing comments --- internal/ls/folding.go | 28 +++++++++++++--------------- internal/ls/format.go | 3 +++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index f2b8affe7f..26b23267d1 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -23,13 +23,13 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l if a.StartLine != b.StartLine { return cmp.Compare(a.StartLine, b.StartLine) } - if a.StartCharacter != nil && b.StartCharacter != nil { + if a.StartCharacter != nil && b.StartCharacter != nil && *a.StartCharacter != *b.StartCharacter { 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 { + if a.EndCharacter != nil && b.EndCharacter != nil && *a.EndCharacter != *b.EndCharacter { return cmp.Compare(*a.EndCharacter, *b.EndCharacter) } return 0 @@ -220,7 +220,6 @@ func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFi func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { p := &printer.EmitContext{} - comments := scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceFile.Text(), pos) var foldingRange []*lsproto.FoldingRange firstSingleLineCommentStart := -1 @@ -237,7 +236,7 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l } sourceText := sourceFile.Text() - comments(func(comment ast.CommentRange) bool { + for comment := range scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceText, pos) { commentPos := comment.Pos() commentEnd := comment.End() // cancellationToken.throwIfCancellationRequested(); @@ -273,8 +272,7 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l default: // Debug.assertNever(kind); } - return true - }) + } addedComments := combineAndAddMultipleSingleLineComments() if addedComments != nil { foldingRange = append(foldingRange, addedComments) @@ -318,16 +316,16 @@ 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, 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, 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, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) } } default: @@ -336,17 +334,17 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", "") } case ast.KindModuleBlock: - return spanForNode(n, 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, ast.KindOpenBraceToken, true /*useFullStart */, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) case ast.KindTupleType: - return spanForNode(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, 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, 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: @@ -354,7 +352,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, 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: diff --git a/internal/ls/format.go b/internal/ls/format.go index 1a5677e535..d32ec31178 100644 --- a/internal/ls/format.go +++ b/internal/ls/format.go @@ -137,6 +137,9 @@ func getRangeOfEnclosingComment( precedingToken *ast.Node, tokenAtPosition *ast.Node, ) *ast.CommentRange { + if tokenAtPosition == nil { + tokenAtPosition = astnav.GetTokenAtPosition(file, position) + } jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc) if jsdoc != nil { tokenAtPosition = jsdoc.Parent From 65e981f36092da39bd6c7b7bb294c5e632e0040e Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 15 Jul 2025 15:38:56 -0700 Subject: [PATCH 12/19] adding defined capacity --- internal/ls/folding.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 26b23267d1..3fba23a9ab 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -43,7 +43,7 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l statements := sourceFile.Statements n := len(statements.Nodes) - var foldingRange []*lsproto.FoldingRange + foldingRange := make([]*lsproto.FoldingRange, 0, 40) for current < n { for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) { foldingRange = append(foldingRange, visitNode(statements.Nodes[current], depthRemaining, sourceFile, l)...) @@ -76,8 +76,8 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l } func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { - var regions []*lsproto.FoldingRange - var out []*lsproto.FoldingRange + regions := make([]*lsproto.FoldingRange, 0, 40) + out := make([]*lsproto.FoldingRange, 0, 40) lineStarts := scanner.GetLineStarts(sourceFile) for _, currentLineStart := range lineStarts { lineEnd := scanner.GetLineEndOfPosition(sourceFile, int(currentLineStart)) @@ -122,7 +122,7 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L return nil } // cancellationToken.throwIfCancellationRequested(); - var foldingRange []*lsproto.FoldingRange + foldingRange := make([]*lsproto.FoldingRange, 0, 40) // !!! 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)...) @@ -220,8 +220,7 @@ func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFi func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { p := &printer.EmitContext{} - - var foldingRange []*lsproto.FoldingRange + foldingRange := make([]*lsproto.FoldingRange, 0, 40) firstSingleLineCommentStart := -1 lastSingleLineCommentEnd := -1 singleLineCommentCount := 0 From c84900926b59d80408ab4e2aa3ef5b9d47ef1eb4 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 4 Aug 2025 17:00:37 -0700 Subject: [PATCH 13/19] addressing comments --- internal/ls/folding.go | 6 +++--- internal/ls/folding_test.go | 15 ++++++++------- internal/ls/format.go | 3 --- internal/lsp/server.go | 16 +++++++--------- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 3fba23a9ab..aa11617304 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -15,7 +15,7 @@ import ( "github.com/microsoft/typescript-go/internal/scanner" ) -func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) []*lsproto.FoldingRange { +func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.FoldingRangeResponse, error) { _, sourceFile := l.getProgramAndFile(documentURI) res := l.addNodeOutliningSpans(sourceFile) res = append(res, l.addRegionOutliningSpans(sourceFile)...) @@ -34,7 +34,7 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l } return 0 }) - return res + return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil } func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { @@ -83,7 +83,7 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) [] lineEnd := scanner.GetLineEndOfPosition(sourceFile, int(currentLineStart)) lineText := sourceFile.Text()[currentLineStart:lineEnd] result := parseRegionDelimiter(lineText) - if result == nil || isInComment(sourceFile, int(currentLineStart), nil) != nil { + if result == nil || isInComment(sourceFile, int(currentLineStart), astnav.GetTokenAtPosition(sourceFile, int(currentLineStart))) != nil { continue } diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go index 1bb7222624..bfe237e2ee 100644 --- a/internal/ls/folding_test.go +++ b/internal/ls/folding_test.go @@ -21,9 +21,10 @@ func runFoldingRangeTest(t *testing.T, input string) { }) 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)) + foldingRanges, _ := service.ProvideFoldingRange(ctx, ls.FileNameToDocumentURI("/file1.ts")) + ranges := *foldingRanges.FoldingRanges + if len(ranges) != len(markerPositions) { + t.Fatalf("Expected %d folding ranges, got %d", len(markerPositions), len(ranges)) } slices.SortFunc(markerPositions, func(a, b *fourslash.RangeMarker) int { if a.LSRange.Start.Line != b.LSRange.Start.Line { @@ -36,10 +37,10 @@ func runFoldingRangeTest(t *testing.T, input string) { 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) - 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) + assert.DeepEqual(t, marker.LSRange.Start.Line, ranges[i].StartLine) + assert.DeepEqual(t, marker.LSRange.End.Line, ranges[i].EndLine) + assert.DeepEqual(t, marker.LSRange.Start.Character, *ranges[i].StartCharacter) + assert.DeepEqual(t, marker.LSRange.End.Character, *ranges[i].EndCharacter) } } diff --git a/internal/ls/format.go b/internal/ls/format.go index 9b156c7ab7..978cab815a 100644 --- a/internal/ls/format.go +++ b/internal/ls/format.go @@ -140,9 +140,6 @@ func getRangeOfEnclosingComment( precedingToken *ast.Node, tokenAtPosition *ast.Node, ) *ast.CommentRange { - if tokenAtPosition == nil { - tokenAtPosition = astnav.GetTokenAtPosition(file, position) - } jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc) if jsdoc != nil { tokenAtPosition = jsdoc.Parent diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 7270b0845a..43517a38ca 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -507,6 +507,7 @@ var handlers = sync.OnceValue(func() handlerMap { registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol) registerRequestHandler(handlers, lsproto.TextDocumentDocumentSymbolInfo, (*Server).handleDocumentSymbol) registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve) + registerRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) return handlers }) @@ -628,9 +629,11 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{ Boolean: ptrTo(true), }, + FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{ + Boolean: ptrTo(true), + }, }, - }) -} + } return response, nil } @@ -717,18 +720,13 @@ func (s *Server) handleSignatureHelp(ctx context.Context, params *lsproto.Signat s.initializeParams.Capabilities.TextDocument.SignatureHelp, &ls.UserPreferences{}, ) - s.sendResult(req.ID, signatureHelp) - return nil } -func (s *Server) handleFoldingRange(ctx context.Context, req *lsproto.RequestMessage) error { - params := req.Params.(*lsproto.FoldingRangeParams) +func (s *Server) handleFoldingRange(ctx context.Context, params *lsproto.FoldingRangeParams) (lsproto.FoldingRangeResponse, error) { 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 + return languageService.ProvideFoldingRange(ctx, params.TextDocument.Uri) } func (s *Server) handleDefinition(ctx context.Context, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) { From 404e94f7ec9587697c79ab0acaf9beec68646e81 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 25 Nov 2025 10:30:26 -0800 Subject: [PATCH 14/19] updating the pr --- .../fourslash/_scripts/convertFourslash.mts | 74 ++++++++++++++++++- internal/ls/folding.go | 4 +- internal/lsp/server.go | 9 +-- internal/printer/utilities.go | 2 +- internal/scanner/scanner.go | 38 +++++----- 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 0101d67441..9a74127984 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -208,6 +208,8 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined { case "renameInfoSucceeded": case "renameInfoFailed": return parseRenameInfo(func.text, callExpression.arguments); + case "outliningSpansInCurrentFile": + return parseOutliningSpansArgs(callExpression.arguments); } } // `goTo....` @@ -1439,6 +1441,62 @@ function parseBaselineSmartSelection(args: ts.NodeArray): Cmd { }; } +function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutliningSpansCmd] | undefined { + if (args.length === 0) { + console.error("Expected at least one argument in verify.outliningSpansInCurrentFile"); + return undefined; + } + + // First argument should be spans array + const spansArg = args[0]; + let spans: string; + + if (spansArg.getText() === "test.ranges()") { + spans = "f.Ranges()"; + } else if (ts.isArrayLiteralExpression(spansArg)) { + // Handle array of ranges + const rangeElements: string[] = []; + for (const elem of spansArg.elements) { + const rangeArg = parseBaselineMarkerOrRangeArg(elem); + if (!rangeArg) { + return undefined; + } + rangeElements.push(rangeArg); + } + spans = `[]fourslash.Range{${rangeElements.join(", ")}}`; + } else { + console.error(`Unrecognized spans argument in verify.outliningSpansInCurrentFile: ${spansArg.getText()}`); + return undefined; + } + + // Optional second argument for kind filter + let outliningKind: string | undefined; + if (args.length > 1) { + const kindArg = getStringLiteralLike(args[1]); + if (!kindArg) { + console.error(`Expected string literal for outlining kind, got ${args[1].getText()}`); + return undefined; + } + switch (kindArg.text) { + case "comment": + case "region": + case "code": + case "imports": + outliningKind = `PtrTo("${kindArg.text}")`; + break; + default: + console.error(`Unknown outlining kind: ${kindArg.text}`); + return undefined; + } + } + + return [{ + kind: "verifyOutliningSpans", + spans, + outliningKind, + }]; +} + function parseKind(expr: ts.Expression): string | undefined { if (!ts.isStringLiteral(expr)) { console.error(`Expected string literal for kind, got ${expr.getText()}`); @@ -1649,6 +1707,12 @@ interface VerifyRenameInfoCmd { preferences: string; } +interface VerifyOutliningSpansCmd { + kind: "verifyOutliningSpans"; + spans: string; // Go array of ranges + outliningKind?: string; // "comment" | "region" | "code" | "imports" +} + type Cmd = | VerifyCompletionsCmd | VerifyApplyCodeActionFromCompletionCmd @@ -1662,7 +1726,13 @@ type Cmd = | EditCmd | VerifyQuickInfoCmd | VerifyBaselineRenameCmd - | VerifyRenameInfoCmd; + | VerifyRenameInfoCmd + | VerifyOutliningSpansCmd; + +function generateVerifyOutliningSpans({ spans, outliningKind }: VerifyOutliningSpansCmd): string { + const kindArg = outliningKind ? `, ${outliningKind}` : ""; + return `f.VerifyOutliningSpans(t, ${spans}${kindArg})`; +} function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string { let expectedList: string; @@ -1795,6 +1865,8 @@ function generateCmd(cmd: Cmd): string { return `f.VerifyRenameSucceeded(t, ${cmd.preferences})`; case "renameInfoFailed": return `f.VerifyRenameFailed(t, ${cmd.preferences})`; + case "verifyOutliningSpans": + return generateVerifyOutliningSpans(cmd); default: let neverCommand: never = cmd; throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`); diff --git a/internal/ls/folding.go b/internal/ls/folding.go index aa11617304..c4382899d5 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -78,9 +78,9 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { regions := make([]*lsproto.FoldingRange, 0, 40) out := make([]*lsproto.FoldingRange, 0, 40) - lineStarts := scanner.GetLineStarts(sourceFile) + lineStarts := scanner.GetECMALineStarts(sourceFile) for _, currentLineStart := range lineStarts { - lineEnd := scanner.GetLineEndOfPosition(sourceFile, int(currentLineStart)) + lineEnd := getLineEndOfPosition(sourceFile, int(currentLineStart)) lineText := sourceFile.Text()[currentLineStart:lineEnd] result := parseRegionDelimiter(lineText) if result == nil || isInComment(sourceFile, int(currentLineStart), astnav.GetTokenAtPosition(sourceFile, int(currentLineStart))) != nil { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 4156924dab..e8393245a7 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -490,9 +490,9 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSelectionRangeInfo, (*Server).handleSelectionRange) + registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol) registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve) - registerRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) return handlers }) @@ -822,11 +822,8 @@ func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.La ) } -func (s *Server) handleFoldingRange(ctx context.Context, params *lsproto.FoldingRangeParams) (lsproto.FoldingRangeResponse, error) { - project := s.projectService.EnsureDefaultProjectForURI(params.TextDocument.Uri) - languageService, done := project.GetLanguageServiceForRequest(ctx) - defer done() - return languageService.ProvideFoldingRange(ctx, params.TextDocument.Uri) +func (s *Server) handleFoldingRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.FoldingRangeParams) (lsproto.FoldingRangeResponse, error) { + return ls.ProvideFoldingRange(ctx, params.TextDocument.Uri) } func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) { diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index b2523919aa..a7572d726a 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -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 1feaf18e5e..9c8881f382 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2441,25 +2441,25 @@ func GetECMALineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) ( return line, character } -func GetLineEndOfPosition(sourceFile ast.SourceFileLike, pos int) int { - line, _ := GetLineAndCharacterOfPosition(sourceFile, pos) - lineStarts := GetLineStarts(sourceFile) - - var lastCharPos int - if line+1 >= len(lineStarts) { - 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 && len(fullText) != lastCharPos && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { - return lastCharPos - 1 - } else { - return lastCharPos - } -} +// func GetLineEndOfPosition(sourceFile ast.SourceFileLike, pos int) int { +// line, _ := GetLineAndCharacterOfPosition(sourceFile, pos) +// lineStarts := GetLineStarts(sourceFile) + +// var lastCharPos int +// if line+1 >= len(lineStarts) { +// 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 && len(fullText) != lastCharPos && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { +// return lastCharPos - 1 +// } else { +// return lastCharPos +// } +// } func GetECMAEndLinePosition(sourceFile *ast.SourceFile, line int) int { pos := int(GetECMALineStarts(sourceFile)[line]) From 633bc8b70c1bebf411e5de58d4a622bb0bebb112 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 25 Nov 2025 10:31:04 -0800 Subject: [PATCH 15/19] adding fourslash tests --- .../fourslash/_scripts/convertFourslash.mts | 81 +- internal/fourslash/_scripts/manualTests.txt | 1 + internal/fourslash/fourslash.go | 52 + ...ssionsDontCrashGettingOutlineSpans_test.go | 23 + .../getOutliningForArrayDestructuring_test.go | 56 + .../gen/getOutliningForBlockComments_test.go | 347 ++++ ...getOutliningForObjectDestructuring_test.go | 50 + .../gen/getOutliningForObjectsInArray_test.go | 64 + .../getOutliningForSingleLineComments_test.go | 100 ++ .../gen/getOutliningForTupleType_test.go | 31 + .../gen/getOutliningForTypeLiteral_test.go | 36 + ...getOutliningSpansDepthChainedCalls_test.go | 126 ++ .../gen/getOutliningSpansDepthElseIf_test.go | 99 ++ .../gen/getOutliningSpansForComments_test.go | 31 + .../gen/getOutliningSpansForImports_test.go | 32 + ...ngSpansForRegionsNoSingleLineFolds_test.go | 31 + .../gen/getOutliningSpansForRegions_test.go | 64 + ...etOutliningSpansForTemplateLiteral_test.go | 32 + ...tliningSpansForUnbalancedEndRegion_test.go | 23 + ...tOutliningSpansForUnbalancedRegion_test.go | 23 + .../tests/gen/getOutliningSpans_test.go | 140 ++ .../gen/incrementalParsingWithJsDoc_test.go | 25 + ...pansBlockCommentsWithoutStatements_test.go | 19 + ...ailingBlockCommentsAfterStatements_test.go | 20 + ...ForNonCompleteInterfaceDeclaration_test.go | 17 + .../gen/outliningSpansForArguments_test.go | 30 + ...outliningSpansForArrowFunctionBody_test.go | 27 + .../gen/outliningSpansForFunction_test.go | 96 ++ ...gSpansForImportAndExportAttributes_test.go | 60 + ...outliningSpansForImportsAndExports_test.go | 60 + ...ingSpansForParenthesizedExpression_test.go | 45 + .../gen/outliningSpansSwitchCases_test.go | 49 + .../outliningHintSpansForFunction_test.go | 28 + internal/ls/folding.go | 12 +- internal/ls/folding_test.go | 1446 ----------------- internal/scanner/scanner.go | 20 - 36 files changed, 1884 insertions(+), 1512 deletions(-) create mode 100644 internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForBlockComments_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForTupleType_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForComments_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForImports_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_test.go create mode 100644 internal/fourslash/tests/gen/getOutliningSpans_test.go create mode 100644 internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go create mode 100644 internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go create mode 100644 internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go create mode 100644 internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansForArguments_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansForFunction_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go create mode 100644 internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go create mode 100644 internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go delete mode 100644 internal/ls/folding_test.go diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 9a74127984..979ca8823d 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -209,6 +209,7 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined { case "renameInfoFailed": return parseRenameInfo(func.text, callExpression.arguments); case "outliningSpansInCurrentFile": + case "outliningHintSpansInCurrentFile": return parseOutliningSpansArgs(callExpression.arguments); } } @@ -260,14 +261,26 @@ function parseEditStatement(funcName: string, args: readonly ts.Expression[]): E const arg = args[0]; if (args[0]) { let arg0; - if (!(arg0 = getNumericLiteral(arg))) { - console.error(`Expected numeric literal argument in edit.backspace, got ${arg.getText()}`); - return undefined; + if (arg0 = getNumericLiteral(arg)) { + return { + kind: "edit", + goStatement: `f.Backspace(t, ${arg0.text})`, + }; + } + // Handle test.marker("").position + if (ts.isPropertyAccessExpression(arg) && arg.name.text === "position") { + const expr = arg.expression; + if (ts.isCallExpression(expr) && expr.expression.getText() === "test.marker") { + if (expr.arguments.length === 1 && ts.isStringLiteralLike(expr.arguments[0])) { + return { + kind: "edit", + goStatement: `f.Backspace(t, 1)`, + }; + } + } } - return { - kind: "edit", - goStatement: `f.Backspace(t, ${arg0.text})`, - }; + console.error(`Expected numeric literal or a test marker argument in edit.backspace, got ${arg.getText()}`); + return undefined; } return { kind: "edit", @@ -1447,30 +1460,9 @@ function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutlini return undefined; } - // First argument should be spans array - const spansArg = args[0]; - let spans: string; - - if (spansArg.getText() === "test.ranges()") { - spans = "f.Ranges()"; - } else if (ts.isArrayLiteralExpression(spansArg)) { - // Handle array of ranges - const rangeElements: string[] = []; - for (const elem of spansArg.elements) { - const rangeArg = parseBaselineMarkerOrRangeArg(elem); - if (!rangeArg) { - return undefined; - } - rangeElements.push(rangeArg); - } - spans = `[]fourslash.Range{${rangeElements.join(", ")}}`; - } else { - console.error(`Unrecognized spans argument in verify.outliningSpansInCurrentFile: ${spansArg.getText()}`); - return undefined; - } - + let spans: string = ""; // Optional second argument for kind filter - let outliningKind: string | undefined; + let foldingRangeKind: string | undefined; if (args.length > 1) { const kindArg = getStringLiteralLike(args[1]); if (!kindArg) { @@ -1479,13 +1471,18 @@ function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutlini } switch (kindArg.text) { case "comment": - case "region": - case "code": + foldingRangeKind = "lsproto.FoldingRangeKindComment"; + break; + case "region": + foldingRangeKind = "lsproto.FoldingRangeKindRegion"; + break; case "imports": - outliningKind = `PtrTo("${kindArg.text}")`; + foldingRangeKind = "lsproto.FoldingRangeKindImports"; + break; + case "code": break; default: - console.error(`Unknown outlining kind: ${kindArg.text}`); + console.error(`Unknown folding range kind: ${kindArg.text}`); return undefined; } } @@ -1493,7 +1490,7 @@ function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutlini return [{ kind: "verifyOutliningSpans", spans, - outliningKind, + foldingRangeKind, }]; } @@ -1708,9 +1705,9 @@ interface VerifyRenameInfoCmd { } interface VerifyOutliningSpansCmd { - kind: "verifyOutliningSpans"; - spans: string; // Go array of ranges - outliningKind?: string; // "comment" | "region" | "code" | "imports" + kind: "verifyOutliningSpans" + spans: string; + foldingRangeKind?: string; } type Cmd = @@ -1729,9 +1726,11 @@ type Cmd = | VerifyRenameInfoCmd | VerifyOutliningSpansCmd; -function generateVerifyOutliningSpans({ spans, outliningKind }: VerifyOutliningSpansCmd): string { - const kindArg = outliningKind ? `, ${outliningKind}` : ""; - return `f.VerifyOutliningSpans(t, ${spans}${kindArg})`; +function generateVerifyOutliningSpans({ foldingRangeKind }: VerifyOutliningSpansCmd): string { + if (foldingRangeKind) { + return `f.VerifyOutliningSpans(t, ${foldingRangeKind})`; + } + return `f.VerifyOutliningSpans(t)`; } function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string { diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 492d95e520..bc9430a187 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -5,3 +5,4 @@ completionsWithDeprecatedTag4 renameDefaultKeyword renameForDefaultExport01 tsxCompletion12 +OutliningHintSpansForFunction \ No newline at end of file diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 1b78246b62..0004b1ec73 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1189,6 +1189,58 @@ func (f *FourslashTest) VerifyBaselineGoToTypeDefinition( } } +func (f *FourslashTest) VerifyOutliningSpans(t *testing.T, foldingRangeKind ...lsproto.FoldingRangeKind) { + params := &lsproto.FoldingRangeParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + } + resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentFoldingRangeInfo, params) + if resMsg == nil { + t.Fatalf("Nil response received for folding range request") + } + if !resultOk { + t.Fatalf("Unexpected response type for folding range request: %T", resMsg.AsResponse().Result) + } + + // Extract actual folding ranges from the result and filter by kind if specified + var actualRanges []*lsproto.FoldingRange + if result.FoldingRanges != nil { + actualRanges = *result.FoldingRanges + } + if len(foldingRangeKind) > 0 { + targetKind := foldingRangeKind[0] + var filtered []*lsproto.FoldingRange + for _, r := range actualRanges { + if r.Kind != nil && *r.Kind == targetKind { + filtered = append(filtered, r) + } + } + actualRanges = filtered + } + + if len(actualRanges) != len(f.Ranges()) { + t.Fatalf("verifyOutliningSpans failed - expected total spans to be %d, but was %d", + len(f.Ranges()), len(actualRanges)) + } + + slices.SortFunc(f.Ranges(), func(a, b *RangeMarker) int { + return lsproto.ComparePositions(a.LSPos(), b.LSPos()) + }) + + for i, expectedRange := range f.Ranges() { + actualRange := actualRanges[i] + startPos := lsproto.Position{Line: actualRange.StartLine, Character: *actualRange.StartCharacter} + endPos := lsproto.Position{Line: actualRange.EndLine, Character: *actualRange.EndCharacter} + + if lsproto.ComparePositions(startPos, expectedRange.LSRange.Start) != 0 || + lsproto.ComparePositions(endPos, expectedRange.LSRange.End) != 0 { + t.Fatalf("verifyOutliningSpans failed - span %d has invalid positions: start (%d,%d), end (%d,%d)", + i+1, actualRange.StartLine, *actualRange.StartCharacter, actualRange.EndLine, *actualRange.EndCharacter) + } + } +} + func (f *FourslashTest) VerifyBaselineHover(t *testing.T) { markersAndItems := core.MapFiltered(f.Markers(), func(marker *Marker) (markerAndItem[*lsproto.Hover], bool) { if marker.Name == nil { diff --git a/internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go b/internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go new file mode 100644 index 0000000000..80921fdca2 --- /dev/null +++ b/internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go @@ -0,0 +1,23 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestCorreuptedTryExpressionsDontCrashGettingOutlineSpans(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `try[| { + var x = [ + {% try[||] %}|][|{% except %}|] + ] +} catch (e)[| { + +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go b/internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go new file mode 100644 index 0000000000..548b7778da --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go @@ -0,0 +1,56 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForArrayDestructuring(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const[| [ + a, + b, + c +]|] =[| [ + 1, + 2, + 3 +]|]; +const[| [ + [|[ + [|[ + [|[ + a, + b, + c + ]|] + ]|] + ]|], + [|[ + a1, + b1, + c1 + ]|] +]|] =[| [ + [|[ + [|[ + [|[ + 1, + 2, + 3 + ]|] + ]|] + ]|], + [|[ + 1, + 2, + 3 + ]|] +]|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForBlockComments_test.go b/internal/fourslash/tests/gen/getOutliningForBlockComments_test.go new file mode 100644 index 0000000000..b9b506b0e8 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForBlockComments_test.go @@ -0,0 +1,347 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForBlockComments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|/* + 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 + */|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go b/internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go new file mode 100644 index 0000000000..2d8a8c48a8 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go @@ -0,0 +1,50 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForObjectDestructuring(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `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 +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go b/internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go new file mode 100644 index 0000000000..f19fd06936 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go @@ -0,0 +1,64 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForObjectsInArray(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const x =[| [ + [|{ a: 0 }|], + [|{ b: 1 }|], + [|{ c: 2 }|] +]|]; + +const y =[| [ + [|{ + a: 0 + }|], + [|{ + b: 1 + }|], + [|{ + c: 2 + }|] +]|]; + +const w =[| [ + [|[ 0 ]|], + [|[ 1 ]|], + [|[ 2 ]|] +]|]; + +const z =[| [ + [|[ + 0 + ]|], + [|[ + 1 + ]|], + [|[ + 2 + ]|] +]|]; + +const z =[| [ + [|[ + [|{ hello: 0 }|] + ]|], + [|[ + [|{ hello: 3 }|] + ]|], + [|[ + [|{ hello: 5 }|], + [|{ hello: 7 }|] + ]|] +]|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go b/internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go new file mode 100644 index 0000000000..0124c74201 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go @@ -0,0 +1,100 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForSingleLineComments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|// 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)[| { + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForTupleType_test.go b/internal/fourslash/tests/gen/getOutliningForTupleType_test.go new file mode 100644 index 0000000000..8ae3491ed7 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForTupleType_test.go @@ -0,0 +1,31 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForTupleType(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type A =[| [ + number, + number, + number +]|] + +type B =[| [ + [|[ + [|[ + number, + number, + number + ]|] + ]|] +]|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go b/internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go new file mode 100644 index 0000000000..d15f89b5d5 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go @@ -0,0 +1,36 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForTypeLiteral(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type A =[| { + a: number; +}|] + +type B =[| { + a:[| { + a1:[| { + a2:[| { + x: number; + y: number; + }|] + }|] + }|], + b:[| { + x: number; + }|], + c:[| { + x: number; + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go b/internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go new file mode 100644 index 0000000000..5d270b34bb --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go @@ -0,0 +1,126 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansDepthChainedCalls(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `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 + }|])|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go b/internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go new file mode 100644 index 0000000000..b7e3068686 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go @@ -0,0 +1,99 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansDepthElseIf(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `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; +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go b/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go new file mode 100644 index 0000000000..4175c1fd25 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go @@ -0,0 +1,31 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForComments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|/* + 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";` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindComment) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForImports_test.go b/internal/fourslash/tests/gen/getOutliningSpansForImports_test.go new file mode 100644 index 0000000000..8bd56be4a0 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForImports_test.go @@ -0,0 +1,32 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForImports(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|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");|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindImports) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go b/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go new file mode 100644 index 0000000000..2c2300c338 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go @@ -0,0 +1,31 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForRegionsNoSingleLineFolds(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|//#region +function foo()[| { + +}|] +[|//these +//should|] +//#endregion not you|] +[|// be +// together|] + +[|//#region bla bla bla + +function bar()[| { }|] + +//#endregion|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go b/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go new file mode 100644 index 0000000000..6e4d0a46c7 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go @@ -0,0 +1,64 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForRegions(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// 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 + +// region delimiters not valid when in multiline comment +/* +// #region invalid2 +*/ + +/* +// #endregion +*/` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go b/internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go new file mode 100644 index 0000000000..d6a03f07e7 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go @@ -0,0 +1,32 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForTemplateLiteral(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `declare function tag(...args: any[]): void +const a = [|` + "`" + `signal line` + "`" + `|] +const b = [|` + "`" + `multi +line` + "`" + `|] +const c = tag[|` + "`" + `signal line` + "`" + `|] +const d = tag[|` + "`" + `multi +line` + "`" + `|] +const e = [|` + "`" + `signal ${1} line` + "`" + `|] +const f = [|` + "`" + `multi +${1} +line` + "`" + `|] +const g = tag[|` + "`" + `signal ${1} line` + "`" + `|] +const h = tag[|` + "`" + `multi +${1} +line` + "`" + `|] +const i = ` + "`" + `` + "`" + `` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_test.go b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_test.go new file mode 100644 index 0000000000..b4da80fd87 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_test.go @@ -0,0 +1,23 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForUnbalancedEndRegion(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// bottom-heavy region balance +[|// #region matched + +// #endregion matched|] + +// #endregion unmatched` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_test.go b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_test.go new file mode 100644 index 0000000000..69d2abffcf --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_test.go @@ -0,0 +1,23 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForUnbalancedRegion(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// top-heavy region balance +// #region unmatched + +[|// #region matched + +// #endregion matched|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpans_test.go b/internal/fourslash/tests/gen/getOutliningSpans_test.go new file mode 100644 index 0000000000..eddead57ed --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpans_test.go @@ -0,0 +1,140 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpans(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// 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); + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go b/internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go new file mode 100644 index 0000000000..35e3b9e520 --- /dev/null +++ b/internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go @@ -0,0 +1,25 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestIncrementalParsingWithJsDoc(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|import a from 'a/aaaaaaa/aaaaaaa/aaaaaa/aaaaaaa'; +/**/import b from 'b'; +import c from 'c';|] + +[|/** @internal */|] +export class LanguageIdentifier[| { }|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) + f.GoToMarker(t, "") + f.Backspace(t, 1) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go b/internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go new file mode 100644 index 0000000000..546f7aa02e --- /dev/null +++ b/internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go @@ -0,0 +1,19 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutlineSpansBlockCommentsWithoutStatements(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|/* +/ * Some text + */|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go b/internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go new file mode 100644 index 0000000000..fe9047aed1 --- /dev/null +++ b/internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go @@ -0,0 +1,20 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutlineSpansTrailingBlockCommentsAfterStatements(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `console.log(0); +[|/* +/ * Some text + */|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go b/internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go new file mode 100644 index 0000000000..33517cef01 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go @@ -0,0 +1,17 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningForNonCompleteInterfaceDeclaration(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `interface I[||]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForArguments_test.go b/internal/fourslash/tests/gen/outliningSpansForArguments_test.go new file mode 100644 index 0000000000..c60a256de7 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForArguments_test.go @@ -0,0 +1,30 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForArguments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `console.log(123, 456)l; +console.log( +); +console.log[|( + 123, 456 +)|]; +console.log[|( + 123, + 456 +)|]; +() =>[| console.log[|( + 123, + 456 +)|]|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go b/internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go new file mode 100644 index 0000000000..86a492a098 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go @@ -0,0 +1,27 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForArrowFunctionBody(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `() => 42; +() => ( 42 ); +() =>[| { + 42 +}|]; +() => [|( + 42 +)|]; +() =>[| "foo" + + "bar" + + "baz"|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForFunction_test.go b/internal/fourslash/tests/gen/outliningSpansForFunction_test.go new file mode 100644 index 0000000000..6b72692a70 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForFunction_test.go @@ -0,0 +1,96 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForFunction(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|( + 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) => { + + }|] +)|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go b/internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go new file mode 100644 index 0000000000..31b8eb76f1 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go @@ -0,0 +1,60 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForImportAndExportAttributes(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `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"; +;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go b/internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go new file mode 100644 index 0000000000..724db92ffc --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go @@ -0,0 +1,60 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForImportsAndExports(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `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"; +;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go b/internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go new file mode 100644 index 0000000000..bc037cb518 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go @@ -0,0 +1,45 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForParenthesizedExpression(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const a = [|( + true + ? true + : false + ? true + : false +)|]; + +const b = ( 1 ); + +const c = [|( + 1 +)|]; + +( 1 ); + +[|( + [|( + [|( + 1 + )|] + )|] +)|]; + +[|( + [|( + ( 1 ) + )|] +)|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go b/internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go new file mode 100644 index 0000000000..761a78296c --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go @@ -0,0 +1,49 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansSwitchCases(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `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);|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go b/internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go new file mode 100644 index 0000000000..761cb0a2d5 --- /dev/null +++ b/internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go @@ -0,0 +1,28 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningHintSpansForFunction(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `namespace NS[| { + function f(x: number, y: number)[| { + return x + y; + }|] + + function g[|( + x: number, + y: number, + ): number { + return x + y; + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/ls/folding.go b/internal/ls/folding.go index c4382899d5..cbdfc8d5d2 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -124,7 +124,7 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L // cancellationToken.throwIfCancellationRequested(); foldingRange := make([]*lsproto.FoldingRange, 0, 40) // !!! 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 { + if (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) { @@ -324,9 +324,10 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag } else if tryStatement.FinallyBlock == n { node := findChildOfKind(n.Parent, ast.KindFinallyKeyword, sourceFile) if node != nil { - return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNode(node, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) } } + fallthrough default: // Block was a standalone block. In this case we want to only collapse // the span of the block, independent of any parent span. @@ -366,11 +367,12 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { var elements *ast.NodeList - if node.Kind == ast.KindNamedImports { + switch node.Kind { + case ast.KindNamedImports: elements = node.AsNamedImports().Elements - } else if node.Kind == ast.KindNamedExports { + case ast.KindNamedExports: elements = node.AsNamedExports().Elements - } else if node.Kind == ast.KindImportAttributes { + case ast.KindImportAttributes: elements = node.AsImportAttributes().Attributes } if elements == nil { diff --git a/internal/ls/folding_test.go b/internal/ls/folding_test.go deleted file mode 100644 index bfe237e2ee..0000000000 --- a/internal/ls/folding_test.go +++ /dev/null @@ -1,1446 +0,0 @@ -package ls_test - -import ( - "cmp" - "slices" - "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]string{ - testData.Files[0].FileName(): testData.Files[0].Content, - }) - defer done() - - foldingRanges, _ := service.ProvideFoldingRange(ctx, ls.FileNameToDocumentURI("/file1.ts")) - ranges := *foldingRanges.FoldingRanges - if len(ranges) != len(markerPositions) { - t.Fatalf("Expected %d folding ranges, got %d", len(markerPositions), len(ranges)) - } - 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 cmp.Compare(a.LSRange.End.Character, b.LSRange.End.Character) - }) - for i, marker := range markerPositions { - assert.DeepEqual(t, marker.LSRange.Start.Line, ranges[i].StartLine) - assert.DeepEqual(t, marker.LSRange.End.Line, ranges[i].EndLine) - assert.DeepEqual(t, marker.LSRange.Start.Character, *ranges[i].StartCharacter) - assert.DeepEqual(t, marker.LSRange.End.Character, *ranges[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); -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 in between - - // 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/scanner/scanner.go b/internal/scanner/scanner.go index 9c8881f382..1ee64b62f1 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2441,26 +2441,6 @@ func GetECMALineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) ( return line, character } -// func GetLineEndOfPosition(sourceFile ast.SourceFileLike, pos int) int { -// line, _ := GetLineAndCharacterOfPosition(sourceFile, pos) -// lineStarts := GetLineStarts(sourceFile) - -// var lastCharPos int -// if line+1 >= len(lineStarts) { -// 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 && len(fullText) != lastCharPos && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { -// return lastCharPos - 1 -// } else { -// return lastCharPos -// } -// } - func GetECMAEndLinePosition(sourceFile *ast.SourceFile, line int) int { pos := int(GetECMALineStarts(sourceFile)[line]) for { From 927e872930e6ddbbca1fa6dd8a836bb6453f3e1f Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 1 Dec 2025 22:49:00 -0800 Subject: [PATCH 16/19] addressing pr comments --- .../fourslash/_scripts/convertFourslash.mts | 32 +++---- internal/fourslash/_scripts/manualTests.txt | 5 +- internal/fourslash/fourslash.go | 10 +-- .../gen/getOutliningSpansForComments_test.go | 1 + ...ngSpansForRegionsNoSingleLineFolds_test.go | 1 + .../gen/getOutliningSpansForRegions_test.go | 1 + .../{gen => manual}/getOutliningSpans_test.go | 2 +- .../incrementalParsingWithJsDoc_test.go | 1 - ...ForNonCompleteInterfaceDeclaration_test.go | 2 +- internal/ls/folding.go | 84 +++++++++++-------- 10 files changed, 72 insertions(+), 67 deletions(-) rename internal/fourslash/tests/{gen => manual}/getOutliningSpans_test.go (98%) rename internal/fourslash/tests/{gen => manual}/incrementalParsingWithJsDoc_test.go (99%) rename internal/fourslash/tests/{gen => manual}/outliningForNonCompleteInterfaceDeclaration_test.go (91%) diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 003c547ff5..a0f177d8de 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -277,26 +277,14 @@ function parseEditStatement(funcName: string, args: readonly ts.Expression[]): E const arg = args[0]; if (args[0]) { let arg0; - if (arg0 = getNumericLiteral(arg)) { - return { - kind: "edit", - goStatement: `f.Backspace(t, ${arg0.text})`, - }; - } - // Handle test.marker("").position - if (ts.isPropertyAccessExpression(arg) && arg.name.text === "position") { - const expr = arg.expression; - if (ts.isCallExpression(expr) && expr.expression.getText() === "test.marker") { - if (expr.arguments.length === 1 && ts.isStringLiteralLike(expr.arguments[0])) { - return { - kind: "edit", - goStatement: `f.Backspace(t, 1)`, - }; - } - } + if (!(arg0 = getNumericLiteral(arg))) { + console.error(`Expected numeric literal argument in edit.backspace, got ${arg.getText()}`); + return undefined; } - console.error(`Expected numeric literal or a test marker argument in edit.backspace, got ${arg.getText()}`); - return undefined; + return { + kind: "edit", + goStatement: `f.Backspace(t, ${arg0.text})`, + }; } return { kind: "edit", @@ -2182,7 +2170,7 @@ interface VerifyNavToCmd { } interface VerifyOutliningSpansCmd { - kind: "verifyOutliningSpans" + kind: "verifyOutliningSpans"; spans: string; foldingRangeKind?: string; } @@ -2205,11 +2193,11 @@ type Cmd = | VerifyBaselineInlayHintsCmd | VerifyImportFixAtPositionCmd | VerifyDiagnosticsCmd - | VerifyBaselineDiagnosticsCmd; + | VerifyBaselineDiagnosticsCmd | VerifyOutliningSpansCmd; function generateVerifyOutliningSpans({ foldingRangeKind }: VerifyOutliningSpansCmd): string { - if (foldingRangeKind) { + if (foldingRangeKind) { return `f.VerifyOutliningSpans(t, ${foldingRangeKind})`; } return `f.VerifyOutliningSpans(t)`; diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 756cbefa69..532834c239 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -13,4 +13,7 @@ quickInfoForOverloadOnConst1 renameDefaultKeyword renameForDefaultExport01 tsxCompletion12 -OutliningHintSpansForFunction \ No newline at end of file +outliningHintSpansForFunction +getOutliningSpans +outliningForNonCompleteInterfaceDeclaration +incrementalParsingWithJsDoc \ No newline at end of file diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index d7392ce508..e1d3ea45ac 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1457,9 +1457,7 @@ func (f *FourslashTest) VerifyOutliningSpans(t *testing.T, foldingRangeKind ...l // Extract actual folding ranges from the result and filter by kind if specified var actualRanges []*lsproto.FoldingRange - if result.FoldingRanges != nil { - actualRanges = *result.FoldingRanges - } + actualRanges = *result.FoldingRanges if len(foldingRangeKind) > 0 { targetKind := foldingRangeKind[0] var filtered []*lsproto.FoldingRange @@ -1487,8 +1485,10 @@ func (f *FourslashTest) VerifyOutliningSpans(t *testing.T, foldingRangeKind ...l if lsproto.ComparePositions(startPos, expectedRange.LSRange.Start) != 0 || lsproto.ComparePositions(endPos, expectedRange.LSRange.End) != 0 { - t.Fatalf("verifyOutliningSpans failed - span %d has invalid positions: start (%d,%d), end (%d,%d)", - i+1, actualRange.StartLine, *actualRange.StartCharacter, actualRange.EndLine, *actualRange.EndCharacter) + t.Fatalf("verifyOutliningSpans failed - span %d has invalid positions:\n actual: start (%d,%d), end (%d,%d)\n expected: start (%d,%d), end (%d,%d)", + i+1, + actualRange.StartLine, *actualRange.StartCharacter, actualRange.EndLine, *actualRange.EndCharacter, + expectedRange.LSRange.Start.Line, expectedRange.LSRange.Start.Character, expectedRange.LSRange.End.Line, expectedRange.LSRange.End.Character) } } } diff --git a/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go b/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go index 4175c1fd25..9563a425e9 100644 --- a/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go +++ b/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go @@ -27,5 +27,6 @@ declare module "m"; // line 4|] declare module "n";` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.MarkTestAsStradaServer() f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindComment) } diff --git a/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go b/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go index 2c2300c338..1fcd3b24be 100644 --- a/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go +++ b/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go @@ -27,5 +27,6 @@ function bar()[| { }|] //#endregion|]` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.MarkTestAsStradaServer() f.VerifyOutliningSpans(t) } diff --git a/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go b/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go index 6e4d0a46c7..fc8eb75b01 100644 --- a/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go +++ b/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go @@ -60,5 +60,6 @@ test // #endregion // #endregion */` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.MarkTestAsStradaServer() f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) } diff --git a/internal/fourslash/tests/gen/getOutliningSpans_test.go b/internal/fourslash/tests/manual/getOutliningSpans_test.go similarity index 98% rename from internal/fourslash/tests/gen/getOutliningSpans_test.go rename to internal/fourslash/tests/manual/getOutliningSpans_test.go index eddead57ed..3b06adf2e7 100644 --- a/internal/fourslash/tests/gen/getOutliningSpans_test.go +++ b/internal/fourslash/tests/manual/getOutliningSpans_test.go @@ -84,7 +84,7 @@ class ClassFooWithTrivia[| /* some comments */ /* more trivia */ { - /*some trailing trivia */ + [|/*some trailing trivia */|] }|] /* even more */ // object literals diff --git a/internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go b/internal/fourslash/tests/manual/incrementalParsingWithJsDoc_test.go similarity index 99% rename from internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go rename to internal/fourslash/tests/manual/incrementalParsingWithJsDoc_test.go index 35e3b9e520..e8aeb4f7f1 100644 --- a/internal/fourslash/tests/gen/incrementalParsingWithJsDoc_test.go +++ b/internal/fourslash/tests/manual/incrementalParsingWithJsDoc_test.go @@ -14,7 +14,6 @@ func TestIncrementalParsingWithJsDoc(t *testing.T) { const content = `[|import a from 'a/aaaaaaa/aaaaaaa/aaaaaa/aaaaaaa'; /**/import b from 'b'; import c from 'c';|] - [|/** @internal */|] export class LanguageIdentifier[| { }|]` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) diff --git a/internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go b/internal/fourslash/tests/manual/outliningForNonCompleteInterfaceDeclaration_test.go similarity index 91% rename from internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go rename to internal/fourslash/tests/manual/outliningForNonCompleteInterfaceDeclaration_test.go index 33517cef01..69e3351747 100644 --- a/internal/fourslash/tests/gen/outliningForNonCompleteInterfaceDeclaration_test.go +++ b/internal/fourslash/tests/manual/outliningForNonCompleteInterfaceDeclaration_test.go @@ -11,7 +11,7 @@ func TestOutliningForNonCompleteInterfaceDeclaration(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") - const content = `interface I[||]` + const content = `interface I` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.VerifyOutliningSpans(t) } diff --git a/internal/ls/folding.go b/internal/ls/folding.go index cbdfc8d5d2..d566660937 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -3,13 +3,13 @@ package ls import ( "cmp" "context" - "regexp" "slices" "strings" "unicode" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" @@ -17,7 +17,7 @@ import ( func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.FoldingRangeResponse, error) { _, sourceFile := l.getProgramAndFile(documentURI) - res := l.addNodeOutliningSpans(sourceFile) + res := l.addNodeOutliningSpans(ctx, sourceFile) res = append(res, l.addRegionOutliningSpans(sourceFile)...) slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int { if a.StartLine != b.StartLine { @@ -37,7 +37,7 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil } -func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { +func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { depthRemaining := 40 current := 0 @@ -46,7 +46,7 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l foldingRange := make([]*lsproto.FoldingRange, 0, 40) for current < n { for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) { - foldingRange = append(foldingRange, visitNode(statements.Nodes[current], depthRemaining, sourceFile, l)...) + foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...) current++ } if current == n { @@ -54,7 +54,7 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.SourceFile) []*l } firstImport := current for current < n && ast.IsAnyImportSyntax(statements.Nodes[current]) { - foldingRange = append(foldingRange, visitNode(statements.Nodes[current], depthRemaining, sourceFile, l)...) + foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...) current++ } lastImport := current - 1 @@ -71,7 +71,7 @@ func (l *LanguageService) addNodeOutliningSpans(sourceFile *ast.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)...) + foldingRange = append(foldingRange, visitNode(ctx, sourceFile.EndOfFileToken, depthRemaining, sourceFile, l)...) return foldingRange } @@ -117,29 +117,30 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) [] return out } -func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { +func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { if depthRemaining == 0 { return nil } - // cancellationToken.throwIfCancellationRequested(); + if ctx.Err() != nil { + return nil + } foldingRange := make([]*lsproto.FoldingRange, 0, 40) - // !!! remove !ast.IsBinaryExpression(n) after JSDoc implementation - if (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.IsBinaryExpression(n) && ast.IsDeclaration(n)) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n, sourceFile, l)...) } if ast.IsFunctionLike(n) && n.Parent != nil && ast.IsBinaryExpression(n.Parent) && n.Parent.AsBinaryExpression().Left != nil && ast.IsPropertyAccessExpression(n.Parent.AsBinaryExpression().Left) { - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(n.Parent.AsBinaryExpression().Left, sourceFile, l)...) + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n.Parent.AsBinaryExpression().Left, sourceFile, l)...) } if ast.IsBlock(n) { statements := n.AsBlock().Statements if statements != nil { - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(statements.End(), sourceFile, l)...) + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...) } } if ast.IsModuleBlock(n) { statements := n.AsModuleBlock().Statements if statements != nil { - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(statements.End(), sourceFile, l)...) + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...) } } if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) { @@ -152,7 +153,7 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L members = n.AsInterfaceDeclaration().Members } if members != nil { - foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(members.End(), sourceFile, l)...) + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, members.End(), sourceFile, l)...) } } @@ -164,42 +165,42 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L depthRemaining-- if ast.IsCallExpression(n) { depthRemaining++ - expressionNodes := visitNode(n.Expression(), depthRemaining, sourceFile, l) + expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l) if expressionNodes != nil { foldingRange = append(foldingRange, expressionNodes...) } depthRemaining-- for _, arg := range n.Arguments() { if arg != nil { - foldingRange = append(foldingRange, visitNode(arg, depthRemaining, sourceFile, l)...) + foldingRange = append(foldingRange, visitNode(ctx, arg, depthRemaining, sourceFile, l)...) } } typeArguments := n.TypeArguments() for _, typeArg := range typeArguments { if typeArg != nil { - foldingRange = append(foldingRange, visitNode(typeArg, depthRemaining, sourceFile, l)...) + foldingRange = append(foldingRange, visitNode(ctx, typeArg, depthRemaining, sourceFile, l)...) } } } else if ast.IsIfStatement(n) && n.AsIfStatement().ElseStatement != nil && ast.IsIfStatement(n.AsIfStatement().ElseStatement) { // Consider an 'else if' to be on the same depth as the 'if'. ifStatement := n.AsIfStatement() - expressionNodes := visitNode(n.Expression(), depthRemaining, sourceFile, l) + expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l) if expressionNodes != nil { foldingRange = append(foldingRange, expressionNodes...) } - thenNode := visitNode(ifStatement.ThenStatement, depthRemaining, sourceFile, l) + thenNode := visitNode(ctx, ifStatement.ThenStatement, depthRemaining, sourceFile, l) if thenNode != nil { foldingRange = append(foldingRange, thenNode...) } depthRemaining++ - elseNode := visitNode(ifStatement.ElseStatement, depthRemaining, sourceFile, l) + elseNode := visitNode(ctx, ifStatement.ElseStatement, depthRemaining, sourceFile, l) if elseNode != nil { foldingRange = append(foldingRange, elseNode...) } depthRemaining-- } else { visit := func(node *ast.Node) bool { - childNode := visitNode(node, depthRemaining, sourceFile, l) + childNode := visitNode(ctx, node, depthRemaining, sourceFile, l) if childNode != nil { foldingRange = append(foldingRange, childNode...) } @@ -211,14 +212,14 @@ func visitNode(n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *L return foldingRange } -func addOutliningForLeadingCommentsForNode(n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { +func addOutliningForLeadingCommentsForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { if ast.IsJsxText(n) { return nil } - return addOutliningForLeadingCommentsForPos(n.Pos(), sourceFile, l) + return addOutliningForLeadingCommentsForPos(ctx, n.Pos(), sourceFile, l) } -func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { +func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { p := &printer.EmitContext{} foldingRange := make([]*lsproto.FoldingRange, 0, 40) firstSingleLineCommentStart := -1 @@ -238,7 +239,10 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l for comment := range scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceText, pos) { commentPos := comment.Pos() commentEnd := comment.End() - // cancellationToken.throwIfCancellationRequested(); + + if ctx.Err() != nil { + return nil + } switch comment.Kind { case ast.KindSingleLineCommentTrivia: // never fold region delimiters into single-line comment regions @@ -269,7 +273,7 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l singleLineCommentCount = 0 break default: - // Debug.assertNever(kind); + debug.AssertNever(comment.Kind) } } addedComments := combineAndAddMultipleSingleLineComments() @@ -279,8 +283,6 @@ func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l return foldingRange } -var regionDelimiterRegExp = regexp.MustCompile(`^#(end)?region(.*)\r?$`) - type regionDelimiterResult struct { isStart bool name string @@ -294,14 +296,24 @@ func parseRegionDelimiter(lineText string) *regionDelimiterResult { return nil } lineText = strings.TrimSpace(lineText[2:]) - result := regionDelimiterRegExp.FindStringSubmatch(lineText) - if result != nil { - return ®ionDelimiterResult{ - isStart: result[1] == "", - name: strings.TrimSpace(result[2]), - } + lineText = strings.TrimSuffix(lineText, "\r") + if !strings.HasPrefix(lineText, "#") { + return nil + } + lineText = lineText[1:] + isStart := true + if strings.HasPrefix(lineText, "end") { + isStart = false + lineText = lineText[3:] + } + if !strings.HasPrefix(lineText, "region") { + return nil + } + lineText = lineText[6:] + return ®ionDelimiterResult{ + isStart: isStart, + name: strings.TrimSpace(lineText), } - return nil } func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { From 21cc98878ce71e608cb1d36ce81fb8a5056177f2 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 2 Dec 2025 10:01:59 -0800 Subject: [PATCH 17/19] fixing comparison function --- internal/ls/folding.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index d566660937..8f58bc1b0b 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -20,19 +20,10 @@ func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI l res := l.addNodeOutliningSpans(ctx, sourceFile) res = append(res, l.addRegionOutliningSpans(sourceFile)...) slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int { - if a.StartLine != b.StartLine { - return cmp.Compare(a.StartLine, b.StartLine) + if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 { + return c } - if a.StartCharacter != nil && b.StartCharacter != nil && *a.StartCharacter != *b.StartCharacter { - 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 && *a.EndCharacter != *b.EndCharacter { - return cmp.Compare(*a.EndCharacter, *b.EndCharacter) - } - return 0 + return cmp.Compare(*a.StartCharacter, *b.StartCharacter) }) return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil } From 07d51aa85d290c230a09d23885c30c2b320edddc Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:00:34 -0800 Subject: [PATCH 18/19] Porting differences copilot noticed --- internal/ls/folding.go | 80 ++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 8f58bc1b0b..722f26f604 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -52,7 +52,7 @@ func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile if lastImport != firstImport { foldingRangeKind := lsproto.FoldingRangeKindImports foldingRange = append(foldingRange, createFoldingRangeFromBounds( - astnav.GetStartOfNode(findChildOfKind(statements.Nodes[firstImport], + astnav.GetStartOfNode(astnav.FindChildOfKind(statements.Nodes[firstImport], ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/), statements.Nodes[lastImport].End(), foldingRangeKind, @@ -318,16 +318,16 @@ 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, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNodeWithHintSpan(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, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNodeWithHintSpan(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(node, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + finallyKeyword := astnav.FindChildOfKind(n.Parent, ast.KindFinallyKeyword, sourceFile) + if finallyKeyword != nil { + return spanForNodeWithHintSpan(n, finallyKeyword, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) } } fallthrough @@ -337,7 +337,7 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", "") } case ast.KindModuleBlock: - return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNodeWithHintSpan(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, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) case ast.KindTupleType: @@ -378,15 +378,15 @@ func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l * case ast.KindImportAttributes: elements = node.AsImportAttributes().Attributes } - if elements == nil { + if elements == nil || len(elements.Nodes) == 0 { return nil } - openToken := findChildOfKind(node, ast.KindOpenBraceToken, sourceFile) - closeToken := findChildOfKind(node, ast.KindCloseBraceToken, sourceFile) + openToken := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, sourceFile) + closeToken := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, sourceFile) if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } - return rangeBetweenTokens(openToken, closeToken, sourceFile, false /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, node, sourceFile, false /*useFullStart*/, l) } func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -399,16 +399,16 @@ func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, } func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { - if node.AsCallExpression().Arguments == nil { + if node.AsCallExpression().Arguments == nil || len(node.AsCallExpression().Arguments.Nodes) == 0 { return nil } - openToken := findChildOfKind(node, ast.KindOpenParenToken, sourceFile) - closeToken := findChildOfKind(node, ast.KindCloseParenToken, sourceFile) + openToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile) + closeToken := astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile) if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { return nil } - return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(openToken, closeToken, node, sourceFile, true /*useFullStart*/, l) } func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -428,26 +428,17 @@ func spanForTemplateLiteral(node *ast.Node, sourceFile *ast.SourceFile, l *Langu } 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(astnav.GetStartOfNode(openingElement, sourceFile, false /*includeJSDoc*/), 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, "", bannerText.String()) + jsxElement := node.AsJsxElement() + textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxElement.OpeningElement, sourceFile, false /*includeJSDoc*/), jsxElement.ClosingElement.End(), sourceFile) + tagName := jsxElement.OpeningElement.TagName().Text() + bannerText := "<" + tagName + ">..." + return createFoldingRange(textRange, "", bannerText) + } + // JsxFragment + jsxFragment := node.AsJsxFragment() + textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile) + return createFoldingRange(textRange, "", "<>...") } func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { @@ -471,19 +462,24 @@ func spanForNodeArray(statements *ast.NodeList, sourceFile *ast.SourceFile, l *L } func spanForNode(node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + return spanForNodeWithHintSpan(node, node, open, useFullStart, sourceFile, l) +} + +func spanForNodeWithHintSpan(node *ast.Node, hintSpanNode *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { closeBrace := ast.KindCloseBraceToken if open != ast.KindOpenBraceToken { closeBrace = ast.KindCloseBracketToken } - openToken := findChildOfKind(node, open, sourceFile) - closeToken := findChildOfKind(node, closeBrace, sourceFile) + openToken := astnav.FindChildOfKind(node, open, sourceFile) + closeToken := astnav.FindChildOfKind(node, closeBrace, sourceFile) if openToken != nil && closeToken != nil { - return rangeBetweenTokens(openToken, closeToken, sourceFile, useFullStart, l) + return rangeBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, useFullStart, l) } return nil } -func rangeBetweenTokens(openToken *ast.Node, closeToken *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 { + _ = hintSpanNode // hintSpan is used in TypeScript but not needed for LSP FoldingRange var textRange *lsproto.Range if useFullStart { textRange = l.createLspRangeFromBounds(openToken.Pos(), closeToken.End(), sourceFile) @@ -518,21 +514,21 @@ func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind lsproto.Fol 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) + closeToken := astnav.FindChildOfKind(body, ast.KindCloseBraceToken, sourceFile) if openToken != nil && closeToken != nil { - return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) + return rangeBetweenTokens(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) + openParenToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile) if openParenToken != nil { return openParenToken } } - return findChildOfKind(body, ast.KindOpenBraceToken, sourceFile) + return astnav.FindChildOfKind(body, ast.KindOpenBraceToken, sourceFile) } func isNodeArrayMultiLine(list []*ast.Node, sourceFile *ast.SourceFile) bool { From 6a54dbc8a55d92ce108ff07adc949ec9d98b90c1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:13:35 -0800 Subject: [PATCH 19/19] Remove spanForNodeWithHintSpan accidental addition --- internal/ls/folding.go | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/internal/ls/folding.go b/internal/ls/folding.go index 722f26f604..d37548c7c3 100644 --- a/internal/ls/folding.go +++ b/internal/ls/folding.go @@ -318,16 +318,15 @@ 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 spanForNodeWithHintSpan(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 spanForNodeWithHintSpan(n, n.Parent, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) } else if tryStatement.FinallyBlock == n { - finallyKeyword := astnav.FindChildOfKind(n.Parent, ast.KindFinallyKeyword, sourceFile) - if finallyKeyword != nil { - return spanForNodeWithHintSpan(n, finallyKeyword, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + if span := spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l); span != nil { + return span } } fallthrough @@ -337,7 +336,7 @@ func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *Languag return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", "") } case ast.KindModuleBlock: - return spanForNodeWithHintSpan(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, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) case ast.KindTupleType: @@ -386,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 { @@ -408,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 { @@ -462,10 +461,6 @@ func spanForNodeArray(statements *ast.NodeList, sourceFile *ast.SourceFile, l *L } func spanForNode(node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { - return spanForNodeWithHintSpan(node, node, open, useFullStart, sourceFile, l) -} - -func spanForNodeWithHintSpan(node *ast.Node, hintSpanNode *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { closeBrace := ast.KindCloseBraceToken if open != ast.KindOpenBraceToken { closeBrace = ast.KindCloseBracketToken @@ -473,13 +468,12 @@ func spanForNodeWithHintSpan(node *ast.Node, hintSpanNode *ast.Node, open ast.Ki openToken := astnav.FindChildOfKind(node, open, sourceFile) closeToken := astnav.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 { - _ = hintSpanNode // hintSpan is used in TypeScript but not needed for LSP 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) @@ -516,7 +510,7 @@ func functionSpan(node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l openToken := tryGetFunctionOpenToken(node, body, sourceFile) closeToken := astnav.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 }