From 216aa53d8cc05de14b45109f92c4e0da02086e6d Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Wed, 20 Aug 2025 14:47:53 -0400 Subject: [PATCH] Suggest file system items correctly when an absolute path is used Signed-off-by: Remy Suen --- CHANGELOG.md | 2 + internal/compose/completion_test.go | 58 +++++++++++++++++++++++++++++ internal/pkg/document/document.go | 15 ++++++++ internal/types/common.go | 4 ++ 4 files changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 717ee1c..57aeb20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ All notable changes to the Docker Language Server will be documented in this fil - textDocument/hover - ignore 4XX errors when hovering over images with a non-standard tag ([#371](https://github.com/docker/docker-language-server/issues/371)) - Compose + - textDocument/completion + - correct file system suggestions if an absolute path is used ([#443](https://github.com/docker/docker-language-server/issues/443)) - textDocument/documentLink - stop returning links for alias nodes in included paths ([#439](https://github.com/docker/docker-language-server/issues/439)) - Bake diff --git a/internal/compose/completion_test.go b/internal/compose/completion_test.go index 0a149cd..529c8f2 100644 --- a/internal/compose/completion_test.go +++ b/internal/compose/completion_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -4995,6 +4996,63 @@ services: } } +func testCompletion_RootFolder(t *testing.T, composeFileURI, rootDir, content string, line, character protocol.UInteger) { + entries, err := os.ReadDir(rootDir) + require.NoError(t, err, "could not read root directory %v", rootDir) + require.True(t, len(entries) > 0, "no entries in %v which makes it hard to validate this test", rootDir) + items := make([]protocol.CompletionItem, len(entries)) + for i := range entries { + items[i] = protocol.CompletionItem{Label: entries[i].Name()} + if entries[i].IsDir() { + items[i].Kind = types.CreateCompletionItemKindPointer(protocol.CompletionItemKindFolder) + } else if entries[i].Type() == os.ModeSymlink { + items[i].Kind = types.CreateCompletionItemKindPointer(protocol.CompletionItemKindReference) + } else { + items[i].Kind = types.CreateCompletionItemKindPointer(protocol.CompletionItemKindFile) + } + } + + manager := document.NewDocumentManager() + hub := hub.NewService() + doc := document.NewComposeDocument(manager, uri.URI(composeFileURI), 1, []byte(content)) + list, err := Completion(context.Background(), &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: composeFileURI}, + Position: protocol.Position{Line: line, Character: character}, + }, + }, manager, &hub, doc) + require.NoError(t, err) + require.Equal(t, &protocol.CompletionList{Items: items}, list) +} + +func TestCompletion_RootFolder(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping on windows as / is not a root folder on Windows") + return + } + + composeFileURI := fmt.Sprintf("file://%v", filepath.Join(os.TempDir(), "compose.yaml")) + testCompletion_RootFolder(t, composeFileURI, "/", ` +services: + s: + volumes: + - /`, 4, 9) +} + +func TestCompletion_WindowsRootFolder(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("skipping on non-Windows systems as it will not have a C:\\ folder") + return + } + + composeFileURI := fmt.Sprintf("file:///%v", filepath.Join(os.TempDir(), "compose.yaml")) + testCompletion_RootFolder(t, composeFileURI, "C:\\", ` +services: + s: + volumes: + - C:\`, 4, 11) +} + func TestCompletion_FileStructure(t *testing.T) { testCases := []struct { name string diff --git a/internal/pkg/document/document.go b/internal/pkg/document/document.go index 614fe6b..073d486 100644 --- a/internal/pkg/document/document.go +++ b/internal/pkg/document/document.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "strings" + "unicode" "github.com/docker/docker-language-server/internal/tliron/glsp/protocol" "github.com/docker/docker-language-server/internal/types" @@ -39,13 +40,27 @@ func NewDocument(mgr *Manager, u uri.URI, identifier protocol.LanguageIdentifier return NewDockerfileDocument(u, version, input) } +// DirectoryForPrefix returns the parent directory to be used given the +// document's path and the prefix string that has been inserted into the +// document thus far. +// +// prefixRequired is true if prefix can just be a name without any +// slashes or backslashes. func DirectoryForPrefix(documentPath DocumentPath, prefix, defaultValue string, prefixRequired bool) string { idx := strings.LastIndex(prefix, "/") if idx == -1 { if prefixRequired { + if len(prefix) > 2 && unicode.IsLetter(rune(prefix[0])) && prefix[1] == ':' { + backslashIdx := strings.LastIndex(prefix, "\\") + if backslashIdx != -1 { + return prefix[0 : backslashIdx+1] + } + } return defaultValue } return documentPath.Folder + } else if prefix[0] == '/' { + return prefix[0 : idx+1] } _, folder := types.Concatenate(documentPath.Folder, prefix[0:idx], documentPath.WSLDollarSignHost) return folder diff --git a/internal/types/common.go b/internal/types/common.go index 20c7f22..fb9a3c3 100644 --- a/internal/types/common.go +++ b/internal/types/common.go @@ -124,6 +124,10 @@ func FileStructureCompletionItems(folder string, hideFiles bool) []protocol.Comp item := protocol.CompletionItem{Label: entry.Name()} item.Kind = CreateCompletionItemKindPointer(protocol.CompletionItemKindFolder) items = append(items, item) + } else if entry.Type() == os.ModeSymlink { + item := protocol.CompletionItem{Label: entry.Name()} + item.Kind = CreateCompletionItemKindPointer(protocol.CompletionItemKindReference) + items = append(items, item) } else if !hideFiles { item := protocol.CompletionItem{Label: entry.Name()} item.Kind = CreateCompletionItemKindPointer(protocol.CompletionItemKindFile)