diff --git a/0.21.0-release-notes.md b/0.21.0-release-notes.md
index 0dc8a55a8..95de20b19 100644
--- a/0.21.0-release-notes.md
+++ b/0.21.0-release-notes.md
@@ -14,3 +14,24 @@
- The commands `!=`, `!=s` and `not-eq` now only accepts two arguments
([#1767](https://b.elv.sh/1767)).
+
+- The commands `edit:kill-left-alnum-word` and `edit:kill-right-alnum-word`
+ have been renamed to `edit:kill-alnum-word-left` and
+ `edit:kill-alnum-word-right`, to be consistent with the documentation and
+ the names of other similar commands.
+
+ If you need to write code that supports both names, use `has-key` to detect
+ which name is available:
+
+ ```elvish
+ fn kill-alnum-word-left {
+ if (has-key edit: kill-alnum-word-left~) {
+ edit:kill-alnum-word-left
+ } else {
+ edit:kill-left-alnum-word
+ }
+ }
+ ```
+
+- The implicit cd feature has been removed and replaced by the ability to type
+ a literal path in location mode ([#1099](https://b.elv.sh/1099)).
diff --git a/go.sum b/go.sum
index cb466e6ec..6d860a729 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,7 @@
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
@@ -8,9 +9,11 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U=
github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
@@ -19,5 +22,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
pkg.nimblebun.works/go-lsp v1.1.0 h1:TH5ro4p2vlDtELK4LoVeKs4TsKm6aW1f5WP8jHm/9m4=
pkg.nimblebun.works/go-lsp v1.1.0/go.mod h1:Suh759Ki+DjU0zwf0xkl1H6Ln1C6/+GtYyNofbtfcug=
diff --git a/pkg/cli/modes/location.go b/pkg/cli/modes/location.go
index be1b9b52b..69dce33d5 100644
--- a/pkg/cli/modes/location.go
+++ b/pkg/cli/modes/location.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math"
+ "os"
"path/filepath"
"regexp"
"strings"
@@ -111,7 +112,7 @@ func NewLocation(app cli.App, cfg LocationSpec) (Location, error) {
},
},
OnFilter: func(w tk.ComboBox, p string) {
- w.ListBox().Reset(l.filter(cfg.Filter.makePredicate(p)), 0)
+ w.ListBox().Reset(l.filter(p, cfg.Filter.makePredicate(p)), 0)
},
})
return w, nil
@@ -154,8 +155,16 @@ type locationList struct {
dirs []storedefs.Dir
}
-func (l locationList) filter(p func(string) bool) locationList {
+func (l locationList) filter(path string, p func(string) bool) locationList {
var filteredDirs []storedefs.Dir
+ // The `path` argument is the raw filter string. If it is a valid directory
+ // it is pinned to the top of the resulting filtered location history with a
+ // score that marks it as a special-case. This allows a user to quickly
+ // select a literal directory such as /tmp using location mode rather than
+ // the `cd` command.
+ if fi, err := os.Stat(path); err == nil && fi.Mode().IsDir() {
+ filteredDirs = append(filteredDirs, storedefs.Dir{Path: path, Score: -1})
+ }
for _, dir := range l.dirs {
if p(fsutil.TildeAbbr(dir.Path)) {
filteredDirs = append(filteredDirs, dir)
diff --git a/pkg/cli/modes/location_test.go b/pkg/cli/modes/location_test.go
index 0e68643cd..a4a3041dc 100644
--- a/pkg/cli/modes/location_test.go
+++ b/pkg/cli/modes/location_test.go
@@ -120,6 +120,31 @@ func TestLocation_FullWorkflow(t *testing.T) {
}
}
+// Test filtering that includes a literal path to a directory that exists.
+func TestLocation_LiteralDir(t *testing.T) {
+ home := testutil.InTempHome(t)
+ f := Setup()
+ defer f.Stop()
+
+ dirs := []storedefs.Dir{
+ {Path: home, Score: 100},
+ {Path: fixPath("/tmp/foo/bar/lorem/ipsum"), Score: 50},
+ }
+ startLocation(f.App, LocationSpec{Store: locationStore{
+ storedDirs: dirs,
+ chdir: func(dir string) error { return nil },
+ }})
+
+ wantBuf := locationBuf(
+ fixPath("/"),
+ " -1 "+fixPath("/"),
+ " 50 "+fixPath("/tmp/foo/bar/lorem/ipsum"))
+ for _, c := range fixPath("/") {
+ f.TTY.Inject(term.K(c))
+ }
+ f.TTY.TestBuffer(t, wantBuf)
+}
+
func TestLocation_Hidden(t *testing.T) {
f := Setup()
defer f.Stop()
diff --git a/pkg/edit/buffer_builtins.go b/pkg/edit/buffer_builtins.go
index 875f10027..ea738a1c3 100644
--- a/pkg/edit/buffer_builtins.go
+++ b/pkg/edit/buffer_builtins.go
@@ -51,8 +51,8 @@ var bufferBuiltinsData = map[string]func(*tk.CodeBuffer){
"kill-word-right": makeKill(moveDotRightWord),
"kill-small-word-left": makeKill(moveDotLeftSmallWord),
"kill-small-word-right": makeKill(moveDotRightSmallWord),
- "kill-left-alnum-word": makeKill(moveDotLeftAlnumWord),
- "kill-right-alnum-word": makeKill(moveDotRightAlnumWord),
+ "kill-alnum-word-left": makeKill(moveDotLeftAlnumWord),
+ "kill-alnum-word-right": makeKill(moveDotRightAlnumWord),
"kill-line-left": makeKill(moveDotSOL),
"kill-line-right": makeKill(moveDotEOL),
diff --git a/pkg/eval/builtin_fn_fs.d.elv b/pkg/eval/builtin_fn_fs.d.elv
index 8549fa34c..74c040702 100644
--- a/pkg/eval/builtin_fn_fs.d.elv
+++ b/pkg/eval/builtin_fn_fs.d.elv
@@ -1,12 +1,15 @@
# Changes directory.
#
# This affects the entire process, including parallel tasks that are started
-# implicitly (such as prompt functions) or explicitly (such as one started by
+# implicitly (such as prompt functions) or explicitly (such as started by
# [`peach`]()).
#
-# Note that Elvish's `cd` does not support `cd -`.
+# Note that Elvish's `cd` does not support `cd -`. You can also change to a
+# specific directory by starting [Location
+# Mode](../learn/tour.html#location-mode) and typing the desired path as a
+# filter.
#
-# See also [`$pwd`]().
+# See also [`$pwd`]() and [Location Mode](../learn/tour.html#location-mode).
fn cd {|dirname| }
# If `$path` represents a path under the home directory, replace the home
diff --git a/pkg/eval/builtin_fn_num.d.elv b/pkg/eval/builtin_fn_num.d.elv
index 6a9f0e7eb..a408a0f18 100644
--- a/pkg/eval/builtin_fn_num.d.elv
+++ b/pkg/eval/builtin_fn_num.d.elv
@@ -300,10 +300,6 @@ fn * {|@num| }
# ~> / 2 0.0
# ▶ (num +Inf)
# ```
-#
-# When given no argument, this command is equivalent to `cd /`, due to the
-# implicit cd feature. (The implicit cd feature will probably change to avoid
-# this oddity).
#doc:id div
fn / {|x-num @y-num| }
diff --git a/pkg/eval/builtin_fn_num.go b/pkg/eval/builtin_fn_num.go
index de831badd..036dca766 100644
--- a/pkg/eval/builtin_fn_num.go
+++ b/pkg/eval/builtin_fn_num.go
@@ -258,12 +258,8 @@ func mul(rawNums ...vals.Num) vals.Num {
}
}
+// Implement numerical division.
func slash(fm *Frame, args ...vals.Num) error {
- if len(args) == 0 {
- // cd /
- return fm.Evaler.Chdir("/")
- }
- // Division
result, err := div(args...)
if err != nil {
return err
diff --git a/pkg/eval/external_cmd.go b/pkg/eval/external_cmd.go
index de3f2e753..5c32fc640 100644
--- a/pkg/eval/external_cmd.go
+++ b/pkg/eval/external_cmd.go
@@ -12,7 +12,6 @@ import (
"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/eval/vals"
- "src.elv.sh/pkg/fsutil"
"src.elv.sh/pkg/parse"
"src.elv.sh/pkg/persistent/hash"
)
@@ -23,8 +22,6 @@ var (
//
// TODO: Catch this kind of errors at compilation time.
ErrExternalCmdOpts = errors.New("external commands don't accept elvish options")
- // ErrImplicitCdNoArg is thrown when an implicit cd form is passed arguments.
- ErrImplicitCdNoArg = errors.New("implicit cd accepts no arguments")
)
// externalCmd is an external command.
@@ -61,16 +58,6 @@ func (e externalCmd) Call(fm *Frame, argVals []any, opts map[string]any) error {
if len(opts) > 0 {
return ErrExternalCmdOpts
}
- if fsutil.DontSearch(e.Name) {
- stat, err := os.Stat(e.Name)
- if err == nil && stat.IsDir() {
- // implicit cd
- if len(argVals) > 0 {
- return ErrImplicitCdNoArg
- }
- return fm.Evaler.Chdir(e.Name)
- }
- }
files := make([]*os.File, len(fm.ports))
for i, port := range fm.ports {
diff --git a/pkg/fsutil/search.go b/pkg/fsutil/search.go
index 8efd47715..da9a458d0 100644
--- a/pkg/fsutil/search.go
+++ b/pkg/fsutil/search.go
@@ -11,7 +11,9 @@ import (
// DontSearch determines whether the path to an external command should be
// taken literally and not searched.
func DontSearch(exe string) bool {
- return exe == ".." || strings.ContainsRune(exe, filepath.Separator) ||
+ return exe == "." ||
+ exe == ".." ||
+ strings.ContainsRune(exe, filepath.Separator) ||
strings.ContainsRune(exe, '/')
}
diff --git a/website/learn/tour.md b/website/learn/tour.md
index 0c9effa84..b5dfce6d2 100644
--- a/website/learn/tour.md
+++ b/website/learn/tour.md
@@ -943,7 +943,7 @@ pressing ▲. For example, to walk through commands starting with
@ttyshot learn/tour/history-walk-prefix
-### History listing
+### History mode
Press Ctrl-R to list the full command history:
@@ -960,12 +960,14 @@ last command. Press Alt-, to trigger it:
@ttyshot learn/tour/lastcmd
-## Directory history
+## Location mode
Elvish remembers which directories you have visited. Press Ctrl-L to
list visited directories. Use ▲ and ▼ to navigate the
list, Enter to change to that directory, or Escape to
-cancel.
+cancel. If you filter the results using a path that matches an existing
+directory it will appear at the top of the directories in your history with a
+special score of -1 even if that directory is not in your visited history.
@ttyshot learn/tour/location