diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84a4bab91..d542ff657 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- `string unescape` has been implemented to reverse the effects of `string escape` (#3543).
- The history file can now be specified by setting the `FISH_HISTORY` variable (#102).
- Read history is now controlled by the `FISH_HISTORY` variable rather than the `--mode-name` flag (#1504).
+- Implement a `cdh` (change directory using recent history) command to provide a more friendly alternative to prevd/nextd and pushd/popd (#2847).
## Other significant changes
diff --git a/doc_src/cd.txt b/doc_src/cd.txt
index a2cb95885..caffa842c 100644
--- a/doc_src/cd.txt
+++ b/doc_src/cd.txt
@@ -14,7 +14,7 @@ If `DIRECTORY` is a relative path, the paths found in the `CDPATH` environment v
Note that the shell will attempt to change directory without requiring `cd` if the name of a directory is provided (starting with `.`, `/` or `~`, or ending with `/`).
-Fish also ships a wrapper function around the builtin `cd` that understands `cd -` as changing to the previous directory. See also `prevd`. This wrapper function maintains a history of the 25 most recently visited directories in the `$dirprev` and `$dirnext` global variables.
+Fish also ships a wrapper function around the builtin `cd` that understands `cd -` as changing to the previous directory. See also `prevd`. This wrapper function maintains a history of the 25 most recently visited directories in the `$dirprev` and `$dirnext` global variables. If you make those universal variables your `cd` history is shared among all fish instances.
\subsection cd-example Examples
@@ -25,3 +25,7 @@ cd
cd /usr/src/fish-shell
# changes the working directory to /usr/src/fish-shell
\endfish
+
+\subsection cd-see-also See Also
+
+See also the `cdh` command for changing to a recently visited directory.
diff --git a/doc_src/cdh.txt b/doc_src/cdh.txt
new file mode 100644
index 000000000..7d18acd86
--- /dev/null
+++ b/doc_src/cdh.txt
@@ -0,0 +1,16 @@
+\section cdh cdh - change to a recently visited directory
+
+\subsection cdh-synopsis Synopsis
+\fish{synopsis}
+cdh [ directory ]
+\endfish
+
+\subsection cdh-description Description
+
+`cdh` with no arguments presents a list of recently visited directories. You can then select one of the entries by letter or number. You can also press @key{tab} to use the completion pager to select an item from the list. If you give it a single argument it is equivalent to `cd directory`.
+
+Note that the `cd` command limits directory history to the 25 most recently visited directories. The history is stored in the `$dirprev` and `$dirnext` variables which this command manipulates. If you make those universal variables your `cd` history is shared among all fish instances.
+
+\subsection cdh-see-also See Also
+
+See also the `prevd` and `pushd` commands which also work with the recent `cd` history and are provided for compatibility with other shells.
diff --git a/doc_src/nextd.txt b/doc_src/nextd.txt
index e02598a9b..61e01f1ef 100644
--- a/doc_src/nextd.txt
+++ b/doc_src/nextd.txt
@@ -13,6 +13,8 @@ If the `-l` or `--list` flag is specified, the current directory history is also
Note that the `cd` command limits directory history to the 25 most recently visited directories. The history is stored in the `$dirprev` and `$dirnext` variables which this command manipulates.
+You may be interested in the `cdh` command which provides a more intuitive way to navigate to recently visited directories.
+
\subsection nextd-example Example
\fish
diff --git a/doc_src/popd.txt b/doc_src/popd.txt
index 928168352..93dd4da37 100644
--- a/doc_src/popd.txt
+++ b/doc_src/popd.txt
@@ -9,6 +9,7 @@ popd
`popd` removes the top directory from the directory stack and changes the working directory to the new top directory. Use `pushd` to add directories to the stack.
+You may be interested in the `cdh` command which provides a more intuitive way to navigate to recently visited directories.
\subsection popd-example Example
diff --git a/doc_src/prevd.txt b/doc_src/prevd.txt
index f1e9e6cb3..191d95890 100644
--- a/doc_src/prevd.txt
+++ b/doc_src/prevd.txt
@@ -13,6 +13,8 @@ If the `-l` or `--list` flag is specified, the current history is also displayed
Note that the `cd` command limits directory history to the 25 most recently visited directories. The history is stored in the `$dirprev` and `$dirnext` variables which this command manipulates.
+You may be interested in the `cdh` command which provides a more intuitive way to navigate to recently visited directories.
+
\subsection prevd-example Example
\fish
diff --git a/doc_src/pushd.txt b/doc_src/pushd.txt
index 8fad3bd71..43d62c6dd 100644
--- a/doc_src/pushd.txt
+++ b/doc_src/pushd.txt
@@ -17,6 +17,8 @@ Without arguments, it exchanges the top two directories in the stack.
See also `dirs` and `dirs -c`.
+You may be interested in the `cdh` command which provides a more intuitive way to navigate to recently visited directories.
+
\subsection pushd-example Example
\fish
diff --git a/share/completions/cdh.fish b/share/completions/cdh.fish
new file mode 100644
index 000000000..f54e2a2d1
--- /dev/null
+++ b/share/completions/cdh.fish
@@ -0,0 +1,23 @@
+function __fish_cdh_args
+ set -l all_dirs $dirprev $dirnext
+ set -l uniq_dirs
+
+ # This next bit of code doesn't do anything useful at the moment since the fish pager always
+ # sorts, and eliminates duplicate, entries. But we do this to mimic the modal behavor of `cdh`
+ # and in hope that the fish pager behavior will be changed to preserve the order of entries.
+ for dir in $all_dirs[-1..1]
+ if not contains $dir $uniq_dirs
+ set uniq_dirs $uniq_dirs $dir
+ end
+ end
+
+ for dir in $uniq_dirs
+ set -l home_dir (string match -r "$HOME(/.*|\$)" "$dir")
+ if set -q home_dir[2]
+ set dir "~$home_dir[2]"
+ end
+ echo $dir
+ end
+end
+
+complete -c cdh -xa '(__fish_cdh_args)'
diff --git a/share/functions/cdh.fish b/share/functions/cdh.fish
new file mode 100644
index 000000000..eb6df8e29
--- /dev/null
+++ b/share/functions/cdh.fish
@@ -0,0 +1,87 @@
+# Provide a menu of the directories recently navigated to and ask the user to
+# choose one to make the new current working directory (cwd).
+
+function cdh --description "Menu based cd command"
+ # See if we've been invoked with an argument. Presumably from the `cdh` completion script.
+ # If we have just treat it as `cd` to the specified directory.
+ if set -q argv[1]
+ cd $argv
+ return
+ end
+
+ if set -q argv[2]
+ echo (_ "cdh: Expected zero or one arguments") >&2
+ return 1
+ end
+
+ set -l all_dirs $dirprev $dirnext
+ if not set -q all_dirs[1]
+ echo (_ 'No previous directories to select. You have to cd at least once.') >&2
+ return 0
+ end
+
+ # Reverse the directories so the most recently visited is first in the list.
+ # Also, eliminate duplicates; i.e., we only want the most recent visit to a
+ # given directory in the selection list.
+ set -l uniq_dirs
+ for dir in $all_dirs[-1..1]
+ if not contains $dir $uniq_dirs
+ set uniq_dirs $uniq_dirs $dir
+ end
+ end
+
+ set -l letters a b c d e f g h i j k l m n o p q r s t u v w x y z
+ set -l dirc (count $uniq_dirs)
+ if test $dirc -gt (count $letters)
+ set -l msg (_ 'This should not happen. Have you changed the cd function?')
+ printf "$msg\n"
+ set -l msg (_ 'There are %s unique dirs in your history but I can only handle %s')
+ printf "$msg\n" $dirc (count $letters)
+ return 1
+ end
+
+ # Print the recent directories, oldest to newest. Since we previously
+ # reversed the list, making the newest entry the first item in the array,
+ # we count down rather than up.
+ for i in (seq $dirc -1 1)
+ set -l dir $uniq_dirs[$i]
+ set -l label_color normal
+ set -q fish_color_cwd; and set label_color $fish_color_cwd
+ set -l dir_color_reset (set_color normal)
+ set -l dir_color
+ if test "$dir" = "$PWD"
+ set dir_color (set_color $fish_color_history_current)
+ end
+
+ set -l home_dir (string match -r "$HOME(/.*|\$)" "$dir")
+ if set -q home_dir[2]
+ set dir "~$home_dir[2]"
+ end
+ printf '%s %s %2d) %s %s%s%s\n' (set_color $label_color) $letters[$i] \
+ $i (set_color normal) $dir_color $dir $dir_color_reset
+ end
+
+ # Ask the user which directory from their history they want to cd to.
+ set -l msg (_ 'Select directory by letter or number: ')
+ read -l -p "echo '$msg'" choice
+ if test "$choice" = ""
+ return 0
+ else if string match -q -r '^[a-z]$' $choice
+ # Convert the letter to an index number.
+ set choice (contains -i $choice $letters)
+ end
+
+ set -l msg (_ 'Error: expected a number between 1 and %d or letter in that range, got "%s"')
+ if string match -q -r '^\d+$' $choice
+ if test $choice -ge 1 -a $choice -le $dirc
+ cd $uniq_dirs[$choice]
+ return
+ else
+ printf "$msg\n" $dirc $choice
+ return 1
+ end
+ else
+ printf "$msg\n" $dirc $choice
+ return 1
+ end
+end