diff --git a/doc/pages/changelog.asciidoc b/doc/pages/changelog.asciidoc index 0cab1d0d..544a7d5f 100644 --- a/doc/pages/changelog.asciidoc +++ b/doc/pages/changelog.asciidoc @@ -3,6 +3,10 @@ This changelog contains major and/or breaking changes to Kakoune between released versions. +== Development version + +* `complete-command` (See <> + == Kakoune 2021.11.07 * Support for curly and separately colored underlines (undocumented in 2021.10.28) diff --git a/doc/pages/commands.asciidoc b/doc/pages/commands.asciidoc index 64060388..650cebb4 100644 --- a/doc/pages/commands.asciidoc +++ b/doc/pages/commands.asciidoc @@ -180,6 +180,11 @@ of the file onto the filesystem define a new alias named *name* in *scope* (See <> and <>) +*complete-command* [] []:: + *alias* compl + + configure how a command completion works + (See <>) + *unalias* []:: remove an alias if its current value is the same as the one passed as an optional parameter, remove it unconditionally otherwise @@ -473,25 +478,59 @@ New commands can be defined using the *define-command* command: define the documentation string for the command *-menu*::: - the suggestions generated by the completion options are the only - permitted parameters. - *-file-completion*::: - try file completion on any parameter passed to this command - *-client-completion*::: - try client name completion on any parameter passed to this command - *-buffer-completion*::: - try buffer name completion on any parameter passed to this command - *-command-completion*::: - try command completion on any parameter passed to this command - *-shell-completion*::: - try shell command completion on any parameter passed to this command - *-shell-script-completion*::: + *-shell-script-candidates*::: + old-style command completion specification, function as-if + the switch and its eventual parameter was passed to the + *complete-command* command + (See <>) + + The use of those switches is discouraged in favor of the + *complete-command* command. + +Using shell expansion allows defining complex commands or accessing +Kakoune's state: + +--------------------------------------------------------------------- +# create a directory for current buffer if it does not exist +define-command mkdir %{ nop %sh{ mkdir -p $(dirname $kak_buffile) } } +--------------------------------------------------------------------- + +== Configuring command completion + +Command completion can be configured with the *complete-command* command: + +*complete-command* [] []:: + *switches* can be: + + *-menu*::: + the suggestions generated by the completion options are the only + permitted parameters. Kakoune will autoselect the best completion + candidate on command validation. + + *completion_type* can be: + + *file-completion*::: + try file completion on any parameter passed to the command + + *client-completion*::: + try client name completion on any parameter passed to the command + + *buffer-completion*::: + try buffer name completion on any parameter passed to the command + + *command-completion*::: + try command completion on any parameter passed to the command + + *shell-completion*::: + try shell command completion on any parameter passed to the command + + *shell-script-completion*::: following string is a shell command which takes parameters as positional params and outputs one completion candidate per line. The provided shell command will run after each keypress. @@ -507,14 +546,14 @@ New commands can be defined using the *define-command* command: Position of the cursor inside the token being completed, in bytes from token start. - *-shell-script-candidates*::: - following string is a shell command which takes parameters as + *shell-script-candidates*::: + following string is a shell script which takes parameters as positional params and outputs one completion candidate per line. - The provided shell command will run once at the beginning of each + The provided shell script will run once at the beginning of each completion session, candidates are cached and then used by kakoune internal fuzzy engine. - During the execution of the shell command, the following env vars are + During the execution of the shell script, the following env vars are available: *$kak_token_to_complete*:::: @@ -522,14 +561,6 @@ New commands can be defined using the *define-command* command: Note that unlike the Unix `argv` tradition, 0 is the first argument, not the command name itself. -Using shell expansion allows defining complex commands or accessing -Kakoune's state: - ---------------------------------------------------------------------- -# create a directory for current buffer if it does not exist -define-command mkdir %{ nop %sh{ mkdir -p $(dirname $kak_buffile) } } ---------------------------------------------------------------------- - == Aliases With `:alias`, commands can be given additional names. diff --git a/rc/filetype/diff.kak b/rc/filetype/diff.kak index b4c550a6..bb9e21d6 100644 --- a/rc/filetype/diff.kak +++ b/rc/filetype/diff.kak @@ -19,14 +19,14 @@ add-highlighter shared/diff/ regex "^\+[^\n]*\n" 0:green,default add-highlighter shared/diff/ regex "^-[^\n]*\n" 0:red,default add-highlighter shared/diff/ regex "^@@[^\n]*@@" 0:cyan,default -define-command diff-jump \ - -docstring %{diff-jump [] []: edit the diff's source file at the cursor position. -Paths are resolved relative to , or the current working directory if unspecified. +define-command diff-jump -params .. -docstring %{ + diff-jump [] []: edit the diff's source file at the cursor position. + Paths are resolved relative to , or the current working directory if unspecified. -Switches: - - jump to the old file instead of the new file - - strip leading directory components, like -p in patch(1). Defaults to 1 if there is a 'diff' line (as printed by 'diff -r'), or 0 otherwise.} \ - -params .. -file-completion %{ + Switches: + - jump to the old file instead of the new file + - strip leading directory components, like -p in patch(1). Defaults to 1 if there is a 'diff' line (as printed by 'diff -r'), or 0 otherwise. + } %{ evaluate-commands -draft -save-regs c %{ # Save the column because we will move the cursor. set-register c %val{cursor_column} @@ -146,6 +146,7 @@ Switches: } } } +complete-command diff-jump file § diff --git a/rc/filetype/kakrc.kak b/rc/filetype/kakrc.kak index 00f3c4be..ccb84ec4 100644 --- a/rc/filetype/kakrc.kak +++ b/rc/filetype/kakrc.kak @@ -56,7 +56,7 @@ add-highlighter shared/kakrc/shell8 region -recurse '<' '(^|\h)\K-shell-script- evaluate-commands %sh{ # Grammar keywords="add-highlighter alias arrange-buffers buffer buffer-next buffer-previous catch - change-directory colorscheme debug declare-option declare-user-mode define-command + change-directory colorscheme debug declare-option declare-user-mode define-command complete-command delete-buffer delete-buffer! echo edit edit! enter-user-mode evaluate-commands execute-keys fail hook info kill kill! map menu nop on-key prompt provide-module quit quit! remove-highlighter remove-hooks rename-buffer rename-client rename-session require-module diff --git a/rc/tools/grep.kak b/rc/tools/grep.kak index f68d20e8..c7c27a35 100644 --- a/rc/tools/grep.kak +++ b/rc/tools/grep.kak @@ -4,7 +4,7 @@ declare-option -docstring "name of the client in which utilities display informa str toolsclient declare-option -hidden int grep_current_line 0 -define-command -params .. -file-completion -docstring %{ +define-command -params .. -docstring %{ grep []: grep utility wrapper All optional arguments are forwarded to the grep utility } grep %{ evaluate-commands %sh{ @@ -23,6 +23,7 @@ define-command -params .. -file-completion -docstring %{ hook -always -once buffer BufCloseFifo .* %{ nop %sh{ rm -r $(dirname ${output}) } } }" }} +complete-command grep file hook -group grep-highlight global WinSetOption filetype=grep %{ add-highlighter window/grep group diff --git a/rc/windowing/iterm.kak b/rc/windowing/iterm.kak index f3d72d3e..d99a975c 100644 --- a/rc/windowing/iterm.kak +++ b/rc/windowing/iterm.kak @@ -36,22 +36,25 @@ define-command -hidden -params 2.. iterm-terminal-split-impl %{ } } -define-command iterm-terminal-vertical -params 1.. -shell-completion -docstring ' +define-command iterm-terminal-vertical -params 1.. -docstring ' iterm-terminal-vertical []: create a new terminal as an iterm pane The current pane is split into two, left and right The program passed as argument will be executed in the new terminal'\ %{ iterm-terminal-split-impl 'vertically' %arg{@} } -define-command iterm-terminal-horizontal -params 1.. -shell-completion -docstring ' +complete-command iterm-terminal-vertical shell + +define-command iterm-terminal-horizontal -params 1.. -docstring ' iterm-terminal-horizontal []: create a new terminal as an iterm pane The current pane is split into two, top and bottom The program passed as argument will be executed in the new terminal'\ %{ iterm-terminal-split-impl 'horizontally' %arg{@} } +complete-command iterm-terminal-horizontal shell -define-command iterm-terminal-tab -params 1.. -shell-completion -docstring ' +define-command iterm-terminal-tab -params 1.. -docstring ' iterm-terminal-tab []: create a new terminal as an iterm tab The program passed as argument will be executed in the new terminal'\ %{ @@ -76,8 +79,9 @@ The program passed as argument will be executed in the new terminal'\ -e "end tell" >/dev/null } } +complete-command iterm-terminal-tab shell -define-command iterm-terminal-window -params 1.. -shell-completion -docstring ' +define-command iterm-terminal-window -params 1.. -docstring ' iterm-terminal-window []: create a new terminal as an iterm window The program passed as argument will be executed in the new terminal'\ %{ @@ -100,8 +104,9 @@ The program passed as argument will be executed in the new terminal'\ -e "end tell" >/dev/null } } +complete-command iterm-terminal-window shell -define-command iterm-focus -params ..1 -client-completion -docstring ' +define-command iterm-focus -params ..1 -docstring ' iterm-focus []: focus the given client If no client is passed then the current one is used' \ %{ @@ -131,6 +136,7 @@ If no client is passed then the current one is used' \ fi } } +complete-command iterm-focus client alias global focus iterm-focus alias global terminal iterm-terminal-vertical diff --git a/rc/windowing/kitty.kak b/rc/windowing/kitty.kak index 5790a7c4..1e4880dc 100644 --- a/rc/windowing/kitty.kak +++ b/rc/windowing/kitty.kak @@ -10,7 +10,7 @@ evaluate-commands %sh{ declare-option -docstring %{window type that kitty creates on new and repl calls (window|os-window)} str kitty_window_type window -define-command kitty-terminal -params 1.. -shell-completion -docstring ' +define-command kitty-terminal -params 1.. -docstring ' kitty-terminal []: create a new terminal as a kitty window The program passed as argument will be executed in the new terminal' \ %{ @@ -28,8 +28,9 @@ The program passed as argument will be executed in the new terminal' \ kitty @ $listen launch --no-response --type="$kak_opt_kitty_window_type" --cwd="$PWD" $match "$@" } } +complete-command kitty-terminal shell -define-command kitty-terminal-tab -params 1.. -shell-completion -docstring ' +define-command kitty-terminal-tab -params 1.. -docstring ' kitty-terminal-tab []: create a new terminal as kitty tab The program passed as argument will be executed in the new terminal' \ %{ @@ -47,8 +48,9 @@ The program passed as argument will be executed in the new terminal' \ kitty @ $listen launch --no-response --type=tab --cwd="$PWD" $match "$@" } } +complete-command kitty-terminal-tab shell -define-command kitty-focus -params ..1 -client-completion -docstring ' +define-command kitty-focus -params ..1 -docstring ' kitty-focus []: focus the given client If no client is passed then the current one is used' \ %{ @@ -70,6 +72,7 @@ If no client is passed then the current one is used' \ fi } } +complete-command kitty-focus client alias global terminal kitty-terminal alias global terminal-tab kitty-terminal-tab diff --git a/rc/windowing/new-client.kak b/rc/windowing/new-client.kak index ee48a509..e017344e 100644 --- a/rc/windowing/new-client.kak +++ b/rc/windowing/new-client.kak @@ -1,4 +1,4 @@ -define-command new -params .. -command-completion -docstring ' +define-command new -params .. -docstring ' new []: create a new Kakoune client The ''terminal'' alias is being used to determine the user''s preferred terminal emulator The optional arguments are passed as commands to the new client' \ @@ -6,3 +6,4 @@ The optional arguments are passed as commands to the new client' \ terminal kak -c %val{session} -e "%arg{@}" } +complete-command new command diff --git a/rc/windowing/repl/dtach.kak b/rc/windowing/repl/dtach.kak index b99e4724..02d11ecb 100644 --- a/rc/windowing/repl/dtach.kak +++ b/rc/windowing/repl/dtach.kak @@ -12,7 +12,6 @@ define-command -docstring %{ All optional parameters are forwarded to the new terminal window } \ -params .. \ - -shell-completion \ dtach-repl %{ terminal sh -c %{ file="$(mktemp -u -t kak_dtach_repl.XXXXX)" trap 'rm -f "${file}"' EXIT @@ -22,6 +21,7 @@ define-command -docstring %{ dtach -c "${file}" -E sh -c "${@:-$SHELL}" || "${@:-$SHELL}" } -- %val{client} %val{session} %arg{@} } +complete-command dtach-repl shell define-command dtach-send-text -params 0..1 -docstring %{ dtach-send-text [text]: Send text to the REPL. diff --git a/rc/windowing/repl/kitty.kak b/rc/windowing/repl/kitty.kak index 2b7bb2a0..28ca1b89 100644 --- a/rc/windowing/repl/kitty.kak +++ b/rc/windowing/repl/kitty.kak @@ -4,7 +4,7 @@ hook global ModuleLoaded kitty %{ provide-module kitty-repl %{ -define-command -params .. -shell-completion \ +define-command -params .. \ -docstring %{ kitty-repl []: Create a new window for repl interaction. @@ -31,6 +31,7 @@ define-command -params .. -shell-completion \ kitty @ $listen launch --no-response --keep-focus --type="$kak_opt_kitty_window_type" --title=kak_repl_window --cwd="$PWD" $match $cmd } } +complete-command kitty-repl shell define-command -hidden -params 0..1 \ -docstring %{ diff --git a/rc/windowing/repl/tmux.kak b/rc/windowing/repl/tmux.kak index beee3e57..037799a5 100644 --- a/rc/windowing/repl/tmux.kak +++ b/rc/windowing/repl/tmux.kak @@ -23,17 +23,20 @@ define-command -hidden -params 1.. tmux-repl-impl %{ } } -define-command tmux-repl-vertical -params 0.. -command-completion -docstring "Create a new vertical pane for repl interaction" %{ +define-command tmux-repl-vertical -params 0.. -docstring "Create a new vertical pane for repl interaction" %{ tmux-repl-impl 'split-window -v' %arg{@} } +complete-command tmux-repl-vertical command -define-command tmux-repl-horizontal -params 0.. -command-completion -docstring "Create a new horizontal pane for repl interaction" %{ +define-command tmux-repl-horizontal -params 0.. -docstring "Create a new horizontal pane for repl interaction" %{ tmux-repl-impl 'split-window -h' %arg{@} } +complete-command tmux-repl-horizontal command -define-command tmux-repl-window -params 0.. -command-completion -docstring "Create a new window for repl interaction" %{ +define-command tmux-repl-window -params 0.. -docstring "Create a new window for repl interaction" %{ tmux-repl-impl 'new-window' %arg{@} } +complete-command tmux-repl-window command define-command -params 0..1 tmux-repl-set-pane -docstring %{ tmux-repl-set-pane [pane number]: Set an existing tmux pane for repl interaction diff --git a/rc/windowing/repl/x11.kak b/rc/windowing/repl/x11.kak index 6d9b5c5b..0c37e994 100644 --- a/rc/windowing/repl/x11.kak +++ b/rc/windowing/repl/x11.kak @@ -11,7 +11,6 @@ define-command -docstring %{ All optional parameters are forwarded to the new window } \ -params .. \ - -shell-completion \ x11-repl %{ x11-terminal sh -c %{ winid="${WINDOWID:-$(xdotool search --pid ${PPID} | tail -1)}" printf "evaluate-commands -try-client $1 \ @@ -20,6 +19,7 @@ define-command -docstring %{ [ "$1" ] && "$@" || "$SHELL" } -- %val{client} %val{session} %arg{@} } +complete-command x11-repl shell define-command x11-send-text -params 0..1 -docstring %{ x11-send-text [text]: Send text to the REPL window. diff --git a/rc/windowing/screen.kak b/rc/windowing/screen.kak index 02c63a83..8e4b6ee5 100644 --- a/rc/windowing/screen.kak +++ b/rc/windowing/screen.kak @@ -28,21 +28,25 @@ define-command screen-terminal-impl -hidden -params 3.. %{ } } -define-command screen-terminal-vertical -params 1.. -shell-completion -docstring ' +define-command screen-terminal-vertical -params 1.. -docstring ' screen-terminal-vertical [] []: create a new terminal as a screen pane The current pane is split into two, left and right The program passed as argument will be executed in the new terminal' \ %{ screen-terminal-impl 'split -v' 'focus right' %arg{@} } -define-command screen-terminal-horizontal -params 1.. -shell-completion -docstring ' +complete-command screen-terminal-vertical shell + +define-command screen-terminal-horizontal -params 1.. -docstring ' screen-terminal-horizontal []: create a new terminal as a screen pane The current pane is split into two, top and bottom The program passed as argument will be executed in the new terminal' \ %{ screen-terminal-impl 'split -h' 'focus down' %arg{@} } -define-command screen-terminal-window -params 1.. -shell-completion -docstring ' +complete-command screen-terminal-horizontal shell + +define-command screen-terminal-window -params 1.. -docstring ' screen-terminal-window []: create a new terminal as a screen window The program passed as argument will be executed in the new terminal' \ %{ @@ -51,8 +55,9 @@ The program passed as argument will be executed in the new terminal' \ screen -X screen "$@" < "/dev/$tty" } } +complete-command screen-terminal-window shell -define-command screen-focus -params ..1 -client-completion -docstring ' +define-command screen-focus -params ..1 -docstring ' screen-focus []: focus the given client If no client is passed then the current one is used' \ %{ @@ -67,6 +72,7 @@ If no client is passed then the current one is used' \ fi } } +complete-command screen-focus client alias global focus screen-focus alias global terminal screen-terminal-vertical diff --git a/rc/windowing/tmux.kak b/rc/windowing/tmux.kak index a57bc7c2..fe656cc2 100644 --- a/rc/windowing/tmux.kak +++ b/rc/windowing/tmux.kak @@ -30,28 +30,33 @@ define-command -hidden -params 2.. tmux-terminal-impl %{ } } -define-command tmux-terminal-vertical -params 1.. -shell-completion -docstring ' +define-command tmux-terminal-vertical -params 1.. -docstring ' tmux-terminal-vertical []: create a new terminal as a tmux pane The current pane is split into two, top and bottom The program passed as argument will be executed in the new terminal' \ %{ tmux-terminal-impl 'split-window -v' %arg{@} } -define-command tmux-terminal-horizontal -params 1.. -shell-completion -docstring ' +complete-command tmux-terminal-vertical shell + +define-command tmux-terminal-horizontal -params 1.. -docstring ' tmux-terminal-horizontal []: create a new terminal as a tmux pane The current pane is split into two, left and right The program passed as argument will be executed in the new terminal' \ %{ tmux-terminal-impl 'split-window -h' %arg{@} } -define-command tmux-terminal-window -params 1.. -shell-completion -docstring ' +complete-command tmux-terminal-horizontal shell + +define-command tmux-terminal-window -params 1.. -docstring ' tmux-terminal-window [] []: create a new terminal as a tmux window The program passed as argument will be executed in the new terminal' \ %{ tmux-terminal-impl 'new-window' %arg{@} } +complete-command tmux-terminal-window shell -define-command tmux-focus -params ..1 -client-completion -docstring ' +define-command tmux-focus -params ..1 -docstring ' tmux-focus []: focus the given client If no client is passed then the current one is used' \ %{ @@ -66,6 +71,7 @@ If no client is passed then the current one is used' \ fi } } +complete-command tmux-focus client ## The default behaviour for the `new` command is to open an horizontal pane in a tmux session alias global focus tmux-focus diff --git a/rc/windowing/wayland.kak b/rc/windowing/wayland.kak index 04c7ebdc..dd3632d9 100644 --- a/rc/windowing/wayland.kak +++ b/rc/windowing/wayland.kak @@ -28,7 +28,7 @@ A shell command is appended to the one set in this option at runtime} \ done } -define-command wayland-terminal -params 1.. -shell-completion -docstring ' +define-command wayland-terminal -params 1.. -docstring ' wayland-terminal []: create a new terminal as a Wayland window The program passed as argument will be executed in the new terminal' \ %{ @@ -43,13 +43,15 @@ The program passed as argument will be executed in the new terminal' \ } } } +complete-command wayland-terminal shell -define-command wayland-focus -params ..1 -client-completion -docstring ' +define-command wayland-focus -params ..1 -docstring ' wayland-focus []: focus a given client''s window If no client is passed, then the current client is used' \ %{ fail There is no way to focus another window on Wayland } +complete-command wayland-focus client alias global focus wayland-focus alias global terminal wayland-terminal diff --git a/rc/windowing/x11.kak b/rc/windowing/x11.kak index 6b2d13df..4c39f7ba 100644 --- a/rc/windowing/x11.kak +++ b/rc/windowing/x11.kak @@ -33,7 +33,7 @@ A shell command is appended to the one set in this option at runtime} \ done } -define-command x11-terminal -params 1.. -shell-completion -docstring ' +define-command x11-terminal -params 1.. -docstring ' x11-terminal []: create a new terminal as an X11 window The program passed as argument will be executed in the new terminal' \ %{ @@ -48,8 +48,9 @@ The program passed as argument will be executed in the new terminal' \ } } } +complete-command x11-terminal shell -define-command x11-focus -params ..1 -client-completion -docstring ' +define-command x11-focus -params ..1 -docstring ' x11-focus []: focus a given client''s window If no client is passed, then the current client is used' \ %{ @@ -62,6 +63,7 @@ If no client is passed, then the current client is used' \ fi } } +complete-command x11-focus client alias global focus x11-focus alias global terminal x11-terminal diff --git a/src/command_manager.cc b/src/command_manager.cc index 5663b1e3..0f3cb749 100644 --- a/src/command_manager.cc +++ b/src/command_manager.cc @@ -41,6 +41,15 @@ void CommandManager::register_command(String command_name, std::move(completer) }; } +void CommandManager::set_command_completer(StringView command_name, CommandCompleter completer) +{ + auto it = m_commands.find(command_name); + if (it == m_commands.end()) + throw runtime_error(format("no such command '{}'", command_name)); + + it->value.completer = std::move(completer); +} + bool CommandManager::module_defined(StringView module_name) const { return m_modules.find(module_name) != m_modules.end(); diff --git a/src/command_manager.hh b/src/command_manager.hh index 6489e044..d41c1dcf 100644 --- a/src/command_manager.hh +++ b/src/command_manager.hh @@ -112,6 +112,8 @@ public: CommandHelper helper = CommandHelper(), CommandCompleter completer = CommandCompleter()); + void set_command_completer(StringView command_name, CommandCompleter completer); + Completions complete_command_name(const Context& context, StringView query) const; void clear_last_complete_command() { m_last_complete_command = String{}; } diff --git a/src/commands.cc b/src/commands.cc index e66c043e..2b419e76 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -1159,6 +1159,82 @@ Vector params_to_shell(const ParametersParser& parser) return vars; } +CommandCompleter make_command_completer(StringView type, StringView param, Completions::Flags completions_flags) +{ + if (type == "file") + { + return [=](const Context& context, CompletionFlags flags, + CommandParameters params, + size_t token_to_complete, ByteCount pos_in_token) { + const String& prefix = params[token_to_complete]; + const auto& ignored_files = context.options()["ignored_files"].get(); + return Completions{0_byte, pos_in_token, + complete_filename(prefix, ignored_files, + pos_in_token, FilenameFlags::Expand), + completions_flags}; + }; + } + else if (type == "client") + { + return [=](const Context& context, CompletionFlags flags, + CommandParameters params, + size_t token_to_complete, ByteCount pos_in_token) + { + const String& prefix = params[token_to_complete]; + auto& cm = ClientManager::instance(); + return Completions{0_byte, pos_in_token, + cm.complete_client_name(prefix, pos_in_token), + completions_flags}; + }; + } + else if (type == "buffer") + { + return [=](const Context& context, CompletionFlags flags, + CommandParameters params, + size_t token_to_complete, ByteCount pos_in_token) + { + return add_flags(complete_buffer_name, completions_flags)( + context, flags, params[token_to_complete], pos_in_token); + }; + } + else if (type == "shell-script") + { + if (param.empty()) + throw runtime_error("shell-script requires a shell script parameter"); + + return ShellScriptCompleter{param.str(), completions_flags}; + } + else if (type == "shell-script-candidates") + { + if (param.empty()) + throw runtime_error("shell-script-candidates requires a shell script parameter"); + + return ShellCandidatesCompleter{param.str(), completions_flags}; + } + else if (type == "command") + { + return [](const Context& context, CompletionFlags flags, + CommandParameters params, + size_t token_to_complete, ByteCount pos_in_token) + { + return CommandManager::instance().complete( + context, flags, params, token_to_complete, pos_in_token); + }; + } + else if (type == "shell") + { + return [=](const Context& context, CompletionFlags flags, + CommandParameters params, + size_t token_to_complete, ByteCount pos_in_token) + { + return add_flags(shell_complete, completions_flags)( + context, flags, params[token_to_complete], pos_in_token); + }; + } + else + throw runtime_error(format("invalid command completion type '{}'", type)); +} + void define_command(const ParametersParser& parser, Context& context, const ShellContext&) { const String& cmd_name = parser[0]; @@ -1211,75 +1287,22 @@ void define_command(const ParametersParser& parser, Context& context, const Shel } CommandCompleter completer; - if (parser.get_switch("file-completion")) + for (StringView completion_switch : {"file-completion", "client-completion", "buffer-completion", + "shell-script-completion", "shell-script-candidates", + "command-completion", "shell-completion"}) { - completer = [=](const Context& context, CompletionFlags flags, - CommandParameters params, - size_t token_to_complete, ByteCount pos_in_token) + if (auto param = parser.get_switch(completion_switch)) { - const String& prefix = params[token_to_complete]; - const auto& ignored_files = context.options()["ignored_files"].get(); - return Completions{0_byte, pos_in_token, - complete_filename(prefix, ignored_files, - pos_in_token, FilenameFlags::Expand), - completions_flags}; - }; + constexpr StringView suffix = "-completion"; + if (completion_switch.ends_with(suffix)) + completion_switch = completion_switch.substr(0, completion_switch.length() - suffix.length()); + completer = make_command_completer(completion_switch, *param, completions_flags); + break; + } } - else if (parser.get_switch("client-completion")) - { - completer = [=](const Context& context, CompletionFlags flags, - CommandParameters params, - size_t token_to_complete, ByteCount pos_in_token) - { - const String& prefix = params[token_to_complete]; - auto& cm = ClientManager::instance(); - return Completions{0_byte, pos_in_token, - cm.complete_client_name(prefix, pos_in_token), - completions_flags}; - }; - } - else if (parser.get_switch("buffer-completion")) - { - completer = [=](const Context& context, CompletionFlags flags, - CommandParameters params, - size_t token_to_complete, ByteCount pos_in_token) - { - return add_flags(complete_buffer_name, completions_flags)( - context, flags, params[token_to_complete], pos_in_token); - }; - } - else if (auto shell_script = parser.get_switch("shell-script-completion")) - { - completer = ShellScriptCompleter{shell_script->str(), completions_flags}; - } - else if (auto shell_script = parser.get_switch("shell-script-candidates")) - { - completer = ShellCandidatesCompleter{shell_script->str(), completions_flags}; - } - else if (parser.get_switch("command-completion")) - { - completer = [](const Context& context, CompletionFlags flags, - CommandParameters params, - size_t token_to_complete, ByteCount pos_in_token) - { - return CommandManager::instance().complete( - context, flags, params, token_to_complete, pos_in_token); - }; - } - else if (parser.get_switch("shell-completion")) - { - completer = [=](const Context& context, CompletionFlags flags, - CommandParameters params, - size_t token_to_complete, ByteCount pos_in_token) - { - return add_flags(shell_complete, completions_flags)( - context, flags, params[token_to_complete], pos_in_token); - }; - } - auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{})); - cm.register_command(cmd_name, cmd, docstring, desc, flags, CommandHelper{}, completer); + cm.register_command(cmd_name, cmd, docstring, desc, flags, CommandHelper{}, std::move(completer)); } const CommandDesc define_command_cmd = { @@ -1354,6 +1377,25 @@ const CommandDesc unalias_cmd = { } }; +const CommandDesc complete_command_cmd = { + "complete-command", + "compl", + "complete-command [] []\n" + "define command completion", + ParameterDesc{ + { { "menu", { false, "treat completions as the only valid inputs" } }, }, + ParameterDesc::Flags::None, 2, 3}, + CommandFlags::None, + CommandHelper{}, + make_completer(complete_command_name), + [](const ParametersParser& parser, Context& context, const ShellContext&) + { + const Completions::Flags flags = parser.get_switch("menu") ? Completions::Flags::Menu : Completions::Flags::None; + CommandCompleter completer = make_command_completer(parser[1], parser.positional_count() >= 3 ? parser[2] : StringView{}, flags); + CommandManager::instance().set_command_completer(parser[0], std::move(completer)); + } +}; + const CommandDesc echo_cmd = { "echo", nullptr, @@ -2719,6 +2761,7 @@ void register_commands() register_command(remove_hook_cmd); register_command(trigger_user_hook_cmd); register_command(define_command_cmd); + register_command(complete_command_cmd); register_command(alias_cmd); register_command(unalias_cmd); register_command(echo_cmd); diff --git a/src/main.cc b/src/main.cc index 98e33d83..eb240288 100644 --- a/src/main.cc +++ b/src/main.cc @@ -46,6 +46,7 @@ struct { } constexpr version_notes[] = { { 0, "» pipe commands do not append final end-of-lines anymore\n" + "» {+u}complete-command{} to configure command completion\n" }, { 20211107, "» colored and curly underlines support (undocumented in 20210828)\n" diff --git a/src/string.hh b/src/string.hh index d14929f4..c3e7401f 100644 --- a/src/string.hh +++ b/src/string.hh @@ -67,6 +67,9 @@ public: [[gnu::always_inline]] bool empty() const { return type().length() == 0_byte; } + bool starts_with(StringView str) const; + bool ends_with(StringView str) const; + ByteCount byte_count_to(CharCount count) const { return utf8::advance(begin(), end(), count) - begin(); } @@ -306,6 +309,22 @@ inline StringView StringOps::substr(ColumnCount from, ColumnCoun return StringView{ beg, utf8::advance(beg, end(), length) }; } +template +inline bool StringOps::starts_with(StringView str) const +{ + if (type().length() < str.length()) + return false; + return substr(0, str.length()) == str; +} + +template +inline bool StringOps::ends_with(StringView str) const +{ + if (type().length() < str.length()) + return false; + return substr(type().length() - str.length()) == str; +} + inline String& operator+=(String& lhs, StringView rhs) { lhs.append(rhs.data(), rhs.length()); diff --git a/src/string_utils.cc b/src/string_utils.cc index a1c1c38d..37ca6716 100644 --- a/src/string_utils.cc +++ b/src/string_utils.cc @@ -396,6 +396,16 @@ UnitTest test_string{[]() { kak_assert(String("youpi ") + "matin" == "youpi matin"); + kak_assert(StringView{"youpi"}.starts_with("")); + kak_assert(StringView{"youpi"}.starts_with("you")); + kak_assert(StringView{"youpi"}.starts_with("youpi")); + kak_assert(not StringView{"youpi"}.starts_with("youpi!")); + + kak_assert(StringView{"youpi"}.ends_with("")); + kak_assert(StringView{"youpi"}.ends_with("pi")); + kak_assert(StringView{"youpi"}.ends_with("youpi")); + kak_assert(not StringView{"youpi"}.ends_with("oup")); + auto wrapped = "wrap this paragraph\n respecting whitespaces and much_too_long_words" | wrap_at(16) | gather(); kak_assert(wrapped.size() == 6); kak_assert(wrapped[0] == "wrap this");