From f3e8f4c74fa0cf960464401097b6e77e1ce6f5d1 Mon Sep 17 00:00:00 2001 From: Artem Lytkin Date: Thu, 20 Aug 2026 09:05:19 +0300 Subject: [PATCH] util+buffer: Accept both path separators on Windows Windows accepts `/` as well as `\`, but `ReplaceHome()` only looked for `/` when determining where the `~` component ends, while `FileComplete()` only split on `os.PathSeparator` and always appended `os.PathSeparator` to directory suggestions. As a result `~\foo.txt` was rejected with "Could not find user", neither `~/foo`, `~\foo` nor `C:/Users/User/foo` could be completed, and completing a directory mixed the separators. Normalize the path with `filepath.ToSlash()` before splitting and keep completing with the separator already in use. Both are a no-op on Unix, where `\` is a regular filename character. Fixes #3828 --- internal/buffer/autocomplete.go | 11 +++++++- internal/buffer/autocomplete_test.go | 38 ++++++++++++++++++++++++++++ internal/util/util.go | 2 +- internal/util/util_test.go | 26 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 internal/buffer/autocomplete_test.go diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 6dcda4da51..d8a228f534 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -4,6 +4,7 @@ import ( "bytes" "io/fs" "os" + "path/filepath" "sort" "strings" @@ -106,8 +107,16 @@ func FileComplete(b *Buffer) ([]string, []string) { c := b.GetActiveCursor() input, argstart := b.GetArg() + // Windows accepts both separators, so keep completing with the one that is + // already in use instead of mixing them sep := string(os.PathSeparator) - dirs := strings.Split(input, sep) + if i := strings.LastIndexAny(input, "/"+sep); i >= 0 { + sep = input[i : i+1] + } + + // and normalize the input before splitting it, otherwise a forward slash + // separated path is treated as a single name + dirs := strings.Split(filepath.ToSlash(input), "/") var files []fs.DirEntry var err error diff --git a/internal/buffer/autocomplete_test.go b/internal/buffer/autocomplete_test.go new file mode 100644 index 0000000000..a71eb18e00 --- /dev/null +++ b/internal/buffer/autocomplete_test.go @@ -0,0 +1,38 @@ +package buffer + +import ( + "os" + "path/filepath" + "testing" + + "github.com/micro-editor/micro/v2/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestFileComplete(t *testing.T) { + dir := t.TempDir() + f, err := os.Create(filepath.Join(dir, "foo.txt")) + if err != nil { + t.Fatal(err) + } + f.Close() + if err := os.Mkdir(filepath.Join(dir, "bar"), 0700); err != nil { + t.Fatal(err) + } + + complete := func(input string) []string { + b := NewBufferFromString(input, "", BTDefault) + b.GetActiveCursor().X = util.CharacterCountInString(input) + _, suggestions := FileComplete(b) + return suggestions + } + + // Both separators are valid on Windows, so completion has to work with + // either one of them and has to stay with the one already in use. + for _, sep := range []string{"/", string(os.PathSeparator)} { + prefix := "open " + filepath.ToSlash(dir) + sep + + assert.Equal(t, []string{"foo.txt"}, complete(prefix+"fo")) + assert.Equal(t, []string{"bar" + sep}, complete(prefix+"ba")) + } +} diff --git a/internal/util/util.go b/internal/util/util.go index 9838b025fb..cbca9beedb 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -398,7 +398,7 @@ func ReplaceHome(path string) (string, error) { var userData *user.User var err error - homeString := strings.Split(path, "/")[0] + homeString := strings.Split(filepath.ToSlash(path), "/")[0] if homeString == "~" { userData, err = user.Current() if err != nil { diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 454b5c7963..c23e183227 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -1,6 +1,8 @@ package util import ( + "os" + "os/user" "testing" "github.com/stretchr/testify/assert" @@ -13,6 +15,30 @@ func TestStringWidth(t *testing.T) { assert.Equal(t, 26, n) } +func TestReplaceHome(t *testing.T) { + usr, err := user.Current() + if err != nil { + t.Fatal(err) + } + home := usr.HomeDir + + path, err := ReplaceHome("~") + assert.NoError(t, err) + assert.Equal(t, home, path) + + // Both separators are valid on Windows, so `~` has to be recognized + // regardless of which one follows it. + for _, sep := range []string{"/", string(os.PathSeparator)} { + path, err = ReplaceHome("~" + sep + "foo.txt") + assert.NoError(t, err) + assert.Equal(t, home+sep+"foo.txt", path) + } + + path, err = ReplaceHome("foo~bar") + assert.NoError(t, err) + assert.Equal(t, "foo~bar", path) +} + func TestSliceVisualEnd(t *testing.T) { s := []byte("\thello") slc, n, _ := SliceVisualEnd(s, 2, 4)