Skip to content

Folding Ranges implementation #1326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
538 changes: 538 additions & 0 deletions internal/ls/folding.go

Large diffs are not rendered by default.

1,445 changes: 1,445 additions & 0 deletions internal/ls/folding_test.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions internal/ls/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,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)
Copy link
Member

@jakebailey jakebailey Jul 1, 2025

Choose a reason for hiding this comment

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

This seems weird; do we actually want to do that for all nodes? What happens if we need to make a range out of a JSDoc range itself? (Should your calls be using the other helper below?)

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 this is actually correct. In Strada this is equivalent to createTextSpanFromNode, which uses node.getStart() as the start of the span.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's why I changed it here.

Copy link
Member

Choose a reason for hiding this comment

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

If that's how it was, then that seems fine, and maybe we can close the other PR?

}

func (l *LanguageService) createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range {
Expand Down
16 changes: 16 additions & 0 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@ func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.R
return s.handleDocumentSymbol(ctx, req)
case *lsproto.CompletionItem:
return s.handleCompletionItemResolve(ctx, req)
case *lsproto.FoldingRangeParams:
return s.handleFoldingRange(ctx, req)

default:
switch req.Method {
case lsproto.MethodShutdown:
Expand Down Expand Up @@ -584,6 +587,9 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) {
DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{
Boolean: ptrTo(true),
},
FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{
Boolean: ptrTo(true),
},
},
})
}
Expand Down Expand Up @@ -683,6 +689,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)
Expand Down
2 changes: 1 addition & 1 deletion internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5044,7 +5044,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)
Expand Down
10 changes: 5 additions & 5 deletions internal/printer/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,23 +335,23 @@ 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,
)
}

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 {
Expand All @@ -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
}

Expand Down
20 changes: 20 additions & 0 deletions internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,26 @@ 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) {
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 GetEndLinePosition(sourceFile *ast.SourceFile, line int) int {
pos := int(GetLineStarts(sourceFile)[line])
for {
Expand Down
Loading