From 10cf7f06c68db5ce89bb73f9222e6fdb8fb513b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 9 Sep 2022 19:47:30 +0530 Subject: [PATCH] Code to get file/dir completion candidates for a prefix --- tools/completion/files.go | 56 ++++++++++++++++++++++++ tools/completion/files_test.go | 79 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 tools/completion/files.go create mode 100644 tools/completion/files_test.go diff --git a/tools/completion/files.go b/tools/completion/files.go new file mode 100644 index 000000000..1e20f478b --- /dev/null +++ b/tools/completion/files.go @@ -0,0 +1,56 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package completion + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +var _ = fmt.Print + +type CompleteFilesCallback func(completion_candidate string, abspath string, d fs.DirEntry) error + +func complete_files(prefix string, callback CompleteFilesCallback) error { + base := "." + base_len := len(base) + 1 + has_cwd_prefix := strings.HasPrefix(prefix, "./") + is_abs_path := filepath.IsAbs(prefix) + wd := "" + if is_abs_path { + base = prefix + base_len = 0 + if s, err := os.Stat(prefix); err != nil || !s.IsDir() { + base = filepath.Dir(prefix) + } + } else { + wd, _ = os.Getwd() + } + filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + if path == base { + return nil + } + completion_candidate := path + abspath := path + if is_abs_path { + completion_candidate = path[base_len:] + } else { + abspath = filepath.Join(wd, path) + if has_cwd_prefix { + completion_candidate = "./" + completion_candidate + } + } + if strings.HasPrefix(completion_candidate, prefix) { + return callback(completion_candidate, abspath, d) + } + return nil + }) + + return nil +} diff --git a/tools/completion/files_test.go b/tools/completion/files_test.go new file mode 100644 index 000000000..3cdcff754 --- /dev/null +++ b/tools/completion/files_test.go @@ -0,0 +1,79 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package completion + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "reflect" + "sort" + "testing" +) + +func TestCompleteFiles(t *testing.T) { + tdir := t.TempDir() + cwd, _ := os.Getwd() + if cwd != "" { + defer os.Chdir(cwd) + } + os.Chdir(tdir) + os.Create(filepath.Join(tdir, "one.txt")) + os.Create(filepath.Join(tdir, "two.txt")) + os.Mkdir(filepath.Join(tdir, "odir"), 0700) + os.Create(filepath.Join(tdir, "odir", "three.txt")) + os.Create(filepath.Join(tdir, "odir", "four.txt")) + + test_candidates := func(prefix string, expected ...string) { + if expected == nil { + expected = make([]string, 0) + } + 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") + } + return nil + }) + sort.Strings(actual) + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("Did not get expected completion candidates for prefix: %#v\nExpected: %#v\nActual: %#v", prefix, expected, actual) + } + } + + test_abs_candidates := func(prefix string, expected ...string) { + e := make([]string, len(expected)) + for i, x := range expected { + e[i] = filepath.Join(tdir, x) + } + test_candidates(prefix, e...) + } + + test_cwd_prefix := func(prefix string, expected ...string) { + e := make([]string, len(expected)) + for i, x := range expected { + e[i] = "./" + x + } + test_candidates("./"+prefix, e...) + } + + test_cwd_prefix("", "one.txt", "two.txt", "odir", "odir/three.txt", "odir/four.txt") + test_cwd_prefix("t", "two.txt") + test_cwd_prefix("x") + + test_abs_candidates(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_candidates("", "one.txt", "two.txt", "odir", "odir/three.txt", "odir/four.txt") + 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("odir/", "odir/three.txt", "odir/four.txt") + test_candidates("odir/f", "odir/four.txt") + test_candidates("x") + +}