diff --git a/kitty_tests/completion.py b/kitty_tests/completion.py index e4cb7d839..4f6c6f7a2 100644 --- a/kitty_tests/completion.py +++ b/kitty_tests/completion.py @@ -89,9 +89,9 @@ def completion(self: TestCompletion, tdir: str): add('kitty x', all_words()) add('kitty e', all_words('exe1')) - add('kitty ./', all_words('./bin', './bin/exe1', './sub', './exe2', './sub/exe3')) + add('kitty ./', all_words('./bin/', './sub/', './exe2')) add('kitty ./e', all_words('./exe2')) - add('kitty ./s', all_words('./sub', './sub/exe3')) + add('kitty ./s', all_words('./sub/')) add('kitty ~', all_words('~/exe3')) add('kitty ~/', all_words('~/exe3')) add('kitty ~/e', all_words('~/exe3')) @@ -99,7 +99,7 @@ def completion(self: TestCompletion, tdir: str): add('kitty @ goto-layout ', has_words('tall', 'fat')) add('kitty @ goto-layout spli', all_words('splits')) add('kitty @ goto-layout f f', all_words()) - add('kitty @ set-window-logo ', all_words('exe-not2.jpeg', 'sub/exe-not3.png')) + add('kitty @ set-window-logo ', all_words('exe-not2.jpeg', 'sub/', 'bin/')) add('kitty @ set-window-logo e', all_words('exe-not2.jpeg')) add('kitty @ set-window-logo e e', all_words()) diff --git a/tools/completion/files.go b/tools/completion/files.go index 294582e82..0d63f0dd6 100644 --- a/tools/completion/files.go +++ b/tools/completion/files.go @@ -4,7 +4,6 @@ package completion import ( "fmt" - "io/fs" "os" "path/filepath" "strings" @@ -25,39 +24,82 @@ func absolutize_path(path string) string { return path } -type CompleteFilesCallback func(completion_candidate, abspath string, d fs.DirEntry) error +type FileEntry struct { + name, completion_candidate, abspath string + mode os.FileMode + is_dir, is_symlink, is_empty_dir bool +} -func complete_files(prefix string, callback CompleteFilesCallback) error { - base := prefix - if base != "~" && base != "./" && base != "/" { - // cant use filepath.Dir() as it calls filepath.Clean() which - // can cause base to no longer match prefix - idx := strings.LastIndex(base, string(os.PathSeparator)) - if idx > 0 { - base = base[:idx] +func complete_files(prefix string, callback func(*FileEntry)) error { + location := absolutize_path(prefix) + base_dir := "" + joinable_prefix := "" + switch prefix { + case ".": + base_dir = "." + joinable_prefix = "" + case "./": + base_dir = "." + joinable_prefix = "./" + case "/": + base_dir = "/" + joinable_prefix = "/" + case "~": + base_dir = location + joinable_prefix = "~/" + case "": + base_dir = "." + joinable_prefix = "" + default: + if strings.HasSuffix(prefix, utils.Sep) { + base_dir = location + joinable_prefix = prefix } else { - base = "" + idx := strings.LastIndex(prefix, utils.Sep) + if idx > 0 { + joinable_prefix = prefix[:idx+1] + base_dir = filepath.Dir(location) + } } } - num := 0 - utils.WalkWithSymlink(base, func(path, abspath string, d fs.DirEntry, err error) error { - if err != nil { - return fs.SkipDir - } - num++ - if num == 1 { - return nil - } - completion_candidate := path - if strings.HasPrefix(completion_candidate, prefix) { - return callback(completion_candidate, abspath, d) - } - if d.IsDir() { - return fs.SkipDir - } - return nil - }, absolutize_path) + if base_dir == "" { + base_dir = "." + } + // fmt.Printf("prefix=%#v base_dir=%#v joinable_prefix=%#v\n", prefix, base_dir, joinable_prefix) + entries, err := os.ReadDir(base_dir) + if err != nil { + return err + } + for _, entry := range entries { + q := joinable_prefix + entry.Name() + if !strings.HasPrefix(q, prefix) { + continue + } + abspath := filepath.Join(base_dir, entry.Name()) + dir_to_check := "" + data := FileEntry{ + name: entry.Name(), abspath: abspath, mode: entry.Type(), is_dir: entry.IsDir(), + is_symlink: entry.Type()&os.ModeSymlink == os.ModeSymlink, completion_candidate: q} + if data.is_symlink { + target, err := filepath.EvalSymlinks(abspath) + if err == nil && target != base_dir { + td, err := os.Stat(target) + if err == nil && td.IsDir() { + dir_to_check = target + data.is_dir = true + } + } + } + if dir_to_check != "" { + subentries, err := os.ReadDir(dir_to_check) + data.is_empty_dir = err != nil || len(subentries) == 0 + } + if data.is_dir { + data.completion_candidate += utils.Sep + } + callback(&data) + } return nil } @@ -81,15 +123,18 @@ func complete_executables_in_path(prefix string, paths ...string) []string { func complete_by_fnmatch(prefix string, patterns []string) []string { ans := make([]string, 0, 1024) - complete_files(prefix, func(completion_candidate, abspath string, d fs.DirEntry) error { - q := strings.ToLower(filepath.Base(abspath)) + complete_files(prefix, func(entry *FileEntry) { + if entry.is_dir && !entry.is_empty_dir { + ans = append(ans, entry.completion_candidate) + return + } + q := strings.ToLower(entry.name) for _, pat := range patterns { matched, err := filepath.Match(pat, q) if err == nil && matched { - ans = append(ans, completion_candidate) + ans = append(ans, entry.completion_candidate) } } - return nil }) return ans } diff --git a/tools/completion/files_test.go b/tools/completion/files_test.go index f8c9f7357..958c624e2 100644 --- a/tools/completion/files_test.go +++ b/tools/completion/files_test.go @@ -4,7 +4,7 @@ package completion import ( "fmt" - "io/fs" + "kitty/tools/utils" "os" "path/filepath" "reflect" @@ -13,6 +13,8 @@ import ( "testing" ) +var _ = fmt.Print + func TestCompleteFiles(t *testing.T) { tdir := t.TempDir() cwd, _ := os.Getwd() @@ -37,13 +39,11 @@ func TestCompleteFiles(t *testing.T) { } sort.Strings(expected) actual := make([]string, 0, len(expected)) - complete_files(prefix, func(completion_candidate string, abspath string, d fs.DirEntry) error { - actual = append(actual, completion_candidate) - if _, err := os.Stat(abspath); err != nil { - t.Fatalf("Abspath does not exist: %#v", abspath) - return fmt.Errorf("abspath does not exist") + complete_files(prefix, func(entry *FileEntry) { + actual = append(actual, entry.completion_candidate) + if _, err := os.Stat(entry.abspath); err != nil { + t.Fatalf("Abspath does not exist: %#v", entry.abspath) } - return nil }) sort.Strings(actual) if !reflect.DeepEqual(expected, actual) { @@ -58,6 +58,9 @@ func TestCompleteFiles(t *testing.T) { e[i] = x } else { e[i] = filepath.Join(tdir, x) + if strings.HasSuffix(x, utils.Sep) { + e[i] += utils.Sep + } } } test_candidates(prefix, e...) @@ -71,17 +74,17 @@ func TestCompleteFiles(t *testing.T) { test_candidates("./"+prefix, e...) } - test_cwd_prefix("", "one.txt", "two.txt", "odir", "odir/three.txt", "odir/four.txt") + test_cwd_prefix("", "one.txt", "two.txt", "odir/") test_cwd_prefix("t", "two.txt") test_cwd_prefix("x") - test_abs_candidates(tdir, tdir, "one.txt", "two.txt", "odir", "odir/three.txt", "odir/four.txt") - test_abs_candidates(filepath.Join(tdir, "o"), "one.txt", "odir", "odir/three.txt", "odir/four.txt") + test_abs_candidates(tdir+utils.Sep, "one.txt", "two.txt", "odir/") + test_abs_candidates(filepath.Join(tdir, "o"), "one.txt", "odir/") - test_candidates("", "one.txt", "two.txt", "odir", "odir/three.txt", "odir/four.txt") + test_candidates("", "one.txt", "two.txt", "odir/") test_candidates("t", "two.txt") - test_candidates("o", "one.txt", "odir", "odir/three.txt", "odir/four.txt") - test_candidates("odir", "odir", "odir/three.txt", "odir/four.txt") + test_candidates("o", "one.txt", "odir/") + test_candidates("odir", "odir/") test_candidates("odir/", "odir/three.txt", "odir/four.txt") test_candidates("odir/f", "odir/four.txt") test_candidates("x") diff --git a/tools/completion/kitty.go b/tools/completion/kitty.go index cba7559dc..3e1c61aa6 100644 --- a/tools/completion/kitty.go +++ b/tools/completion/kitty.go @@ -4,7 +4,6 @@ package completion import ( "fmt" - "io/fs" "os" "path/filepath" "strings" @@ -27,21 +26,21 @@ func complete_kitty(completions *Completions, word string, arg_num int) { mg := completions.add_match_group("Executables") mg.IsFiles = true - complete_files(word, func(q, abspath string, d fs.DirEntry) error { - if d.IsDir() { + complete_files(word, func(entry *FileEntry) { + if entry.is_dir && !entry.is_empty_dir { // only allow directories that have sub-dirs or executable files in them - entries, err := os.ReadDir(abspath) + entries, err := os.ReadDir(entry.abspath) if err == nil { for _, x := range entries { - if x.IsDir() || unix.Access(filepath.Join(abspath, x.Name()), unix.X_OK) == nil { - mg.add_match(q) + if x.IsDir() || unix.Access(filepath.Join(entry.abspath, x.Name()), unix.X_OK) == nil { + mg.add_match(entry.completion_candidate) + break } } } - } else if unix.Access(abspath, unix.X_OK) == nil { - mg.add_match(q) + } else if unix.Access(entry.abspath, unix.X_OK) == nil { + mg.add_match(entry.completion_candidate) } - return nil }) } } diff --git a/tools/completion/types.go b/tools/completion/types.go index f83254d92..c1601906f 100644 --- a/tools/completion/types.go +++ b/tools/completion/types.go @@ -34,6 +34,11 @@ type Completions struct { } func (self *Completions) add_match_group(title string) *MatchGroup { + for _, q := range self.Groups { + if q.Title == title { + return q + } + } ans := MatchGroup{Title: title, Matches: make([]*Match, 0, 8)} self.Groups = append(self.Groups, &ans) return &ans diff --git a/tools/utils/paths.go b/tools/utils/paths.go index a6c10559d..9c55b4590 100644 --- a/tools/utils/paths.go +++ b/tools/utils/paths.go @@ -11,6 +11,8 @@ import ( "strings" ) +var Sep = string(os.PathSeparator) + func Expanduser(path string) string { if !strings.HasPrefix(path, "~") { return path @@ -28,7 +30,7 @@ func Expanduser(path string) string { if path == "~" { return home } - path = strings.ReplaceAll(path, string(os.PathSeparator), "/") + path = strings.ReplaceAll(path, Sep, "/") parts := strings.Split(path, "/") if parts[0] == "~" { parts[0] = home @@ -41,7 +43,7 @@ func Expanduser(path string) string { } } } - return strings.Join(parts, string(os.PathSeparator)) + return strings.Join(parts, Sep) } func Abspath(path string) string { @@ -114,8 +116,6 @@ type transformed_walker struct { needs_recurse_func func(string, fs.DirEntry) bool } -var sep = string(os.PathSeparator) - func (self *transformed_walker) walk(dirpath string) error { resolved_path := self.transform_func(dirpath) if self.seen[resolved_path] { @@ -134,8 +134,8 @@ func (self *transformed_walker) walk(dirpath string) error { } // we cant use filepath.Join here as it calls Clean() which can alter dirpath if it contains .. or . etc. path_based_on_original_dir := dirpath - if !strings.HasSuffix(dirpath, sep) && dirpath != "" { - path_based_on_original_dir += sep + if !strings.HasSuffix(dirpath, Sep) && dirpath != "" { + path_based_on_original_dir += Sep } path_based_on_original_dir += rpath if self.needs_recurse_func(path, d) {