├── README.md ├── fsutil.lua ├── init.lua ├── json.lua ├── linter.lua ├── linters ├── luacheck.lua ├── nelua.lua ├── nim.lua ├── php.lua ├── python.lua ├── rust.lua ├── shellcheck.lua ├── teal.lua ├── typescript.lua ├── v.lua └── zig.lua ├── liteipc.lua ├── manifest.json ├── renderutil.lua └── screenshots ├── 1.png └── 2.png /README.md: -------------------------------------------------------------------------------- 1 | # lint+ 2 | 3 | An improved linting plugin for [Lite XL](https://github.com/lite-xl/lite-xl). 4 | 5 | Includes compatibility layer for [`linter`](https://github.com/drmargarido/linters). 6 | 7 | ## Screenshots 8 | 9 | ![1st screenshot](screenshots/1.png) 10 |

11 | Features ErrorLens-style warnings and error messages for quickly scanning 12 | through code for errors. 13 |

14 | 15 |
16 | 17 | ![2nd screenshot](screenshots/2.png) 18 |

19 | The status view shows either the first error, or the full message of the error 20 | under your text cursor. No mouse interaction needed! 21 |

22 | 23 | 24 | ## Motivation 25 | 26 | There were a few problems I had with the existing `linter` plugin: 27 | 28 | - It can only show "warnings" - there's no severity levels 29 | (info/hint/warning/error). 30 | - It doesn't show the messages after lines (ErrorLens style), you have to hover 31 | over the warning first. 32 | - It spam-runs the linter command, but Nim (and possibly other languages) 33 | compiles relatively slowly, which lags the editor to hell. 34 | - It doesn't display the first or current error message on the status view. 35 | 36 | lint+ aims to fix all of the above problems. 37 | 38 | ### Why not just fix `linter`? 39 | 40 | - It works fundamentally differently from lint+, so fixing it would be more 41 | costly than just making a new plugin. 42 | - I haven't ever made my own linter support plugin, so this was a good exercise. 43 | 44 | ## Installation 45 | 46 | Navigate to your `plugins` folder, and clone the repository: 47 | 48 | ```sh 49 | $ git clone https://github.com/liquidev/lintplus 50 | ``` 51 | 52 | To enable the different linters available on the [linters](linters/) 53 | subdirectory, you have to load them on your lite-xl user module file (`init.lua`). 54 | 55 | You can load a single linter: 56 | ```lua 57 | local lintplus = require "plugins.lintplus" 58 | lintplus.load("luacheck") 59 | ``` 60 | or multiple linters by passing a table: 61 | ```lua 62 | local lintplus = require "plugins.lintplus" 63 | lintplus.load({"php", "luacheck"}) 64 | ``` 65 | 66 | If you want to use plugins designed for the other `linter`, you will also need 67 | to enable the compatibility plugin `linter.lua` *from this repository*. 68 | 69 | ```sh 70 | $ ln -s $PWD/{lintplus/linter,linter}.lua 71 | ``` 72 | 73 | Keep in mind that plugins designed for `linter` will not work as well as lint+ 74 | plugins, because of `linter`'s lack of multiple severity levels. All warnings 75 | reported by `linter` linters will be reported with the `warning` level. 76 | 77 | ### Automatic Linting 78 | 79 | To enable automatic linting upon opening/saving a file, add the following 80 | code tou your lite-xl user module: 81 | ```lua 82 | local lintplus = require "plugins.lintplus" 83 | lintplus.setup.lint_on_doc_load() 84 | lintplus.setup.lint_on_doc_save() 85 | ``` 86 | This overrides `Doc.load` and `Doc.save` with some extra behavior to enable 87 | automatic linting. 88 | 89 | ## Commands 90 | 91 | Available commands from the lite-xl commands palette (ctrl+shift+p): 92 | 93 | * `lint+:check` - run the appropriate linter command for the current document 94 | * `lint+:goto-previous-message` (alt+up) - jump to previous message on current document 95 | * `lint+:goto-next-message` (alt+down) - jump to next message on current document 96 | 97 | ## Configuration 98 | 99 | lint+ itself looks for the following configuration options: 100 | 101 | - `config.lint.kind_pretty_names` 102 | - table: 103 | - `info`: string = `"I"` 104 | - `hint`: string = `"H"` 105 | - `warning`: string = `"W"` 106 | - `error`: string = `"E"` 107 | - controls the prefix prepended to messages displayed on the status bar. 108 | for example, setting `error` to `Error` will display `Error: …` or 109 | `line 10 Error: …` instead of `E: …` or `line 10 E: …`. 110 | - `config.lint.lens_style` 111 | - string: 112 | - `"blank"`: do not draw underline on line messages 113 | - `"solid"`: draw single line underline on line messages (default) 114 | - `"dots"`: draw dotted underline on line messages (slower performance) 115 | - function(x, y, width, color): a custom drawing routine 116 | - `x`: number 117 | - `y`: number 118 | - `width`: number 119 | - `color`: renderer.color 120 | 121 | All options are unset (`nil`) by default, so eg. setting 122 | `config.lint.kind_pretty_names.hint` will *not* work because 123 | `config.lint.kind_pretty_names` does not exist. 124 | 125 | Individual plugins may also look for options in the `config.lint` table. 126 | Refer to each plugin's source code for more information. 127 | 128 | ### Styling 129 | 130 | The screenshots above use a theme with extra colors for the linter's messages. 131 | The default color is the same color used for literals, which isn't always what 132 | you want. Most of the time you want to have some clear visual distinction 133 | between severity levels, so lint+ is fully stylable. 134 | 135 | - `style.lint` 136 | - table: 137 | - `info`: Color - the color used for infos 138 | - `hint`: Color - the color used for hints 139 | - `warning`: Color - the color used for warnings 140 | - `error`: Color - the color used for errors 141 | 142 | Example: 143 | 144 | ```lua 145 | local common = require "core.common" 146 | local style = require "core.style" 147 | style.lint = { 148 | info = style.syntax["keyword2"], 149 | hint = style.syntax["function"], 150 | warning = style.syntax["function"], 151 | error = { common.color "#FF3333" } 152 | } 153 | ``` 154 | 155 | As with config, you need to provide all or no colors. 156 | 157 | ## Creating new linters 158 | 159 | Just like `linter`, lint+ allows you to create new linters for languages not 160 | supported out of the box. The API is very simple: 161 | 162 | ```lua 163 | Severity: enum { 164 | "info", -- suggestions on how to fix things, may be used in tandem with 165 | -- other messages 166 | "hint", -- suggestions on small things that don't affect program behavior 167 | "warning", -- warnings about possible mistakes that may affect behavior 168 | "error", -- syntax or semantic errors that prevent compilation 169 | } 170 | 171 | LintContext: table { 172 | :gutter_rail(): number 173 | -- creates a new gutter rail and returns its index 174 | :gutter_rail_count(): number 175 | -- returns how many gutter rails have been created in this context 176 | -- You may create additional fields in this table, but keys prefixed with _ 177 | -- are reserved by lint+. 178 | } 179 | 180 | lintplus.add(linter_name: string)(linter: table { 181 | filename: pattern, 182 | procedure: table { 183 | command: function (filename: string): {string}, 184 | -- Returns the lint command for the given filename. 185 | interpreter: (function (filename, line: string, context: LintContext): 186 | function (): 187 | nil or 188 | (filename: string, line, column: number, 189 | kind: Severity, message: string, rail: number or nil)) or "bail" 190 | -- Creates and returns a message iterator, which yields all messages 191 | -- from the line. 192 | -- If the return value is "bail", reading the lint command is aborted 193 | -- immediately. This is done as a mitigation for processes that may take 194 | -- too long to execute or block indefinitely. 195 | -- `rail` is optional and specifies the gutter rail to which the message 196 | -- should be attached. 197 | } 198 | }) 199 | ``` 200 | 201 | Because writing command and interpreter functions can quickly get tedious, there 202 | are some helpers that return pre-built functions for you: 203 | 204 | ```lua 205 | lintplus.command(cmd: {string}): function (string): {string} 206 | -- Returns a function that replaces `lintplus.filename` in the given table 207 | -- with the linted file's name. 208 | lintplus.interpreter(spec: table { 209 | info: pattern or nil, 210 | hint: pattern or nil, 211 | warning: pattern or nil, 212 | error: pattern or nil, 213 | -- Defines patterns for all the severity levels. Each pattern must have 214 | -- four captures: the first one being the filename, the second and third 215 | -- being the line and column, and the fourth being the message. 216 | -- When any of these are nil, the interpreter simply will not produce the 217 | -- given severity levels. 218 | strip: pattern or nil, 219 | -- Defines a pattern for stripping unnecessary information from the message 220 | -- capture from one of the previously defined patterns. When this is `nil`, 221 | -- nothing is stripped and the message remains as-is. 222 | }) 223 | ``` 224 | 225 | An example linter built with these primitives: 226 | 227 | ```lua 228 | lintplus.add("nim") { 229 | filename = "%.nim$", 230 | procedure = { 231 | command = lintplus.command { 232 | "nim", "check", "--listFullPaths", "--stdout", lintplus.filename 233 | }, 234 | interpreter = lintplus.interpreter { 235 | -- The format for these three in Nim is almost exactly the same: 236 | hint = "(.-)%((%d+), (%d+)%) Hint: (.+)", 237 | warning = "(.-)%((%d+), (%d+)%) Warning: (.+)", 238 | error = "(.-)%((%d+), (%d+)%) Error: (.+)", 239 | -- We want to strip annotations like [XDeclaredButNotUsed] from the end: 240 | strip = "%s%[%w+%]$", 241 | -- Note that info was omitted. This is because all of the severity levels 242 | -- are optional, so eg. you don't have to provide an info pattern. 243 | }, 244 | }, 245 | } 246 | ``` 247 | 248 | If you want to let the user of your linter specify some extra arguments, 249 | `lintplus.args_command` can be used instead of `lintplus.command`: 250 | 251 | ```lua 252 | -- ... 253 | command = lintplus.args_command( 254 | { "luacheck", 255 | lintplus.args, 256 | "--formatter=visual_studio", 257 | lintplus.filename }, 258 | "luacheck_args" 259 | ) 260 | -- ... 261 | ``` 262 | 263 | To enable plugins for different languages, do the same thing, but with 264 | `lintplus_*.lua`. For example, to enable support for Nim and Rust: 265 | The second argument to this function is the name of the field in the 266 | `config.lint` table. Then, the user provides arguments like so: 267 | 268 | ```lua 269 | config.lint.luacheck_args = { "--max-line-length=80", "--std=love" } 270 | ``` 271 | 272 | ## Known problems 273 | 274 | - Due to the fact that it shows the most severe message at the end of the 275 | line, displaying more than one message per line is really difficult with 276 | the limited horizontal real estate, so it can only display one message per 277 | line. 278 | - It is unable to underline the offending token, simply because some linter 279 | error messages do not contain enough information about where the error start 280 | and end is. It will highlight the correct line and column, though. 281 | 282 | -------------------------------------------------------------------------------- /fsutil.lua: -------------------------------------------------------------------------------- 1 | -- file system utilities 2 | 3 | local fs = {} 4 | 5 | function fs.normalize_path(path) 6 | if PLATFORM == "Windows" then 7 | return path:gsub('\\', '/') 8 | else 9 | return path 10 | end 11 | end 12 | 13 | function fs.parent_directory(path) 14 | path = fs.normalize_path(path) 15 | path = path:match("^(.-)/*$") 16 | local last_slash_pos = -1 17 | for i = #path, 1, -1 do 18 | if path:sub(i, i) == '/' then 19 | last_slash_pos = i 20 | break 21 | end 22 | end 23 | if last_slash_pos < 0 then 24 | return nil 25 | end 26 | return path:sub(1, last_slash_pos - 1) 27 | end 28 | 29 | return fs 30 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- mod-version:3 2 | 3 | -- lint+ - an improved linter for lite 4 | -- copyright (C) lqdev, 2020 5 | -- licensed under the MIT license 6 | 7 | 8 | --- STATIC CONFIG --- 9 | 10 | 11 | local kind_priority = { 12 | info = -1, 13 | hint = 0, 14 | warning = 1, 15 | error = 2, 16 | } 17 | 18 | local default_kind_pretty_names = { 19 | info = "I", 20 | hint = "H", 21 | warning = "W", 22 | error = "E", 23 | } 24 | 25 | 26 | --- IMPLEMENTATION --- 27 | 28 | 29 | local core = require "core" 30 | local command = require "core.command" 31 | local common = require "core.common" 32 | local config = require "core.config" 33 | local style = require "core.style" 34 | local keymap = require "core.keymap" 35 | local syntax = require "core.syntax" 36 | 37 | local Doc = require "core.doc" 38 | local DocView = require "core.docview" 39 | local StatusView = require "core.statusview" 40 | 41 | local liteipc = require "plugins.lintplus.liteipc" 42 | 43 | 44 | local lint = {} 45 | lint.fs = require "plugins.lintplus.fsutil" 46 | lint.ipc = liteipc 47 | 48 | 49 | lint.index = {} 50 | lint.messages = {} 51 | 52 | 53 | local LintContext = {} 54 | LintContext.__index = LintContext 55 | 56 | 57 | function LintContext:create_gutter_rail() 58 | if not self._doc then return 0 end 59 | local lp = self._doc.__lintplus 60 | lp.rail_count = lp.rail_count + 1 61 | return lp.rail_count 62 | end 63 | 64 | 65 | function LintContext:gutter_rail_count() 66 | if not self._doc then return 0 end 67 | return self._doc.__lintplus.rail_count 68 | end 69 | 70 | 71 | -- Can be used by other plugins to properly set the context when loading a doc 72 | function lint.init_doc(filename, doc) 73 | filename = core.project_absolute_path(filename) 74 | local context = setmetatable({ 75 | _doc = doc or nil, 76 | _user_context = nil, 77 | }, LintContext) 78 | 79 | if doc then 80 | doc.__lintplus_context = {} 81 | context._user_context = doc.__lintplus_context 82 | 83 | doc.__lintplus = { 84 | rail_count = 0, 85 | } 86 | end 87 | 88 | if not lint.messages[filename] then 89 | lint.messages[filename] = { 90 | context = context, 91 | lines = {}, 92 | rails = {}, 93 | } 94 | elseif doc then 95 | lint.messages[filename].context = context 96 | end 97 | end 98 | 99 | 100 | -- Returns an appropriate linter for the given doc, or nil if no linter is 101 | -- found. 102 | function lint.get_linter_for_doc(doc) 103 | if not doc.filename then 104 | return nil 105 | end 106 | 107 | local file = core.project_absolute_path(doc.filename) 108 | for name, linter in pairs(lint.index) do 109 | if common.match_pattern(file, linter.filename) then 110 | return linter, name 111 | end 112 | if linter.syntax ~= nil then 113 | local header = doc:get_text(1, 1, doc:position_offset(1, 1, 128)) 114 | local syn = syntax.get(doc.filename, header) 115 | for i = #linter.syntax, 1, -1 do 116 | local s = linter.syntax[i] 117 | if syn.name == s then 118 | return linter, name 119 | end 120 | end 121 | end 122 | end 123 | end 124 | 125 | 126 | -- unused for now, because it was a bit buggy 127 | -- Note: Should be fixed now 128 | function lint.clear_messages(filename) 129 | filename = core.project_absolute_path(filename) 130 | 131 | if lint.messages[filename] then 132 | lint.messages[filename].lines = {} 133 | lint.messages[filename].rails = {} 134 | end 135 | end 136 | 137 | 138 | function lint.add_message(filename, line, column, kind, message, rail) 139 | filename = core.project_absolute_path(filename) 140 | if not lint.messages[filename] then 141 | -- This allows us to at least store messages until context is properly 142 | -- set from the calling plugin. 143 | lint.init_doc(filename) 144 | end 145 | local file_messages = lint.messages[filename] 146 | local lines, rails = file_messages.lines, file_messages.rails 147 | lines[line] = lines[line] or {} 148 | if rail ~= nil then 149 | rails[rail] = rails[rail] or { lines_taken = {} } 150 | if not rails[rail].lines_taken[line] then 151 | rails[rail].lines_taken[line] = true 152 | table.insert(rails[rail], { 153 | line = line, 154 | column = column, 155 | kind = kind, 156 | }) 157 | end 158 | end 159 | table.insert(lines[line], { 160 | column = column, 161 | kind = kind, 162 | message = message, 163 | rail = rail, 164 | }) 165 | end 166 | 167 | 168 | local function process_line(doc, linter, line, context) 169 | local file = core.project_absolute_path(doc.filename) 170 | 171 | local had_messages = false 172 | 173 | local iterator = linter.procedure.interpreter(file, line, context) 174 | if iterator == "bail" then return iterator end 175 | 176 | if os.getenv("LINTPLUS_DEBUG_LINES") then 177 | print("lint+ | "..line) 178 | end 179 | 180 | for rawfile, lineno, columnno, kind, message, rail in iterator do 181 | assert(type(rawfile) == "string") 182 | local absfile = core.project_absolute_path(rawfile) 183 | if absfile == file then -- TODO: support project-wide errors 184 | assert(type(lineno) == "number") 185 | assert(type(columnno) == "number") 186 | assert(type(kind) == "string") 187 | assert(type(message) == "string") 188 | assert(rail == nil or type(rail) == "number") 189 | 190 | lint.add_message(absfile, lineno, columnno, kind, message, rail) 191 | core.redraw = true 192 | end 193 | end 194 | 195 | return had_messages 196 | end 197 | 198 | 199 | local function compare_message_priorities(a, b) 200 | return kind_priority[a.kind] > kind_priority[b.kind] 201 | end 202 | 203 | local function compare_messages(a, b) 204 | if a.column == b.column then 205 | return compare_message_priorities(a, b) 206 | end 207 | return a.column > b.column 208 | end 209 | 210 | local function compare_rail_messages(a, b) 211 | return a.line < b.line 212 | end 213 | 214 | 215 | function lint.check(doc) 216 | if doc.filename == nil then return end 217 | 218 | local linter, linter_name = lint.get_linter_for_doc(doc) 219 | if linter == nil then 220 | core.error("no linter available for the given filetype") 221 | return 222 | end 223 | 224 | local filename = core.project_absolute_path(doc.filename) 225 | local context = setmetatable({ 226 | _doc = doc, 227 | _user_context = doc.__lintplus_context, 228 | }, LintContext) 229 | 230 | doc.__lintplus = { 231 | rail_count = 0, 232 | } 233 | -- clear_messages(linter) 234 | lint.messages[filename] = { 235 | context = context, 236 | lines = {}, 237 | rails = {}, 238 | } 239 | 240 | local function report_error(msg) 241 | core.log_quiet( 242 | "lint+/" .. linter_name .. ": " .. 243 | doc.filename .. ": " .. msg 244 | ) 245 | end 246 | 247 | local cmd, cwd = linter.procedure.command(filename), nil 248 | if cmd.set_cwd then 249 | cwd = lint.fs.parent_directory(filename) 250 | end 251 | local process = liteipc.start_process(cmd, cwd) 252 | core.add_thread(function () 253 | -- poll the process for lines of output 254 | while true do 255 | local exit, code, errmsg = process:poll(function (line) 256 | process_line(doc, linter, line, context) 257 | end) 258 | if exit ~= nil then 259 | -- If linter exited with exit code non 0 or 1 log it 260 | if exit == "signal" then 261 | report_error( 262 | "linter exited with signal " .. code 263 | .. (errmsg and " : " .. errmsg or "") 264 | ) 265 | end 266 | break 267 | end 268 | coroutine.yield(0) 269 | end 270 | -- after reading some lines, sort messages by priority in all files 271 | -- and sort rail connections by line number 272 | for _, file_messages in pairs(lint.messages) do 273 | for _, messages in pairs(file_messages.lines) do 274 | table.sort(messages, compare_messages) 275 | end 276 | for _, rail in pairs(file_messages.rails) do 277 | table.sort(rail, compare_rail_messages) 278 | end 279 | file_messages.rails_sorted = true 280 | core.redraw = true 281 | coroutine.yield(0) 282 | end 283 | end) 284 | end 285 | 286 | 287 | -- inject initialization routines to documents 288 | 289 | local Doc_load, Doc_save, Doc_on_close = Doc.load, Doc.save, Doc.on_close 290 | 291 | local function init_linter_for_doc(doc) 292 | local linter, _ = lint.get_linter_for_doc(doc) 293 | if linter == nil then return end 294 | doc.__lintplus_context = {} 295 | if linter.procedure.init ~= nil then 296 | linter.procedure.init( 297 | core.project_absolute_path(doc.filename), 298 | doc.__lintplus_context 299 | ) 300 | end 301 | end 302 | 303 | function Doc:load(filename) 304 | local old_filename = self.filename 305 | Doc_load(self, filename) 306 | if old_filename ~= filename then 307 | init_linter_for_doc(self) 308 | end 309 | end 310 | 311 | function Doc:save(filename, abs_filename) 312 | local old_filename = self.filename 313 | Doc_save(self, filename, abs_filename) 314 | if old_filename ~= filename then 315 | init_linter_for_doc(self) 316 | end 317 | end 318 | 319 | function Doc:on_close() 320 | Doc_on_close(self) 321 | if not self.filename then return end 322 | local filename = core.project_absolute_path(self.filename) 323 | -- release Doc object for proper garbage collection 324 | if lint.messages[filename] then 325 | lint.messages[filename] = nil 326 | end 327 | end 328 | 329 | 330 | -- inject hooks to Doc.insert and Doc.remove to shift messages around 331 | 332 | local function sort_positions(line1, col1, line2, col2) 333 | if line1 > line2 334 | or line1 == line2 and col1 > col2 then 335 | return line2, col2, line1, col1, true 336 | end 337 | return line1, col1, line2, col2, false 338 | end 339 | 340 | local Doc_insert = Doc.insert 341 | function Doc:insert(line, column, text) 342 | Doc_insert(self, line, column, text) 343 | 344 | if self.filename == nil then return end 345 | if line == math.huge then return end 346 | 347 | local filename = core.project_absolute_path(self.filename) 348 | local file_messages = lint.messages[filename] 349 | local lp = self.__lintplus 350 | if file_messages == nil or lp == nil then return end 351 | 352 | -- shift line messages downwards 353 | local shift = 0 354 | for _ in text:gmatch('\n') do 355 | shift = shift + 1 356 | end 357 | if shift == 0 then return end 358 | 359 | local lines = file_messages.lines 360 | for i = #self.lines, line, -1 do 361 | if lines[i] ~= nil then 362 | if not (i == line and lines[i][1].column < column) then 363 | lines[i + shift] = lines[i] 364 | lines[i] = nil 365 | end 366 | end 367 | end 368 | 369 | -- shift rails downwards 370 | local rails = file_messages.rails 371 | for _, rail in pairs(rails) do 372 | for _, message in ipairs(rail) do 373 | if message.line >= line then 374 | message.line = message.line + shift 375 | end 376 | end 377 | end 378 | end 379 | 380 | local function update_messages_after_removal( 381 | doc, 382 | line1, column1, 383 | line2, column2 384 | ) 385 | if line1 == line2 then return end 386 | if line2 == math.huge then return end 387 | if doc.filename == nil then return end 388 | 389 | local filename = core.project_absolute_path(doc.filename) 390 | local file_messages = lint.messages[filename] 391 | local lp = doc.__lintplus 392 | if file_messages == nil or lp == nil then return end 393 | 394 | local lines = file_messages.lines 395 | 396 | line1, column1, line2, column2 = 397 | sort_positions(line1, column1, line2, column2) 398 | local shift = line2 - line1 399 | 400 | -- remove all messages in this range 401 | for i = line1, line2 do 402 | lines[i] = nil 403 | end 404 | 405 | -- shift all line messages up 406 | for i = line1, #doc.lines do 407 | if lines[i] ~= nil then 408 | lines[i - shift] = lines[i] 409 | lines[i] = nil 410 | end 411 | end 412 | 413 | -- remove all rail messages in this range 414 | local rails = file_messages.rails 415 | for _, rail in pairs(rails) do 416 | local remove_indices = {} 417 | for i, message in ipairs(rail) do 418 | if message.line >= line1 and message.line < line2 then 419 | table.insert(remove_indices, i) 420 | elseif message.line > line1 then 421 | message.line = message.line - shift 422 | end 423 | end 424 | for i = #remove_indices, 1, -1 do 425 | table.remove(rail, remove_indices[i]) 426 | end 427 | end 428 | end 429 | 430 | local Doc_remove = Doc.remove 431 | function Doc:remove(line1, column1, line2, column2) 432 | update_messages_after_removal(self, line1, column1, line2, column2) 433 | Doc_remove(self, line1, column1, line2, column2) 434 | end 435 | 436 | 437 | -- inject rendering routines 438 | 439 | local renderutil = require "plugins.lintplus.renderutil" 440 | 441 | local function rail_width(dv) 442 | return dv:get_line_height() / 3 -- common.round(style.padding.x / 2) 443 | end 444 | 445 | local function rail_spacing(dv) 446 | return common.round(rail_width(dv) / 4) 447 | end 448 | 449 | local DocView_get_gutter_width = DocView.get_gutter_width 450 | function DocView:get_gutter_width() 451 | local extra_width = 0 452 | if self.doc.filename ~= nil then 453 | local file_messages = lint.messages[core.project_absolute_path(self.doc.filename)] 454 | if file_messages ~= nil then 455 | local rail_count = file_messages.context:gutter_rail_count() 456 | extra_width = rail_count * (rail_width(self) + rail_spacing(self)) 457 | end 458 | end 459 | local original_width, padding = DocView_get_gutter_width(self) 460 | return original_width + extra_width, padding 461 | end 462 | 463 | 464 | local function get_gutter_rail_x(dv, index) 465 | return 466 | dv.position.x + dv:get_gutter_width() - 467 | (rail_width(dv) + rail_spacing(dv)) * index + rail_spacing(dv) 468 | end 469 | 470 | 471 | local function get_message_group_color(messages) 472 | if style.lint ~= nil then 473 | return style.lint[messages[1].kind] 474 | else 475 | local default_colors = { 476 | info = style.syntax["normal"], 477 | hint = style.syntax["function"], 478 | warning = style.syntax["number"], 479 | error = style.syntax["keyword2"] 480 | } 481 | return default_colors[messages[1].kind] 482 | end 483 | end 484 | 485 | local function get_underline_y(dv, line) 486 | local _, y = dv:get_line_screen_position(line) 487 | local line_height = dv:get_line_height() 488 | local extra_space = line_height - dv:get_font():get_height() 489 | return y + line_height - extra_space / 2 490 | end 491 | 492 | local function draw_gutter_rail(dv, index, messages) 493 | local rail = messages.rails[index] 494 | if rail == nil or #rail < 2 then return end 495 | 496 | local first_message = rail[1] 497 | local last_message = rail[#rail] 498 | 499 | local x = get_gutter_rail_x(dv, index) 500 | local rw = rail_width(dv) 501 | local start_y = get_underline_y(dv, first_message.line) 502 | local fin_y = get_underline_y(dv, last_message.line) 503 | 504 | -- connect with lens 505 | local line_x = x + rw 506 | for i, message in ipairs(rail) do 507 | -- connect with lens 508 | local lx, _ = dv:get_line_screen_position(message.line) 509 | local ly = get_underline_y(dv, message.line) 510 | local line_messages = messages.lines[message.line] 511 | if line_messages ~= nil then 512 | local column = line_messages[1].column 513 | local message_left = line_messages[1].message:sub(1, column - 1) 514 | local line_color = get_message_group_color(line_messages) 515 | local xoffset = (x + rw) % 2 516 | local line_w = dv:get_font():get_width(message_left) - line_x + lx 517 | renderutil.draw_dotted_line(x + rw + xoffset, ly, line_w, 'x', line_color) 518 | -- draw curve 519 | ly = ly - rw * (i == 1 and 0 or 1) + (i ~= 1 and 1 or 0) 520 | renderutil.draw_quarter_circle(x, ly, rw, style.accent, i > 1) 521 | end 522 | end 523 | 524 | -- draw vertical part 525 | local height = fin_y - start_y + 1 - rw * 2 526 | renderer.draw_rect(x, start_y + rw, 1, height, style.accent) 527 | 528 | end 529 | 530 | local DocView_draw = DocView.draw 531 | function DocView:draw() 532 | DocView_draw(self) 533 | 534 | local filename = self.doc.filename 535 | if filename == nil then return end 536 | filename = core.project_absolute_path(filename) 537 | local messages = lint.messages[filename] 538 | if messages == nil or not messages.rails_sorted then return end 539 | local rails = messages.rails 540 | 541 | local pos, size = self.position, self.size 542 | core.push_clip_rect(pos.x, pos.y, size.x, size.y) 543 | for i = 1, #rails do 544 | draw_gutter_rail(self, i, messages) 545 | end 546 | core.pop_clip_rect() 547 | end 548 | 549 | 550 | local lens_underlines = { 551 | 552 | blank = function () end, 553 | 554 | solid = function (x, y, width, color) 555 | renderer.draw_rect(x, y, width, 1, color) 556 | end, 557 | 558 | dots = function (x, y, width, color) 559 | renderutil.draw_dotted_line(x, y, width, 'x', color) 560 | end, 561 | 562 | } 563 | 564 | local function draw_lens_underline(x, y, width, color) 565 | local lens_style = config.lint.lens_style or "solid" 566 | if type(lens_style) == "string" then 567 | local fn = lens_underlines[lens_style] or lens_underlines.blank 568 | fn(x, y, width, color) 569 | elseif type(lens_style) == "function" then 570 | lens_style(x, y, width, color) 571 | end 572 | end 573 | 574 | local function get_or_default(t, index, default) 575 | if t ~= nil and t[index] ~= nil then 576 | return t[index] 577 | else 578 | return default 579 | end 580 | end 581 | 582 | local DocView_draw_line_text = DocView.draw_line_text 583 | function DocView:draw_line_text(idx, x, y) 584 | local line_height = DocView_draw_line_text(self, idx, x, y) 585 | 586 | local lp = self.doc.__lintplus 587 | if lp == nil then return line_height end 588 | 589 | local yy = get_underline_y(self, idx) 590 | local file_messages = lint.messages[core.project_absolute_path(self.doc.filename)] 591 | if file_messages == nil then return line_height end 592 | local messages = file_messages.lines[idx] 593 | if messages == nil then return line_height end 594 | 595 | local underline_start = messages[1].column 596 | 597 | local font = self:get_font() 598 | local underline_color = get_message_group_color(messages) 599 | local line = self.doc.lines[idx] 600 | local line_left = line:sub(1, underline_start - 1) 601 | local line_right = line:sub(underline_start, -2) 602 | local underline_x = font:get_width(line_left) 603 | local w = font:get_width('w') 604 | 605 | local msg_x = x + w * 3 + underline_x + font:get_width(line_right) 606 | local text_y = y + self:get_line_text_y_offset() 607 | for i, msg in ipairs(messages) do 608 | local text_color = get_or_default(style.lint, msg.kind, underline_color) 609 | msg_x = renderer.draw_text(font, msg.message, msg_x, text_y, text_color) 610 | if i < #messages then 611 | msg_x = renderer.draw_text(font, ", ", msg_x, text_y, style.syntax.comment) 612 | end 613 | end 614 | 615 | local underline_width = msg_x - x - underline_x 616 | draw_lens_underline(x + underline_x, yy, underline_width, underline_color) 617 | return line_height 618 | end 619 | 620 | 621 | local function table_add(t, d) 622 | for _, v in ipairs(d) do 623 | table.insert(t, v) 624 | end 625 | end 626 | 627 | 628 | local function kind_pretty_name(kind) 629 | return (config.kind_pretty_names or default_kind_pretty_names)[kind] 630 | end 631 | 632 | 633 | local function get_error_messages(doc, ordered) 634 | if not doc then return nil end 635 | local messages = lint.messages[core.project_absolute_path(doc.filename)] 636 | if not messages then return nil end 637 | if not ordered then return messages.lines end 638 | -- sort lines 639 | local lines = {} 640 | for line, _ in pairs(messages.lines) do 641 | table.insert(lines, line) 642 | end 643 | table.sort(lines, function(a, b) return a < b end) 644 | local lines_info = {} 645 | -- store in array instead of dictionary to keep insertion order 646 | for _, line in ipairs(lines) do 647 | table.insert( 648 | lines_info, 649 | {line = line, table.unpack(messages.lines[line])} 650 | ) 651 | end 652 | return lines_info 653 | end 654 | 655 | 656 | local function get_current_error(doc) 657 | local file_messages = get_error_messages(doc) 658 | local line, message = math.huge, nil 659 | for ln, messages in pairs(file_messages) do 660 | local msg = messages[1] 661 | if msg.kind == "error" and ln < line then 662 | line, message = ln, msg 663 | end 664 | end 665 | if message ~= nil then 666 | return line, message.kind, message.message 667 | end 668 | return nil, nil, nil 669 | end 670 | 671 | 672 | local function goto_prev_message() 673 | local doc = core.active_view.doc 674 | local current_line = doc:get_selection() 675 | local file_messages = get_error_messages(doc, true) 676 | if file_messages ~= nil then 677 | local prev = nil 678 | local found = false 679 | local last = nil 680 | for _, line_info in pairs(file_messages) do 681 | local line = line_info.line 682 | if current_line <= line then 683 | found = true 684 | end 685 | if not found then 686 | prev = line 687 | end 688 | last = line 689 | end 690 | local line = prev or last 691 | if line then 692 | doc:set_selection(line, 1, line, 1) 693 | end 694 | end 695 | end 696 | 697 | 698 | local function goto_next_message() 699 | local doc = core.active_view.doc 700 | local current_line = doc:get_selection() 701 | local file_messages = get_error_messages(doc, true) 702 | if file_messages ~= nil then 703 | local first = nil 704 | local next = nil 705 | for _, line_info in pairs(file_messages) do 706 | local line = line_info.line 707 | if not first then 708 | first = line 709 | end 710 | if line > current_line then 711 | next = line 712 | break 713 | end 714 | end 715 | local line = next or first 716 | if line then 717 | doc:set_selection(line, 1, line, 1) 718 | end 719 | end 720 | end 721 | 722 | 723 | local function get_status_view_items() 724 | local doc = core.active_view.doc 725 | local line1, _, line2, _ = doc:get_selection() 726 | local file_messages = get_error_messages(doc) 727 | if file_messages ~= nil then 728 | if file_messages[line1] ~= nil and line1 == line2 then 729 | local msg = file_messages[line1][1] 730 | return { 731 | kind_pretty_name(msg.kind), ": ", 732 | style.text, msg.message, 733 | } 734 | else 735 | local line, kind, message = get_current_error(doc) 736 | if line ~= nil then 737 | return { 738 | "line ", tostring(line), " ", kind_pretty_name(kind), ": ", 739 | style.text, message, 740 | } 741 | end 742 | end 743 | end 744 | return {} 745 | end 746 | 747 | if StatusView["add_item"] then 748 | core.status_view:add_item({ 749 | predicate = function() 750 | local doc = core.active_view.doc 751 | if 752 | doc and doc.filename -- skip new files 753 | and 754 | getmetatable(core.active_view) == DocView 755 | and 756 | ( 757 | lint.get_linter_for_doc(doc) 758 | or 759 | lint.messages[core.project_absolute_path(doc.filename)] 760 | ) 761 | then 762 | return true 763 | end 764 | return false 765 | end, 766 | name = "lint+:message", 767 | alignment = StatusView.Item.LEFT, 768 | get_item = get_status_view_items, 769 | command = function() 770 | local doc = core.active_view.doc 771 | local line = get_current_error(doc) 772 | if line ~= nil then 773 | doc:set_selection(line, 1, line, 1) 774 | end 775 | end, 776 | position = -1, 777 | tooltip = "Lint+ error message", 778 | separator = core.status_view.separator2 779 | }) 780 | else 781 | local StatusView_get_items = StatusView.get_items 782 | function StatusView:get_items() 783 | local left, right = StatusView_get_items(self) 784 | local doc = core.active_view.doc 785 | 786 | if 787 | doc and doc.filename -- skip new files 788 | and 789 | getmetatable(core.active_view) == DocView 790 | and 791 | ( 792 | lint.get_linter_for_doc(doc) 793 | or 794 | lint.messages[core.project_absolute_path(doc.filename)] 795 | ) 796 | then 797 | local items = get_status_view_items() 798 | if #items > 0 then 799 | table.insert(left, {style.dim, self.separator2, table.unpack(items)}) 800 | end 801 | end 802 | 803 | return left, right 804 | end 805 | end 806 | 807 | 808 | command.add(DocView, { 809 | ["lint+:check"] = function () 810 | lint.check(core.active_view.doc) 811 | end 812 | }) 813 | 814 | command.add(DocView, { 815 | ["lint+:goto-previous-message"] = function () 816 | goto_prev_message() 817 | end 818 | }) 819 | 820 | command.add(DocView, { 821 | ["lint+:goto-next-message"] = function () 822 | goto_next_message() 823 | end 824 | }) 825 | 826 | keymap.add { 827 | ["alt+up"] = "lint+:goto-previous-message", 828 | ["alt+down"] = "lint+:goto-next-message" 829 | } 830 | 831 | 832 | --- LINTER PLUGINS --- 833 | 834 | 835 | function lint.add(name) 836 | return function (linter) 837 | lint.index[name] = linter 838 | end 839 | end 840 | 841 | 842 | --- SETUP --- 843 | 844 | 845 | lint.setup = {} 846 | 847 | function lint.setup.lint_on_doc_load() 848 | 849 | local doc_load = Doc.load 850 | function Doc:load(filename) 851 | doc_load(self, filename) 852 | if not self.filename then return end 853 | if lint.get_linter_for_doc(self) ~= nil then 854 | lint.check(self) 855 | end 856 | end 857 | 858 | end 859 | 860 | function lint.setup.lint_on_doc_save() 861 | 862 | local doc_save = Doc.save 863 | function Doc:save(filename, abs_filename) 864 | doc_save(self, filename, abs_filename) 865 | if lint.get_linter_for_doc(self) ~= nil then 866 | lint.check(self) 867 | end 868 | end 869 | 870 | end 871 | 872 | function lint.enable_async() 873 | core.error("lint+: calling enable_async() is not needed anymore") 874 | end 875 | 876 | 877 | --- LINTER CREATION UTILITIES --- 878 | 879 | 880 | lint.filename = {} 881 | lint.args = {} 882 | 883 | 884 | local function map(tab, fn) 885 | local result = {} 886 | for k, v in pairs(tab) do 887 | local mapped, mode = fn(k, v) 888 | if mode == "append" then 889 | table_add(result, mapped) 890 | elseif type(k) == "number" then 891 | table.insert(result, mapped) 892 | else 893 | result[k] = mapped 894 | end 895 | end 896 | return result 897 | end 898 | 899 | 900 | function lint.command(cmd) 901 | return function (filename) 902 | return map(cmd, function (k, v) 903 | if type(k) == "number" and v == lint.filename then 904 | return filename 905 | end 906 | return v 907 | end) 908 | end 909 | end 910 | 911 | 912 | function lint.args_command(cmd, config_option) 913 | return function (filename) 914 | local c = map(cmd, function (k, v) 915 | if type(k) == "number" and v == lint.args then 916 | local args = lint.config[config_option] or {} 917 | return args, "append" 918 | end 919 | return v 920 | end) 921 | return lint.command(c)(filename) 922 | end 923 | end 924 | 925 | 926 | function lint.interpreter(i) 927 | local patterns = { 928 | info = i.info, 929 | hint = i.hint, 930 | warning = i.warning, 931 | error = i.error, 932 | } 933 | local strip_pattern = i.strip 934 | 935 | return function (_, line) 936 | local line_processed = false 937 | return function () 938 | if line_processed then 939 | return nil 940 | end 941 | for kind, patt in pairs(patterns) do 942 | assert( 943 | type(patt) == "string", 944 | "lint+: interpreter pattern must be a string") 945 | local file, ln, column, message = line:match(patt) 946 | if file then 947 | if strip_pattern then 948 | message = message:gsub(strip_pattern, "") 949 | end 950 | line_processed = true 951 | return file, tonumber(ln), tonumber(column), kind, message 952 | end 953 | end 954 | end 955 | end 956 | end 957 | 958 | 959 | function lint.load(linter) 960 | if type(linter) == "table" then 961 | for _, v in ipairs(linter) do 962 | require("plugins.lintplus.linters." .. v) 963 | end 964 | elseif type(linter) == "string" then 965 | require("plugins.lintplus.linters." .. linter) 966 | end 967 | end 968 | 969 | 970 | if type(config.lint) ~= "table" then 971 | config.lint = {} 972 | end 973 | lint.config = config.lint 974 | 975 | 976 | --- END --- 977 | 978 | return lint 979 | -------------------------------------------------------------------------------- /json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | -------------------------------------------------------------------------------- /linter.lua: -------------------------------------------------------------------------------- 1 | -- lite-xl 1.16 2 | 3 | -- linter compatibility module for lint+ 4 | -- this module simply defines a linter.add_language function for compatibility 5 | -- with the existing linter module. 6 | -- note that linter modules are not capable of using all of lint+'s 7 | -- functionality: namely, they cannot use the four available levels of severity. 8 | -- all messages will have the warning severity level. 9 | 10 | local lintplus = require "plugins.lintplus" 11 | 12 | local linter = {} 13 | local name_counter = 0 14 | 15 | function linter.add_language(t) 16 | lintplus.add("compat.linter"..name_counter) { 17 | filename = t.file_patterns, 18 | procedure = { 19 | command = lintplus.command( 20 | t.command 21 | :gsub("%$FILENAME", "$filename") 22 | :gsub("%$ARGS", table.concat(t.args, ' ')) 23 | ), 24 | 25 | -- can't use the lintplus interpreter simply because it doesn't work 26 | -- exactly as linter does 27 | interpreter = function (filename, line) 28 | local yielded_message = false 29 | return function () 30 | if yielded_message then return nil end 31 | local ln, column, message = line:match(t.warning_pattern) 32 | if ln then 33 | -- we return the original filename to show all warnings 34 | -- because... say it with me... that's how linter works!! 35 | yielded_message = true 36 | return filename, tonumber(ln), tonumber(column), "warning", message 37 | end 38 | end 39 | end, 40 | }, 41 | } 42 | name_counter = name_counter + 1 43 | end 44 | 45 | 46 | return linter 47 | -------------------------------------------------------------------------------- /linters/luacheck.lua: -------------------------------------------------------------------------------- 1 | -- luacheck plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | -- config.lint.luacheck_args: table[string] 6 | -- passes the specified arguments to luacheck 7 | 8 | --- IMPLEMENTATION --- 9 | 10 | local common = require "core.common" 11 | local lintplus = require "plugins.lintplus" 12 | 13 | local config_options = { ",--config", ",--no-config," } --, ",--default-config" } 14 | local function contains_config_options(haystack) 15 | for _, needle in ipairs(config_options) do 16 | if nil ~= string.find(haystack, needle, 1, true) then 17 | return true 18 | end 19 | end 20 | 21 | return false 22 | end 23 | 24 | local function command(filename) 25 | local def = { 26 | "luacheck", 27 | lintplus.args, 28 | "--formatter", 29 | "visual_studio", 30 | lintplus.filename, 31 | } 32 | local luacheck_args = lintplus.config.luacheck_args or {} 33 | local args_string = "," .. table.concat(luacheck_args, ",") .. "," 34 | if contains_config_options(args_string) then 35 | -- User has configured luacheck arguments dealing with config. 36 | return lintplus.args_command(def, "luacheck_args")(filename) 37 | end 38 | 39 | -- We need to look for config file up the tree. 40 | local path = common.dirname(filename) 41 | local config_path 42 | while path do 43 | config_path = string.format("%s%s.luacheckrc", path, PATHSEP) 44 | if system.get_file_info(config_path) then 45 | table.insert(def, 2, string.format("--config %s", config_path)) 46 | break 47 | end 48 | path = common.dirname(path) 49 | end 50 | return lintplus.args_command(def, "luacheck_args")(filename) 51 | end 52 | 53 | lintplus.add("luacheck") { 54 | filename = "%.lua$", 55 | procedure = { 56 | command = command, 57 | interpreter = lintplus.interpreter { 58 | warning = "(.-)%((%d+),(%d+)%) : warning .-: (.+)", 59 | error = "(.-)%((%d+),(%d+)%) : error .-: (.+)", 60 | } 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /linters/nelua.lua: -------------------------------------------------------------------------------- 1 | -- Nelua plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | -- config.lint.nelua_mode: "analyze" | "lint" 6 | -- changes the linting mode, "analyze" (default) does a complete checking, 7 | -- while "lint" only checks for syntax errors. 8 | 9 | --- IMPLEMENTATION --- 10 | 11 | local core = require 'core' 12 | local lintplus = require 'plugins.lintplus' 13 | 14 | local mode = lintplus.config.nelua_mode or "analyze" 15 | 16 | if mode ~= "analyze" and mode ~= "lint" then 17 | core.error("lint+/nelua: invalid nelua_mode '%s'. Available modes: 'analyze', 'lint'", mode) 18 | mode = "lint" 19 | end 20 | 21 | local command = lintplus.command { 22 | 'nelua', 23 | '--no-color', 24 | '--'..mode, 25 | lintplus.filename 26 | } 27 | 28 | lintplus.add 'nelua' { 29 | filename = '%.nelua$', 30 | procedure = { 31 | command = command, 32 | interpreter = lintplus.interpreter { 33 | error = "(.-):(%d+):(%d+):.-error: (.+)" 34 | }, 35 | }, 36 | } 37 | 38 | -------------------------------------------------------------------------------- /linters/nim.lua: -------------------------------------------------------------------------------- 1 | -- Nim plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | -- config.lint.use_nimc: bool 6 | -- switches the linting backend from `nim check` to `nim c`. this can 7 | -- eliminate certain kinds of errors but is less safe due to `nim c` allowing 8 | -- staticExec 9 | -- config.lint.nim_args: string 10 | -- passes the specified arguments to the lint command. 11 | -- extra arguments may also be passed via a nim.cfg or config.nims. 12 | 13 | --- IMPLEMENTATION --- 14 | 15 | local lintplus = require "plugins.lintplus" 16 | 17 | local nullfile 18 | if PLATFORM == "Windows" then 19 | nullfile = "NUL" 20 | elseif PLATFORM == "Linux" then 21 | nullfile = "/dev/null" 22 | end 23 | 24 | local cmd = { 25 | "nim", 26 | "--listFullPaths", 27 | "--stdout", 28 | lintplus.args, 29 | } 30 | if nullfile == nil or not lintplus.config.use_nimc then 31 | table.insert(cmd, "check") 32 | else 33 | table.insert(cmd, "-o:" .. nullfile) 34 | table.insert(cmd, "c") 35 | end 36 | table.insert(cmd, lintplus.filename) 37 | 38 | lintplus.add("nim") { 39 | filename = "%.nim$", 40 | procedure = { 41 | command = lintplus.args_command(cmd, "nim_args"), 42 | interpreter = lintplus.interpreter { 43 | hint = "(.-)%((%d+), (%d+)%) Hint: (.+)", 44 | warning = "(.-)%((%d+), (%d+)%) Warning: (.+)", 45 | error = "(.-)%((%d+), (%d+)%) Error: (.+)", 46 | strip = "%s%[%w+%]$", 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /linters/php.lua: -------------------------------------------------------------------------------- 1 | -- PHP lint plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | -- config.lint.php_args: {string} 6 | -- passes the specified arguments to php 7 | 8 | --- IMPLEMENTATION --- 9 | 10 | local lintplus = require "plugins.lintplus" 11 | 12 | lintplus.add("php") { 13 | filename = "%.php$", 14 | procedure = { 15 | command = lintplus.args_command( 16 | { 17 | "php", 18 | "-l", 19 | lintplus.args, 20 | lintplus.filename 21 | }, 22 | "php_args" 23 | ), 24 | interpreter = function (filename, line, context) 25 | local line_processed = false 26 | return function () 27 | if line_processed then 28 | return nil 29 | end 30 | local message, file, line_num = line:match( 31 | "[%a ]+:%s*(.*)%s+in%s+(%g+)%s+on%sline%s+(%d+)" 32 | ) 33 | if line_num then 34 | line_processed = true 35 | return filename, tonumber(line_num), 1, "error", message 36 | end 37 | end 38 | end 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /linters/python.lua: -------------------------------------------------------------------------------- 1 | local lintplus = require "plugins.lintplus" 2 | 3 | lintplus.add("flake8") { 4 | filename = "%.py$", 5 | procedure = { 6 | command = lintplus.command( 7 | { "flake8", 8 | lintplus.filename }, 9 | "flake8_args" 10 | ), 11 | interpreter = lintplus.interpreter { 12 | warning = "(.-):(%d+):(%d+): [FCW]%d+ (.+)", 13 | error = "(.-):(%d+):(%d+): E%d+ (.+)", 14 | } 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /linters/rust.lua: -------------------------------------------------------------------------------- 1 | -- Rust plugin for lint+ 2 | 3 | 4 | --- IMPLEMENTATION --- 5 | 6 | 7 | local common = require "core.common" 8 | local core = require "core" 9 | 10 | local lintplus = require "plugins.lintplus" 11 | local json = require "plugins.lintplus.json" 12 | 13 | 14 | -- common functions 15 | 16 | 17 | local function no_op() end 18 | 19 | 20 | local function parent_directories(filename) 21 | 22 | return function () 23 | filename = lintplus.fs.parent_directory(filename) 24 | return filename 25 | end 26 | 27 | end 28 | 29 | 30 | -- message processing 31 | 32 | local function message_spans_multiple_lines(message, line) 33 | if #message.spans == 0 then return false end 34 | for _, span in ipairs(message.spans) do 35 | if span.line_start ~= line then 36 | return true 37 | end 38 | end 39 | for _, child in ipairs(message.children) do 40 | local child_spans_multiple_lines = message_spans_multiple_lines(child, line) 41 | if child_spans_multiple_lines then 42 | return true 43 | end 44 | end 45 | return false 46 | end 47 | 48 | local function process_message( 49 | context, 50 | message, 51 | out_messages, 52 | rail 53 | ) 54 | local msg = message.message 55 | local span = message.spans[1] 56 | 57 | local kind do 58 | local l = message.level 59 | if l == "error" or l == "warning" then 60 | kind = l 61 | elseif l == "error: internal compiler error" then 62 | kind = "error" 63 | else 64 | kind = "info" 65 | end 66 | end 67 | 68 | local nonprimary_spans = 0 69 | for _, sp in ipairs(message.spans) do 70 | if not sp.is_primary then 71 | nonprimary_spans = nonprimary_spans + 1 72 | end 73 | end 74 | 75 | -- only assign a rail if there are children or multiple non-primary spans 76 | if span ~= nil then 77 | local filename = context.workspace_root .. '/' .. span.file_name 78 | local line, column = span.line_start, span.column_start 79 | 80 | if rail == nil then 81 | if message_spans_multiple_lines(message, line) then 82 | rail = context:create_gutter_rail() 83 | end 84 | end 85 | 86 | for _, sp in ipairs(message.spans) do 87 | if sp.label ~= nil and not sp.is_primary then 88 | local s_filename = context.workspace_root .. '/' .. span.file_name 89 | local s_line, s_column = sp.line_start, sp.column_start 90 | table.insert(out_messages, 91 | { s_filename, s_line, s_column, "info", sp.label, rail }) 92 | end 93 | end 94 | 95 | if span.suggested_replacement ~= nil then 96 | local suggestion = span.suggested_replacement:match("(.-)\r?\n") 97 | if suggestion ~= nil then 98 | msg = msg .. " `" .. suggestion .. '`' 99 | end 100 | end 101 | table.insert(out_messages, { filename, line, column, kind, msg, rail }) 102 | end 103 | 104 | for _, child in ipairs(message.children) do 105 | process_message(context, child, out_messages, rail) 106 | end 107 | end 108 | 109 | 110 | local function get_messages(context, event) 111 | -- filename, line, column, kind, message 112 | local messages = {} 113 | process_message(context, event.message, messages) 114 | return messages 115 | end 116 | 117 | 118 | -- linter 119 | 120 | lintplus.add("rust") { 121 | filename = "%.rs$", 122 | procedure = { 123 | 124 | init = function (filename, context) 125 | local process = lintplus.ipc.start_process({ 126 | "cargo", "locate-project", "--workspace" 127 | }, lintplus.fs.parent_directory(filename)) 128 | while true do 129 | local exit, _ = process:poll(function (line) 130 | local ok, process_result = pcall(json.decode, line) 131 | if not ok then return end 132 | context.workspace_root = 133 | lintplus.fs.parent_directory(process_result.root) 134 | end) 135 | if exit ~= nil then break end 136 | end 137 | end, 138 | 139 | command = lintplus.command { 140 | set_cwd = true, 141 | "cargo", "clippy", 142 | "--message-format", "json", 143 | "--color", "never", 144 | -- "--tests", 145 | }, 146 | 147 | interpreter = function (filename, line, context) 148 | -- initial checks 149 | if context.workspace_root == nil then 150 | core.error( 151 | "lint+/rust: "..filename.." is not situated in a cargo crate" 152 | ) 153 | return no_op 154 | end 155 | if line:match("^ *Blocking") then 156 | return "bail" 157 | end 158 | 159 | local ok, event = pcall(json.decode, line) 160 | if not ok then return no_op end 161 | 162 | if event.reason == "compiler-message" then 163 | local messages = get_messages(context, event) 164 | local i = 1 165 | 166 | return function () 167 | local msg = messages[i] 168 | if msg ~= nil then 169 | i = i + 1 170 | return table.unpack(msg) 171 | else 172 | return nil 173 | end 174 | end 175 | else 176 | return no_op 177 | end 178 | end, 179 | 180 | }, 181 | } 182 | -------------------------------------------------------------------------------- /linters/shellcheck.lua: -------------------------------------------------------------------------------- 1 | -- shellcheck plugin for lint+ 2 | 3 | --- INSTALLATION --- 4 | -- In order to use this linter, please ensure you have the shellcheck binary 5 | -- in your path. For installation notes please see 6 | -- https://github.com/koalaman/shellcheck#user-content-installing 7 | 8 | --- CONFIG --- 9 | 10 | -- config.lint.shellcheck_args: table[string] 11 | -- passes the given arguments to shellcheck. 12 | 13 | --- IMPLEMENTATION --- 14 | 15 | local lintplus = require "plugins.lintplus" 16 | 17 | lintplus.add("shellcheck") { 18 | filename = "%.sh$", 19 | syntax = { 20 | "Shell script", 21 | "shellscript", 22 | "bashscript", 23 | "Bash script", 24 | "Bash", 25 | "bash", 26 | }, 27 | procedure = { 28 | command = lintplus.args_command( 29 | { "shellcheck", 30 | "--format=gcc", 31 | lintplus.args, 32 | lintplus.filename 33 | }, 34 | "shellcheck_args" 35 | ), 36 | interpreter = lintplus.interpreter { 37 | info = "(.*):(%d+):(%d+): note: (.+)", 38 | error = "(.*):(%d+):(%d+): error: (.+)", 39 | warning = "(.*):(%d+):(%d+): warning: (.+)", 40 | } 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /linters/teal.lua: -------------------------------------------------------------------------------- 1 | -- teal plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | -- config.lint.teal_args: table[string] 6 | -- passes the specified arguments to teal 7 | 8 | --- IMPLEMENTATION --- 9 | 10 | local core = require "core" 11 | local lintplus = require "plugins.lintplus" 12 | 13 | local line_processed = false 14 | local severity = "error" 15 | 16 | local function interpreter(filename, line, context) 17 | return function () 18 | if line_processed then 19 | line_processed = false 20 | return nil 21 | end 22 | local num, type = line:match("^(%d+)%s+([a-rt-z]+)s?:$") -- treat `s` differently 23 | if num then 24 | severity = type 25 | return nil 26 | end 27 | local line_num, column, message = line:match("^/[^:]+:(%d+):(%d+):%s*(.+)$") 28 | if line_num then 29 | line_processed = true 30 | return filename, tonumber(line_num), tonumber(column), severity, message 31 | end 32 | return nil 33 | end 34 | end 35 | 36 | lintplus.add("teal") { 37 | filename = "%.tl$", 38 | procedure = { 39 | command = lintplus.args_command( 40 | { "tl", 41 | lintplus.args, 42 | "check", 43 | lintplus.filename }, 44 | "teal_args" 45 | ), 46 | interpreter = interpreter 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /linters/typescript.lua: -------------------------------------------------------------------------------- 1 | -- Typescript lint plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | --- __Site__: https://github.com/eslint/eslint 6 | --- __Installation__: `npm install -g eslint typescript --save-dev` 7 | --- __Local Config__: [optional] npx eslint --init 8 | 9 | --- IMPLEMENTATION --- 10 | local lintplus = require "plugins.lintplus" 11 | 12 | lintplus.add("typescript") { 13 | filename = "%.ts$", 14 | procedure = { 15 | 16 | command = lintplus.command( 17 | { 18 | "eslint", 19 | "--rule", "{}", 20 | "--format", "visualstudio", 21 | lintplus.filename 22 | } 23 | ), 24 | 25 | interpreter = lintplus.interpreter ({ 26 | warning = "^(.+)%((%d+),(%d+)%)%: warning%s?[^:]*: (.+)$", 27 | error = "^(.+)%((%d+),(%d+)%)%: error%s?[^:]*: (.+)$" 28 | }), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /linters/v.lua: -------------------------------------------------------------------------------- 1 | -- v plugin for lint+ 2 | 3 | --- INSTALLATION --- 4 | -- In order to use this linter, please ensure you have the v binary 5 | -- in your $PATH. For installation notes please see 6 | -- https://github.com/vlang/v/blob/master/doc/docs.md#installing-v-from-source 7 | 8 | --- CONFIG --- 9 | 10 | -- config.lint.v_mode: "check" | "check-syntax" 11 | -- changes the linting mode. check scans, parses, and checks the files 12 | -- without compiling the program (default), 13 | -- check-syntax only scan and parse the files, but then stops. 14 | -- Useful for very quick syntax checks. 15 | -- config.lint.v_args: table[string] 16 | -- passes the given arguments to v. 17 | 18 | --- IMPLEMENTATION --- 19 | 20 | local core = require "core" 21 | local lintplus = require "plugins.lintplus" 22 | 23 | local mode = lintplus.config.v_mode or "check" 24 | if mode ~= "check" and mode ~= "check-syntax" then 25 | core.error("lint+/v: invalid v_mode '%s'. ".. 26 | "available modes: 'check', 'check-syntax'") 27 | return 28 | end 29 | 30 | local command 31 | if mode == "check" then 32 | command = lintplus.command { 33 | "v", 34 | "-check", 35 | "-nocolor", 36 | "-shared", 37 | "-message-limit", "-1", 38 | lintplus.args, 39 | lintplus.filename 40 | } 41 | elseif mode == "check-syntax" then 42 | command = lintplus.args_command({ 43 | "v", 44 | "-check-syntax", 45 | "-nocolor", 46 | "-shared", 47 | "-message-limit", "-1", 48 | lintplus.args, 49 | lintplus.filename 50 | }, "v_args") 51 | end 52 | 53 | lintplus.add("v") { 54 | filename = "%.v$", 55 | syntax = { 56 | "V", 57 | "v", 58 | "Vlang", 59 | "vlang", 60 | }, 61 | procedure = { 62 | command = command, 63 | interpreter = lintplus.interpreter { 64 | error = "(.*):(%d+):(%d+): error: (.+)", 65 | warning = "(.*):(%d+):(%d+): warning: (.+)", 66 | }, 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /linters/zig.lua: -------------------------------------------------------------------------------- 1 | -- Zig plugin for lint+ 2 | 3 | --- CONFIG --- 4 | 5 | -- config.lint.zig_mode: "ast-check" | "build" 6 | -- changes the linting mode. ast-check is a quick'n'dirty check (default), 7 | -- build compiles the tests in a file (but does not run them). 8 | -- config.lint.zig_args: table[string] 9 | -- passes the given table of arguments to zig test. this does not have any 10 | -- effect in "ast-check" mode. 11 | 12 | --- IMPLEMENTATION --- 13 | 14 | local core = require "core" 15 | local lintplus = require "plugins.lintplus" 16 | 17 | local mode = lintplus.config.zig_mode or "ast-check" 18 | if mode ~= "ast-check" and mode ~= "build" then 19 | core.error("lint+/zig: invalid zig_mode '%s'. ".. 20 | "available modes: 'ast-check', 'build'") 21 | return 22 | end 23 | 24 | local command 25 | if mode == "ast-check" then 26 | command = lintplus.command { 27 | "zig", 28 | "ast-check", 29 | "--color", "off", 30 | lintplus.filename 31 | } 32 | elseif mode == "build" then 33 | command = lintplus.args_command({ 34 | "zig", 35 | "test", 36 | "--color", "off", 37 | "-fno-emit-bin", 38 | lintplus.args, 39 | lintplus.filename 40 | }, "zig_args") 41 | end 42 | 43 | 44 | lintplus.add("zig") { 45 | filename = "%.zig$", 46 | procedure = { 47 | command = command, 48 | interpreter = lintplus.interpreter { 49 | hint = "(.-):(%d+):(%d+): note: (.+)", 50 | error = "(.-):(%d+):(%d+): error: (.+)", 51 | warning = "(.-):(%d+):(%d+): warning: (.+)", 52 | } 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /liteipc.lua: -------------------------------------------------------------------------------- 1 | -- liteipc - async IPC for lite 2 | 3 | local liteipc = {} 4 | 5 | local Process = {} 6 | Process.__index = Process 7 | 8 | function liteipc.start_process(args, cwd) 9 | local proc = setmetatable({ 10 | popen = process.start(args, {cwd = cwd}), 11 | read_from = "" 12 | }, Process) 13 | return proc 14 | end 15 | 16 | function Process.poll(self, callback) 17 | local line = "" 18 | local read = nil 19 | 20 | while self.read_from == "" and self.popen:returncode() == nil do 21 | local stderr = self.popen:read_stderr(1) 22 | local stdout = self.popen:read_stdout(1) 23 | local out = nil 24 | if stderr ~= nil and stderr ~= "" then 25 | out = stderr 26 | self.read_from = "stderr" 27 | elseif stdout ~= nil and stdout ~= "" then 28 | out = stdout 29 | self.read_from = "stdout" 30 | end 31 | if out ~= nil then 32 | if out ~= "\n" then 33 | line = line .. out 34 | end 35 | break 36 | end 37 | end 38 | 39 | while true do 40 | if self.read_from == "stderr" then 41 | read = self.popen:read_stderr(1) 42 | else 43 | read = self.popen:read_stdout(1) 44 | end 45 | if read == nil or read == "\n" then 46 | if line ~= "" then callback(line) end 47 | break 48 | else 49 | line = line .. read 50 | end 51 | end 52 | 53 | if not self.popen:running() and read == nil then 54 | local exit = "exit" 55 | local retcode = self.popen:returncode() 56 | if retcode ~= 1 and retcode ~= 0 then 57 | exit = "signal" 58 | end 59 | local errmsg = process.strerror(retcode) 60 | return exit, retcode, errmsg 61 | end 62 | 63 | return nil, nil, nil 64 | end 65 | 66 | return liteipc 67 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": [ 3 | { 4 | "id": "lintplus", 5 | "name": "Lint+", 6 | "description": "An improved linting plugin.", 7 | "version": "0.4", 8 | "mod_version": "3", 9 | "tags": [ 10 | "linter" 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /renderutil.lua: -------------------------------------------------------------------------------- 1 | -- rendering utilities 2 | 3 | local common = require "core.common" 4 | 5 | local renderutil = {} 6 | 7 | function renderutil.draw_dotted_line(x, y, length, axis, color) 8 | if axis == 'x' then 9 | for xx = x, x + length, 2 do 10 | renderer.draw_rect(xx, y, 1, 1, color) 11 | end 12 | elseif axis == 'y' then 13 | for yy = y, y + length, 2 do 14 | renderer.draw_rect(x, yy, 1, 1, color) 15 | end 16 | end 17 | end 18 | 19 | local function plot(x, y, color) 20 | renderer.draw_rect(x, y, 1, 1, color) 21 | end 22 | 23 | function renderutil.draw_quarter_circle(x, y, r, color, flipy) 24 | -- inefficient for large circles, but it works. 25 | color = { table.unpack(color) } 26 | local a = color[4] 27 | for dx = 0, r - 1 do 28 | for dy = 0, r - 1 do 29 | local xx = r - 1 - dx 30 | local yy = dy 31 | if not flipy then 32 | yy = r - 1 - dy 33 | end 34 | local t = math.abs(math.sqrt(xx*xx + yy*yy) - r + 1) 35 | t = common.clamp(1 - t, 0, 1) 36 | if t > 0 then 37 | color[4] = a * t 38 | plot(x + dx, y + dy, color) 39 | end 40 | end 41 | end 42 | end 43 | 44 | return renderutil 45 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liquidev/lintplus/eaff3321f569e89aca57e76dc1f684a37aecd254/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liquidev/lintplus/eaff3321f569e89aca57e76dc1f684a37aecd254/screenshots/2.png --------------------------------------------------------------------------------