Initial commit

This commit is contained in:
Patrick Alvin Alcala 2025-06-26 16:53:43 +08:00
commit 209ba130c0
4852 changed files with 1517959 additions and 0 deletions

View file

@ -0,0 +1,281 @@
# lint+
An improved linting plugin for [Lite XL](https://github.com/lite-xl/lite-xl).
Includes compatibility layer for [`linter`](https://github.com/drmargarido/linters).
## Screenshots
![1st screenshot](screenshots/1.png)
<p align="center">
Features ErrorLens-style warnings and error messages for quickly scanning
through code for errors.
</p>
<br>
![2nd screenshot](screenshots/2.png)
<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:
- It can only show "warnings" - there's no severity levels
(info/hint/warning/error).
- It doesn't show the messages after lines (ErrorLens style), you have to hover
over the warning first.
- It spam-runs the linter command, but Nim (and possibly other languages)
compiles relatively slowly, which lags the editor to hell.
- It doesn't display the first or current error message on the status view.
lint+ aims to fix all of the above problems.
### Why not just fix `linter`?
- It works fundamentally differently from lint+, so fixing it would be more
costly than just making a new plugin.
- I haven't ever made my own linter support plugin, so this was a good exercise.
## Installation
Navigate to your `plugins` folder, and clone the repository:
```sh
$ git clone https://github.com/liquidev/lintplus
```
To enable the different linters available on the [linters](linters/)
subdirectory, you have to load them on your lite-xl user module file (`init.lua`).
You can load a single linter:
```lua
local lintplus = require "plugins.lintplus"
lintplus.load("luacheck")
```
or multiple linters by passing a table:
```lua
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*.
```sh
$ 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:
```lua
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):
* `lint+:lint` - run the appropriate linter command for the current document
* `lint+:goto-previous-message` (alt+up) - jump to previous message on current document
* `lint+:goto-next-message` (alt+down) - jump to next message on current document
## Configuration
lint+ itself looks for the following configuration options:
- `config.lint.kind_pretty_names`
- table:
- `info`: string = `"I"`
- `hint`: string = `"H"`
- `warning`: string = `"W"`
- `error`: string = `"E"`
- controls the prefix prepended to messages displayed on the status bar.
for example, setting `error` to `Error` will display `Error: …` or
`line 10 Error: …` instead of `E: …` or `line 10 E: …`.
- `config.lint.lens_style`
- string:
- `"blank"`: do not draw underline on line messages
- `"solid"`: draw single line underline on line messages (default)
- `"dots"`: draw dotted underline on line messages (slower performance)
- function(x, y, width, color): a custom drawing routine
- `x`: number
- `y`: number
- `width`: number
- `color`: renderer.color
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.
- `style.lint`
- table:
- `info`: Color - the color used for infos
- `hint`: Color - the color used for hints
- `warning`: Color - the color used for warnings
- `error`: Color - the color used for errors
Example:
```lua
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:
```lua
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:
```lua
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:
```lua
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`:
```lua
-- ...
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:
```lua
config.lint.luacheck_args = { "--max-line-length=80", "--std=love" }
```
## Known problems
- Due to the fact that it shows the most severe message at the end of the
line, displaying more than one message per line is really difficult with
the limited horizontal real estate, so it can only display one message per
line.
- It is unable to underline the offending token, simply because some linter
error messages do not contain enough information about where the error start
and end is. It will highlight the correct line and column, though.

View file

@ -0,0 +1,29 @@
-- file system utilities
local fs = {}
function fs.normalize_path(path)
if PLATFORM == "Windows" then
return path:gsub('\\', '/')
else
return path
end
end
function fs.parent_directory(path)
path = fs.normalize_path(path)
path = path:match("^(.-)/*$")
local last_slash_pos = -1
for i = #path, 1, -1 do
if path:sub(i, i) == '/' then
last_slash_pos = i
break
end
end
if last_slash_pos < 0 then
return nil
end
return path:sub(1, last_slash_pos - 1)
end
return fs

View file

@ -0,0 +1,977 @@
-- mod-version:3
-- lint+ - an improved linter for lite
-- copyright (C) lqdev, 2020
-- licensed under the MIT license
--- STATIC CONFIG ---
local kind_priority = {
info = -1,
hint = 0,
warning = 1,
error = 2,
}
local default_kind_pretty_names = {
info = "I",
hint = "H",
warning = "W",
error = "E",
}
--- IMPLEMENTATION ---
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local style = require "core.style"
local keymap = require "core.keymap"
local syntax = require "core.syntax"
local Doc = require "core.doc"
local DocView = require "core.docview"
local StatusView = require "core.statusview"
local liteipc = require "plugins.lintplus.liteipc"
local lint = {}
lint.fs = require "plugins.lintplus.fsutil"
lint.ipc = liteipc
lint.index = {}
lint.messages = {}
local LintContext = {}
LintContext.__index = LintContext
function LintContext:create_gutter_rail()
if not self._doc then return 0 end
local lp = self._doc.__lintplus
lp.rail_count = lp.rail_count + 1
return lp.rail_count
end
function LintContext:gutter_rail_count()
if not self._doc then return 0 end
return self._doc.__lintplus.rail_count
end
-- Can be used by other plugins to properly set the context when loading a doc
function lint.init_doc(filename, doc)
filename = core.project_absolute_path(filename)
local context = setmetatable({
_doc = doc or nil,
_user_context = nil,
}, LintContext)
if doc then
doc.__lintplus_context = {}
context._user_context = doc.__lintplus_context
doc.__lintplus = {
rail_count = 0,
}
end
if not lint.messages[filename] then
lint.messages[filename] = {
context = context,
lines = {},
rails = {},
}
elseif doc then
lint.messages[filename].context = context
end
end
-- Returns an appropriate linter for the given doc, or nil if no linter is
-- found.
function lint.get_linter_for_doc(doc)
if not doc.filename then
return nil
end
local file = core.project_absolute_path(doc.filename)
for name, linter in pairs(lint.index) do
if common.match_pattern(file, linter.filename) then
return linter, name
end
if linter.syntax ~= nil then
local header = doc:get_text(1, 1, doc:position_offset(1, 1, 128))
local syn = syntax.get(doc.filename, header)
for i = #linter.syntax, 1, -1 do
local s = linter.syntax[i]
if syn.name == s then
return linter, name
end
end
end
end
end
-- unused for now, because it was a bit buggy
-- Note: Should be fixed now
function lint.clear_messages(filename)
filename = core.project_absolute_path(filename)
if lint.messages[filename] then
lint.messages[filename].lines = {}
lint.messages[filename].rails = {}
end
end
function lint.add_message(filename, line, column, kind, message, rail)
filename = core.project_absolute_path(filename)
if not lint.messages[filename] then
-- This allows us to at least store messages until context is properly
-- set from the calling plugin.
lint.init_doc(filename)
end
local file_messages = lint.messages[filename]
local lines, rails = file_messages.lines, file_messages.rails
lines[line] = lines[line] or {}
if rail ~= nil then
rails[rail] = rails[rail] or { lines_taken = {} }
if not rails[rail].lines_taken[line] then
rails[rail].lines_taken[line] = true
table.insert(rails[rail], {
line = line,
column = column,
kind = kind,
})
end
end
table.insert(lines[line], {
column = column,
kind = kind,
message = message,
rail = rail,
})
end
local function process_line(doc, linter, line, context)
local file = core.project_absolute_path(doc.filename)
local had_messages = false
local iterator = linter.procedure.interpreter(file, line, context)
if iterator == "bail" then return iterator end
if os.getenv("LINTPLUS_DEBUG_LINES") then
print("lint+ | "..line)
end
for rawfile, lineno, columnno, kind, message, rail in iterator do
assert(type(rawfile) == "string")
local absfile = core.project_absolute_path(rawfile)
if absfile == file then -- TODO: support project-wide errors
assert(type(lineno) == "number")
assert(type(columnno) == "number")
assert(type(kind) == "string")
assert(type(message) == "string")
assert(rail == nil or type(rail) == "number")
lint.add_message(absfile, lineno, columnno, kind, message, rail)
core.redraw = true
end
end
return had_messages
end
local function compare_message_priorities(a, b)
return kind_priority[a.kind] > kind_priority[b.kind]
end
local function compare_messages(a, b)
if a.column == b.column then
return compare_message_priorities(a, b)
end
return a.column > b.column
end
local function compare_rail_messages(a, b)
return a.line < b.line
end
function lint.check(doc)
if doc.filename == nil then return end
local linter, linter_name = lint.get_linter_for_doc(doc)
if linter == nil then
core.error("no linter available for the given filetype")
return
end
local filename = core.project_absolute_path(doc.filename)
local context = setmetatable({
_doc = doc,
_user_context = doc.__lintplus_context,
}, LintContext)
doc.__lintplus = {
rail_count = 0,
}
-- clear_messages(linter)
lint.messages[filename] = {
context = context,
lines = {},
rails = {},
}
local function report_error(msg)
core.log_quiet(
"lint+/" .. linter_name .. ": " ..
doc.filename .. ": " .. msg
)
end
local cmd, cwd = linter.procedure.command(filename), nil
if cmd.set_cwd then
cwd = lint.fs.parent_directory(filename)
end
local process = liteipc.start_process(cmd, cwd)
core.add_thread(function ()
-- poll the process for lines of output
while true do
local exit, code, errmsg = process:poll(function (line)
process_line(doc, linter, line, context)
end)
if exit ~= nil then
-- If linter exited with exit code non 0 or 1 log it
if exit == "signal" then
report_error(
"linter exited with signal " .. code
.. (errmsg and " : " .. errmsg or "")
)
end
break
end
coroutine.yield(0)
end
-- after reading some lines, sort messages by priority in all files
-- and sort rail connections by line number
for _, file_messages in pairs(lint.messages) do
for _, messages in pairs(file_messages.lines) do
table.sort(messages, compare_messages)
end
for _, rail in pairs(file_messages.rails) do
table.sort(rail, compare_rail_messages)
end
file_messages.rails_sorted = true
core.redraw = true
coroutine.yield(0)
end
end)
end
-- inject initialization routines to documents
local Doc_load, Doc_save, Doc_on_close = Doc.load, Doc.save, Doc.on_close
local function init_linter_for_doc(doc)
local linter, _ = lint.get_linter_for_doc(doc)
if linter == nil then return end
doc.__lintplus_context = {}
if linter.procedure.init ~= nil then
linter.procedure.init(
core.project_absolute_path(doc.filename),
doc.__lintplus_context
)
end
end
function Doc:load(filename)
local old_filename = self.filename
Doc_load(self, filename)
if old_filename ~= filename then
init_linter_for_doc(self)
end
end
function Doc:save(filename, abs_filename)
local old_filename = self.filename
Doc_save(self, filename, abs_filename)
if old_filename ~= filename then
init_linter_for_doc(self)
end
end
function Doc:on_close()
Doc_on_close(self)
if not self.filename then return end
local filename = core.project_absolute_path(self.filename)
-- release Doc object for proper garbage collection
if lint.messages[filename] then
lint.messages[filename] = nil
end
end
-- inject hooks to Doc.insert and Doc.remove to shift messages around
local function sort_positions(line1, col1, line2, col2)
if line1 > line2
or line1 == line2 and col1 > col2 then
return line2, col2, line1, col1, true
end
return line1, col1, line2, col2, false
end
local Doc_insert = Doc.insert
function Doc:insert(line, column, text)
Doc_insert(self, line, column, text)
if self.filename == nil then return end
if line == math.huge then return end
local filename = core.project_absolute_path(self.filename)
local file_messages = lint.messages[filename]
local lp = self.__lintplus
if file_messages == nil or lp == nil then return end
-- shift line messages downwards
local shift = 0
for _ in text:gmatch('\n') do
shift = shift + 1
end
if shift == 0 then return end
local lines = file_messages.lines
for i = #self.lines, line, -1 do
if lines[i] ~= nil then
if not (i == line and lines[i][1].column < column) then
lines[i + shift] = lines[i]
lines[i] = nil
end
end
end
-- shift rails downwards
local rails = file_messages.rails
for _, rail in pairs(rails) do
for _, message in ipairs(rail) do
if message.line >= line then
message.line = message.line + shift
end
end
end
end
local function update_messages_after_removal(
doc,
line1, column1,
line2, column2
)
if line1 == line2 then return end
if line2 == math.huge then return end
if doc.filename == nil then return end
local filename = core.project_absolute_path(doc.filename)
local file_messages = lint.messages[filename]
local lp = doc.__lintplus
if file_messages == nil or lp == nil then return end
local lines = file_messages.lines
line1, column1, line2, column2 =
sort_positions(line1, column1, line2, column2)
local shift = line2 - line1
-- remove all messages in this range
for i = line1, line2 do
lines[i] = nil
end
-- shift all line messages up
for i = line1, #doc.lines do
if lines[i] ~= nil then
lines[i - shift] = lines[i]
lines[i] = nil
end
end
-- remove all rail messages in this range
local rails = file_messages.rails
for _, rail in pairs(rails) do
local remove_indices = {}
for i, message in ipairs(rail) do
if message.line >= line1 and message.line < line2 then
table.insert(remove_indices, i)
elseif message.line > line1 then
message.line = message.line - shift
end
end
for i = #remove_indices, 1, -1 do
table.remove(rail, remove_indices[i])
end
end
end
local Doc_remove = Doc.remove
function Doc:remove(line1, column1, line2, column2)
update_messages_after_removal(self, line1, column1, line2, column2)
Doc_remove(self, line1, column1, line2, column2)
end
-- inject rendering routines
local renderutil = require "plugins.lintplus.renderutil"
local function rail_width(dv)
return dv:get_line_height() / 3 -- common.round(style.padding.x / 2)
end
local function rail_spacing(dv)
return common.round(rail_width(dv) / 4)
end
local DocView_get_gutter_width = DocView.get_gutter_width
function DocView:get_gutter_width()
local extra_width = 0
if self.doc.filename ~= nil then
local file_messages = lint.messages[core.project_absolute_path(self.doc.filename)]
if file_messages ~= nil then
local rail_count = file_messages.context:gutter_rail_count()
extra_width = rail_count * (rail_width(self) + rail_spacing(self))
end
end
local original_width, padding = DocView_get_gutter_width(self)
return original_width + extra_width, padding
end
local function get_gutter_rail_x(dv, index)
return
dv.position.x + dv:get_gutter_width() -
(rail_width(dv) + rail_spacing(dv)) * index + rail_spacing(dv)
end
local function get_message_group_color(messages)
if style.lint ~= nil then
return style.lint[messages[1].kind]
else
local default_colors = {
info = style.syntax["normal"],
hint = style.syntax["function"],
warning = style.syntax["number"],
error = style.syntax["keyword2"]
}
return default_colors[messages[1].kind]
end
end
local function get_underline_y(dv, line)
local _, y = dv:get_line_screen_position(line)
local line_height = dv:get_line_height()
local extra_space = line_height - dv:get_font():get_height()
return y + line_height - extra_space / 2
end
local function draw_gutter_rail(dv, index, messages)
local rail = messages.rails[index]
if rail == nil or #rail < 2 then return end
local first_message = rail[1]
local last_message = rail[#rail]
local x = get_gutter_rail_x(dv, index)
local rw = rail_width(dv)
local start_y = get_underline_y(dv, first_message.line)
local fin_y = get_underline_y(dv, last_message.line)
-- connect with lens
local line_x = x + rw
for i, message in ipairs(rail) do
-- connect with lens
local lx, _ = dv:get_line_screen_position(message.line)
local ly = get_underline_y(dv, message.line)
local line_messages = messages.lines[message.line]
if line_messages ~= nil then
local column = line_messages[1].column
local message_left = line_messages[1].message:sub(1, column - 1)
local line_color = get_message_group_color(line_messages)
local xoffset = (x + rw) % 2
local line_w = dv:get_font():get_width(message_left) - line_x + lx
renderutil.draw_dotted_line(x + rw + xoffset, ly, line_w, 'x', line_color)
-- draw curve
ly = ly - rw * (i == 1 and 0 or 1) + (i ~= 1 and 1 or 0)
renderutil.draw_quarter_circle(x, ly, rw, style.accent, i > 1)
end
end
-- draw vertical part
local height = fin_y - start_y + 1 - rw * 2
renderer.draw_rect(x, start_y + rw, 1, height, style.accent)
end
local DocView_draw = DocView.draw
function DocView:draw()
DocView_draw(self)
local filename = self.doc.filename
if filename == nil then return end
filename = core.project_absolute_path(filename)
local messages = lint.messages[filename]
if messages == nil or not messages.rails_sorted then return end
local rails = messages.rails
local pos, size = self.position, self.size
core.push_clip_rect(pos.x, pos.y, size.x, size.y)
for i = 1, #rails do
draw_gutter_rail(self, i, messages)
end
core.pop_clip_rect()
end
local lens_underlines = {
blank = function () end,
solid = function (x, y, width, color)
renderer.draw_rect(x, y, width, 1, color)
end,
dots = function (x, y, width, color)
renderutil.draw_dotted_line(x, y, width, 'x', color)
end,
}
local function draw_lens_underline(x, y, width, color)
local lens_style = config.lint.lens_style or "solid"
if type(lens_style) == "string" then
local fn = lens_underlines[lens_style] or lens_underlines.blank
fn(x, y, width, color)
elseif type(lens_style) == "function" then
lens_style(x, y, width, color)
end
end
local function get_or_default(t, index, default)
if t ~= nil and t[index] ~= nil then
return t[index]
else
return default
end
end
local DocView_draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(idx, x, y)
DocView_draw_line_text(self, idx, x, y)
local lp = self.doc.__lintplus
if lp == nil then return end
local yy = get_underline_y(self, idx)
local file_messages = lint.messages[core.project_absolute_path(self.doc.filename)]
if file_messages == nil then return end
local messages = file_messages.lines[idx]
if messages == nil then return end
local underline_start = messages[1].column
local font = self:get_font()
local underline_color = get_message_group_color(messages)
local line = self.doc.lines[idx]
local line_left = line:sub(1, underline_start - 1)
local line_right = line:sub(underline_start, -2)
local underline_x = font:get_width(line_left)
local w = font:get_width('w')
local msg_x = x + w * 3 + underline_x + font:get_width(line_right)
local text_y = y + self:get_line_text_y_offset()
for i, msg in ipairs(messages) do
local text_color = get_or_default(style.lint, msg.kind, underline_color)
msg_x = renderer.draw_text(font, msg.message, msg_x, text_y, text_color)
if i < #messages then
msg_x = renderer.draw_text(font, ", ", msg_x, text_y, style.syntax.comment)
end
end
local underline_width = msg_x - x - underline_x
draw_lens_underline(x + underline_x, yy, underline_width, underline_color)
end
local function table_add(t, d)
for _, v in ipairs(d) do
table.insert(t, v)
end
end
local function kind_pretty_name(kind)
return (config.kind_pretty_names or default_kind_pretty_names)[kind]
end
local function get_error_messages(doc, ordered)
if not doc then return nil end
local messages = lint.messages[core.project_absolute_path(doc.filename)]
if not messages then return nil end
if not ordered then return messages.lines end
-- sort lines
local lines = {}
for line, _ in pairs(messages.lines) do
table.insert(lines, line)
end
table.sort(lines, function(a, b) return a < b end)
local lines_info = {}
-- store in array instead of dictionary to keep insertion order
for _, line in ipairs(lines) do
table.insert(
lines_info,
{line = line, table.unpack(messages.lines[line])}
)
end
return lines_info
end
local function get_current_error(doc)
local file_messages = get_error_messages(doc)
local line, message = math.huge, nil
for ln, messages in pairs(file_messages) do
local msg = messages[1]
if msg.kind == "error" and ln < line then
line, message = ln, msg
end
end
if message ~= nil then
return line, message.kind, message.message
end
return nil, nil, nil
end
local function goto_prev_message()
local doc = core.active_view.doc
local current_line = doc:get_selection()
local file_messages = get_error_messages(doc, true)
if file_messages ~= nil then
local prev = nil
local found = false
local last = nil
for _, line_info in pairs(file_messages) do
local line = line_info.line
if current_line <= line then
found = true
end
if not found then
prev = line
end
last = line
end
local line = prev or last
if line then
doc:set_selection(line, 1, line, 1)
end
end
end
local function goto_next_message()
local doc = core.active_view.doc
local current_line = doc:get_selection()
local file_messages = get_error_messages(doc, true)
if file_messages ~= nil then
local first = nil
local next = nil
for _, line_info in pairs(file_messages) do
local line = line_info.line
if not first then
first = line
end
if line > current_line then
next = line
break
end
end
local line = next or first
if line then
doc:set_selection(line, 1, line, 1)
end
end
end
local function get_status_view_items()
local doc = core.active_view.doc
local line1, _, line2, _ = doc:get_selection()
local file_messages = get_error_messages(doc)
if file_messages ~= nil then
if file_messages[line1] ~= nil and line1 == line2 then
local msg = file_messages[line1][1]
return {
kind_pretty_name(msg.kind), ": ",
style.text, msg.message,
}
else
local line, kind, message = get_current_error(doc)
if line ~= nil then
return {
"line ", tostring(line), " ", kind_pretty_name(kind), ": ",
style.text, message,
}
end
end
end
return {}
end
if StatusView["add_item"] then
core.status_view:add_item({
predicate = function()
local doc = core.active_view.doc
if
doc and doc.filename -- skip new files
and
getmetatable(core.active_view) == DocView
and
(
lint.get_linter_for_doc(doc)
or
lint.messages[core.project_absolute_path(doc.filename)]
)
then
return true
end
return false
end,
name = "lint+:message",
alignment = StatusView.Item.LEFT,
get_item = get_status_view_items,
command = function()
local doc = core.active_view.doc
local line = get_current_error(doc)
if line ~= nil then
doc:set_selection(line, 1, line, 1)
end
end,
position = -1,
tooltip = "Lint+ error message",
separator = core.status_view.separator2
})
else
local StatusView_get_items = StatusView.get_items
function StatusView:get_items()
local left, right = StatusView_get_items(self)
local doc = core.active_view.doc
if
doc and doc.filename -- skip new files
and
getmetatable(core.active_view) == DocView
and
(
lint.get_linter_for_doc(doc)
or
lint.messages[core.project_absolute_path(doc.filename)]
)
then
local items = get_status_view_items()
if #items > 0 then
table.insert(left, {style.dim, self.separator2, table.unpack(items)})
end
end
return left, right
end
end
command.add(DocView, {
["lint+:check"] = function ()
lint.check(core.active_view.doc)
end
})
command.add(DocView, {
["lint+:goto-previous-message"] = function ()
goto_prev_message()
end
})
command.add(DocView, {
["lint+:goto-next-message"] = function ()
goto_next_message()
end
})
keymap.add {
["alt+up"] = "lint+:goto-previous-message",
["alt+down"] = "lint+:goto-next-message"
}
--- LINTER PLUGINS ---
function lint.add(name)
return function (linter)
lint.index[name] = linter
end
end
--- SETUP ---
lint.setup = {}
function lint.setup.lint_on_doc_load()
local doc_load = Doc.load
function Doc:load(filename)
doc_load(self, filename)
if not self.filename then return end
if lint.get_linter_for_doc(self) ~= nil then
lint.check(self)
end
end
end
function lint.setup.lint_on_doc_save()
local doc_save = Doc.save
function Doc:save(filename, abs_filename)
doc_save(self, filename, abs_filename)
if lint.get_linter_for_doc(self) ~= nil then
lint.check(self)
end
end
end
function lint.enable_async()
core.error("lint+: calling enable_async() is not needed anymore")
end
--- LINTER CREATION UTILITIES ---
lint.filename = {}
lint.args = {}
local function map(tab, fn)
local result = {}
for k, v in pairs(tab) do
local mapped, mode = fn(k, v)
if mode == "append" then
table_add(result, mapped)
elseif type(k) == "number" then
table.insert(result, mapped)
else
result[k] = mapped
end
end
return result
end
function lint.command(cmd)
return function (filename)
return map(cmd, function (k, v)
if type(k) == "number" and v == lint.filename then
return filename
end
return v
end)
end
end
function lint.args_command(cmd, config_option)
return function (filename)
local c = map(cmd, function (k, v)
if type(k) == "number" and v == lint.args then
local args = lint.config[config_option] or {}
return args, "append"
end
return v
end)
return lint.command(c)(filename)
end
end
function lint.interpreter(i)
local patterns = {
info = i.info,
hint = i.hint,
warning = i.warning,
error = i.error,
}
local strip_pattern = i.strip
return function (_, line)
local line_processed = false
return function ()
if line_processed then
return nil
end
for kind, patt in pairs(patterns) do
assert(
type(patt) == "string",
"lint+: interpreter pattern must be a string")
local file, ln, column, message = line:match(patt)
if file then
if strip_pattern then
message = message:gsub(strip_pattern, "")
end
line_processed = true
return file, tonumber(ln), tonumber(column), kind, message
end
end
end
end
end
function lint.load(linter)
if type(linter) == "table" then
for _, v in ipairs(linter) do
require("plugins.lintplus.linters." .. v)
end
elseif type(linter) == "string" then
require("plugins.lintplus.linters." .. linter)
end
end
if type(config.lint) ~= "table" then
config.lint = {}
end
lint.config = config.lint
--- END ---
return lint

View file

@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

View file

@ -0,0 +1,46 @@
-- lite-xl 1.16
-- linter compatibility module for lint+
-- this module simply defines a linter.add_language function for compatibility
-- with the existing linter module.
-- note that linter modules are not capable of using all of lint+'s
-- functionality: namely, they cannot use the four available levels of severity.
-- all messages will have the warning severity level.
local lintplus = require "plugins.lintplus"
local linter = {}
local name_counter = 0
function linter.add_language(t)
lintplus.add("compat.linter"..name_counter) {
filename = t.file_patterns,
procedure = {
command = lintplus.command(
t.command
:gsub("%$FILENAME", "$filename")
:gsub("%$ARGS", table.concat(t.args, ' '))
),
-- can't use the lintplus interpreter simply because it doesn't work
-- exactly as linter does
interpreter = function (filename, line)
local yielded_message = false
return function ()
if yielded_message then return nil end
local ln, column, message = line:match(t.warning_pattern)
if ln then
-- we return the original filename to show all warnings
-- because... say it with me... that's how linter works!!
yielded_message = true
return filename, tonumber(ln), tonumber(column), "warning", message
end
end
end,
},
}
name_counter = name_counter + 1
end
return linter

View file

@ -0,0 +1,28 @@
-- luacheck plugin for lint+
--- CONFIG ---
-- config.lint.luacheck_args: table[string]
-- passes the specified arguments to luacheck
--- IMPLEMENTATION ---
local lintplus = require "plugins.lintplus"
lintplus.add("luacheck") {
filename = "%.lua$",
procedure = {
command = lintplus.args_command(
{ "luacheck",
lintplus.args,
"--formatter",
"visual_studio",
lintplus.filename },
"luacheck_args"
),
interpreter = lintplus.interpreter {
warning = "(.-)%((%d+),(%d+)%) : warning .-: (.+)",
error = "(.-)%((%d+),(%d+)%) : error .-: (.+)",
}
},
}

View file

@ -0,0 +1,37 @@
-- Nelua plugin for lint+
--- CONFIG ---
-- config.lint.nelua_mode: "analyze" | "lint"
-- changes the linting mode, "analyze" (default) does a complete checking,
-- while "lint" only checks for syntax errors.
--- IMPLEMENTATION ---
local core = require 'core'
local lintplus = require 'plugins.lintplus'
local mode = lintplus.config.nelua_mode or "analyze"
if mode ~= "analyze" and mode ~= "lint" then
core.error("lint+/nelua: invalid nelua_mode '%s'. Available modes: 'analyze', 'lint'", mode)
mode = "lint"
end
local command = lintplus.command {
'nelua',
'--no-color',
'--'..mode,
lintplus.filename
}
lintplus.add 'nelua' {
filename = '%.nelua$',
procedure = {
command = command,
interpreter = lintplus.interpreter {
error = "(.-):(%d+):(%d+):.-error: (.+)"
},
},
}

View file

@ -0,0 +1,49 @@
-- Nim plugin for lint+
--- CONFIG ---
-- config.lint.use_nimc: bool
-- switches the linting backend from `nim check` to `nim c`. this can
-- eliminate certain kinds of errors but is less safe due to `nim c` allowing
-- staticExec
-- config.lint.nim_args: string
-- passes the specified arguments to the lint command.
-- extra arguments may also be passed via a nim.cfg or config.nims.
--- IMPLEMENTATION ---
local lintplus = require "plugins.lintplus"
local nullfile
if PLATFORM == "Windows" then
nullfile = "NUL"
elseif PLATFORM == "Linux" then
nullfile = "/dev/null"
end
local cmd = {
"nim",
"--listFullPaths",
"--stdout",
lintplus.args,
}
if nullfile == nil or not lintplus.config.use_nimc then
table.insert(cmd, "check")
else
table.insert(cmd, "-o:" .. nullfile)
table.insert(cmd, "c")
end
table.insert(cmd, lintplus.filename)
lintplus.add("nim") {
filename = "%.nim$",
procedure = {
command = lintplus.args_command(cmd, "nim_args"),
interpreter = lintplus.interpreter {
hint = "(.-)%((%d+), (%d+)%) Hint: (.+)",
warning = "(.-)%((%d+), (%d+)%) Warning: (.+)",
error = "(.-)%((%d+), (%d+)%) Error: (.+)",
strip = "%s%[%w+%]$",
},
},
}

View file

@ -0,0 +1,40 @@
-- PHP lint plugin for lint+
--- CONFIG ---
-- config.lint.php_args: {string}
-- passes the specified arguments to php
--- IMPLEMENTATION ---
local lintplus = require "plugins.lintplus"
lintplus.add("php") {
filename = "%.php$",
procedure = {
command = lintplus.args_command(
{
"php",
"-l",
lintplus.args,
lintplus.filename
},
"php_args"
),
interpreter = function (filename, line, context)
local line_processed = false
return function ()
if line_processed then
return nil
end
local message, file, line_num = line:match(
"[%a ]+:%s*(.*)%s+in%s+(%g+)%s+on%sline%s+(%d+)"
)
if line_num then
line_processed = true
return filename, tonumber(line_num), 1, "error", message
end
end
end
},
}

View file

@ -0,0 +1,16 @@
local lintplus = require "plugins.lintplus"
lintplus.add("flake8") {
filename = "%.py$",
procedure = {
command = lintplus.command(
{ "flake8",
lintplus.filename },
"flake8_args"
),
interpreter = lintplus.interpreter {
warning = "(.-):(%d+):(%d+): [FCW]%d+ (.+)",
error = "(.-):(%d+):(%d+): E%d+ (.+)",
}
},
}

View file

@ -0,0 +1,181 @@
-- Rust plugin for lint+
--- IMPLEMENTATION ---
local common = require "core.common"
local core = require "core"
local lintplus = require "plugins.lintplus"
local json = require "plugins.lintplus.json"
-- common functions
local function no_op() end
local function parent_directories(filename)
return function ()
filename = lintplus.fs.parent_directory(filename)
return filename
end
end
-- message processing
local function message_spans_multiple_lines(message, line)
if #message.spans == 0 then return false end
for _, span in ipairs(message.spans) do
if span.line_start ~= line then
return true
end
end
for _, child in ipairs(message.children) do
local child_spans_multiple_lines = message_spans_multiple_lines(child, line)
if child_spans_multiple_lines then
return true
end
end
return false
end
local function process_message(
context,
message,
out_messages,
rail
)
local msg = message.message
local span = message.spans[1]
local kind do
local l = message.level
if l == "error" or l == "warning" then
kind = l
elseif l == "error: internal compiler error" then
kind = "error"
else
kind = "info"
end
end
local nonprimary_spans = 0
for _, sp in ipairs(message.spans) do
if not sp.is_primary then
nonprimary_spans = nonprimary_spans + 1
end
end
-- only assign a rail if there are children or multiple non-primary spans
if span ~= nil then
local filename = context.workspace_root .. '/' .. span.file_name
local line, column = span.line_start, span.column_start
if rail == nil then
if message_spans_multiple_lines(message, line) then
rail = context:create_gutter_rail()
end
end
for _, sp in ipairs(message.spans) do
if sp.label ~= nil and not sp.is_primary then
local s_filename = context.workspace_root .. '/' .. span.file_name
local s_line, s_column = sp.line_start, sp.column_start
table.insert(out_messages,
{ s_filename, s_line, s_column, "info", sp.label, rail })
end
end
if span.suggested_replacement ~= nil then
local suggestion = span.suggested_replacement:match("(.-)\r?\n")
if suggestion ~= nil then
msg = msg .. " `" .. suggestion .. '`'
end
end
table.insert(out_messages, { filename, line, column, kind, msg, rail })
end
for _, child in ipairs(message.children) do
process_message(context, child, out_messages, rail)
end
end
local function get_messages(context, event)
-- filename, line, column, kind, message
local messages = {}
process_message(context, event.message, messages)
return messages
end
-- linter
lintplus.add("rust") {
filename = "%.rs$",
procedure = {
init = function (filename, context)
local process = lintplus.ipc.start_process({
"cargo", "locate-project", "--workspace"
}, lintplus.fs.parent_directory(filename))
while true do
local exit, _ = process:poll(function (line)
local ok, process_result = pcall(json.decode, line)
if not ok then return end
context.workspace_root =
lintplus.fs.parent_directory(process_result.root)
end)
if exit ~= nil then break end
end
end,
command = lintplus.command {
set_cwd = true,
"cargo", "clippy",
"--message-format", "json",
"--color", "never",
-- "--tests",
},
interpreter = function (filename, line, context)
-- initial checks
if context.workspace_root == nil then
core.error(
"lint+/rust: "..filename.." is not situated in a cargo crate"
)
return no_op
end
if line:match("^ *Blocking") then
return "bail"
end
local ok, event = pcall(json.decode, line)
if not ok then return no_op end
if event.reason == "compiler-message" then
local messages = get_messages(context, event)
local i = 1
return function ()
local msg = messages[i]
if msg ~= nil then
i = i + 1
return table.unpack(msg)
else
return nil
end
end
else
return no_op
end
end,
},
}

View file

@ -0,0 +1,42 @@
-- shellcheck plugin for lint+
--- INSTALLATION ---
-- In order to use this linter, please ensure you have the shellcheck binary
-- in your path. For installation notes please see
-- https://github.com/koalaman/shellcheck#user-content-installing
--- CONFIG ---
-- config.lint.shellcheck_args: table[string]
-- passes the given arguments to shellcheck.
--- IMPLEMENTATION ---
local lintplus = require "plugins.lintplus"
lintplus.add("shellcheck") {
filename = "%.sh$",
syntax = {
"Shell script",
"shellscript",
"bashscript",
"Bash script",
"Bash",
"bash",
},
procedure = {
command = lintplus.args_command(
{ "shellcheck",
"--format=gcc",
lintplus.args,
lintplus.filename
},
"shellcheck_args"
),
interpreter = lintplus.interpreter {
info = "(.*):(%d+):(%d+): note: (.+)",
error = "(.*):(%d+):(%d+): error: (.+)",
warning = "(.*):(%d+):(%d+): warning: (.+)",
}
},
}

View file

@ -0,0 +1,68 @@
-- v plugin for lint+
--- INSTALLATION ---
-- In order to use this linter, please ensure you have the v binary
-- in your $PATH. For installation notes please see
-- https://github.com/vlang/v/blob/master/doc/docs.md#installing-v-from-source
--- CONFIG ---
-- config.lint.v_mode: "check" | "check-syntax"
-- changes the linting mode. check scans, parses, and checks the files
-- without compiling the program (default),
-- check-syntax only scan and parse the files, but then stops.
-- Useful for very quick syntax checks.
-- config.lint.v_args: table[string]
-- passes the given arguments to v.
--- IMPLEMENTATION ---
local core = require "core"
local lintplus = require "plugins.lintplus"
local mode = lintplus.config.v_mode or "check"
if mode ~= "check" and mode ~= "check-syntax" then
core.error("lint+/v: invalid v_mode '%s'. "..
"available modes: 'check', 'check-syntax'")
return
end
local command
if mode == "check" then
command = lintplus.command {
"v",
"-check",
"-nocolor",
"-shared",
"-message-limit", "-1",
lintplus.args,
lintplus.filename
}
elseif mode == "check-syntax" then
command = lintplus.args_command({
"v",
"-check-syntax",
"-nocolor",
"-shared",
"-message-limit", "-1",
lintplus.args,
lintplus.filename
}, "v_args")
end
lintplus.add("v") {
filename = "%.v$",
syntax = {
"V",
"v",
"Vlang",
"vlang",
},
procedure = {
command = command,
interpreter = lintplus.interpreter {
error = "(.*):(%d+):(%d+): error: (.+)",
warning = "(.*):(%d+):(%d+): warning: (.+)",
},
},
}

View file

@ -0,0 +1,54 @@
-- Zig plugin for lint+
--- CONFIG ---
-- config.lint.zig_mode: "ast-check" | "build"
-- changes the linting mode. ast-check is a quick'n'dirty check (default),
-- build compiles the tests in a file (but does not run them).
-- config.lint.zig_args: table[string]
-- passes the given table of arguments to zig test. this does not have any
-- effect in "ast-check" mode.
--- IMPLEMENTATION ---
local core = require "core"
local lintplus = require "plugins.lintplus"
local mode = lintplus.config.zig_mode or "ast-check"
if mode ~= "ast-check" and mode ~= "build" then
core.error("lint+/zig: invalid zig_mode '%s'. "..
"available modes: 'ast-check', 'build'")
return
end
local command
if mode == "ast-check" then
command = lintplus.command {
"zig",
"ast-check",
"--color", "off",
lintplus.filename
}
elseif mode == "build" then
command = lintplus.args_command({
"zig",
"test",
"--color", "off",
"-fno-emit-bin",
lintplus.args,
lintplus.filename
}, "zig_args")
end
lintplus.add("zig") {
filename = "%.zig$",
procedure = {
command = command,
interpreter = lintplus.interpreter {
hint = "(.-):(%d+):(%d+): note: (.+)",
error = "(.-):(%d+):(%d+): error: (.+)",
warning = "(.-):(%d+):(%d+): warning: (.+)",
}
},
}

View file

@ -0,0 +1,66 @@
-- liteipc - async IPC for lite
local liteipc = {}
local Process = {}
Process.__index = Process
function liteipc.start_process(args, cwd)
local proc = setmetatable({
popen = process.start(args, {cwd = cwd}),
read_from = ""
}, Process)
return proc
end
function Process.poll(self, callback)
local line = ""
local read = nil
while self.read_from == "" and self.popen:returncode() == nil do
local stderr = self.popen:read_stderr(1)
local stdout = self.popen:read_stdout(1)
local out = nil
if stderr ~= nil and stderr ~= "" then
out = stderr
self.read_from = "stderr"
elseif stdout ~= nil and stdout ~= "" then
out = stdout
self.read_from = "stdout"
end
if out ~= nil then
if out ~= "\n" then
line = line .. out
end
break
end
end
while true do
if self.read_from == "stderr" then
read = self.popen:read_stderr(1)
else
read = self.popen:read_stdout(1)
end
if read == nil or read == "\n" then
if line ~= "" then callback(line) end
break
else
line = line .. read
end
end
if not self.popen:running() and read == nil then
local exit = "exit"
local retcode = self.popen:returncode()
if retcode ~= 1 and retcode ~= 0 then
exit = "signal"
end
local errmsg = process.strerror(retcode)
return exit, retcode, errmsg
end
return nil, nil, nil
end
return liteipc

View file

@ -0,0 +1,14 @@
{
"addons": [
{
"id": "lintplus",
"name": "Lint+",
"description": "An improved linting plugin.",
"version": "0.2",
"mod_version": "3",
"tags": [
"linter"
]
}
]
}

View file

@ -0,0 +1,44 @@
-- rendering utilities
local common = require "core.common"
local renderutil = {}
function renderutil.draw_dotted_line(x, y, length, axis, color)
if axis == 'x' then
for xx = x, x + length, 2 do
renderer.draw_rect(xx, y, 1, 1, color)
end
elseif axis == 'y' then
for yy = y, y + length, 2 do
renderer.draw_rect(x, yy, 1, 1, color)
end
end
end
local function plot(x, y, color)
renderer.draw_rect(x, y, 1, 1, color)
end
function renderutil.draw_quarter_circle(x, y, r, color, flipy)
-- inefficient for large circles, but it works.
color = { table.unpack(color) }
local a = color[4]
for dx = 0, r - 1 do
for dy = 0, r - 1 do
local xx = r - 1 - dx
local yy = dy
if not flipy then
yy = r - 1 - dy
end
local t = math.abs(math.sqrt(xx*xx + yy*yy) - r + 1)
t = common.clamp(1 - t, 0, 1)
if t > 0 then
color[4] = a * t
plot(x + dx, y + dy, color)
end
end
end
end
return renderutil

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB