Home

Awesome

lint+

An improved linting plugin for Lite XL.

Includes compatibility layer for linter.

Screenshots

1st screenshot

<p align="center"> Features ErrorLens-style warnings and error messages for quickly scanning through code for errors. </p> <br>

2nd screenshot

<p align="center"> The status view shows either the first error, or the full message of the error under your text cursor. No mouse interaction needed! </p>

Motivation

There were a few problems I had with the existing linter plugin:

lint+ aims to fix all of the above problems.

Why not just fix linter?

Installation

Navigate to your plugins folder, and clone the repository:

$ git clone https://github.com/liquidev/lintplus

To enable the different linters available on the linters subdirectory, you have to load them on your lite-xl user module file (init.lua).

You can load a single linter:

local lintplus = require "plugins.lintplus"
lintplus.load("luacheck")

or multiple linters by passing a table:

local lintplus = require "plugins.lintplus"
lintplus.load({"php", "luacheck"})

If you want to use plugins designed for the other linter, you will also need to enable the compatibility plugin linter.lua from this repository.

$ ln -s $PWD/{lintplus/linter,linter}.lua

Keep in mind that plugins designed for linter will not work as well as lint+ plugins, because of linter's lack of multiple severity levels. All warnings reported by linter linters will be reported with the warning level.

Automatic Linting

To enable automatic linting upon opening/saving a file, add the following code tou your lite-xl user module:

local lintplus = require "plugins.lintplus"
lintplus.setup.lint_on_doc_load()
lintplus.setup.lint_on_doc_save()

This overrides Doc.load and Doc.save with some extra behavior to enable automatic linting.

Commands

Available commands from the lite-xl commands palette (ctrl+shift+p):

Configuration

lint+ itself looks for the following configuration options:

All options are unset (nil) by default, so eg. setting config.lint.kind_pretty_names.hint will not work because config.lint.kind_pretty_names does not exist.

Individual plugins may also look for options in the config.lint table. Refer to each plugin's source code for more information.

Styling

The screenshots above use a theme with extra colors for the linter's messages. The default color is the same color used for literals, which isn't always what you want. Most of the time you want to have some clear visual distinction between severity levels, so lint+ is fully stylable.

Example:

local common = require "core.common"
local style = require "core.style"
style.lint = {
  info = style.syntax["keyword2"],
  hint = style.syntax["function"],
  warning = style.syntax["function"],
  error = { common.color "#FF3333" }
}

As with config, you need to provide all or no colors.

Creating new linters

Just like linter, lint+ allows you to create new linters for languages not supported out of the box. The API is very simple:

Severity: enum {
  "info",     -- suggestions on how to fix things, may be used in tandem with
              -- other messages
  "hint",     -- suggestions on small things that don't affect program behavior
  "warning",  -- warnings about possible mistakes that may affect behavior
  "error",    -- syntax or semantic errors that prevent compilation
}

LintContext: table {
  :gutter_rail(): number
    -- creates a new gutter rail and returns its index
  :gutter_rail_count(): number
    -- returns how many gutter rails have been created in this context
  -- You may create additional fields in this table, but keys prefixed with _
  -- are reserved by lint+.
}

lintplus.add(linter_name: string)(linter: table {
  filename: pattern,
  procedure: table {
    command: function (filename: string): {string},
      -- Returns the lint command for the given filename.
    interpreter: (function (filename, line: string, context: LintContext):
      function ():
        nil or
        (filename: string, line, column: number,
         kind: Severity, message: string, rail: number or nil)) or "bail"
      -- Creates and returns a message iterator, which yields all messages
      -- from the line.
      -- If the return value is "bail", reading the lint command is aborted
      -- immediately. This is done as a mitigation for processes that may take
      -- too long to execute or block indefinitely.
      -- `rail` is optional and specifies the gutter rail to which the message
      -- should be attached.
  }
})

Because writing command and interpreter functions can quickly get tedious, there are some helpers that return pre-built functions for you:

lintplus.command(cmd: {string}): function (string): {string}
  -- Returns a function that replaces `lintplus.filename` in the given table
  -- with the linted file's name.
lintplus.interpreter(spec: table {
  info: pattern or nil,
  hint: pattern or nil,
  warning: pattern or nil,
  error: pattern or nil,
    -- Defines patterns for all the severity levels. Each pattern must have
    -- four captures: the first one being the filename, the second and third
    -- being the line and column, and the fourth being the message.
    -- When any of these are nil, the interpreter simply will not produce the
    -- given severity levels.
  strip: pattern or nil,
    -- Defines a pattern for stripping unnecessary information from the message
    -- capture from one of the previously defined patterns. When this is `nil`,
    -- nothing is stripped and the message remains as-is.
})

An example linter built with these primitives:

lintplus.add("nim") {
  filename = "%.nim$",
  procedure = {
    command = lintplus.command {
      "nim", "check", "--listFullPaths", "--stdout", lintplus.filename
    },
    interpreter = lintplus.interpreter {
      -- The format for these three in Nim is almost exactly the same:
      hint = "(.-)%((%d+), (%d+)%) Hint: (.+)",
      warning = "(.-)%((%d+), (%d+)%) Warning: (.+)",
      error = "(.-)%((%d+), (%d+)%) Error: (.+)",
      -- We want to strip annotations like [XDeclaredButNotUsed] from the end:
      strip = "%s%[%w+%]$",
      -- Note that info was omitted. This is because all of the severity levels
      -- are optional, so eg. you don't have to provide an info pattern.
    },
  },
}

If you want to let the user of your linter specify some extra arguments, lintplus.args_command can be used instead of lintplus.command:

-- ...
    command = lintplus.args_command(
      { "luacheck",
        lintplus.args,
        "--formatter=visual_studio",
        lintplus.filename },
      "luacheck_args"
    )
-- ...

To enable plugins for different languages, do the same thing, but with lintplus_*.lua. For example, to enable support for Nim and Rust: The second argument to this function is the name of the field in the config.lint table. Then, the user provides arguments like so:

config.lint.luacheck_args = { "--max-line-length=80", "--std=love" }

Known problems