diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d66fa..b86ef34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,21 @@ All notable changes to the Docker Language Server will be documented in this fil - Compose - update schema to the latest version + - textDocument/completion + - support completing model object names ([#343](https://github.com/docker/docker-language-server/issues/343)) + - textDocument/definition + - support jumping to referenced model objects ([#343](https://github.com/docker/docker-language-server/issues/343)) + - textDocument/documentHighlight + - support highlighting referenced models objects ([#343](https://github.com/docker/docker-language-server/issues/343)) - textDocument/documentLink - support recursing into anchors when searching for document links ([#329](https://github.com/docker/docker-language-server/issues/329)) - return document links for the `file` attribute of a service object's `credential_spec` ([#338](https://github.com/docker/docker-language-server/issues/338)) + - textDocument/documentSymbol + - show model objects in the document symbol tree ([#343](https://github.com/docker/docker-language-server/issues/343)) + - textDocument/prepareRename + - allow preparing rename on model objects ([#343](https://github.com/docker/docker-language-server/issues/343)) + - textDocument/rename + - support renaming model objects ([#343](https://github.com/docker/docker-language-server/issues/343)) ### Fixed diff --git a/internal/compose/completion.go b/internal/compose/completion.go index 49bbb1c..912f19a 100644 --- a/internal/compose/completion.go +++ b/internal/compose/completion.go @@ -479,6 +479,7 @@ func createBuildStageItems(params *protocol.CompletionParams, manager *document. func dependencyCompletionItems(file *ast.File, u *url.URL, path []*ast.MappingValueNode, params *protocol.CompletionParams, prefixLength protocol.UInteger) []protocol.CompletionItem { dependency := map[string]string{ "depends_on": "services", + "models": "models", "networks": "networks", } for serviceAttribute, dependencyType := range dependency { diff --git a/internal/compose/completion_test.go b/internal/compose/completion_test.go index 9b10556..cef6aee 100644 --- a/internal/compose/completion_test.go +++ b/internal/compose/completion_test.go @@ -3964,6 +3964,28 @@ secrets: }, }, }, + { + name: "model names suggested", + content: ` +services: + app: + image: app + models: + - +models: + ai_model: + model: ai/model`, + line: 5, + character: 8, + list: &protocol.CompletionList{ + Items: []protocol.CompletionItem{ + { + Label: "ai_model", + TextEdit: textEdit("ai_model", 5, 8, 0), + }, + }, + }, + }, } composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/")) diff --git a/internal/compose/definition_test.go b/internal/compose/definition_test.go index b6cf3a5..24b7a3a 100644 --- a/internal/compose/definition_test.go +++ b/internal/compose/definition_test.go @@ -144,6 +144,32 @@ func TestDefinition_Secrets(t *testing.T) { } } +func TestDefinition_Models(t *testing.T) { + composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/")) + u := uri.URI(composeFileURI) + for _, tc := range modelReferenceTestCases { + doc := document.NewComposeDocument(document.NewDocumentManager(), u, 1, []byte(tc.content)) + params := protocol.DefinitionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI}, + Position: protocol.Position{Line: tc.line, Character: tc.character}, + }, + } + + t.Run(fmt.Sprintf("%v (Location)", tc.name), func(t *testing.T) { + locations, err := Definition(context.Background(), false, doc, ¶ms) + require.NoError(t, err) + require.Equal(t, tc.locations(composeFileURI), locations) + }) + + t.Run(fmt.Sprintf("%v (LocationLink)", tc.name), func(t *testing.T) { + links, err := Definition(context.Background(), true, doc, ¶ms) + require.NoError(t, err) + require.Equal(t, tc.links(composeFileURI), links) + }) + } +} + func TestDefinition_Fragments(t *testing.T) { composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/")) u := uri.URI(composeFileURI) diff --git a/internal/compose/documentHighlight.go b/internal/compose/documentHighlight.go index 1b2f15f..21f33ff 100644 --- a/internal/compose/documentHighlight.go +++ b/internal/compose/documentHighlight.go @@ -277,10 +277,12 @@ func DocumentHighlights(doc document.ComposeDocument, position protocol.Position var volumeRefs []*token.Token var configRefs []*token.Token var secretRefs []*token.Token + var modelRefs []*token.Token var networkDeclarations []*token.Token var volumeDeclarations []*token.Token var configDeclarations []*token.Token var secretDeclarations []*token.Token + var modelDeclarations []*token.Token for _, node := range mappingNode.Values { name, value := convertTopLevelNode(node) if name == nil || value == nil { @@ -299,6 +301,7 @@ func DocumentHighlights(doc document.ComposeDocument, position protocol.Position networkRefs = serviceDependencyReferences(value, "networks", false) configRefs = serviceDependencyReferences(value, "configs", true) secretRefs = serviceDependencyReferences(value, "secrets", true) + modelRefs = serviceDependencyReferences(value, "models", false) volumeRefs = volumeReferences(value) case "networks": networkDeclarations = declarations(value) @@ -308,6 +311,8 @@ func DocumentHighlights(doc document.ComposeDocument, position protocol.Position configDeclarations = declarations(value) case "secrets": secretDeclarations = declarations(value) + case "models": + modelDeclarations = declarations(value) } } name, highlights := highlightReferences("networks", networkRefs, networkDeclarations, line, character) @@ -326,6 +331,10 @@ func DocumentHighlights(doc document.ComposeDocument, position protocol.Position if len(highlights.documentHighlights) > 0 { return name, highlights } + name, highlights = highlightReferences("models", modelRefs, modelDeclarations, line, character) + if len(highlights.documentHighlights) > 0 { + return name, highlights + } fragments := []protocol.DocumentHighlight{} anchor, aliases := fragmentReference(mappingNode, line, character) diff --git a/internal/compose/documentHighlight_test.go b/internal/compose/documentHighlight_test.go index 453c337..5128a51 100644 --- a/internal/compose/documentHighlight_test.go +++ b/internal/compose/documentHighlight_test.go @@ -4539,6 +4539,763 @@ func TestDocumentHighlight_Secrets(t *testing.T) { } } +var modelReferenceTestCases = []struct { + name string + content string + line protocol.UInteger + character protocol.UInteger + locations func(protocol.DocumentUri) any + links func(protocol.DocumentUri) any + ranges []protocol.DocumentHighlight + renameEdits func(protocol.DocumentUri) *protocol.WorkspaceEdit + prepareRename *protocol.Range +}{ + { + name: "write highlight on a models object", + content: ` +models: + test:`, + line: 2, + character: 4, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(2, 2, 2, 6, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, + }, + { + name: "write highlight on a model with the top level models object value anchored", + content: ` +models: &anchor + test:`, + line: 2, + character: 4, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(2, 2, 2, 6, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 2}, + End: protocol.Position{Line: 2, Character: 6}, + }, + }, + { + name: "read highlight on an undefined models array item", + content: ` +services: + test: + models: + - test2`, + line: 4, + character: 10, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + name: "read highlight on an undefined models array item with the models array anchored", + content: ` +services: + test: + &anchor models: + - test2`, + line: 4, + character: 10, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + name: "read highlight on an undefined models array item with the models array value anchor", + content: ` +services: + test: + models: &anchor + - test2`, + line: 4, + character: 10, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + name: "read highlight on an undefined models array item with a string value anchored", + content: ` +services: + test: + models: + - &anchor test2`, + line: 4, + character: 18, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 16, 4, 21, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 16}, + End: protocol.Position{Line: 4, Character: 21}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 16}, + End: protocol.Position{Line: 4, Character: 21}, + }, + }, + { + name: "read highlight on an undefined models object", + content: ` +services: + test: + models: + test2:`, + line: 4, + character: 9, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 6, 4, 11, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + { + name: "read highlight on an undefined models object with the object itself anchored", + content: ` +services: + test: + models: &anchor + test2:`, + line: 4, + character: 9, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 6, 4, 11, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + { + name: "read highlight on an undefined volumes array item, duplicated", + content: ` +services: + test: + models: + - test2 + - test2`, + line: 4, + character: 10, + locations: func(u protocol.DocumentUri) any { return nil }, + links: func(u protocol.DocumentUri) any { return nil }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead), + documentHighlight(5, 8, 5, 13, protocol.DocumentHighlightKindRead), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 8}, + End: protocol.Position{Line: 5, Character: 13}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + name: "read/write highlight on a models array item (cursor on read)", + content: ` +services: + test: + models: + - test2 +models: + test2:`, + line: 4, + character: 10, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead), + documentHighlight(6, 2, 6, 7, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + name: "read/write highlight on a models array item (cursor on write)", + content: ` +services: + test: + models: + - test2 +models: + test2:`, + line: 6, + character: 5, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, &protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 8, 4, 13, protocol.DocumentHighlightKindRead), + documentHighlight(6, 2, 6, 7, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 8}, + End: protocol.Position{Line: 4, Character: 13}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, + }, + { + name: "read/write highlight on a models object (cursor on read)", + content: ` +services: + test: + models: + test2: +models: + test2:`, + line: 4, + character: 10, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 6, 4, 11, protocol.DocumentHighlightKindRead), + documentHighlight(6, 2, 6, 7, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + { + name: "read/write highlight on a models object (cursor on write)", + content: ` +services: + test: + models: + test2: +models: + test2:`, + line: 6, + character: 5, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, &protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(4, 6, 4, 11, protocol.DocumentHighlightKindRead), + documentHighlight(6, 2, 6, 7, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 6}, + End: protocol.Position{Line: 4, Character: 11}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 6, Character: 2}, + End: protocol.Position{Line: 6, Character: 7}, + }, + }, + { + name: "anchor name conflicts with a model (cursor on anchor)", + content: ` +services: + first: &second + image: scratch + models: + - second +models: + second:`, + line: 2, + character: 13, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 10}, + End: protocol.Position{Line: 2, Character: 16}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 2, Character: 10}, + End: protocol.Position{Line: 2, Character: 16}, + }, &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 10}, + End: protocol.Position{Line: 2, Character: 16}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(2, 10, 2, 16, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 10}, + End: protocol.Position{Line: 2, Character: 16}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 10}, + End: protocol.Position{Line: 2, Character: 16}, + }, + }, + { + name: "anchor name conflicts with a model (cursor on read reference)", + content: ` +services: + first: &second + image: scratch + models: + - second +models: + second:`, + line: 5, + character: 10, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 8}, + End: protocol.Position{Line: 5, Character: 14}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(5, 8, 5, 14, protocol.DocumentHighlightKindRead), + documentHighlight(7, 2, 7, 8, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 8}, + End: protocol.Position{Line: 5, Character: 14}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 8}, + End: protocol.Position{Line: 5, Character: 14}, + }, + }, + { + name: "anchor name conflicts with a model (cursor on write reference)", + content: ` +services: + first: &second + image: scratch + models: + - second +models: + second:`, + line: 7, + character: 6, + locations: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(false, protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, nil, u) + }, + links: func(u protocol.DocumentUri) any { + return types.CreateDefinitionResult(true, protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, &protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, u) + }, + ranges: []protocol.DocumentHighlight{ + documentHighlight(5, 8, 5, 14, protocol.DocumentHighlightKindRead), + documentHighlight(7, 2, 7, 8, protocol.DocumentHighlightKindWrite), + }, + renameEdits: func(u protocol.DocumentUri) *protocol.WorkspaceEdit { + return &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + u: { + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 8}, + End: protocol.Position{Line: 5, Character: 14}, + }, + }, + { + NewText: "newName", + Range: protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, + }, + }, + }, + } + }, + prepareRename: &protocol.Range{ + Start: protocol.Position{Line: 7, Character: 2}, + End: protocol.Position{Line: 7, Character: 8}, + }, + }, +} + +func TestDocumentHighlight_Models(t *testing.T) { + composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/")) + u := uri.URI(composeFileURI) + for _, tc := range modelReferenceTestCases { + t.Run(tc.name, func(t *testing.T) { + doc := document.NewComposeDocument(document.NewDocumentManager(), u, 1, []byte(tc.content)) + ranges, err := DocumentHighlight(doc, protocol.Position{Line: tc.line, Character: tc.character}) + require.NoError(t, err) + require.Equal(t, tc.ranges, ranges) + }) + } +} + var fragmentTestCases = []struct { name string content string diff --git a/internal/compose/documentSymbol.go b/internal/compose/documentSymbol.go index 122aedc..57de22b 100644 --- a/internal/compose/documentSymbol.go +++ b/internal/compose/documentSymbol.go @@ -15,6 +15,7 @@ var symbolKinds = map[string]protocol.SymbolKind{ "volumes": protocol.SymbolKindFile, "configs": protocol.SymbolKindVariable, "secrets": protocol.SymbolKindKey, + "models": protocol.SymbolKindModule, } func findSymbols(value string, n *ast.MappingValueNode, mapping map[string]protocol.SymbolKind) (result []any) { diff --git a/internal/compose/documentSymbol_test.go b/internal/compose/documentSymbol_test.go index 1c45b63..d5cc71a 100644 --- a/internal/compose/documentSymbol_test.go +++ b/internal/compose/documentSymbol_test.go @@ -155,6 +155,25 @@ func TestDocumentSymbol(t *testing.T) { }, }, }, + { + name: "models block", + content: `models: + ai_model:`, + symbols: []*protocol.DocumentSymbol{ + { + Name: "ai_model", + Kind: protocol.SymbolKindModule, + Range: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 2}, + End: protocol.Position{Line: 1, Character: 10}, + }, + SelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 2}, + End: protocol.Position{Line: 1, Character: 10}, + }, + }, + }, + }, { name: "include array", content: `include: diff --git a/internal/compose/rename_test.go b/internal/compose/rename_test.go index 952a0eb..574a219 100644 --- a/internal/compose/rename_test.go +++ b/internal/compose/rename_test.go @@ -114,6 +114,25 @@ func TestRename_Secrets(t *testing.T) { } } +func TestRename_Models(t *testing.T) { + composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/")) + u := uri.URI(composeFileURI) + for _, tc := range modelReferenceTestCases { + t.Run(tc.name, func(t *testing.T) { + doc := document.NewComposeDocument(document.NewDocumentManager(), u, 1, []byte(tc.content)) + edits, err := Rename(doc, &protocol.RenameParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI}, + Position: protocol.Position{Line: tc.line, Character: tc.character}, + }, + NewName: "newName", + }) + require.NoError(t, err) + require.Equal(t, tc.renameEdits(composeFileURI), edits) + }) + } +} + func TestRename_Fragments(t *testing.T) { composeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(os.TempDir(), "compose.yaml")), "/")) u := uri.URI(composeFileURI)