Skip to content
Jesse Hoyos edited this page May 5, 2021 · 36 revisions

Linting

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"

AsciiDoc

hook global WinSetOption filetype=asciidoc %{
    set-option window lintcmd "proselint"
}

Bazel

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

C

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

C++

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

Clojure

Using this linter requires the clj-kakoune-joker wrapper.

hook global WinSetOption filetype=clojure %{
    set-option window lintcmd "clj-kj.sh"
}

CoffeeScript

CSS

hook global WinSetOption filetype=css %{
    set-option window lintcmd "stylelint --fix --stdin-filename='%val{buffile}'"
}

D

hook global WinSetOption filetype=d %{
    set-option window lintcmd "dscanner -S --errorFormat '{filepath}:{line}:{column}: {type}: {message}'"
}

Elixir

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

Elm

Fish

Go

HAML

HTML

hook global WinSetOption filetype=html %{
    set-option window lintcmd "tidy -e --gnu-emacs yes --quiet yes 2>&1"
}

Handlebars

Haskell

INI

Java

JavaScript

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>'
    }
}

JSON

Julia

Latex

Lua

LISP

Makefile

Markdown

hook global WinSetOption filetype=markdown %{
    set-option window lintcmd "proselint"
}

MoonScript

Nim

Ocaml

Perl

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 \
    }
}

PHP

PHPcs

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'
}

Python

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

Pony

Pug

Ragel

Ruby

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'
}

Rust

SASS & SCSS

Scala

Shell

hook global WinSetOption filetype=sh %{
    set-option window lintcmd "shellcheck -fgcc -Cnever"
}

Swift

hook global WinSetOption filetype=swift %{
    set-option window lintcmd "swiftlint --quiet --path"
}

TUP

YAML

# 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 }                                                                                                                          
}  

Zig

Clone this wiki locally