Dont recurse for file completion
We could potentially end up recursing over the entire file system. And for completion we only present the candidates in the immediate directory anyway.
This commit is contained in:
parent
c9d986f9a8
commit
3c29ce936b
@ -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())
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user