diff --git a/plugins/fz/LICENSE b/plugins/fz/LICENSE new file mode 100644 index 00000000..c135c6fc --- /dev/null +++ b/plugins/fz/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Henry Chang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/fz/README-zh.md b/plugins/fz/README-zh.md new file mode 100644 index 00000000..fd9b1fd5 --- /dev/null +++ b/plugins/fz/README-zh.md @@ -0,0 +1,144 @@ +# fz + +完全不須再綁定其他熱鍵,fz 無縫接軌地為 [z](https://github.com/rupa/z) 的 tab 自動完成(自動補全、自動補完、tab completion)加上模糊搜尋功能。使用者可以透過此程式在歷史目錄中自由跳躍。支援 Bash 與 zsh。 + +* [展示](#展示) +* [安裝程序](#安裝程序) + * [macOS](#macos) + * [Bash](#bash) + * [zsh](#zsh) + * [Ubuntu](#ubuntu) + * [Bash](#bash-1) + * [zsh](#zsh-1) +* [使用說明](#使用說明) +* [相關資訊](#相關資訊) +* [授權條款](#授權條款) + +## 展示 + +![示意圖](fz-demo.gif) + +## 安裝程序 + +由 shell 中 source 對應的程式檔案即可使用。但因本程式仰仗 [z](https://github.com/rupa/z) 與 [fzf](https://github.com/junegunn/fzf),故此二者亦須一併安裝至系統中。 + +注意:`fz` 需在 `z` 之後 source。 + +### macOS + +#### Bash + +1. 經由 [Homebrew](https://brew.sh/) 安裝 fzf: + + ```sh + brew install fzf + ``` + +2. 下載 z 與 fz: + + ```sh + mkdir ~/.bash_completion.d + curl "https://raw.githubusercontent.com/rupa/z/master/{z.sh}" \ + -o ~/.bash_completion.d/"#1" + curl "https://raw.githubusercontent.com/changyuheng/fz/master/{fz.sh}" \ + -o ~/.bash_completion.d/z"#1" + ``` + +3. 在 `~/.bashrc` 中加入以下資訊: + + ```sh + if [ -d ~/.bash_completion.d ]; then + for file in ~/.bash_completion.d/*; do + . $file + done + fi + ``` + +#### zsh + +1. 經由 [Homebrew](https://brew.sh/) 安裝 fzf: + + ```sh + brew install fzf + ``` + +2. 透過 [zplug](https://github.com/zplug/zplug) 部署 z 與 fz。將以下資訊加入 `~/.zshrc`: + + ```sh + zplug "changyuheng/fz", defer:1 + zplug "rupa/z", use:z.sh + ``` + +### Ubuntu + +#### Bash + +1. 安裝 fzf: + + ```sh + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf + ~/.fzf/install + ``` + +2. 下載 z 與 fz: + + ```sh + mkdir ~/.bash_completion.d + curl "https://raw.githubusercontent.com/rupa/z/master/{z.sh}" \ + -o ~/.bash_completion.d/"#1" + curl "https://raw.githubusercontent.com/changyuheng/fz/master/{fz.sh}" \ + -o ~/.bash_completion.d/z"#1" + ``` + +3. 在 `~/.bashrc` 中加入以下資訊: + + ```sh + if [ -d ~/.bash_completion.d ]; then + for file in ~/.bash_completion.d/*; do + . $file + done + fi + ``` + +#### zsh + +1. 安裝 fzf: + + ```sh + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf + ~/.fzf/install + ``` + +2. 透過 [zplug](https://github.com/zplug/zplug) 部署 z 與 fz。將以下資訊加入 `~/.zshrc`: + + ```sh + zplug "changyuheng/fz", defer:1 + zplug "rupa/z", use:z.sh + ``` + +## 使用說明 + +``` +z [dir name slug] +zz [dir name slug] +``` + +- 程式的功能與 [z](https://github.com/rupa/z) 雷同。`zz` 指令限制搜尋範圍為當前目錄及其子目錄。 +- `tab`/`shift-tab`、`ctrl-n`/`ctrl-p`、`ctrl-j`/`ctrl-k` 選擇下一個、上一個選項。`Enter` 確定。 +- `FZ_CMD=z` 指定指令名稱。預設為 `z`。 +- `FZ_SUBDIR_CMD=zz` 指定指令名稱。預設為 `zz`。 +- `FZ_SUBDIR_TRAVERSAL=0` 關閉子目錄補完。預設為開啟。 +- `FZ_CASE_INSENSITIVE=0` 關閉子目錄補完不限大小寫。預設為開啟。 +- `FZ_ABBREVIATE_HOME=0` 不展開 `~` 變數。預設為展開。 + +## 相關資訊 + +- [cdr](https://github.com/willghatch/zsh-cdr) + [zaw](https://github.com/zsh-users/zaw) +- fzf 的[自動完成說明](https://github.com/junegunn/fzf#fuzzy-completion-for-bash-and-zsh)及其[維基頁面](https://github.com/junegunn/fzf/wiki) +- [fasd](https://github.com/clvv/fasd) +- [autojump](https://github.com/wting/autojump) +- [命令行上的narrowing(随着输入逐步减少备选项)工具](http://www.cnblogs.com/bamanzi/p/cli-narrowing-tools.html) + +## 授權條款 + +本軟體以 [MIT 授權條款](LICENSE)授權。 diff --git a/plugins/fz/README.md b/plugins/fz/README.md new file mode 100644 index 00000000..65b4623b --- /dev/null +++ b/plugins/fz/README.md @@ -0,0 +1,161 @@ +# fz + +A shell plugin that seamlessly adds fuzzy search to tab completion of +[z](https://github.com/rupa/z), +lets you easy to jump around among your historical directories. +Not any additional key binding is needed. Currently supports Bash and zsh. + +* [Demo](#demo) +* [Installation](#installation) + * [macOS](#macos) + * [Bash](#bash) + * [zsh](#zsh) + * [Ubuntu](#ubuntu) + * [Bash](#bash-1) + * [zsh](#zsh-1) +* [Usage](#usage) +* [See Also](#see-also) +* [License](#license) + +## Demo + +![gif-demo](fz-demo.gif) + +## Installation + +By simply sourcing corresponding script file for your shell, you're all set. +However, this plugin is sitting on top of [z](https://github.com/rupa/z) and +[fzf](https://github.com/junegunn/fzf), so you must have them installed as well. + +N.B. `fz` needs to be sourced after `z`. + +### macOS + +#### Bash + +1. Install fzf via [Homebrew](https://brew.sh/). + + ```sh + brew install fzf + ``` + +2. Download z and fz. + + ```sh + mkdir ~/.bash_completion.d + curl "https://raw.githubusercontent.com/rupa/z/master/{z.sh}" \ + -o ~/.bash_completion.d/"#1" + curl "https://raw.githubusercontent.com/changyuheng/fz/master/{fz.sh}" \ + -o ~/.bash_completion.d/z"#1" + ``` + +3. Add the following content to `~/.bashrc`: + + ```sh + if [ -d ~/.bash_completion.d ]; then + for file in ~/.bash_completion.d/*; do + . $file + done + fi + ``` + +#### zsh + +1. Install fzf via [Homebrew](https://brew.sh/). + + ```sh + brew install fzf + ``` + +2. Install z and fz via [zplug](https://github.com/zplug/zplug). + Add the following content to `~/.zshrc`: + + ```sh + zplug "changyuheng/fz", defer:1 + zplug "rupa/z", use:z.sh + ``` + +### Ubuntu + +#### Bash + +1. Install fzf. + + ```sh + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf + ~/.fzf/install + ``` + +2. Download z and fz. + + ```sh + mkdir ~/.bash_completion.d + curl "https://raw.githubusercontent.com/rupa/z/master/{z.sh}" \ + -o ~/.bash_completion.d/"#1" + curl "https://raw.githubusercontent.com/changyuheng/fz/master/{fz.sh}" \ + -o ~/.bash_completion.d/z"#1" + ``` + +3. Add the following content to `~/.bashrc`: + + ```sh + if [ -d ~/.bash_completion.d ]; then + for file in ~/.bash_completion.d/*; do + . $file + done + fi + ``` + +#### zsh + +1. Install fzf. + + ```sh + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf + ~/.fzf/install + ``` + +2. Install z and fz via [zplug](https://github.com/zplug/zplug). + Add the following content to `~/.zshrc`: + + ```sh + zplug "changyuheng/fz", defer:1 + zplug "rupa/z", use:z.sh + ``` + +## Usage + +``` +z [dir name slug] +zz [dir name slug] +``` + +- The function of fz is pretty much like what it is of + [z](https://github.com/rupa/z). + `zz` limits the search base starting from current working directory. + Check z’s doc for more information. +- `tab`/`shift-tab`, `ctrl-n`/`ctrl-p`, `ctrl-j`/`ctrl-k`, + for next and previous item. `Enter` for selection. + Check fzf’s [doc](https://github.com/junegunn/fzf#search-syntax) + for the search syntaxes. +- `FZ_CMD=z` specifies command name of `fz`. Default is `z`. +- `FZ_SUBDIR_CMD=zz` specifies command name for subdirectory only `z`. + Default is `zz`. +- `FZ_SUBDIR_TRAVERSAL=0` disables subdirectory completion. + Default is enabled. +- `FZ_CASE_INSENSITIVE=0` disables case-insensitive subdirectory completion. + Default is enabled. +- `FZ_ABBREVIATE_HOME=0` disables abbreviating `~`. Default is enabled. + +## See Also + +- [cdr](https://github.com/willghatch/zsh-cdr) + [zaw](https://github.com/zsh-users/zaw) +- fzf’s [readme of completion](https://github.com/junegunn/fzf#fuzzy-completion-for-bash-and-zsh) + and its [wiki](https://github.com/junegunn/fzf/wiki) +- [fasd](https://github.com/clvv/fasd) +- [autojump](https://github.com/wting/autojump) +- [命令行上的narrowing(随着输入逐步减少备选项)工具](http://www.cnblogs.com/bamanzi/p/cli-narrowing-tools.html) + +## License + +This software is licensed under a [MIT License](LICENSE). diff --git a/plugins/fz/fz-demo.gif b/plugins/fz/fz-demo.gif new file mode 100644 index 00000000..d293cc4f Binary files /dev/null and b/plugins/fz/fz-demo.gif differ diff --git a/plugins/fz/fz.bash b/plugins/fz/fz.bash new file mode 120000 index 00000000..cd78f6a1 --- /dev/null +++ b/plugins/fz/fz.bash @@ -0,0 +1 @@ +fz.sh \ No newline at end of file diff --git a/plugins/fz/fz.plugin.zsh b/plugins/fz/fz.plugin.zsh new file mode 120000 index 00000000..cd78f6a1 --- /dev/null +++ b/plugins/fz/fz.plugin.zsh @@ -0,0 +1 @@ +fz.sh \ No newline at end of file diff --git a/plugins/fz/fz.sh b/plugins/fz/fz.sh new file mode 100644 index 00000000..df272970 --- /dev/null +++ b/plugins/fz/fz.sh @@ -0,0 +1,297 @@ +[[ -n "$FZ_CMD" ]] || FZ_CMD=z +[[ -n "$FZ_SUBDIR_CMD" ]] || FZ_SUBDIR_CMD=zz + +[[ -n "$FZ_HISTORY_CD_CMD" ]] || FZ_HISTORY_CD_CMD=_z +[[ -n "$FZ_SUBDIR_HISTORY_CD_CMD" ]] || FZ_SUBDIR_HISTORY_CD_CMD="_z -c" + +[[ -n "$FZ_HISTORY_LIST_GENERATOR" ]] \ + || FZ_HISTORY_LIST_GENERATOR=__fz_generate_matched_history_list +[[ -n "$FZ_SUBDIR_HISTORY_LIST_GENERATOR" ]] \ + || FZ_SUBDIR_HISTORY_LIST_GENERATOR=__fz_generate_matched_subdir_history_list + +[[ -n "$FZ_SUBDIR_TRAVERSAL" ]] || FZ_SUBDIR_TRAVERSAL=1 +[[ -n "$FZ_CASE_INSENSITIVE" ]] || FZ_CASE_INSENSITIVE=1 +[[ -n "$FZ_ABBREVIATE_HOME" ]] || FZ_ABBREVIATE_HOME=1 + +alias ${FZ_CMD}='_fz' +alias ${FZ_SUBDIR_CMD}='_fzz' + +__fz_generate_matched_subdir_list() { + local dir seg starts_with_dir + if [[ "$1" == */ ]]; then + dir="$1" + find -L "$(cd "$dir" 2>/dev/null && pwd)" -mindepth 1 -maxdepth 1 -type d \ + 2>/dev/null | while read -r line; do + base="${line##*/}" + if [[ "$base" == .* ]]; then + continue + fi + echo "$line" + done + else + dir=$(dirname -- "$1") + seg=$(basename -- "$1") + if [[ "$FZ_CASE_INSENSITIVE" == "1" ]]; then + seg=$(echo "$seg" | tr '[:upper:]' '[:lower:]') + fi + starts_with_dir=$( \ + find -L "$(cd "$dir" 2>/dev/null && pwd)" -mindepth 1 -maxdepth 1 \ + -type d 2>/dev/null | while read -r line; do \ + base="${line##*/}" + if [[ "$seg" != .* && "$base" == .* ]]; then + continue + fi + if [[ "$FZ_CASE_INSENSITIVE" != "1" ]]; then + if [[ "$base" == "$seg"* ]]; then + echo "$line" + fi + else + if [[ -n "$BASH_VERSION" ]]; then + if [[ "${base,,}" == "${seg,,}"* ]]; then + echo "$line" + fi + elif [[ -n "$ZSH_VERSION" ]]; then + if [[ "${base:l}" == "${seg:l}"* ]]; then + echo "$line" + fi + fi + fi + done + ) + if [ -n "$starts_with_dir" ]; then + echo "$starts_with_dir" + else + find -L "$(cd "$dir" 2>/dev/null && pwd)" -mindepth 1 -maxdepth 1 \ + -type d 2>/dev/null | while read -r line; do \ + base="${line##*/}" + if [[ "$seg" != .* && "$base" == .* ]]; then + continue + fi + if [[ "$FZ_CASE_INSENSITIVE" != "1" ]]; then + if [[ "$base" == *"$seg"* ]]; then + echo "$line" + fi + else + if [[ -n "$BASH_VERSION" ]]; then + if [[ "${base,,}" == *"${seg,,}"* ]]; then + echo "$line" + fi + elif [[ -n "$ZSH_VERSION" ]]; then + if [[ "${base:l}" == *"${seg:l}"* ]]; then + echo "$line" + fi + fi + fi + done + fi + fi +} + +__fz_generate_matched_history_list() { + _z -l $@ 2>&1 | while read -r line; do + if [[ "$line" == common:* ]]; then continue; fi + # Reverse the order and cut off the scores + echo "$line" + done | sed '1!G;h;$!d' | cut -b 12- +} + +__fz_generate_matched_subdir_history_list() { + __fz_generate_matched_history_list -c "$@" +} + +__fz_generate_matches() { + local cmd histories subdirs + + if [[ -n "$BASH_VERSION" ]]; then + cmd=$([[ "${COMP_WORDS[0]}" =~ [[:space:]]*([^[:space:]]|[^[:space:]].*[^[:space:]])[[:space:]]* ]]; \ + echo -n "${BASH_REMATCH[1]}") + elif [[ -n "$ZSH_VERSION" ]]; then + cmd=(${(z)LBUFFER}) + cmd=${cmd[1]} + fi + + if [[ "$cmd" == "$FZ_CMD" ]]; then + if [[ "$FZ_ABBREVIATE_HOME" == "1" ]]; then + if [ "$FZ_SUBDIR_TRAVERSAL" == "1" ]; then + cat <("$FZ_HISTORY_LIST_GENERATOR" "$@") \ + <(__fz_generate_matched_subdir_list "$@") \ + | sed '/^$/d' | sed -e "s,^$HOME,~," | awk '!seen[$0]++' + else + cat <("$FZ_HISTORY_LIST_GENERATOR" "$@") \ + | sed '/^$/d' | sed -e "s,^$HOME,~," | awk '!seen[$0]++' + fi + else + if [ "$FZ_SUBDIR_TRAVERSAL" == "1" ]; then + cat <("$FZ_HISTORY_LIST_GENERATOR" "$@") \ + <(__fz_generate_matched_subdir_list "$@") \ + | sed '/^$/d' | awk '!seen[$0]++' + else + cat <("$FZ_HISTORY_LIST_GENERATOR" "$@") \ + | sed '/^$/d' | awk '!seen[$0]++' + fi + fi + elif [[ "$cmd" == "$FZ_SUBDIR_CMD" ]]; then + histories=$("$FZ_SUBDIR_HISTORY_LIST_GENERATOR" "$@") + if [[ "$FZ_ABBREVIATE_HOME" == "1" ]]; then + if [ "$FZ_SUBDIR_TRAVERSAL" == "1" ]; then + cat <("$FZ_SUBDIR_HISTORY_LIST_GENERATOR" "$@") \ + <(__fz_generate_matched_subdir_list "$@") \ + | sed '/^$/d' | sed -e "s,^$HOME,~," | awk '!seen[$0]++' + else + cat <("$FZ_SUBDIR_HISTORY_LIST_GENERATOR" "$@") \ + | sed '/^$/d' | sed -e "s,^$HOME,~," | awk '!seen[$0]++' + fi + else + if [ "$FZ_SUBDIR_TRAVERSAL" == "1" ]; then + cat <("$FZ_SUBDIR_HISTORY_LIST_GENERATOR" "$@") \ + <(__fz_generate_matched_subdir_list "$@") \ + | sed '/^$/d' | awk '!seen[$0]++' + else + cat <("$FZ_SUBDIR_HISTORY_LIST_GENERATOR" "$@") \ + | sed '/^$/d' | awk '!seen[$0]++' + fi + fi + fi +} + +__fz_bash_completion() { + COMPREPLY=() + + local selected slug + eval "slug=${COMP_WORDS[@]:(-1)}" + + if [[ "$(__fz_generate_matches "$slug" | head | wc -l)" -gt 1 ]]; then + selected=$(__fz_generate_matches "$slug" \ + | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse \ + --bind 'shift-tab:up,tab:down' $FZF_DEFAULT_OPTS" fzf) + elif [[ "$(__fz_generate_matches "$slug" | head | wc -l)" -eq 1 ]]; then + selected=$(__fz_generate_matches "$slug") + else + return + fi + + if [[ -n "$selected" ]]; then + if [[ "$FZ_ABBREVIATE_HOME" == "1" ]]; then + selected=${selected/#\~/$HOME} + fi + selected=$(printf %q "$selected") + if [[ "$selected" != */ ]]; then + selected="${selected}/" + fi + if [[ "$FZ_ABBREVIATE_HOME" == "1" ]]; then + selected=${selected/#$HOME/\~} + fi + COMPREPLY=( "$selected" ) + fi + + printf '\e[5n' +} + +__fz_zsh_completion() { + setopt localoptions noshwordsplit noksh_arrays noposixbuiltins nonomatch + local args cmd selected slug + + args=(${(z)LBUFFER}) + cmd=${args[1]} + + if [[ "$cmd" != "$FZ_CMD" && "$cmd" != "$FZ_SUBDIR_CMD" ]] \ + || [[ "$cmd" == "$FZ_CMD" && "$LBUFFER" =~ "^\s*$FZ_CMD$" ]] \ + || [[ "$cmd" == "$FZ_SUBDIR_CMD" && "$LBUFFER" =~ "^\s*$FZ_SUBDIR_CMD$" ]]; then + zle ${__fz_zsh_default_completion:-expand-or-complete} + return + fi + + if [[ "${#args}" -gt 1 ]]; then + eval "slug=${args[-1]}" + fi + + if [[ "$(__fz_generate_matches "$slug" | head | wc -l)" -gt 1 ]]; then + selected=$(__fz_generate_matches "$slug" \ + | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse \ + --bind 'shift-tab:up,tab:down' $FZF_DEFAULT_OPTS" fzf) + elif [[ "$(__fz_generate_matches "$slug" | head | wc -l)" -eq 1 ]]; then + selected=$(__fz_generate_matches "$slug") + else + return + fi + + if [[ -n "$selected" ]]; then + if [[ "$FZ_ABBREVIATE_HOME" == "1" ]]; then + selected=${selected/#\~/$HOME} + fi + selected="${(q)selected}" + if [[ "$selected" != */ ]]; then + selected="${selected}/" + fi + if [[ "$FZ_ABBREVIATE_HOME" == "1" ]]; then + selected=${selected/#$HOME/\~} + fi + LBUFFER="$cmd $selected" + fi + + zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init +} + +__fz_init_bash_completion() { + # Enable redrawing line by printf '\e[5n' + bind '"\e[0n": redraw-current-line' + + complete -o nospace -F __fz_bash_completion "$FZ_CMD" + complete -o nospace -F __fz_bash_completion "$FZ_SUBDIR_CMD" +} + +__fz_init_zsh_completion() { + [ -n "$__fz_zsh_default_completion" ] || { + binding=$(bindkey '^I') + # $binding[(s: :w)2] + # The command substitution and following word splitting to determine the + # default zle widget for ^I formerly only works if the IFS parameter contains + # a space via $binding[(w)2]. Now it specifically splits at spaces, regardless + # of IFS. + # It’s not compatitable with bash so use awk instead. + [[ $binding =~ 'undefined-key' ]] \ + || __fz_zsh_default_completion=$(echo "$binding" | awk '{print $2}') + unset binding + } + zle -N __fz_zsh_completion + bindkey '^I' __fz_zsh_completion +} + +_fz() { + local rc + if [[ "$($FZ_HISTORY_LIST_GENERATOR "$@" | head | wc -l)" -gt 0 ]]; then + "$FZ_HISTORY_CD_CMD" "$@" + elif [[ "$FZ_SUBDIR_TRAVERSAL" -ne 0 ]]; then + err=$(cd "${@: -1}" 2>&1) + rc=$? + if ! cd "${@: -1}" 2>/dev/null; then + echo ${err#* } >&2 + return $rc + fi + fi +} + +_fzz() { + local rc + if [[ "$($FZ_SUBDIR_HISTORY_LIST_GENERATOR "$@" | head | wc -l)" -gt 0 ]]; then + if [[ -n "$BASH_VERSION" ]]; then + $FZ_SUBDIR_HISTORY_CD_CMD "$@" + elif [[ -n "$ZSH_VERSION" ]]; then + ${=FZ_SUBDIR_HISTORY_CD_CMD} "$@" + fi + elif [[ "$FZ_SUBDIR_TRAVERSAL" -ne 0 ]]; then + err=$(cd "${@: -1}" 2>&1) + rc=$? + if ! cd "${@: -1}" 2>/dev/null; then + echo ${err#* } >&2 + return $rc + fi + fi +} + +if [[ -n "$BASH_VERSION" ]]; then + __fz_init_bash_completion +elif [[ -n "$ZSH_VERSION" ]]; then + __fz_init_zsh_completion +fi