From 3d2eb6e9fa2a6d6ca06d13158b973d00d6bf849f Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Thu, 26 Mar 2026 20:42:46 +0200 Subject: [PATCH 1/3] Fix go to definition for YAML alias --- docs/docs/changelog.md | 1 + internal/lsp/handlers.go | 13 +-- internal/lsp/treesitter.go | 148 ++++++++++++++++++++++++++++++++ internal/lsp/treesitter_test.go | 110 ++++++++++++++++++++++++ 4 files changed, 263 insertions(+), 9 deletions(-) diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 561211b3..56e0aff6 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -18,6 +18,7 @@ title: Changelog * `[Added]` Add `lets self doc` command to open the online documentation in a browser. * `[Added]` Show background update notifications for interactive sessions, with Homebrew-aware guidance and `LETS_CHECK_UPDATE` opt-out. * `[Changed]` Centralize the `lets:` log prefix in the formatter and render debug messages in blue. +* `[Fixed]` Resolve `go to definition` from YAML merge aliases such as `<<: *test` to the referenced command in `lets self lsp`. ## [0.0.59](https://github.com/lets-cli/lets/releases/tag/v0.0.59) diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index d53ad04d..89219dcf 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -86,17 +86,12 @@ func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.Defini } func (h *definitionHandler) findCommandDefinition(doc *string, params *lsp.DefinitionParams) (any, error) { - line := getLine(doc, params.Position.Line) - if line == "" { + commandName := h.parser.extractCommandReference(doc, params.Position) + if commandName == "" { return nil, nil } - word := wordUnderCursor(line, ¶ms.Position) - if word == "" { - return nil, nil - } - - command := h.parser.findCommand(doc, word) + command := h.parser.findCommand(doc, commandName) if command == nil { return nil, nil } @@ -163,7 +158,7 @@ func (s *lspServer) textDocumentDefinition(context *glsp.Context, params *lsp.De switch p.getPositionType(doc, params.Position) { case PositionTypeMixins: return definitionHandler.findMixinsDefinition(doc, params) - case PositionTypeDepends: + case PositionTypeDepends, PositionTypeCommandAlias: return definitionHandler.findCommandDefinition(doc, params) default: return nil, nil diff --git a/internal/lsp/treesitter.go b/internal/lsp/treesitter.go index 81630b42..a9a27f34 100644 --- a/internal/lsp/treesitter.go +++ b/internal/lsp/treesitter.go @@ -14,6 +14,7 @@ type PositionType int const ( PositionTypeMixins PositionType = iota PositionTypeDepends + PositionTypeCommandAlias PositionTypeNone ) @@ -115,6 +116,8 @@ func (p *parser) getPositionType(document *string, position lsp.Position) Positi return PositionTypeMixins } else if p.inDependsPosition(document, position) { return PositionTypeDepends + } else if p.inCommandAliasPosition(document, position) { + return PositionTypeCommandAlias } return PositionTypeNone @@ -229,6 +232,47 @@ func (p *parser) inDependsPosition(document *string, position lsp.Position) bool return false } +func (p *parser) inCommandAliasPosition(document *string, position lsp.Position) bool { + tree, docBytes, err := parseYAMLDocument(document) + if err != nil { + return false + } + defer tree.Release() + + query, err := ts.NewQuery(` + (block_mapping_pair + key: (flow_node) @keymerge + value: (flow_node(alias) @alias) + (#eq? @keymerge "<<") + ) + `, yamlLanguage) + if err != nil { + return false + } + + root := tree.RootNode() + if root == nil { + return false + } + + matches := query.Exec(root, yamlLanguage, docBytes) + + for { + match, ok := matches.NextMatch() + if !ok { + break + } + + for _, capture := range match.Captures { + if capture.Name == "alias" && isCursorWithinNode(capture.Node, position) { + return true + } + } + } + + return false +} + func (p *parser) extractFilenameFromMixins(document *string, position lsp.Position) string { tree, docBytes, err := parseYAMLDocument(document) if err != nil { @@ -275,6 +319,110 @@ func (p *parser) extractFilenameFromMixins(document *string, position lsp.Positi return "" } +func (p *parser) extractCommandReference(document *string, position lsp.Position) string { + if commandName := p.extractDependsCommandReference(document, position); commandName != "" { + return commandName + } + + return p.extractAliasCommandReference(document, position) +} + +func (p *parser) extractDependsCommandReference(document *string, position lsp.Position) string { + tree, docBytes, err := parseYAMLDocument(document) + if err != nil { + return "" + } + defer tree.Release() + + query, err := ts.NewQuery(` + (block_mapping_pair + key: (flow_node) @keydepends + value: [ + (flow_node + (flow_sequence + (flow_node + (plain_scalar + (string_scalar)) @reference))) + (block_node + (block_sequence + (block_sequence_item + (flow_node + (plain_scalar + (string_scalar)) @reference)))) + ] + (#eq? @keydepends "depends") + ) + `, yamlLanguage) + if err != nil { + return "" + } + + root := tree.RootNode() + if root == nil { + return "" + } + + matches := query.Exec(root, yamlLanguage, docBytes) + + for { + match, ok := matches.NextMatch() + if !ok { + break + } + + for _, capture := range match.Captures { + if capture.Name == "reference" && isCursorWithinNode(capture.Node, position) { + return capture.Node.Text(docBytes) + } + } + } + + return "" +} + +func (p *parser) extractAliasCommandReference(document *string, position lsp.Position) string { + tree, docBytes, err := parseYAMLDocument(document) + if err != nil { + return "" + } + defer tree.Release() + + query, err := ts.NewQuery(` + (block_mapping_pair + key: (flow_node) @keymerge + value: (flow_node(alias) @reference) + (#eq? @keymerge "<<") + ) + `, yamlLanguage) + if err != nil { + return "" + } + + root := tree.RootNode() + if root == nil { + return "" + } + + matches := query.Exec(root, yamlLanguage, docBytes) + + for { + match, ok := matches.NextMatch() + if !ok { + break + } + + for _, capture := range match.Captures { + if capture.Name != "reference" || !isCursorWithinNode(capture.Node, position) { + continue + } + + return strings.TrimPrefix(capture.Node.Text(docBytes), "*") + } + } + + return "" +} + type Command struct { name string // TODO: maybe range will be more appropriate diff --git a/internal/lsp/treesitter_test.go b/internal/lsp/treesitter_test.go index 516abd09..3dd7a009 100644 --- a/internal/lsp/treesitter_test.go +++ b/internal/lsp/treesitter_test.go @@ -197,6 +197,38 @@ commands: } } +func TestDetectCommandAliasPosition(t *testing.T) { + doc := `commands: + test: &test + cmd: echo Test + run-test: + <<: *test + cmd: echo Run` + + tests := []struct { + pos lsp.Position + want bool + }{ + {pos: pos(4, 7), want: false}, + {pos: pos(4, 8), want: true}, + {pos: pos(4, 10), want: true}, + {pos: pos(4, 13), want: true}, + {pos: pos(5, 8), want: false}, + } + + p := newParser(logger) + for i, tt := range tests { + got := p.inCommandAliasPosition(&doc, tt.pos) + if got != tt.want { + t.Errorf("case %d: expected %v, actual %v", i, tt.want, got) + } + } + + if got := p.getPositionType(&doc, pos(4, 10)); got != PositionTypeCommandAlias { + t.Fatalf("expected PositionTypeCommandAlias, got %v", got) + } +} + func TestGetCommands(t *testing.T) { doc := `shell: bash mixins: @@ -336,6 +368,84 @@ commands: } } +func TestExtractCommandReference(t *testing.T) { + doc := `commands: + build: + cmd: echo Build + test: &test + depends: + - build + cmd: echo Test + run-test: + <<: *test + depends: [build, test] + cmd: echo Run` + + tests := []struct { + name string + pos lsp.Position + want string + }{ + {name: "block depends item", pos: pos(5, 8), want: "build"}, + {name: "merge alias star", pos: pos(8, 8), want: "test"}, + {name: "merge alias name", pos: pos(8, 10), want: "test"}, + {name: "flow depends first item", pos: pos(9, 14), want: "build"}, + {name: "flow depends second item", pos: pos(9, 21), want: "test"}, + {name: "outside reference", pos: pos(10, 10), want: ""}, + } + + p := newParser(logger) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := p.extractCommandReference(&doc, tt.pos) + if got != tt.want { + t.Fatalf("extractCommandReference() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestFindCommandDefinitionFromAlias(t *testing.T) { + doc := `commands: + test: &test + cmd: echo Test + run-test: + <<: *test + cmd: echo Run` + + handler := definitionHandler{parser: newParser(logger)} + params := &lsp.DefinitionParams{ + TextDocumentPositionParams: lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{URI: "file:///tmp/lets.yaml"}, + Position: pos(4, 10), + }, + } + + got, err := handler.findCommandDefinition(&doc, params) + if err != nil { + t.Fatalf("findCommandDefinition() error = %v", err) + } + + locations, ok := got.([]lsp.Location) + if !ok { + t.Fatalf("findCommandDefinition() type = %T, want []lsp.Location", got) + } + + want := []lsp.Location{ + { + URI: "file:///tmp/lets.yaml", + Range: lsp.Range{ + Start: pos(1, 2), + End: pos(1, 2), + }, + }, + } + + if !reflect.DeepEqual(locations, want) { + t.Fatalf("findCommandDefinition() = %#v, want %#v", locations, want) + } +} + func TestMixinsHelpersWithMultipleItems(t *testing.T) { blockDoc := `shell: bash mixins: From 5e0d5cb67661effe00a6010c4231548e2b74f010 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sat, 28 Mar 2026 12:31:44 +0200 Subject: [PATCH 2/3] Add LSP debug logs and resolve alias definitions by anchor --- internal/lsp/handlers.go | 28 ++++- internal/lsp/server.go | 17 ++- internal/lsp/treesitter.go | 186 ++++++++++++++++++++------------ internal/lsp/treesitter_test.go | 33 ++++-- 4 files changed, 189 insertions(+), 75 deletions(-) diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 89219dcf..5934c61c 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -68,15 +68,19 @@ func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.Defini filename := h.parser.extractFilenameFromMixins(doc, params.Position) if filename == "" { + h.parser.log.Debugf("no mixin filename resolved at %s:%d:%d", path, params.Position.Line, params.Position.Character) return nil, nil } absFilename := replacePathFilename(path, filename) if !util.FileExists(absFilename) { + h.parser.log.Debugf("mixin target does not exist: %s", absFilename) return nil, nil } + h.parser.log.Debugf("resolved mixin definition %q -> %s", filename, absFilename) + return []lsp.Location{ { URI: pathToURI(absFilename), @@ -86,16 +90,27 @@ func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.Defini } func (h *definitionHandler) findCommandDefinition(doc *string, params *lsp.DefinitionParams) (any, error) { + path := normalizePath(params.TextDocument.URI) commandName := h.parser.extractCommandReference(doc, params.Position) if commandName == "" { + h.parser.log.Debugf("no command reference resolved at %s:%d:%d", path, params.Position.Line, params.Position.Character) return nil, nil } command := h.parser.findCommand(doc, commandName) if command == nil { + h.parser.log.Debugf("command reference %q did not match any local command", commandName) return nil, nil } + h.parser.log.Debugf( + "resolved command definition %q -> %s:%d:%d", + commandName, + path, + command.position.Line, + command.position.Character, + ) + // TODO: theoretically we can have multiple commands with the same name if we have mixins return []lsp.Location{ { @@ -154,13 +169,22 @@ func (s *lspServer) textDocumentDefinition(context *glsp.Context, params *lsp.De doc := s.storage.GetDocument(params.TextDocument.URI) p := newParser(s.log) - - switch p.getPositionType(doc, params.Position) { + positionType := p.getPositionType(doc, params.Position) + s.log.Debugf( + "definition request uri=%s line=%d char=%d type=%s", + normalizePath(params.TextDocument.URI), + params.Position.Line, + params.Position.Character, + positionType, + ) + + switch positionType { case PositionTypeMixins: return definitionHandler.findMixinsDefinition(doc, params) case PositionTypeDepends, PositionTypeCommandAlias: return definitionHandler.findCommandDefinition(doc, params) default: + s.log.Debugf("definition request ignored: unsupported cursor position") return nil, nil } } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f60d6a1f..4020df36 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -3,6 +3,7 @@ package lsp import ( "context" + "github.com/lets-cli/lets/internal/env" "github.com/tliron/commonlog" _ "github.com/tliron/commonlog/simple" lsp "github.com/tliron/glsp/protocol_3_16" @@ -24,8 +25,22 @@ func (s *lspServer) Run() error { return s.server.RunStdio() } +func lspLogVerbosity() (verbosity int) { + verbosity = 1 + + defer func() { + _ = recover() + }() + + if env.DebugLevel() > 0 { + verbosity = 2 + } + + return verbosity +} + func Run(ctx context.Context, version string) error { - commonlog.Configure(1, nil) + commonlog.Configure(lspLogVerbosity(), nil) logger := commonlog.GetLogger(lsName) logger.Infof("Lets LSP server starting %s", version) diff --git a/internal/lsp/treesitter.go b/internal/lsp/treesitter.go index a9a27f34..d08f7294 100644 --- a/internal/lsp/treesitter.go +++ b/internal/lsp/treesitter.go @@ -18,6 +18,19 @@ const ( PositionTypeNone ) +func (p PositionType) String() string { + switch p { + case PositionTypeMixins: + return "mixins" + case PositionTypeDepends: + return "depends" + case PositionTypeCommandAlias: + return "command_alias" + default: + return "none" + } +} + var yamlLanguage = grammars.YamlLanguage() func isCursorWithinNode(node *ts.Node, pos lsp.Position) bool { @@ -58,6 +71,41 @@ func parseYAMLDocument(document *string) (*ts.Tree, []byte, error) { return tree, docBytes, nil } +func executeYAMLQuery(document *string, queryText string, visit func(capture ts.QueryCapture, docBytes []byte) bool) bool { + tree, docBytes, err := parseYAMLDocument(document) + if err != nil { + return false + } + defer tree.Release() + + query, err := ts.NewQuery(queryText, yamlLanguage) + if err != nil { + return false + } + + root := tree.RootNode() + if root == nil { + return false + } + + matches := query.Exec(root, yamlLanguage, docBytes) + + for { + match, ok := matches.NextMatch() + if !ok { + break + } + + for _, capture := range match.Captures { + if visit(capture, docBytes) { + return true + } + } + } + + return false +} + func getLine(document *string, line uint32) string { lines := strings.Split(*document, "\n") if line >= uint32(len(lines)) { @@ -233,44 +281,15 @@ func (p *parser) inDependsPosition(document *string, position lsp.Position) bool } func (p *parser) inCommandAliasPosition(document *string, position lsp.Position) bool { - tree, docBytes, err := parseYAMLDocument(document) - if err != nil { - return false - } - defer tree.Release() - - query, err := ts.NewQuery(` + return executeYAMLQuery(document, ` (block_mapping_pair key: (flow_node) @keymerge value: (flow_node(alias) @alias) (#eq? @keymerge "<<") ) - `, yamlLanguage) - if err != nil { - return false - } - - root := tree.RootNode() - if root == nil { - return false - } - - matches := query.Exec(root, yamlLanguage, docBytes) - - for { - match, ok := matches.NextMatch() - if !ok { - break - } - - for _, capture := range match.Captures { - if capture.Name == "alias" && isCursorWithinNode(capture.Node, position) { - return true - } - } - } - - return false + `, func(capture ts.QueryCapture, _ []byte) bool { + return capture.Name == "alias" && isCursorWithinNode(capture.Node, position) + }) } func (p *parser) extractFilenameFromMixins(document *string, position lsp.Position) string { @@ -321,20 +340,22 @@ func (p *parser) extractFilenameFromMixins(document *string, position lsp.Positi func (p *parser) extractCommandReference(document *string, position lsp.Position) string { if commandName := p.extractDependsCommandReference(document, position); commandName != "" { + p.log.Debugf("resolved command reference from depends: %q", commandName) return commandName } - return p.extractAliasCommandReference(document, position) + commandName := p.extractAliasCommandReference(document, position) + if commandName != "" { + p.log.Debugf("resolved command reference from alias: %q", commandName) + } + + return commandName } func (p *parser) extractDependsCommandReference(document *string, position lsp.Position) string { - tree, docBytes, err := parseYAMLDocument(document) - if err != nil { - return "" - } - defer tree.Release() + var commandName string - query, err := ts.NewQuery(` + executeYAMLQuery(document, ` (block_mapping_pair key: (flow_node) @keydepends value: [ @@ -352,35 +373,52 @@ func (p *parser) extractDependsCommandReference(document *string, position lsp.P ] (#eq? @keydepends "depends") ) - `, yamlLanguage) - if err != nil { - return "" - } + `, func(capture ts.QueryCapture, docBytes []byte) bool { + if capture.Name == "reference" && isCursorWithinNode(capture.Node, position) { + commandName = capture.Node.Text(docBytes) + return true + } - root := tree.RootNode() - if root == nil { - return "" - } + return false + }) - matches := query.Exec(root, yamlLanguage, docBytes) + return commandName +} - for { - match, ok := matches.NextMatch() - if !ok { - break - } +func (p *parser) extractAliasCommandReference(document *string, position lsp.Position) string { + var anchorName string - for _, capture := range match.Captures { - if capture.Name == "reference" && isCursorWithinNode(capture.Node, position) { - return capture.Node.Text(docBytes) - } + executeYAMLQuery(document, ` + (block_mapping_pair + key: (flow_node) @keymerge + value: (flow_node(alias) @reference) + (#eq? @keymerge "<<") + ) + `, func(capture ts.QueryCapture, docBytes []byte) bool { + if capture.Name == "reference" && isCursorWithinNode(capture.Node, position) { + anchorName = strings.TrimPrefix(capture.Node.Text(docBytes), "*") + return true } + + return false + }) + + if anchorName == "" { + return "" } - return "" + commandName := p.findCommandNameByAnchor(document, anchorName) + if commandName == "" { + p.log.Debugf("alias anchor %q did not match any local command anchor", anchorName) + return "" + } + + p.log.Debugf("resolved alias anchor %q to command %q", anchorName, commandName) + + return commandName } -func (p *parser) extractAliasCommandReference(document *string, position lsp.Position) string { +func (p *parser) findCommandNameByAnchor(document *string, anchorName string) string { tree, docBytes, err := parseYAMLDocument(document) if err != nil { return "" @@ -389,9 +427,17 @@ func (p *parser) extractAliasCommandReference(document *string, position lsp.Pos query, err := ts.NewQuery(` (block_mapping_pair - key: (flow_node) @keymerge - value: (flow_node(alias) @reference) - (#eq? @keymerge "<<") + key: (flow_node(plain_scalar(string_scalar)) @commands) + value: (block_node + (block_mapping + (block_mapping_pair + key: (flow_node + (plain_scalar + (string_scalar)) @cmd_key) + value: (block_node + (anchor + (anchor_name) @anchor_name))))) + (#eq? @commands "commands") ) `, yamlLanguage) if err != nil { @@ -411,12 +457,20 @@ func (p *parser) extractAliasCommandReference(document *string, position lsp.Pos break } + var commandName string + var matchedAnchor string + for _, capture := range match.Captures { - if capture.Name != "reference" || !isCursorWithinNode(capture.Node, position) { - continue + switch capture.Name { + case "cmd_key": + commandName = capture.Node.Text(docBytes) + case "anchor_name": + matchedAnchor = capture.Node.Text(docBytes) } + } - return strings.TrimPrefix(capture.Node.Text(docBytes), "*") + if matchedAnchor == anchorName { + return commandName } } diff --git a/internal/lsp/treesitter_test.go b/internal/lsp/treesitter_test.go index 3dd7a009..48537e8d 100644 --- a/internal/lsp/treesitter_test.go +++ b/internal/lsp/treesitter_test.go @@ -407,17 +407,18 @@ func TestExtractCommandReference(t *testing.T) { func TestFindCommandDefinitionFromAlias(t *testing.T) { doc := `commands: - test: &test - cmd: echo Test - run-test: - <<: *test - cmd: echo Run` + publish-docs: &docs + work_dir: docs + cmd: npm run doc:deploy + run-docs: + <<: *docs + cmd: npm start` handler := definitionHandler{parser: newParser(logger)} params := &lsp.DefinitionParams{ TextDocumentPositionParams: lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: "file:///tmp/lets.yaml"}, - Position: pos(4, 10), + Position: pos(5, 10), }, } @@ -446,6 +447,26 @@ func TestFindCommandDefinitionFromAlias(t *testing.T) { } } +func TestFindCommandNameByAnchor(t *testing.T) { + doc := `commands: + publish-docs: &docs + work_dir: docs + cmd: npm run doc:deploy + run-docs: + <<: *docs + cmd: npm start` + + p := newParser(logger) + + if got := p.findCommandNameByAnchor(&doc, "docs"); got != "publish-docs" { + t.Fatalf("findCommandNameByAnchor() = %q, want %q", got, "publish-docs") + } + + if got := p.findCommandNameByAnchor(&doc, "missing"); got != "" { + t.Fatalf("findCommandNameByAnchor() = %q, want empty", got) + } +} + func TestMixinsHelpersWithMultipleItems(t *testing.T) { blockDoc := `shell: bash mixins: From cdd37d026897bd20f758a4df3d1b0ae7dff72288 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Sat, 28 Mar 2026 19:35:08 +0200 Subject: [PATCH 3/3] Remove unused LSP helpers and fix log verbosity --- internal/cli/cli.go | 1 + internal/lsp/handlers.go | 1 + internal/lsp/server.go | 4 +-- internal/lsp/treesitter.go | 49 +++------------------------------ internal/lsp/treesitter_test.go | 25 ----------------- 5 files changed, 8 insertions(+), 72 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index b7d5a688..1783adf9 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -136,6 +136,7 @@ func Main(version string, buildDate string) int { if errors.As(err, &depErr) { executor.PrintDependencyTree(depErr, os.Stderr) log.Errorf("%s", depErr.FailureMessage()) + return getExitCode(err, 1) } diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 5934c61c..7882fa51 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -91,6 +91,7 @@ func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.Defini func (h *definitionHandler) findCommandDefinition(doc *string, params *lsp.DefinitionParams) (any, error) { path := normalizePath(params.TextDocument.URI) + commandName := h.parser.extractCommandReference(doc, params.Position) if commandName == "" { h.parser.log.Debugf("no command reference resolved at %s:%d:%d", path, params.Position.Line, params.Position.Character) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 4020df36..873f0002 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -25,8 +25,8 @@ func (s *lspServer) Run() error { return s.server.RunStdio() } -func lspLogVerbosity() (verbosity int) { - verbosity = 1 +func lspLogVerbosity() int { + verbosity := 1 defer func() { _ = recover() diff --git a/internal/lsp/treesitter.go b/internal/lsp/treesitter.go index d08f7294..537f0f7e 100644 --- a/internal/lsp/treesitter.go +++ b/internal/lsp/treesitter.go @@ -106,49 +106,6 @@ func executeYAMLQuery(document *string, queryText string, visit func(capture ts. return false } -func getLine(document *string, line uint32) string { - lines := strings.Split(*document, "\n") - if line >= uint32(len(lines)) { - return "" - } - - return lines[line] -} - -// position. -func wordUnderCursor(text string, position *lsp.Position) string { - if len(text) == 0 { - return "" - } - - character := position.Character - - if character >= uint32(len(text)) { - return "" - } - - if text[character] == ' ' { - return "" - } - - // Find word boundaries - start := position.Character - for start > 0 && isWordChar(text[start-1]) { - start-- - } - - end := position.Character - for end < uint32(len(text)) && isWordChar(text[end]) { - end++ - } - - return text[start:end] -} - -func isWordChar(c byte) bool { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' -} - type parser struct { log commonlog.Logger } @@ -457,8 +414,10 @@ func (p *parser) findCommandNameByAnchor(document *string, anchorName string) st break } - var commandName string - var matchedAnchor string + var ( + commandName string + matchedAnchor string + ) for _, capture := range match.Captures { switch capture.Name { diff --git a/internal/lsp/treesitter_test.go b/internal/lsp/treesitter_test.go index 48537e8d..20e8da5d 100644 --- a/internal/lsp/treesitter_test.go +++ b/internal/lsp/treesitter_test.go @@ -694,28 +694,3 @@ commands: t.Fatalf("extractDependsValues() = %#v, want %#v", got, want) } } - -func TestWordUnderCursor(t *testing.T) { - tests := []struct { - line string - position lsp.Position - want string - }{ - {"test word here", lsp.Position{Character: 0}, "test"}, - {"test word here", lsp.Position{Character: 2}, "test"}, - {"test word here", lsp.Position{Character: 5}, "word"}, - {"test word here", lsp.Position{Character: 10}, "here"}, - {"test-word_123", lsp.Position{Character: 5}, "test-word_123"}, - {"", lsp.Position{Character: 0}, ""}, - {"test", lsp.Position{Character: 10}, ""}, - {"test word", lsp.Position{Character: 4}, ""}, - {" test ", lsp.Position{Character: 3}, "test"}, - } - - for i, tt := range tests { - got := wordUnderCursor(tt.line, &tt.position) - if got != tt.want { - t.Errorf("case %d: got %q, want %q", i, got, tt.want) - } - } -}