summaryrefslogtreecommitdiff
path: root/autoload/tools
diff options
context:
space:
mode:
Diffstat (limited to 'autoload/tools')
-rw-r--r--autoload/tools/autorestore.asciidoc13
-rw-r--r--autoload/tools/autorestore.kak93
-rw-r--r--autoload/tools/autowrap.kak50
-rw-r--r--autoload/tools/clang.kak196
-rw-r--r--autoload/tools/comment.kak218
-rw-r--r--autoload/tools/ctags.kak168
-rw-r--r--autoload/tools/doc.asciidoc45
-rw-r--r--autoload/tools/doc.kak195
-rw-r--r--autoload/tools/format.kak38
-rw-r--r--autoload/tools/git.kak787
-rw-r--r--autoload/tools/go/gopls.kak98
-rw-r--r--autoload/tools/grep.kak66
-rw-r--r--autoload/tools/jump.kak70
-rw-r--r--autoload/tools/lint.asciidoc26
-rw-r--r--autoload/tools/lint.kak452
-rw-r--r--autoload/tools/make.kak92
-rw-r--r--autoload/tools/man.kak139
-rw-r--r--autoload/tools/menu.kak85
-rw-r--r--autoload/tools/patch-range.pl113
-rw-r--r--autoload/tools/patch.kak63
-rw-r--r--autoload/tools/python/jedi.kak77
-rw-r--r--autoload/tools/rust/racer.kak123
-rw-r--r--autoload/tools/spell.kak184
23 files changed, 3391 insertions, 0 deletions
diff --git a/autoload/tools/autorestore.asciidoc b/autoload/tools/autorestore.asciidoc
new file mode 100644
index 0000000..528fcf5
--- /dev/null
+++ b/autoload/tools/autorestore.asciidoc
@@ -0,0 +1,13 @@
+= Automatically restore unsaved work after a crash.
+
+When Kakoune crashes, it automatically writes out unsaved changes to backup
+files with predictable names. When you edit a file, if such a backup file
+exists, this plugin will automatically load the content of the backup file
+instead.
+
+By default, backup files are deleted when restored. You can set the
+`autorestore_purge_restored` option to `false` to prevent this.
+
+If you don't want backups to be restored automatically, use the
+`autorestore-disable` command to disable the feature for the current session,
+or put it in your `kakrc` to disable the feature forever.
diff --git a/autoload/tools/autorestore.kak b/autoload/tools/autorestore.kak
new file mode 100644
index 0000000..52febe3
--- /dev/null
+++ b/autoload/tools/autorestore.kak
@@ -0,0 +1,93 @@
+declare-option -docstring %{
+ Remove backups once they've been restored
+
+ See `:doc autorestore` for details.
+ } \
+ bool autorestore_purge_restored true
+
+## Insert the content of the backup file into the current buffer, if a suitable one is found
+define-command autorestore-restore-buffer \
+ -docstring %{
+ Restore the backup for the current file if it exists
+
+ See `:doc autorestore` for details.
+ } \
+%{
+ evaluate-commands %sh{
+ buffer_basename="${kak_buffile##*/}"
+ buffer_dirname=$(dirname "${kak_buffile}")
+
+ if [ -f "${kak_buffile}" ]; then
+ newer=$(find "${buffer_dirname}"/".${buffer_basename}.kak."* -newer "${kak_buffile}" -exec ls -1t {} + 2>/dev/null | head -n 1)
+ older=$(find "${buffer_dirname}"/".${buffer_basename}.kak."* \! -newer "${kak_buffile}" -exec ls -1t {} + 2>/dev/null | head -n 1)
+ else
+ # New buffers that were never written to disk.
+ newer=$(ls -1t "${buffer_dirname}"/".${buffer_basename}.kak."* 2>/dev/null | head -n 1)
+ older=""
+ fi
+
+ if [ -z "${newer}" ]; then
+ if [ -n "${older}" ]; then
+ printf %s\\n "
+ echo -debug Old backup file(s) found: will not restore ${older} .
+ "
+ fi
+ exit
+ fi
+
+ printf %s\\n "
+ ## Replace the content of the buffer with the content of the backup file
+ echo -debug Restoring file: ${newer}
+
+ execute-keys -draft %{%d!cat<space>\"${newer}\"<ret>jd}
+
+ ## If the backup file has to be removed, issue the command once
+ ## the current buffer has been saved
+ ## If the autorestore_purge_restored option has been unset right after the
+ ## buffer was restored, do not remove the backup
+ hook -group autorestore buffer BufWritePost '${kak_buffile}' %{
+ nop %sh{
+ if [ \"\${kak_opt_autorestore_purge_restored}\" = true ];
+ then
+ rm -f \"${buffer_dirname}/.${buffer_basename}.kak.\"*
+ fi
+ }
+ }
+ "
+ }
+}
+
+## Remove all the backups that have been created for the current buffer
+define-command autorestore-purge-backups \
+ -docstring %{
+ Remove all the backups of the current buffer
+
+ See `:doc autorestore` for details.
+ } \
+%{
+ evaluate-commands %sh{
+ [ ! -f "${kak_buffile}" ] && exit
+
+ buffer_basename="${kak_bufname##*/}"
+ buffer_dirname=$(dirname "${kak_bufname}")
+
+ rm -f "${buffer_dirname}/.${buffer_basename}.kak."*
+
+ printf %s\\n "
+ echo -markup {Information}Backup files removed.
+ "
+ }
+}
+
+## If for some reason, backup files need to be ignored
+define-command autorestore-disable \
+ -docstring %{
+ Disable automatic backup recovering
+
+ See `:doc autorestore` for details.
+ } \
+%{
+ remove-hooks global autorestore
+}
+
+hook -group autorestore global BufCreate .* %{ autorestore-restore-buffer }
diff --git a/autoload/tools/autowrap.kak b/autoload/tools/autowrap.kak
new file mode 100644
index 0000000..d742f6e
--- /dev/null
+++ b/autoload/tools/autowrap.kak
@@ -0,0 +1,50 @@
+declare-option -docstring "maximum amount of characters per line, after which a newline character will be inserted" \
+ int autowrap_column 80
+
+declare-option -docstring %{
+ when enabled, paragraph formatting will reformat the whole paragraph in which characters are being inserted
+ This can potentially break formatting of documents containing markup (e.g. markdown)
+} bool autowrap_format_paragraph no
+declare-option -docstring %{
+ command to which the paragraphs to wrap will be passed
+ all occurences of '%c' are replaced with `autowrap_column`
+} str autowrap_fmtcmd 'fold -s -w %c'
+
+define-command -hidden autowrap-cursor %{ evaluate-commands -save-regs '/"|^@m' %{
+ try %{
+ ## if the line isn't too long, do nothing
+ execute-keys -draft "x<a-k>^[^\n]{%opt{autowrap_column},}[^\n]<ret>"
+
+ try %{
+ reg m "%val{selections_desc}"
+
+ ## if we're adding characters past the limit, just wrap them around
+ execute-keys -draft "<a-h><a-k>.{%opt{autowrap_column}}\h*[^\s]*<ret>1s(\h+)[^\h]*\z<ret>c<ret>"
+ } catch %{
+ ## if we're adding characters in the middle of a sentence, use
+ ## the `fmtcmd` command to wrap the entire paragraph
+ evaluate-commands %sh{
+ if [ "${kak_opt_autowrap_format_paragraph}" = true ] \
+ && [ -n "${kak_opt_autowrap_fmtcmd}" ]; then
+ format_cmd=$(printf %s "${kak_opt_autowrap_fmtcmd}" \
+ | sed "s/%c/${kak_opt_autowrap_column}/g")
+ printf %s "
+ evaluate-commands -draft %{
+ execute-keys '<a-]>px<a-j>|${format_cmd}<ret>'
+ try %{ execute-keys s\h+$<ret> d }
+ }
+ select '${kak_main_reg_m}'
+ "
+ fi
+ }
+ }
+ }
+} }
+
+define-command autowrap-enable -docstring "Automatically wrap the lines in which characters are inserted" %{
+ hook -group autowrap window InsertChar [^\n] autowrap-cursor
+}
+
+define-command autowrap-disable -docstring "Disable automatic line wrapping" %{
+ remove-hooks window autowrap
+}
diff --git a/autoload/tools/clang.kak b/autoload/tools/clang.kak
new file mode 100644
index 0000000..41f6c55
--- /dev/null
+++ b/autoload/tools/clang.kak
@@ -0,0 +1,196 @@
+hook -once global BufSetOption filetype=(c|cpp) %{
+ require-module clang
+}
+
+provide-module clang %[
+
+declare-option -docstring "options to pass to the `clang` shell command" \
+ str clang_options
+
+declare-option -docstring "directory from which to invoke clang" \
+ str clang_directory
+
+declare-option -hidden completions clang_completions
+declare-option -hidden line-specs clang_flags
+declare-option -hidden line-specs clang_errors
+
+define-command -params ..1 \
+ -docstring %{
+ Parse the contents of the current buffer
+ The syntaxic errors detected during parsing are shown when auto-diagnostics are enabled
+ } clang-parse %{
+ evaluate-commands %sh{
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-clang.XXXXXXXX)
+ mkfifo ${dir}/fifo
+ printf %s\\n "
+ evaluate-commands -no-hooks write -sync -method replace ${dir}/buf
+ evaluate-commands -draft %{
+ edit! -fifo ${dir}/fifo -debug *clang-output*
+ set-option buffer filetype make
+ set-option buffer jump_current_line 0
+ hook -once -always buffer BufCloseFifo .* %{ nop %sh{ rm -r ${dir} } }
+ }"
+
+ # this runs in a detached shell, asynchronously, so that kakoune does
+ # not hang while clang is running. As completions references a cursor
+ # position and a buffer timestamp, only valid completions should be
+ # displayed.
+ ((
+ trap - INT QUIT
+ until [ -f ${dir}/buf ]; do :; done # wait for the buffer to be written
+
+ if [ -n "$kak_opt_clang_directory" ]; then
+ cd "$kak_opt_clang_directory"
+ fi
+ case ${kak_opt_filetype} in
+ (c) ft=c ;;
+ (cpp) ft=c++ ;;
+ (obj-c) ft=objective-c ;;
+ (*) ft=c++ ;;
+ esac
+
+ if [ "$1" = "-complete" ]; then
+ pos=-:${kak_cursor_line}:${kak_cursor_column}
+ header="${kak_cursor_line}.${kak_cursor_column}@${kak_timestamp}"
+ compl=$(clang++ -x ${ft} -fsyntax-only ${kak_opt_clang_options} \
+ -Xclang -code-completion-brief-comments -Xclang -code-completion-at=${pos} - < ${dir}/buf 2> ${dir}/stderr |
+ awk -F ': ' '
+ /^COMPLETION:/ && $2 !~ /[(,](Hidden|Inaccessible)[),]/ {
+ candidate=$3
+ gsub(/[[<{]#[^#]*#[]>}]/, "", candidate)
+ gsub(/~/, "~~", candidate)
+ gsub(/\|/, "\\|", candidate)
+
+ gsub(/[[{<]#|#[]}>]/, " ", $3)
+ gsub(/:: /, "::", $3)
+ gsub(/ ,/, ",", $3)
+ gsub(/^ +| +$/, "", $3)
+ docstring=$4 ? $3 "\n" $4 : $3
+
+ gsub(/~|!/, "&&", docstring)
+ gsub(/\|/, "\\|", docstring)
+ if (candidate in candidates)
+ candidates[candidate]=candidates[candidate] "\n" docstring
+ else
+ candidates[candidate]=docstring
+ }
+ END {
+ for (candidate in candidates) {
+ menu=candidate
+ gsub(/(^|[^[:alnum:]_])(operator|new|delete)($|[^{}_[:alnum:]]+)/, "{keyword}&{}", menu)
+ gsub(/(^|[[:space:]])(int|size_t|bool|char|unsigned|signed|long)($|[[:space:]])/, "{type}&{}", menu)
+ gsub(/[^{}_[:alnum:]]+/, "{operator}&{}", menu)
+ printf "%%~%s|info -style menu %!%s!|%s~ ", candidate, candidates[candidate], menu
+ }
+ }')
+ printf %s\\n "evaluate-commands -client ${kak_client} echo 'clang completion done'
+ set-option 'buffer=${kak_buffile}' clang_completions ${header} ${compl}" | kak -p ${kak_session}
+ else
+ clang++ -x ${ft} -fsyntax-only ${kak_opt_clang_options} - < ${dir}/buf 2> ${dir}/stderr
+ printf %s\\n "evaluate-commands -client ${kak_client} echo 'clang parsing done'" | kak -p ${kak_session}
+ fi
+
+ flags=$(cat ${dir}/stderr | sed -Ene "
+ /^<stdin>:[0-9]+:([0-9]+:)? (fatal )?error/ { s/^<stdin>:([0-9]+):.*/'\1|{red}█'/; p }
+ /^<stdin>:[0-9]+:([0-9]+:)? warning/ { s/^<stdin>:([0-9]+):.*/'\1|{yellow}█'/; p }
+ " | paste -s -d ' ' -)
+
+ errors=$(cat ${dir}/stderr | sed -Ene "
+ /^<stdin>:[0-9]+:([0-9]+:)? ((fatal )?error|warning)/ {
+ s/'/''/g; s/^<stdin>:([0-9]+):([0-9]+:)? (.*)/'\1|\3'/; p
+ }" | sort -n | paste -s -d ' ' -)
+
+ sed -e "s|<stdin>|${kak_bufname}|g" < ${dir}/stderr > ${dir}/fifo
+
+ printf %s\\n "set-option 'buffer=${kak_buffile}' clang_flags ${kak_timestamp} ${flags}
+ set-option 'buffer=${kak_buffile}' clang_errors ${kak_timestamp} ${errors}" | kak -p ${kak_session}
+ ) & ) > /dev/null 2>&1 < /dev/null
+ }
+}
+
+define-command clang-complete -docstring "Complete the current selection" %{ clang-parse -complete }
+
+define-command -hidden clang-show-completion-info %[ try %[
+ evaluate-commands -draft %[
+ execute-keys ,{( <a-k> ^\( <ret> b <a-k> \A\w+\z <ret>
+ evaluate-commands %sh[
+ desc=$(printf %s\\n "${kak_opt_clang_completions}" | sed -e "{ s/\([^\\]\):/\1\n/g }" | sed -ne "/^${kak_selection}|/ { s/^[^|]\+|//; s/|.*$//; s/\\\:/:/g; p }")
+ if [ -n "$desc" ]; then
+ printf %s\\n "evaluate-commands -client $kak_client %{info -anchor ${kak_cursor_line}.${kak_cursor_column} -style above %{${desc}}}"
+ fi
+ ] ]
+] ]
+
+define-command clang-enable-autocomplete -docstring "Enable automatic clang completion" %{
+ set-option window completers "option=clang_completions" %opt{completers}
+ hook window -group clang-autocomplete InsertIdle .* %{
+ try %{
+ execute-keys -draft <a-h><a-k>(\.|->|::).\z<ret>
+ echo 'completing...'
+ clang-complete
+ }
+ clang-show-completion-info
+ }
+ alias window complete clang-complete
+}
+
+define-command clang-disable-autocomplete -docstring "Disable automatic clang completion" %{
+ evaluate-commands %sh{ printf "set-option window completers %s\n" $(printf %s "${kak_opt_completers}" | sed -e "s/'option=clang_completions'//g") }
+ remove-hooks window clang-autocomplete
+ unalias window complete clang-complete
+}
+
+define-command -hidden clang-show-error-info %{
+ update-option buffer clang_errors # Ensure we are up to date with buffer changes
+ evaluate-commands %sh{
+ eval "set -- ${kak_quoted_opt_clang_errors}"
+ shift # skip timestamp
+ desc=$(for error in "$@"; do
+ if [ "${error%%|*}" = "$kak_cursor_line" ]; then
+ printf '%s\n' "${error##*|}"
+ fi
+ done)
+ if [ -n "$desc" ]; then
+ desc=$(printf %s "${desc}" | sed "s/'/''/g")
+ printf "info -anchor %d.%d '%s'\n" "${kak_cursor_line}" "${kak_cursor_column}" "${desc}"
+ fi
+ } }
+
+define-command clang-enable-diagnostics -docstring %{
+ Activate automatic error reporting and diagnostics
+ Information about the analysis will be shown after the buffer has been parsed with the clang-parse function
+} %{
+ add-highlighter window/clang_flags flag-lines default clang_flags
+ hook window -group clang-diagnostics NormalIdle .* %{ clang-show-error-info }
+ hook window -group clang-diagnostics WinSetOption ^clang_errors=.* %{ info; clang-show-error-info }
+}
+
+define-command clang-disable-diagnostics -docstring "Disable automatic error reporting and diagnostics" %{
+ remove-highlighter window/clang_flags
+ remove-hooks window clang-diagnostics
+}
+
+define-command clang-diagnostics-next -docstring "Jump to the next line that contains an error" %{
+ update-option buffer clang_errors # Ensure we are up to date with buffer changes
+ evaluate-commands %sh{
+ eval "set -- ${kak_quoted_opt_clang_errors}"
+ shift # skip timestamp
+ unset line
+ unset first_line
+ for error in "$@"; do
+ candidate=${error%%|*}
+ first_line=${first_line-$candidate}
+ if [ "$candidate" -gt $kak_cursor_line ]; then
+ line=$candidate
+ break
+ fi
+ done
+ line=${line-$first_line}
+ if [ -n "$line" ]; then
+ printf %s\\n "execute-keys ${line} g"
+ else
+ echo "fail no next clang diagnostic"
+ fi
+ } }
+
+]
diff --git a/autoload/tools/comment.kak b/autoload/tools/comment.kak
new file mode 100644
index 0000000..b610073
--- /dev/null
+++ b/autoload/tools/comment.kak
@@ -0,0 +1,218 @@
+# Line comments
+# If the language has no line comments, set to ''
+declare-option -docstring "characters inserted at the beginning of a commented line" \
+ str comment_line '#'
+
+# Block comments
+declare-option -docstring "characters inserted before a commented block" \
+ str comment_block_begin
+declare-option -docstring "characters inserted after a commented block" \
+ str comment_block_end
+
+# Default comments for all languages
+hook global BufSetOption filetype=asciidoc %{
+ set-option buffer comment_line '//'
+ set-option buffer comment_block_begin '////'
+ set-option buffer comment_block_end '////'
+}
+
+hook global BufSetOption filetype=(c|cpp|dart|gluon|go|java|javascript|objc|odin|php|pony|protobuf|rust|sass|scala|scss|swift|typescript|groovy) %{
+ set-option buffer comment_line '//'
+ set-option buffer comment_block_begin '/*'
+ set-option buffer comment_block_end '*/'
+}
+
+hook global BufSetOption filetype=(cabal|haskell|moon|idris|elm|dhall|purescript) %{
+ set-option buffer comment_line '--'
+ set-option buffer comment_block_begin '{-'
+ set-option buffer comment_block_end '-}'
+}
+
+hook global BufSetOption filetype=clojure %{
+ set-option buffer comment_line '#_'
+ set-option buffer comment_block_begin '(comment '
+ set-option buffer comment_block_end ')'
+}
+
+hook global BufSetOption filetype=janet %{
+ set-option buffer comment_line '#'
+ set-option buffer comment_block_begin '(comment '
+ set-option buffer comment_block_end ')'
+}
+
+hook global BufSetOption filetype=coffee %{
+ set-option buffer comment_block_begin '###'
+ set-option buffer comment_block_end '###'
+}
+
+hook global BufSetOption filetype=conf %{
+ set-option buffer comment_line '#'
+}
+
+hook global BufSetOption filetype=css %{
+ set-option buffer comment_line ''
+ set-option buffer comment_block_begin '/*'
+ set-option buffer comment_block_end '*/'
+}
+
+hook global BufSetOption filetype=d %{
+ set-option buffer comment_line '//'
+ set-option buffer comment_block_begin '/+'
+ set-option buffer comment_block_end '+/'
+}
+
+hook global BufSetOption filetype=(fennel|gas|ini) %{
+ set-option buffer comment_line ';'
+}
+
+hook global BufSetOption filetype=haml %{
+ set-option buffer comment_line '-#'
+}
+
+hook global BufSetOption filetype=(html|xml) %{
+ set-option buffer comment_line ''
+ set-option buffer comment_block_begin '<!--'
+ set-option buffer comment_block_end '-->'
+}
+
+hook global BufSetOption filetype=(latex|mercury) %{
+ set-option buffer comment_line '%'
+}
+
+hook global BufSetOption filetype=ledger %{
+ set-option buffer comment_line ';'
+}
+
+hook global BufSetOption filetype=(lisp|scheme) %{
+ set-option buffer comment_line ';'
+ set-option buffer comment_block_begin '#|'
+ set-option buffer comment_block_end '|#'
+}
+
+hook global BufSetOption filetype=lua %{
+ set-option buffer comment_line '--'
+ set-option buffer comment_block_begin '--[['
+ set-option buffer comment_block_end ']]'
+}
+
+hook global BufSetOption filetype=markdown %{
+ set-option buffer comment_line ''
+ set-option buffer comment_block_begin '[//]: # "'
+ set-option buffer comment_block_end '"'
+}
+
+hook global BufSetOption filetype=(ocaml|coq) %{
+ set-option buffer comment_line ''
+ set-option buffer comment_block_begin '(* '
+ set-option buffer comment_block_end ' *)'
+}
+
+hook global BufSetOption filetype=((free|object)?pascal|delphi) %{
+ set-option buffer comment_line '//'
+ set-option buffer comment_block_begin '{'
+ set-option buffer comment_block_end '}'
+}
+
+hook global BufSetOption filetype=perl %{
+ set-option buffer comment_block_begin '#['
+ set-option buffer comment_block_end ']'
+}
+
+hook global BufSetOption filetype=(pug|zig|cue|hare) %{
+ set-option buffer comment_line '//'
+}
+
+hook global BufSetOption filetype=python %{
+ set-option buffer comment_block_begin "'''"
+ set-option buffer comment_block_end "'''"
+}
+
+hook global BufSetOption filetype=r %{
+ set-option buffer comment_line '#'
+}
+
+hook global BufSetOption filetype=ragel %{
+ set-option buffer comment_line '%%'
+ set-option buffer comment_block_begin '%%{'
+ set-option buffer comment_block_end '}%%'
+}
+
+hook global BufSetOption filetype=ruby %{
+ set-option buffer comment_block_begin '^begin='
+ set-option buffer comment_block_end '^=end'
+}
+
+hook global BufSetOption filetype=sql %{
+ set-option buffer comment_line '--'
+ set-option buffer comment_block_begin '/*'
+ set-option buffer comment_block_end '*/'
+}
+
+define-command comment-block -docstring '(un)comment selections using block comments' %{
+ evaluate-commands %sh{
+ if [ -z "${kak_opt_comment_block_begin}" ] || [ -z "${kak_opt_comment_block_end}" ]; then
+ echo "fail \"The 'comment_block' options are empty, could not comment the selection\""
+ fi
+ }
+ evaluate-commands -save-regs '"/' -draft %{
+ # Keep non-empty selections
+ execute-keys <a-K>\A\s*\z<ret>
+
+ try %{
+ # Assert that the selection has been commented
+ set-register / "\A\Q%opt{comment_block_begin}\E.*\Q%opt{comment_block_end}\E\n*\z"
+ execute-keys "s<ret>"
+ # Uncomment it
+ set-register / "\A\Q%opt{comment_block_begin}\E|\Q%opt{comment_block_end}\E\n*\z"
+ execute-keys s<ret>d
+ } catch %{
+ # Comment the selection
+ set-register '"' "%opt{comment_block_begin}"
+ execute-keys -draft P
+ set-register '"' "%opt{comment_block_end}"
+ execute-keys p
+ }
+ }
+}
+
+define-command comment-line -docstring '(un)comment selected lines using line comments' %{
+ evaluate-commands %sh{
+ if [ -z "${kak_opt_comment_line}" ]; then
+ echo "fail \"The 'comment_line' option is empty, could not comment the line\""
+ fi
+ }
+ evaluate-commands -save-regs '"/' -draft %{
+ # Select the content of the lines, without indentation
+ execute-keys <a-s>gi<a-l>
+
+ try %{
+ # Keep non-empty lines
+ execute-keys <a-K>\A\s*\z<ret>
+ }
+
+ try %{
+ set-register / "\A\Q%opt{comment_line}\E\h?"
+
+ try %{
+ # See if there are any uncommented lines in the selection
+ execute-keys -draft <a-K><ret>
+
+ # There are uncommented lines, so comment everything
+ set-register '"' "%opt{comment_line} "
+ align-selections-left
+ execute-keys P
+ } catch %{
+ # All lines were commented, so uncomment everything
+ execute-keys s<ret>d
+ }
+ }
+ }
+}
+
+define-command align-selections-left -docstring 'extend selections to the left to align with the leftmost selected column' %{
+ evaluate-commands %sh{
+ leftmost_column=$(echo "$kak_selections_desc" | tr ' ' '\n' | cut -d',' -f1 | cut -d'.' -f2 | sort -n | head -n1)
+ aligned_selections=$(echo "$kak_selections_desc" | sed -E "s/\.[0-9]+,/.$leftmost_column,/g")
+ echo "select $aligned_selections"
+ }
+}
diff --git a/autoload/tools/ctags.kak b/autoload/tools/ctags.kak
new file mode 100644
index 0000000..9ad9cbb
--- /dev/null
+++ b/autoload/tools/ctags.kak
@@ -0,0 +1,168 @@
+# Kakoune CTags support script
+#
+# This script requires the readtags command available in universal-ctags
+
+declare-option -docstring "minimum characters before triggering autocomplete" \
+ int ctags_min_chars 3
+
+declare-option -docstring "list of paths to tag files to parse when looking up a symbol" \
+ str-list ctagsfiles 'tags'
+
+declare-option -hidden completions ctags_completions
+
+declare-option -docstring "shell command to run" str readtagscmd "readtags"
+
+define-command -params ..1 \
+ -shell-script-candidates %{
+ realpath() { ( cd "$(dirname "$1")"; printf "%s/%s\n" "$(pwd -P)" "$(basename "$1")" ) }
+ eval "set -- $kak_quoted_opt_ctagsfiles"
+ for candidate in "$@"; do
+ [ -f "$candidate" ] && realpath "$candidate"
+ done | awk '!x[$0]++;' | # remove duplicates
+ while read -r tags; do
+ namecache="${tags%/*}/.kak.${tags##*/}.namecache"
+ if [ -z "$(find "$namecache" -prune -newer "$tags")" ]; then
+ cut -f 1 "$tags" | grep -v '^!' | uniq > "$namecache"
+ fi
+ cat "$namecache"
+ done
+ } \
+ -docstring %{
+ ctags-search [<symbol>]: jump to a symbol's definition
+ If no symbol is passed then the current selection is used as symbol name
+ } \
+ ctags-search %[ require-module menu; evaluate-commands %sh[
+ realpath() { ( cd "$(dirname "$1")"; printf "%s/%s\n" "$(pwd -P)" "$(basename "$1")" ) }
+ export tagname="${1:-${kak_selection}}"
+ eval "set -- $kak_quoted_opt_ctagsfiles"
+ for candidate in "$@"; do
+ [ -f "$candidate" ] && realpath "$candidate"
+ done | awk '!x[$0]++' | # remove duplicates
+ while read -r tags; do
+ printf '!TAGROOT\t%s\n' "$(realpath "${tags%/*}")/"
+ ${kak_opt_readtagscmd} -t "$tags" "$tagname"
+ done | awk -F '\t|\n' '
+ /^!TAGROOT\t/ { tagroot=$2 }
+ /[^\t]+\t[^\t]+\t\/\^.*\$?\// {
+ line = $0; sub(".*\t/\\^", "", line); sub("\\$?/$", "", line);
+ menu_info = line; gsub("!", "!!", menu_info); gsub(/^[\t ]+/, "", menu_info); gsub(/\t/, " ", menu_info);
+ keys = line; gsub(/</, "<lt>", keys); gsub(/\t/, "<c-v><c-i>", keys); gsub("!", "!!", keys); gsub("&", "&&", keys); gsub("#", "##", keys); gsub("\\|", "||", keys); gsub("\\\\/", "/", keys);
+ menu_item = $2; gsub("!", "!!", menu_item);
+ edit_path = path($2); gsub("&", "&&", edit_path); gsub("#", "##", edit_path); gsub("\\|", "||", edit_path);
+ select = $1; gsub(/</, "<lt>", select); gsub(/\t/, "<c-v><c-i>", select); gsub("!", "!!", select); gsub("&", "&&", select); gsub("#", "##", select); gsub("\\|", "||", select);
+ out = out "%!" menu_item ": " menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|/\\Q" keys "<ret>vc| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "<ret>| & # !"
+ }
+ /[^\t]+\t[^\t]+\t[0-9]+/ {
+ menu_item = $2; gsub("!", "!!", menu_item);
+ select = $1; gsub(/</, "<lt>", select); gsub(/\t/, "<c-v><c-i>", select); gsub("!", "!!", select); gsub("&", "&&", select); gsub("#", "##", select); gsub("\\|", "||", select);
+ menu_info = $3; gsub("!", "!!", menu_info);
+ edit_path = path($2); gsub("!", "!!", edit_path); gsub("#", "##", edit_path); gsub("&", "&&", edit_path); gsub("\\|", "||", edit_path);
+ line_number = $3;
+ out = out "%!" menu_item ": " menu_info "! %!evaluate-commands %# try %& edit -existing %|" edit_path "|; execute-keys %|" line_number "gx| & catch %& fail unable to find tag &; try %& execute-keys %|s\\Q" select "<ret>| & # !"
+ }
+ END { print ( length(out) == 0 ? "fail no such tag " ENVIRON["tagname"] : "menu -markup -auto-single " out ) }
+ # Ensure x is an absolute file path, by prepending with tagroot
+ function path(x) { return x ~/^\// ? x : tagroot x }'
+ ]]
+
+define-command ctags-complete -docstring "Complete the current selection" %{
+ nop %sh{
+ (
+ header="${kak_cursor_line}.${kak_cursor_column}@${kak_timestamp}"
+ compl=$(
+ eval "set -- $kak_quoted_opt_ctagsfiles"
+ for ctagsfile in "$@"; do
+ ${kak_opt_readtagscmd} -p -t "$ctagsfile" ${kak_selection}
+ done | awk '{ uniq[$1]++ } END { for (elem in uniq) printf " %s||%s", elem, elem }'
+ )
+ printf %s\\n "evaluate-commands -client ${kak_client} set-option buffer=${kak_bufname} ctags_completions ${header}${compl}" | \
+ kak -p ${kak_session}
+ ) > /dev/null 2>&1 < /dev/null &
+ }
+}
+
+define-command ctags-funcinfo -docstring "Display ctags information about a selected function" %{
+ evaluate-commands -draft %{
+ try %{
+ execute-keys '[(;B<a-k>[a-zA-Z_]+\(<ret><a-;>'
+ evaluate-commands %sh{
+ f=${kak_selection%?}
+ sig='\tsignature:(.*)'
+ csn='\t(class|struct|namespace):(\S+)'
+ sigs=$(${kak_opt_readtagscmd} -e -Q '(eq? $kind "f")' "${f}" | sed -Ee "s/^.*${csn}.*${sig}$/\3 [\2::${f}]/ ;t ;s/^.*${sig}$/\1 [${f}]/")
+ if [ -n "$sigs" ]; then
+ printf %s\\n "evaluate-commands -client ${kak_client} %{info -anchor $kak_cursor_line.$kak_cursor_column -style above '$sigs'}"
+ fi
+ }
+ }
+ }
+}
+
+define-command ctags-enable-autoinfo -docstring "Automatically display ctags information about function" %{
+ hook window -group ctags-autoinfo NormalIdle .* ctags-funcinfo
+ hook window -group ctags-autoinfo InsertIdle .* ctags-funcinfo
+}
+
+define-command ctags-disable-autoinfo -docstring "Disable automatic ctags information displaying" %{ remove-hooks window ctags-autoinfo }
+
+declare-option -docstring "shell command to run" \
+ str ctagscmd "ctags -R --fields=+S"
+declare-option -docstring "path to the directory in which the tags file will be generated" str ctagspaths "."
+
+define-command ctags-generate -docstring 'Generate tag file asynchronously' %{
+ echo -markup "{Information}launching tag generation in the background"
+ nop %sh{ (
+ trap - INT QUIT
+ while ! mkdir .tags.kaklock 2>/dev/null; do sleep 1; done
+ trap 'rmdir .tags.kaklock' EXIT
+
+ if ${kak_opt_ctagscmd} -f .tags.kaktmp ${kak_opt_ctagspaths}; then
+ mv .tags.kaktmp tags
+ msg="tags generation complete"
+ else
+ msg="tags generation failed"
+ fi
+
+ printf %s\\n "evaluate-commands -client $kak_client echo -markup '{Information}${msg}'" | kak -p ${kak_session}
+ ) > /dev/null 2>&1 < /dev/null & }
+}
+
+define-command ctags-update-tags -docstring 'Update tags for the given file' %{
+ nop %sh{ (
+ trap - INT QUIT
+ while ! mkdir .tags.kaklock 2>/dev/null; do sleep 1; done
+ trap 'rmdir .tags.kaklock' EXIT
+
+ if ${kak_opt_ctagscmd} -f .file_tags.kaktmp $kak_bufname; then
+ export LC_COLLATE=C LC_ALL=C # ensure ASCII sorting order
+ # merge the updated tags tags with the general tags (filtering out out of date tags from it) into the target file
+ grep -Fv "$(printf '\t%s\t' "$kak_bufname")" tags | grep -v '^!' | sort --merge - .file_tags.kaktmp >> .tags.kaktmp
+ rm .file_tags.kaktmp
+ mv .tags.kaktmp tags
+ msg="tags updated for $kak_bufname"
+ else
+ msg="tags update failed for $kak_bufname"
+ fi
+
+ printf %s\\n "evaluate-commands -client $kak_client echo -markup '{Information}${msg}'" | kak -p ${kak_session}
+ ) > /dev/null 2>&1 < /dev/null & }
+}
+
+define-command ctags-enable-autocomplete -docstring "Enable automatic ctags completion" %{
+ set-option window completers "option=ctags_completions" %opt{completers}
+ hook window -group ctags-autocomplete InsertIdle .* %{
+ try %{
+ evaluate-commands -draft %{ # select previous word >= ctags_min_chars
+ execute-keys ",b_<a-k>.{%opt{ctags_min_chars},}<ret>"
+ ctags-complete # run in draft context to preserve selection
+ }
+ }
+ }
+}
+
+define-command ctags-disable-autocomplete -docstring "Disable automatic ctags completion" %{
+ evaluate-commands %sh{
+ printf "set-option window completers %s\n" $(printf %s "${kak_opt_completers}" | sed -e "s/'option=ctags_completions'//g")
+ }
+ remove-hooks window ctags-autocomplete
+}
diff --git a/autoload/tools/doc.asciidoc b/autoload/tools/doc.asciidoc
new file mode 100644
index 0000000..bb2e262
--- /dev/null
+++ b/autoload/tools/doc.asciidoc
@@ -0,0 +1,45 @@
+= Kakoune's online documentation
+
+This is Kakoune's online documentation system.
+
+To see what documentation topics are available, type `:doc` and look at the
+completion menu. To view a particular topic, type its name or select it
+from the completion menu. Then hit Enter.
+
+Documentation will be displayed in the client named in the `docsclient` option.
+
+== Using the documentation browser
+
+Documentation buffers are like any other buffer, so you can scroll through
+them as normal, search within a topic with `/`, etc. However, they can also
+contain links: <<doc#demonstration-target,like this>>. Links can be followed
+by moving the cursor onto them and pressing Enter. If a link takes you to
+a different documentation topic, you can return to the original by using the
+`:buffer` command.
+
+== Writing documentation
+
+Documentation must be in AsciiDoc format, with the extension `.asciidoc`.
+It must be stored somewhere within <<doc#sources,the documentation search
+path>>. Kakoune's built-in documentation renderer does not necessarily
+support every feature, so don't go overboard with formatting.
+
+To create a link to another documentation topic, the URL should be the topic's
+name, just like `:doc` uses. Because topics are identified only by their
+basename, you should take care that your topic's name does not conflict with
+any of the names used either by other plugins or by Kakoune's standard library.
+
+== Sources
+
+The `:doc` command searches within the following locations for
+documents in the AsciiDoc format (`*.asciidoc`):
+
+* The user plugin directory, `"%val{config}/autoload"`
+* The system documentation directory, `"%val{runtime}/doc"`
+* The system plugin directory, `"%val{runtime}/rc"`
+
+It searches recursively, and follows symlinks.
+
+== Demonstration target
+
+Well done! You can <<doc#using-the-documentation-browser,go back now>>!
diff --git a/autoload/tools/doc.kak b/autoload/tools/doc.kak
new file mode 100644
index 0000000..4b6afe3
--- /dev/null
+++ b/autoload/tools/doc.kak
@@ -0,0 +1,195 @@
+declare-option -docstring "name of the client in which documentation is to be displayed" \
+ str docsclient
+
+declare-option -hidden range-specs doc_render_ranges
+declare-option -hidden range-specs doc_links
+declare-option -hidden range-specs doc_anchors
+
+define-command -hidden -params 4 doc-render-regex %{
+ evaluate-commands -draft %{ try %{
+ execute-keys <percent> s %arg{1} <ret>
+ execute-keys -draft s %arg{2} <ret> d
+ execute-keys "%arg{3}"
+ evaluate-commands %sh{
+ face="$4"
+ eval "set -- $kak_quoted_selections_desc"
+ ranges=""
+ for desc in "$@"; do ranges="$ranges '$desc|$face'"; done
+ echo "update-option buffer doc_render_ranges"
+ echo "set-option -add buffer doc_render_ranges $ranges"
+ }
+ } }
+}
+
+define-command -hidden doc-parse-links %{
+ evaluate-commands -draft %{ try %{
+ execute-keys <percent> s <lt><lt>(.*?),.*?<gt><gt> <ret>
+ execute-keys -draft s <lt><lt>.*,|<gt><gt> <ret> d
+ execute-keys H
+ set-option buffer doc_links %val{timestamp}
+ update-option buffer doc_render_ranges
+ evaluate-commands -itersel %{
+ set-option -add buffer doc_links "%val{selection_desc}|%reg{1}"
+ set-option -add buffer doc_render_ranges "%val{selection_desc}|default+u"
+ }
+ } }
+}
+
+define-command -hidden doc-parse-anchors %{
+ evaluate-commands -draft %{ try %{
+ set-option buffer doc_anchors %val{timestamp}
+ # Find sections as add them as imlicit anchors
+ execute-keys <percent> s ^={2,}\h+([^\n]+)$ <ret>
+ evaluate-commands -itersel %{
+ set-option -add buffer doc_anchors "%val{selection_desc}|%sh{printf '%s' ""$kak_main_reg_1"" | tr '[A-Z ]' '[a-z-]'}"
+ }
+
+ # Parse explicit anchors and remove their text
+ execute-keys <percent> s \[\[(.*?)\]\]\s* <ret>
+ evaluate-commands -itersel %{
+ set-option -add buffer doc_anchors "%val{selection_desc}|%reg{1}"
+ }
+ execute-keys d
+ update-option buffer doc_anchors
+ } }
+}
+
+define-command -hidden doc-jump-to-anchor -params 1 %{
+ update-option buffer doc_anchors
+ evaluate-commands %sh{
+ anchor="$1"
+ eval "set -- $kak_quoted_opt_doc_anchors"
+
+ shift
+ for range in "$@"; do
+ if [ "${range#*|}" = "$anchor" ]; then
+ printf '%s\n' "select '${range%|*}'; execute-keys vv"
+ exit
+ fi
+ done
+ printf "fail No such anchor '%s'\n" "${anchor}"
+ }
+}
+
+define-command -hidden doc-follow-link %{
+ update-option buffer doc_links
+ evaluate-commands %sh{
+ eval "set -- $kak_quoted_opt_doc_links"
+ for link in "$@"; do
+ printf '%s\n' "$link"
+ done | awk -v FS='[.,|#]' '
+ BEGIN {
+ l=ENVIRON["kak_cursor_line"];
+ c=ENVIRON["kak_cursor_column"];
+ }
+ l >= $1 && c >= $2 && l <= $3 && c <= $4 {
+ if (NF == 6) {
+ print "doc " $5
+ if ($6 != "") {
+ print "doc-jump-to-anchor %{" $6 "}"
+ }
+ } else {
+ print "doc-jump-to-anchor %{" $5 "}"
+ }
+ exit
+ }
+ '
+ }
+}
+
+define-command -params 1 -hidden doc-render %{
+ edit! -scratch "*doc-%sh{basename $1 .asciidoc}*"
+ execute-keys "!cat '%arg{1}'<ret>gg"
+
+ doc-parse-anchors
+
+ # Join paragraphs together
+ try %{
+ execute-keys -draft '%S\n{2,}|(?<lt>=\+)\n|^[^\n]+::\n|^\h*[*-]\h+<ret>' \
+ <a-K>^\h*-{2,}(\n|\z)<ret> S\n\z<ret> <a-k>\n<ret> <a-j>
+ }
+
+ # Remove some line end markers
+ try %{ execute-keys -draft <percent> s \h*(\+|:{2,})$ <ret> d }
+
+ # Setup the doc_render_ranges option
+ set-option buffer doc_render_ranges %val{timestamp}
+ doc-render-regex \B(?<!\\)\*(?=\S)[^\n]+?(?<=\S)(?<!\\)\*\B \A|.\z 'H' default+b
+ doc-render-regex \b(?<!\\)_(?=\S)[^\n]+?(?<=\S)(?<!\\)_\b \A|.\z 'H' default+i
+ doc-render-regex \B(?<!\\)`(?=\S)[^\n]+?(?<=\S)`\B \A|.\z 'H' mono
+ doc-render-regex ^=\h+[^\n]+ ^=\h+ '~' title
+ doc-render-regex ^={2,}\h+[^\n]+ ^={2,}\h+ '' header
+ doc-render-regex ^\h*-{2,}\n\h*.*?^\h*-{2,}\n ^\h*-{2,}\n '' block
+
+ doc-parse-links
+
+ # Remove escaping of * and `
+ try %{ execute-keys -draft <percent> s \\((?=\*)|(?=`)) <ret> d }
+ # Go to beginning of file
+ execute-keys 'gg'
+
+ set-option buffer readonly true
+ add-highlighter buffer/ ranges doc_render_ranges
+ add-highlighter buffer/ wrap -word -indent
+ map buffer normal <ret> :doc-follow-link<ret>
+}
+
+define-command doc -params 0..2 -docstring %{
+ doc <topic> [<keyword>]: open a buffer containing documentation about a given topic
+ An optional keyword argument can be passed to the function, which will be automatically selected in the documentation
+
+ See `:doc doc` for details.
+ } %{
+ evaluate-commands %sh{
+ topic="doc"
+ if [ $# -ge 1 ]; then
+ topic="$1"
+ fi
+ page=$(
+ find -L \
+ "${kak_config}/autoload/" \
+ "${kak_runtime}/doc/" \
+ "${kak_runtime}/rc/" \
+ -type f -name "$topic.asciidoc" 2>/dev/null |
+ head -1
+ )
+ if [ -f "${page}" ]; then
+ jump_cmd=""
+ if [ $# -eq 2 ]; then
+ jump_cmd="doc-jump-to-anchor '$2'"
+ fi
+ printf %s\\n "evaluate-commands -try-client %opt{docsclient} %{ doc-render ${page}; ${jump_cmd} }"
+ else
+ printf 'fail No such doc file: %s\n' "$topic.asciidoc"
+ fi
+ }
+}
+
+complete-command -menu doc shell-script-candidates %{
+ case "$kak_token_to_complete" in
+ 0)
+ find -L \
+ "${kak_config}/autoload/" \
+ "${kak_runtime}/doc/" \
+ "${kak_runtime}/rc/" \
+ -type f -name "*.asciidoc" 2>/dev/null |
+ sed 's,.*/,,; s/\.[^.]*$//';;
+ 1)
+ page=$(
+ find -L \
+ "${kak_config}/autoload/" \
+ "${kak_runtime}/doc/" \
+ "${kak_runtime}/rc/" \
+ -type f -name "$1.asciidoc" 2>/dev/null |
+ head -1
+ )
+ if [ -f "${page}" ]; then
+ awk '
+ /^==+ +/ { sub(/^==+ +/, ""); print }
+ /^\[\[[^\]]+\]\]/ { sub(/^\[\[/, ""); sub(/\]\].*/, ""); print }
+ ' < $page | tr '[A-Z ]' '[a-z-]'
+ fi;;
+ esac | sort
+}
+
+alias global help doc
diff --git a/autoload/tools/format.kak b/autoload/tools/format.kak
new file mode 100644
index 0000000..2435af0
--- /dev/null
+++ b/autoload/tools/format.kak
@@ -0,0 +1,38 @@
+declare-option -docstring "shell command used for the 'format-selections' and 'format-buffer' commands" \
+ str formatcmd
+
+define-command format-buffer -docstring "Format the contents of the buffer" %{
+ evaluate-commands -draft %{
+ execute-keys '%'
+ format-selections
+ }
+}
+
+define-command format-selections -docstring "Format the selections individually" %{
+ evaluate-commands %sh{
+ if [ -z "${kak_opt_formatcmd}" ]; then
+ echo "fail 'The option ''formatcmd'' must be set'"
+ fi
+ }
+ evaluate-commands -draft -no-hooks -save-regs 'e|' %{
+ set-register e nop
+ set-register '|' %{
+ format_in="$(mktemp "${TMPDIR:-/tmp}"/kak-formatter.XXXXXX)"
+ format_out="$(mktemp "${TMPDIR:-/tmp}"/kak-formatter.XXXXXX)"
+
+ cat > "$format_in"
+ eval "$kak_opt_formatcmd" < "$format_in" > "$format_out"
+ if [ $? -eq 0 ]; then
+ cat "$format_out"
+ else
+ echo "set-register e fail formatter returned an error (exit code $?)" >"$kak_command_fifo"
+ cat "$format_in"
+ fi
+ rm -f "$format_in" "$format_out"
+ }
+ execute-keys '|<ret>'
+ %reg{e}
+ }
+}
+
+alias global format format-buffer
diff --git a/autoload/tools/git.kak b/autoload/tools/git.kak
new file mode 100644
index 0000000..def591d
--- /dev/null
+++ b/autoload/tools/git.kak
@@ -0,0 +1,787 @@
+declare-option -docstring "name of the client in which documentation is to be displayed" \
+ str docsclient
+
+declare-option -docstring "git diff added character" \
+ str git_diff_add_char "▏"
+
+declare-option -docstring "git diff modified character" \
+ str git_diff_mod_char "▏"
+
+declare-option -docstring "git diff deleted character" \
+ str git_diff_del_char "_"
+
+declare-option -docstring "git diff top deleted character" \
+ str git_diff_top_char "‾"
+
+hook -group git-log-highlight global WinSetOption filetype=git-log %{
+ add-highlighter window/git-log group
+ add-highlighter window/git-log/ regex '^([*|\\ /_.-])*' 0:keyword
+ add-highlighter window/git-log/ regex '^( ?[*|\\ /_.-])*\h{,3}(commit )?(\b[0-9a-f]{4,40}\b)' 2:keyword 3:comment
+ add-highlighter window/git-log/ regex '^( ?[*|\\ /_.-])*\h{,3}([a-zA-Z_-]+:) (.*?)$' 2:variable 3:value
+ hook -once -always window WinSetOption filetype=.* %{ remove-highlighter window/git-log }
+}
+
+hook global WinSetOption filetype=diff %{
+ try %{
+ execute-keys -draft %{/^diff --git\b<ret>}
+ evaluate-commands %sh{
+ if [ -n "$(git ls-files -- "${kak_buffile}")" ]; then
+ echo fail
+ fi
+ }
+ set-option buffer filetype git-diff
+ }
+}
+
+hook -group git-diff-highlight global WinSetOption filetype=(git-diff|git-log) %{
+ require-module diff
+ add-highlighter %exp{window/%val{hook_param_capture_1}-ref-diff} ref diff
+ hook -once -always window WinSetOption filetype=.* %exp{
+ remove-highlighter window/%val{hook_param_capture_1}-ref-diff
+ }
+}
+
+hook global WinSetOption filetype=(?:git-diff|git-log) %{
+ map buffer normal <ret> %exp{:git-diff-goto-source # %val{hook_param}<ret>} -docstring 'Jump to source from git diff'
+ hook -once -always window WinSetOption filetype=.* %exp{
+ unmap buffer normal <ret> %%{:git-diff-goto-source # %val{hook_param}<ret>}
+ }
+}
+
+hook -group git-status-highlight global WinSetOption filetype=git-status %{
+ add-highlighter window/git-status group
+ add-highlighter window/git-status/ regex '^## ' 0:comment
+ add-highlighter window/git-status/ regex '^## (\S*[^\s\.@])' 1:green
+ add-highlighter window/git-status/ regex '^## (\S*[^\s\.@])(\.\.+)(\S*[^\s\.@])' 1:green 2:comment 3:red
+ add-highlighter window/git-status/ regex '^(##) (No commits yet on) (\S*[^\s\.@])' 1:comment 2:Default 3:green
+ add-highlighter window/git-status/ regex '^## \S+ \[[^\n]*ahead (\d+)[^\n]*\]' 1:green
+ add-highlighter window/git-status/ regex '^## \S+ \[[^\n]*behind (\d+)[^\n]*\]' 1:red
+ add-highlighter window/git-status/ regex '^(?:([Aa])|([Cc])|([Dd!?])|([MUmu])|([Rr])|([Tt]))[ !\?ACDMRTUacdmrtu]\h' 1:green 2:blue 3:red 4:yellow 5:cyan 6:cyan
+ add-highlighter window/git-status/ regex '^[ !\?ACDMRTUacdmrtu](?:([Aa])|([Cc])|([Dd!?])|([MUmu])|([Rr])|([Tt]))\h' 1:green 2:blue 3:red 4:yellow 5:cyan 6:cyan
+ add-highlighter window/git-status/ regex '^R[ !\?ACDMRTUacdmrtu] [^\n]+( -> )' 1:cyan
+ add-highlighter window/git-status/ regex '^\h+(?:((?:both )?modified:)|(added:|new file:)|(deleted(?: by \w+)?:)|(renamed:)|(copied:))(?:.*?)$' 1:yellow 2:green 3:red 4:cyan 5:blue 6:magenta
+
+ hook -once -always window WinSetOption filetype=.* %{ remove-highlighter window/git-status }
+}
+
+hook -group git-show-branch-highlight global WinSetOption filetype=git-show-branch %{
+ add-highlighter window/git-show-branch group
+ add-highlighter window/git-show-branch/ regex '(\*)|(\+)|(!)' 1:red 2:green 3:green
+ add-highlighter window/git-show-branch/ regex '(!\D+\{0\}\])|(!\D+\{1\}\])|(!\D+\{2\}\])|(!\D+\{3\}\])' 1:red 2:green 3:yellow 4:blue
+ add-highlighter window/git-show-branch/ regex '(\B\+\D+\{0\}\])|(\B\+\D+\{1\}\])|(\B\+\D+\{2\}\])|(\B\+\D+\{3\}\])|(\B\+\D+\{1\}\^\])' 1:red 2:green 3:yellow 4:blue 5:magenta
+
+ hook -once -always window WinSetOption filetype=.* %{ remove-highlighter window/git-show-branch}
+}
+
+declare-option -hidden line-specs git_blame_flags
+declare-option -hidden line-specs git_blame_index
+declare-option -hidden str git_blame
+declare-option -hidden str git_blob
+declare-option -hidden line-specs git_diff_flags
+declare-option -hidden int-list git_hunk_list
+
+define-command -params 1.. \
+ -docstring %{
+ git [<arguments>]: git wrapping helper
+ All the optional arguments are forwarded to the git utility
+ Available commands:
+ add
+ apply - alias for "patch git apply"
+ blame - toggle blame annotations
+ blame-jump - show the commit that added the line at cursor
+ checkout
+ commit
+ diff
+ edit
+ grep
+ hide-diff
+ init
+ log
+ next-hunk
+ prev-hunk
+ reset
+ rm
+ show
+ show-branch
+ show-diff
+ status
+ update-diff
+ } -shell-script-candidates %{
+ if [ $kak_token_to_complete -eq 0 ]; then
+ printf %s\\n \
+ apply \
+ blame \
+ blame-jump \
+ checkout \
+ commit \
+ diff \
+ edit \
+ grep \
+ hide-diff \
+ init \
+ log \
+ next-hunk \
+ prev-hunk \
+ reset \
+ rm \
+ show \
+ show-branch \
+ show-diff \
+ status \
+ update-diff \
+ ;
+ else
+ case "$1" in
+ commit) printf -- "--amend\n--no-edit\n--all\n--reset-author\n--fixup\n--squash\n"; git ls-files -m ;;
+ add) git ls-files -dmo --exclude-standard ;;
+ apply) printf -- "--reverse\n--cached\n--index\n--3way\n" ;;
+ grep|edit) git ls-files -c --recurse-submodules ;;
+ esac
+ fi
+ } \
+ git %{ evaluate-commands %sh{
+ cd_bufdir() {
+ dirname_buffer="${kak_buffile%/*}"
+ cd "${dirname_buffer}" 2>/dev/null || {
+ printf 'fail Unable to change the current working directory to: %s\n' "${dirname_buffer}"
+ exit 1
+ }
+ }
+ kakquote() {
+ printf "%s" "$1" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"
+ }
+
+ show_git_cmd_output() {
+ local filetype
+
+ case "$1" in
+ diff) filetype=git-diff ;;
+ show) filetype=git-log ;;
+ show-branch) filetype=git-show-branch ;;
+ log) filetype=git-log ;;
+ status) filetype=git-status ;;
+ *) return 1 ;;
+ esac
+ output=$(mktemp -d "${TMPDIR:-/tmp}"/kak-git.XXXXXXXX)/fifo
+ mkfifo ${output}
+ ( trap - INT QUIT; git "$@" > ${output} 2>&1 & ) > /dev/null 2>&1 < /dev/null
+
+ printf %s "evaluate-commands -try-client '$kak_opt_docsclient' '
+ edit! -fifo ${output} *git*
+ set-option buffer filetype ${filetype}
+ $(hide_blame)
+ set-option buffer git_blob %{}
+ hook -always -once buffer BufCloseFifo .* ''
+ nop %sh{ rm -r $(dirname ${output}) }
+ $(printf %s "${on_close_fifo}" | sed "s/'/''''/g")
+ ''
+ '"
+ }
+
+ hide_blame() {
+ printf %s "
+ set-option buffer git_blame_flags $kak_timestamp
+ set-option buffer git_blame_index $kak_timestamp
+ set-option buffer git_blame %{}
+ try %{ remove-highlighter window/git-blame }
+ unmap window normal <ret> %{:git blame-jump<ret>}
+ "
+ }
+
+ prepare_git_blame_args='
+ if [ -n "${kak_opt_git_blob}" ]; then {
+ contents_fifo=/dev/null
+ set -- "$@" "${kak_opt_git_blob%%:*}" -- "${kak_opt_git_blob#*:}"
+ } else {
+ contents_fifo=$(mktemp -d "${TMPDIR:-/tmp}"/kak-git.XXXXXXXX)/fifo
+ mkfifo ${contents_fifo}
+ echo >${kak_command_fifo} "evaluate-commands -save-regs | %{
+ set-register | %{
+ contents=\$(cat; printf .)
+ ( printf %s \"\${contents%.}\" >${contents_fifo} ) >/dev/null 2>&1 &
+ }
+ execute-keys -client ${kak_client} -draft %{%<a-|><ret>}
+ }"
+ set -- "$@" --contents - -- "${kak_buffile}"
+ } fi
+ '
+
+ blame_toggle() {
+ echo >${kak_command_fifo} "try %{
+ add-highlighter window/git-blame flag-lines Information git_blame_flags
+ echo -to-file ${kak_response_fifo}
+ } catch %{
+ echo -to-file ${kak_response_fifo} 'hide_blame; exit'
+ }"
+ eval $(cat ${kak_response_fifo})
+ if [ -z "${kak_opt_git_blob}" ] && {
+ [ "${kak_opt_filetype}" = git-diff ] || [ "${kak_opt_filetype}" = git-log ]
+ } then {
+ echo 'try %{ remove-highlighter window/git-blame }'
+ printf >${kak_command_fifo} %s '
+ evaluate-commands -client '${kak_client}' -draft %{
+ try %{
+ execute-keys <a-l><semicolon><a-?>^commit<ret><a-semicolon>
+ } catch %{
+ # Missing commit line, assume it is an uncommitted change.
+ execute-keys <a-l><semicolon>Gg<a-semicolon>
+ }
+ require-module diff
+ try %{
+ diff-parse END %{
+ my $line = $file_line;
+ if (not defined $commit) {
+ $commit = "HEAD";
+ $line = $other_file_line;
+ if ($diff_line_text =~ m{^\+}) {
+ print "echo -to-file '${kak_response_fifo}' -quoting shell "
+ . "%{git blame: blame from HEAD does not work on added lines}";
+ exit;
+ }
+ } elsif ($diff_line_text =~ m{^[-]}) {
+ $commit = "$commit~";
+ $line = $other_file_line;
+ }
+ $line = $line or 1;
+ printf "echo -to-file '${kak_response_fifo}' -quoting shell %s %s %d %d",
+ $commit, quote($file), $line, ('${kak_cursor_column}' - 1);
+ }
+ } catch %{
+ echo -to-file '${kak_response_fifo}' -quoting shell -- %val{error}
+ }
+ }
+ '
+ n=$#
+ eval set -- "$(cat ${kak_response_fifo})" "$@"
+ if [ $# -eq $((n+1)) ]; then
+ echo fail -- "$(kakquote "$1")"
+ exit
+ fi
+ commit=$1
+ file=${2#"$PWD/"}
+ cursor_line=$3
+ cursor_column=$4
+ shift 4
+ # Log commit and file name because they are only echoed briefly
+ # and not shown elsewhere (we don't have a :messages buffer).
+ message="Blaming $file as of $(git rev-parse --short $commit)"
+ echo "echo -debug -- $(kakquote "$message")"
+ on_close_fifo="
+ execute-keys -client ${kak_client} ${cursor_line}g<a-h>${cursor_column}lh
+ evaluate-commands -client ${kak_client} %{
+ set-option buffer git_blob $(kakquote "$commit:$file")
+ git blame $(for arg; do kakquote "$arg"; printf " "; done)
+ hook -once window NormalIdle .* %{
+ execute-keys vv
+ echo -markup -- $(kakquote "{Information}{\\}$message. Press <ret> to jump to blamed commit")
+ }
+ }
+ " show_git_cmd_output show "$commit:$file"
+ exit
+ } fi
+ eval "$prepare_git_blame_args"
+ echo 'map window normal <ret> %{:git blame-jump<ret>}'
+ echo 'echo -markup {Information}Press <ret> to jump to blamed commit'
+ (
+ trap - INT QUIT
+ cd_bufdir
+ printf %s "evaluate-commands -client '$kak_client' %{
+ set-option buffer=$kak_bufname git_blame_flags '$kak_timestamp'
+ set-option buffer=$kak_bufname git_blame_index '$kak_timestamp'
+ set-option buffer=$kak_bufname git_blame ''
+ }" | kak -p ${kak_session}
+ if ! stderr=$({ git blame --incremental "$@" <${contents_fifo} | perl -wne '
+ use POSIX qw(strftime);
+ sub quote {
+ my $SQ = "'\''";
+ my $token = shift;
+ $token =~ s/$SQ/$SQ$SQ/g;
+ return "$SQ$token$SQ";
+ }
+ sub send_flags {
+ my $is_last_call = shift;
+ if (not defined $line) {
+ if ($is_last_call) { exit 1; }
+ return;
+ }
+ my $text = substr($sha,0,7) . " " . $dates{$sha} . " " . $authors{$sha};
+ $text =~ s/~/~~/g;
+ for ( my $i = 0; $i < $count; $i++ ) {
+ $flags .= " %~" . ($line+$i) . "|$text~";
+ }
+ $now = time();
+ # Send roughly one update per second, to avoid creating too many kak processes.
+ if (!$is_last_call && defined $last_sent && $now - $last_sent < 1) {
+ return
+ }
+ open CMD, "|-", "kak -p $ENV{kak_session}";
+ print CMD "set-option -add buffer=$ENV{kak_bufname} git_blame_flags $flags;";
+ print CMD "set-option -add buffer=$ENV{kak_bufname} git_blame_index $index;";
+ print CMD "set-option -add buffer=$ENV{kak_bufname} git_blame " . quote $raw_blame;
+ close(CMD);
+ $flags = "";
+ $index = "";
+ $raw_blame = "";
+ $last_sent = $now;
+ }
+ $raw_blame .= $_;
+ chomp;
+ if (m/^([0-9a-f]+) ([0-9]+) ([0-9]+) ([0-9]+)/) {
+ send_flags(0);
+ $sha = $1;
+ $line = $3;
+ $count = $4;
+ for ( my $i = 0; $i < $count; $i++ ) {
+ $index .= " " . ($line+$i) . "|$.,$i";
+ }
+ }
+ if (m/^author /) {
+ $authors{$sha} = substr($_,7);
+ $authors{$sha} = "Not Committed Yet" if $authors{$sha} eq "External file (--contents)";
+ }
+ if (m/^author-time ([0-9]*)/) { $dates{$sha} = strftime("%F %T", localtime $1) }
+ END { send_flags(1); }'
+ } 2>&1); then
+ escape2() { printf %s "$*" | sed "s/'/''''/g"; }
+ echo "evaluate-commands -client ${kak_client} '
+ evaluate-commands -draft %{
+ buffer %{${kak_buffile}}
+ git hide-blame
+ }
+ echo -debug failed to run git blame
+ echo -debug git stderr: <<<
+ echo -debug ''$(escape2 "$stderr")>>>''
+ hook -once buffer NormalIdle .* %{
+ echo -markup %{{Error}failed to run git blame, see *debug* buffer}
+ }
+ '" | kak -p ${kak_session}
+ fi
+ if [ "$contents_fifo" != /dev/null ]; then
+ rm -r $(dirname $contents_fifo)
+ fi
+ ) > /dev/null 2>&1 < /dev/null &
+ }
+
+ run_git_cmd() {
+ if git "${@}" > /dev/null 2>&1; then
+ printf %s "echo -markup '{Information}git $1 succeeded'"
+ else
+ printf 'fail git %s failed\n' "$1"
+ fi
+ }
+
+ update_diff() {
+ (
+ cd_bufdir
+ git --no-pager diff --no-ext-diff -U0 "$kak_buffile" | perl -e '
+ use utf8;
+ $flags = $ENV{"kak_timestamp"};
+ $add_char = $ENV{"kak_opt_git_diff_add_char"};
+ $del_char = $ENV{"kak_opt_git_diff_del_char"};
+ $top_char = $ENV{"kak_opt_git_diff_top_char"};
+ $mod_char = $ENV{"kak_opt_git_diff_mod_char"};
+ foreach $line (<STDIN>) {
+ if ($line =~ /@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?/) {
+ $from_line = $1;
+ $from_count = ($2 eq "" ? 1 : $2);
+ $to_line = $3;
+ $to_count = ($4 eq "" ? 1 : $4);
+
+ if ($from_count == 0 and $to_count > 0) {
+ for $i (0..$to_count - 1) {
+ $line = $to_line + $i;
+ $flags .= " $line|\{green\}$add_char";
+ }
+ }
+ elsif ($from_count > 0 and $to_count == 0) {
+ if ($to_line == 0) {
+ $flags .= " 1|\{red\}$top_char";
+ } else {
+ $flags .= " $to_line|\{red\}$del_char";
+ }
+ }
+ elsif ($from_count > 0 and $from_count == $to_count) {
+ for $i (0..$to_count - 1) {
+ $line = $to_line + $i;
+ $flags .= " $line|\{blue\}$mod_char";
+ }
+ }
+ elsif ($from_count > 0 and $from_count < $to_count) {
+ for $i (0..$from_count - 1) {
+ $line = $to_line + $i;
+ $flags .= " $line|\{blue\}$mod_char";
+ }
+ for $i ($from_count..$to_count - 1) {
+ $line = $to_line + $i;
+ $flags .= " $line|\{green\}$add_char";
+ }
+ }
+ elsif ($to_count > 0 and $from_count > $to_count) {
+ for $i (0..$to_count - 2) {
+ $line = $to_line + $i;
+ $flags .= " $line|\{blue\}$mod_char";
+ }
+ $last = $to_line + $to_count - 1;
+ $flags .= " $last|\{blue+u\}$mod_char";
+ }
+ }
+ }
+ print "set-option buffer git_diff_flags $flags"
+ ' )
+ }
+
+ jump_hunk() {
+ direction=$1
+ set -- ${kak_opt_git_diff_flags}
+ shift
+
+ if [ $# -lt 1 ]; then
+ echo "fail 'no git hunks found, try \":git show-diff\" first'"
+ exit
+ fi
+
+ # Update hunk list if required
+ if [ "$kak_timestamp" != "${kak_opt_git_hunk_list%% *}" ]; then
+ hunks=$kak_timestamp
+
+ prev_line="-1"
+ for line in "$@"; do
+ line="${line%%|*}"
+ if [ "$((line - prev_line))" -gt 1 ]; then
+ hunks="$hunks $line"
+ fi
+ prev_line="$line"
+ done
+ echo "set-option buffer git_hunk_list $hunks"
+ hunks=${hunks#* }
+ else
+ hunks=${kak_opt_git_hunk_list#* }
+ fi
+
+ prev_hunk=""
+ next_hunk=""
+ for hunk in ${hunks}; do
+ if [ "$hunk" -lt "$kak_cursor_line" ]; then
+ prev_hunk=$hunk
+ elif [ "$hunk" -gt "$kak_cursor_line" ]; then
+ next_hunk=$hunk
+ break
+ fi
+ done
+
+ wrapped=false
+ if [ "$direction" = "next" ]; then
+ if [ -z "$next_hunk" ]; then
+ next_hunk=${hunks%% *}
+ wrapped=true
+ fi
+ if [ -n "$next_hunk" ]; then
+ echo "select $next_hunk.1,$next_hunk.1"
+ fi
+ elif [ "$direction" = "prev" ]; then
+ if [ -z "$prev_hunk" ]; then
+ wrapped=true
+ prev_hunk=${hunks##* }
+ fi
+ if [ -n "$prev_hunk" ]; then
+ echo "select $prev_hunk.1,$prev_hunk.1"
+ fi
+ fi
+
+ if [ "$wrapped" = true ]; then
+ echo "echo -markup '{Information}git hunk search wrapped around buffer'"
+ fi
+ }
+
+ commit() {
+ # Handle case where message needs not to be edited
+ if grep -E -q -e "-m|-F|-C|--message=.*|--file=.*|--reuse-message=.*|--no-edit|--fixup.*|--squash.*"; then
+ if git commit "$@" > /dev/null 2>&1; then
+ echo 'echo -markup "{Information}Commit succeeded"'
+ else
+ echo 'fail Commit failed'
+ fi
+ exit
+ fi <<-EOF
+ $@
+ EOF
+
+ # fails, and generate COMMIT_EDITMSG
+ GIT_EDITOR='' EDITOR='' git commit "$@" > /dev/null 2>&1
+ msgfile="$(git rev-parse --git-dir)/COMMIT_EDITMSG"
+ printf %s "edit '$msgfile'
+ hook buffer BufWritePost '.*\Q$msgfile\E' %{ evaluate-commands %sh{
+ if git commit -F '$msgfile' --cleanup=strip $* > /dev/null; then
+ printf %s 'evaluate-commands -client $kak_client echo -markup %{{Information}Commit succeeded}; delete-buffer'
+ else
+ printf 'evaluate-commands -client %s fail Commit failed\n' "$kak_client"
+ fi
+ } }"
+ }
+
+ blame_jump() {
+ echo >${kak_command_fifo} "echo -to-file ${kak_response_fifo} -- %opt{git_blame}"
+ blame_info=$(cat < ${kak_response_fifo})
+ blame_index=
+ cursor_column=${kak_cursor_column}
+ cursor_line=${kak_cursor_line}
+ if [ -n "$blame_info" ]; then {
+ echo >${kak_command_fifo} "
+ update-option buffer git_blame_index
+ echo -to-file ${kak_response_fifo} -- %opt{git_blame_index}
+ "
+ blame_index=$(cat < ${kak_response_fifo})
+ } elif [ "${kak_opt_filetype}" = git-diff ] || [ "${kak_opt_filetype}" = git-log ]; then {
+ printf >${kak_command_fifo} %s '
+ evaluate-commands -draft %{
+ try %{
+ execute-keys <a-l><semicolon><a-?>^commit<ret><a-semicolon>
+ } catch %{
+ # Missing commit line, assume it is an uncommitted change.
+ execute-keys <a-l><semicolon><a-?>\A<ret><a-semicolon>
+ }
+ require-module diff
+ try %{
+ diff-parse BEGIN %{
+ $version = "-";
+ } END %{
+ if ($diff_line_text !~ m{^[ -]}) {
+ print "set-register e fail git blame-jump: recursive blame only works on context or deleted lines";
+ } else {
+ if (not defined $commit) {
+ $commit = "HEAD";
+ } else {
+ $commit = "$commit~" if $diff_line_text =~ m{^[- ]};
+ }
+ printf "echo -to-file '${kak_response_fifo}' -quoting shell %s %s %d %d",
+ $commit, quote($file), $file_line, ('$cursor_column' - 1);
+ }
+ }
+ } catch %{
+ echo -to-file '${kak_response_fifo}' -quoting shell -- %val{error}
+ }
+ }
+ '
+ eval set -- "$(cat ${kak_response_fifo})"
+ if [ $# -eq 1 ]; then
+ echo fail -- "$(kakquote "$1")"
+ exit
+ fi
+ starting_commit=$1
+ file=$2
+ cursor_line=$3
+ cursor_column=$4
+ blame_info=$(git blame --porcelain "$starting_commit" -L"$cursor_line,$cursor_line" -- "$file")
+ if [ $? -ne 0 ]; then
+ echo 'echo -markup %{{Error}failed to run git blame, see *debug* buffer}'
+ exit
+ fi
+ } else {
+ set --
+ eval "$prepare_git_blame_args"
+ blame_info=$(git blame --porcelain -L"$cursor_line,$cursor_line" "$@" <${contents_fifo})
+ status=$?
+ if [ "$contents_fifo" != /dev/null ]; then
+ rm -r $(dirname $contents_fifo)
+ fi
+ if [ $status -ne 0 ]; then
+ echo 'echo -markup %{{Error}failed to run git blame, see *debug* buffer}'
+ exit
+ fi
+ } fi
+ eval "$(printf '%s\n---\n%s' "$blame_index" "$blame_info" |
+ client=${kak_opt_docsclient:-$kak_client} \
+ cursor_line=$cursor_line cursor_column=$cursor_column \
+ perl -wne '
+ BEGIN {
+ use POSIX qw(strftime);
+ our $SQ = "'\''";
+ sub escape {
+ return shift =~ s/$SQ/$SQ$SQ/gr
+ }
+ sub quote {
+ my $token = escape shift;
+ return "$SQ$token$SQ";
+ }
+ sub shellquote {
+ my $token = shift;
+ $token =~ s/$SQ/$SQ\\$SQ$SQ/g;
+ return "$SQ$token$SQ";
+ }
+ sub perlquote {
+ my $token = shift;
+ $token =~ s/\\/\\\\/g;
+ $token =~ s/$SQ/\\$SQ/g;
+ return "$SQ$token$SQ";
+ }
+ $target = $ENV{"cursor_line"};
+ $state = "index";
+ }
+ chomp;
+ if ($state eq "index") {
+ if ($_ eq "---") {
+ $state = "blame";
+ next;
+ }
+ @blame_index = split;
+ next unless @blame_index;
+ shift @blame_index;
+ foreach (@blame_index) {
+ $_ =~ m{(\d+)\|(\d+),(\d+)} or die "bad blame index flag: $_";
+ my $buffer_line = $1;
+ if ($buffer_line == $target) {
+ $target_in_blame = $2;
+ $target_offset = $3;
+ last;
+ }
+ }
+ defined $target_in_blame and next, or last;
+ }
+ if (m/^([0-9a-f]+) ([0-9]+) ([0-9]+) ([0-9]+)/) {
+ if ($done) {
+ last;
+ }
+ $sha = $1;
+ $old_line = $2;
+ $new_line = $3;
+ $count = $4;
+ if (defined $target_in_blame) {
+ if ($target_in_blame == $. - 2) {
+ $old_line += $target_offset;
+ $done = 1;
+ }
+ } else {
+ if ($new_line <= $target and $target < $new_line + $count) {
+ $old_line += $target - $new_line;
+ $done = 1;
+ }
+ }
+ }
+ if (m/^filename /) { $old_filenames{$sha} = substr($_,9) }
+ if (m/^author /) { $authors{$sha} = substr($_,7) }
+ if (m/^author-time ([0-9]*)/) { $dates{$sha} = strftime("%F", localtime $1) }
+ if (m/^summary /) { $summaries{$sha} = substr($_,8) }
+ END {
+ if (@blame_index and not defined $target_in_blame) {
+ print "echo fail git blame-jump: line has no blame information;";
+ exit;
+ }
+ if (not defined $sha) {
+ print "echo fail git blame-jump: missing blame info";
+ exit;
+ }
+ if (not $done) {
+ print "echo \"fail git blame-jump: line not found in annotations (blame still loading?)\"";
+ exit;
+ }
+ $info = "{Information}{\\}";
+ if ($sha =~ m{^0+$}) {
+ $old_filename = $ENV{"kak_buffile"};
+ $old_filename = substr $old_filename, length($ENV{"PWD"}) + 1;
+ $show_diff = "diff HEAD";
+ $info .= "Not committed yet";
+ } else {
+ $old_filename = $old_filenames{$sha};
+ $author = $authors{$sha};
+ $date = $dates{$sha};
+ $summary = $summaries{$sha};
+ $show_diff = "show $sha";
+ $info .= "$date $author \"$summary\"";
+ }
+ $on_close_fifo = "
+ evaluate-commands -draft $SQ
+ execute-keys <percent>
+ require-module diff
+ diff-parse BEGIN %{
+ \$in_file = " . escape(perlquote($old_filename)) . ";
+ \$in_file_line = $old_line;
+ } END $SQ$SQ
+ print \"execute-keys -client $ENV{client} \${diff_line}g<a-h>$ENV{cursor_column}l;\";
+ printf \"evaluate-commands -client $ENV{client} $SQ$SQ$SQ$SQ
+ hook -once window NormalIdle .* $SQ$SQ$SQ$SQ$SQ$SQ$SQ$SQ
+ execute-keys vv
+ echo -markup -- %s
+ $SQ$SQ$SQ$SQ$SQ$SQ$SQ$SQ
+ $SQ$SQ$SQ$SQ ;\"," . escape(escape(perlquote(escape(escape(quote($info)))))) . ";
+ $SQ$SQ
+ $SQ
+ ";
+ printf "on_close_fifo=%s show_git_cmd_output %s",
+ shellquote($on_close_fifo), $show_diff;
+ }
+ ')"
+ }
+
+ case "$1" in
+ apply)
+ shift
+ enquoted="$(printf '"%s" ' "$@")"
+ echo "require-module patch"
+ echo "patch git apply $enquoted"
+ ;;
+ show|show-branch|log|diff|status)
+ show_git_cmd_output "$@"
+ ;;
+ blame)
+ shift
+ blame_toggle "$@"
+ ;;
+ blame-jump)
+ blame_jump
+ ;;
+ hide-blame)
+ hide_blame
+ ;;
+ show-diff)
+ echo 'try %{ add-highlighter window/git-diff flag-lines Default git_diff_flags }'
+ update_diff
+ ;;
+ hide-diff)
+ echo 'try %{ remove-highlighter window/git-diff }'
+ ;;
+ update-diff) update_diff ;;
+ next-hunk) jump_hunk next ;;
+ prev-hunk) jump_hunk prev ;;
+ commit)
+ shift
+ commit "$@"
+ ;;
+ init)
+ shift
+ git init "$@" > /dev/null 2>&1
+ ;;
+ add|rm)
+ cmd="$1"
+ shift
+ run_git_cmd $cmd "${@:-"${kak_buffile}"}"
+ ;;
+ reset|checkout)
+ run_git_cmd "$@"
+ ;;
+ grep)
+ shift
+ enquoted="$(printf '"%s" ' "$@")"
+ printf %s "try %{
+ set-option current grepcmd 'git grep -n --column'
+ grep $enquoted
+ set-option current grepcmd '$kak_opt_grepcmd'
+ }"
+ ;;
+ edit)
+ shift
+ enquoted="$(printf '"%s" ' "$@")"
+ printf %s "edit -existing -- $enquoted"
+ ;;
+ *)
+ printf "fail unknown git command '%s'\n" "$1"
+ exit
+ ;;
+ esac
+}}
+
+# Works within :git diff and :git show
+define-command git-diff-goto-source \
+ -docstring 'Navigate to source by pressing the enter key in hunks when git diff is displayed. Works within :git diff and :git show' %{
+ require-module diff
+ diff-jump %sh{ git rev-parse --show-toplevel }
+}
diff --git a/autoload/tools/go/gopls.kak b/autoload/tools/go/gopls.kak
new file mode 100644
index 0000000..1e295ef
--- /dev/null
+++ b/autoload/tools/go/gopls.kak
@@ -0,0 +1,98 @@
+# gopls.kak: gopls bindings for kakoune
+
+define-command -params 1 -docstring %{
+gopls <command>: gopls command wrapper
+
+All commands are forwarded to gopls utility
+Available commands are:
+ format
+ imports
+ definition
+ references
+} -shell-script-candidates %{
+ printf "format\nimports\ndefinition\nreferences\n"
+} \
+gopls %{
+ require-module gopls
+ evaluate-commands %sh{
+ case "$1" in
+ format|imports)
+ printf %s\\n "gopls-cmd $1"
+ ;;
+ definition)
+ printf %s\\n "gopls-def"
+ ;;
+ references)
+ printf %s\\n "gopls-ref"
+ ;;
+ *)
+ printf "fail Unknown gopls command '%s'\n" "$1"
+ exit
+ ;;
+ esac
+ }
+}
+
+provide-module gopls %§
+
+evaluate-commands %sh{
+ if ! command -v gopls > /dev/null 2>&1; then
+ echo "fail Please install gopls or add to PATH!"
+ fi
+}
+
+# Temp dir preparation
+declare-option -hidden str gopls_tmp_dir
+define-command -hidden -params 0 gopls-prepare %{
+ evaluate-commands %sh{
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-gopls.XXXXXXXX)
+ printf %s\\n "set-option buffer gopls_tmp_dir ${dir}"
+ }
+}
+
+# gopls format/imports
+define-command -hidden -params 1 gopls-cmd %{
+ gopls-prepare
+ evaluate-commands %sh{
+ dir=${kak_opt_gopls_tmp_dir}
+ gopls "$1" -w "${kak_buffile}" 2> "${dir}/stderr"
+ if [ $? -ne 0 ]; then
+ # show error messages in *debug* buffer
+ printf %s\\n "echo -debug %file{${dir}/stderr}"
+ fi
+ }
+ edit!
+ nop %sh{ rm -rf "${kak_opt_gopls_tmp_dir}" }
+}
+
+# gopls definition
+define-command -hidden -params 0 gopls-def %{
+ evaluate-commands %sh{
+ jump=$( gopls definition "${kak_buffile}:${kak_cursor_line}:${kak_cursor_column}" 2> /dev/null \
+ |sed -e 's/-[0-9]\+:.*//; s/:/ /g; q' )
+ if [ -n "${jump}" ]; then
+ printf %s\\n "evaluate-commands -try-client '${kak_opt_jumpclient}' %{
+ edit ${jump}
+ }"
+ fi
+ }
+}
+
+# gopls references
+define-command -hidden -params 0 gopls-ref %{
+ gopls-prepare
+ evaluate-commands %sh{
+ dir=${kak_opt_gopls_tmp_dir}
+ mkfifo "${dir}/fifo"
+ ( { trap - INT QUIT; gopls references "${kak_buffile}:${kak_cursor_line}:${kak_cursor_column}"
+ } > "${dir}/fifo" 2> /dev/null & ) > /dev/null 2>&1 < /dev/null
+ # using filetype=grep for nice hilight and <ret> mapping
+ printf %s\\n "evaluate-commands -try-client '${kak_opt_toolsclient}' %{
+ edit! -fifo '${dir}/fifo' *gopls-refs*
+ set-option buffer filetype grep
+ hook -always -once buffer BufCloseFifo .* %{ nop %sh{ rm -r '${dir}' } }
+ }"
+ }
+}
+
diff --git a/autoload/tools/grep.kak b/autoload/tools/grep.kak
new file mode 100644
index 0000000..13b5b9c
--- /dev/null
+++ b/autoload/tools/grep.kak
@@ -0,0 +1,66 @@
+declare-option -docstring "shell command run to search for subtext in a file/directory" \
+ str grepcmd 'grep -RHn'
+
+provide-module grep %{
+
+require-module jump
+
+define-command -params .. -docstring %{
+ grep [<arguments>]: grep utility wrapper
+ All optional arguments are forwarded to the grep utility
+ Passing no argument will perform a literal-string grep for the current selection
+} grep %{ evaluate-commands %sh{
+ if [ $# -eq 0 ]; then
+ case "$kak_opt_grepcmd" in
+ ag\ * | git\ grep\ * | grep\ * | rg\ * | ripgrep\ * | ugrep\ * | ug\ *)
+ set -- -F "${kak_selection}"
+ ;;
+ ack\ *)
+ set -- -Q "${kak_selection}"
+ ;;
+ *)
+ set -- "${kak_selection}"
+ ;;
+ esac
+ fi
+
+ output=$(mktemp -d "${TMPDIR:-/tmp}"/kak-grep.XXXXXXXX)/fifo
+ mkfifo ${output}
+ ( { trap - INT QUIT; ${kak_opt_grepcmd} "$@" 2>&1 | tr -d '\r'; } > ${output} 2>&1 & ) > /dev/null 2>&1 < /dev/null
+
+ printf %s\\n "evaluate-commands -try-client '$kak_opt_toolsclient' %{
+ edit! -fifo ${output} *grep*
+ set-option buffer filetype grep
+ set-option buffer jump_current_line 0
+ 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
+ add-highlighter window/grep/ regex "^([^:\n]+):(\d+):(\d+)?" 1:cyan 2:green 3:green
+ add-highlighter window/grep/ line %{%opt{jump_current_line}} default+b
+ hook -once -always window WinSetOption filetype=.* %{ remove-highlighter window/grep }
+}
+
+hook global WinSetOption filetype=grep %{
+ hook buffer -group grep-hooks NormalKey <ret> jump
+ hook -once -always window WinSetOption filetype=.* %{ remove-hooks buffer grep-hooks }
+}
+
+define-command -hidden grep-jump %{
+ jump
+}
+
+define-command grep-next-match -docstring %{alias for "jump-next *grep*"} %{
+ jump-next -matching \*grep(-.*)?\*
+}
+
+define-command grep-previous-match -docstring %{alias for "jump-previous *grep*"} %{
+ jump-previous -matching \*grep(-.*)?\*
+}
+
+}
+
+hook -once global KakBegin .* %{ require-module grep }
diff --git a/autoload/tools/jump.kak b/autoload/tools/jump.kak
new file mode 100644
index 0000000..60d777d
--- /dev/null
+++ b/autoload/tools/jump.kak
@@ -0,0 +1,70 @@
+declare-option -docstring "name of the client in which all source code jumps will be executed" \
+ str jumpclient
+declare-option -docstring "name of the client in which utilities display information" \
+ str toolsclient
+
+provide-module jump %{
+
+declare-option -hidden int jump_current_line 0
+
+define-command -hidden jump %{
+ evaluate-commands -save-regs a %{ # use evaluate-commands to ensure jumps are collapsed
+ try %{
+ evaluate-commands -draft %{
+ execute-keys ',xs^([^:\n]+):(\d+):(\d+)?<ret>'
+ set-register a %reg{1} %reg{2} %reg{3}
+ }
+ set-option buffer jump_current_line %val{cursor_line}
+ evaluate-commands -try-client %opt{jumpclient} -verbatim -- edit -existing -- %reg{a}
+ try %{ focus %opt{jumpclient} }
+ }
+ }
+}
+
+define-command jump-next -params 1.. -docstring %{
+ jump-next <bufname>: jump to next location listed in the given *grep*-like location list buffer.
+} %{
+ evaluate-commands -try-client %opt{jumpclient} -save-regs / %{
+ buffer %arg{@}
+ jump-select-next
+ jump
+ }
+ try %{
+ evaluate-commands -client %opt{toolsclient} %{
+ buffer %arg{@}
+ execute-keys gg %opt{jump_current_line}g
+ }
+ }
+}
+complete-command jump-next buffer
+define-command -hidden jump-select-next %{
+ # First jump to end of buffer so that if jump_current_line == 0
+ # 0g<a-l> will be a no-op and we'll jump to the first result.
+ # Yeah, thats ugly...
+ execute-keys ge %opt{jump_current_line}g<a-l> /^[^:\n]+:\d+:<ret>
+}
+
+define-command jump-previous -params 1.. -docstring %{
+ jump-previous <bufname>: jump to previous location listed in the given *grep*-like location list buffer.
+} %{
+ evaluate-commands -try-client %opt{jumpclient} -save-regs / %{
+ buffer %arg{@}
+ jump-select-previous
+ jump
+ }
+ try %{
+ evaluate-commands -client %opt{toolsclient} %{
+ buffer %arg{@}
+ execute-keys gg %opt{jump_current_line}g
+ }
+ }
+}
+complete-command jump-previous buffer
+define-command -hidden jump-select-previous %{
+ # See comment in jump-select-next
+ execute-keys ge %opt{jump_current_line}g<a-h> <a-/>^[^:\n]+:\d+:<ret>
+}
+
+}
+
+hook -once global KakBegin .* %{ require-module jump }
diff --git a/autoload/tools/lint.asciidoc b/autoload/tools/lint.asciidoc
new file mode 100644
index 0000000..469b1e5
--- /dev/null
+++ b/autoload/tools/lint.asciidoc
@@ -0,0 +1,26 @@
+= Integrate with tools that check files for problems.
+
+Many file-formats have "lint" tools that check for common problems and point out
+where they occur. Most of these tools produce output in the traditional message
+format:
+
+----
+{filename}:{line}:{column}: {kind}: {message}
+----
+
+If the 'kind' field contains 'error', the message is treated as an error,
+otherwise it is assumed to be a warning.
+
+The `:lint-buffer` and `:lint-selections` commands will run the shell command
+specified in the `lintcmd` option, passing it the path to a temporary file
+containing the text to be linted. The results are collected in the
+`*lint-output*` buffer, and analyze it. If `toolsclient` is set, the
+`*lint-output*` buffer will be displayed in the named client.
+
+Each reported error or warning causes a marker to appear in the left-hand
+margin of the buffer that was checked. When the main cursor moves onto that
+line, the associated messages are displayed. If they get distracting, you can
+turn off the markers and messages with the `:lint-hide-diagnostics` command.
+
+You can also use `:lint-next-message` and `:lint-previous-message` to jump
+between the lines with messages.
diff --git a/autoload/tools/lint.kak b/autoload/tools/lint.kak
new file mode 100644
index 0000000..471edc9
--- /dev/null
+++ b/autoload/tools/lint.kak
@@ -0,0 +1,452 @@
+# require-module jump
+
+declare-option \
+ -docstring %{
+ The shell command used by lint-buffer and lint-selections.
+
+ See `:doc lint` for details.
+ } \
+ str lintcmd
+
+declare-option -hidden line-specs lint_flags
+declare-option -hidden line-specs lint_messages
+declare-option -hidden int lint_error_count
+declare-option -hidden int lint_warning_count
+
+define-command -hidden -params 1 lint-open-output-buffer %{
+ evaluate-commands -try-client %opt{toolsclient} %{
+ edit! -fifo "%arg{1}/fifo" -debug *lint-output*
+ set-option buffer filetype make
+ set-option buffer jump_current_line 0
+ }
+}
+
+define-command \
+ -hidden \
+ -params 1 \
+ -docstring %{
+ lint-cleaned-selections <linter>: Check each selection with <linter>.
+
+ Assumes selections all have anchor before cursor, and that
+ %val{selections} and %val{selections_desc} are in the same order.
+ } \
+ lint-cleaned-selections \
+%{
+ # Create a temporary directory to keep all our state.
+ evaluate-commands %sh{
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ # Before we clobber our arguments,
+ # let's record the lintcmd we were given.
+ lintcmd="$1"
+
+ # Some linters care about the name or extension
+ # of the file being linted, so we'll store the text we want to lint
+ # in a file with the same name as the original buffer.
+ filename="${kak_buffile##*/}"
+
+ # A directory to keep all our temporary data.
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-lint.XXXXXXXX)
+
+ # Write all the selection descriptions to files.
+ eval set -- "$kak_selections_desc"
+ i=0
+ for desc; do
+ mkdir -p "$dir"/sel-"$i"
+ printf "%s" "$desc" > "$dir"/sel-$i/desc
+ i=$(( i + 1 ))
+ done
+
+ # Write all the selection contents to files.
+ eval set -- "$kak_quoted_selections"
+ i=0
+ for text; do
+ # The selection text needs to be stored in a subdirectory,
+ # so we can be sure the filename won't clash with one of ours.
+ mkdir -p "$dir"/sel-"$i"/text/
+ printf "%s" "$text" > "$dir"/sel-$i/text/"$filename"
+ i=$(( i + 1 ))
+ done
+
+ # We do redirection trickiness to record stderr from
+ # this background task and route it back to Kakoune,
+ # but shellcheck isn't a fan.
+ # shellcheck disable=SC2094
+ ({ # do the parsing in the background and when ready send to the session
+ trap - INT QUIT
+
+ for selpath in "$dir"/sel-*; do
+ # Read in the line and column offset of this selection.
+ IFS=".," read -r start_line start_byte _ < "$selpath"/desc
+
+ # Run the linter, and record the exit-code.
+ eval "$lintcmd '$selpath/text/$filename'" |
+ sort -t: -k2,2 -n |
+ awk \
+ -v line_offset=$(( start_line - 1 )) \
+ -v first_line_byte_offset=$(( start_byte - 1 )) \
+ '
+ BEGIN { OFS=":"; FS=":" }
+
+ /:[1-9][0-9]*:[1-9][0-9]*:/ {
+ $1 = ENVIRON["kak_bufname"]
+ if ( $2 == 1 ) {
+ $3 += first_line_byte_offset
+ }
+ $2 += line_offset
+ print $0
+ }
+ ' >>"$dir"/result
+ done
+
+ # Load all the linter messages into Kakoune options.
+ # Inside this block, shellcheck warns us that the shell doesn't
+ # need backslash-continuation chars in a single-quoted string,
+ # but awk still needs them.
+ # shellcheck disable=SC1004
+ awk -v file="$kak_buffile" -v stamp="$kak_timestamp" -v client="$kak_client" '
+ function kakquote(text) {
+ # \x27 is apostrophe, escaped for shell-quoting reasons.
+ gsub(/\x27/, "\x27\x27", text)
+ return "\x27" text "\x27"
+ }
+
+ BEGIN {
+ OFS=":"
+ FS=":"
+ error_count = 0
+ warning_count = 0
+ }
+
+ /:[1-9][0-9]*:[1-9][0-9]*:/ {
+ # Remember that an error or a warning occurs on this line..
+ if ($4 ~ /[Ee]rror/) {
+ # We definitely have an error on this line.
+ flags_by_line[$2] = "{Error}x"
+ error_count++
+ } else if (flags_by_line[$2] ~ /Error/) {
+ # We have a warning on this line,
+ # but we already have an error, so do nothing.
+ warning_count++
+ } else {
+ # We have a warning on this line,
+ # and no previous error.
+ flags_by_line[$2] = "{Information}!"
+ warning_count++
+ }
+
+ # The message starts with the severity indicator.
+ msg = substr($4, 2)
+
+ # fix case where $5 is not the last field
+ # because of extra colons in the message
+ for (i=5; i<=NF; i++) msg = msg ":" $i
+
+ # Mention the column where this problem occurs,
+ # so that information is not lost.
+ msg = msg "(col " $3 ")"
+
+ # Messages will be stored in a line-specs option,
+ # and each record in the option uses "|"
+ # as a field delimiter, so we need to escape them.
+ gsub(/\|/, "\\|", msg)
+
+ if ($2 in messages_by_line) {
+ # We already have a message on this line,
+ # so append our new message.
+ messages_by_line[$2] = messages_by_line[$2] "\n" msg
+ } else {
+ # A brand-new message on this line.
+ messages_by_line[$2] = msg
+ }
+ }
+
+ END {
+ printf("set-option %s lint_flags %s", kakquote("buffer=" file), stamp);
+ for (line in flags_by_line) {
+ flag = flags_by_line[line]
+ printf(" %s", kakquote(line "|" flag));
+ }
+ printf("\n");
+
+ printf("set-option %s lint_messages %s", kakquote("buffer=" file), stamp);
+ for (line in messages_by_line) {
+ msg = messages_by_line[line]
+ printf(" %s", kakquote(line "|" msg));
+ }
+ printf("\n");
+
+ print "set-option " \
+ kakquote("buffer=" file) " " \
+ "lint_error_count " \
+ error_count
+ print "set-option " \
+ kakquote("buffer=" file) " " \
+ "lint_warning_count " \
+ warning_count
+ }
+ ' "$dir"/result | kak -p "$kak_session"
+
+ # Send any linting errors to the debug buffer,
+ # for visibility.
+ if [ -s "$dir"/stderr ]; then
+ # Errors were detected!"
+ printf "echo -debug Linter errors: <<<\n"
+ while read -r LINE; do
+ printf "echo -debug %s\n" "$(kakquote " $LINE")"
+ done < "$dir"/stderr
+ printf "echo -debug >>>\n"
+ # FIXME: When #3254 is fixed, this can become a "fail"
+ printf "eval -client %s echo -markup {Error}%s\n" \
+ "$kak_client" \
+ "lint failed, see *debug* for details"
+ else
+ # No errors detected, show the results.
+ printf "eval -client %s 'lint-show-diagnostics; lint-show-counters'" \
+ "$kak_client"
+ fi | kak -p "$kak_session"
+
+ # A fifo to send the results back to a Kakoune buffer.
+ mkfifo "$dir"/fifo
+ # Send the results to kakoune if the session is still valid.
+ if printf 'lint-open-output-buffer %s' "$(kakquote "$dir")" | kak -p "$kak_session"; then
+ cat "$dir"/result > "$dir"/fifo
+ fi
+ # Clean up.
+ rm -rf "$dir"
+
+ } & ) >"$dir"/stderr 2>&1 </dev/null
+ }
+}
+
+define-command \
+ -params 0..2 \
+ -docstring %{
+ lint-selections [<switches>]: Check each selection with a linter.
+
+ Switches:
+ -command <cmd> Use the given linter.
+ If not given, the lintcmd option is used.
+
+ See `:doc lint` for details.
+ } \
+ lint-selections \
+%{
+ evaluate-commands -draft %{
+ # Make sure all the selections are "forward" (anchor before cursor)
+ execute-keys <a-:>
+
+ # Make sure the selections are in document order.
+ evaluate-commands %sh{
+ printf "select "
+ printf "%s\n" "$kak_selections_desc" |
+ tr ' ' '\n' |
+ sort -n -t. |
+ tr '\n' ' '
+ }
+
+ evaluate-commands %sh{
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ if [ "$1" = "-command" ]; then
+ if [ -z "$2" ]; then
+ echo 'fail -- -command option requires a value'
+ exit 1
+ fi
+ lintcmd="$2"
+ elif [ -n "$1" ]; then
+ echo "fail -- Unrecognised parameter $(kakquote "$1")"
+ exit 1
+ elif [ -z "${kak_opt_lintcmd}" ]; then
+ echo 'fail The lintcmd option is not set'
+ exit 1
+ else
+ lintcmd="$kak_opt_lintcmd"
+ fi
+
+ printf 'lint-cleaned-selections %s\n' "$(kakquote "$lintcmd")"
+ }
+ }
+}
+
+define-command \
+ -docstring %{
+ lint-buffer: Check the current buffer with a linter.
+
+ See `:doc lint` for details.
+ } \
+ lint-buffer \
+%{
+ evaluate-commands %sh{
+ if [ -z "${kak_opt_lintcmd}" ]; then
+ echo 'fail The lintcmd option is not set'
+ exit 1
+ fi
+ }
+ evaluate-commands -draft %{
+ execute-keys '%'
+ lint-cleaned-selections %opt{lintcmd}
+ }
+}
+
+alias global lint lint-buffer
+
+define-command -hidden lint-show-current-line %{
+ update-option buffer lint_messages
+ evaluate-commands %sh{
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ eval set -- "${kak_quoted_opt_lint_messages}"
+ shift # skip the timestamp
+
+ while [ $# -gt 0 ]; do
+ lineno=${1%%|*}
+ msg=${1#*|}
+
+ if [ "$lineno" -eq "$kak_cursor_line" ]; then
+ printf "info -anchor %d.%d %s\n" \
+ "$kak_cursor_line" \
+ "$kak_cursor_column" \
+ "$(kakquote "$msg")"
+ break
+ fi
+ shift
+ done
+ }
+}
+
+define-command -hidden lint-show-counters %{
+ echo -markup "linting results: {Error} %opt{lint_error_count} error(s) {Information} %opt{lint_warning_count} warning(s) "
+}
+
+define-command -hidden lint-show-diagnostics %{
+ try %{
+ # Assume that if the highlighter is set, then hooks also are
+ add-highlighter window/lint flag-lines default lint_flags
+ hook window -group lint-diagnostics NormalIdle .* %{ lint-show-current-line }
+ hook window -group lint-diagnostics WinSetOption lint_flags=.* %{ info; lint-show-current-line }
+ }
+}
+
+define-command lint-hide-diagnostics -docstring "Hide line markers and disable automatic diagnostic displaying" %{
+ remove-highlighter window/lint
+ remove-hooks window lint-diagnostics
+}
+
+# FIXME: Is there some way we can re-use make-next-error
+# instead of re-implementing it?
+define-command \
+ -docstring "Jump to the next line that contains a lint message" \
+ lint-next-message \
+%{
+ update-option buffer lint_messages
+
+ evaluate-commands %sh{
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ eval "set -- ${kak_quoted_opt_lint_messages}"
+ shift
+
+ if [ "$#" -eq 0 ]; then
+ printf 'fail no lint messages'
+ exit
+ fi
+
+ first_lineno=""
+ first_msg=""
+
+ for lint_message; do
+ lineno="${lint_message%%|*}"
+ msg="${lint_message#*|}"
+
+ if [ -z "$first_lineno" ]; then
+ first_lineno=$lineno
+ first_msg=$msg
+ fi
+
+ if [ "$lineno" -gt "$kak_cursor_line" ]; then
+ printf "execute-keys %dg\n" "$lineno"
+ printf "info -anchor %d.%d %s\n" \
+ "$lineno" "1" "$(kakquote "$msg")"
+ exit
+ fi
+ done
+
+ # We didn't find any messages after the current line,
+ # let's wrap around to the beginning.
+ printf "execute-keys %dg\n" "$first_lineno"
+ printf "info -anchor %d.%d %s\n" \
+ "$first_lineno" "1" "$(kakquote "$first_msg")"
+ printf "echo -markup \
+ {Information}lint message search wrapped around buffer\n"
+
+ }
+}
+
+# FIXME: Is there some way we can re-use make-previous-error
+# instead of re-implementing it?
+define-command \
+ -docstring "Jump to the previous line that contains a lint message" \
+ lint-previous-message \
+%{
+ update-option buffer lint_messages
+
+ evaluate-commands %sh{
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ eval "set -- ${kak_quoted_opt_lint_messages}"
+ shift
+
+ if [ "$#" -eq 0 ]; then
+ printf 'fail no lint messages'
+ exit
+ fi
+
+ prev_lineno=""
+ prev_msg=""
+
+ for lint_message; do
+ lineno="${lint_message%%|*}"
+ msg="${lint_message#*|}"
+
+ # If this message comes on or after the cursor position...
+ if [ "$lineno" -ge "${kak_cursor_line}" ]; then
+ # ...and we had a previous message...
+ if [ -n "$prev_lineno" ]; then
+ # ...then go to the previous message and display it.
+ printf "execute-keys %dg\n" "$prev_lineno"
+ printf "info -anchor %d.%d %s\n" \
+ "$lineno" "1" "$(kakquote "$prev_msg")"
+ exit
+
+ # We are after the cursor position, but there has been
+ # no previous message; we'll need to do something else.
+ else
+ break
+ fi
+ fi
+
+ # We have not yet reached the cursor position, stash this message
+ # and try the next.
+ prev_lineno="$lineno"
+ prev_msg="$msg"
+ done
+
+ # There is no message before the cursor position,
+ # let's wrap around to the end.
+ shift $(( $# - 1 ))
+ last_lineno="${1%%|*}"
+ last_msg="${1#*|}"
+
+ printf "execute-keys %dg\n" "$last_lineno"
+ printf "info -anchor %d.%d %s\n" \
+ "$last_lineno" "1" "$(kakquote "$last_msg")"
+ printf "echo -markup \
+ {Information}lint message search wrapped around buffer\n"
+ }
+}
diff --git a/autoload/tools/make.kak b/autoload/tools/make.kak
new file mode 100644
index 0000000..61d9b6c
--- /dev/null
+++ b/autoload/tools/make.kak
@@ -0,0 +1,92 @@
+declare-option -docstring "shell command run to build the project" \
+ str makecmd make
+declare-option -docstring "pattern that describes lines containing information about errors in the output of the `makecmd` command. Capture groups must be: 1: filename 2: line number 3: optional column 4: optional error description" \
+ regex make_error_pattern "^([^:\n]+):(\d+):(?:(\d+):)? (?:fatal )?error:([^\n]+)?"
+
+provide-module make %{
+
+require-module jump
+
+define-command -params .. \
+ -docstring %{
+ make [<arguments>]: make utility wrapper
+ All the optional arguments are forwarded to the make utility
+ } make %{ evaluate-commands %sh{
+ output=$(mktemp -d "${TMPDIR:-/tmp}"/kak-make.XXXXXXXX)/fifo
+ mkfifo ${output}
+ ( { trap - INT QUIT; eval "${kak_opt_makecmd}" "$@"; } > ${output} 2>&1 & ) > /dev/null 2>&1 < /dev/null
+
+ printf %s\\n "evaluate-commands -try-client '$kak_opt_toolsclient' %{
+ edit! -fifo ${output} -scroll *make*
+ set-option buffer filetype make
+ set-option buffer jump_current_line 0
+ hook -always -once buffer BufCloseFifo .* %{ nop %sh{ rm -r $(dirname ${output}) } }
+ }"
+}}
+
+add-highlighter shared/make group
+add-highlighter shared/make/ regex "^([^:\n]+):(\d+):(?:(\d+):)?\h+(?:((?:fatal )?error)|(warning)|(note)|(required from(?: here)?))?.*?$" 1:cyan 2:green 3:green 4:red 5:yellow 6:blue 7:yellow
+add-highlighter shared/make/ regex "^\h*(~*(?:(\^)~*)?)$" 1:green 2:cyan+b
+add-highlighter shared/make/ line '%opt{jump_current_line}' default+b
+
+hook -group make-highlight global WinSetOption filetype=make %{
+ add-highlighter window/make ref make
+ hook -once -always window WinSetOption filetype=.* %{ remove-highlighter window/make }
+}
+
+hook global WinSetOption filetype=make %{
+ alias buffer jump make-jump
+ alias buffer jump-select-next make-select-next
+ alias buffer jump-select-previous make-select-previous
+ hook buffer -group make-hooks NormalKey <ret> make-jump
+ hook -once -always window WinSetOption filetype=.* %{ remove-hooks buffer make-hooks }
+}
+
+define-command -hidden make-open-error -params 4 %{
+ evaluate-commands -try-client %opt{jumpclient} %{
+ edit -existing "%arg{1}" %arg{2} %arg{3}
+ echo -markup "{Information}{\}%arg{4}"
+ try %{ focus }
+ }
+}
+
+define-command -hidden make-jump %{
+ evaluate-commands -save-regs a/ %{
+ evaluate-commands -draft %{
+ execute-keys ,
+ try %{
+ execute-keys gl<a-?> "Entering directory" <ret><a-:>
+ # Try to parse the error into capture groups, failing on absolute paths
+ execute-keys s "Entering directory [`']([^']+)'.*\n([^:\n/][^:\n]*):(\d+):(?:(\d+):)?([^\n]+)\n?\z" <ret>l
+ set-option buffer jump_current_line %val{cursor_line}
+ set-register a "%reg{1}/%reg{2}" "%reg{3}" "%reg{4}" "%reg{5}"
+ } catch %{
+ set-register / %opt{make_error_pattern}
+ execute-keys <a-h><a-l> s<ret>l
+ set-option buffer jump_current_line %val{cursor_line}
+ set-register a "%reg{1}" "%reg{2}" "%reg{3}" "%reg{4}"
+ }
+ }
+ make-open-error %reg{a}
+ }
+}
+define-command -hidden make-select-next %{
+ set-register / %opt{make_error_pattern}
+ execute-keys "%opt{jump_current_line}ggl" "/<ret>"
+}
+define-command -hidden make-select-previous %{
+ set-register / %opt{make_error_pattern}
+ execute-keys "%opt{jump_current_line}g" "<a-/><ret>"
+}
+
+define-command make-next-error -docstring %{alias for "jump-next *make*"} %{
+ jump-next *make*
+}
+
+define-command make-previous-error -docstring %{alias for "jump-previous *make*"} %{
+ jump-previous *make*
+}
+
+}
+
+hook -once global KakBegin .* %{ require-module make }
diff --git a/autoload/tools/man.kak b/autoload/tools/man.kak
new file mode 100644
index 0000000..2fcc981
--- /dev/null
+++ b/autoload/tools/man.kak
@@ -0,0 +1,139 @@
+declare-option -docstring "name of the client in which documentation is to be displayed" \
+ str docsclient
+
+declare-option -hidden str-list manpage
+
+hook -group man-highlight global WinSetOption filetype=man %{
+ add-highlighter window/man-highlight group
+ # Sections
+ add-highlighter window/man-highlight/ regex ^\S.*?$ 0:title
+ # Subsections
+ add-highlighter window/man-highlight/ regex '^ {3}\S.*?$' 0:default+b
+ # Command line options
+ add-highlighter window/man-highlight/ regex '^ {7}-[^\s,]+(,\s+-[^\s,]+)*' 0:list
+ # References to other manpages
+ add-highlighter window/man-highlight/ regex [-a-zA-Z0-9_.]+\([a-z0-9]+\) 0:header
+
+ map window normal <ret> :man-jump<ret>
+
+ hook -once -always window WinSetOption filetype=.* %{
+ remove-highlighter window/man-highlight
+ unmap window normal <ret>
+ }
+}
+
+hook global WinSetOption filetype=man %{
+ hook -group man-hooks window WinResize .* %{ man-impl %opt{manpage} }
+ hook -once -always window WinSetOption filetype=.* %{ remove-hooks window man-hooks }
+}
+
+define-command -hidden -params ..3 man-impl %{ evaluate-commands %sh{
+ buffer_name="$1"
+ if [ -z "${buffer_name}" ]; then
+ exit
+ fi
+ shift
+ manout=$(mktemp "${TMPDIR:-/tmp}"/kak-man.XXXXXX)
+ manerr=$(mktemp "${TMPDIR:-/tmp}"/kak-man.XXXXXX)
+ colout=$(mktemp "${TMPDIR:-/tmp}"/kak-man.XXXXXX)
+ env MANWIDTH=${kak_window_range##* } man "$@" > "$manout" 2> "$manerr"
+ retval=$?
+ if command -v col >/dev/null; then
+ col -b -x > ${colout} < ${manout}
+ else
+ sed 's/.//g' > ${colout} < ${manout}
+ fi
+ rm ${manout}
+
+ if [ "${retval}" -eq 0 ]; then
+ printf %s\\n "
+ edit -scratch %{*$buffer_name ${*}*}
+ execute-keys '%|cat<space>${colout}<ret>gk'
+ nop %sh{ rm ${colout}; rm ${manerr} }
+ set-option buffer filetype man
+ set-option window manpage $buffer_name $*
+ "
+ else
+ printf '
+ fail %%{%s}
+ nop %%sh{ rm "%s"; rm "%s" }
+ ' "$(cat "$manerr")" "${colout}" "${manerr}"
+ fi
+} }
+
+define-command -params ..1 \
+ -shell-script-candidates %{
+ find /usr/share/man/ $(printf %s "${MANPATH}" |
+ sed 's/:/ /') -name '*.[1-8]*' |
+ sed 's,^.*/\(.*\)\.\([1-8][a-zA-Z]*\).*$,\1(\2),'
+ } \
+ -docstring %{
+ man [<page>]: manpage viewer wrapper
+ If no argument is passed to the command, the selection will be used as page
+ The page can be a word, or a word directly followed by a section number between parenthesis, e.g. kak(1)
+ } man %{ evaluate-commands %sh{
+ subject=${1-$kak_selection}
+
+ ## The completion suggestions display the page number, strip them if present
+ case "${subject}" in
+ *\([1-8]*\))
+ pagenum="${subject##*\(}"
+ pagenum="${pagenum%\)}"
+ subject="${subject%%\(*}"
+ ;;
+ *)
+ pagenum=""
+ ;;
+ esac
+
+ printf %s\\n "evaluate-commands -try-client %opt{docsclient} man-impl man $pagenum $subject"
+} }
+
+
+
+# The following section of code enables a user
+# to go to next or previous man page links and to follow man page links,
+# for example, apropos(1), that would normally appear in SEE ALSO sections.
+# The user would position the cursor on any character of the link
+# and then press <ret> to change to a buffer showing the man page.
+
+# Regex pattern defining a man page link.
+# Used for determining if a selection, which may just be a link, is a link.
+declare-option -hidden regex man_link1 \
+ [\w_.:-]+\(\d[a-z]*\)
+
+# Same as above but with lookbehind and lookahead patterns.
+# Used for searching for a man page link.
+declare-option -hidden regex man_link2 \
+ "(?:^|(?<=\W))%opt{man_link1}(?=\W)"
+
+# Define a useful command sequence for searching a given regex
+# and a given sequence of search keys.
+define-command -hidden man-search -params 2 %{
+ set-register / %arg[1]
+ try %{
+ execute-keys %arg[2]
+ } catch %{
+ fail "Could not find man page link"
+ }
+}
+
+define-command -docstring 'Go to next man page link' \
+man-link-next %{ man-search %opt[man_link2] n }
+
+define-command -docstring 'Go to previous man page link' \
+man-link-prev %{ man-search %opt[man_link2] <a-n> }
+
+define-command -docstring 'Try to jump to a man page' \
+man-jump %{
+ try %{ execute-keys <a-a><a-w> s %opt[man_link1] <ret> } catch %{ fail 'Not a valid man page link' }
+ try %{ man } catch %{ fail 'No man page link to follow' }
+}
+
+# Suggested keymaps for a user mode
+declare-user-mode man
+
+map global man 'g' -docstring 'Jump to a man page using selected man page link' :man-jump<ret>
+map global man 'j' -docstring 'Go to next man page link' :man-link-next<ret>
+map global man 'k' -docstring 'Go to previous man page link' :man-link-prev<ret>
+map global man 'm' -docstring 'Look up a man page' :man<space>
diff --git a/autoload/tools/menu.kak b/autoload/tools/menu.kak
new file mode 100644
index 0000000..4fd7dde
--- /dev/null
+++ b/autoload/tools/menu.kak
@@ -0,0 +1,85 @@
+provide-module menu %§§
+
+define-command menu -params 1.. -docstring %{
+ menu [<switches>] <name1> <commands1> <name2> <commands2>...: display a
+ menu and execute commands for the selected item
+
+ -auto-single instantly validate if only one item is available
+ -select-cmds each item specify an additional command to run when selected
+} %{
+ evaluate-commands %sh{
+ auto_single=false
+ select_cmds=false
+ stride=2
+ on_abort=
+ while true
+ do
+ case "$1" in
+ (-auto-single) auto_single=true ;;
+ (-select-cmds) select_cmds=true; stride=3 ;;
+ (-on-abort) on_abort="$2"; shift ;;
+ (-markup) ;; # no longer supported
+ (*) break ;;
+ esac
+ shift
+ done
+ if [ $(( $# % $stride )) -ne 0 ]; then
+ echo fail "wrong argument count"
+ exit
+ fi
+ if $auto_single && [ $# -eq $stride ]; then
+ printf %s "$2"
+ exit
+ fi
+ shellquote() {
+ printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g; s/§/§§/g; $2")"
+ }
+ cases=
+ select_cases=
+ completion=
+ nl=$(printf '\n.'); nl=${nl%.}
+ while [ $# -gt 0 ]; do
+ title=$1
+ command=$2
+ completion="${completion}${title}${nl}"
+ cases="${cases}
+ ($(shellquote "$title" s/¶/¶¶/g))
+ printf '%s\\n' $(shellquote "$command" s/¶/¶¶/g)
+ ;;"
+ if $select_cmds; then
+ select_command=$3
+ select_cases="${select_cases}
+ ($(shellquote "$title" s/¶/¶¶/g))
+ printf '%s\\n' $(shellquote "$select_command" s/¶/¶¶/g)
+ ;;"
+ fi
+ shift $stride
+ done
+ printf "\
+ prompt '' %%§
+ evaluate-commands %%sh¶
+ case \"\$kak_text\" in \
+ %s
+ (*) echo fail -- no such item: \"'\$(printf %%s \"\$kak_text\" | sed \"s/'/''/g\")'\" ;;
+ esac
+ ¶
+ §" "$cases"
+ if $select_cmds; then
+ printf " \
+ -on-change %%§
+ evaluate-commands %%sh¶
+ case \"\$kak_text\" in \
+ %s
+ (*) : ;;
+ esac
+ ¶
+ §" "$select_cases"
+ fi
+ if [ -n "$on_abort" ]; then
+ printf " -on-abort '%s'" "$(printf %s "$on_abort" | sed "s/'/''/g")"
+ fi
+ printf ' -menu -shell-script-candidates %%§
+ printf %%s %s
+ §\n' "$(shellquote "$completion")"
+ }
+}
diff --git a/autoload/tools/patch-range.pl b/autoload/tools/patch-range.pl
new file mode 100644
index 0000000..978f45c
--- /dev/null
+++ b/autoload/tools/patch-range.pl
@@ -0,0 +1,113 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+my $min_line = $ARGV[0];
+shift @ARGV;
+my $max_line = $ARGV[0];
+shift @ARGV;
+
+my $patch_cmd;
+if (defined $ARGV[0] and $ARGV[0] =~ m{^[^-]}) {
+ $patch_cmd = "@ARGV";
+} else {
+ $patch_cmd = "patch @ARGV";
+}
+my $reverse = grep /^(--reverse|-R)$/, @ARGV;
+
+my $lineno = 0;
+my $original = "";
+my $diff_header = "";
+my $wheat = "";
+my $chaff = "";
+my $state = undef;
+my $hunk_wheat = undef;
+my $hunk_chaff = undef;
+my $hunk_header = undef;
+my $hunk_remaining_lines = undef;
+my $signature = "";
+
+sub compute_hunk_header {
+ my $original_header = shift;
+ my $hunk = shift;
+ my $old_lines = 0;
+ my $new_lines = 0;
+ for (split /\n/, $hunk) {
+ $old_lines++ if m{^[ -]};
+ $new_lines++ if m{^[ +]};
+ }
+ my $updated_header = $original_header =~ s/^@@ -(\d+),\d+\s+\+(\d+),\d+ @@(.*)/@@ -$1,$old_lines +$2,$new_lines @\@$3/mr;
+ return $updated_header;
+}
+
+sub finish_hunk {
+ return unless defined $hunk_header;
+ if ($hunk_wheat =~ m{^[-+]}m) {
+ if ($diff_header) {
+ $wheat .= $diff_header;
+ $diff_header = "";
+ }
+ $wheat .= (compute_hunk_header $hunk_header, $hunk_wheat). $hunk_wheat;
+ }
+ $chaff .= (compute_hunk_header $hunk_header, $hunk_chaff) . $hunk_chaff . $signature;
+ $hunk_header = undef;
+}
+
+while (<STDIN>) {
+ ++$lineno;
+ $original .= $_;
+ if (m{^diff}) {
+ finish_hunk();
+ $state = "diff header";
+ $diff_header = "";
+ }
+ if ($state eq "signature") {
+ $signature .= $_;
+ next;
+ }
+ if (m{^@@ -\d+(?:,(\d)+)? \+\d+(?:,\d+)? @@}) {
+ $hunk_remaining_lines = $1 or 1;
+ finish_hunk();
+ $state = "diff hunk";
+ $hunk_header = $_;
+ $hunk_wheat = "";
+ $hunk_chaff = "";
+ $signature = "";
+ next;
+ }
+ if ($state eq "diff header") {
+ $diff_header .= $_;
+ $chaff .= $_;
+ next;
+ }
+ if ($hunk_remaining_lines == 0 and m{^-- $}) {
+ $state = "signature";
+ $signature .= $_;
+ next;
+ }
+ --$hunk_remaining_lines if m{^[ -]};
+ my $include = m{^ } || ($lineno >= $min_line && $lineno <= $max_line);
+ if ($include) {
+ $hunk_wheat .= $_;
+ $hunk_chaff .= $_ if m{^ };
+ if ($reverse ? m{^[-]} : m{^\+}) {
+ $hunk_chaff .= " " . substr $_, 1;
+ }
+ } else {
+ if ($reverse ? m{^\+} : m{^-}) {
+ $hunk_wheat .= " " . substr $_, 1;
+ }
+ $hunk_chaff .= $_;
+ }
+}
+finish_hunk();
+
+open PATCH_COMMAND, "|-", "$patch_cmd 1>&2" or die "patch-range.pl: error running '$patch_cmd': $!";
+print PATCH_COMMAND $wheat;
+if (not close PATCH_COMMAND) {
+ print $original;
+ print STDERR "patch-range.pl: error running:\n" . "\$ $patch_cmd << EOF\n$wheat" . "EOF\n";
+ exit 1;
+}
+print $chaff;
diff --git a/autoload/tools/patch.kak b/autoload/tools/patch.kak
new file mode 100644
index 0000000..a481ff4
--- /dev/null
+++ b/autoload/tools/patch.kak
@@ -0,0 +1,63 @@
+define-command patch -params .. -docstring %{
+ patch [<arguments>]: apply selections in diff to a file
+
+ Given some selections within a unified diff, apply the changed lines in
+ each selection by piping them to "patch <arguments> 1>&2"
+ (or "<arguments> 1>&2" if <arguments> starts with a non-option argument).
+ If successful, the in-buffer diff will be updated to reflect the applied
+ changes.
+ For selections that contain no newline, the entire enclosing diff hunk
+ is applied (unless the cursor is inside a diff header, in which case
+ the entire diff is applied).
+ To revert changes, <arguments> must contain "--reverse" or "-R".
+} %{
+ evaluate-commands -draft -itersel -save-regs aes|^ %{
+ try %{
+ execute-keys <a-k>\n<ret>
+ } catch %{
+ # The selection contains no newline.
+ execute-keys -save-regs '' Z
+ execute-keys <a-l><semicolon><a-?>^diff<ret>
+ try %{
+ execute-keys <a-k>^@@<ret>
+ # If the cursor is in a diff hunk, stage the entire hunk.
+ execute-keys z
+ execute-keys /.*?(?:(?=\n@@)|(?=\ndiff)|(?=\n\n)|\z)<ret>x<semicolon><a-?>^@@<ret>
+ } catch %{
+ # If the cursor is in a diff header, stage the entire diff.
+ execute-keys <a-semicolon>?.*?(?:(?=\ndiff)|(?=\n\n)|\z)<ret>
+ }
+ }
+ # We want to apply only the selected lines. Remember them.
+ execute-keys <a-:>
+ set-register s %val{selection_desc}
+ # Select forward until the end of the last hunk.
+ execute-keys H?.*?(?:(?=\n@@)|(?=\ndiff)|(?=\n\n)|\z)<ret>x
+ # Select backward to the beginning of the first hunk's diff header.
+ execute-keys <a-semicolon><a-L><a-?>^diff<ret>
+ # Move cursor to the beginning so we know the diff's offset within the buffer.
+ execute-keys <a-:><a-semicolon>
+ set-register a %arg{@}
+ set-register e nop
+ set-register | %{
+ # The selected range to apply.
+ IFS=' .,' read min_line _ max_line _ <<-EOF
+ $kak_reg_s
+ EOF
+ min_line=$((min_line - kak_cursor_line + 1))
+ max_line=$((max_line - kak_cursor_line + 1))
+
+ # Since registers are never empty, we get an empty arg even if
+ # there were no args. This does no harm because we pass it to
+ # a shell where it expands to nothing.
+ eval set -- "$kak_quoted_reg_a"
+
+ perl "${kak_runtime}"/rc/tools/patch-range.pl $min_line $max_line "$@" ||
+ echo >$kak_command_fifo "set-register e fail 'patch: failed to apply selections, see *debug* buffer'"
+ }
+ execute-keys |<ret>
+ %reg{e}
+ }
+}
+
+provide-module patch %§§
diff --git a/autoload/tools/python/jedi.kak b/autoload/tools/python/jedi.kak
new file mode 100644
index 0000000..82d799a
--- /dev/null
+++ b/autoload/tools/python/jedi.kak
@@ -0,0 +1,77 @@
+hook -once global BufSetOption filetype=python %{
+ require-module jedi
+}
+
+provide-module jedi %{
+
+declare-option -hidden str jedi_tmp_dir
+declare-option -hidden completions jedi_completions
+declare-option -docstring "colon separated list of path added to `python`'s $PYTHONPATH environment variable" \
+ str jedi_python_path
+
+define-command jedi-complete -docstring "Complete the current selection" %{
+ evaluate-commands %sh{
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-jedi.XXXXXXXX)
+ mkfifo ${dir}/fifo
+ printf %s\\n "set-option buffer jedi_tmp_dir ${dir}"
+ printf %s\\n "evaluate-commands -no-hooks write -sync ${dir}/buf"
+ }
+ evaluate-commands %sh{
+ dir=${kak_opt_jedi_tmp_dir}
+ printf %s\\n "evaluate-commands -draft %{ edit! -fifo ${dir}/fifo *jedi-output* }"
+ ((
+ trap - INT QUIT
+ cd $(dirname ${kak_buffile})
+
+ export PYTHONPATH="$kak_opt_jedi_python_path:$PYTHONPATH"
+ python 2> "${dir}/fifo" -c 'if 1:
+ import os
+ dir = os.environ["kak_opt_jedi_tmp_dir"]
+ buffile = os.environ["kak_buffile"]
+ line = int(os.environ["kak_cursor_line"])
+ column = int(os.environ["kak_cursor_column"])
+ timestamp = os.environ["kak_timestamp"]
+ client = os.environ["kak_client"]
+ pipe_escape = lambda s: s.replace("|", "\\|")
+ def quote(s):
+ c = chr(39) # single quote
+ return c + s.replace(c, c+c) + c
+ import jedi
+ script = jedi.Script(code=open(dir + "/buf", "r").read(), path=buffile)
+ completions = (
+ quote(
+ pipe_escape(str(c.name)) + "|" +
+ pipe_escape("info -style menu -- " + quote(c.docstring())) + "|" +
+ pipe_escape(str(c.name))
+ )
+ for c in script.complete(line=line, column=column-1)
+ )
+ header = str(line) + "." + str(column) + "@" + timestamp
+ cmds = [
+ "echo completed",
+ " ".join(("set-option", quote("buffer=" + buffile), "jedi_completions", header, *completions)),
+ ]
+ print("evaluate-commands -client", quote(client), quote("\n".join(cmds)))
+ ' | kak -p "${kak_session}"
+ rm -r ${dir}
+ ) & ) > /dev/null 2>&1 < /dev/null
+ }
+}
+
+define-command jedi-enable-autocomplete -docstring "Add jedi completion candidates to the completer" %{
+ set-option window completers option=jedi_completions %opt{completers}
+ hook window -group jedi-autocomplete InsertIdle .* %{ try %{
+ execute-keys -draft <a-h><a-k>\..\z<ret>
+ echo 'completing...'
+ jedi-complete
+ } }
+ alias window complete jedi-complete
+}
+
+define-command jedi-disable-autocomplete -docstring "Disable jedi completion" %{
+ set-option window completers %sh{ printf %s\\n "'${kak_opt_completers}'" | sed -e 's/option=jedi_completions://g' }
+ remove-hooks window jedi-autocomplete
+ unalias window complete jedi-complete
+}
+
+}
diff --git a/autoload/tools/rust/racer.kak b/autoload/tools/rust/racer.kak
new file mode 100644
index 0000000..ed85e3c
--- /dev/null
+++ b/autoload/tools/rust/racer.kak
@@ -0,0 +1,123 @@
+hook -once global BufSetOption filetype=rust %{
+ require-module racer
+}
+
+provide-module racer %{
+
+declare-option -hidden str racer_tmp_dir
+declare-option -hidden completions racer_completions
+
+define-command racer-complete -docstring "Complete the current selection with racer" %{
+ evaluate-commands %sh{
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-racer.XXXXXXXX)
+ printf %s\\n "set-option buffer racer_tmp_dir ${dir}"
+ printf %s\\n "evaluate-commands -no-hooks %{ write ${dir}/buf }"
+ }
+ evaluate-commands %sh{
+ dir=${kak_opt_racer_tmp_dir}
+ (
+ trap - INT QUIT
+ cursor="${kak_cursor_line} $((${kak_cursor_column} - 1))"
+ racer_data=$(racer --interface tab-text complete-with-snippet ${cursor} ${kak_buffile} ${dir}/buf)
+ compl=$(printf %s\\n "${racer_data}" | awk '
+ BEGIN { FS = "\t"; ORS = " " }
+ /^PREFIX/ {
+ column = ENVIRON["kak_cursor_column"] + $2 - $3
+ print ENVIRON["kak_cursor_line"] "." column "@@" ENVIRON["kak_timestamp"]
+ }
+ /^MATCH/ {
+ word = $2
+ desc = substr($9, 2, length($9) - 2)
+ gsub(/\|/, "\\|", desc)
+ gsub(/\\n/, "\n", desc)
+ gsub(/!/, "!!", desc)
+ info = $8
+ gsub(/\|/, "\\|", info)
+
+ candidate = word "|info -style menu %!" desc "!|" word " {MenuInfo}" info
+
+ gsub(/@/, "@@", candidate)
+ gsub(/~/, "~~", candidate)
+ print "%~" candidate "~"
+ }'
+ )
+ printf %s\\n "evaluate-commands -client '${kak_client}' %@ set-option 'buffer=${kak_bufname}' racer_completions ${compl%?} @" | kak -p ${kak_session}
+ rm -r ${dir}
+ ) > /dev/null 2>&1 < /dev/null &
+ }
+}
+
+define-command racer-enable-autocomplete -docstring "Add racer completion candidates to the completer" %{
+ set-option window completers option=racer_completions %opt{completers}
+ hook window -group racer-autocomplete InsertIdle .* %{ try %{
+ execute-keys -draft <a-h><a-k>([\w\.]|::).\z<ret>
+ racer-complete
+ } }
+ alias window complete racer-complete
+}
+
+define-command racer-disable-autocomplete -docstring "Disable racer completion" %{
+ evaluate-commands %sh{ printf "set-option window completers %s\n" $(printf %s "${kak_opt_completers}" | sed -e "s/'option=racer_completions'//g") }
+ remove-hooks window racer-autocomplete
+ unalias window complete racer-complete
+}
+
+define-command racer-go-definition -docstring "Jump to where the rust identifier below the cursor is defined" %{
+ evaluate-commands %sh{
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-racer.XXXXXXXX)
+ printf %s\\n "set-option buffer racer_tmp_dir ${dir}"
+ printf %s\\n "evaluate-commands -no-hooks %{ write ${dir}/buf }"
+ }
+ evaluate-commands %sh{
+ dir=${kak_opt_racer_tmp_dir}
+ cursor="${kak_cursor_line} $((${kak_cursor_column} - 1))"
+ racer_data=$(racer --interface tab-text find-definition ${cursor} "${kak_buffile}" "${dir}/buf" | head -n 1)
+
+ racer_match=$(printf %s\\n "$racer_data" | cut -f1 )
+ if [ "$racer_match" = "MATCH" ]; then
+ racer_line=$(printf %s\\n "$racer_data" | cut -f3 )
+ racer_column=$(printf %s\\n "$racer_data" | cut -f4 )
+ racer_file=$(printf %s\\n "$racer_data" | cut -f5 )
+ printf %s\\n "edit -existing '$racer_file' $racer_line $racer_column"
+ case ${racer_file} in
+ "${RUST_SRC_PATH}"* | "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*)
+ printf %s\\n "set-option buffer readonly true";;
+ esac
+ else
+ printf %s\\n "echo -debug 'racer could not find a definition'"
+ fi
+ }
+}
+
+define-command racer-show-doc -docstring "Show the documentation about the rust identifier below the cursor" %{
+ evaluate-commands %sh{
+ dir=$(mktemp -d "${TMPDIR:-/tmp}"/kak-racer.XXXXXXXX)
+ printf %s\\n "set-option buffer racer_tmp_dir ${dir}"
+ printf %s\\n "evaluate-commands -no-hooks %{ write ${dir}/buf }"
+ }
+ evaluate-commands %sh{
+ dir=${kak_opt_racer_tmp_dir}
+ cursor="${kak_cursor_line} ${kak_cursor_column}"
+ racer_data=$(racer --interface tab-text complete-with-snippet ${cursor} "${kak_buffile}" "${dir}/buf" | sed -n 2p )
+ racer_match=$(printf %s\\n "$racer_data" | cut -f1)
+ if [ "$racer_match" = "MATCH" ]; then
+ racer_doc=$(
+ printf %s\\n "$racer_data" |
+ cut -f9 |
+ sed -e '
+
+ # Remove leading and trailing quotes
+ s/^"\(.*\)"$/\1/g
+
+ # Escape all @ so that it can be properly used in the string expansion
+ s/@/\\@/g
+
+ ')
+ printf "info %%@$racer_doc@"
+ else
+ printf %s\\n "echo -debug 'racer could not find a definition'"
+ fi
+ }
+}
+
+}
diff --git a/autoload/tools/spell.kak b/autoload/tools/spell.kak
new file mode 100644
index 0000000..4bd3305
--- /dev/null
+++ b/autoload/tools/spell.kak
@@ -0,0 +1,184 @@
+declare-option -hidden range-specs spell_regions
+declare-option -hidden str spell_last_lang
+
+declare-option -docstring "default language to use when none is passed to the spell-check command" str spell_lang
+
+define-command -params ..1 -docstring %{
+ spell [<language>]: spell check the current buffer
+
+ The first optional argument is the language against which the check will be performed (overrides `spell_lang`)
+ Formats of language supported:
+ - ISO language code, e.g. 'en'
+ - language code above followed by a dash or underscore with an ISO country code, e.g. 'en-US'
+ } spell %{
+ try %{ add-highlighter window/ ranges 'spell_regions' }
+ evaluate-commands %sh{
+ use_lang() {
+ if ! printf %s "$1" | grep -qE '^[a-z]{2,3}([_-][A-Z]{2})?$'; then
+ echo "fail 'Invalid language code (examples of expected format: en, en_US, en-US)'"
+ exit 1
+ else
+ options="-l '$1'"
+ printf 'set-option buffer spell_last_lang %s\n' "$1"
+ fi
+ }
+
+ if [ $# -ge 1 ]; then
+ use_lang "$1"
+ elif [ -n "${kak_opt_spell_lang}" ]; then
+ use_lang "${kak_opt_spell_lang}"
+ fi
+
+ printf 'eval -no-hooks write %s\n' "${kak_response_fifo}" > $kak_command_fifo
+
+ {
+ trap - INT QUIT
+ sed 's/^/^/' | eval "aspell --byte-offsets -a $options" 2>&1 | awk '
+ BEGIN {
+ line_num = 1
+ regions = ENVIRON["kak_timestamp"]
+ server_command = sprintf("kak -p \"%s\"", ENVIRON["kak_session"])
+ }
+
+ {
+ if (/^@\(#\)/) {
+ # drop the identification message
+ }
+
+ else if (/^\*/) {
+ # nothing
+ }
+
+ else if (/^[+-]/) {
+ # required to ignore undocumented aspell functionality
+ }
+
+ else if (/^$/) {
+ line_num++
+ }
+
+ else if (/^[#&]/) {
+ word_len = length($2)
+ word_pos = substr($0, 1, 1) == "&" ? substr($4, 1, length($4) - 1) : $3;
+ regions = regions " " line_num "." word_pos "+" word_len "|DiagnosticError"
+ }
+
+ else {
+ line = $0
+ gsub(/"/, "&&", line)
+ command = "fail \"" line "\""
+ exit
+ }
+ }
+
+ END {
+ if (!length(command))
+ command = "set-option \"buffer=" ENVIRON["kak_bufname"] "\" spell_regions " regions
+
+ print command | server_command
+ close(server_command)
+ }
+ '
+ } <$kak_response_fifo >/dev/null 2>&1 &
+ }
+}
+
+define-command spell-clear %{
+ unset-option buffer spell_regions
+}
+
+define-command spell-next %{ evaluate-commands %sh{
+ anchor_line="${kak_selection_desc%%.*}"
+ anchor_col="${kak_selection_desc%%,*}"
+ anchor_col="${anchor_col##*.}"
+
+ start_first="${kak_opt_spell_regions%%|*}"
+ start_first="${start_first#* }"
+
+ # Make sure properly formatted selection descriptions are in `%opt{spell_regions}`
+ if ! printf %s "${start_first}" | grep -qE '^[0-9]+\.[0-9]+,[0-9]+\.[0-9]+$'; then
+ exit
+ fi
+
+ printf %s "${kak_opt_spell_regions#* }" | awk -v start_first="${start_first}" \
+ -v anchor_line="${anchor_line}" \
+ -v anchor_col="${anchor_col}" '
+ BEGIN {
+ anchor_line = int(anchor_line)
+ anchor_col = int(anchor_col)
+ }
+
+ {
+ for (i = 1; i <= NF; i++) {
+ sel = $i
+ sub(/\|.+$/, "", sel)
+
+ start_line = sel
+ sub(/\..+$/, "", start_line)
+ start_line = int(start_line)
+
+ start_col = sel
+ sub(/,.+$/, "", start_col)
+ sub(/^.+\./, "", start_col)
+ start_col = int(start_col)
+
+ if (start_line < anchor_line \
+ || (start_line == anchor_line && start_col <= anchor_col))
+ continue
+
+ target_sel = sel
+ break
+ }
+ }
+
+ END {
+ if (!target_sel)
+ target_sel = start_first
+
+ printf "select %s\n", target_sel
+ }'
+} }
+
+define-command \
+ -docstring "Suggest replacement words for the current selection, against the last language used by the spell-check command" \
+ spell-replace %{
+ prompt \
+ -shell-script-candidates %{
+ options=""
+ if [ -n "$kak_opt_spell_last_lang" ]; then
+ options="-l '$kak_opt_spell_last_lang'"
+ fi
+ printf %s "$kak_selection" |
+ eval "aspell -a $options" |
+ sed -n -e '/^&/ { s/^[^:]*: //; s/, /\n/g; p;}'
+ } \
+ "Replace with: " \
+ %{
+ evaluate-commands -save-regs a %{
+ set-register a %val{text}
+ execute-keys c <c-r>a <esc>
+ }
+ }
+}
+
+
+define-command -params 0.. \
+ -docstring "Add the current selection to the dictionary" \
+ spell-add %{ evaluate-commands %sh{
+ options=""
+ if [ -n "$kak_opt_spell_last_lang" ]; then
+ options="-l '$kak_opt_spell_last_lang'"
+ fi
+ if [ $# -eq 0 ]; then
+ # use selections
+ eval set -- "$kak_quoted_selections"
+ fi
+ while [ $# -gt 0 ]; do
+ word="$1"
+ if ! printf '*%s\n#\n' "${word}" | eval "aspell -a $options" >/dev/null; then
+ printf 'fail "Unable to add word: %s"' "$(printf %s "${word}" | sed 's/"/&&/g')"
+ exit 1
+ fi
+ shift
+ done
+}}