Skip to content
Jonas Cosandey edited this page Jul 21, 2023 · 36 revisions


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 --format unix'

In the above code, we set eslint as our lintcmd command. In order for eslint to output diagnostics that can be understood by kakoune, use set the outputted format option to unix

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 ""



hook global WinSetOption filetype=css %{
    set-option window lintcmd "npx stylelint --formatter unix --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},
    # ...
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'"



Glimmer .gjs/.gts

eslint-plugin-ember is required for eslint to work. To lint both the js code and the template code in the file:

hook global WinSetOption filetype=gjs %{
    set buffer lintcmd 'run () { cat "$1" | yarn --silent eslint --stdin --stdin-filename "$kak_buffile" --config .eslintrc.js -f unix; cat "$1" | yarn --silent ember-template-lint --format kakoune --no-ignore-pattern; } && run'




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'

I find the configuration above to be simpler, in case you find the two below to be overly-complex. Have been using this with .js, .vue, .svelte without issues.

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`.

# 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 '|' %{
            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"
                printf 'eval -client %s %%{ fail eslint formatter returned an error %s }\n' "$kak_client" "$?" | kak -p "$kak_session"
                printf "%s" "$kak_quoted_selection"
            rm -f "$format_out"

        # Replace all with content from register:
        execute-keys '|<ret>'


hook global WinSetOption filetype=json %{
    set-option window lintcmd %{ run() { cat -- "$1" | jq 2>&1 | awk -v filename="$1" '/ at line [0-9]+, column [0-9]+$/ { line=$(NF - 2); column=$NF; sub(/ at line [0-9]+, column [0-9]+$/, ""); printf "%s:%d:%d: error: %s", filename, line, column, $0; }'; } && run }







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=php %{
    set-option window lintcmd 'run() { cat "$1" | phpcs --report="emacs" --stdin-path="$kak_buffile" - | sed "s/ - /: /" ; } && run'
    set-option window formatcmd 'phpcbf -q --stdin-path="$kak_buffile" - || true'


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,E242,E704,W503,W504,E501,E221,E127,E128,E129,F405"
hook global WinSetOption filetype=python %{
    set-option window lintcmd "mypy --show-column-numbers"





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'

If your application uses bundler, you can also execute rubocop with bundle:

hook global WinSetOption filetype=ruby %{
    set-option window lintcmd %{ run() { cat "${1}" | bundle exec rubocop --format emacs -s "${kak_buffile}" 2>&1; } && run }





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 }                                                                                                                          


hook global BufSetOption filetype=zig %{
    set-option buffer lintcmd 'zig fmt --color off --ast-check 2>&1'
    # To enable auto linting on buffer write
    #hook -group zig-auto-lint buffer BufWritePre .* lint-buffer
Clone this wiki locally