diff --git a/README.md b/README.md index 186440a..067ab0c 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,67 @@ source ~/.zshrc # または新しいターミナルを開く ``` +### シェル補完のセットアップ(オプション) + +タブ補完機能を使うと、コマンド入力が格段に便利になります: + +```bash +bmc g # ブックマーク一覧を表示 +bmc g sam # 'sample-project' に補完 +bmc list --tag # タグ一覧を表示 +``` + +#### Bash + +`~/.bashrc` に以下を追加: + +```bash +# bookmark-cli completion +source ~/.local/share/bookmark-cli/completions/bmc.bash +``` + +設定を反映: +```bash +source ~/.bashrc +``` + +#### Zsh + +`~/.zshrc` に以下を追加: + +```bash +# bookmark-cli completion +fpath=(~/.local/share/bookmark-cli/completions $fpath) +autoload -Uz compinit && compinit +``` + +設定を反映: +```bash +source ~/.zshrc +``` + +#### Fish + +補完ファイルをシンボリックリンク: + +```bash +mkdir -p ~/.config/fish/completions +ln -s ~/.local/share/bookmark-cli/completions/bmc.fish ~/.config/fish/completions/ +``` + +Fishは次回起動時に自動的に補完を読み込みます。 + +#### 補完機能の確認 + +セットアップ後、以下でテスト: + +```bash +bmc # 全コマンドを表示 +bmc g # ブックマーク一覧を表示 +bmc g sam # 'sample-project' に補完 +bmc list --tag # タグ一覧を表示 +``` + ### 基本コマンド ```bash diff --git a/completions/_bmc b/completions/_bmc new file mode 100644 index 0000000..19057d4 --- /dev/null +++ b/completions/_bmc @@ -0,0 +1,123 @@ +#compdef bmc bm + +_bmc() { + local curcontext="$curcontext" state line + typeset -A opt_args + + local -a commands + commands=( + 'add:Add a bookmark for a directory' + 'a:Alias for add' + 'go:Navigate to a bookmarked directory' + 'g:Alias for go' + 'list:List all bookmarks' + 'ls:Alias for list' + 'tags:Show all tags' + 'ui:Interactive UI with fzf' + 'browse:Alias for ui' + 'fz:Alias for ui' + 'remove:Remove a bookmark' + 'rm:Alias for remove' + 'del:Alias for remove' + 'rename:Rename a bookmark' + 'mv:Alias for rename' + 'export:Export bookmarks to JSON' + 'exp:Alias for export' + 'import:Import bookmarks from file' + 'imp:Alias for import' + 'edit:Edit bookmark file with $EDITOR' + 'e:Alias for edit' + 'validate:Validate all bookmarks' + 'check:Alias for validate' + 'clean:Remove invalid bookmarks' + 'doctor:Show bookmark health diagnostic' + 'recent:Show recently used bookmarks' + 'r:Alias for recent' + 'frequent:Show frequently used bookmarks' + 'stats:Show bookmark usage statistics' + 'help:Show help message' + 'h:Alias for help' + ) + + _arguments -C \ + '1: :->command' \ + '*:: :->args' + + case $state in + command) + _describe -t commands 'bmc commands' commands + ;; + args) + local cmd="${words[1]}" + + # Normalize aliases + case "$cmd" in + a) cmd="add" ;; + g) cmd="go" ;; + ls) cmd="list" ;; + browse|fz) cmd="ui" ;; + rm|del) cmd="remove" ;; + mv) cmd="rename" ;; + exp) cmd="export" ;; + imp) cmd="import" ;; + e) cmd="edit" ;; + check) cmd="validate" ;; + r) cmd="recent" ;; + h) cmd="help" ;; + esac + + case "$cmd" in + add) + _arguments \ + '1:bookmark name:' \ + '2:path:_directories' \ + '(-d --description)'{-d,--description}'[Description]:description:' \ + '(-t --tags)'{-t,--tags}'[Tags (comma-separated)]:tags:' + ;; + go|remove) + _arguments '1:bookmark:_bmc_bookmarks' + ;; + rename) + _arguments \ + '1:old bookmark name:_bmc_bookmarks' \ + '2:new bookmark name:' + ;; + list) + _arguments '--tag[Filter by tag]:tag:_bmc_tags' + ;; + clean) + _arguments '--dry-run[Preview without removing]' + ;; + import|export) + _arguments '1:file:_files' + ;; + esac + ;; + esac +} + +_bmc_bookmarks() { + local -a bookmarks + local bmc_cmd="bmc" + + command -v "$bmc_cmd" >/dev/null 2>&1 || return 1 + + bookmarks=(${(f)"$(NO_COLOR=1 $bmc_cmd list 2>/dev/null | \ + awk 'NR > 2 && NF > 0 && $1 != "Tags:" { print $1 }')"}) + + _describe 'bookmarks' bookmarks +} + +_bmc_tags() { + local -a tags + local bmc_cmd="bmc" + + command -v "$bmc_cmd" >/dev/null 2>&1 || return 1 + + tags=(${(f)"$(NO_COLOR=1 $bmc_cmd tags 2>/dev/null | \ + awk 'NF > 0 && $1 != "All" && $1 != "No" { print $1 }')"}) + + _describe 'tags' tags +} + +_bmc "$@" diff --git a/completions/bmc.bash b/completions/bmc.bash new file mode 100644 index 0000000..926e4a0 --- /dev/null +++ b/completions/bmc.bash @@ -0,0 +1,101 @@ +#!/bin/bash +# Bash completion for bmc (bookmark-cli) + +_bmc_completions() { + local cur prev words cword + _init_completion || return + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # All commands (primary + aliases) + local commands="add a go g list ls tags ui browse fz remove rm del rename mv export exp import imp edit e validate check clean doctor recent r frequent stats help h" + + # First argument: complete commands + if [ $COMP_CWORD -eq 1 ]; then + COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) + return 0 + fi + + # Get primary command + local cmd="${COMP_WORDS[1]}" + + # Normalize aliases + case "$cmd" in + a) cmd="add" ;; + g) cmd="go" ;; + ls) cmd="list" ;; + browse|fz) cmd="ui" ;; + rm|del) cmd="remove" ;; + mv) cmd="rename" ;; + exp) cmd="export" ;; + imp) cmd="import" ;; + e) cmd="edit" ;; + check) cmd="validate" ;; + r) cmd="recent" ;; + h) cmd="help" ;; + esac + + # Command-specific completion + case "$cmd" in + go|remove) + local bookmarks=$(_bmc_get_bookmarks) + COMPREPLY=( $(compgen -W "$bookmarks" -- "$cur") ) + ;; + rename) + if [ $COMP_CWORD -eq 2 ]; then + local bookmarks=$(_bmc_get_bookmarks) + COMPREPLY=( $(compgen -W "$bookmarks" -- "$cur") ) + fi + ;; + add) + case "$prev" in + -d|--description|-t|--tags) + COMPREPLY=() + ;; + *) + COMPREPLY=( $(compgen -d -- "$cur") ) + ;; + esac + ;; + list) + if [[ "$cur" == -* ]]; then + COMPREPLY=( $(compgen -W "--tag" -- "$cur") ) + elif [[ "$prev" == "--tag" ]]; then + local tags=$(_bmc_get_tags) + COMPREPLY=( $(compgen -W "$tags" -- "$cur") ) + fi + ;; + clean) + [[ "$cur" == -* ]] && COMPREPLY=( $(compgen -W "--dry-run" -- "$cur") ) + ;; + import|export) + COMPREPLY=( $(compgen -f -- "$cur") ) + ;; + esac + + return 0 +} + +# Get bookmark names +_bmc_get_bookmarks() { + local bmc_cmd="bmc" + + command -v "$bmc_cmd" >/dev/null 2>&1 || return 1 + + NO_COLOR=1 "$bmc_cmd" list 2>/dev/null | \ + awk 'NR > 2 && NF > 0 && $1 != "Tags:" { print $1 }' || true +} + +# Get tags +_bmc_get_tags() { + local bmc_cmd="bmc" + + command -v "$bmc_cmd" >/dev/null 2>&1 || return 1 + + NO_COLOR=1 "$bmc_cmd" tags 2>/dev/null | \ + awk 'NF > 0 && $1 != "All" && $1 != "No" { print $1 }' || true +} + +complete -F _bmc_completions bmc +command -v bm >/dev/null 2>&1 && complete -F _bmc_completions bm diff --git a/completions/bmc.fish b/completions/bmc.fish new file mode 100644 index 0000000..713a853 --- /dev/null +++ b/completions/bmc.fish @@ -0,0 +1,87 @@ +# Fish completion for bmc (bookmark-cli) + +function __fish_bmc_needs_command + set -l cmd (commandline -opc) + test (count $cmd) -eq 1 +end + +function __fish_bmc_using_command + set -l cmd (commandline -opc) + if test (count $cmd) -gt 1 + test $argv[1] = $cmd[2] + else + return 1 + end +end + +function __fish_bmc_bookmarks + command -v bmc >/dev/null 2>&1 || return 1 + + NO_COLOR=1 bmc list 2>/dev/null | \ + awk 'NR > 2 && NF > 0 && $1 != "Tags:" { print $1 }' +end + +function __fish_bmc_tags + command -v bmc >/dev/null 2>&1 || return 1 + + NO_COLOR=1 bmc tags 2>/dev/null | \ + awk 'NF > 0 && $1 != "All" && $1 != "No" { print $1 }' +end + +# Subcommands +complete -c bmc -f -n __fish_bmc_needs_command -a add -d 'Add a bookmark' +complete -c bmc -f -n __fish_bmc_needs_command -a a -d 'Alias for add' +complete -c bmc -f -n __fish_bmc_needs_command -a go -d 'Navigate to bookmark' +complete -c bmc -f -n __fish_bmc_needs_command -a g -d 'Alias for go' +complete -c bmc -f -n __fish_bmc_needs_command -a list -d 'List bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a ls -d 'Alias for list' +complete -c bmc -f -n __fish_bmc_needs_command -a tags -d 'Show all tags' +complete -c bmc -f -n __fish_bmc_needs_command -a ui -d 'Interactive UI' +complete -c bmc -f -n __fish_bmc_needs_command -a browse -d 'Alias for ui' +complete -c bmc -f -n __fish_bmc_needs_command -a fz -d 'Alias for ui' +complete -c bmc -f -n __fish_bmc_needs_command -a remove -d 'Remove bookmark' +complete -c bmc -f -n __fish_bmc_needs_command -a rm -d 'Alias for remove' +complete -c bmc -f -n __fish_bmc_needs_command -a del -d 'Alias for remove' +complete -c bmc -f -n __fish_bmc_needs_command -a rename -d 'Rename bookmark' +complete -c bmc -f -n __fish_bmc_needs_command -a mv -d 'Alias for rename' +complete -c bmc -f -n __fish_bmc_needs_command -a export -d 'Export bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a exp -d 'Alias for export' +complete -c bmc -f -n __fish_bmc_needs_command -a import -d 'Import bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a imp -d 'Alias for import' +complete -c bmc -f -n __fish_bmc_needs_command -a edit -d 'Edit bookmark file' +complete -c bmc -f -n __fish_bmc_needs_command -a e -d 'Alias for edit' +complete -c bmc -f -n __fish_bmc_needs_command -a validate -d 'Validate bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a check -d 'Alias for validate' +complete -c bmc -f -n __fish_bmc_needs_command -a clean -d 'Remove invalid bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a doctor -d 'Health diagnostic' +complete -c bmc -f -n __fish_bmc_needs_command -a recent -d 'Recent bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a r -d 'Alias for recent' +complete -c bmc -f -n __fish_bmc_needs_command -a frequent -d 'Frequent bookmarks' +complete -c bmc -f -n __fish_bmc_needs_command -a stats -d 'Usage statistics' +complete -c bmc -f -n __fish_bmc_needs_command -a help -d 'Show help' +complete -c bmc -f -n __fish_bmc_needs_command -a h -d 'Alias for help' + +# Bookmark name completion +complete -c bmc -f -n '__fish_bmc_using_command go' -a '(__fish_bmc_bookmarks)' +complete -c bmc -f -n '__fish_bmc_using_command g' -a '(__fish_bmc_bookmarks)' +complete -c bmc -f -n '__fish_bmc_using_command remove' -a '(__fish_bmc_bookmarks)' +complete -c bmc -f -n '__fish_bmc_using_command rm' -a '(__fish_bmc_bookmarks)' +complete -c bmc -f -n '__fish_bmc_using_command del' -a '(__fish_bmc_bookmarks)' +complete -c bmc -f -n '__fish_bmc_using_command rename' -a '(__fish_bmc_bookmarks)' +complete -c bmc -f -n '__fish_bmc_using_command mv' -a '(__fish_bmc_bookmarks)' + +# Options for add command +complete -c bmc -f -n '__fish_bmc_using_command add' -s d -l description -d 'Bookmark description' +complete -c bmc -f -n '__fish_bmc_using_command add' -s t -l tags -d 'Tags (comma-separated)' +complete -c bmc -f -n '__fish_bmc_using_command a' -s d -l description -d 'Bookmark description' +complete -c bmc -f -n '__fish_bmc_using_command a' -s t -l tags -d 'Tags (comma-separated)' + +# Options for list command +complete -c bmc -f -n '__fish_bmc_using_command list' -l tag -d 'Filter by tag' -a '(__fish_bmc_tags)' +complete -c bmc -f -n '__fish_bmc_using_command ls' -l tag -d 'Filter by tag' -a '(__fish_bmc_tags)' + +# Options for clean command +complete -c bmc -f -n '__fish_bmc_using_command clean' -l dry-run -d 'Preview without removing' + +# Alias support +complete -c bm -w bmc diff --git a/tests/test_completion.bats b/tests/test_completion.bats new file mode 100644 index 0000000..10ea5c9 --- /dev/null +++ b/tests/test_completion.bats @@ -0,0 +1,108 @@ +#!/usr/bin/env bats + +setup() { + export TEST_DIR=$(mktemp -d) + export BM_FILE="$TEST_DIR/bookmarks" + export BM_HISTORY="$TEST_DIR/history" + + # Create test bookmarks + mkdir -p "$TEST_DIR/projects" + echo "project1:$TEST_DIR/projects/proj1:Test project 1:work,dev" > "$BM_FILE" + echo "project2:$TEST_DIR/projects/proj2:Test project 2:personal" >> "$BM_FILE" + echo "sample-project:$TEST_DIR/projects/sample:Sample:test" >> "$BM_FILE" + + export PATH="$BATS_TEST_DIRNAME/../cli:$PATH" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +@test "completion: bmc list outputs parseable bookmark names" { + run bmc list + [ "$status" -eq 0 ] + [[ "$output" =~ "project1" ]] + [[ "$output" =~ "project2" ]] + [[ "$output" =~ "sample-project" ]] +} + +@test "completion: bmc list with NO_COLOR produces clean output" { + NO_COLOR=1 run bmc list + [ "$status" -eq 0 ] + [[ ! "$output" =~ $'\033' ]] +} + +@test "completion: bookmark names can be extracted from list output" { + local names=$(NO_COLOR=1 bmc list | awk 'NR > 2 && NF > 0 && $1 != "Tags:" { print $1 }') + + [ $(echo "$names" | wc -l | tr -d ' ') -eq 3 ] + echo "$names" | grep -q "^project1$" + echo "$names" | grep -q "^project2$" + echo "$names" | grep -q "^sample-project$" +} + +@test "completion: tags can be extracted from tags output" { + run bmc tags + [ "$status" -eq 0 ] + [[ "$output" =~ "work" ]] + [[ "$output" =~ "personal" ]] + [[ "$output" =~ "dev" ]] + [[ "$output" =~ "test" ]] +} + +@test "completion: empty bookmark file handled gracefully" { + > "$BM_FILE" + run bmc list + [ "$status" -eq 0 ] + [[ "$output" =~ "No bookmarks found" ]] +} + +@test "completion: bash completion script has valid syntax" { + [ -f "$BATS_TEST_DIRNAME/../completions/bmc.bash" ] + bash -n "$BATS_TEST_DIRNAME/../completions/bmc.bash" +} + +@test "completion: zsh completion script exists" { + [ -f "$BATS_TEST_DIRNAME/../completions/_bmc" ] +} + +@test "completion: fish completion script exists" { + [ -f "$BATS_TEST_DIRNAME/../completions/bmc.fish" ] +} + +@test "completion: bookmark names with hyphens and underscores" { + echo "my-project:$TEST_DIR:Test:work" >> "$BM_FILE" + echo "my_project:$TEST_DIR:Test:work" >> "$BM_FILE" + + local names=$(NO_COLOR=1 bmc list | awk 'NR > 2 && NF > 0 && $1 != "Tags:" { print $1 }') + echo "$names" | grep -q "my-project" + echo "$names" | grep -q "my_project" +} + +@test "completion: bmc list completes quickly with many bookmarks" { + for i in {1..50}; do + echo "test$i:$TEST_DIR:Test $i:test" >> "$BM_FILE" + done + + local start=$(date +%s) + run bmc list + local end=$(date +%s) + local duration=$((end - start)) + + [ "$status" -eq 0 ] + [ "$duration" -lt 2 ] +} + +@test "completion: bash completion can be sourced" { + bash -c "source $BATS_TEST_DIRNAME/../completions/bmc.bash && declare -F _bmc_completions" +} + +@test "completion: bash helper functions work" { + bash -c " + source $BATS_TEST_DIRNAME/../completions/bmc.bash + export BM_FILE='$BM_FILE' + export PATH='$BATS_TEST_DIRNAME/../cli:\$PATH' + bookmarks=\$(_bmc_get_bookmarks) + echo \"\$bookmarks\" | grep -q 'project1' + " +}