internal/ls/selectionranges.go (151 lines of code) (raw):

package ls import ( "context" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" ) func (l *LanguageService) ProvideSelectionRanges(ctx context.Context, params *lsproto.SelectionRangeParams) (lsproto.SelectionRangeResponse, error) { _, sourceFile := l.getProgramAndFile(params.TextDocument.Uri) if sourceFile == nil { return lsproto.SelectionRangesOrNull{}, nil } var results []*lsproto.SelectionRange for _, position := range params.Positions { pos := l.converters.LineAndCharacterToPosition(sourceFile, position) selectionRange := getSmartSelectionRange(l, sourceFile, int(pos)) if selectionRange != nil { results = append(results, selectionRange) } } return lsproto.SelectionRangesOrNull{SelectionRanges: &results}, nil } func getSmartSelectionRange(l *LanguageService, sourceFile *ast.SourceFile, pos int) *lsproto.SelectionRange { factory := &ast.NodeFactory{} nodeContainsPosition := func(node *ast.Node) bool { if node == nil { return false } start := scanner.GetTokenPosOfNode(node, sourceFile, true /*includeJSDoc*/) end := node.End() return start <= pos && pos < end } pushSelectionRange := func(current *lsproto.SelectionRange, start, end int) *lsproto.SelectionRange { if start == end { return current } if !(start <= pos && pos <= end) { return current } lspRange := l.converters.ToLSPRange(sourceFile, core.NewTextRange(start, end)) if current != nil && current.Range == lspRange { return current } return &lsproto.SelectionRange{ Range: lspRange, Parent: current, } } pushSelectionCommentRange := func(current *lsproto.SelectionRange, start, end int) *lsproto.SelectionRange { current = pushSelectionRange(current, start, end) commentPos := start text := sourceFile.Text() for commentPos < end && commentPos < len(text) && text[commentPos] == '/' { commentPos++ } current = pushSelectionRange(current, commentPos, end) return current } positionsAreOnSameLine := func(pos1, pos2 int) bool { if pos1 == pos2 { return true } lspPos1 := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos1)) lspPos2 := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos2)) return lspPos1.Line == lspPos2.Line } shouldSkipNode := func(node *ast.Node, parent *ast.Node) bool { if ast.IsBlock(node) { return true } if ast.IsTemplateSpan(node) || ast.IsTemplateHead(node) || ast.IsTemplateTail(node) { return true } if parent != nil && ast.IsVariableDeclarationList(node) && ast.IsVariableStatement(parent) { return true } // Skip lone variable declarations if parent != nil && ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(parent) { decl := parent.AsVariableDeclarationList() if decl != nil && len(decl.Declarations.Nodes) == 1 { return true } } if ast.IsJSDocTypeExpression(node) || ast.IsJSDocSignature(node) || ast.IsJSDocTypeLiteral(node) { return true } return false } fullRange := l.converters.ToLSPRange(sourceFile, core.NewTextRange(sourceFile.Pos(), sourceFile.End())) result := &lsproto.SelectionRange{ Range: fullRange, } var current *ast.Node for current = sourceFile.AsNode(); current != nil; { var next *ast.Node parent := current visit := func(node *ast.Node) *ast.Node { if node != nil && next == nil { var foundComment *ast.CommentRange for comment := range scanner.GetTrailingCommentRanges(factory, sourceFile.Text(), node.End()) { foundComment = &comment break } if foundComment != nil && foundComment.Kind == ast.KindSingleLineCommentTrivia { result = pushSelectionCommentRange(result, foundComment.Pos(), foundComment.End()) } if nodeContainsPosition(node) { // Add range for multi-line function bodies before skipping the block if ast.IsBlock(node) && ast.IsFunctionLikeDeclaration(parent) { if !positionsAreOnSameLine(astnav.GetStartOfNode(node, sourceFile, false), node.End()) { start := astnav.GetStartOfNode(node, sourceFile, false) end := node.End() result = pushSelectionRange(result, start, end) } } if !shouldSkipNode(node, parent) { start := astnav.GetStartOfNode(node, sourceFile, false) end := node.End() result = pushSelectionRange(result, start, end) } next = node } } return node } visitNodes := func(nodes *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList { if nodes != nil && len(nodes.Nodes) > 0 { shouldSkipList := parent != nil && ast.IsVariableDeclarationList(parent) if !shouldSkipList { start := astnav.GetStartOfNode(nodes.Nodes[0], sourceFile, false) end := nodes.Nodes[len(nodes.Nodes)-1].End() if start <= pos && pos < end { result = pushSelectionRange(result, start, end) } } } return v.VisitNodes(nodes) } // Visit JSDoc nodes first if they exist if current.Flags&ast.NodeFlagsHasJSDoc != 0 { for _, jsdoc := range current.JSDoc(sourceFile) { visit(jsdoc) } } tempVisitor := ast.NewNodeVisitor(visit, nil, ast.NodeVisitorHooks{ VisitNodes: visitNodes, }) current.VisitEachChild(tempVisitor) current = next } return result }