summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxime Coste <mawww@kakoune.org>2020-02-16 10:39:04 +1100
committerMaxime Coste <mawww@kakoune.org>2020-02-16 10:39:04 +1100
commitaf7091f5734ea6951b4ccfeba2f66650e0054319 (patch)
treeff41d460b30144dd20b26a0808ebfa35850d4f4d
parentdf4f71aaed0ef7a9dcba6e80201d94f2fe3c1615 (diff)
parent92771216954f6522772d6d03c6438e89a51dd301 (diff)
Merge remote-tracking branch 'Screwtapello/lint-selection'
-rw-r--r--rc/tools/lint.kak469
1 files changed, 373 insertions, 96 deletions
diff --git a/rc/tools/lint.kak b/rc/tools/lint.kak
index a0accd08..3774c365 100644
--- a/rc/tools/lint.kak
+++ b/rc/tools/lint.kak
@@ -1,113 +1,327 @@
-declare-option -docstring %{
- shell command to which the path of a copy of the current buffer will be passed
- The output returned by this command is expected to comply with the following format:
- {filename}:{line}:{column}: {kind}: {message}
-} str lintcmd
+declare-option \
+ -docstring %{
+ The shell command used by lint-buffer and lint-selections.
+
+ It will be given the path to a file containing the text to be
+ linted, and must produce output in the 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.
+ } \
+ str lintcmd
declare-option -hidden line-specs lint_flags
-declare-option -hidden range-specs lint_errors
+declare-option -hidden line-specs lint_messages
declare-option -hidden int lint_error_count
declare-option -hidden int lint_warning_count
-define-command lint -docstring 'Parse the current buffer with a linter' %{
+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 \
+%{
+ # Clear the current contents of the various options.
+ set-option buffer lint_flags %val{timestamp}
+ set-option buffer lint_messages %val{timestamp}
+ set-option buffer lint_error_count 0
+ set-option buffer lint_warning_count 0
+
+ # Create a temporary directory to keep all our state.
evaluate-commands %sh{
- if [ -z "${kak_opt_lintcmd}" ]; then
- echo 'fail The `lintcmd` option is not set'
- exit 1
- fi
+ # 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)
- mkfifo "$dir"/fifo
- printf '%s\n' "evaluate-commands -no-hooks write -sync $dir/${filename}"
+ # A fifo to send the results back to a Kakoune buffer.
+ # FIXME: Should we put the lint output in toolsclient?
+ mkfifo "$dir"/fifo
printf '%s\n' "evaluate-commands -draft %{
- edit! -fifo $dir/fifo -debug *lint-output*
+ edit! -fifo $(kakquote "$dir/fifo") -debug *lint-output*
set-option buffer filetype make
set-option buffer make_current_error_line 0
- hook -always -once buffer BufCloseFifo .* %{ nop %sh{ rm -r '$dir' } }
}"
+ # 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
- eval "$kak_opt_lintcmd '$dir'/${filename}" | sort -t: -k2,2 -n > "$dir"/stderr
+ 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 client="$kak_client" '
+ function kakquote(text) {
+ # \x27 is apostrophe, escaped for shell-quoting reasons.
+ gsub(/\x27/, "\x27\x27", text)
+ return "\x27" text "\x27"
+ }
- # Flags for the gutter:
- # stamp l3|{red}█ l11|{yellow}█
- # Contextual error messages:
- # stamp 'l1.c1,l1.c1|kind:message' 'l2.c2,l2.c2|kind:message'
- awk -F: -v file="$kak_buffile" -v stamp="$kak_timestamp" -v client="$kak_client" '
BEGIN {
+ OFS=":"
+ FS=":"
error_count = 0
warning_count = 0
}
- /:[1-9][0-9]*:[1-9][0-9]*: ([Ff]atal )?[Ee]rror/ {
- flags = flags " " $2 "|{red}█"
- error_count++
- }
+
/:[1-9][0-9]*:[1-9][0-9]*:/ {
- if ($4 !~ /[Ee]rror/) {
- flags = flags " " $2 "|{yellow}█"
+ # 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++
}
- }
- /:[1-9][0-9]*:[1-9][0-9]*:/ {
- kind = substr($4, 2)
- error = $2 "." $3 "," $2 "." $3 "|" kind
- msg = ""
- # fix case where $5 is not the last field because of extra colons in the message
+
+ # 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)
- gsub("'\''", "'"''"'", msg)
- error = error msg " (col " $3 ")"
- errors = errors " '\''" error "'\''"
+
+ 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 {
- print "set-option \"buffer=" file "\" lint_flags " stamp flags
- gsub("~", "\\~", errors)
- print "set-option \"buffer=" file "\" lint_errors " stamp errors
- print "set-option \"buffer=" file "\" lint_error_count " error_count
- print "set-option \"buffer=" file "\" lint_warning_count " warning_count
- print "evaluate-commands -client " client " lint-show-counters"
- }
- ' "$dir"/stderr | kak -p "$kak_session"
+ for (line in flags_by_line) {
+ flag = flags_by_line[line]
+
+ print "set-option -add " \
+ kakquote("buffer=" file) " " \
+ "lint_flags " \
+ kakquote(line "|" flag)
+ }
+
+ for (line in messages_by_line) {
+ msg = messages_by_line[line]
+
+ print "set-option -add " \
+ kakquote("buffer=" file) " " \
+ "lint_messages " \
+ kakquote(line "|" msg)
+ }
- cut -d: -f2- "$dir"/stderr | awk -v bufname="${kak_bufname}" '
- /^[1-9][0-9]*:[1-9][0-9]*:/ {
- print bufname ":" $0
+ print "set-option " \
+ kakquote("buffer=" file) " " \
+ "lint_error_count " \
+ error_count
+ print "set-option " \
+ kakquote("buffer=" file) " " \
+ "lint_warning_count " \
+ warning_count
}
- ' > "$dir"/fifo
+ ' "$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-counters" \
+ "$kak_client"
+ fi | kak -p "$kak_session"
- } & ) >/dev/null 2>&1 </dev/null
+ # We are done here. Send the results to Kakoune,
+ # and clean up.
+ cat "$dir"/result > "$dir"/fifo
+ 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.
+ } \
+ 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.
+
+ Set the lintcmd option to control which linter is used.
+ } \
+ lint-buffer \
+%{
+ evaluate-commands -draft %{
+ execute-keys '%'
+ lint-cleaned-selections %opt{lintcmd}
+ }
+}
+
+alias global lint lint-buffer
+
define-command -hidden lint-show %{
- update-option buffer lint_errors
+ update-option buffer lint_messages
evaluate-commands %sh{
- eval "set -- ${kak_quoted_opt_lint_errors}"
- shift
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
- s=""
- for i in "$@"; do
- s="${s}
-${i}"
- done
+ eval set -- "${kak_quoted_opt_lint_messages}"
+ shift # skip the timestamp
- printf %s\\n "${s}" | awk -v line="${kak_cursor_line}" \
- -v column="${kak_cursor_column}" \
- "/^${kak_cursor_line}\./"' {
- gsub(/"|%/, "&&")
- msg = substr($0, index($0, "|"))
- sub(/^[^ \t]+[ \t]+/, "", msg)
- printf "info -anchor %d.%d \"%s\"\n", line, column, msg
- }'
+ 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:{red} %opt{lint_error_count} error(s){yellow} %opt{lint_warning_count} warning(s)
+ echo -markup "linting results: {Error} %opt{lint_error_count} error(s) {Information} %opt{lint_warning_count} warning(s) "
}
define-command lint-enable -docstring "Activate automatic diagnostics of the code" %{
@@ -121,54 +335,117 @@ define-command lint-disable -docstring "Disable automatic diagnostics of the cod
remove-hooks window lint-diagnostics
}
-define-command lint-next-error -docstring "Jump to the next line that contains an error" %{
- update-option buffer lint_errors
+# 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{
- eval "set -- ${kak_quoted_opt_lint_errors}"
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ eval "set -- ${kak_quoted_opt_lint_messages}"
shift
- for i in "$@"; do
- candidate="${i%%|*}"
- if [ "${candidate%%.*}" -gt "${kak_cursor_line}" ]; then
- range="${candidate}"
- break
+ 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
- range="${range-${1%%|*}}"
- if [ -n "${range}" ]; then
- printf 'select %s\n' "${range}"
- else
- echo 'fail no lint diagnostics'
- fi
+ # 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"
+
}
}
-define-command lint-previous-error -docstring "Jump to the previous line that contains an error" %{
- update-option buffer lint_errors
+# 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{
- eval "set -- ${kak_quoted_opt_lint_errors}"
+ # This is going to come in handy later.
+ kakquote() { printf "%s" "$*" | sed "s/'/''/g; 1s/^/'/; \$s/\$/'/"; }
+
+ eval "set -- ${kak_quoted_opt_lint_messages}"
shift
- for i in "$@"; do
- candidate="${i%%|*}"
+ if [ "$#" -eq 0 ]; then
+ printf 'fail no lint messages'
+ exit
+ fi
- if [ "${candidate%%.*}" -ge "${kak_cursor_line}" ]; then
- range="${last_candidate}"
- break
+ 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
- last_candidate="${candidate}"
+ # We have not yet reached the cursor position, stash this message
+ # and try the next.
+ prev_lineno="$lineno"
+ prev_msg="$msg"
done
- if [ $# -ge 1 ]; then
- shift $(($# - 1))
- range="${range:-${1%%|*}}"
- printf 'select %s\n' "${range}"
- else
- echo 'fail no lint diagnostics'
- fi
+ # 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"
}
}