From 098fb595fe63fb1fc8e91368f740be641451a54f Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Thu, 2 Apr 2026 20:13:54 +0300 Subject: [PATCH 1/3] Add index for commands and use it in depends definition resolution --- internal/lsp/handlers.go | 56 ++++++++++++++---------- internal/lsp/index.go | 76 ++++++++++++++++++++++++++++++++ internal/lsp/index_test.go | 89 ++++++++++++++++++++++++++++++++++++++ internal/lsp/server.go | 3 ++ 4 files changed, 200 insertions(+), 24 deletions(-) create mode 100644 internal/lsp/index.go create mode 100644 internal/lsp/index_test.go diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 7882fa5..62594aa 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -5,6 +5,7 @@ import ( "slices" "github.com/lets-cli/lets/internal/util" + "github.com/tliron/commonlog" "github.com/tliron/glsp" lsp "github.com/tliron/glsp/protocol_3_16" ) @@ -43,6 +44,7 @@ func (s *lspServer) setTrace(context *glsp.Context, params *lsp.SetTraceParams) func (s *lspServer) textDocumentDidOpen(context *glsp.Context, params *lsp.DidOpenTextDocumentParams) error { s.storage.AddDocument(params.TextDocument.URI, params.TextDocument.Text) + go s.index.IndexDocument(params.TextDocument.URI, params.TextDocument.Text) return nil } @@ -51,6 +53,7 @@ func (s *lspServer) textDocumentDidChange(context *glsp.Context, params *lsp.Did switch c := change.(type) { case lsp.TextDocumentContentChangeEventWhole: s.storage.AddDocument(params.TextDocument.URI, c.Text) + go s.index.IndexDocument(params.TextDocument.URI, c.Text) case lsp.TextDocumentContentChangeEvent: return errors.New("incremental changes not supported") } @@ -60,7 +63,9 @@ func (s *lspServer) textDocumentDidChange(context *glsp.Context, params *lsp.Did } type definitionHandler struct { + log commonlog.Logger parser *parser + index *index } func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.DefinitionParams) (any, error) { @@ -89,46 +94,47 @@ func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.Defini }, nil } +func locationForCommand(uri string, position lsp.Position) lsp.Location { + return lsp.Location{ + URI: uri, + Range: lsp.Range{ + Start: lsp.Position{ + Line: position.Line, + Character: 2, // TODO: do we have to assume indentation? + }, + End: lsp.Position{ + Line: position.Line, + Character: 2, // TODO: do we need + len ? + }, + }, + } +} + 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) + h.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) + commandInfo, found := h.index.findCommand(commandName) + if !found { + h.log.Debugf("command reference %q did not match any local command", commandName) return nil, nil } - h.parser.log.Debugf( + h.log.Debugf( "resolved command definition %q -> %s:%d:%d", commandName, path, - command.position.Line, - command.position.Character, + commandInfo.position.Line, + commandInfo.position.Character, ) - // TODO: theoretically we can have multiple commands with the same name if we have mixins - return []lsp.Location{ - { - // TODO: support commands in other files - URI: params.TextDocument.URI, - Range: lsp.Range{ - Start: lsp.Position{ - Line: command.position.Line, - Character: 2, // TODO: do we have to assume indentation? - }, - End: lsp.Position{ - Line: command.position.Line, - Character: 2, // TODO: do we need + len ? - }, - }, - }, - }, nil + loc := locationForCommand(commandInfo.fileURI, commandInfo.position) + return []lsp.Location{loc}, nil } type completionHandler struct { @@ -165,7 +171,9 @@ func (h *completionHandler) buildDependsCompletions(doc *string, params *lsp.Com // Returns: Location | []Location | []LocationLink | nil. func (s *lspServer) textDocumentDefinition(context *glsp.Context, params *lsp.DefinitionParams) (any, error) { definitionHandler := definitionHandler{ + log: s.log, parser: newParser(s.log), + index: s.index, } doc := s.storage.GetDocument(params.TextDocument.URI) diff --git a/internal/lsp/index.go b/internal/lsp/index.go new file mode 100644 index 0000000..3ab6145 --- /dev/null +++ b/internal/lsp/index.go @@ -0,0 +1,76 @@ +package lsp + +import ( + "sync" + + "github.com/tliron/commonlog" + lsp "github.com/tliron/glsp/protocol_3_16" +) + +// TODO: maybe use Command struct ? +type commandInfo struct { + fileURI string + // position stored at the time of indexing and may be stale + position lsp.Position +} + +type index struct { + log commonlog.Logger + mu sync.RWMutex + commands map[string]commandInfo + commandsByURI map[string]map[string]struct{} +} + +func newIndex(log commonlog.Logger) *index { + return &index{ + log: log, + commands: make(map[string]commandInfo), + commandsByURI: make(map[string]map[string]struct{}), + } +} + +// IndexDocument extracts commands from a document and updates the index to reflect that document's current state. +func (i *index) IndexDocument(uri string, doc string) { + parser := newParser(i.log) + commands := parser.getCommands(&doc) + + indexedCommands := make(map[string]commandInfo, len(commands)) + indexedNames := make(map[string]struct{}, len(commands)) + + for _, command := range commands { + indexedCommands[command.name] = commandInfo{ + fileURI: uri, + position: command.position, + } + // TODOL maybe use Set + indexedNames[command.name] = struct{}{} + } + + i.mu.Lock() + defer i.mu.Unlock() + + i.log.Debugf("Indexed %d commands in file %s", len(indexedNames), uri) + + for name := range i.commandsByURI[uri] { + delete(i.commands, name) + } + + for name, info := range indexedCommands { + i.commands[name] = info + } + + if len(indexedNames) == 0 { + delete(i.commandsByURI, uri) + return + } + + i.commandsByURI[uri] = indexedNames +} + +func (i *index) findCommand(name string) (commandInfo, bool) { + i.mu.RLock() + defer i.mu.RUnlock() + + command, ok := i.commands[name] + return command, ok +} diff --git a/internal/lsp/index_test.go b/internal/lsp/index_test.go new file mode 100644 index 0000000..3bf6432 --- /dev/null +++ b/internal/lsp/index_test.go @@ -0,0 +1,89 @@ +package lsp + +import ( + "reflect" + "testing" +) + +func TestIndexDocumentStoresCommands(t *testing.T) { + doc := `commands: + build: + cmd: echo build + test: + cmd: echo test` + + idx := newIndex(logger) + idx.IndexDocument("file:///tmp/lets.yaml", doc) + + tests := []struct { + name string + want commandInfo + }{ + { + name: "build", + want: commandInfo{ + fileURI: "file:///tmp/lets.yaml", + position: pos(1, 2), + }, + }, + { + name: "test", + want: commandInfo{ + fileURI: "file:///tmp/lets.yaml", + position: pos(3, 2), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := idx.findCommand(tt.name) + if !ok { + t.Fatalf("findCommand(%q) did not find command", tt.name) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("findCommand(%q) = %#v, want %#v", tt.name, got, tt.want) + } + }) + } +} + +func TestIndexDocumentReplacesCommandsForSameDocument(t *testing.T) { + originalDoc := `commands: + build: + cmd: echo build + test: + cmd: echo test` + + updatedDoc := `commands: + release: + depends: [build] + cmd: echo release` + + idx := newIndex(logger) + idx.IndexDocument("file:///tmp/lets.yaml", originalDoc) + idx.IndexDocument("file:///tmp/lets.yaml", updatedDoc) + + if _, ok := idx.findCommand("build"); ok { + t.Fatal("expected build to be removed after reindex") + } + + if _, ok := idx.findCommand("test"); ok { + t.Fatal("expected test to be removed after reindex") + } + + got, ok := idx.findCommand("release") + if !ok { + t.Fatal("expected release to be indexed after reindex") + } + + want := commandInfo{ + fileURI: "file:///tmp/lets.yaml", + position: pos(1, 2), + } + + if !reflect.DeepEqual(got, want) { + t.Fatalf("findCommand(%q) = %#v, want %#v", "release", got, want) + } +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 873f000..5ca8057 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -18,6 +18,7 @@ type lspServer struct { version string server *server.Server storage *storage + index *index log commonlog.Logger } @@ -54,6 +55,7 @@ func Run(ctx context.Context, version string) error { version: version, server: glspServer, storage: newStorage(), + index: newIndex(logger), log: logger, } @@ -65,6 +67,7 @@ func Run(ctx context.Context, version string) error { handler.TextDocumentDidChange = lspServer.textDocumentDidChange handler.TextDocumentDefinition = lspServer.textDocumentDefinition handler.TextDocumentCompletion = lspServer.textDocumentCompletion + // TODO: add onDelete return lspServer.Run() } From 669058ced21470179588eea529c09c5bd67ec2be Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Thu, 2 Apr 2026 20:26:15 +0300 Subject: [PATCH 2/3] load mixin files and index them asynchronously --- docs/docs/changelog.md | 1 + internal/executor/dependency_error.go | 1 + internal/lsp/handlers.go | 39 ++++++++++ internal/lsp/handlers_test.go | 108 ++++++++++++++++++++++++++ internal/lsp/index.go | 16 ++-- internal/lsp/storage.go | 9 +++ internal/lsp/treesitter.go | 27 ++++++- internal/lsp/treesitter_test.go | 27 ++++++- 8 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 internal/lsp/handlers_test.go diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 617dde0..9d2d266 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -19,6 +19,7 @@ title: Changelog * `[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`. +* `[Added]` Load local mixin files into LSP storage and command index so mixin commands are available for navigation. ## [0.0.59](https://github.com/lets-cli/lets/releases/tag/v0.0.59) diff --git a/internal/executor/dependency_error.go b/internal/executor/dependency_error.go index 0b2ed75..0996cad 100644 --- a/internal/executor/dependency_error.go +++ b/internal/executor/dependency_error.go @@ -44,6 +44,7 @@ func (e *DependencyError) FailureMessage() string { func (e *DependencyError) TreeMessage() string { red := color.New(color.FgRed).SprintFunc() + var builder strings.Builder builder.WriteString(dependencyTreeHeader) diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 62594aa..7c17e6f 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -2,7 +2,9 @@ package lsp import ( "errors" + "os" "slices" + "strings" "github.com/lets-cli/lets/internal/util" "github.com/tliron/commonlog" @@ -42,9 +44,43 @@ func (s *lspServer) setTrace(context *glsp.Context, params *lsp.SetTraceParams) return nil } +// loadMixins reads local mixin files referenced by a document and adds them to storage and index. +func (s *lspServer) loadMixins(uri string) { + doc := s.storage.GetDocument(uri) + if doc == nil { + return + } + + parser := newParser(s.log) + path := normalizePath(uri) + + for _, filename := range parser.getMixinFilenames(doc) { + mixinPath := replacePathFilename(path, strings.TrimPrefix(filename, "-")) + if !util.FileExists(mixinPath) { + s.log.Debugf("mixin target does not exist: %s", mixinPath) + continue + } + + data, err := os.ReadFile(mixinPath) + if err != nil { + s.log.Warningf("failed to read mixin %s: %v", mixinPath, err) + continue + } + + mixinURI := pathToURI(mixinPath) + text := string(data) + + s.storage.AddDocument(mixinURI, text) + s.index.IndexDocument(mixinURI, text) + } +} + func (s *lspServer) textDocumentDidOpen(context *glsp.Context, params *lsp.DidOpenTextDocumentParams) error { s.storage.AddDocument(params.TextDocument.URI, params.TextDocument.Text) + go s.index.IndexDocument(params.TextDocument.URI, params.TextDocument.Text) + go s.loadMixins(params.TextDocument.URI) + return nil } @@ -53,7 +89,9 @@ func (s *lspServer) textDocumentDidChange(context *glsp.Context, params *lsp.Did switch c := change.(type) { case lsp.TextDocumentContentChangeEventWhole: s.storage.AddDocument(params.TextDocument.URI, c.Text) + go s.index.IndexDocument(params.TextDocument.URI, c.Text) + go s.loadMixins(params.TextDocument.URI) case lsp.TextDocumentContentChangeEvent: return errors.New("incremental changes not supported") } @@ -134,6 +172,7 @@ func (h *definitionHandler) findCommandDefinition(doc *string, params *lsp.Defin ) loc := locationForCommand(commandInfo.fileURI, commandInfo.position) + return []lsp.Location{loc}, nil } diff --git a/internal/lsp/handlers_test.go b/internal/lsp/handlers_test.go new file mode 100644 index 0000000..b479c18 --- /dev/null +++ b/internal/lsp/handlers_test.go @@ -0,0 +1,108 @@ +package lsp + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadMixinsStoresAndIndexesMixinDocuments(t *testing.T) { + dir := t.TempDir() + + mainPath := filepath.Join(dir, "lets.yaml") + baseMixinPath := filepath.Join(dir, "lets.base.yaml") + localMixinPath := filepath.Join(dir, "lets.local.yaml") + + baseMixinDoc := `commands: + build: + cmd: echo build` + + localMixinDoc := `commands: + test: + cmd: echo test` + + if err := os.WriteFile(baseMixinPath, []byte(baseMixinDoc), 0o644); err != nil { + t.Fatalf("WriteFile(%s) error = %v", baseMixinPath, err) + } + + if err := os.WriteFile(localMixinPath, []byte(localMixinDoc), 0o644); err != nil { + t.Fatalf("WriteFile(%s) error = %v", localMixinPath, err) + } + + mainDoc := `mixins: + - lets.base.yaml + - -lets.local.yaml +commands: + release: + depends: [build, test] + cmd: echo release` + + server := &lspServer{ + storage: newStorage(), + index: newIndex(logger), + log: logger, + } + + mainURI := pathToURI(mainPath) + server.storage.AddDocument(mainURI, mainDoc) + server.loadMixins(mainURI) + + baseMixinURI := pathToURI(baseMixinPath) + localMixinURI := pathToURI(localMixinPath) + + if got := server.storage.GetDocument(baseMixinURI); got == nil || *got != baseMixinDoc { + t.Fatalf("storage for %s = %#v, want %q", baseMixinURI, got, baseMixinDoc) + } + + if got := server.storage.GetDocument(localMixinURI); got == nil || *got != localMixinDoc { + t.Fatalf("storage for %s = %#v, want %q", localMixinURI, got, localMixinDoc) + } + + buildInfo, ok := server.index.findCommand("build") + if !ok { + t.Fatal("expected build command from mixin to be indexed") + } + + if buildInfo.fileURI != baseMixinURI { + t.Fatalf("build indexed at %s, want %s", buildInfo.fileURI, baseMixinURI) + } + + testInfo, ok := server.index.findCommand("test") + if !ok { + t.Fatal("expected test command from mixin to be indexed") + } + + if testInfo.fileURI != localMixinURI { + t.Fatalf("test indexed at %s, want %s", testInfo.fileURI, localMixinURI) + } +} + +func TestLoadMixinsSkipsMissingFiles(t *testing.T) { + dir := t.TempDir() + + mainPath := filepath.Join(dir, "lets.yaml") + mainURI := pathToURI(mainPath) + + mainDoc := `mixins: + - missing.yaml +commands: + release: + cmd: echo release` + + server := &lspServer{ + storage: newStorage(), + index: newIndex(logger), + log: logger, + } + + server.storage.AddDocument(mainURI, mainDoc) + server.loadMixins(mainURI) + + if got := server.storage.GetDocument(pathToURI(filepath.Join(dir, "missing.yaml"))); got != nil { + t.Fatalf("expected missing mixin to not be stored, got %#v", got) + } + + if _, ok := server.index.findCommand("missing"); ok { + t.Fatal("expected no indexed command for missing mixin") + } +} diff --git a/internal/lsp/index.go b/internal/lsp/index.go index 3ab6145..d335526 100644 --- a/internal/lsp/index.go +++ b/internal/lsp/index.go @@ -1,8 +1,10 @@ package lsp import ( + "maps" "sync" + "github.com/lets-cli/lets/internal/set" "github.com/tliron/commonlog" lsp "github.com/tliron/glsp/protocol_3_16" ) @@ -18,14 +20,14 @@ type index struct { log commonlog.Logger mu sync.RWMutex commands map[string]commandInfo - commandsByURI map[string]map[string]struct{} + commandsByURI map[string]set.Set[string] } func newIndex(log commonlog.Logger) *index { return &index{ log: log, commands: make(map[string]commandInfo), - commandsByURI: make(map[string]map[string]struct{}), + commandsByURI: make(map[string]set.Set[string]), } } @@ -35,15 +37,14 @@ func (i *index) IndexDocument(uri string, doc string) { commands := parser.getCommands(&doc) indexedCommands := make(map[string]commandInfo, len(commands)) - indexedNames := make(map[string]struct{}, len(commands)) + indexedNames := set.NewSet[string]() for _, command := range commands { indexedCommands[command.name] = commandInfo{ fileURI: uri, position: command.position, } - // TODOL maybe use Set - indexedNames[command.name] = struct{}{} + indexedNames.Add(command.name) } i.mu.Lock() @@ -55,9 +56,7 @@ func (i *index) IndexDocument(uri string, doc string) { delete(i.commands, name) } - for name, info := range indexedCommands { - i.commands[name] = info - } + maps.Copy(i.commands, indexedCommands) if len(indexedNames) == 0 { delete(i.commandsByURI, uri) @@ -72,5 +71,6 @@ func (i *index) findCommand(name string) (commandInfo, bool) { defer i.mu.RUnlock() command, ok := i.commands[name] + return command, ok } diff --git a/internal/lsp/storage.go b/internal/lsp/storage.go index fb0bab6..46b5d23 100644 --- a/internal/lsp/storage.go +++ b/internal/lsp/storage.go @@ -1,6 +1,9 @@ package lsp +import "sync" + type storage struct { + mu sync.RWMutex documents map[string]*string } @@ -11,9 +14,15 @@ func newStorage() *storage { } func (s *storage) GetDocument(uri string) *string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.documents[uri] } func (s *storage) AddDocument(uri string, text string) { + s.mu.Lock() + defer s.mu.Unlock() + s.documents[uri] = &text } diff --git a/internal/lsp/treesitter.go b/internal/lsp/treesitter.go index 537f0f7..b3fe5b9 100644 --- a/internal/lsp/treesitter.go +++ b/internal/lsp/treesitter.go @@ -295,6 +295,31 @@ func (p *parser) extractFilenameFromMixins(document *string, position lsp.Positi return "" } +func (p *parser) getMixinFilenames(document *string) []string { + var filenames []string + + executeYAMLQuery(document, ` + (block_mapping_pair + key: (flow_node) @key + value: (block_node + (block_sequence + (block_sequence_item + (flow_node + (plain_scalar + (string_scalar)) @value)))) + (#eq? @key "mixins") + ) + `, func(capture ts.QueryCapture, docBytes []byte) bool { + if capture.Name == "value" { + filenames = append(filenames, capture.Node.Text(docBytes)) + } + + return false + }) + + return filenames +} + 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) @@ -550,7 +575,7 @@ func (p *parser) getCurrentCommand(document *string, position lsp.Position) *Com return nil } -func (p *parser) findCommand(document *string, commandName string) *Command { +func (p *parser) findCommand(document *string, commandName string) *Command { //nolint tree, docBytes, err := parseYAMLDocument(document) if err != nil { return nil diff --git a/internal/lsp/treesitter_test.go b/internal/lsp/treesitter_test.go index 20e8da5..52f4124 100644 --- a/internal/lsp/treesitter_test.go +++ b/internal/lsp/treesitter_test.go @@ -414,7 +414,14 @@ func TestFindCommandDefinitionFromAlias(t *testing.T) { <<: *docs cmd: npm start` - handler := definitionHandler{parser: newParser(logger)} + idx := newIndex(logger) + idx.IndexDocument("file:///tmp/lets.yaml", doc) + + handler := definitionHandler{ + log: logger, + parser: newParser(logger), + index: idx, + } params := &lsp.DefinitionParams{ TextDocumentPositionParams: lsp.TextDocumentPositionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: "file:///tmp/lets.yaml"}, @@ -540,6 +547,24 @@ commands: } } +func TestGetMixinFilenames(t *testing.T) { + doc := `shell: bash +mixins: + - lets.base.yaml + - -lets.local.yaml +commands: + build: + cmd: echo build` + + p := newParser(logger) + got := p.getMixinFilenames(&doc) + want := []string{"lets.base.yaml", "-lets.local.yaml"} + + if !reflect.DeepEqual(got, want) { + t.Fatalf("getMixinFilenames() = %#v, want %#v", got, want) + } +} + func TestDependsHelpersWithBlockAndFlowSequences(t *testing.T) { doc := `shell: bash commands: From 02d11fe363ca9b0e29356be7b02fe38cdf972a50 Mon Sep 17 00:00:00 2001 From: "m.kindritskiy" Date: Thu, 2 Apr 2026 20:41:42 +0300 Subject: [PATCH 3/3] Use single parser in lsp --- internal/lsp/handlers.go | 13 +++++-------- internal/lsp/handlers_test.go | 2 ++ internal/lsp/index.go | 5 +++-- internal/lsp/server.go | 2 ++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 7c17e6f..394e6c4 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -51,10 +51,9 @@ func (s *lspServer) loadMixins(uri string) { return } - parser := newParser(s.log) path := normalizePath(uri) - for _, filename := range parser.getMixinFilenames(doc) { + for _, filename := range s.parser.getMixinFilenames(doc) { mixinPath := replacePathFilename(path, strings.TrimPrefix(filename, "-")) if !util.FileExists(mixinPath) { s.log.Debugf("mixin target does not exist: %s", mixinPath) @@ -211,13 +210,12 @@ func (h *completionHandler) buildDependsCompletions(doc *string, params *lsp.Com func (s *lspServer) textDocumentDefinition(context *glsp.Context, params *lsp.DefinitionParams) (any, error) { definitionHandler := definitionHandler{ log: s.log, - parser: newParser(s.log), + parser: s.parser, index: s.index, } doc := s.storage.GetDocument(params.TextDocument.URI) - p := newParser(s.log) - positionType := p.getPositionType(doc, params.Position) + positionType := s.parser.getPositionType(doc, params.Position) s.log.Debugf( "definition request uri=%s line=%d char=%d type=%s", normalizePath(params.TextDocument.URI), @@ -240,12 +238,11 @@ func (s *lspServer) textDocumentDefinition(context *glsp.Context, params *lsp.De // Returns: []CompletionItem | CompletionList | nil. func (s *lspServer) textDocumentCompletion(context *glsp.Context, params *lsp.CompletionParams) (any, error) { completionHandler := completionHandler{ - parser: newParser(s.log), + parser: s.parser, } doc := s.storage.GetDocument(params.TextDocument.URI) - p := newParser(s.log) - switch p.getPositionType(doc, params.Position) { + switch s.parser.getPositionType(doc, params.Position) { case PositionTypeDepends: return completionHandler.buildDependsCompletions(doc, params) default: diff --git a/internal/lsp/handlers_test.go b/internal/lsp/handlers_test.go index b479c18..41c3ae9 100644 --- a/internal/lsp/handlers_test.go +++ b/internal/lsp/handlers_test.go @@ -39,6 +39,7 @@ commands: server := &lspServer{ storage: newStorage(), + parser: newParser(logger), index: newIndex(logger), log: logger, } @@ -91,6 +92,7 @@ commands: server := &lspServer{ storage: newStorage(), + parser: newParser(logger), index: newIndex(logger), log: logger, } diff --git a/internal/lsp/index.go b/internal/lsp/index.go index d335526..6266b27 100644 --- a/internal/lsp/index.go +++ b/internal/lsp/index.go @@ -18,6 +18,7 @@ type commandInfo struct { type index struct { log commonlog.Logger + parser *parser mu sync.RWMutex commands map[string]commandInfo commandsByURI map[string]set.Set[string] @@ -26,6 +27,7 @@ type index struct { func newIndex(log commonlog.Logger) *index { return &index{ log: log, + parser: newParser(log), commands: make(map[string]commandInfo), commandsByURI: make(map[string]set.Set[string]), } @@ -33,8 +35,7 @@ func newIndex(log commonlog.Logger) *index { // IndexDocument extracts commands from a document and updates the index to reflect that document's current state. func (i *index) IndexDocument(uri string, doc string) { - parser := newParser(i.log) - commands := parser.getCommands(&doc) + commands := i.parser.getCommands(&doc) indexedCommands := make(map[string]commandInfo, len(commands)) indexedNames := set.NewSet[string]() diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 5ca8057..3e664f8 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -18,6 +18,7 @@ type lspServer struct { version string server *server.Server storage *storage + parser *parser index *index log commonlog.Logger } @@ -55,6 +56,7 @@ func Run(ctx context.Context, version string) error { version: version, server: glspServer, storage: newStorage(), + parser: newParser(logger), index: newIndex(logger), log: logger, }