From 0f9fffa0aa8a7288b4131d1fc7c28184a1d9090d Mon Sep 17 00:00:00 2001 From: itchyny Date: Sat, 12 Oct 2024 20:33:43 +0900 Subject: [PATCH] implement :cd, :chdir, :pwd commands to change the working directory --- README.md | 2 ++ cmdline/command.go | 3 +++ cmdline/completor.go | 14 +++++++--- cmdline/completor_test.go | 37 +++++++++++++++++++++++++ editor/editor.go | 57 +++++++++++++++++++++++++++++++++++++-- editor/editor_test.go | 30 +++++++++++++++++++++ event/event.go | 3 +++ 7 files changed, 140 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 14f3e9e..c295819 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ So if you have experience with Vim, you will notice most of basic operations of - File operations - `:edit`, `:enew`, `:new`, `:vnew`, `:only` +- Current working directory + - `:cd`, `:chdir`, `:pwd` - Quit and save - `:quit`, `:qall`, `:write`, `:wq`, `:xit`, `:xall`, `:cquit` - Window operations diff --git a/cmdline/command.go b/cmdline/command.go index 67ccd8e..aa4dc4c 100644 --- a/cmdline/command.go +++ b/cmdline/command.go @@ -21,6 +21,9 @@ var commands = []command{ {"u[ndo]", event.Undo}, {"red[o]", event.Redo}, + {"pw[d]", event.PrintDirectory}, + {"cd", event.ChangeDirectory}, + {"chd[ir]", event.ChangeDirectory}, {"exi[t]", event.Quit}, {"q[uit]", event.Quit}, {"qa[ll]", event.QuitAll}, diff --git a/cmdline/completor.go b/cmdline/completor.go index 0c6882a..a42ddc0 100644 --- a/cmdline/completor.go +++ b/cmdline/completor.go @@ -31,7 +31,9 @@ func (c *completor) clear() { func (c *completor) complete(cmdline string, cmd command, prefix string, arg string, forward bool) string { switch cmd.eventType { case event.Edit, event.New, event.Vnew, event.Write: - return c.completeFilepaths(cmdline, prefix, arg, forward) + return c.completeFilepaths(cmdline, prefix, arg, forward, false) + case event.ChangeDirectory: + return c.completeFilepaths(cmdline, prefix, arg, forward, true) case event.Wincmd: return c.completeWincmd(cmdline, prefix, arg, forward) default: @@ -53,7 +55,9 @@ func (c *completor) completeNext(prefix string, forward bool) string { return prefix + c.arg + c.results[c.index] } -func (c *completor) completeFilepaths(cmdline string, prefix string, arg string, forward bool) string { +func (c *completor) completeFilepaths( + cmdline string, prefix string, arg string, forward bool, dirOnly bool, +) string { if !strings.HasSuffix(prefix, " ") { prefix += " " } @@ -62,7 +66,7 @@ func (c *completor) completeFilepaths(cmdline string, prefix string, arg string, } c.target = cmdline c.index = 0 - c.arg, c.results = c.listFileNames(arg) + c.arg, c.results = c.listFileNames(arg, dirOnly) if len(c.results) == 1 { cmdline := prefix + c.arg + c.results[0] c.results = nil @@ -81,7 +85,7 @@ func (c *completor) completeFilepaths(cmdline string, prefix string, arg string, const separator = string(filepath.Separator) -func (c *completor) listFileNames(arg string) (string, []string) { +func (c *completor) listFileNames(arg string, dirOnly bool) (string, []string) { var targets []string path, homedir, hasHomedirPrefix, err := expandHomedir(arg) if err != nil { @@ -132,6 +136,8 @@ func (c *completor) listFileNames(arg string) (string, []string) { } if isDir { name += separator + } else if dirOnly { + continue } targets = append(targets, name) } diff --git a/cmdline/completor_test.go b/cmdline/completor_test.go index 043e67b..f795712 100644 --- a/cmdline/completor_test.go +++ b/cmdline/completor_test.go @@ -255,6 +255,43 @@ func TestCompletorCompleteFilepathRoot(t *testing.T) { } } +func TestCompletorCompleteFilepathChangeDirectory(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skip on Windows") + } + c := newCompletor(&mockFilesystem{}) + cmdline := "cd " + cmd, _, prefix, _, arg, _ := parse([]rune(cmdline)) + cmdline = c.complete(cmdline, cmd, prefix, arg, false) + if expected := "cd editor/"; cmdline != expected { + t.Errorf("cmdline should be %q but got %q", expected, cmdline) + } + if expected := "cd "; c.target != expected { + t.Errorf("completion target should be %q but got %q", expected, c.target) + } + if c.index != 3 { + t.Errorf("completion index should be %d but got %d", 3, c.index) + } + + c.clear() + cmdline = c.complete(cmdline, cmd, prefix, "~/", false) + if expected := "cd ~/Pictures/"; cmdline != expected { + t.Errorf("cmdline should be %q but got %q", expected, cmdline) + } + if c.index != 2 { + t.Errorf("completion index should be %d but got %d", 0, c.index) + } + + c.clear() + cmdline = c.complete(cmdline, cmd, prefix, "/", true) + if expected := "cd /bin/"; cmdline != expected { + t.Errorf("cmdline should be %q but got %q", expected, cmdline) + } + if c.index != 0 { + t.Errorf("completion index should be %d but got %d", 0, c.index) + } +} + func TestCompletorCompleteWincmd(t *testing.T) { c := newCompletor(&mockFilesystem{}) cmdline := "winc" diff --git a/editor/editor.go b/editor/editor.go index d742b6c..79423d6 100644 --- a/editor/editor.go +++ b/editor/editor.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "io" + "os" + "path/filepath" "strconv" "strings" "sync" @@ -24,6 +26,7 @@ type Editor struct { searchTarget string searchMode rune prevEventType event.Type + prevDir string buffer *buffer.Buffer err error errtyp int @@ -147,7 +150,7 @@ func (e *Editor) emit(ev event.Event) (redraw bool, finish bool, err error) { } switch ev.Type { case event.QuitAll: - if len(ev.Arg) > 0 { + if ev.Arg != "" { e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError redraw = true } else { @@ -171,8 +174,35 @@ func (e *Editor) emit(ev event.Event) (redraw bool, finish bool, err error) { err = &quitErr{1} finish = true } + case event.PrintDirectory: + if ev.Arg != "" { + e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError + redraw = true + break + } + fallthrough + case event.ChangeDirectory: + if ev.Arg == "-" && e.prevDir == "" { + e.err, e.errtyp = errors.New("no previous working directory"), state.MessageError + } else if dir, err := os.Getwd(); err != nil { + e.err, e.errtyp = err, state.MessageError + } else if ev.Arg == "" { + e.err, e.errtyp = errors.New(dir), state.MessageInfo + } else { + if ev.Arg != "-" { + dir, e.prevDir = ev.Arg, dir + } else { + dir, e.prevDir = e.prevDir, dir + } + if dir, err = e.chdir(dir); err != nil { + e.err, e.errtyp = err, state.MessageError + } else { + e.err, e.errtyp = errors.New(dir), state.MessageInfo + } + } + redraw = true case event.Suspend: - if len(ev.Arg) > 0 { + if ev.Arg != "" { e.err, e.errtyp = errors.New("too many arguments for "+ev.CmdName), state.MessageError } else { e.mu.Unlock() @@ -340,6 +370,18 @@ func (e *Editor) redraw() (err error) { return e.ui.Redraw(s) } +func (e *Editor) chdir(dir string) (string, error) { + if dir, err := expandHomedir(dir); err != nil { + return "", err + } else if err = os.Chdir(dir); err != nil { + return "", err + } else if dir, err = os.Getwd(); err != nil { + return "", err + } else { + return dir, nil + } +} + func (e *Editor) suspend() error { return suspend(e) } @@ -354,3 +396,14 @@ func (e *Editor) Close() error { e.wm.Close() return e.ui.Close() } + +func expandHomedir(path string) (string, error) { + if !strings.HasPrefix(path, "~") { + return path, nil + } + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(homeDir, path[1:]), nil +} diff --git a/editor/editor_test.go b/editor/editor_test.go index 650287b..77d82ca 100644 --- a/editor/editor_test.go +++ b/editor/editor_test.go @@ -778,3 +778,33 @@ func TestEditorCmdlineQuitErr(t *testing.T) { t.Errorf("err should be nil but got: %v", err) } } + +func TestEditorChangeDirectory(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Errorf("err should be nil but got: %v", err) + } + ui := newTestUI() + editor := NewEditor(ui, window.NewManager(), cmdline.NewCmdline()) + if err := editor.Init(); err != nil { + t.Errorf("err should be nil but got: %v", err) + } + if err := editor.OpenEmpty(); err != nil { + t.Errorf("err should be nil but got: %v", err) + } + go func() { + ui.Emit(event.Event{Type: event.PrintDirectory}) + ui.Emit(event.Event{Type: event.ChangeDirectory, Arg: "../"}) + ui.Emit(event.Event{Type: event.ChangeDirectory, Arg: "-"}) + ui.Emit(event.Event{Type: event.Quit}) + }() + if err := editor.Run(); err != nil { + t.Errorf("err should be nil but got: %v", err) + } + if err := editor.err; err == nil || err.Error() != dir { + t.Errorf("err should end with %q but got: %v", dir, err) + } + if err := editor.Close(); err != nil { + t.Errorf("err should be nil but got: %v", err) + } +} diff --git a/event/event.go b/event/event.go index 967f4df..b7594df 100644 --- a/event/event.go +++ b/event/event.go @@ -127,6 +127,9 @@ const ( MoveWindowBottom MoveWindowLeft MoveWindowRight + + PrintDirectory + ChangeDirectory Suspend Quit QuitAll