diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d18067..0c3d37e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to the Docker Language Server will be documented in this fil - Compose - textDocument/completion - check the prefix string before trying to use it for looking up image tags ([#486](https://github.com/docker/docker-language-server/issues/486)) + - textDocument/documentLink + - correct the link range if the string is wrapped in single quotes ([#487](https://github.com/docker/docker-language-server/issues/487)) - Bake - fix parsing error caused by referencing a variable with no value ([#490](https://github.com/docker/docker-language-server/issues/490)) diff --git a/internal/compose/documentLink.go b/internal/compose/documentLink.go index e3792ec..ebc74aa 100644 --- a/internal/compose/documentLink.go +++ b/internal/compose/documentLink.go @@ -18,7 +18,7 @@ import ( func createRange(t *token.Token, length int) protocol.Range { offset := 0 - if t.Type == token.DoubleQuoteType { + if t.Type == token.DoubleQuoteType || t.Type == token.SingleQuoteType { offset = 1 } return protocol.Range{ diff --git a/internal/compose/documentLink_test.go b/internal/compose/documentLink_test.go index a8ce8fb..40d83ae 100644 --- a/internal/compose/documentLink_test.go +++ b/internal/compose/documentLink_test.go @@ -117,6 +117,30 @@ func TestDocumentLink_IncludedFiles(t *testing.T) { }, }, }, + { + name: "included files, string array with single quotes", + content: `include: + - file.yml + - 'file2.yml'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 4}, + End: protocol.Position{Line: 1, Character: 12}, + }, + Target: documentLinkTarget(testsFolder, "file.yml"), + Tooltip: documentLinkTooltip(testsFolder, "file.yml"), + }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 5}, + End: protocol.Position{Line: 2, Character: 14}, + }, + Target: documentLinkTarget(testsFolder, "file2.yml"), + Tooltip: documentLinkTooltip(testsFolder, "file2.yml"), + }, + }, + }, { name: "included files, path as a string", content: `include: @@ -141,6 +165,30 @@ func TestDocumentLink_IncludedFiles(t *testing.T) { }, }, }, + { + name: "included files, path as a string with single quotes", + content: `include: + - path: file.yml + - path: 'file2.yml'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 18}, + }, + Target: documentLinkTarget(testsFolder, "file.yml"), + Tooltip: documentLinkTooltip(testsFolder, "file.yml"), + }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 11}, + End: protocol.Position{Line: 2, Character: 20}, + }, + Target: documentLinkTarget(testsFolder, "file2.yml"), + Tooltip: documentLinkTooltip(testsFolder, "file2.yml"), + }, + }, + }, { name: "included files, attribute misspelt", content: `include: @@ -250,6 +298,34 @@ include: }, }, }, + { + name: "regular file with single quotes", + content: ` +services: + backend: + +include: + - file.yml + - 'file2.yml'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 4}, + End: protocol.Position{Line: 5, Character: 12}, + }, + Target: documentLinkTarget(testsFolder, "file.yml"), + Tooltip: documentLinkTooltip(testsFolder, "file.yml"), + }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 6, Character: 5}, + End: protocol.Position{Line: 6, Character: 14}, + }, + Target: documentLinkTarget(testsFolder, "file2.yml"), + Tooltip: documentLinkTooltip(testsFolder, "file2.yml"), + }, + }, + }, { name: "anchors and aliases", content: ` @@ -401,7 +477,9 @@ include: name: "env_file string attribute", content: ` include: - - env_file: .env`, + - env_file: .env + - env_file: '.env' + - env_file: ".env"`, links: []protocol.DocumentLink{ { Range: protocol.Range{ @@ -411,6 +489,22 @@ include: Target: documentLinkTarget(testsFolder, ".env"), Tooltip: documentLinkTooltip(testsFolder, ".env"), }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 15}, + End: protocol.Position{Line: 3, Character: 19}, + }, + Target: documentLinkTarget(testsFolder, ".env"), + Tooltip: documentLinkTooltip(testsFolder, ".env"), + }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 15}, + End: protocol.Position{Line: 4, Character: 19}, + }, + Target: documentLinkTarget(testsFolder, ".env"), + Tooltip: documentLinkTooltip(testsFolder, ".env"), + }, }, }, { @@ -441,7 +535,9 @@ include: content: ` include: - env_file: - - .env`, + - .env + - '.env' + - ".env"`, links: []protocol.DocumentLink{ { Range: protocol.Range{ @@ -451,6 +547,22 @@ include: Target: documentLinkTarget(testsFolder, ".env"), Tooltip: documentLinkTooltip(testsFolder, ".env"), }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 7}, + End: protocol.Position{Line: 4, Character: 11}, + }, + Target: documentLinkTarget(testsFolder, ".env"), + Tooltip: documentLinkTooltip(testsFolder, ".env"), + }, + { + Range: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 7}, + End: protocol.Position{Line: 5, Character: 11}, + }, + Target: documentLinkTarget(testsFolder, ".env"), + Tooltip: documentLinkTooltip(testsFolder, ".env"), + }, }, }, { @@ -514,7 +626,7 @@ services: }, }, { - name: "quoted string", + name: "double quoted string", content: ` services: test: @@ -530,6 +642,23 @@ services: }, }, }, + { + name: "single quoted string", + content: ` +services: + test: + image: 'alpine'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 12}, + End: protocol.Position{Line: 3, Character: 18}, + }, + Target: types.CreateStringPointer("https://hub.docker.com/_/alpine"), + Tooltip: types.CreateStringPointer("https://hub.docker.com/_/alpine"), + }, + }, + }, { name: "two services", content: ` @@ -706,6 +835,14 @@ services: image: "ghcr.io:tag"`, links: []protocol.DocumentLink{}, }, + { + name: "image: 'ghcr.io:'", + content: ` +services: + test: + image: 'ghcr.io:tag'`, + links: []protocol.DocumentLink{}, + }, { name: "image: mcr.microsoft.com/powershell", content: ` @@ -838,6 +975,14 @@ services: image: "mcr.microsoft.com:"`, links: []protocol.DocumentLink{}, }, + { + name: "image: 'mcr.microsoft.com:'", + content: ` +services: + test: + image: 'mcr.microsoft.com:'`, + links: []protocol.DocumentLink{}, + }, { name: "image: quay.io/prometheus/node-exporter", content: ` @@ -936,6 +1081,14 @@ services: image: "quay.io:"`, links: []protocol.DocumentLink{}, }, + { + name: "image: 'quay.io:'", + content: ` +services: + test: + image: 'quay.io:'`, + links: []protocol.DocumentLink{}, + }, { name: "anchors and aliases to nothing", content: ` @@ -1134,6 +1287,19 @@ services: End: protocol.Position{Line: 4, Character: 32}, }, }, + { + name: `'./Dockerfile2'`, + content: ` +services: + test: + build: + dockerfile: './Dockerfile2'`, + path: filepath.Join(testsFolder, "Dockerfile2"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 19}, + End: protocol.Position{Line: 4, Character: 32}, + }, + }, { name: "anchors and aliases to nothing", content: ` @@ -1308,6 +1474,19 @@ services: End: protocol.Position{Line: 4, Character: 35}, }, }, + { + name: `'./credential-spec.json'`, + content: ` +services: + test: + credential_spec: + file: './credential-spec.json'`, + path: filepath.Join(testsFolder, "credential-spec.json"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 13}, + End: protocol.Position{Line: 4, Character: 35}, + }, + }, { name: "anchors and aliases to nothing", content: ` @@ -1481,7 +1660,7 @@ services: }, }, { - name: "quoted string value \"./.env\"", + name: "double quoted string value \"./.env\"", content: ` services: test: @@ -1492,6 +1671,18 @@ services: End: protocol.Position{Line: 3, Character: 21}, }, }, + { + name: "single quoted string value './.env'", + content: ` +services: + test: + env_file: './.env'`, + path: filepath.Join(testsFolder, "./.env"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 15}, + End: protocol.Position{Line: 3, Character: 21}, + }, + }, { name: "attribute value is null", content: ` @@ -1500,7 +1691,7 @@ services: env_file: null`, }, { - name: "array items", + name: "array item as a string", content: ` services: test: @@ -1512,6 +1703,32 @@ services: End: protocol.Position{Line: 4, Character: 14}, }, }, + { + name: "array item as a double quoted string", + content: ` +services: + test: + env_file: + - "./.env"`, + path: filepath.Join(testsFolder, "./.env"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 9}, + End: protocol.Position{Line: 4, Character: 15}, + }, + }, + { + name: "array item as a single quoted string", + content: ` +services: + test: + env_file: + - './.env'`, + path: filepath.Join(testsFolder, "./.env"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 9}, + End: protocol.Position{Line: 4, Character: 15}, + }, + }, { name: "array item is null", content: ` @@ -1671,7 +1888,7 @@ func TestDocumentLink_ServiceExtendsFileLinks(t *testing.T) { linkRange protocol.Range }{ { - name: "no anchors", + name: "string value", content: ` services: test2: @@ -1684,6 +1901,34 @@ services: End: protocol.Position{Line: 5, Character: 32}, }, }, + { + name: "string value in single quotes", + content: ` +services: + test2: + extends: + service: test + file: './compose.other.yaml'`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 13}, + End: protocol.Position{Line: 5, Character: 33}, + }, + }, + { + name: "string value in double quotes", + content: ` +services: + test2: + extends: + service: test + file: "./compose.other.yaml"`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 13}, + End: protocol.Position{Line: 5, Character: 33}, + }, + }, { name: "anchors and aliases to nothing", content: ` @@ -1821,6 +2066,7 @@ services: }) } } + func TestDocumentLink_ServiceLabelFileLinks(t *testing.T) { testsFolder := filepath.Join(os.TempDir(), t.Name()) composeStringURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(testsFolder, "compose.yaml")), "/")) @@ -1867,6 +2113,18 @@ services: End: protocol.Position{Line: 3, Character: 29}, }, }, + { + name: "single quoted string value './app.labels'", + content: ` +services: + test: + label_file: './app.labels'`, + path: filepath.Join(testsFolder, "./app.labels"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 17}, + End: protocol.Position{Line: 3, Character: 29}, + }, + }, { name: "attribute value is null", content: ` @@ -2065,6 +2323,32 @@ services: End: protocol.Position{Line: 4, Character: 15}, }, }, + { + name: "mount local file (string in single quotes)", + content: ` +services: + test: + volumes: + - './a.txt:/mount/a.txt'`, + path: filepath.Join(testsFolder, "./a.txt"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 9}, + End: protocol.Position{Line: 4, Character: 16}, + }, + }, + { + name: "mount local file (string in double quotes)", + content: ` +services: + test: + volumes: + - "./a.txt:/mount/a.txt"`, + path: filepath.Join(testsFolder, "./a.txt"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 9}, + End: protocol.Position{Line: 4, Character: 16}, + }, + }, { name: "mount local file (string is anchored)", content: ` @@ -2266,6 +2550,18 @@ configs: End: protocol.Position{Line: 3, Character: 23}, }, }, + { + name: `'./httpd.conf'`, + content: ` +configs: + test: + file: './httpd.conf'`, + path: filepath.Join(testsFolder, "httpd.conf"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 11}, + End: protocol.Position{Line: 3, Character: 23}, + }, + }, { name: "anchors and aliases to nothing", content: ` @@ -2393,6 +2689,18 @@ secrets: End: protocol.Position{Line: 3, Character: 24}, }, }, + { + name: `'./server.cert'`, + content: ` +secrets: + test: + file: './server.cert'`, + path: filepath.Join(testsFolder, "server.cert"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 11}, + End: protocol.Position{Line: 3, Character: 24}, + }, + }, { name: "anchors and aliases to nothing", content: ` @@ -2543,6 +2851,23 @@ models: }, }, }, + { + name: "'ai/llama3.3'", + content: ` +models: + modelA: + model: 'ai/llama3.3'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 12}, + End: protocol.Position{Line: 3, Character: 23}, + }, + Target: types.CreateStringPointer("https://hub.docker.com/r/ai/llama3.3"), + Tooltip: types.CreateStringPointer("https://hub.docker.com/r/ai/llama3.3"), + }, + }, + }, { name: "\"ai/llama3.3:\"", content: ` @@ -2560,6 +2885,23 @@ models: }, }, }, + { + name: "'ai/llama3.3:'", + content: ` +models: + modelA: + model: 'ai/llama3.3:'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 12}, + End: protocol.Position{Line: 3, Character: 23}, + }, + Target: types.CreateStringPointer("https://hub.docker.com/r/ai/llama3.3"), + Tooltip: types.CreateStringPointer("https://hub.docker.com/r/ai/llama3.3"), + }, + }, + }, { name: "hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF", content: ` @@ -2611,6 +2953,23 @@ models: }, }, }, + { + name: "'hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF'", + content: ` +models: + modelA: + model: 'hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 12}, + End: protocol.Position{Line: 3, Character: 54}, + }, + Target: types.CreateStringPointer("https://hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF"), + Tooltip: types.CreateStringPointer("https://hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF"), + }, + }, + }, { name: "\"hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF:\"", content: ` @@ -2628,6 +2987,23 @@ models: }, }, }, + { + name: "'hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF:'", + content: ` +models: + modelA: + model: 'hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF:'`, + links: []protocol.DocumentLink{ + { + Range: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 12}, + End: protocol.Position{Line: 3, Character: 54}, + }, + Target: types.CreateStringPointer("https://hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF"), + Tooltip: types.CreateStringPointer("https://hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF"), + }, + }, + }, { name: "hf.co", content: ` @@ -2644,6 +3020,14 @@ models: model: "hf.co:"`, links: []protocol.DocumentLink{}, }, + { + name: "'hf.co:'", + content: ` +models: + modelA: + model: 'hf.co:'`, + links: []protocol.DocumentLink{}, + }, { name: "anchors and aliases to nothing", content: `