diff options
author | thing1 <thing1@seacrossedlovers.xyz> | 2025-01-28 09:14:32 +0000 |
---|---|---|
committer | thing1 <thing1@seacrossedlovers.xyz> | 2025-01-28 09:14:32 +0000 |
commit | 904cec3c4a329cf89fc3219d359239910d61f3f6 (patch) | |
tree | 8d113899921dfbaca0e77c49ab5fc827362d1091 /autoload/tools |
Diffstat (limited to 'autoload/tools')
-rw-r--r-- | autoload/tools/autorestore.asciidoc | 13 | ||||
-rw-r--r-- | autoload/tools/autorestore.kak | 93 | ||||
-rw-r--r-- | autoload/tools/autowrap.kak | 50 | ||||
-rw-r--r-- | autoload/tools/clang.kak | 196 | ||||
-rw-r--r-- | autoload/tools/comment.kak | 218 | ||||
-rw-r--r-- | autoload/tools/ctags.kak | 168 | ||||
-rw-r--r-- | autoload/tools/doc.asciidoc | 45 | ||||
-rw-r--r-- | autoload/tools/doc.kak | 195 | ||||
-rw-r--r-- | autoload/tools/format.kak | 38 | ||||
-rw-r--r-- | autoload/tools/git.kak | 787 | ||||
-rw-r--r-- | autoload/tools/go/gopls.kak | 98 | ||||
-rw-r--r-- | autoload/tools/grep.kak | 66 | ||||
-rw-r--r-- | autoload/tools/jump.kak | 70 | ||||
-rw-r--r-- | autoload/tools/lint.asciidoc | 26 | ||||
-rw-r--r-- | autoload/tools/lint.kak | 452 | ||||
-rw-r--r-- | autoload/tools/make.kak | 92 | ||||
-rw-r--r-- | autoload/tools/man.kak | 139 | ||||
-rw-r--r-- | autoload/tools/menu.kak | 85 | ||||
-rw-r--r-- | autoload/tools/patch-range.pl | 113 | ||||
-rw-r--r-- | autoload/tools/patch.kak | 63 | ||||
-rw-r--r-- | autoload/tools/python/jedi.kak | 77 | ||||
-rw-r--r-- | autoload/tools/rust/racer.kak | 123 | ||||
-rw-r--r-- | autoload/tools/spell.kak | 184 |
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 +}} |