Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
* `[Fixed]` Resolve `go to definition` from command references such as `ref: build` 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)
Expand Down
3 changes: 1 addition & 2 deletions docs/docs/ide_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Lsp support includes:

- [x] Goto definition
- Navigate to definitions of mixins files
- Navigate to definitions of command from `depends`
- Navigate to definitions of command from `depends`, `ref`, and YAML merge aliases
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (typo): Consider using the plural 'commands' instead of 'command' for grammatical agreement.

Now that you mention multiple sources (depends, ref, and YAML merge aliases), please use the plural form “commands” to keep the sentence grammatically correct.

Suggested change
- Navigate to definitions of command from `depends`, `ref`, and YAML merge aliases
- Navigate to definitions of commands from `depends`, `ref`, and YAML merge aliases

- [x] Completion
- Complete commands in depends
- [ ] Diagnostics
Expand Down Expand Up @@ -122,4 +122,3 @@ servers = {
},
}
```

2 changes: 1 addition & 1 deletion internal/lsp/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func (s *lspServer) textDocumentDefinition(context *glsp.Context, params *lsp.De
switch positionType {
case PositionTypeMixins:
return definitionHandler.findMixinsDefinition(doc, params)
case PositionTypeDepends, PositionTypeCommandAlias:
case PositionTypeDepends, PositionTypeCommandAlias, PositionTypeRef:
return definitionHandler.findCommandDefinition(doc, params)
default:
s.log.Debugf("definition request ignored: unsupported cursor position")
Expand Down
51 changes: 49 additions & 2 deletions internal/lsp/treesitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
PositionTypeMixins PositionType = iota
PositionTypeDepends
PositionTypeCommandAlias
PositionTypeRef
PositionTypeNone
)

Expand All @@ -26,6 +27,8 @@ func (p PositionType) String() string {
return "depends"
case PositionTypeCommandAlias:
return "command_alias"
case PositionTypeRef:
return "ref"
default:
return "none"
}
Expand Down Expand Up @@ -123,6 +126,8 @@ func (p *parser) getPositionType(document *string, position lsp.Position) Positi
return PositionTypeDepends
} else if p.inCommandAliasPosition(document, position) {
return PositionTypeCommandAlias
} else if p.inRefPosition(document, position) {
return PositionTypeRef
}

return PositionTypeNone
Expand Down Expand Up @@ -249,6 +254,20 @@ func (p *parser) inCommandAliasPosition(document *string, position lsp.Position)
})
}

func (p *parser) inRefPosition(document *string, position lsp.Position) bool {
return executeYAMLQuery(document, `
(block_mapping_pair
key: (flow_node) @keyref
value: (flow_node
(plain_scalar
(string_scalar)) @reference)
(#eq? @keyref "ref")
)
`, func(capture ts.QueryCapture, _ []byte) bool {
return capture.Name == "reference" && isCursorWithinNode(capture.Node, position)
})
}

func (p *parser) extractFilenameFromMixins(document *string, position lsp.Position) string {
tree, docBytes, err := parseYAMLDocument(document)
if err != nil {
Expand Down Expand Up @@ -326,9 +345,14 @@ func (p *parser) extractCommandReference(document *string, position lsp.Position
return commandName
}

commandName := p.extractAliasCommandReference(document, position)
if commandName != "" {
if commandName := p.extractAliasCommandReference(document, position); commandName != "" {
p.log.Debugf("resolved command reference from alias: %q", commandName)
return commandName
}

commandName := p.extractRefCommandReference(document, position)
if commandName != "" {
p.log.Debugf("resolved command reference from ref: %q", commandName)
}

return commandName
Expand Down Expand Up @@ -400,6 +424,29 @@ func (p *parser) extractAliasCommandReference(document *string, position lsp.Pos
return commandName
}

func (p *parser) extractRefCommandReference(document *string, position lsp.Position) string {
var commandName string

executeYAMLQuery(document, `
(block_mapping_pair
key: (flow_node) @keyref
value: (flow_node
(plain_scalar
(string_scalar)) @reference)
(#eq? @keyref "ref")
)
`, func(capture ts.QueryCapture, docBytes []byte) bool {
if capture.Name == "reference" && isCursorWithinNode(capture.Node, position) {
commandName = capture.Node.Text(docBytes)
return true
}

return false
})

return commandName
}

func (p *parser) findCommandNameByAnchor(document *string, anchorName string) string {
tree, docBytes, err := parseYAMLDocument(document)
if err != nil {
Expand Down
84 changes: 83 additions & 1 deletion internal/lsp/treesitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,37 @@ func TestDetectCommandAliasPosition(t *testing.T) {
}
}

func TestDetectRefPosition(t *testing.T) {
doc := `commands:
build:
cmd: echo Build
deploy:
ref: build
args: --prod`

tests := []struct {
pos lsp.Position
want bool
}{
{pos: pos(4, 7), want: false},
{pos: pos(4, 9), want: true},
{pos: pos(4, 12), want: true},
{pos: pos(5, 8), want: false},
}

p := newParser(logger)
for i, tt := range tests {
got := p.inRefPosition(&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, 11)); got != PositionTypeRef {
t.Fatalf("expected PositionTypeRef, got %v", got)
}
}

func TestGetCommands(t *testing.T) {
doc := `shell: bash
mixins:
Expand Down Expand Up @@ -379,7 +410,9 @@ func TestExtractCommandReference(t *testing.T) {
run-test:
<<: *test
depends: [build, test]
cmd: echo Run`
cmd: echo Run
deploy:
ref: build`

tests := []struct {
name string
Expand All @@ -392,6 +425,7 @@ func TestExtractCommandReference(t *testing.T) {
{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: ""},
{name: "ref command value", pos: pos(12, 10), want: "build"},
}

p := newParser(logger)
Expand Down Expand Up @@ -454,6 +488,54 @@ func TestFindCommandDefinitionFromAlias(t *testing.T) {
}
}

func TestFindCommandDefinitionFromRef(t *testing.T) {
doc := `commands:
build:
cmd: echo Build
deploy:
ref: build
args: --prod`

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"},
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 TestFindCommandNameByAnchor(t *testing.T) {
doc := `commands:
publish-docs: &docs
Expand Down
Loading