-
Notifications
You must be signed in to change notification settings - Fork 715
Lint
A lint.kak rc script is provided by default in Kakoune.
Its goal is to run an external linter program asynchronously and parse its output, to:
-
List code diagnostics (warning, errors, other) in a new
*lint-output*
buffer -
Add colored flags in the gutter column for each line that requires your attention
-
Display diagnostic messages and notes in inline information popups on the relevant lines
In order to make the lint operations work, you must instruct Kakoune
how to run your external linter by setting the lintcmd
option.
Kakoune expects a linter to output diagnostics in a certain format on stdout. If messages are printed on stderr, they won’t be read by the editor. The expected output format is documented in lint.kak:
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
You can set a different linter for any specific languages, using hooks. For
example, for JavaScript, you can append the following snippet to your
kakrc
:
hook global WinSetOption filetype=javascript %{
set-option window lintcmd 'eslint -f ~/eslint-kakoune.js'
}
In the above code, we set eslint as our
lintcmd
command. In order for eslint
to output diagnostics
that can be understood by kakoune, we wrap the utility using a
custom formatter.
Make also sure that eventual formatting capabilities of the linter are disabled.
The purpose of a linting tool within the context of the :lint
command is to
focus on reporting errors and warnings, it is therefore not expected to modify the buffer.
The following sections document values for the lintcmd
option that work
with different linters.
The lint.kak
script also offers the following commands:
# main linting functions
def lint-buffer -docstring "Check the current buffer with a linter"
def lint-selections -docstring "Check each selection with a linter"
alias global lint lint-buffer
# switches
def lint-enable -docstring "Activate automatic diagnostics of the code"
def lint-disable -docstring "Disable automatic diagnostics of the code"
# navigation commands
def lint-next-message -docstring "Jump to the next line that contains a lint message"
def lint-prev-message -docstring "Jump to the previous line that contains a lint message"
hook global WinSetOption filetype=asciidoc %{
set-option window lintcmd "proselint"
}
Note: Uses bazel_build filetype as defined in kakoune-extra-filetypes
hook global WinSetOption filetype=(bazel_build) %{
set-option window lintcmd %{ run() { cat $1 | buildifier -mode check -lint warn -format json $kak_buffile | jq -r '.files | .[0] | .warnings | .[] | "\(.start.line):\(.start.column): warning: \(.message)"' | sed -e "s|^|$kak_buffile:|" ; } && run }
}
hook global WinSetOption filetype=c %{
set-option window lintcmd "cppcheck --language=c --enable=warning,style,information --template='{file}:{line}:{column}: {severity}: {message}' --suppress='*:*.h' 2>&1"
}
hook global WinSetOption filetype=cpp %{
set-option window lintcmd "cppcheck --language=c++ --enable=warning,style,information --template='{file}:{line}:{column}: {severity}: {message}' --suppress='*:*.h' --suppress='*:*.hh' 2>&1"
}
Using this linter requires the clj-kakoune-joker wrapper.
hook global WinSetOption filetype=clojure %{
set-option window lintcmd "clj-kj.sh"
}
hook global WinSetOption filetype=css %{
set-option window lintcmd "stylelint --fix --stdin-filename='%val{buffile}'"
}
hook global WinSetOption filetype=d %{
set-option window lintcmd "dscanner -S --errorFormat '{filepath}:{line}:{column}: {type}: {message}'"
}
Requires credo
to be a dependency of your mix project. E.g. like so:
# mix.exs
defp deps do
[
# ...
{:credo, "~> 1.4", only: :dev},
# ...
]
end
hook global WinSetOption filetype=elixir %{
# NOTE: The `Elixir.CredoNaming.Check.Consistency.ModuleFilename` rule is
# not supported because Kakoune moves the file to a temporary directory
# before linting.
set-option window lintcmd "mix credo list --config-file=.credo.exs --format=flycheck --ignore-checks='Elixir.CredoNaming.Check.Consistency.ModuleFilename'"
}
hook global WinSetOption filetype=html %{
set-option window lintcmd "tidy -e --gnu-emacs yes --quiet yes 2>&1"
}
hook global WinSetOption filetype=javascript %{
set-option window lintcmd 'run() { cat "$1" | npx eslint -f unix --stdin --stdin-filename "$kak_buffile";} && run '
# using npx to run local eslint over global
# formatting with prettier `npm i prettier --save-dev`
set-option window formatcmd 'npx prettier --stdin-filepath=${kak_buffile}'
alias window fix format2 # the patched version, renamed to `format2`.
lint-enable
}
# Formatting with eslint:
define-command eslint-fix %{
evaluate-commands -draft -no-hooks -save-regs '|' %sh{
path_file_tmp=$(mktemp kak-formatter-XXXXXX)
printf %s\\n "write -sync \"${path_file_tmp}\"
nop %sh{ npx eslint --fix \"${path_file_tmp}\" }
execute-keys '%|cat<space>$path_file_tmp<ret>'
nop %sh{rm -f "${path_file_tmp}"}
"
}
}
The above formatting command copies the contents of the buffer into a temporary file outside your project directory which may lead to wrong settings for eslint
. If you have jq available in your shell (or another JSON processor), you can do the following instead. It pipes the buffer into eslint
on stdin
and replaces it with the fixed output from eslint
:
define-command format-eslint -docstring %{
Formats the current buffer using eslint.
Respects your local project setup in eslintrc.
} %{
evaluate-commands -draft -no-hooks -save-regs '|' %{
# Select all to format
execute-keys '%'
# eslint does a fix-dry-run with a json formatter which results in a JSON output to stdout that includes the fixed file.
# jq then extracts the fixed file output from the JSON. -j returns the raw output without any escaping.
set-register '|' %{
format_out="$(mktemp)"
cat | \
npx eslint --format json \
--fix-dry-run \
--stdin \
--stdin-filename "$kak_buffile" | \
jq -j ".[].output" > "$format_out"
if [ $? -eq 0 ] && [ $(wc -c < "$format_out") -gt 4 ]; then
cat "$format_out"
else
printf 'eval -client %s %%{ fail eslint formatter returned an error %s }\n' "$kak_client" "$?" | kak -p "$kak_session"
printf "%s" "$kak_quoted_selection"
fi
rm -f "$format_out"
}
# Replace all with content from register:
execute-keys '|<ret>'
}
}
hook global WinSetOption filetype=markdown %{
set-option window lintcmd "proselint"
}
hook global WinSetOption filetype=perl %{
set-option window lintcmd %{ perlcritic --quiet --verbose '%f:%l:%c: severity %s: %m [%p]\n'"
}
perlcritic doesn’t necessarily have the criteria of "warning" or "error".
Instead, things it points out are given severity numbers. lint.kak
will classify all of these as
warnings, since "error:" doesn’t exist in the line.
You might wish to change its output to distinguish between warnings and errors with sed.
hook global WinSetOption filetype=perl %{
set-option window lintcmd %{ \
pc() { \
perlcritic --quiet --verbose '%f:%l:%c: severity %s: %m [%p]\n' "$1" \
| sed \
-e '/: severity 5:/ s/: severity 5:/: error:/' \
-e '/: severity [0-4]:/ s/: severity [0-4]:/: warning:/'; \
} && pc \
}
}
hook global WinSetOption filetype=python %{
set-option window lintcmd %{ run() { pylint --msg-template='{path}:{line}:{column}: {category}: {msg_id}: {msg} ({symbol})' "$1" | awk -F: 'BEGIN { OFS=":" } { if (NF == 6) { $3 += 1; print } }'; } && run }
}
where we needed to modify pylint
output format to use 1-based columns and emit an error category that is parseable by lint.kak
.
hook global WinSetOption filetype=python %{
set-option window lintcmd "flake8 --filename='*' --format='%%(path)s:%%(row)d:%%(col)d: error: %%(text)s' --ignore=E121,E123,E126,E226,E24,E704,W503,W504,E501,E221,E127,E128,E129,F405"
}
When using Ruby version management, it might be convenient to set link to rubocop in /usr/local/bin/
like so sudo ln -s path/to/rubocop /usr/local/bin/rubocop
. If using rvm, use wrappers
folder to locate rubocop binary: ~/.rvm/gems/ruby-x.x.x/wrappers/rubocop
hook global WinSetOption filetype=ruby %{
set-option window lintcmd 'rubocop -l --format emacs'
}
hook global WinSetOption filetype=sh %{
set-option window lintcmd "shellcheck -fgcc -Cnever"
}
hook global WinSetOption filetype=swift %{
set-option window lintcmd "swiftlint --quiet --path"
}
# yaml linter hook global WinSetOption filetype=yaml %{
# set-option window lintcmd "yamllint -f parsable"
set-option window lintcmd %{
run() {
# change [message-type] to message-type:
yamllint -f parsable "$1" | sed 's/ \[\(.*\)\] / \1: /'
} && run }
}
- Normal mode commands
- Avoid the escape key
- Implementing user mode (Leader key)
- Kakoune explain
- Kakoune TV