Skip to content

Conversation

@navya9singh
Copy link
Member

@navya9singh navya9singh commented Jun 30, 2025

This PR implements folding ranges in the lsp. tsserver also had hint spans while lsp doesn't so I have combined those tests into folding tests.

Fixes #2176

Copilot AI review requested due to automatic review settings June 30, 2025 20:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request implements folding ranges support for the LSP by combining tests from tsserver and LSP, adding several utility functions and test cases to handle folding functionality in the compiler codebase. Key changes include:

  • Introducing new folding range support in the language service (internal/ls/folding.go) and integrating it via the LSP server (internal/lsp/server.go).
  • Adding a new helper function (GetLineEndOfPosition) in the scanner and refactoring certain utility functions in the printer.
  • Expanding the test coverage for outlining and folding with extensive tests in internal/ls/folding_test.go.

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/scanner/scanner.go Added GetLineEndOfPosition to compute the end position of a line.
internal/printer/utilities.go Updated and renamed helper functions to use consistent naming conventions.
internal/lsp/server.go Added folding range handling in LSP server.
internal/ls/utilities.go Adjusted LSP range creation using astnav.GetStartOfNode.
internal/ls/folding_test.go Introduced comprehensive tests for folding range functionality.
internal/ls/folding.go Implemented folding range creation and management functions.
internal/ast/utilities.go Added helper isDeclarationKind to classify declaration nodes.
Comments suppressed due to low confidence (1)

internal/scanner/scanner.go:2300

  • [nitpick] The naming of GetLineEndOfPosition is very similar to GetEndLinePosition, which could be confusing. Consider standardizing the naming to more clearly distinguish their roles (e.g., by using a consistent verb pattern).
func GetLineEndOfPosition(sourceFile ast.SourceFileLike, pos int) int {

}

func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
regions := make([]*lsproto.FoldingRange, 0, 40)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, where does the 40 initial capacity come from?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 is the max depth that strada used, but I don't think that we actually need to prealloc these lists.


func addOutliningForLeadingCommentsForPos(pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
p := &printer.EmitContext{}
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect to typically see around 40 ranges for a comment position?

return out
}

func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think thins might turn out better if all of this code were instead methods in on a struct, like foldingRangeCollector, then you don't need to pass everything down so much, the deth can just be there, you can append into a slice there, etc

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I resolved all of the outdated comments.

Most everything else are just refactors or nits. I think this is pretty much ready? The only one I think we should actually fix is the comparison function, then everything else can be done later depending on who has time/bandwidth.

}

func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
regions := make([]*lsproto.FoldingRange, 0, 40)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 is the max depth that strada used, but I don't think that we actually need to prealloc these lists.

@navya9singh
Copy link
Member Author

If you merge from the foldingRangesUpdates branch, that should have adjustments based off of main, and should get you in sync.

The feedback I have in this PR still applies though. Also, I tried the branch out locally and wrote the following (notice the leading newline)

let x = {

}

and hit the following crash:

panic handling request textDocument/foldingRange runtime error: index out of range [-1] goroutine 189 [running]:
runtime/debug.Stack()
	/home/daniel/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25rc1.linux-arm64/src/runtime/debug/stack.go:26 +0x74
github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop.func1.1()
	/home/daniel/typescript-go/internal/lsp/server.go:357 +0x58
panic({0x8fb700?, 0x4004e11008?})
	/home/daniel/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25rc1.linux-arm64/src/runtime/panic.go:783 +0xec
github.com/microsoft/typescript-go/internal/scanner.GetLineEndOfPosition({0xaf8590, 0x4000532008}, 0x0)
	/home/daniel/typescript-go/internal/scanner/scanner.go:2379 +0x1d4
github.com/microsoft/typescript-go/internal/ls.(*LanguageService).addRegionOutliningSpans(0x40064fb1a0, 0x4000532008)
	/home/daniel/typescript-go/internal/ls/folding.go:83 +0x104
github.com/microsoft/typescript-go/internal/ls.(*LanguageService).ProvideFoldingRange(0x40064fb1a0, {0xafa3f0, 0x40001552c0}, {0x4002939a40, 0x13})
	/home/daniel/typescript-go/internal/ls/folding.go:21 +0x90
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleFoldingRange(0x40001b8a20, {0xafa3f0, 0x40001552c0}, 0x4002448ed0)
	/home/daniel/typescript-go/internal/lsp/server.go:697 +0x10c
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleRequestOrNotification(0x40001b8a20, {0xafa3f0, 0x40001552c0}, 0x4002448ed0)
	/home/daniel/typescript-go/internal/lsp/server.go:504 +0x7d8
github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop.func1()
	/home/daniel/typescript-go/internal/lsp/server.go:370 +0xa8
created by github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop in goroutine 34
	/home/daniel/typescript-go/internal/lsp/server.go:388 +0x414

In fact, all you need is a blank file with a newline at the beginning.

This doesn't panic anymore.

@jakebailey
Copy link
Member

This unfortunately crashed on me when I opened into checker.ts:


[Error - 10:32:43 AM] Request textDocument/foldingRange failed.
  Message: InternalError: panic handling request textDocument/foldingRange: Token cache mismatch: parent. Expected parent of kind KindTryStatement, got KindFinallyKeyword
  Code: -32603 
panic handling request textDocument/foldingRange Token cache mismatch: parent. Expected parent of kind KindTryStatement, got KindFinallyKeyword goroutine 978 [running]:
runtime/debug.Stack()
	/usr/local/go/src/runtime/debug/stack.go:26 +0x8e
github.com/microsoft/typescript-go/internal/lsp.(*Server).recover(0xc0001c2008, 0xc0108f2060)
	/home/jabaile/work/TypeScript-go/internal/lsp/server.go:848 +0x53
panic({0xfad5a0?, 0xc011e3c7b0?})
	/usr/local/go/src/runtime/panic.go:783 +0x136
github.com/microsoft/typescript-go/internal/ast.(*SourceFile).GetOrCreateToken(0xc0074e2608, 0x61, 0x10b3d, 0x10b52, 0xc00dafea80)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:10957 +0x505
github.com/microsoft/typescript-go/internal/ls.findChildOfKind(0xc00dafea80, 0x12, 0xc0074e2608)
	/home/jabaile/work/TypeScript-go/internal/ls/utilities.go:217 +0x297
github.com/microsoft/typescript-go/internal/ls.spanForNode(0xc00dafea80, 0x12, 0x1, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:478 +0x65
github.com/microsoft/typescript-go/internal/ls.getOutliningSpanForNode(0xc0011156c8, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:330 +0x5e5
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc0011156c8, 0x1e, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:151 +0xd4b
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc0011156c8)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visit(0xc00ce95840, 0xc0011156c8)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:21 +0x32
github.com/microsoft/typescript-go/internal/ast.(*TryStatement).ForEachChild(0xc0009ae7e0, 0xc00ce95840)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:3484 +0x9c
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc0009ae7e0, 0xc00ce95840)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc0009ae7e0, 0x1f, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc0009ae7e0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visitNodes(0xc00ce94c40, {0xc0015ad7d8, 0x4, 0x4})
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:28 +0x78
github.com/microsoft/typescript-go/internal/ast.visitNodeList(0xc00ce94c40, 0xc0017c9b28)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:37 +0x51
github.com/microsoft/typescript-go/internal/ast.(*Block).ForEachChild(0xc001115728, 0xc00ce94c40)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:3679 +0x32
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc001115728, 0xc00ce94c40)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc001115728, 0x20, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc001115728)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visit(0xc00ce94980, 0xc001115728)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:21 +0x32
github.com/microsoft/typescript-go/internal/ast.(*ArrowFunction).ForEachChild(0xc002e16960, 0xc00ce94980)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:6461 +0x174
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc002e16960, 0xc00ce94980)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc002e16960, 0x21, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc002e16960)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visit(0xc00ce948c0, 0xc002e16960)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:21 +0x32
github.com/microsoft/typescript-go/internal/ast.(*PropertyAssignment).ForEachChild(0xc002e030b0, 0xc00ce948c0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:7406 +0x106
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc002e030b0, 0xc00ce948c0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc002e030b0, 0x22, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc002e030b0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visitNodes(0xc00c3873c0, {0xc002e3e908, 0xae, 0xae})
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:28 +0x78
github.com/microsoft/typescript-go/internal/ast.visitNodeList(0xc00c3873c0, 0xc0017c9be8)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:37 +0x51
github.com/microsoft/typescript-go/internal/ast.(*ObjectLiteralExpression).ForEachChild(0xc000d485a0, 0xc00c3873c0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:7308 +0x32
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc000d485a0, 0xc00c3873c0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc000d485a0, 0x23, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc000d485a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visit(0xc00c387200, 0xc000d485a0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:21 +0x32
github.com/microsoft/typescript-go/internal/ast.(*VariableDeclaration).ForEachChild(0xc0026fab68, 0xc00c387200)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:3776 +0xd1
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc0026fab68, 0xc00c387200)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc0026fab68, 0x24, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc0026fab68)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visitNodes(0xc00c387180, {0xc002e3ee78, 0x1, 0x1})
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:28 +0x78
github.com/microsoft/typescript-go/internal/ast.visitNodeList(0xc00c387180, 0xc0017c9c08)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:37 +0x51
github.com/microsoft/typescript-go/internal/ast.(*VariableDeclarationList).ForEachChild(0xc00204e108, 0xc00c387180)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:3826 +0x32
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc00204e108, 0xc00c387180)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc00204e108, 0x25, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc00204e108)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visit(0xc00c387140, 0xc00204e108)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:21 +0x32
github.com/microsoft/typescript-go/internal/ast.(*VariableStatement).ForEachChild(0xc002079118, 0xc00c387140)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:3722 +0x5e
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc002079118, 0xc00c387140)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc002079118, 0x26, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc002079118)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visitNodes(0xc00c0192c0, {0xc00b4c6008, 0x921, 0x921})
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:28 +0x78
github.com/microsoft/typescript-go/internal/ast.visitNodeList(0xc00c0192c0, 0xc005678608)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:37 +0x51
github.com/microsoft/typescript-go/internal/ast.(*Block).ForEachChild(0xc00b1e1f08, 0xc00c0192c0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:3679 +0x32
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc00b1e1f08, 0xc00c0192c0)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc00b1e1f08, 0x27, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.visitNode.func1(0xc00b1e1f08)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:194 +0x8f
github.com/microsoft/typescript-go/internal/ast.visit(0xc00c018f80, 0xc00b1e1f08)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:21 +0x32
github.com/microsoft/typescript-go/internal/ast.(*FunctionDeclaration).ForEachChild(0xc00a0c4e50, 0xc00c018f80)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:4104 +0x1b4
github.com/microsoft/typescript-go/internal/ast.(*Node).ForEachChild(0xc00a0c4e50, 0xc00c018f80)
	/home/jabaile/work/TypeScript-go/internal/ast/ast.go:242 +0x2f
github.com/microsoft/typescript-go/internal/ls.visitNode({0x14144f8, 0xc00cffd140}, 0xc00a0c4e50, 0x28, 0xc0074e2608, 0xc00cffd1a0)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:200 +0x178f
github.com/microsoft/typescript-go/internal/ls.(*LanguageService).addNodeOutliningSpans(0xc00cffd1a0, {0x14144f8, 0xc00cffd140}, 0xc0074e2608)
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:40 +0x34e
github.com/microsoft/typescript-go/internal/ls.(*LanguageService).ProvideFoldingRange(0xc00cffd1a0, {0x14144f8, 0xc00cffd140}, {0xc00892e100, 0x3c})
	/home/jabaile/work/TypeScript-go/internal/ls/folding.go:20 +0xe5
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleFoldingRange(0xc0001c2008, {0x14144f8, 0xc00cffd140}, 0xc00cffd1a0, 0xc000454020)
	/home/jabaile/work/TypeScript-go/internal/lsp/server.go:1144 +0x96
github.com/microsoft/typescript-go/internal/lsp.registerLanguageServiceDocumentRequestHandler[...].func1({0x14144f8, 0xc00cffd140}, 0xc0108f2060)
	/home/jabaile/work/TypeScript-go/internal/lsp/server.go:597 +0x28e
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleRequestOrNotification(0xc0001c2008, {0x14144f8, 0xc00cffd140}, 0xc0108f2060)
	/home/jabaile/work/TypeScript-go/internal/lsp/server.go:478 +0xf3
github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop.func1()
	/home/jabaile/work/TypeScript-go/internal/lsp/server.go:381 +0x65
created by github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop in goroutine 10
	/home/jabaile/work/TypeScript-go/internal/lsp/server.go:401 +0x5c5

@jakebailey
Copy link
Member

Since I pushed, I can't review anymore. Can someone else look?

Copilot finished reviewing on behalf of jakebailey December 2, 2025 19:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.

@jakebailey jakebailey added this pull request to the merge queue Dec 2, 2025
Merged via the queue into microsoft:main with commit dc6ce07 Dec 2, 2025
28 checks passed
@mthines
Copy link

mthines commented Dec 3, 2025

On version 7.0.0-dev.20251203.1 I still get the Request textDocument/foldingRange failed. error when opening one of my files in VSCode

I can provide you file, but not in a public thread :)

@jakebailey
Copy link
Member

Please file an issue with at least the panic stack trace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code folding doesn't work

5 participants