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,21 @@
MIT License
Copyright (c) 2023 Francisco Barreiras
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.

View file

@ -0,0 +1,41 @@
# Code+
> A quality of life plugin for the Lite XL text editor. Offering improvements such as highlighted comments and autocomplete for brackets, quotes and more.
## Basic usage
Highlight comments with special properties using the **@Todo(...)** and **@Fixme(...)** keywords for an enhanced coding experience.
Streamline coding by auto-completing brackets, parentheses, quotation marks reducing manual effort and improving the writing code experience.
## Demonstration
![Code+ demo](https://s12.gifyu.com/images/Sckre.gif)
## Instalation
Navigate to the `data/plugins` folder and run the following command:
```bash
git clone https://github.com/chqs-git/code-plus.git
```
Alternatively you can download and rename the `init.lua ` file to `code+.lua` and drop it into the `data/plugins` folder.
## Configuration
Using the settings plugin for Lite Xl you can easily configure your experience by changing the highlight colors for the *@todo* and *@fixme* operators.
If you wish to add more highlights you can simply update the following code:
```lua
function DocView:draw_line_text(line, x, y)
local lh = draw_line_text(self, line, x, y)
if config.plugins.code_plus.enabled then
highlight_comment(self, line, x, y, "@todo", config.plugins.code_plus.todo)
highlight_comment(self, line, x, y, "@fixme", config.plugins.code_plus.fixme)
-- add a new highlight! the color is just an example
highlight_comment(self, line, x, y, "@new_tag", {common.color "#ffffff"})
end
return lh
end
```
To extend the already auto-completing utilities to other keywords, you can simply use the `complete` function. Create a new command for the new auto-complete utility (required) and map a *key* to the command.

View file

@ -0,0 +1,110 @@
-- mod-version:3
local config = require "core.config"
local command = require "core.command"
local keymap = require "core.keymap"
local common = require "core.common"
local DocView = require "core.docview"
config.plugins.code_plus = common.merge({
enabled = true, --- enabled by default
config_spec = { --- config specification used by the settings gui
name = "Code+",
{
label = "Enable",
description = "Toggle to enable this plugin.",
path = "enabled",
type = "toggle",
default = true
},
{
label = "Todo Color",
description = "Define the color that highlights the todo comments.",
path = "todo",
type = "color",
default = "#5592CF"
},
{
label = "Fixme Color",
description = "Defines the color that highlights the fixme comments.",
path = "fixme",
type = "color",
default = "#EF6385"
},
}
}, config.plugins.code_plus)
--- draw comments highlights
local white = { common.color "#ffffff" }
local function draw_highlight(self, str, line, x, y, s, e, color)
local x1 = x + self:get_col_x_offset(line, s)
local x2 = x + self:get_col_x_offset(line, e + 1)
local oy = self:get_line_text_y_offset()
renderer.draw_rect(x1, y, x2 - x1, self:get_line_height(), color)
renderer.draw_text(self:get_font(), str, x1, y + oy, white)
end
local function highlight_comment(self, line, x, y, comment, color)
local text = self.doc.lines[line]
local s, e = 0, 0
while true do
s, e = text:lower():find(comment .. "%((.-)%)", e + 1)
if s then
local str = text:sub(s, e)
draw_highlight(self, str, line, x, y, s, e, color)
end
if not s then
break
end
end
end
local draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(line, x, y)
local lh = draw_line_text(self, line, x, y)
if config.plugins.code_plus.enabled then
highlight_comment(self, line, x, y, "@todo", config.plugins.code_plus.todo)
highlight_comment(self, line, x, y, "@fixme", config.plugins.code_plus.fixme)
end
return lh
end
--- auto complete brackets, parantheses, etc...
local function complete(dv, characters)
local doc = dv.doc
local idx = dv.doc.last_selection
local line1, col1 = doc:get_selection_idx(idx)
doc:insert(line1, col1, characters)
doc:move_to_cursor(idx, idx)
end
command.add("core.docview!", {
["code_plus:complete_brackets"] = function(dv)
complete(dv, "[]")
end,
["code_plus:complete_curly_brackets"] = function(dv)
complete(dv, "{}")
end,
["code_plus:complete_parantheses"] = function(dv)
complete(dv, "()")
end,
["code_plus:complete_quotation_marks"] = function(dv)
complete(dv, '""')
end,
})
keymap.add {
["altgr+8"] = "code_plus:complete_brackets",
["ctrl+alt+8"] = "code_plus:complete_brackets",
["altgr+7"] = "code_plus:complete_curly_brackets",
["ctrl+alt+7"] = "code_plus:complete_curly_brackets",
["shift+8"] = "code_plus:complete_parantheses",
["shift+2"] = "code_plus:complete_quotation_marks"
}

View file

@ -0,0 +1,12 @@
{
"addons": [
{
"id": "codeplus",
"mod_version": "3",
"name": "codeplus",
"path": ".",
"type": "plugin",
"version": "0.1"
}
]
}

View file

@ -0,0 +1,61 @@
# EditorConfig
This plugin implements the [EditorConfig](https://editorconfig.org/) spec
purely on lua by leveraging lua patterns and the regex engine on lite-xl.
Installing additional dependencies is not required.
The EditorConfig spec was implemented as best understood,
if you find any bugs please report them on this repository
[issue tracker](https://github.com/lite-xl/lite-xl-plugins/issues).
## Implemented Features
Global options:
* root - prevents upward searching of .editorconfig files
Applied to documents indent info:
* indent_style
* indent_size
* tab_width
Applied on document save:
* end_of_line - if set to `cr` it is ignored
* trim_trailing_whitespace
* insert_final_newline boolean
## Not implemented
* charset - this feature would need the encoding
[PR](https://github.com/lite-xl/lite-xl/pull/1161) or
[plugin](https://github.com/jgmdev/lite-xl-encoding)
## Extras
* Supports multiple project directories
* Implements hot reloading, so modifying an .editorconfig file from within
the editor will re-apply all rules to currently opened files.
## Testing
This plugin includes a test suite to check how well the .editorconfig parser
is working.
The [editorconfig-core-test](https://github.com/editorconfig/editorconfig-core-test)
glob, parser and properties cmake tests where ported and we are getting a 100%
pass rate.
If you are interested in running the test suite, from the terminal execute
the following:
```sh
lite-xl test editorconfig
```
To inspect the generated sections and regex rules:
```sh
lite-xl test editorconfig --parsers
```

View file

@ -0,0 +1,441 @@
-- mod-version:3
--
-- EditorConfig plugin for Lite XL
-- @copyright Jefferson Gonzalez <jgmdev@gmail.com>
-- @license MIT
--
-- Note: this plugin needs to be loaded after detectindent plugin,
-- since the name editorconfig.lua is ordered after detectindent.lua
-- there shouldn't be any issues. Just a reminder for the future in
-- case of a plugin that could also handle document identation type
-- and size, and has a name with more weight than this plugin.
--
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local trimwhitespace = require "plugins.trimwhitespace"
local Doc = require "core.doc"
local Parser = require "plugins.editorconfig.parser"
---@class config.plugins.editorconfig
---@field debug boolean
config.plugins.editorconfig = common.merge({
debug = false,
-- The config specification used by the settings gui
config_spec = {
name = "EditorConfig",
{
label = "Debug",
description = "Display debugging messages on the log.",
path = "debug",
type = "toggle",
default = false
}
}
}, config.plugins.editorconfig)
---Cache of .editorconfig options to reduce parsing for every opened file.
---@type table<string, plugins.editorconfig.parser>
local project_configs = {}
---Keep track of main project directory so when changed we can assign a new
---.editorconfig object if neccesary.
---@type string
local main_project = core.project_dir
---Functionality that will be exposed by the plugin.
---@class plugins.editorconfig
local editorconfig = {}
---Load global .editorconfig options for a project.
---@param project_dir string
---@return boolean loaded
function editorconfig.load(project_dir)
local editor_config = project_dir .. "/" .. ".editorconfig"
local file = io.open(editor_config)
if file then
file:close()
project_configs[project_dir] = Parser.new(editor_config)
return true
end
return false
end
---Helper to add or substract final new line, it also makes final new line
---visble which lite-xl does not.
---@param doc core.doc
---@param raw? boolean If true does not register change on undo stack
---@return boolean handled_new_line
local function handle_final_new_line(doc, raw)
local handled = false
---@diagnostic disable-next-line
if doc.insert_final_newline then
handled = true
if doc.lines[#doc.lines] ~= "\n" then
if not raw then
doc:insert(#doc.lines, math.huge, "\n")
else
table.insert(doc.lines, "\n")
end
end
---@diagnostic disable-next-line
elseif type(doc.insert_final_newline) == "boolean" then
handled = true
if trimwhitespace.trim_empty_end_lines then
trimwhitespace.trim_empty_end_lines(doc, raw)
-- TODO: remove this once 2.1.1 is released
else
for _=#doc.lines, 1, -1 do
local l = #doc.lines
if l > 1 and doc.lines[l] == "\n" then
local current_line = doc:get_selection()
if current_line == l then
doc:set_selection(l-1, math.huge, l-1, math.huge)
end
if not raw then
doc:remove(l-1, math.huge, l, math.huge)
else
table.remove(doc.lines, l)
end
end
end
end
end
return handled
end
---Split the given relative path by / or \ separators.
---@param path string The path to split
---@return table
local function split_path(path)
local result = {};
for match in (path.."/"):gmatch("(.-)".."[\\/]") do
table.insert(result, match);
end
return result;
end
---Check if the given file path exists.
---@param file_path string
local function file_exists(file_path)
local file = io.open(file_path, "r")
if not file then return false end
file:close()
return true
end
---Merge a config options to target if they don't already exists on target.
---@param config_target? plugins.editorconfig.parser.section
---@param config_from? plugins.editorconfig.parser.section
local function merge_config(config_target, config_from)
if config_target and config_from then
for name, value in pairs(config_from) do
if type(config_target[name]) == "nil" then
config_target[name] = value
end
end
end
end
---Scan for .editorconfig files from current file path to upper project path
---if root attribute is not found first and returns matching config.
---@param file_path string
---@return plugins.editorconfig.parser.section?
local function recursive_get_config(file_path)
local project_dir = ""
local root_config
for path, editor_config in pairs(project_configs) do
if common.path_belongs_to(file_path, path) then
project_dir = path
root_config = editor_config:getConfig(
common.relative_path(path, file_path)
)
break
end
end
if project_dir == "" then
for _, project in ipairs(core.project_directories) do
if common.path_belongs_to(file_path, project.name) then
project_dir = project.name
break
end
end
end
local relative_file_path = common.relative_path(project_dir, file_path)
local dir = common.dirname(relative_file_path)
local editor_config = {}
local config_found = false
if not dir and root_config then
editor_config = root_config
config_found = true
elseif dir then
local path_list = split_path(dir)
local root_found = false
for p=#path_list, 1, -1 do
local path = project_dir .. "/" .. table.concat(path_list, "/", 1, p)
if file_exists(path .. "/" .. ".editorconfig") then
---@type plugins.editorconfig.parser
local parser = Parser.new(path .. "/" .. ".editorconfig")
local pconfig = parser:getConfig(common.relative_path(path, file_path))
if pconfig then
merge_config(editor_config, pconfig)
config_found = true
end
if parser.root then
root_found = true
break
end
end
end
if not root_found and root_config then
merge_config(editor_config, root_config)
config_found = true
end
end
-- clean unset options
if config_found then
local all_unset = true
for name, value in pairs(editor_config) do
if value == "unset" then
editor_config[name] = nil
else
all_unset = false
end
end
if all_unset then config_found = false end
end
return config_found and editor_config or nil
end
---Apply editorconfig rules to given doc if possible.
---@param doc core.doc
function editorconfig.apply(doc)
if not doc.abs_filename and not doc.filename then return end
local file_path = doc.abs_filename or (main_project .. "/" .. doc.filename)
local options = recursive_get_config(file_path)
if options then
if config.plugins.editorconfig.debug then
core.log_quiet(
"[EditorConfig]: %s applied %s",
file_path, common.serialize(options, {pretty = true})
)
end
local indent_type, indent_size = doc:get_indent_info()
if options.indent_style then
if options.indent_style == "tab" then
indent_type = "hard"
else
indent_type = "soft"
end
end
if options.indent_size and options.indent_size == "tab" then
if options.tab_width then
options.indent_size = options.tab_width
else
options.indent_size = config.indent_size or 2
end
end
if options.indent_size then
indent_size = options.indent_size
end
if doc.indent_info then
doc.indent_info.type = indent_type
doc.indent_info.size = indent_size
doc.indent_info.confirmed = true
else
doc.indent_info = {
type = indent_type,
size = indent_size,
confirmed = true
}
end
if options.end_of_line then
if options.end_of_line == "crlf" then
doc.crlf = true
elseif options.end_of_line == "lf" then
doc.crlf = false
end
end
if options.trim_trailing_whitespace then
doc.trim_trailing_whitespace = true
elseif options.trim_trailing_whitespace == false then
doc.trim_trailing_whitespace = false
else
doc.trim_trailing_whitespace = nil
end
if options.insert_final_newline then
doc.insert_final_newline = true
elseif options.insert_final_newline == false then
doc.insert_final_newline = false
else
doc.insert_final_newline = nil
end
if
(
type(doc.trim_trailing_whitespace) == "boolean"
or
type(doc.insert_final_newline) == "boolean"
)
-- TODO: remove this once 2.1.1 is released
and
trimwhitespace.disable
then
trimwhitespace.disable(doc)
end
handle_final_new_line(doc, true)
end
end
---Applies .editorconfig options to all open documents if possible.
function editorconfig.apply_all()
for _, doc in ipairs(core.docs) do
editorconfig.apply(doc)
end
end
--------------------------------------------------------------------------------
-- Load .editorconfig on all projects loaded at startup and apply it
--------------------------------------------------------------------------------
core.add_thread(function()
local loaded = false
-- scan all opened project directories
if core.project_directories then
for i=1, #core.project_directories do
local found = editorconfig.load(core.project_directories[i].name)
if found then loaded = true end
end
end
-- if an editorconfig was found then try to apply it to opened docs
if loaded then
editorconfig.apply_all()
end
end)
--------------------------------------------------------------------------------
-- Override various core project loading functions for .editorconfig scanning
--------------------------------------------------------------------------------
local core_open_folder_project = core.open_folder_project
function core.open_folder_project(directory)
core_open_folder_project(directory)
if project_configs[main_project] then project_configs[main_project] = nil end
main_project = core.project_dir
editorconfig.load(main_project)
end
local core_remove_project_directory = core.remove_project_directory
function core.remove_project_directory(path)
local out = core_remove_project_directory(path)
if project_configs[path] then project_configs[path] = nil end
return out
end
local core_add_project_directory = core.add_project_directory
function core.add_project_directory(directory)
local out = core_add_project_directory(directory)
editorconfig.load(directory)
return out
end
--------------------------------------------------------------------------------
-- Hook into the core.doc to apply editor config options
--------------------------------------------------------------------------------
local doc_new = Doc.new
function Doc:new(...)
doc_new(self, ...)
editorconfig.apply(self)
end
---Cloned trimwitespace plugin until it is exposed for other plugins.
---@param doc core.doc
local function trim_trailing_whitespace(doc)
if trimwhitespace.trim then
trimwhitespace.trim(doc)
return
end
-- TODO: remove this once 2.1.1 is released
local cline, ccol = doc:get_selection()
for i = 1, #doc.lines do
local old_text = doc:get_text(i, 1, i, math.huge)
local new_text = old_text:gsub("%s*$", "")
-- don't remove whitespace which would cause the caret to reposition
if cline == i and ccol > #new_text then
new_text = old_text:sub(1, ccol - 1)
end
if old_text ~= new_text then
doc:insert(i, 1, new_text)
doc:remove(i, #new_text + 1, i, math.huge)
end
end
end
local doc_save = Doc.save
function Doc:save(...)
local new_file = self.new_file
---@diagnostic disable-next-line
if self.trim_trailing_whitespace then
trim_trailing_whitespace(self)
end
local lc = #self.lines
local handle_new_line = handle_final_new_line(self)
-- remove the unnecesary visible \n\n or the disabled \n
if handle_new_line then
self.lines[lc] = self.lines[lc]:gsub("\n$", "")
end
doc_save(self, ...)
-- restore the visible \n\n or disabled \n
if handle_new_line then
self.lines[lc] = self.lines[lc] .. "\n"
end
if common.basename(self.abs_filename) == ".editorconfig" then
-- blindlessly reload related project .editorconfig options
for _, project in ipairs(core.project_directories) do
if common.path_belongs_to(self.abs_filename, project.name) then
editorconfig.load(project.name)
break
end
end
-- re-apply editorconfig options to all open files
editorconfig.apply_all()
elseif new_file then
-- apply editorconfig options for file that was previously unsaved
editorconfig.apply(self)
end
end
--------------------------------------------------------------------------------
-- Run the test suite if requested on CLI with: lite-xl test editorconfig
--------------------------------------------------------------------------------
for i, argument in ipairs(ARGS) do
if argument == "test" and ARGS[i+1] == "editorconfig" then
require "plugins.editorconfig.runtest"
os.exit()
end
end
return editorconfig

View file

@ -0,0 +1,553 @@
-- Lua parser implementation of the .editorconfig spec as best understood.
-- @copyright Jefferson Gonzalez <jgmdev@gmail.com>
-- @license MIT
local core = require "core"
local config = require "core.config"
local STANDALONE = false
for i, argument in ipairs(ARGS) do
if argument == "test" and ARGS[i+1] == "editorconfig" then
STANDALONE = true
end
end
---Logger that will output using lite-xl logging functions or print to
---terminal if the parser is running in standalone mode.
---@param type "log" | "error"
---@param format string
---@param ... any
local function log(type, format, ...)
if not STANDALONE then
core[type]("[EditorConfig]: " .. format, ...)
else
print("[" .. type:upper() .. "]: " .. string.format(format, ...))
end
end
---Represents an .editorconfig path rule/expression.
---@class plugins.editorconfig.parser.rule
---Path expression as found between square brackets.
---@field expression string | table<integer,string>
---The expression converted to a regex.
---@field regex string | table<integer,string>
---@field regex_compiled any? | table<integer,string>
---@field negation boolean Indicates that the expression is a negation.
---@field ranges table<integer,number> List of ranges found on the expression.
---Represents a section of the .editorconfig with all its config options.
---@class plugins.editorconfig.parser.section
---@field rule plugins.editorconfig.parser.rule
---@field equivalent_rules plugins.editorconfig.parser.rule[]
---@field indent_style "tab" | "space"
---@field indent_size integer
---@field tab_width integer
---@field end_of_line "lf" | "cr" | "crlf"
---@field charset "latin1" | "utf-8" | "utf-8-bom" | "utf-16be" | "utf-16le"
---@field trim_trailing_whitespace boolean
---@field insert_final_newline boolean
---EditorConfig parser class and filename config matching.
---@class plugins.editorconfig.parser
---@field config_path string
---@field sections plugins.editorconfig.parser.section[]
---@field root boolean
local Parser = {}
Parser.__index = Parser
---Constructor
---@param config_path string
---@return plugins.editorconfig.parser
function Parser.new(config_path)
local self = {}
setmetatable(self, Parser)
self.config_path = config_path
self.sections = {}
self.root = false
self:read()
return self
end
--- char to hex cache and automatic converter
---@type table<string,string>
local hex_value = {}
setmetatable(hex_value, {
__index = function(t, k)
local v = rawget(t, k)
if v == nil then
v = string.format("%x", string.byte(k))
rawset(t, k, v)
end
return v
end
})
---Simplifies managing rules with other inner rules like {...} which can
---contain escaped \\{ \\} and expressions that are easier handled after
---converting the escaped special characters to \xXX counterparts.
---@param value string
---@return string escaped_values
local function escapes_to_regex_hex(value)
local escaped_chars = {}
for char in value:ugmatch("\\(.)") do
table.insert(escaped_chars, char)
end
for _, char in ipairs(escaped_chars) do
value = value:ugsub("\\" .. char, "\\x" .. hex_value[char])
end
return value
end
---An .editorconfig path expression to regex conversion rule.
---@class rule
---@field rule string Lua pattern.
---Callback conversion function.
---@field conversion fun(match:string, section:plugins.editorconfig.parser.section):string
---List of conversion rules applied to brace expressions.
---@type rule[]
local RULES_BRACES = {
{ rule = "^%(", conversion = function() return "\\(" end },
{ rule = "^%)", conversion = function() return "\\)" end },
{ rule = "^%.", conversion = function() return "\\." end },
{ rule = "^\\%[", conversion = function() return "\\[" end },
{ rule = "^\\%]", conversion = function() return "\\]" end },
{ rule = "^\\!", conversion = function() return "!" end },
{ rule = "^\\;", conversion = function() return ";" end },
{ rule = "^\\#", conversion = function() return "#" end },
{ rule = "^\\,", conversion = function() return "," end },
{ rule = "^\\{", conversion = function() return "{" end },
{ rule = "^\\}", conversion = function() return "}" end },
{ rule = "^,", conversion = function() return "|" end },
{ rule = "^\\%*", conversion = function() return "\\*" end },
{ rule = "^%*", conversion = function() return "[^\\/]*" end },
{ rule = "^%*%*", conversion = function() return ".*" end },
{ rule = "^%?", conversion = function() return "." end },
{ rule = "^{}", conversion = function() return "{}" end },
{ rule = "^{[^,]+}", conversion = function(match) return match end },
{ rule = "^%b{}",
conversion = function(match)
local out = match:ugsub("%(", "\\(")
:ugsub("%)", "\\)")
:ugsub("%.", "\\.")
:ugsub("\\%[", "[\\[]")
:ugsub("\\%]", "[\\]]")
:ugsub("^\\!", "!")
:ugsub("^\\;", ";")
:ugsub("^\\#", "#")
-- negation chars list
:ugsub("%[!(%a+)%]", "[^%1]")
:ugsub("\\\\", "[\\]")
-- escaped braces
:ugsub("\\{", "[{]")
:ugsub("\\}", "[}]")
-- non escaped braces
:ugsub("{([^%]])", "(%1")
:ugsub("}([^%]])", ")%1")
:ugsub("^{", "(")
:ugsub("}$", ")")
-- escaped globs
:ugsub("\\%*", "[\\*]")
:ugsub("\\%?", "[\\?]")
-- non escaped globs
:ugsub("%*%*", "[*][*]") -- prevent this glob from expanding to next sub
:ugsub("%*([^%]])", "[^\\/]*%1")
:ugsub("%[%*%]%[%*%]", ".*")
:ugsub("%?([^%]])", ".%1")
-- escaped comma
:ugsub("\\,", "[,]")
-- non escaped comma
:ugsub(",([^%]])", "|%1")
return out
end
},
{ rule = "^%[[^/%]]*%]",
conversion = function(match)
local negation = match:umatch("^%[!")
local chars = match:umatch("^%[!?(.-)%]")
chars = chars:ugsub("^%-", "\\-"):ugsub("%-$", "\\-")
local out = ""
if negation then
out = "[^"..chars.."]"
else
out = "["..chars.."]"
end
return out
end
},
}
---List of conversion rules applied to .editorconfig path expressions.
---@type rule[]
local RULES = {
-- normalize escaped .editorconfig special chars or keep them escaped
{ rule = "^\\x[a-fA-F][a-fA-F]", conversion = function(match) return match end },
{ rule = "^\\%*", conversion = function() return "\\*" end },
{ rule = "^\\%?", conversion = function() return "\\?" end },
{ rule = "^\\{", conversion = function() return "{" end },
{ rule = "^\\}", conversion = function() return "}" end },
{ rule = "^\\%[", conversion = function() return "\\[" end },
{ rule = "^\\%]", conversion = function() return "\\]" end },
{ rule = "^\\!", conversion = function() return "!" end },
{ rule = "^\\;", conversion = function() return ";" end },
{ rule = "^\\#", conversion = function() return "#" end },
-- escape special chars
{ rule = "^%.", conversion = function() return "\\." end },
{ rule = "^%(", conversion = function() return "\\(" end },
{ rule = "^%)", conversion = function() return "\\)" end },
{ rule = "^%[[^/%]]*%]",
conversion = function(match)
local negation = match:umatch("^%[!")
local chars = match:umatch("^%[!?(.-)%]")
chars = chars:ugsub("^%-", "\\-"):ugsub("%-$", "\\-")
local out = ""
if negation then
out = "[^"..chars.."]"
else
out = "["..chars.."]"
end
return out
end
},
-- Is this negation rule valid?
{ rule = "^!%w+",
conversion = function(match)
local chars = match:umatch("%w+")
return "[^"..chars.."]"
end
},
-- escape square brackets
{ rule = "^%[", conversion = function() return "\\[" end },
{ rule = "^%]", conversion = function() return "\\]" end },
-- match any characters
{ rule = "^%*%*", conversion = function() return ".*" end },
-- match any characters excluding path separators, \ not needed but just in case
{ rule = "^%*", conversion = function() return "[^\\/]*" end },
-- match optional character, doesn't matters what or should only be a \w?
{ rule = "^%?", conversion = function() return "[^/]" end },
-- threat empty braces literally
{ rule = "^{}", conversion = function() return "{}" end },
-- match a number range
{ rule = "^{%-?%d+%.%.%-?%d+}",
conversion = function(match, section)
local min, max = match:umatch("(-?%d+)%.%.(-?%d+)")
min = tonumber(min)
max = tonumber(max)
if min and max then
if not section.rule.ranges then section.rule.ranges = {} end
table.insert(section.rule.ranges, {
math.min(min, max),
math.max(min, max)
})
end
local minus = ""
if min < 0 or max < 0 then minus = "\\-?" end
return "(?<!0)("..minus.."[1-9]\\d*)"
end
},
-- threat single option braces literally
{ rule = "^{[^,]+}", conversion = function(match) return match end },
-- match invalid range
{ rule = "^{[^%.]+%.%.[^%.]+}", conversion = function(match) return match end },
-- match any of the strings separated by commas inside the curly braces
{ rule = "^%b{}",
conversion = function(rule, section)
rule = rule:gsub("^{", ""):gsub("}$", "")
local pos, len, exp = 1, rule:ulen(), ""
while pos <= len do
local found = false
for _, r in ipairs(RULES_BRACES) do
local match = rule:umatch(r.rule, pos)
if match then
exp = exp .. r.conversion(match, section)
pos = pos + match:ulen()
found = true
break
end
end
if not found then
exp = exp .. rule:usub(pos, pos)
pos = pos + 1
end
end
return "(" .. exp .. ")"
end
}
}
---Adds the regex equivalent of a section path expression.
---@param section plugins.editorconfig.parser.section | string
---@return plugins.editorconfig.parser.section
function Parser:rule_to_regex(section)
if type(section) == "string" then
section = {rule = {expression = section}}
end
local rule = section.rule.expression
-- match everything rule which is different from regular *
-- that doesn't matches path separators
if rule == "*" then
section.rule.regex = ".+"
section.rule.regex_compiled = regex.compile(".+")
return section
end
rule = escapes_to_regex_hex(section.rule.expression)
local pos, len, exp = 1, rule:ulen(), ""
-- if expression starts with ! it is treated entirely as a negation
local negation = rule:umatch("^%s*!")
if negation then
pos = pos + negation:ulen() + 1
end
-- apply all conversion rules by looping the path expression/rule
while pos <= len do
local found = false
for _, r in ipairs(RULES) do
local match = rule:umatch(r.rule, pos)
if match then
exp = exp .. r.conversion(match, section)
pos = pos + match:ulen()
found = true
break
end
end
if not found then
exp = exp .. rule:usub(pos, pos)
pos = pos + 1
end
end
-- force match up to the end
exp = exp .. "$"
-- allow expressions that start with * to match anything on start
if exp:match("^%[^\\/%]%*") then
exp = exp:gsub("^%[^\\/%]%*", ".*")
-- fixes two failing tests
elseif exp:match("^%[") then
exp = "^" .. exp
-- match only on root dir
elseif exp:match("^/") then
exp = exp:gsub("^/", "^")
end
-- store changes to the section rule
section.rule.regex, section.rule.negation = exp, negation
section.rule.regex_compiled = regex.compile(section.rule.regex)
if not section.rule.regex_compiled then
log(
"error",
"could not compile '[%s]' to regex '%s'",
rule, section.rule.regex
)
end
return section
end
---Parses the associated .editorconfig file and stores each section.
function Parser:read()
local file = io.open(self.config_path, "r")
self.sections = {}
if not file then
log("log", "could not read %s", self.config_path)
return
end
---@type plugins.editorconfig.parser.section
local section = {}
for line in file:lines() do
---@cast line string
-- first we try to see if the line is a rule section
local rule = ""
rule = line:umatch("^%s*%[(.+)%]%s*$")
if rule then
if section.rule then
-- save previous section and crerate new one
table.insert(self.sections, section)
section = {}
end
section.rule = {
expression = rule
}
-- convert the expression to a regex directly on the section table
self:rule_to_regex(section)
local clone = rule
if clone:match("//+") or clone:match("/%*%*/") then
section.equivalent_rules = {}
end
while clone:match("//+") or clone:match("/%*%*/") do
---@type plugins.editorconfig.parser.section[]
if clone:match("//+") then
clone = clone:ugsub("//+", "/", 1)
table.insert(section.equivalent_rules, self:rule_to_regex(clone).rule)
end
if clone:match("/%*%*/") then
clone = clone:ugsub("/%*%*/", "/", 1)
table.insert(section.equivalent_rules, self:rule_to_regex(clone).rule)
end
end
end
if not rule then
local name, value = line:umatch("^%s*(%w%S+)%s*=%s*([^\n\r]+)")
if name and value then
name = name:ulower()
-- do not lowercase property values that start with test_
if not name:match("^test_") then
value = value:ulower()
end
if value == "true" then
value = true
elseif value == "false" then
value = false
elseif math.tointeger and math.tointeger(value) then
value = math.tointeger(value)
elseif tonumber(value) then
value = tonumber(value)
end
if section.rule then
section[name] = value
elseif name == "root" and type(value) == "boolean" then
self.root = value
end
end
end
end
if section.rule then
table.insert(self.sections, section)
end
end
---Helper function that converts a regex offset results into a list
---of strings, omitting the first result which is the complete match.
---@param offsets table<integer,integer>
---@param value string
---@return table<integer, string>
local function regex_result_to_table(offsets, value)
local result = {}
local offset_fix = 0
if not regex.find_offsets then
offset_fix = 1
end
for i=3, #offsets, 2 do
table.insert(result, value:sub(offsets[i], offsets[i+1]-offset_fix))
end
return result
end
---Get a matching config for the given filename or nil if nothing found.
---@param file_name string
---@param defaults? boolean Set indent size to defaults when needed,
---@return plugins.editorconfig.parser.section?
function Parser:getConfig(file_name, defaults)
if PLATFORM == "Windows" then
file_name = file_name:gsub("\\", "/")
end
local regex_match = regex.match
if regex.find_offsets then
regex_match = regex.find_offsets
end
local properties = {}
local found = false
for _, section in ipairs(self.sections) do
if section.rule.regex_compiled then
local negation = section.rule.negation
-- default rule
local matched = {regex_match(section.rule.regex_compiled, file_name)}
-- try equivalent rules if available
if not matched[1] and section.equivalent_rules then
for _, esection in ipairs(section.equivalent_rules) do
matched = {regex_match(esection.regex_compiled, file_name)}
if matched[1] then
break
end
end
end
if (matched[1] and not negation) or (not matched[1] and negation) then
local ranges_match = true
if section.rule.ranges then
local results = regex_result_to_table(matched, file_name)
if #results < #section.rule.ranges then
ranges_match = false
else
for i, range in ipairs(section.rule.ranges) do
local number = tonumber(results[i])
if not number then
ranges_match = false
break
end
if number < range[1] or number > range[2] then
ranges_match = false
break
end
end
end
end
if ranges_match then
found = true
for name, value in pairs(section) do
if name ~= "rule" and name ~= "equivalent_rules" then
properties[name] = value
end
end
end
end
end
end
if found and defaults then
if properties.indent_style and properties.indent_style == "space" then
if properties.indent_size and not properties.tab_width then
properties.tab_width = 4
end
elseif properties.indent_style and properties.indent_style == "tab" then
if not properties.tab_width and not properties.indent_size then
properties.indent_size = "tab"
elseif properties.tab_width then
properties.indent_size = properties.tab_width
end
end
end
return found and properties or nil
end
---Get a matching config for the given filename or nil if nothing found.
---@param file_name string
---@return string
function Parser:getConfigString(file_name)
local out = ""
local properties = self:getConfig(file_name, true)
if properties then
local config_sorted = {}
for name, value in pairs(properties) do
table.insert(config_sorted, {name = name, value = value})
end
table.sort(config_sorted, function(a, b)
return a.name < b.name
end)
for _, value in ipairs(config_sorted) do
out = out .. value.name .. "=" .. tostring(value.value) .. "\n"
end
end
return out
end
return Parser

View file

@ -0,0 +1,63 @@
local core = require "core"
local tests = require "plugins.editorconfig.tests"
-- disable print buffer for immediate output
io.stdout:setvbuf "no"
-- overwrite to print into stdout
function core.error(format, ...)
print(string.format(format, ...))
end
function core.log(format, ...)
print(string.format(format, ...))
end
function core.log_quiet(format, ...)
print(string.format(format, ...))
end
-- check if --parsers flag was given to only output the path expressions and
-- their conversion into regular expressions.
local PARSERS = false
for _, argument in ipairs(ARGS) do
if argument == "--parsers" then
PARSERS = true
end
end
if not PARSERS then
require "plugins.editorconfig.tests.glob"
require "plugins.editorconfig.tests.parser"
require "plugins.editorconfig.tests.properties"
tests.run()
else
-- Globs
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/braces.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/brackets.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/question.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/star.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/star_star.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/utf8char.in")
-- Parser
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/basic.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/bom.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/comments.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/comments_and_newlines.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/comments_only.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/crlf.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/empty.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/limits.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/newlines_only.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/whitespace.in")
-- Properties
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/indent_size_default.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/lowercase_names.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/lowercase_values.in")
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/tab_width_default.in")
tests.run_parsers()
end

View file

@ -0,0 +1,71 @@
; test { and }
root=true
; word choice
[*.{py,js,html}]
choice=true
; single choice
[{single}.b]
choice=single
; empty choice
[{}.c]
empty=all
; choice with empty word
[a{b,c,}.d]
empty=word
; choice with empty words
[a{,b,,c,}.e]
empty=words
; no closing brace
[{.f]
closing=false
; nested braces
[{word,{also},this}.g]
nested=true
; nested braces, adjacent at start
[{{a,b},c}.k]
nested_start=true
; nested braces, adjacent at end
[{a,{b,c}}.l]
nested_end=true
; closing inside beginning
[{},b}.h]
closing=inside
; opening inside beginning
[{{,b,c{d}.i]
unmatched=true
; escaped comma
[{a\,b,cd}.txt]
comma=yes
; escaped closing brace
[{e,\},f}.txt]
closing=yes
; escaped backslash
[{g,\\,i}.txt]
backslash=yes
; patterns nested in braces
[{some,a{*c,b}[ef]}.j]
patterns=nested
; numeric braces
[{3..120}]
number=true
; alphabetical
[{aardvark..antelope}]
words=a

View file

@ -0,0 +1,51 @@
; test [ and ]
root=true
; Character choice
[[ab].a]
choice=true
; Negative character choice
[[!ab].b]
choice=false
; Character range
[[d-g].c]
range=true
; Negative character range
[[!d-g].d]
range=false
; Range and choice
[[abd-g].e]
range_and_choice=true
; Choice with dash
[[-ab].f]
choice_with_dash=true
; Close bracket inside
[[\]ab].g]
close_inside=true
; Close bracket outside
[[ab]].g]
close_outside=true
; Negative close bracket inside
[[!\]ab].g]
close_inside=false
; Negative¬close bracket outside
[[!ab]].g]
close_outside=false
; Slash inside brackets
[ab[e/]cd.i]
slash_inside=true
; Slash after an half-open bracket
[ab[/c]
slash_half_open=true

View file

@ -0,0 +1,241 @@
local tests = require "plugins.editorconfig.tests"
-- Tests for *
-- matches a single characters
tests.add("star_single_ML", "glob/star.in", "ace.c", "key=value[ \t\n\r]+keyc=valuec[ \t\n\r]*")
-- matches zero characters
tests.add("star_zero_ML", "glob/star.in", "ae.c", "key=value[ \t\n\r]+keyc=valuec[ \t\n\r]*")
-- matches multiple characters
tests.add("star_multiple_ML", "glob/star.in", "abcde.c", "key=value[ \t\n\r]+keyc=valuec[ \t\n\r]*")
-- does not match path separator
tests.add("star_over_slash", "glob/star.in", "a/e.c", "^[ \t\n\r]*keyc=valuec[ \t\n\r]*$")
-- star after a slash
tests.add("star_after_slash_ML", "glob/star.in", "Bar/foo.txt", "keyb=valueb[ \t\n\r]+keyc=valuec[ \t\n\r]*")
-- star matches a dot file after slash
tests.add("star_matches_dot_file_after_slash_ML", "glob/star.in", "Bar/.editorconfig", "keyb=valueb[ \t\n\r]+keyc=valuec[ \t\n\r]*")
-- star matches a dot file
tests.add("star_matches_dot_file", "glob/star.in", ".editorconfig", "^keyc=valuec[ \t\n\r]*$")
-- Tests for ?
-- matches a single character
tests.add("question_single", "glob/question.in", "some.c", "^key=value[ \t\n\r]*$")
-- does not match zero characters
tests.add("question_zero", "glob/question.in", "som.c", "^[ \t\n\r]*$")
-- does not match multiple characters
tests.add("question_multiple", "glob/question.in", "something.c", "^[ \t\n\r]*$")
-- does not match slash
tests.add("question_slash", "glob/question.in", "som/.c", "^[ \t\n\r]*$")
-- Tests for [ and ]
-- close bracket inside
tests.add("brackets_close_inside", "glob/brackets.in", "].g", "^close_inside=true[ \t\n\r]*$")
-- close bracket outside
tests.add("brackets_close_outside", "glob/brackets.in", "b].g", "^close_outside=true[ \t\n\r]*$")
-- negative close bracket inside
tests.add("brackets_nclose_inside", "glob/brackets.in", "c.g", "^close_inside=false[ \t\n\r]*$")
-- negative close bracket outside
tests.add("brackets_nclose_outside", "glob/brackets.in", "c].g", "^close_outside=false[ \t\n\r]*$")
-- character choice
tests.add("brackets_choice", "glob/brackets.in", "a.a", "^choice=true[ \t\n\r]*$")
-- character choice 2
tests.add("brackets_choice2", "glob/brackets.in", "c.a", "^[ \t\n\r]*$")
-- negative character choice
tests.add("brackets_nchoice", "glob/brackets.in", "c.b", "^choice=false[ \t\n\r]*$")
-- negative character choice 2
tests.add("brackets_nchoice2", "glob/brackets.in", "a.b", "^[ \t\n\r]*$")
-- character range
tests.add("brackets_range", "glob/brackets.in", "f.c", "^range=true[ \t\n\r]*$")
-- character range 2
tests.add("brackets_range2", "glob/brackets.in", "h.c", "^[ \t\n\r]*$")
-- negative character range
tests.add("brackets_nrange", "glob/brackets.in", "h.d", "^range=false[ \t\n\r]*$")
-- negative character range 2
tests.add("brackets_nrange2", "glob/brackets.in", "f.d", "^[ \t\n\r]*$")
-- range and choice
tests.add("brackets_range_and_choice", "glob/brackets.in", "e.e",
"^range_and_choice=true[ \t\n\r]*$")
-- character choice with a dash
tests.add("brackets_choice_with_dash", "glob/brackets.in", "-.f",
"^choice_with_dash=true[ \t\n\r]*$")
-- slash inside brackets
tests.add("brackets_slash_inside1", "glob/brackets.in", "ab/cd.i",
"^[ \t\n\r]*$")
tests.add("brackets_slash_inside2", "glob/brackets.in", "abecd.i",
"^[ \t\n\r]*$")
tests.add("brackets_slash_inside3", "glob/brackets.in", "ab[e/]cd.i",
"^slash_inside=true[ \t\n\r]*$")
tests.add("brackets_slash_inside4", "glob/brackets.in", "ab[/c",
"^slash_half_open=true[ \t\n\r]*$")
-- Tests for { and }
-- word choice
tests.add("braces_word_choice1", "glob/braces.in", "test.py", "^choice=true[ \t\n\r]*$")
tests.add("braces_word_choice2", "glob/braces.in", "test.js", "^choice=true[ \t\n\r]*$")
tests.add("braces_word_choice3", "glob/braces.in", "test.html", "^choice=true[ \t\n\r]*$")
tests.add("braces_word_choice4", "glob/braces.in", "test.pyc", "^[ \t\n\r]*$")
-- single choice
tests.add("braces_single_choice", "glob/braces.in", "{single}.b", "^choice=single[ \t\n\r]*$")
tests.add("braces_single_choice_negative", "glob/braces.in", ".b", "^[ \t\n\r]*$")
-- empty choice
tests.add("braces_empty_choice", "glob/braces.in", "{}.c", "^empty=all[ \t\n\r]*$")
tests.add("braces_empty_choice_negative", "glob/braces.in", ".c", "^[ \t\n\r]*$")
-- choice with empty word
tests.add("braces_empty_word1", "glob/braces.in", "a.d", "^empty=word[ \t\n\r]*$")
tests.add("braces_empty_word2", "glob/braces.in", "ab.d", "^empty=word[ \t\n\r]*$")
tests.add("braces_empty_word3", "glob/braces.in", "ac.d", "^empty=word[ \t\n\r]*$")
tests.add("braces_empty_word4", "glob/braces.in", "a,.d", "^[ \t\n\r]*$")
-- choice with empty words
tests.add("braces_empty_words1", "glob/braces.in", "a.e", "^empty=words[ \t\n\r]*$")
tests.add("braces_empty_words2", "glob/braces.in", "ab.e", "^empty=words[ \t\n\r]*$")
tests.add("braces_empty_words3", "glob/braces.in", "ac.e", "^empty=words[ \t\n\r]*$")
tests.add("braces_empty_words4", "glob/braces.in", "a,.e", "^[ \t\n\r]*$")
-- no closing brace
tests.add("braces_no_closing", "glob/braces.in", "{.f", "^closing=false[ \t\n\r]*$")
tests.add("braces_no_closing_negative", "glob/braces.in", ".f", "^[ \t\n\r]*$")
-- nested braces
tests.add("braces_nested1", "glob/braces.in", "word,this}.g", "^[ \t\n\r]*$")
tests.add("braces_nested2", "glob/braces.in", "{also,this}.g", "^[ \t\n\r]*$")
tests.add("braces_nested3", "glob/braces.in", "word.g", "^nested=true[ \t\n\r]*$")
tests.add("braces_nested4", "glob/braces.in", "{also}.g", "^nested=true[ \t\n\r]*$")
tests.add("braces_nested5", "glob/braces.in", "this.g", "^nested=true[ \t\n\r]*$")
-- nested braces, adjacent at start
tests.add("braces_nested_start1", "glob/braces.in", "{{a,b},c}.k", "^[ \t\n\r]*$")
tests.add("braces_nested_start2", "glob/braces.in", "{a,b}.k", "^[ \t\n\r]*$")
tests.add("braces_nested_start3", "glob/braces.in", "a.k", "^nested_start=true[ \t\n\r]*$")
tests.add("braces_nested_start4", "glob/braces.in", "b.k", "^nested_start=true[ \t\n\r]*$")
tests.add("braces_nested_start5", "glob/braces.in", "c.k", "^nested_start=true[ \t\n\r]*$")
-- nested braces, adjacent at end
tests.add("braces_nested_end1", "glob/braces.in", "{a,{b,c}}.l", "^[ \t\n\r]*$")
tests.add("braces_nested_end2", "glob/braces.in", "{b,c}.l", "^[ \t\n\r]*$")
tests.add("braces_nested_end3", "glob/braces.in", "a.l", "^nested_end=true[ \t\n\r]*$")
tests.add("braces_nested_end4", "glob/braces.in", "b.l", "^nested_end=true[ \t\n\r]*$")
tests.add("braces_nested_end5", "glob/braces.in", "c.l", "^nested_end=true[ \t\n\r]*$")
-- closing inside beginning
tests.add("braces_closing_in_beginning", "glob/braces.in", "{},b}.h", "^closing=inside[ \t\n\r]*$")
-- missing closing braces
tests.add("braces_unmatched1", "glob/braces.in", "{{,b,c{d}.i", "^unmatched=true[ \t\n\r]*$")
tests.add("braces_unmatched2", "glob/braces.in", "{.i", "^[ \t\n\r]*$")
tests.add("braces_unmatched3", "glob/braces.in", "b.i", "^[ \t\n\r]*$")
tests.add("braces_unmatched4", "glob/braces.in", "c{d.i", "^[ \t\n\r]*$")
tests.add("braces_unmatched5", "glob/braces.in", ".i", "^[ \t\n\r]*$")
-- escaped comma
tests.add("braces_escaped_comma1", "glob/braces.in", "a,b.txt", "^comma=yes[ \t\n\r]*$")
tests.add("braces_escaped_comma2", "glob/braces.in", "a.txt", "^[ \t\n\r]*$")
tests.add("braces_escaped_comma3", "glob/braces.in", "cd.txt", "^comma=yes[ \t\n\r]*$")
-- escaped closing brace
tests.add("braces_escaped_brace1", "glob/braces.in", "e.txt", "^closing=yes[ \t\n\r]*$")
tests.add("braces_escaped_brace2", "glob/braces.in", "}.txt", "^closing=yes[ \t\n\r]*$")
tests.add("braces_escaped_brace3", "glob/braces.in", "f.txt", "^closing=yes[ \t\n\r]*$")
-- escaped backslash
tests.add("braces_escaped_backslash1", "glob/braces.in", "g.txt", "^backslash=yes[ \t\n\r]*$")
if PLATFORM ~= "Windows" then
tests.add("braces_escaped_backslash2", "glob/braces.in", "\\.txt", "^backslash=yes[ \t\n\r]*$")
end
tests.add("braces_escaped_backslash3", "glob/braces.in", "i.txt", "^backslash=yes[ \t\n\r]*$")
-- patterns nested in braces
tests.add("braces_patterns_nested1", "glob/braces.in", "some.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested2", "glob/braces.in", "abe.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested3", "glob/braces.in", "abf.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested4", "glob/braces.in", "abg.j", "^[ \t\n\r]*$")
tests.add("braces_patterns_nested5", "glob/braces.in", "ace.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested6", "glob/braces.in", "acf.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested7", "glob/braces.in", "acg.j", "^[ \t\n\r]*$")
tests.add("braces_patterns_nested8", "glob/braces.in", "abce.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested9", "glob/braces.in", "abcf.j", "^patterns=nested[ \t\n\r]*$")
tests.add("braces_patterns_nested10", "glob/braces.in", "abcg.j", "^[ \t\n\r]*$")
tests.add("braces_patterns_nested11", "glob/braces.in", "ae.j", "^[ \t\n\r]*$")
tests.add("braces_patterns_nested12", "glob/braces.in", ".j", "^[ \t\n\r]*$")
-- numeric brace range
tests.add("braces_numeric_range1", "glob/braces.in", "1", "^[ \t\n\r]*$")
tests.add("braces_numeric_range2", "glob/braces.in", "3", "^number=true[ \t\n\r]*$")
tests.add("braces_numeric_range3", "glob/braces.in", "15", "^number=true[ \t\n\r]*$")
tests.add("braces_numeric_range4", "glob/braces.in", "60", "^number=true[ \t\n\r]*$")
tests.add("braces_numeric_range5", "glob/braces.in", "5a", "^[ \t\n\r]*$")
tests.add("braces_numeric_range6", "glob/braces.in", "120", "^number=true[ \t\n\r]*$")
tests.add("braces_numeric_range7", "glob/braces.in", "121", "^[ \t\n\r]*$")
tests.add("braces_numeric_range8", "glob/braces.in", "060", "^[ \t\n\r]*$")
-- alphabetical brace range: letters should not be considered for ranges
tests.add("braces_alpha_range1", "glob/braces.in", "{aardvark..antelope}", "^words=a[ \t\n\r]*$")
tests.add("braces_alpha_range2", "glob/braces.in", "a", "^[ \t\n\r]*$")
tests.add("braces_alpha_range3", "glob/braces.in", "aardvark", "^[ \t\n\r]*$")
tests.add("braces_alpha_range4", "glob/braces.in", "agreement", "^[ \t\n\r]*$")
tests.add("braces_alpha_range5", "glob/braces.in", "antelope", "^[ \t\n\r]*$")
tests.add("braces_alpha_range6", "glob/braces.in", "antimatter", "^[ \t\n\r]*$")
-- Tests for **
-- test EditorConfig files with UTF-8 characters larger than 127
tests.add("utf_8_char", "glob/utf8char.in", "中文.txt", "^key=value[ \t\n\r]*$")
-- matches over path separator
tests.add("star_star_over_separator1", "glob/star_star.in", "a/z.c", "^key1=value1[ \t\n\r]*$")
tests.add("star_star_over_separator2", "glob/star_star.in", "amnz.c", "^key1=value1[ \t\n\r]*$")
tests.add("star_star_over_separator3", "glob/star_star.in", "am/nz.c", "^key1=value1[ \t\n\r]*$")
tests.add("star_star_over_separator4", "glob/star_star.in", "a/mnz.c", "^key1=value1[ \t\n\r]*$")
tests.add("star_star_over_separator5", "glob/star_star.in", "amn/z.c", "^key1=value1[ \t\n\r]*$")
tests.add("star_star_over_separator6", "glob/star_star.in", "a/mn/z.c", "^key1=value1[ \t\n\r]*$")
tests.add("star_star_over_separator7", "glob/star_star.in", "b/z.c", "^key2=value2[ \t\n\r]*$")
tests.add("star_star_over_separator8", "glob/star_star.in", "b/mnz.c", "^key2=value2[ \t\n\r]*$")
tests.add("star_star_over_separator9", "glob/star_star.in", "b/mn/z.c", "^key2=value2[ \t\n\r]*$")
tests.add("star_star_over_separator10", "glob/star_star.in", "bmnz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator11", "glob/star_star.in", "bm/nz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator12", "glob/star_star.in", "bmn/z.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator13", "glob/star_star.in", "c/z.c", "^key3=value3[ \t\n\r]*$")
tests.add("star_star_over_separator14", "glob/star_star.in", "cmn/z.c", "^key3=value3[ \t\n\r]*$")
tests.add("star_star_over_separator15", "glob/star_star.in", "c/mn/z.c", "^key3=value3[ \t\n\r]*$")
tests.add("star_star_over_separator16", "glob/star_star.in", "cmnz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator17", "glob/star_star.in", "cm/nz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator18", "glob/star_star.in", "c/mnz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator19", "glob/star_star.in", "d/z.c", "^key4=value4[ \t\n\r]*$")
tests.add("star_star_over_separator20", "glob/star_star.in", "d/mn/z.c", "^key4=value4[ \t\n\r]*$")
tests.add("star_star_over_separator21", "glob/star_star.in", "dmnz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator22", "glob/star_star.in", "dm/nz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator23", "glob/star_star.in", "d/mnz.c", "^[ \t\n\r]*$")
tests.add("star_star_over_separator24", "glob/star_star.in", "dmn/z.c", "^[ \t\n\r]*$")

View file

@ -0,0 +1,7 @@
; test ?
root=true
[som?.c]
key=value

View file

@ -0,0 +1,12 @@
; test *
root=true
[a*e.c]
key=value
[Bar/*]
keyb=valueb
[*]
keyc=valuec

View file

@ -0,0 +1,15 @@
; test **
root=true
[a**z.c]
key1=value1
[b/**z.c]
key2=value2
[c**/z.c]
key3=value3
[d/**/z.c]
key4=value4

View file

@ -0,0 +1,6 @@
; test EditorConfig files with UTF-8 characters larger than 127
root = true
[中文.txt]
key = value

View file

@ -0,0 +1,143 @@
local Parser = require "plugins.editorconfig.parser"
local tests = {}
---@class tests.test
---@field name string Name of test
---@field config string Path to config file
---@field in_match string A path to test against the config
---@field out_match string A regex to match against the result
---Registered tests
---@type tests.test[]
tests.list = {}
--- parsers cache
---@type table<string,plugins.editorconfig.parser>
local parsers = {}
setmetatable(parsers, {
__index = function(t, k)
local v = rawget(t, k)
if v == nil then
v = Parser.new(k)
rawset(t, k, v)
end
return v
end
})
---Adds color to given text on non windows systems.
---@param text string
---@param color "red" | "green" | "yellow"
---@return string colorized_text
local function colorize(text, color)
if PLATFORM ~= "Windows" then
if color == "green" then
return "\27[92m"..text.."\27[0m"
elseif color == "red" then
return "\27[91m"..text.."\27[0m"
elseif color == "yellow" then
return "\27[93m"..text.."\27[0m"
end
end
return text
end
local PASSED = colorize("PASSED", "green")
local FAILED = colorize("FAILED", "red")
---Runs an individual test (executed by tests.run())
---@param name string Test name
---@param config_path string Relative path to tests diretory for a [config].in
---@param in_match string Filename to match
---@param out_match string | table Result to match regex
function tests.check_config(name, config_path, in_match, out_match, pos, total)
if type(out_match) == "string" then
out_match = { out_match }
end
local parser = parsers[USERDIR .. "/plugins/editorconfig/tests/" .. config_path]
local config = parser:getConfigString(in_match)
local passed = true
for _, match in ipairs(out_match) do
if not regex.match(match, config) then
passed = false
break
end
end
if pos then
pos = "[" .. pos .. "/" .. total .. "] "
else
pos = ""
end
if passed then
print(pos .. string.format("%s - %s - '%s': %s", name, in_match, config_path, PASSED))
else
print(pos .. string.format("%s - %s - '%s': %s", name, in_match, config_path, FAILED))
print(config)
end
return passed
end
---Register a new test to be run later.
---@param name string Test name
---@param config_path string Relative path to tests diretory for a [config].in
---@param in_match string Filename to match
---@param out_match string | table Result to match regex
function tests.add(name, config_path, in_match, out_match)
table.insert(tests.list, {
name = name,
config = config_path,
in_match = in_match,
out_match = out_match
})
end
---Runs all registered tests and outputs the results to terminal.
function tests.run()
print "========================================================="
print "Running Tests"
print "========================================================="
local failed = 0
local passed = 0
local total = #tests.list
for i, test in ipairs(tests.list) do
local res = tests.check_config(
test.name, test.config, test.in_match, test.out_match, i, total
)
if res then passed = passed + 1 else failed = failed + 1 end
end
print "========================================================="
print (
string.format(
"%s %s %s",
colorize("Total tests: " .. #tests.list, "yellow"),
colorize("Passed: " .. passed, "green"),
colorize("Failed: " .. failed, "red")
)
)
print "========================================================="
end
function tests.add_parser(config_path)
return parsers[config_path]
end
function tests.run_parsers()
print "========================================================="
print "Running Parsers"
print "========================================================="
for config, parser in pairs(parsers) do
print "---------------------------------------------------------"
print(string.format("%s results:", config))
for _, section in ipairs(parser.sections) do
print(string.format("\nPath expression: %s", section.rule.expression))
print(string.format("Regex: %s", section.rule.regex))
print(string.format("Negation: %s", section.rule.negation and "true" or "false"))
print(string.format("Ranges: %s\n", section.rule.ranges and #section.rule.ranges or "0"))
end
print "---------------------------------------------------------"
end
end
return tests

View file

@ -0,0 +1,16 @@
[*.a]
option1=value1
; repeat section
[*.a]
option2=value2
[*.b]
option1 = a
option2 = a
[b.b]
option2 = b
[*.b]
option1 = c

View file

@ -0,0 +1,6 @@
; test EditorConfig files with BOM
root = true
[*]
key = value

View file

@ -0,0 +1,47 @@
; test comments
root = true
[test3.c]
; Comment before properties ignored
key=value
[test4.c]
key1=value1
; Comment between properties ignored
key2=value2
; Semicolon or hash at end of value read as part of value
[test5.c]
key1=value; not comment
key2=value # not comment
; Backslash before a semicolon or hash is part of the value
[test6.c]
key1=value \; not comment
key2=value \# not comment
; Escaped semicolon in section name
[test\;.c]
key=value
[test9.c]
# Comment before properties ignored
key=value
[test10.c]
key1=value1
# Comment between properties ignored
key2=value2
# Octothorpe at end of value read as part of value
[test11.c]
key=value# not comment
# Escaped octothorpe in value
[test12.c]
key=value \# not comment
# Escaped octothorpe in section name
[test\#.c]
key=value

View file

@ -0,0 +1,4 @@
# Just comments
# ... and newlines

View file

@ -0,0 +1 @@
# Just a comment, nothing else

View file

@ -0,0 +1,6 @@
; test EditorConfig files with CRLF line separators
root = true
[*]
key = value

View file

@ -0,0 +1,107 @@
local tests = require "plugins.editorconfig.tests"
-- Basic parser tests
-- test repeat sections
tests.add("repeat_sections_ML", "parser/basic.in", "a.a", "option1=value1[ \t]*[\n\r]+option2=value2[ \t\n\r]*")
tests.add("basic_cascade_ML", "parser/basic.in", "b.b", "option1=c[ \t]*[\n\r]+option2=b[ \t\n\r]*")
-- Tests for whitespace parsing
-- test no whitespaces in property assignment
tests.add("no_whitespace", "parser/whitespace.in", "test1.c", "^key=value[ \t\n\r]*$")
-- test single spaces around equals sign
tests.add("single_spaces_around_equals", "parser/whitespace.in", "test2.c",
"^key=value[ \t\n\r]*$")
-- test multiple spaces around equals sign
tests.add("multiple_spaces_around_equals", "parser/whitespace.in", "test3.c",
"^key=value[ \t\n\r]*$")
-- test spaces before property name
tests.add("spaces_before_property_name", "parser/whitespace.in", "test4.c",
"^key=value[ \t\n\r]*$")
-- test spaces before after property value
tests.add("spaces_after_property_value", "parser/whitespace.in", "test5.c",
"^key=value[ \t\n\r]*$")
-- test blank lines between properties
tests.add("blank_lines_between_properties_ML", "parser/whitespace.in", "test6.c",
"key1=value1[ \t]*[\n\r]+key2=value2[ \t\n\r]*")
-- test spaces in section name
tests.add("spaces_in_section_name", "parser/whitespace.in", " test 7 ",
"^key=value[ \t\n\r]*$")
-- test spaces before section name are ignored
tests.add("spaces_before_section_name", "parser/whitespace.in", "test8.c",
"^key=value[ \t\n\r]*$")
-- test spaces after section name
tests.add("spaces_after_section_name", "parser/whitespace.in", "test9.c", "^key=value[ \t\n\r]*$")
-- test spaces at beginning of line between properties
tests.add("spaces_before_middle_property_ML", "parser/whitespace.in", "test10.c",
"key1=value1[ \t]*[\n\r]+key2=value2[ \t]*[\n\r]+key3=value3[ \t\n\r]*")
-- Tests for comment parsing
-- test comments ignored before properties
tests.add("comment_before_props", "parser/comments.in", "test3.c",
"^key=value[ \t\n\r]*$")
-- test comments ignored between properties
tests.add("comment_between_props_ML", "parser/comments.in", "test4.c",
"key1=value1[ \t]*[\n\r]+key2=value2[ \t\n\r]*")
-- test semicolons and hashes at end of property value are included in value
tests.add("semicolon_or_hash_in_property", "parser/comments.in", "test5.c",
"^key1=value; not comment[\n\r]+key2=value # not comment[ \t\n\r]*$")
-- test that backslashes before semicolons and hashes in property values
-- are included in value.
-- NOTE: [\\] matches a single literal backslash.
tests.add("backslashed_semicolon_or_hash_in_property", "parser/comments.in", "test6.c",
"^key1=value [\\\\]; not comment[\n\r]+key2=value [\\\\]# not comment[ \t\n\r]*$")
-- test escaped semicolons are included in section names
tests.add("escaped_semicolon_in_section", "parser/comments.in", "test;.c",
"^key=value[ \t\n\r]*$")
-- test octothorpe comments ignored before properties
tests.add("octothorpe_comment_before_props", "parser/comments.in", "test9.c",
"^key=value[ \t\n\r]*$")
-- test octothorpe comments ignored between properties
tests.add("octothorpe_comment_between_props_ML", "parser/comments.in", "test10.c",
"key1=value1[ \t]*[\n\r]+key2=value2[ \t\n\r]*")
-- test octothorpe at end of property value are included in value
tests.add("octothorpe_in_value", "parser/comments.in", "test11.c",
"^key=value# not comment[ \t\n\r]*$")
-- test escaped octothorpes are included in section names
tests.add("escaped_octothorpe_in_section", "parser/comments.in", "test#.c",
"^key=value[ \t\n\r]*$")
-- test EditorConfig files with BOM at the head
tests.add("bom_at_head", "parser/bom.in", "a.c", "^key=value[ \t\n\r]*$")
-- test EditorConfig files with CRLF line separators
tests.add("crlf_linesep", "parser/crlf.in", "a.c", "^key=value[ \t\n\r]*$")
-- Test minimum supported lengths of section name, key and value
tests.add("min_supported_key_length", "parser/limits.in", "test1",
"^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=v1024[ \t\n\r]*$")
tests.add("min_supported_value_length", "parser/limits.in", "test2",
"^k4096=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ \t\n\r]*$")
tests.add("min_supported_section_name_length", "parser/limits.in", "test3",
"^k1024=v1024[ \t\n\r]*$")
-- Empty .editorconfig files
tests.add("empty_editorconfig_file", "parser/empty.in", "test4", "^[ \t\n\r]*$")
tests.add("newlines_only_editorconfig_file", "parser/newlines_only.in", "test4", "^[ \t\n\r]*$")
tests.add("comments_only_editorconfig_file", "parser/comments_only.in", "test4", "^[ \t\n\r]*$")
tests.add("comments_and_newlines_editorconfig_file", "parser/comments_and_newlines.in", "test4", "^[ \t\n\r]*$")

View file

@ -0,0 +1,13 @@
root = true
; minimum supported key length of 1024 characters
[test1]
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=v1024
; minimum supported value length of 4096 characters
[test2]
k4096=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
; minimum supported section name length of 1024 characters (excluding [] brackets)
[{test3,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}]
k1024=v1024

View file

@ -0,0 +1,48 @@
; test whitespace usage
root = true
; no whitespace
[test1.c]
key=value
; spaces around equals
[test2.c]
key = value
; lots of space after equals
[test3.c]
key = value
; spaces before property name
[test4.c]
key=value
; spaces after property value
[test5.c]
key=value
; blank lines between properties
[test6.c]
key1=value1
key2=value2
; spaces in section name
[ test 7 ]
key=value
; spaces before section name
[test8.c]
key=value
; spaces after section name
[test9.c]
key=value
; spacing before middle property
[test10.c]
key1=value1
key2=value2
key3=value3

View file

@ -0,0 +1,11 @@
root = true
[test.c]
indent_style = tab
[test2.c]
indent_style = space
[test3.c]
indent_style = tab
tab_width = 2

View file

@ -0,0 +1,42 @@
local tests = require "plugins.editorconfig.tests"
-- test tab_width default
tests.add("tab_width_default_ML", "properties/tab_width_default.in", "test.c",
"indent_size=4[ \t]*[\n\r]+indent_style=space[ \t]*[\n\r]+tab_width=4[\t\n\r]*")
-- Tab_width should not be set to any value if indent_size is "tab" and
-- tab_width is not set
tests.add("tab_width_default_indent_size_tab_ML", "properties/tab_width_default.in",
"test2.c", "indent_size=tab[ \t]*[\n\r]+indent_style=tab[ \t\n\r]*")
-- Test indent_size default. When indent_style is "tab", indent_size defaults to
-- "tab".
tests.add("indent_size_default_ML", "properties/indent_size_default.in", "test.c",
"indent_size=tab[ \t]*[\n\r]+indent_style=tab[ \t\n\r]*")
-- Test indent_size default. When indent_style is "space", indent_size has no
-- default value.
tests.add("indent_size_default_space", "properties/indent_size_default.in", "test2.c",
"^indent_style=space[ \t\n\r]*$")
-- Test indent_size default. When indent_style is "tab" and tab_width is set,
-- indent_size should default to tab_width
tests.add("indent_size_default_with_tab_width_ML",
"properties/indent_size_default.in", "test3.c",
"indent_size=2[ \t]*[\n\r]+indent_style=tab[ \t]*[\n\r]+tab_width=2[ \t\n\r]*")
-- test that same property values are lowercased (v0.9.0 properties)
tests.add("lowercase_values1_ML", "properties/lowercase_values.in", "test1.c",
"end_of_line=crlf[ \t]*[\n\r]+indent_style=space[ \t\n\r]*")
-- test that same property values are lowercased (v0.9.0 properties)
tests.add("lowercase_values2_ML", "properties/lowercase_values.in", "test2.c",
"charset=utf-8[ \t]*[\n\r]+insert_final_newline=true[ \t]*[\n\r]+trim_trailing_whitespace=false[ \t\n\r]*$")
-- test that same property values are not lowercased
tests.add("lowercase_values3", "properties/lowercase_values.in", "test3.c",
"^test_property=TestValue[ \t\n\r]*$")
-- test that all property names are lowercased
tests.add("lowercase_names", "properties/lowercase_names.in", "test.c",
"^testproperty=testvalue[ \t\n\r]*$")

View file

@ -0,0 +1,6 @@
; test that property names are lowercased
root = true
[test.c]
TestProperty = testvalue

View file

@ -0,0 +1,15 @@
; test property name lowercasing
root = true
[test1.c]
indent_style = Space
end_of_line = CRLF
[test2.c]
insert_final_newline = TRUE
trim_trailing_whitespace = False
charset = UTF-8
[test3.c]
test_property = TestValue

View file

@ -0,0 +1,9 @@
root = true
[test.c]
indent_style = space
indent_size = 4
[test2.c]
indent_style = tab
indent_size = tab

View file

@ -0,0 +1,232 @@
-- mod-version:3
local syntax = require "core.syntax"
syntax.add {
name = "Go",
files = { "%.go$" },
comment = "//",
block_comment = {"/*", "*/"},
patterns = {
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "`", "`", '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = "0[oO_][0-7]+i?", type = "number" },
{ pattern = "-?0x[%x_]+i?", type = "number" },
{ pattern = "-?%d+_%di?", type = "number" },
{ pattern = "-?%d+[%d%.eE]*f?i?", type = "number" },
{ pattern = "-?%.?%d+f?i?", type = "number" },
-- goto label
{ pattern = "^%s+()[%a_][%w%_]*()%s*:%s$", -- this is to fix `default:`
type = { "normal", "function", "normal" }
},
{ pattern = "^%s*[%a_][%w%_]*()%s*:%s$",
type = { "function", "normal" }
},
-- pointer, generic and reference type
{ pattern = "[%*~&]()[%a_][%w%_]*",
type = { "operator", "keyword2" }
},
-- slice type
{ pattern = "%[%]()[%a_][%w%_]*",
type = { "operator", "keyword2" }
},
-- type coerce
{
pattern = "%.%(()[%a_][%w_]*()%)",
type = { "normal", "keyword2", "normal" }
},
-- struct literal
{ pattern = "[%a_][%w%_]*()%s*{%s*",
type = { "keyword2", "normal" }
},
-- operators
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
{ pattern = ":=", type = "operator" },
-- function calls
{ pattern = "func()%s*[%a_][%w_]*()%f[%[(]", -- function statement
type = {"keyword", "function", "normal"}
},
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
{ pattern = "%.()[%a_][%w_]*%f[(]",
type = { "normal", "function" }
},
-- type declaration
{ pattern = "type()%s+()[%a_][%w%_]*",
type = { "keyword", "normal", "keyword2" }
},
-- variable declaration
{ pattern = "var()%s+()[%a_][%w%_]*",
type = { "keyword", "normal", "symbol" }
},
-- goto
{ pattern = "goto()%s+()[%a_][%w%_]*",
type = { "keyword", "normal", "function" }
},
-- if fix
{ pattern = "if()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- for fix
{ pattern = "for()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- return fix
{ pattern = "return()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- range fix
{ pattern = "range()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- func fix
{ pattern = "func()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- switch fix
{ pattern = "switch()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- case fix
{ pattern = "case()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- break fix
{ pattern = "break()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- continue fix
{ pattern = "continue()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- package fix
{ pattern = "package()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- go fix
{ pattern = "go()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- chan fix
{ pattern = "chan()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- defer fix
{ pattern = "defer()%s+%f[%a_]",
type = { "keyword", "normal" }
},
-- field declaration
{ pattern = "[%a_][%w%_]*()%s*():%s*%f[%w%p]",
type = { "function", "normal", "operator" }
},
-- parameters or declarations
{ pattern = "[%a_][%w%_]*()%s+()[%*~&]?()[%a_][%w%_]*",
type = { "literal", "normal", "operator", "keyword2" }
},
{ pattern = "[%a_][%w_]*()%s+()%[%]()[%a_][%w%_]*",
type = { "literal", "normal", "normal", "keyword2" }
},
-- single return type
{
pattern = "%)%s+%(?()[%a_][%w%_]*()%)?%s+%{",
type = { "normal", "keyword2", "normal" }
},
-- sub fields
{ pattern = "%.()[%a_][%w_]*",
type = { "normal", "literal" }
},
-- every other symbol
{ pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["if"] = "keyword",
["else"] = "keyword",
["elseif"] = "keyword",
["for"] = "keyword",
["continue"] = "keyword",
["return"] = "keyword",
["struct"] = "keyword",
["switch"] = "keyword",
["case"] = "keyword",
["default"] = "keyword",
["const"] = "keyword",
["package"] = "keyword",
["import"] = "keyword",
["func"] = "keyword",
["var"] = "keyword",
["type"] = "keyword",
["interface"] = "keyword",
["select"] = "keyword",
["break"] = "keyword",
["range"] = "keyword",
["chan"] = "keyword",
["defer"] = "keyword",
["go"] = "keyword",
["fallthrough"] = "keyword",
["goto"] = "keyword",
["iota"] = "keyword2",
["int"] = "keyword2",
["int64"] = "keyword2",
["int32"] = "keyword2",
["int16"] = "keyword2",
["int8"] = "keyword2",
["uint"] = "keyword2",
["uint64"] = "keyword2",
["uint32"] = "keyword2",
["uint16"] = "keyword2",
["uint8"] = "keyword2",
["uintptr"] = "keyword2",
["float64"] = "keyword2",
["float32"] = "keyword2",
["map"] = "keyword2",
["string"] = "keyword2",
["rune"] = "keyword2",
["bool"] = "keyword2",
["byte"] = "keyword2",
["error"] = "keyword2",
["complex64"] = "keyword2",
["complex128"] = "keyword2",
["true"] = "literal",
["false"] = "literal",
["nil"] = "literal",
},
}
syntax.add {
name = "Go",
files = { "go%.mod" },
comment = "//",
patterns = {
{ pattern = "//.-\n", type = "comment"},
{ pattern = "module() %S+()",
type = { "keyword", "string", "normal"}
},
{ pattern = "go() %S+()",
type = { "keyword", "string", "normal" }
},
{ pattern = "%S+() v%S+()",
type = { "string", "keyword", "normal" }
},
},
symbols = {
["require"] = "keyword",
["module"] = "keyword",
["go"] = "keyword",
}
}
syntax.add {
name = "Go",
files = { "go%.sum" },
patterns = {
{ pattern = "%S+() v[^/]-() h1:()%S+()=",
type = { "string", "keyword", "normal", "string", "normal" }
},
{ pattern = "%S+() v[^/]-()/%S+() h1:()%S+()=",
type = { "string", "keyword", "string", "normal", "string", "normal" }
},
},
symbols = {}
}

View file

@ -0,0 +1,34 @@
-- mod-version:3 priority:110
local syntax = require "core.syntax"
syntax.add {
name = "JSON",
files = {
"%.json$",
"%.cjson$",
"%.jsonc$",
"%.ipynb$",
},
comment = "//",
block_comment = {"/*", "*/"},
patterns = {
-- cjson support
{ pattern = "//.*", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ regex = [["(?:[^"\\]|\\.)*"()\s*:]], type = { "keyword", "normal" } }, -- key
{ regex = [["(?:[^"\\]|\\.)*"]], type = "string" }, -- value
{ pattern = "0x[%da-fA-F]+", type = "number" },
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
{ pattern = "-?%.?%d+", type = "number" },
{ pattern = "null", type = "literal" },
{ pattern = "true", type = "literal" },
{ pattern = "false", type = "literal" }
},
symbols = { }
}

View file

@ -0,0 +1,98 @@
-- mod-version:3
local syntax = require "core.syntax"
syntax.add {
name = "Shell script",
files = { "%.sh$", "%.bash$", "^%.bashrc$", "^%.bash_profile$", "^%.profile$", "%.zsh$", "%.fish$" },
headers = "^#!.*bin.*sh\n",
comment = "#",
patterns = {
-- $# is a bash special variable and the '#' shouldn't be interpreted
-- as a comment.
{ pattern = "$[%a_@*#][%w_]*", type = "keyword2" },
-- Comments
{ pattern = "#.*\n", type = "comment" },
-- Strings
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
{ pattern = { '`', '`', '\\' }, type = "string" },
-- Ignore numbers that start with dots or slashes
{ pattern = "%f[%w_%.%/]%d[%d%.]*%f[^%w_%.]", type = "number" },
-- Operators
{ pattern = "[!<>|&%[%]:=*]", type = "operator" },
-- Match parameters
{ pattern = "%f[%S][%+%-][%w%-_:]+", type = "function" },
{ pattern = "%f[%S][%+%-][%w%-_]+%f[=]", type = "function" },
-- Prevent parameters with assignments from been matched as variables
{
pattern = "%s%-%a[%w_%-]*%s+()%d[%d%.]+",
type = { "function", "number" }
},
{
pattern = "%s%-%a[%w_%-]*%s+()%a[%a%-_:=]+",
type = { "function", "symbol" }
},
-- Match variable assignments
{ pattern = "[_%a][%w_]+%f[%+=]", type = "keyword2" },
-- Match variable expansions
{ pattern = "${.-}", type = "keyword2" },
{ pattern = "$[%d$%a_@*][%w_]*", type = "keyword2" },
-- Functions
{ pattern = "[%a_%-][%w_%-]*[%s]*%f[(]", type = "function" },
-- Everything else
{ pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["case"] = "keyword",
["in"] = "keyword",
["esac"] = "keyword",
["if"] = "keyword",
["then"] = "keyword",
["elif"] = "keyword",
["else"] = "keyword",
["fi"] = "keyword",
["while"] = "keyword",
["do"] = "keyword",
["done"] = "keyword",
["for"] = "keyword",
["break"] = "keyword",
["continue"] = "keyword",
["function"] = "keyword",
["local"] = "keyword",
["echo"] = "keyword",
["return"] = "keyword",
["exit"] = "keyword",
["alias"] = "keyword",
["test"] = "keyword",
["cd"] = "keyword",
["declare"] = "keyword",
["enable"] = "keyword",
["eval"] = "keyword",
["exec"] = "keyword",
["export"] = "keyword",
["getopts"] = "keyword",
["hash"] = "keyword",
["history"] = "keyword",
["help"] = "keyword",
["jobs"] = "keyword",
["kill"] = "keyword",
["let"] = "keyword",
["mapfile"] = "keyword",
["printf"] = "keyword",
["read"] = "keyword",
["readarray"] = "keyword",
["pwd"] = "keyword",
["select"] = "keyword",
["set"] = "keyword",
["shift"] = "keyword",
["source"] = "keyword",
["time"] = "keyword",
["type"] = "keyword",
["until"] = "keyword",
["unalias"] = "keyword",
["unset"] = "keyword",
["true"] = "literal",
["false"] = "literal"
}
}

View file

@ -0,0 +1,40 @@
-- mod-version:3
local syntax = require "core.syntax"
syntax.add {
name = "TOML",
files = { "%.toml$" },
comment = '#',
patterns = {
{ pattern = "#.*", type = "comment" },
{ pattern = { '"""', '"""', '\\' }, type = "string" },
{ pattern = { "'''", "'''" }, type = "string" },
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'" }, type = "string" },
{ pattern = "[%w_%.%-]+%s*%f[=]", type = "function" },
{ pattern = {"^%s*%[", "%]"}, type = "keyword" },
{ pattern = "0x[%x_]+", type = "number" },
{ pattern = "0o[0-7_]+", type = "number" },
{ pattern = "0b[01_]+", type = "number" },
{ pattern = "%d[%d_]*%.?[%d_]*[eE][%-+]?[%d_]+", type = "number" },
{ pattern = "%d[%d_]*%.?[$d_]*", type = "number" },
{ pattern = "%f[-+%w_][-+]%f[%w%.]", type = "number" },
{ pattern = "[%+%-:TZ]", type = "operator" },
{ pattern = "%a+", type = "symbol" },
},
symbols = {
["true"] = "literal",
["false"] = "literal",
["nan"] = "number",
["inf"] = "number"
},
}

View file

@ -0,0 +1,154 @@
-- mod-version:3
local syntax = require "core.syntax"
local yaml_bracket_list = {
patterns = {
-- comments
{ pattern = { "#", "\n"}, type = "comment" },
-- strings
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
-- keys
{
pattern = "[%w%d]+%g+()%s*():()%s",
type = { "keyword2", "normal", "operator", "normal" }
},
-- variables
{ pattern = "%$%a%w+", type = "keyword" },
{ pattern = "%$%{%{.-%}%}", type = "keyword" },
-- numeric place holders
{ pattern = "%-?%.inf", type = "number" },
{ pattern = "%.NaN", type = "number" },
-- numbers
{ pattern = "[%+%-]?0%d+", type = "number" },
{ pattern = "[%+%-]?0x%x+", type = "number" },
{ pattern = "[%+%-]?%d+[,%.eE:%+%d]*%d+", type = "number" },
{ pattern = "[%+%-]?%d+", type = "number" },
-- others
{ pattern = ",", type = "operator" },
{ pattern = "%w+", type = "string" },
{
pattern = "[_%(%)%*@~`!%%%^&=%+%-\\;%.><%?/%s]+",
type = "string"
}
},
symbols = {}
}
syntax.add {
name = "YAML",
files = { "%.yml$", "%.yaml$" },
comment = "#",
space_handling = false,
patterns = {
--- rules that start with spaces first and those taking precedence
-- parent and child keys
{
pattern = "^[%w%d]+%g+%s*%f[:]",
type = "literal"
},
{
pattern = "^%s+[%w%d]+%g+%s*%f[:]",
type = "keyword2"
},
-- bracket lists after key declaration
{
pattern = { ":%s+%[", "%]" },
syntax = yaml_bracket_list, type = "operator"
},
{
pattern = { ":%s+{", "}" },
syntax = yaml_bracket_list, type = "operator"
},
-- child key
{
pattern = "^%s+()[%w%d]+%g+()%s*():()%s",
type = { "normal", "keyword2", "normal", "operator", "normal" }
},
-- child list element
{
pattern = "^%s+()%-()%s+()[%w%d]+%g+()%s*():()%s",
type = { "normal", "operator", "normal", "keyword2", "normal", "operator", "normal" }
},
-- unkeyed bracket lists
{
pattern = { "^%s*%[", "%]" },
syntax = yaml_bracket_list, type = "operator"
},
{
pattern = { "^%s*{", "}" },
syntax = yaml_bracket_list, type = "operator"
},
{
pattern = { "^%s*%-%s*%[", "%]" },
syntax = yaml_bracket_list, type = "operator"
},
{
pattern = { "^%s*%-%s*{", "}" },
syntax = yaml_bracket_list, type = "operator"
},
-- rule to optimize space handling
{ pattern = "%s+", type = "normal" },
--- all the other rules
-- comments
{ pattern = { "#", "\n"}, type = "comment" },
-- strings
{ pattern = { '"', '"', '\\' }, type = "string" },
{ pattern = { "'", "'", '\\' }, type = "string" },
-- extra bracket lists rules on explicit type
{
pattern = { "!!%w+%s+%[", "%]"},
syntax = yaml_bracket_list, type = "operator"
},
{
pattern = { "!!%w+%s+{", "}"},
syntax = yaml_bracket_list, type = "operator"
},
-- numeric place holders
{ pattern = "%-?%.inf", type = "number" },
{ pattern = "%.NaN", type = "number" },
-- parent list element
{
pattern = "^%-()%s+()[%w%d]+%g+()%s*():()%s",
type = { "operator", "normal", "keyword2", "normal", "operator", "normal" }
},
-- key label
{
pattern = "%&()%g+",
type = { "keyword", "literal" }
},
-- key elements expansion
{ pattern = "<<", type = "literal" },
{
pattern = "%*()[%w%d_]+",
type = { "keyword", "literal" }
},
-- explicit data types
{ pattern = "!!%g+", type = "keyword" },
-- parent key
{
pattern = "^[%w%d]+%g+()%s*():()%s",
type = { "literal", "normal", "operator", "normal" }
},
-- variables
{ pattern = "%$%a%w+", type = "keyword" },
{ pattern = "%$%{%{.-%}%}", type = "keyword" },
-- numbers
{ pattern = "[%+%-]?0%d+", type = "number" },
{ pattern = "[%+%-]?0x%x+", type = "number" },
{ pattern = "[%+%-]?%d+[,%.eE:%+%d]*%d+", type = "number" },
{ pattern = "[%+%-]?%d+", type = "number" },
-- special operators
{ pattern = "[%*%|%!>%%]", type = "keyword" },
{ pattern = "[%-%$:%?]+", type = "operator" },
-- Everything else as a string
{ pattern = "[%d%a_][%g_]*", type = "string" },
{ pattern = "%p+", type = "string" }
},
symbols = {
["true"] = "number",
["false"] = "number",
["y"] = "number",
["n"] = "number"
}
}

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

View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021-2023 Jefferson González
Copyright (c) 2023-present Lite XL team
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.

View file

@ -0,0 +1,271 @@
# LSP Plugin for Lite XL editor
Plugin that provides intellisense for Lite XL by leveraging the [LSP protocol]
While still a work in progress it already implements all the most important
features to make your life easier while coding with Lite XL. Using it
requires __Lite XL v2.1+__ (for __Lite XL v2.0.1__ to __Lite XL v2.0.5__
check out the __0.1__ branch). [lint+] is optionally used to render
diagnostic messages while you type so make sure to get it. Also, the
[snippets] plugin is used to properly process the received autocompletions
in the form of snippets, so grab that too.
To use, clone this project into the __lsp__ directory in your plugins
folder. Finally you will need the [Widgets] lib so make sure to also drop
it into your lite-xl configs directory. For example:
```sh
cd ~/.config/lite-xl/
git clone https://github.com/lite-xl/lite-xl-lsp plugins/lsp
git clone https://github.com/lite-xl/lite-xl-widgets libraries/widget
git clone https://github.com/liquidev/lintplus plugins/lintplus
wget https://raw.githubusercontent.com/vqns/lite-xl-snippets/main/snippets.lua \
-O plugins/snippets.lua
wget https://raw.githubusercontent.com/vqns/lite-xl-snippets/main/lsp_snippets.lua \
-O plugins/lsp_snippets.lua
```
The lite-xl configs directory should have:
* ~/.config/lite-xl/libraries/widget/
* ~/.config/lite-xl/plugins/lsp/
* ~/.config/lite-xl/plugins/lintplus/
* ~/.config/lite-xl/plugins/snippets.lua
* ~/.config/lite-xl/plugins/lsp_snippets.lua
## Features
Stuff that is currently implemented:
* Code auto completion (**ctrl+space**)
* Function signatures tooltip (**ctrl+shift+space**)
* Current cursor symbol details tooltip on mouse hover or shortcut (**alt+a**)
* Goto definition (**alt+d**)
* Goto implementation (**alt+shift+d**)
* View/jump to current document symbols (**alt+s**)
* Find workspace symbols (**alt+shift+s**)
* View/jump to symbol references (**alt+f**)
* View/jump to document diagnostic messages (**alt+e**)
* Document format (**alt+shift+f**)
* Optional diagnostics rendering while typing with [lint+]
(**alt+shift+e** to toggle)
* List all documents with diagnostics (**ctrl+alt+e**)
* Snippets processing using the [snippets] plugin
## Setting a LSP Server
The easiest method of setting up a lsp server is by using the __[config.lua]__
file shipped with the lsp plugin which already contains a list of predefined
servers (notice: not all of them have been tested to work). Require this file
on your users **init.lua**, call `setup()` on the desired lsp servers or
overwrite the configuration options of the defined lsp servers if needed
as shown below:
__Examples:__
```lua
local lspconfig = require "plugins.lsp.config"
-- Activate clangd without overwriting any settings for c/c++
-- autocompletion (requires a compile_commands.json file on
-- your project directory usually generated by build tools
-- like cmake or meson)
-- See: https://clangd.llvm.org/installation.html#project-setup
lspconfig.clangd.setup()
-- Activate gopls
lspconfig.gopls.setup()
-- Activate the lua-language-server, set the server command and
-- modify the default settings in order to disable diagnostics.
lspconfig.sumneko_lua.setup {
command = {
"/path/to/lua-language-server/bin/Linux/lua-language-server",
"-E",
"/path/to/lua-language-server/main.lua"
},
settings = {
Lua = {
diagnostics = {
enable = false
}
}
}
}
-- Activate intelephense and pass additional initializationOptions
-- like the license key and storage path.
lspconfig.intelephense.setup {
init_options = {
licenceKey = "MYLICENSEKEY",
storagePath = "/home/myuser/.cache/intelephense"
}
}
```
If your preferred LSP server is not listed on the config.lua file feel free
to submit a __pull request__ with the addition!
## Manually Configuring a LSP Server
Besides the __[config.lua]__ method, you can fully define an lsp server in
your user init.lua file. You would need to require the lsp plugin and use the
**add_server** function as shown on the following example:
```lua
local lsp = require "plugins.lsp"
lsp.add_server {
-- Name of server
name = "intelephense",
-- Main language
language = "PHP",
-- If the server supports multiple languages:
-- language = {
-- { id = "javascript", pattern = "%.js$" },
-- { id = "typescript", pattern = "%.ts$" },
-- }
-- If no pattern matches, the file extension is used instead.
-- File types that are supported by this server
file_patterns = { "%.php$" },
-- LSP command and optional arguments
command = { "intelephense", "--stdio" },
-- Optional table of settings to pass into the lsp
-- Note that also having a settings.json or settings.lua in
-- your workspace directory with a table of settings is supported.
settings = {
intelephense = {
files = {
exclude = {
"**/.git/**"
}
}
}
},
-- Optional table of initializationOptions for the LSP
init_options = {
storagePath = "/home/myuser/.cache/intelephense"
},
-- Set by default to 16 should only be modified if having issues with a server
requests_per_second = 16,
-- Some servers like bash language server support incremental changes
-- which are more performant but don't advertise it, set to true to force
-- incremental changes even if server doesn't advertise them.
incremental_changes = false,
-- True to debug the lsp client when developing it
verbose = false
}
```
## LSP Plugin Settings
Configuration options that can be used to control the plugin behaviour:
```lua
---Show a symbol hover information when mouse cursor is on top.
---@type boolean
config.plugins.lsp.mouse_hover = true
---The amount of time in milliseconds before showing the tooltip.
---@type integer
config.plugins.lsp.mouse_hover_delay = 300
---Show diagnostic messages
---@type boolean
config.plugins.lsp.show_diagnostics = true
---Stop servers that aren't needed by any of the open files
---@type boolean
config.plugins.lsp.stop_unneeded_servers = true
---Set to a file path to log all json
---@type string
config.plugins.lsp.log_file = ""
---Setting to true prettyfies json for more readability on the log
---but this setting will impact performance so only enable it when
---in need of easy to read json output when developing the plugin.
---@type boolean
config.plugins.lsp.prettify_json = false
---Send a server stderr output to lite log
---@type boolean
config.plugins.lsp.log_server_stderr = false
---Force verbosity off even if a server is configured with verbosity on
---@type boolean
config.plugins.lsp.force_verbosity_off = false
---Yield when reading from LSP which may give you better UI responsiveness
---when receiving large responses, but will affect LSP performance.
---@type boolean
config.plugins.lsp.more_yielding = false
```
## TODO
- [ ] Properly handle multiple opened project directories
- [ ] Handle window/showMessage, window/showMessageRequest,
$/progress, telemetry/event
- [x] Be able to search workspace symbols 'workspace/symbol'
- [ ] Completion preselectSupport (needs autocomplete plugin change)
- [ ] Add symbol renaming support 'textDocument/rename'
- [x] Add Snippets support (this will need a whole standalone [snippets] plugin).
- [x] Fix issues when parsing stdout from some lsp servers (really fixed?).
- [x] More improvements to autocomplete.lua plugin
- [x] Detect view edges and render to the most visible side
- [x] Description box, detect view width and expand accordingly
- [ ] Support for pre-selected item
- [ ] Be able to use a custom sorting field.
- [x] Add hover support for function arguments
- [x] Add custom tooltip that accents active parameter and signature
- [x] Figure out how to get an autocompletion item full documentation with
'completionItem/resolve' or any other in order to better populate
the new autocomplete item description
- [x] (we kill it) Detect if lsp server hangs and restart it (eg: clangd)
- [x] Exit LSP server if no open document needs it.
- [x] Add hover support for symbols
- [x] Generate list of current document symbols for easy document navigation
- [x] Goto definition
- [x] Display select box when more than one result
- [x] Show diagnostics on active document similar to the linter plugin.
- [x] Send incremental changes on textDocument/didChange notification
since sending the whole document content on big files is slow and bad.
## Screenshots
Some images to easily visualize the progress :)
### Completion
![Completion](screenshots/completion01.png)
![Completion](screenshots/completion02.png)
![Completion](screenshots/completion03.png)
![Completion](screenshots/completion04.png)
### Symbol hover
![Hover](screenshots/hover01.png)
![Hover](screenshots/hover02.png)
### Function signatures
![Signature](screenshots/signatures01.png)
### Document symbols
![Doc Symbols](screenshots/docsym01.png)
![Doc Symbols](screenshots/docsym02.png)
### Goto definition
![Goto Definition](screenshots/gotodef01.png)
### Diagnostics rendering using Lint+
![Diagnostics](screenshots/diagnostics01.png)
[LSP protocol]: https://microsoft.github.io/language-server-protocol/specifications/specification-current/
[lint+]: https://github.com/liquidev/lintplus
[snippets]: https://github.com/vqns/lite-xl-snippets
[Widgets]: https://github.com/lite-xl/lite-xl-widgets
[config.lua]: config.lua

View file

@ -0,0 +1,956 @@
--
-- A list of servers.
--
-- Can be used by doing a 'local lspconfig = require "plugins.lsp.config"'
-- on your user init.lua, for more details check the README.md
--
-- Servers taken from:
-- https://github.com/prabirshrestha/vim-lsp/wiki/Servers
-- https://github.com/mattn/vim-lsp-settings/tree/master/settings
--
local lsp = require "plugins.lsp"
local util = require "plugins.lsp.util"
local config = require "core.config"
local snippets = pcall(require, "plugins.snippets") and config.plugins.lsp.snippets
---Options that can be passed to a LSP server to overwrite the defaults.
---@class lsp.config.options
---
---Name of server.
---@field name string
---Main language, eg: C.
---Can be a string or a table.
---If the table is empty, the file extension will be used instead.
---The table should be an array of tables containing `id` and `pattern`.
---The `pattern` will be matched with the file path.
---Will use the `id` of the first `pattern` that matches.
---If no pattern matches, the file extension will be used instead.
---@field language string | lsp.server.languagematch[]
---File types that are supported by this server.
---@field file_patterns string[]
---LSP command and optional arguments.
---@field command table<integer,string|table>
---On Windows, avoid running the LSP server with cmd.exe.
---@field windows_skip_cmd? boolean
---Enviroment variables to set for the server command.
---@field env? table<string, string>
---Seconds before closing the server when not needed anymore.
---@field quit_timeout? number
---Optional table of settings to pass into the LSP.
---Note that also having a settings.json or settings.lua in
---your workspace directory with a table of settings is supported.
---@field settings? table<string,any>
---Optional table of initializationOptions for the LSP.
---@field init_options? table<string,any>
---Optional table of capabilities that will be merged with our default one.
---@field custom_capabilities? table<string,any>
---Function called when the server has been started.
---@field on_start? fun(server: lsp.server)
---Set by default to 16 should only be modified if having issues with a server.
---@field requests_per_second? integer
---Some servers like bash language server support incremental changes
---which are more performant but don't advertise it, set to true to force
---incremental changes even if server doesn't advertise them.
---@field incremental_changes? boolean
---Set to true to debug the lsp client when developing it.
---@field verbose? boolean
---@class lsp.config.server
---Register the lsp server for usage.
---@field setup fun(options?:lsp.config.options)
---Get the default lsp server options.
---@field get_options fun():lsp.config.options
---Helper to register a language server.
---@param options lsp.config.options
---@return lsp.config.server
local function add_lsp(options)
return {
setup = function(user_options)
local merged_options = util.deep_merge(options, user_options)
lsp.add_server(merged_options)
end,
get_options = function()
return options
end
}
end
---List of predefined language servers that can be easily enabled at runtime.
---@class lsp.config
local lspconfig = {}
---# bash-language-server
--- __Status__: Works
--- __Site__: https://github.com/bash-lsp/bash-language-server
--- __Installation__: `npm i -g bash-language-server`
--- __Note__: also install `shellcheck` for linting
lspconfig.bashls = add_lsp {
name = "bash-language-server",
language = "shellscript",
file_patterns = { "%.sh$" },
command = { "bash-language-server", "start" },
incremental_changes = true,
verbose = false
}
---# ccls
--- __Status__: Works
--- __Site__: https://github.com/MaskRay/ccls/
--- __Installation__: https://github.com/MaskRay/ccls/wiki
lspconfig.ccls = add_lsp {
name = "ccls",
language = {
{ id = "c", pattern = "%.[ch]$" },
{ id = "cpp", pattern = "%.[ch]pp$" },
{ id = "cpp", pattern = "%.[CH]$" },
{ id = "cpp", pattern = "%.[ch]%+%+$" },
},
file_patterns = {
"%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$",
"%.cc$", "%.C$", "%.cxx$", "%.c++$", "%.hh$",
"%.H$", "%.hxx$", "%.h++$", "%.objc$", "%.objcpp$"
},
command = { "ccls" },
verbose = false
}
---# clangd
--- __Status__: Works
--- __Site__: https://clangd.llvm.org/
--- __Installation__: install the clang software package on your system
--- __Note__: See https://clangd.llvm.org/installation.html#project-setup
lspconfig.clangd = add_lsp {
name = "clangd",
language = {
{ id = "c", pattern = "%.[ch]$" },
{ id = "cpp", pattern = "%.[ch]pp$" },
{ id = "cpp", pattern = "%.[CH]$" },
{ id = "cpp", pattern = "%.[ch]%+%+$" },
},
file_patterns = {
"%.c$", "%.h$", "%.inl$", "%.cpp$", "%.hpp$",
"%.cc$", "%.C$", "%.cxx$", "%.c++$", "%.hh$",
"%.H$", "%.hxx$", "%.h++$", "%.objc$", "%.objcpp$"
},
command = { "clangd", "-background-index" },
verbose = false
}
---# Clojure
--- __Status__: Untested
--- __Site__: https://clojure-lsp.github.io/
--- __Installation__: https://clojure-lsp.github.io/clojure-lsp/installation/
lspconfig.clojure_lsp = add_lsp {
name = "clojure-lsp",
language = "clojure",
file_patterns = { "%.clj$", "%.cljs$", "%.clc$", "%.edn$" },
command = { "clojure-lsp" },
verbose = false
}
---# Crystal
--- __Status__: Works
--- __Site__: https://github.com/elbywan/crystalline
--- __Installation__: 'paru -S crystalline-bin'
lspconfig.crystalline = add_lsp {
name = "crystalline",
language = "crystal",
file_patterns = { "%.cr$" },
command = { "crystalline", "--stdio" },
verbose = false
}
---# vscode-css-languageserver
--- __Status__: Works
--- __Site__: https://github.com/vscode-langservers/vscode-css-languageserver-bin
--- __Installation__: `npm install -g vscode-css-languageserver-bin`
--- or `pacman -S vscode-css-languageserver`
lspconfig.cssls = add_lsp {
name = "css-languageserver",
language = "css",
file_patterns = { "%.css$", "%.less$", "%.sass$" },
command = {
{
'vscode-css-languageserver',
'vscode-css-language-server',
'css-languageserver'
},
'--stdio'
},
fake_snippets = true,
verbose = false
}
---# D
--- __Status__: Works
--- __Site__: https://github.com/Pure-D/serve-d
--- __Installation__: https://github.com/Pure-D/serve-d?tab=readme-ov-file#installation
lspconfig.serve_d = add_lsp {
name = "serve_d",
language = "d",
file_patterns = { "%.di?$" },
command = { "serve-d" },
verbose = false
}
---# dartls
--- __Status__: Untested
--- __Site__: https://github.com/dart-lang/sdk
--- __Installation__: Provided in dart sdk
lspconfig.dartls = add_lsp {
name = "dart",
language = "dart",
file_patterns = { "%.dart$" },
command = { "dart", "language-server", "--protocol=lsp" },
verbose = false
}
---# Deno
--- __Status__: Works
--- __Site__: https://deno.land/manual/advanced/language_server
--- __Installation__: Provided in Deno runtime
lspconfig.deno = add_lsp {
name = "deno",
language = {
{ id = "javascript", pattern = "%.js$" },
{ id = "javascriptreact", pattern = "%.jsx$" },
{ id = "typescript", pattern = "%.ts$" },
{ id = "typescriptreact", pattern = "%.tsx$" },
},
file_patterns = { "%.[tj]s$", "%.[tj]sx$" },
command = { 'deno', 'lsp' },
verbose = false,
settings = {
deno = {
enable = true,
unstable = true,
config = "./deno.json",
importMap = "./import_map.json",
lint = true,
codeLens = {
implementations = true,
references = true,
test = true,
referencesAllFunctions = true
},
suggest = {
names = true,
paths = true,
completeFunctionCalls = true,
imports = {
autoDiscover = true,
hosts = {
["https://deno.land/"] = true,
["https://nest.land/"] = true,
["https://crux.land/"] = true
}
},
autoImports = true
}
}
}
}
---# Dockerfile
--- __Status__: Untested
--- __Site__: https://github.com/rcjsuen/dockerfile-language-server-nodejs
--- __Installation__: `npm install -g dockerfile-language-server-nodejs`
lspconfig.dockerls = add_lsp {
name = "docker-langserver",
language = "dockerfile",
file_patterns = { "Dockerfile$" },
command = { "docker-langserver", "--stdio" },
verbose = false
}
---# Elixir
--- __Status__: Works
--- __Site__: https://github.com/elixir-lsp/elixir-ls
--- __Installation__: 'paru -S elixir-ls'
lspconfig.elixirls = add_lsp {
name = "elixirls",
language = "elixir",
file_patterns = { "%.ex$", "%.exs$" },
command = { "elixir-ls" },
verbose = false
}
---# Elm
--- __Status__: Untested
--- __Site__: https://github.com/elm-tooling/elm-language-server
--- __Installation__: `paru -S elm-language-server`
lspconfig.elmls = add_lsp {
name = "elmls",
language = "elm",
file_patterns = { "%.elm$" },
command = { "elm-language-server" },
verbose = false
}
---# Erlang
--- __Status__: Untested
--- __Site__: https://github.com/erlang-ls/erlang_ls
--- __Installation__: ?
lspconfig.erlangls = add_lsp {
name = "erlangls",
language = "erlang",
file_patterns = { "%.erl$", "%.hrl$" },
command = { 'Erlang', 'LS', '-t', 'stdio' },
verbose = false
}
---# fennel-ls
--- __Status__: Untested
--- __Site__: https://git.sr.ht/~xerool/fennel-ls
--- __Installation__:
--- ```sh
--- git clone https://git.sr.ht/~xerool/fennel-ls
--- make -C fennel-ls
--- sudo make -C fennel-ls install
--- ```
lspconfig.fennells = add_lsp {
name = "fennel-ls",
language = "fennel",
file_patterns = { "%.fnl$" },
command = { "fennel-ls" },
verbose = false
}
---# Flow - JavaScript
--- __Status__: Untested
--- __Site__: https://flow.org/
--- __Installation__: `npm install -g flow-bin`
lspconfig.flow = add_lsp {
name = "flow",
language = "javascript",
file_patterns = { "%.js$", "%.jsx$" },
command = { "flow", "lsp" },
verbose = false
}
---# Fortran - fortls
--- __Status__: Works
--- __Site__: https://fortls.fortran-lang.org/index.html
--- __Installation__: `paru -S fortls`
lspconfig.fortls = add_lsp {
name = "fortls",
language = "fortran",
file_patterns = { "%.f$", "%.f90$", "%.f95$", "%.F$" },
command = { "fortls", "--notify_init" },
verbose = false
}
---# Gleam
--- __Status__: Works (the gleam lsp itself acts kinda weird)
--- __Site__: https://gleam.run/
--- __Installation__: Included with the gleam compiler binary
lspconfig.gleam = add_lsp {
name = "gleam",
language = "gleam",
file_patterns = { "%.gleam$" },
command = { "gleam", "lsp" },
verbose = false
}
---# gopls
--- __Status__: Works
--- __Site__: https://pkg.go.dev/golang.org/x/tools/gopls
--- __Installation__: `go get -u golang.org/x/tools/gopls`
lspconfig.gopls = add_lsp {
name = "gopls",
language = "go",
file_patterns = { "%.go$" },
command = { "gopls" },
verbose = false
}
---# groovy-language-server
--- __Status__: Untested
--- __Site__: https://github.com/prominic/groovy-language-server
--- __Installation__:
--- ```sh
--- mkdir ~/lsp
--- cd ~/lsp
--- git clone https://github.com/prominic/groovy-language-server.git
--- cd ~/lsp/groovy-language-server
--- ./gradlew build
--- ```
lspconfig.groovyls = add_lsp {
name = "groovy-language-server",
language = "groovy",
file_patterns = { "%.groovy$", "%.gvy$", "%.gy$", "%.gsh$" },
-- command = { "java", "-jar", "/path/to/groovy-language-server-all.jar" },
command = { "groovy-language-server" },
verbose = false
}
---# haskell-language-server
--- __Status__: Untested
--- __Site__: https://github.com/haskell/haskell-language-server
--- __Installation__: `ghcup install hls`
--- or https://github.com/haskell/haskell-language-server#installation
lspconfig.hls = add_lsp {
name = "haskell-language-server",
language = "haskell",
file_patterns = { "%.hs$", "%.lhs$" },
command = { 'haskell-language-server-wrapper', '--lsp' },
verbose = false
}
---# vscode-html-languageserver
--- __Status__: Works
--- __Site__: https://github.com/vscode-langservers/vscode-html-languageserver-bin
--- __Installation__: `npm install --global vscode-html-languageserver-bin`
--- or `pacman -S vscode-html-languageserver`
lspconfig.html = add_lsp {
name = "html-languageserver",
language = "html",
file_patterns = { "%.html$" },
command = {
{
'vscode-html-languageserver',
'vscode-html-language-server',
'html-languageserver'
},
'--stdio'
},
verbose = false
}
---# intelephense
--- __Status__: Works
--- __Site__: https://github.com/bmewburn/intelephense-docs
--- __Installation__: `npm -g install intelephense`
--- __Note__: Set your license and storage by passing the init_options as follows:
--- ```lua
--- init_options = { licenceKey = "...", storagePath = "/some/path"}
--- ```
lspconfig.intelephense = add_lsp {
name = "intelephense",
language = "php",
file_patterns = { "%.php$" },
command = { "intelephense", "--stdio" },
verbose = false
}
---# java
--- __Status__: Works
--- __Site__: https://github.com/eclipse/eclipse.jdt.ls
lspconfig.jdtls = add_lsp {
name = "jdtls",
language = "java",
file_patterns = { "%.java$" },
command = { "jdtls" },
verbose = false
}
---# Scala
--- __Status__: Works
--- __Site__: https://scalameta.org/metals/
--- __Installation__: `paru -S metals`
lspconfig.metals = add_lsp {
name = "metals",
language = "scala",
file_patterns = { "%.scala$" },
command = { "metals" },
verbose = false
}
---# vscode-json-languageserver
--- __Status__: Works
--- __Site__: https://www.npmjs.com/package/vscode-json-languageserver
--- __Installation__: `npm install -g vscode-json-languageserver`
--- or `pacman -S vscode-json-languageserver`
lspconfig.jsonls = add_lsp {
name = "json-languageserver",
language = "json",
file_patterns = { "%.json$", "%.jsonc$" },
command = {
{
'vscode-json-languageserver',
'vscode-json-language-server',
'json-languageserver',
},
'--stdio'
},
verbose = false
}
---# kotlin-language-server
--- __Status__: Untested
--- __Site__: https://github.com/fwcd/kotlin-language-server
--- __Installation__: https://github.com/fwcd/kotlin-language-server/releases
lspconfig.kotlin_language_server = add_lsp {
name = "kotlin-language-server",
language = "kotlin",
file_patterns = { "%.kt$", "%.kts$", "%.ktm$" },
command = { 'kotlin-language-server' },
verbose = false
}
---# XML
--- __Status__: Works
--- __Site__: https://github.com/eclipse/lemminx
--- __Installation__: 'paru -S lemminx'
lspconfig.lemminx = add_lsp {
name = "lemminx",
language = "xml",
file_patterns = { "%.xml$" },
command = { "lemminx" },
verbose = false
}
---# nil
--- __Status__: Works
--- __Site__: https://github.com/oxalica/nil
--- __Installation__: cargo install --git https://github.com/oxalica/nil nil
--- __Note__: nix >= 2.4 needs to be installed
lspconfig.nillsp = add_lsp {
name = "nil",
language = "nix",
file_patterns = { "%.nix$" },
command = { "nil" },
verbose = false
}
---# nimlsp
--- __Status__: Works
--- __Site__: https://github.com/PMunch/nimlsp
--- __Installation__: `nimble install nimlsp`
lspconfig.nimlsp = add_lsp {
name = "nimlsp",
language = "nim",
file_patterns = { "%.nim$" },
command = { "nimlsp" },
requests_per_second = 25,
incremental_changes = false,
verbose = false
}
---# ocaml-lsp
--- __Status__: Reported working on https://github.com/jgmdev/lite-xl-lsp/issues/17
--- __Site__: https://github.com/ocaml/ocaml-lsp
--- __Installation__: https://github.com/ocaml/ocaml-lsp#installation
lspconfig.ocaml_lsp = add_lsp {
name = "ocaml-lsp",
language = "ocaml",
file_patterns = { "%.ml$", "%.mli$" },
command = { "ocamllsp" },
verbose = false
}
---# Odin
--- __Status__: Works
--- __Site__: https://github.com/DanielGavin/ols
--- __Installation__: `paru -S odinls`
lspconfig.odinls = add_lsp {
name = "odinls",
language = "odin",
file_patterns = { "%.odin$" },
command = { "ols" },
verbose = false
}
---# omnisharp
--- __Status__: Works but, freeze on large projects (https://github.com/ppy/osu.git)
--- __Site__: https://github.com/OmniSharp/omnisharp-roslyn
--- __Installation__: See official website for instructions
lspconfig.omnisharp = add_lsp {
name = "omnisharp",
language = "csharp",
file_patterns = { "%.cs$" },
command = { "omnisharp", "-lsp" },
verbose = false
}
---# PerlNavigator - Perl
--- __Status__: Works
--- __Site__: https://github.com/bscan/PerlNavigator
--- __Installation__: `paru -S perlnavigator`
lspconfig.perlnavigator = add_lsp {
name = "perlnavigator",
language = "perl",
file_patterns = { "%.pl$", "%.pm$" },
command = { "perlnavigator" },
settings = {
perlnavigator = {
-- The following setting is only needed if you want to set a custom perl path. It already defaults to "perl"
perlPath = "perl"
}
}
}
---# python-language-server
--- __Status__: Works (deprecated in favor of python-lsp-server)
--- __Site__: https://github.com/palantir/python-language-server
--- __Installation__: `pip install python-language-server`
--- __Note__: Also don't forget to install any additional optional dependencies
--- for additional features (see official site for details).
lspconfig.pyls = add_lsp {
name = "pyls",
language = "python",
file_patterns = { "%.py$" },
command = { 'pyls' },
verbose = false
}
---# python-lsp-server
--- __Status__: Works
--- __Site__: https://github.com/python-lsp/python-lsp-server
--- __Installation__: `pip install python-lsp-server`
--- __Note__: Also don't forget to install any additional optional dependencies
--- for additional features (see official site for details).
lspconfig.pylsp = add_lsp {
name = "pylsp",
language = "python",
file_patterns = { "%.py$" },
command = { 'pylsp' },
verbose = false
}
--# pyright
--- __Status__: Works
--- __Site__: https://github.com/microsoft/pyright
--- __Installation__: `pip install pyright` or `npm install -g pyright`
lspconfig.pyright = add_lsp {
name = "pyright",
language = "python",
file_patterns = { "%.py$" },
command = { "pyright-langserver", "--stdio" },
verbose = false
}
---# quick-lint-js
--- __Status__: Works
--- __Site__: https://github.com/quick-lint/quick-lint-js
--- __Installation__: Arch Linux: `yay -Syu quick-lint-js`
lspconfig.quicklintjs = add_lsp {
name = "quick-lint-js",
language = {
{ id = "javascriptreact", pattern = "%.jsx$" },
{ id = "javascript", pattern = "%.js$" },
{ id = "typescriptdefinition", pattern = "%.d%.ts$" },
{ id = "typescriptsource", pattern = "%.ts$" },
{ id = "typescriptreact", pattern = "%.tsx$" },
{ id = "typescript", pattern = ".*" },
},
file_patterns = { "%.[mc]?jsx?$", "%.tsx?$" },
command = { "quick-lint-js", "--lsp-server" },
verbose = false
}
---# R
-- __Status__: Works
-- __Site__:https://github.com/REditorSupport/languageserver#installation
-- __Installation__: `paru -S r-languageserver`
lspconfig.rlanguageserver = add_lsp {
name = "rlanguageserver",
language = "r",
file_patterns = { "%.r$", "%.R$" },
command = {'R', '--slave', '-e', 'languageserver::run()'},
verbose = false
}
---# Rust Language Server
--- __Status__: Works
--- __Site__: https://github.com/rust-lang/rls
--- __Installation__: Install rust on your system
lspconfig.rls = add_lsp {
name = "rust-language-server",
language = "rust",
file_patterns = { "%.rs$" },
command = { 'rls' },
verbose = false
}
---# Ruby LSP
--- __Status__: Untested
--- __Site__: https://github.com/Shopify/ruby-lsp
--- __Instalation__: gem install ruby-lsp
--- __Note__: Also don't forget to install any additional optional dependecies
--- for additional features (see official site for details).
lspconfig.ruby_lsp = add_lsp {
name = "ruby-lsp",
language = "ruby",
file_patterns = { "%.rb$" },
command = { 'ruby-lsp' },
-- Override command to one below if You want to use it with bundler
-- command = { 'bundle', 'exec', 'ruby-lsp'},
incremental_changes = true,
init_options = {
enabledFeatures = {
"codeActions",
"diagnostics",
-- semanticHighlighting should be use only when running with bundle at the moment
--"semanticHighlighting",
"documentHighlights",
"documentLink",
"documentSymbols",
"foldingRanges",
"formatting",
"hover",
"inlayHint",
"onTypeFormatting",
"selectionRanges",
"completion"
},
-- enableExperimentalFeatures = true,
-- rubyVersionManager = "",
},
verbose = false
}
---# Rust Analyzer
--- __Status__: Works
--- __Site__: https://rust-analyzer.github.io/
--- __Installation__: See official website for instructions
lspconfig.rust_analyzer = add_lsp {
name = "rust-analyzer",
language = "rust",
file_patterns = { "%.rs$" },
command = { 'rust-analyzer' },
verbose = false
}
---# Solargraph
--- __Status__: Untested
--- __Site__: https://github.com/castwide/solargraph
--- __Installation__: `gem install solargraph`
lspconfig.solargraph = add_lsp {
name = "solargraph",
language = "ruby",
file_patterns = { "%.rb$" },
command = { 'solargraph', 'stdio' },
verbose = false
}
---# sql-language-server
--- __Status__: Works
--- __Site__: https://github.com/joe-re/sql-language-server
--- __Installation__: `npm i -g sql-language-server`
lspconfig.sqlls = add_lsp {
name = "sql-language-server",
language = "sql",
file_patterns = { "%.sql$" },
command = { 'sql-language-server', 'up', '--method', 'stdio' },
verbose = false
}
---# lua-language-server
--- __Status__: Works
--- __Site__: https://github.com/sumneko/lua-language-server
--- __Installation__: https://github.com/sumneko/lua-language-server/wiki/Build-and-Run-(Standalone)
lspconfig.sumneko_lua = add_lsp {
name = "lua-language-server",
language = "lua",
file_patterns = { "%.lua$" },
command = { 'lua-language-server' },
verbose = false,
settings = {
Lua = {
completion = {
enable = true,
callSnippet = snippets and "Replace" or "Disable",
keywordSnippet = snippets and "Replace" or "Disable"
},
develop = {
enable = false,
debuggerPort = 11412,
debuggerWait = false
},
diagnostics = {
enable = true,
},
hover = {
enable = true,
viewNumber = true,
viewString = true,
viewStringMax = 1000
},
runtime = {
version = 'Lua 5.4',
path = {
"?.lua",
"?/init.lua",
"?/?.lua",
"/usr/share/5.4/?.lua",
"/usr/share/lua/5.4/?/init.lua"
}
},
signatureHelp = {
enable = true
},
workspace = {
library = {
DATADIR,
USERDIR
},
maxPreload = 2000,
preloadFileSize = 1000
},
telemetry = {
enable = false
}
}
}
}
---# svelte-language-server
--- __Status__: Works
--- __Site__: https://github.com/sveltejs/language-tools/tree/master/packages/language-server
--- __Installation__: `npm install -g svelte-language-server`
--- __Note__: Also don't forget to install any additional optional dependencies
--- for additional features (see official site for details).
lspconfig.sveltels = add_lsp {
name = "sveltels",
language = "svelte",
file_patterns = { "%.svelte$" },
command = { 'svelteserver', '--stdio' },
verbose = false
}
---# Tailwind CSS
--- __Status__: Broken (freezes when writing class names inside html doc, requires new implementation of json.lua)
--- __Site__: https://github.com/tailwindlabs/tailwindcss-intellisense
--- __Installation__: Arch Linux: `sudo pacman -S tailwindcss-language-server`
lspconfig.tailwindcss = add_lsp {
name = "tailwindcss",
language = "html",
file_patterns = { "%.html$"},
command = {'tailwindcss-language-server', '--stdio'},
fake_snippets = true,
verbose = false
}
---# LaTeX Texlab language server
--- __Status__: Works
--- __Site__: https://github.com/latex-lsp/texlab
--- __Installation__: git clone https://github.com/latex-lsp/texlab.git , then inside the texlab folder, run: cargo build --release
--- __Note__: Rust has to be installed
lspconfig.texlab = add_lsp {
name = "texlab",
language = "latex",
file_patterns = { "%.tex$", "%.bib$" , "%.dtx$", "%.sty$", "%.ins$", "%.cls$" },
command = { 'texlab' }
}
---# TOML - Taplo
--- __Status__: Works
--- __Site__: https://github.com/tamasfe/taplo
--- __Installation__: 'sudo pacman -S taplo-cli'
lspconfig.taplo = add_lsp {
name = "taplo",
language = "toml",
file_patterns = { "%.toml$" },
command = { "taplo", "lsp", "stdio" },
verbose = false
}
---# typescript-language-server
--- __Status__: Works
--- __Site__: https://github.com/typescript-language-server/typescript-language-server
--- __Installation__: `npm install -g typescript-language-server typescript`
lspconfig.tsserver = add_lsp {
name = "typescript-language-server",
language = {
{ id = "javascript", pattern = "%.[cm]?js$" },
{ id = "javascriptreact", pattern = "%.jsx$" },
{ id = "typescript", pattern = "%.ts$" },
{ id = "typescriptreact", pattern = "%.tsx$" },
},
file_patterns = { "%.jsx?$", "%.[cm]js$", "%.tsx?$" },
command = { 'typescript-language-server', '--stdio' },
verbose = false
}
---# typst-lsp
--- __Status: Works
--- __Site__: https://github.com/nvarner/typst-lsp
--- __Instalation__: `yay typst-lsp-bin`
lspconfig.typst_lsp = add_lsp {
name = "typst-lsp",
language = "typst",
file_patterns = { "%.typ$" },
command = { 'typst-lsp' },
verbose = false,
settings = {
exportPdf = "never", -- Choose onType, onSave or never.
experimentalFormatterMode = "on" -- Choose on, or off
}
}
---# vim-language-server
--- __Status__: Untested
--- __Site__: https://github.com/iamcco/vim-language-server
--- __Installation__: `npm install -g vim-language-server`
lspconfig.vimls = add_lsp {
name = "vim-language-server",
language = "vim",
file_patterns = { "%.vim$" },
command = { 'vim-language-server', '--stdio' },
verbose = false
}
---# V
--- __Status__: Works
--- __Site__: https://github.com/vlang/v-analyzer
--- __Installation__: https://github.com/vlang/v-analyzer?tab=readme-ov-file#installation
lspconfig.v_analyzer = add_lsp {
name = "v_analyzer",
language = "v",
file_patterns = { "%.vv?$", "%.vsh$" },
command = { "v-analyzer", "--stdio" },
verbose = false
}
---# Vala - vala-language-server
--- __Status__: Works
--- __Site__: https://github.com/vala-lang/vala-language-server
--- __Installation__: `paru -S vala-language-server`
lspconfig.vala_ls = add_lsp {
name = "vala_ls",
language = "vala",
file_patterns = { "%.vala$" },
command = { "vala-language-server" },
verbose = false
}
---# vlang-vls
--- __Status__: doesn't respond to completion requests (no longer officially maintained in favor of v-analyzer)
--- __Site__: https://github.com/vlang/vls
--- __Installation__: https://github.com/vlang/vls?tab=readme-ov-file#installation
lspconfig.vls = add_lsp {
name = "vlang-vls",
language = "v",
file_patterns = { "%.vv?$", "%.vsh$" },
command = { 'vlang-vls' },
verbose = false
}
---# yaml-language-server
--- __Status__: Untested
--- __Site__: https://github.com/redhat-developer/yaml-language-server
--- __Installation__: See official website for instructions
lspconfig.yamlls = add_lsp {
name = "yaml-language-server",
language = "yaml",
file_patterns = { "%.yml$", "%.yaml$" },
command = { 'yaml-language-server', '--stdio' },
verbose = false
}
---# Zig Language Server
--- __Status__: Untested
--- __Site__: https://github.com/zigtools/zls
--- __Installation__: See official website for instructions
lspconfig.zls = add_lsp {
name = "zls",
language = "zig",
file_patterns = { "%.zig$" },
command = { 'zls' },
verbose = false
}
return lspconfig

View file

@ -0,0 +1,305 @@
-- Store diagnostic messages received by an LSP.
-- @copyright Jefferson Gonzalez
-- @license MIT
local core = require "core"
local config = require "core.config"
local util = require "plugins.lsp.util"
local Timer = require "plugins.lsp.timer"
---@class lsp.diagnostics
local diagnostics = {}
---@class lsp.diagnostics.position
---@field line integer
---@field character integer
---@class lsp.diagnostics.range
---@field start lsp.diagnostics.position
---@field end lsp.diagnostics.position
---@class lsp.diagnostics.severity
---@field ERROR integer
---@field WARNING integer
---@field INFO integer
---@field HINT integer
diagnostics.severity = {
ERROR = 1,
WARNING = 2,
INFO = 3,
HINT = 4
}
---@alias lsp.diagnostics.severity_code
---|>`diagnostics.severity.ERROR`
---| `diagnostics.severity.WARNING`
---| `diagnostics.severity.INFO`
---| `diagnostics.severity.HINT`
---@class lsp.diagnostics.code_description
---@field href string
---@class lsp.diagnostics.tag
---@field UNNECESSARY integer
---@field DEPRECATED integer
diagnostics.tag = {
UNNECESSARY = 1,
DEPRECATED = 2
}
---@alias lsp.diagnostics.tag_code
---|>`diagnostics.tag.UNNECESSARY`
---| `diagnostics.tag.DEPRECATED`
---@class lsp.diagnostics.location
---@field uri string
---@field range lsp.diagnostics.range
---@class lsp.diagnostics.related_information
---@field location lsp.diagnostics.location
---@field message string
---A diagnostic message.
---@class lsp.diagnostics.message
---@field filename string
---@field range lsp.diagnostics.position
---@field severity lsp.diagnostics.severity_code | integer
---@field code integer | string
---@field codeDescription lsp.diagnostics.code_description
---@field source string
---@field message string
---@field tags lsp.diagnostics.tag_code[]
---@field relatedInformation lsp.diagnostics.related_information
---A diagnostic item.
---@class lsp.diagnostics.item
---@field filename string
---@field messages lsp.diagnostics.message[]
---@type table<integer, lsp.diagnostics.item>
diagnostics.list = {}
---@type integer
diagnostics.count = 0
-- Try to load lintplus plugin if available for diagnostics rendering
local lintplus_found, lintplus = nil, nil
if config.plugins.lintplus ~= false then
lintplus_found, lintplus = pcall(require, "plugins.lintplus")
end
local lintplus_kinds = { "error", "warning", "info", "hint" }
---List of linplus coroutines to delay messages population
---@type table<string,lsp.timer>
local lintplus_delays = {}
---Used to set proper diagnostic type on lintplus
---@type table<integer, string>
diagnostics.lintplus_kinds = lintplus_kinds
---@type boolean
diagnostics.lintplus_found = lintplus_found
---@param a lsp.diagnostics.message
---@param b lsp.diagnostics.message
local function sort_helper(a, b)
return a.severity < b.severity
end
---Helper to catch some trange occurances where nil is given as filename
---@param filename string|nil
---@return string | nil
local function get_absolute_path(filename)
if not filename then
core.error(
"[LSP Diagnostics]: nil filename given",
tostring(filename)
)
return nil
end
return core.project_absolute_path(filename)
end
---Get the position of diagnostics associated to a file.
---@param filename string
---@return integer | nil
function diagnostics.get_index(filename)
---@cast filename +nil
filename = get_absolute_path(filename)
if not filename then return nil end
for index, diagnostic in ipairs(diagnostics.list) do
if diagnostic.filename == filename then
return index
end
end
return nil
end
---Get the diagnostics associated to a file.
---@param filename string
---@param severity? lsp.diagnostics.severity_code | integer
---@return lsp.diagnostics.message[] | nil
function diagnostics.get(filename, severity)
---@cast filename +nil
filename = get_absolute_path(filename)
if not filename then return nil end
for _, diagnostic in ipairs(diagnostics.list) do
if diagnostic.filename == filename then
if not severity then return diagnostic.messages end
local results = {}
for _, message in ipairs(diagnostic.messages) do
if message.severity == severity then table.insert(results, message) end
end
return #results > 0 and results or nil
end
end
return nil
end
---Adds a new list of diagnostics associated to a file replacing previous one.
---@param filename string
---@param messages lsp.diagnostics.message[]
---@return boolean
function diagnostics.add(filename, messages)
local index = diagnostics.get_index(filename)
---@cast filename +nil
filename = get_absolute_path(filename)
if not filename then return false end
table.sort(messages, sort_helper)
if not index then
diagnostics.count = diagnostics.count + 1
table.insert(diagnostics.list, {
filename = filename, messages = messages
})
else
diagnostics.list[index].messages = messages
end
return true
end
---Removes all diagnostics associated to a file.
---@param filename string
function diagnostics.clear(filename)
local index = diagnostics.get_index(filename)
if index then
table.remove(diagnostics.list, index)
diagnostics.count = diagnostics.count - 1
end
end
---Get the amount of diagnostics associated to a file.
---@param filename string
---@param severity? lsp.diagnostics.severity_code | integer
function diagnostics.get_messages_count(filename, severity)
local index = diagnostics.get_index(filename)
if not index then return 0 end
if not severity then return #diagnostics.list[index].messages end
local count = 0
for _, message in ipairs(diagnostics.list[index].messages) do
if message.severity == severity then count = count + 1 end
end
return count
end
---@param doc core.doc
function diagnostics.lintplus_init_doc(doc)
if lintplus_found then
lintplus.init_doc(doc.filename, doc)
end
end
---Remove registered diagnostics from lintplus for the given file or for
---all files if no filename is given.
---@param filename? string
---@param force boolean
function diagnostics.lintplus_clear_messages(filename, force)
if lintplus_found then
if
not force and lintplus_delays[filename]
and
lintplus_delays[filename]:running()
then
return
end
if filename then
lintplus.clear_messages(filename)
else
for fname, _ in pairs(lintplus.messages) do
if lintplus_delays[fname] then
lintplus_delays[fname]:stop()
lintplus_delays[fname] = nil
end
lintplus.clear_messages(fname)
end
end
end
end
---@param filename string
function diagnostics.lintplus_populate(filename)
if lintplus_found then
diagnostics.lintplus_clear_messages(filename, true)
if not filename then
for _, diagnostic in ipairs(diagnostics.list) do
local fname = core.normalize_to_project_dir(diagnostic.filename)
for _, message in pairs(diagnostic.messages) do
local line, col = util.toselection(message.range)
local text = message.message
local kind = lintplus_kinds[message.severity]
lintplus.add_message(fname, line, col, kind, text)
end
end
else
local messages = diagnostics.get(filename)
if messages then
for _, message in pairs(messages) do
local line, col = util.toselection(message.range)
local text = message.message
local kind = lintplus_kinds[message.severity]
lintplus.add_message(
core.normalize_to_project_dir(filename),
line, col, kind, text
)
end
end
end
end
end
---@param filename string
---@param user_typed boolean
function diagnostics.lintplus_populate_delayed(filename)
if lintplus_found then
if not lintplus_delays[filename] then
lintplus_delays[filename] = Timer(
config.plugins.lsp.diagnostics_delay or 500,
true
)
lintplus_delays[filename].on_timer = function()
diagnostics.lintplus_populate(filename)
lintplus_delays[filename] = nil
end
lintplus_delays[filename]:start()
else
lintplus_delays[filename]:reset()
lintplus_delays[filename]:start()
end
end
end
return diagnostics

View file

@ -0,0 +1,29 @@
# Fonts used by the LSP Plugin
This directory holds fonts to enhance the LSP expirience. They are
generated by copying only the desired glyphs from other known fonts
([Nerd Fonts](https://www.nerdfonts.com/)).
## Requirements
* Symbols-2048-em Nerd Font Complete Mono.ttf - symbols.ttf
**Resources:**
* https://www.nerdfonts.com/font-downloads
* https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.0/NerdFontsSymbolsOnly.zip
## How to generate
Copy all the required base fonts into this directory and run:
```sh
./generate-fonts.py
```
or
```sh
fontforge -script generate-fonts.py
```
## Generated Fonts
* `symbols.ttf` - LSP symbols font used as icons on the autocomplete plugin

View file

@ -0,0 +1,101 @@
#!/usr/bin/fontforge -script
#
# Generates a small LSP symbol icons font using 'Symbols Nerd Font'
#
# Usage:
# fontforge -script generate-font.py
#
# References:
# https://www.nerdfonts.com/font-downloads
# https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.0/NerdFontsSymbolsOnly.zip
#
import fontforge
# Define the path to the source font file
# Recommended font from the Symbols Nerd Font package is:
# Symbols-2048-em Nerd Font Complete Mono.ttf
src_font_path = "Symbols-2048-em Nerd Font Complete Mono.ttf"
# List of symbols to copy
# The symbol mappings were taken from:
# * https://github.com/onsails/lspkind.nvim
# * https://github.com/TorchedSammy/lite-xl-lspkind
symbols = [
#
# Nerdicons Preset
#
'', # 0xF77E Text
'', # 0xF6A6 Method
'', # 0xF794 Function
'', # 0xF423 Constructor
'', # 0xFC20 Field
'', # 0xF52A Variable
'', # 0xFD2F Class
'', # 0xF0E8 Interface
'', # 0xF487 Module
'', # 0xFC20 Property
'', # 0xF96C Unit
'', # 0xF89F Value
'', # 0xF15D Enum
'', # 0xF80A Keyword
'', # 0xF44F Snippet
'', # 0xF8D7 Color
'', # 0xF718 File
'', # 0xF706 Reference
'', # 0xF74A Folder
'', # 0xF15D EnumMember
'', # 0xF8FE Constant
'', # 0xFB44 Struct
'', # 0xF0E7 Event
'', # 0xF694 Operator
'', # 0xF128 Unknown
'', # TypeParameter
#
# Codicons Preset
#
'', # Text
'', # Method
'', # Function
'', # Constructor
'', # Field
'', # Variable
'', # Class
'', # Interface
'', # Module
'', # Property
'', # Unit
'', # Value
'', # Enum
'', # Keyword
'', # Snippet
'', # Color
'', # File
'', # Reference
'', # Folder
'', # EnumMember
'', # Constant
'', # Struct
'', # Event
'', # Operator
'', # Unknown
'' # TypeParameter
]
# Convert symbols list to an integers list a.k.a. unicode values
unicode_values = []
for char in symbols:
unicode_values.append(ord(char))
# Load the source font into FontForge
src_font = fontforge.open(src_font_path)
# Remove unwanted glyph
src_font.selection.select(*unicode_values)
src_font.selection.invert()
src_font.clear()
# Save as new font
src_font.fontname = "LSPSymbols"
src_font.familyname = "LSP Symbols"
src_font.fullname = "LSP Symbols"
src_font.generate("symbols.ttf")

Binary file not shown.

View file

@ -0,0 +1,32 @@
---@type core.doc
local Doc = require "core.doc"
---A readonly core.doc.
---@class lsp.helpdoc : core.doc
local HelpDoc = Doc:extend()
---Set the help text.
---@param text string
function HelpDoc:set_text(text)
self.lines = {}
local i = 1
for line in text:gmatch("([^\n]*)\n?") do
if line:byte(-1) == 13 then
line = line:sub(1, -2)
self.crlf = true
end
table.insert(self.lines, line .. "\n")
self.highlighter.lines[i] = false
i = i + 1
end
self:reset_syntax()
end
function HelpDoc:raw_insert(...) end
function HelpDoc:raw_remove(...) end
function HelpDoc:load(...) end
function HelpDoc:reload() end
function HelpDoc:save(...) end
return HelpDoc

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,532 @@
--
-- json.lua
-- Origin: https://github.com/rxi/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" }
local error_message = ""
-- Lets us explicitly add null values to table elements
json.null = "{{json::null}}"
-- Treat numbers longer than 14 digits as a string by adding this to the
-- beginning of the string for encoder to recognize. This prevents any data
-- loss due to lua 5.2 not supporting big integer numbers and converting big
-- integers to floats. The drawback is that the user should manually convert
-- these strings to a number. Numbers with less than 15 digits are not affected.
json.number_flag = "{{json::num}}"
local number_flag_len = #json.number_flag
-------------------------------------------------------------------------------
-- 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
if #res > 0 then
return "[" .. table.concat(res, ",") .. "]"
else
return "{}"
end
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)
if val == json.null then
return "null"
elseif
#val > number_flag_len
and
string.sub(val, 1, number_flag_len) == json.number_flag
then
local num = string.sub(val, number_flag_len+1)
return num
end
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, prettify)
local out = ( encode(val) )
if prettify then
return json.prettify(out)
end
return out
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 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_message = string.format("%s at line %d col %d", msg, line_count, col_count)
end
local function next_char(str, idx, set, negate)
if type(idx) ~= "number" then
decode_error(str, #str, "invalid json string")
return #str + 1
end
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
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 = nil
if #s > 14 then
n = json.number_flag .. s
else
n = tonumber(s)
end
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.last_error()
return error_message
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
error_message = ""
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
if error_message ~= "" then
return false
end
return res
end
local function indent(code, level, indent_width)
return string.rep(" ", level * indent_width) .. code
end
--- Implemented some json prettifier but not a parser so
--- don't expect it to give you parsing errors :D
--- @param text string The json string
--- @param indent_width? integer The amount of spaces per indentation
--- @return string
function json.prettify(text, indent_width)
if type(text) ~= "string" then
return ""
end
local out = ""
indent_width = indent_width or 2
local indent_level = 0
local reading_literal = false
local previous_was_escape = false
local inside_string = false
local in_value = false
local last_was_bracket = false
local string_char = ""
local last_char = ""
for char in text:gmatch(".") do
if (char == "{" or char == "[") and not inside_string then
if not in_value or last_was_bracket then
out = out .. indent(char, indent_level, indent_width) .. "\n"
else
out = out .. char .. "\n"
end
last_was_bracket = true
in_value = false
indent_level = indent_level + 1
elseif (char == '"' or char == "'") and not inside_string then
inside_string = true
string_char = char
if not in_value then
out = out .. indent(char, indent_level, indent_width)
else
out = out .. char
end
elseif inside_string then
local pe_set = false
if char == "\\" and previous_was_escape then
previous_was_escape = false
elseif char == "\\" then
previous_was_escape = true
pe_set = true
end
out = out .. char
if char == string_char and not previous_was_escape then
inside_string = false
elseif previous_was_escape and not pe_set then
previous_was_escape = false
end
elseif char == ":" then
in_value = true
last_was_bracket = false
out = out .. char .. " "
elseif char == "," then
in_value = false
reading_literal = false
out = out .. char .. "\n"
elseif char == "}" or char == "]" then
indent_level = indent_level - 1
if
(char == "}" and last_char == "{")
or
(char == "]" and last_char == "[")
then
out = out:gsub("%s*\n$", "") .. char
else
out = out .. "\n" .. indent(char, indent_level, indent_width)
end
elseif not char:match("%s") and not reading_literal then
reading_literal = true
if not in_value or last_was_bracket then
out = out .. indent(char, indent_level, indent_width)
last_was_bracket = false
else
out = out .. char
end
elseif not char:match("%s") then
out = out .. char
end
if not char:match("%s") then
last_char = char
end
end
return out
end
return json

View file

@ -0,0 +1,580 @@
-- A configurable listbox that can be used as tooltip, selection box and
-- selection box with fuzzy search, this may change in the future.
--
-- @note This code is a readaptation of autocomplete plugin from rxi :)
--
-- TODO implement select box with fuzzy search
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local style = require "core.style"
local keymap = require "core.keymap"
local util = require "plugins.lsp.util"
local RootView = require "core.rootview"
local DocView = require "core.docview"
---@class lsp.listbox.item
---@field text string
---@field info string
---@field on_draw fun(item:lsp.listbox.item, x:number, y:number, calc_only?:boolean):number
---@alias lsp.listbox.callback fun(doc: core.doc, item: lsp.listbox.item)
---@class lsp.listbox.signature_param
---@field label string
---@class lsp.listbox.signature
---@field label string
---@field activeParameter? integer
---@field activeSignature? integer
---@field parameters lsp.listbox.signature_param[]
---@class lsp.listbox.signature_list
---@field activeParameter? integer
---@field activeSignature? integer
---@field signatures lsp.listbox.signature[]
---@class lsp.listbox.position
---@field line integer
---@field col integer
---@class lsp.listbox
local listbox = {}
---@class lsp.listbox.settings
---@field items lsp.listbox.item[]
---@field shown_items lsp.listbox.item[]
---@field selected_item_idx integer
---@field show_items_count boolean
---@field max_height integer
---@field active_view core.docview | nil
---@field line integer | nil
---@field col integer | nil
---@field last_line integer | nil
---@field last_col integer | nil
---@field callback lsp.listbox.callback | nil
---@field is_list boolean
---@field has_fuzzy_search boolean
---@field above_text boolean
local settings = {
items = {},
shown_items = {},
selected_item_idx = 1,
show_items_count = false,
max_height = 6,
active_view = nil,
line = nil,
col = nil,
last_line = nil,
last_col = nil,
callback = nil,
is_list = false,
has_fuzzy_search = false,
above_text = false,
}
local mt = { __tostring = function(t) return t.text end }
--------------------------------------------------------------------------------
-- Private functions
--------------------------------------------------------------------------------
---@return core.docview | nil
local function get_active_view()
if getmetatable(core.active_view) == DocView then
return core.active_view
end
end
---@param active_view core.docview
---@return number x
---@return number y
---@return number width
---@return number height
local function get_suggestions_rect(active_view)
if #settings.shown_items == 0 then
listbox.hide()
return 0, 0, 0, 0
end
local line, col
if settings.line then
line, col = settings.line, settings.col
else
line, col = active_view.doc:get_selection()
end
-- Validate line against current view because there can be cases
-- when user rapidly switches between tabs causing the deferred draw
-- to be called late and the current document view already changed.
if line > #active_view.doc.lines then
listbox.hide()
return 0, 0, 0, 0
end
local x, y = active_view:get_line_screen_position(line)
-- This function causes tokenizer to fail if given line is greater than
-- the amount of lines the document holds, so validation above is needed.
x = x + active_view:get_col_x_offset(line, col)
local padding_x = style.padding.x
local padding_y = style.padding.y
if settings.above_text and line > 1 then
y = y - active_view:get_line_height() - style.padding.y
else
y = y + active_view:get_line_height() + style.padding.y
end
local font = settings.is_list and active_view:get_font() or style.font
local text_height = font:get_height()
local max_width = 0
for _, item in ipairs(settings.shown_items) do
local w = 0
if item.on_draw then
w = item.on_draw(item, 0, 0, true)
else
w = font:get_width(item.text)
if item.info then
w = w + style.font:get_width(item.info) + style.padding.x
end
end
max_width = math.max(max_width, w)
end
local max_items = #settings.shown_items
if settings.is_list and max_items > settings.max_height then
max_items = settings.max_height
end
-- additional line to display total items
if settings.show_items_count then
max_items = max_items + 1
end
if max_width < 150 then
max_width = 150
end
local height = max_items * (text_height + (padding_y/4)) + (padding_y*2)
local width = max_width + padding_x * 2
x = x - padding_x
y = y - padding_y
local win_w = system.get_window_size()
if (width/win_w*100) >= 85 and (width+style.padding.x*4) < win_w then
x = win_w - width - style.padding.x*2
elseif width > (win_w - x) then
x = x - (width - (win_w - x))
if x < 0 then
x = 0
end
end
return x, y, width, height
end
---@param av core.docview
local function draw_listbox(av)
if #settings.shown_items <= 0 then
return
end
-- draw background rect
local rx, ry, rw, rh = get_suggestions_rect(av)
-- draw border
if not settings.is_list then
local border_width = 1
renderer.draw_rect(
rx - border_width,
ry - border_width,
rw + (border_width * 2),
rh + (border_width * 2),
style.divider
)
end
renderer.draw_rect(rx, ry, rw, rh, style.background3)
local padding_x = style.padding.x
local padding_y = style.padding.y
-- draw text
local font = settings.is_list and av:get_font() or style.font
local line_height = font:get_height() + (padding_y / 4)
local y = ry + padding_y
local max_height = settings.max_height
local show_count = (
#settings.shown_items <= max_height or not settings.is_list
) and
#settings.shown_items or max_height
local start_index = settings.selected_item_idx > max_height and
(settings.selected_item_idx-(max_height-1)) or 1
for i=start_index, start_index+show_count-1, 1 do
if not settings.shown_items[i] then
break
end
local item = settings.shown_items[i]
if item.on_draw then
item.on_draw(item, rx + padding_x, y)
else
local color = (i == settings.selected_item_idx and settings.is_list) and
style.accent or style.text
common.draw_text(
font, color, item.text, "left",
rx + padding_x, y, rw, line_height
)
if item.info then
color = (i == settings.selected_item_idx and settings.is_list) and
style.text or style.dim
common.draw_text(
style.font, color, item.info, "right",
rx, y, rw - padding_x, line_height
)
end
end
y = y + line_height
end
if settings.show_items_count then
renderer.draw_rect(rx, y, rw, 2, style.caret)
renderer.draw_rect(rx, y+2, rw, line_height, style.background)
common.draw_text(
style.font,
style.accent,
"Items",
"left",
rx + padding_x, y, rw, line_height
)
common.draw_text(
style.font,
style.accent,
tostring(settings.selected_item_idx) .. "/" .. tostring(#settings.shown_items),
"right",
rx, y, rw - padding_x, line_height
)
end
end
---Set the document position where the listbox will be draw.
---@param position? lsp.listbox.position
local function set_position(position)
if type(position) == "table" then
settings.line = position.line
settings.col = position.col
else
settings.line = nil
settings.col = nil
end
end
--------------------------------------------------------------------------------
-- Public functions
--------------------------------------------------------------------------------
---@param elements lsp.listbox.item[]
function listbox.add(elements)
if type(elements) == "table" and #elements > 0 then
local items = {}
for _, element in pairs(elements) do
table.insert(items, setmetatable(element, mt))
end
settings.items = items
end
end
function listbox.clear()
settings.items = {}
settings.selected_item_idx = 1
settings.shown_items = {}
settings.line = nil
settings.col = nil
end
---@param element lsp.listbox.item
function listbox.append(element)
table.insert(settings.items, setmetatable(element, mt))
end
function listbox.hide()
settings.active_view = nil
settings.line = nil
settings.col = nil
settings.selected_item_idx = 1
settings.shown_items = {}
core.redraw = true
end
---@param is_list? boolean
---@param position? lsp.listbox.position
function listbox.show(is_list, position)
set_position(position)
local active_view = get_active_view()
if active_view then
settings.active_view = active_view
settings.last_line, settings.last_col = active_view.doc:get_selection()
if settings.items and #settings.items > 0 then
settings.is_list = is_list or false
settings.shown_items = settings.items
end
end
core.redraw = true
end
---@param text string
---@param position? lsp.listbox.position
function listbox.show_text(text, position)
if text and type("text") == "string" then
local win_w = system.get_window_size() - style.padding.x * 6
text = util.wrap_text(text, style.font, win_w)
local items = {}
for result in string.gmatch(text.."\n", "(.-)\n") do
table.insert(items, {text = result})
end
listbox.add(items)
end
listbox.show(false, position)
end
---@param items lsp.listbox.item[]
---@param callback lsp.listbox.callback
---@param position? lsp.listbox.position
function listbox.show_list(items, callback, position)
listbox.add(items)
if callback then
settings.callback = callback
end
listbox.show(true, position)
end
---@param signatures lsp.listbox.signature_list
---@param position? lsp.listbox.position
function listbox.show_signatures(signatures, position)
local active_parameter = nil
local active_signature = nil
if signatures.activeParameter then
active_parameter = signatures.activeParameter + 1
end
if signatures.activeSignature then
active_signature = signatures.activeSignature + 1
end
local signatures_count = #signatures.signatures
local items = {}
for index, signature in ipairs(signatures.signatures) do
table.insert(items, {
text = signature.label,
signature = signature,
on_draw = function(item, x, y, calc_only)
local width = 0
local height = style.font:get_height()
if item.signature.parameters then
if signatures_count > 1 then
if index == active_signature then
width = style.font:get_width("> ")
else
width = style.font:get_width("> ")
x = x + style.font:get_width("> ")
end
end
width = width
+ style.font:get_width("(")
+ style.font:get_width(")")
if not calc_only then
if signatures_count > 1 and index == active_signature then
x = renderer.draw_text(style.font, "> ", x, y, style.caret)
end
x = renderer.draw_text(style.font, "(", x, y, style.text)
end
local params_count = #item.signature.parameters
for pindex, param in ipairs(item.signature.parameters) do
local label = ""
if type(param.label) == "table" then
label = signature.label:sub(param.label[1]+1, param.label[2])
else
label = param.label
end
if label and pindex ~= params_count then
label = label .. ", "
end
width = width + style.font:get_width(label)
if not calc_only then
local color = style.text
if
(
signature.activeParameter
and
(signature.activeParameter + 1) == pindex
)
or
(index == active_signature and active_parameter == pindex)
then
color = style.accent
end
x = renderer.draw_text(
style.font,
label,
x, y,
color
)
end
end
if not calc_only then
renderer.draw_text(style.font, ")", x, y, style.text)
end
else
width = style.font:get_width(item.signature.label)
if not calc_only then
renderer.draw_text(
style.font,
item.signature.label,
x, y,
style.text
)
end
end
return width, width > 0 and height or 0
end
})
end
listbox.add(items)
listbox.show(false, position)
end
function listbox.toggle_above(enable)
if enable then
settings.above_text = true
else
settings.above_text = false
end
end
--------------------------------------------------------------------------------
-- Patch event logic into RootView
--------------------------------------------------------------------------------
local root_view_update = RootView.update
local root_view_draw = RootView.draw
RootView.update = function(...)
root_view_update(...)
if not settings.active_view then return end
local active_view = get_active_view()
if active_view then
-- reset suggestions if caret was moved or not same active view
local line, col = active_view.doc:get_selection()
if
settings.active_view ~= active_view
or
line ~= settings.last_line or col ~= settings.last_col
then
listbox.hide()
end
else
listbox.hide()
end
end
RootView.draw = function(...)
if settings.active_view then
local active_view = get_active_view()
if
active_view and settings.active_view == active_view
and
#settings.shown_items > 0
then
-- draw suggestions box after everything else
core.root_view:defer_draw(draw_listbox, active_view)
end
end
root_view_draw(...)
end
--------------------------------------------------------------------------------
-- Commands
--------------------------------------------------------------------------------
local function predicate()
local av = get_active_view()
return av and settings.active_view and #settings.shown_items > 0, av
end
command.add(predicate, {
["listbox:select"] = function(av)
---@cast av core.docview
if settings.is_list then
local doc = av.doc
local item = settings.shown_items[settings.selected_item_idx]
if settings.callback then
settings.callback(doc, item)
end
listbox.hide()
end
end,
["listbox:previous"] = function()
if settings.is_list then
settings.selected_item_idx = math.max(settings.selected_item_idx - 1, 1)
else
listbox.hide()
end
end,
["listbox:next"] = function()
if settings.is_list then
settings.selected_item_idx = math.min(
settings.selected_item_idx + 1, #settings.shown_items
)
else
listbox.hide()
end
end,
["listbox:cancel"] = function()
listbox.hide()
end,
})
--------------------------------------------------------------------------------
-- Keymaps
--------------------------------------------------------------------------------
keymap.add {
["tab"] = "listbox:select",
["up"] = "listbox:previous",
["down"] = "listbox:next",
["escape"] = "listbox:cancel",
}
return listbox

View file

@ -0,0 +1,19 @@
{
"addons": [
{
"id": "lsp",
"name": "Language Server Protocol",
"description": "Provides intellisense by leveraging the LSP protocol.",
"version": "0.8",
"mod_version": "3",
"dependencies": {
"widget": { "version": ">=0.1" },
"lintplus": { "version": ">=0.2", "optional": true },
"lsp_snippets": { "optional": true }
},
"tags": [
"language server protocol"
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,121 @@
--
-- SymbolResults Widget/View.
-- @copyright Jefferson Gonzalez
-- @license MIT
--
local style = require "core.style"
local Widget = require "libraries.widget"
local Label = require "libraries.widget.label"
local Line = require "libraries.widget.line"
local ListBox = require "libraries.widget.listbox"
local Server = require "plugins.lsp.server"
local Lsp = {}
---@class lsp.symbolresults : widget
---@field public searching boolean
---@field public symbol string
---@field private title widget.label
---@field private line widget.line
---@field private list_container widget
---@field private list widget.listbox
local SymbolResults = Widget:extend()
function SymbolResults:new(symbol)
SymbolResults.super.new(self)
Lsp = require "plugins.lsp"
self.name = "Symbols Search"
self.defer_draw = false
self.searching = true
self.symbol = symbol or ""
self.title = Label(self, "Searching symbols for: " .. symbol)
self.line = Line(self, 2, style.padding.x)
self.list_container = Widget(self)
self.list_container.border.width = 0
self.list_container:set_size(200, 200)
self.list = ListBox(self.list_container)
self.list.border.width = 0
self.list:enable_expand(true)
self.list:add_column("Num.")
self.list:add_column("Symbol")
self.list:add_column("Kind")
self.list:add_column("Location")
local list_on_row_click = self.list.on_row_click
self.list.on_row_click = function(this, idx, data)
list_on_row_click(this, idx, data)
self:on_selected(idx, data)
end
self.num = 1
self.border.width = 0
self:set_size(200, 200)
self:show()
end
function SymbolResults:add_result(result)
local preview, position = Lsp.get_location_preview(result.location)
local container_name = result.containerName and
result.containerName .. "\n" or ""
local row = {
tostring(self.num),
ListBox.COLEND,
style.syntax.keyword, container_name .. result.name,
ListBox.COLEND,
style.syntax.literal, Server.get_symbol_kind(result.kind),
ListBox.COLEND,
style.text, position, ListBox.NEWLINE, style.accent, preview
}
self.num = self.num + 1
self.list:add_row(row, result)
end
function SymbolResults:stop_searching()
self.searching = false
end
function SymbolResults:on_selected(idx, data)
Lsp.goto_location(data.location)
end
function SymbolResults:update()
if not SymbolResults.super.update(self) then return end
-- update the positions and sizes
self.background_color = style.background
self.title:set_position(style.padding.x, style.padding.y)
if not self.searching or #self.list.rows > 0 then
local label = "Finished: "
if self.searching then
label = "Searching: "
end
self.title:set_label(
label
.. #self.list.rows
.. " results found for "
.. '"'
.. self.symbol
.. '"'
)
end
self.line:set_position(0, self.title:get_bottom() + 10)
self.list_container:set_position(style.padding.x, self.line:get_bottom() + 10)
self.list_container:set_size(
self.size.x - (style.padding.x * 2),
self.size.y - self.line:get_bottom()
)
end
return SymbolResults

View file

@ -0,0 +1,101 @@
local core = require "core"
local Object = require "core.object"
---Timer class
---@class lsp.timer : core.object
---@field public interval integer
---@field public single_shot boolean
---@field private started boolean
---@field private last_run integer
local Timer = Object:extend()
---Constructor
---@param interval integer The interval in milliseconds
---@param single_shot boolean Indicates if timer should only run once
function Timer:new(interval, single_shot)
Timer.super.new(self)
self.single_shot = single_shot or false
self.started = false
self.cancel_thread_marker = { cancel = true }
self.last_run = 0
self:set_interval(interval or 1000)
end
---Starts a non running timer.
function Timer:start()
if self.started then return end
self.started = true
self.cancel_thread_marker = { cancel = false }
local this = self
-- Save marker so that we keep this one and not an "updated" one
local marker = self.cancel_thread_marker
core.add_thread(function()
while true do
if marker.cancel == true then return end
this:reset()
local now = system.get_time()
local remaining = (this.last_run + this.interval) - now
if remaining > 0 then
repeat
if not this.started or marker.cancel then return end
coroutine.yield(remaining)
now = system.get_time()
remaining = (this.last_run + this.interval) - now
until remaining <= 0
end
if not this.started or marker.cancel then return end
this:on_timer()
if this.single_shot then break end
end
this.started = false
end)
end
---Stops a running timer.
function Timer:stop()
self.started = false
end
---Resets the timer countdown for execution.
function Timer:reset()
self.last_run = system.get_time()
end
---Restarts the timer countdown for execution.
function Timer:restart()
self:reset()
if not self.started then
self:start()
end
end
---Check if the timer is running.
---@return boolean
function Timer:running()
return self.started
end
---Appropriately set the timer interval by converting milliseconds to seconds.
---@param interval integer The interval in milliseconds
function Timer:set_interval(interval)
local new_interval = interval / 1000
-- As this new interval might be shorter than the currently running one, and
-- because we might already be sleeping waiting for the old interval, we
-- mark the already running coroutine as cancelled, and create a new one.
if self.started and self.interval > new_interval then
self.cancel_thread_marker.cancel = true
self:stop()
self:start()
end
self.interval = new_interval
end
---To be overwritten by the instantiated timer objects
function Timer:on_timer() end
return Timer

View file

@ -0,0 +1,534 @@
-- Some functions adapted from: https://github.com/orbitalquark/textadept-lsp
-- and others added as needed.
--
-- @copyright Jefferson Gonzalez
-- @license MIT
local core = require "core"
local common = require "core.common"
local config = require "core.config"
local json = require "plugins.lsp.json"
local util = {}
---Check if the given file is currently opened on the editor.
---@param abs_filename string
function util.doc_is_open(abs_filename)
-- make path separator consistent
abs_filename = abs_filename:gsub("\\", "/")
for _, doc in ipairs(core.docs) do
---@cast doc core.doc
if doc.abs_filename then
local doc_path = doc.abs_filename:gsub("\\", "/")
if doc_path == abs_filename then
return true;
end
end
end
return false
end
---Converts a utf-8 column position into the equivalent utf-16 position.
---@param doc core.doc
---@param line integer
---@param column integer
---@return integer col_position
function util.doc_utf8_to_utf16(doc, line, column)
local ltext = doc.lines[line]
local ltext_len = ltext and #ltext or 0
local ltext_ulen = ltext and utf8extra.len(ltext) or 0
column = common.clamp(column, 1, ltext_len > 0 and ltext_len or 1)
-- no need for conversion so return column as is
if ltext_len == ltext_ulen then return column end
if column > 1 then
local col = 1
for pos, code in utf8extra.next, ltext do
if pos >= column then
return col
end
-- Codepoints that high are encoded using surrogate pairs
if code < 0x010000 then
col = col + 1
else
col = col + 2
end
end
return col
end
return column
end
---Converts a utf-16 column position into the equivalent utf-8 position.
---@param doc core.doc
---@param line integer
---@param column integer
---@return integer col_position
function util.doc_utf16_to_utf8(doc, line, column)
local ltext = doc.lines[line]
local ltext_len = ltext and #ltext or 0
local ltext_ulen = ltext and utf8extra.len(ltext) or 0
column = common.clamp(column, 1, ltext_len > 0 and ltext_len or 1)
-- no need for conversion so return column as is
if ltext_len == ltext_ulen then return column end
if column > 1 then
local col = 1
local utf8_pos = 1
for pos, code in utf8extra.next, ltext do
if col >= column then
return pos
end
utf8_pos = pos
-- Codepoints that high are encoded using surrogate pairs
if code < 0x010000 then
col = col + 1
else
col = col + 2
end
end
return utf8_pos
end
return column
end
---Split a string by the given delimeter
---@param s string The string to split
---@param delimeter string Delimeter without lua patterns
---@param delimeter_pattern? string Optional delimeter with lua patterns
---@return table
---@return boolean ends_with_delimiter
function util.split(s, delimeter, delimeter_pattern)
if not delimeter_pattern then
delimeter_pattern = delimeter
end
local last_idx = 1
local result = {}
for match_idx, afer_match_idx in s:gmatch("()"..delimeter_pattern.."()") do
table.insert(result, string.sub(s, last_idx, match_idx - 1))
last_idx = afer_match_idx
end
if last_idx > #s then
return result, true
else
table.insert(result, string.sub(s, last_idx))
return result, false
end
end
---Get the extension component of a filename.
---@param filename string
---@return string
function util.file_extension(filename)
local parts = util.split(filename, "%.")
if #parts > 1 then
return parts[#parts]:gsub("%%", "")
end
return filename
end
---Check if a file exists.
---@param file_path string
---@return boolean
function util.file_exists(file_path)
local file = io.open(file_path, "r")
if file ~= nil then
file:close()
return true
end
return false
end
---Converts the given LSP DocumentUri into a valid filename path.
---@param uri string LSP DocumentUri to convert into a filename.
---@return string
function util.tofilename(uri)
local filename = ""
if PLATFORM == "Windows" then
filename = uri:gsub('^file:///', '')
else
filename = uri:gsub('^file://', '')
end
filename = filename:gsub(
'%%(%x%x)',
function(hex) return string.char(tonumber(hex, 16)) end
)
if PLATFORM == "Windows" then filename = filename:gsub('/', '\\') end
return filename
end
---Convert a file path to a LSP valid uri.
---@param file_path string
---@return string
function util.touri(file_path)
if PLATFORM ~= "Windows" then
file_path = 'file://' .. file_path
else
file_path = 'file:///' .. file_path:gsub('\\', '/')
end
return file_path
end
---Converts a document range returned by lsp to a valid document selection.
---@param range table LSP Range.
---@param doc? core.doc
---@return integer line1
---@return integer col1
---@return integer line2
---@return integer col2
function util.toselection(range, doc)
local line1 = range.start.line + 1
local col1 = range.start.character + 1
local line2 = range['end'].line + 1
local col2 = range['end'].character + 1
if doc then
col1 = util.doc_utf16_to_utf8(doc, line1, col1)
col2 = util.doc_utf16_to_utf8(doc, line2, col2)
end
return line1, col1, line2, col2
end
---Opens the given location on a external application.
---@param location string
function util.open_external(location)
local filelauncher = ""
if PLATFORM == "Windows" then
filelauncher = "start"
elseif PLATFORM == "Mac OS X" then
filelauncher = "open"
else
filelauncher = "xdg-open"
end
-- non-Windows platforms need the text quoted (%q)
if PLATFORM ~= "Windows" then
location = string.format("%q", location)
end
system.exec(filelauncher .. " " .. location)
end
---Prettify json output and logs it if config.lsp.log_file is set.
---@param code string
---@return string
function util.jsonprettify(code)
if config.plugins.lsp.prettify_json then
code = json.prettify(code)
end
if config.plugins.lsp.log_file and #config.plugins.lsp.log_file > 0 then
local log = io.open(config.plugins.lsp.log_file, "a+")
log:write("Output: \n" .. tostring(code) .. "\n\n")
log:close()
end
return code
end
---Gets the last component of a path. For example:
---/my/path/to/somwhere would return somewhere.
---@param path string
---@return string
function util.getpathname(path)
local components = {}
if PLATFORM == "Windows" then
components = util.split(path, "\\")
else
components = util.split(path, "/")
end
if #components > 0 then
return components[#components]
end
return path
end
---Check if a value is on a table.
---@param value any
---@param table_array table
---@return boolean
function util.intable(value, table_array)
for _, element in pairs(table_array) do
if element == value then
return true
end
end
return false
end
---Check if a command exists on the system by inspecting the PATH envar.
---@param command string
---@return boolean
function util.command_exists(command)
local command_win = nil
if PLATFORM == "Windows" then
if not command:find("%.exe$") then
command_win = command .. ".exe"
end
end
if
util.file_exists(command)
or
(command_win and util.file_exists(command_win))
then
return true
end
local env_path = os.getenv("PATH")
local path_list = {}
if PLATFORM ~= "Windows" then
path_list = util.split(env_path, ":")
else
path_list = util.split(env_path, ";")
end
-- Automatic support for brew, macports, etc...
if PLATFORM == "Mac OS X" then
if
system.get_file_info("/usr/local/bin")
and
not string.find(env_path, "/usr/local/bin", 1, true)
then
table.insert(path_list, 1, "/usr/local/bin")
end
end
for _, path in pairs(path_list) do
local path_fix = path:gsub("[/\\]$", "") .. PATHSEP
if util.file_exists(path_fix .. command) then
return true
elseif command_win and util.file_exists(path_fix .. command_win) then
return true
end
end
return false
end
---From a list of executable names get the one that is installed
---on the system or first one if none of them exists.
---@param executables table<integer,string>
---@return string executable_name
function util.get_best_executable(executables)
for _, executable in ipairs(executables) do
if util.command_exists(executable) then
return executable
end
end
return executables[1]
end
---Remove by key from a table and returns a new
---table with element removed.
---@param table_object table
---@param key_name string|integer
---@return table
function util.table_remove_key(table_object, key_name)
local new_table = {}
for key, data in pairs(table_object) do
if key ~= key_name then
new_table[key] = data
end
end
return new_table
end
---Get a table specific field or nil if not found.
---@param t table The table we are going to search for the field.
---@param fieldset string A field spec in the format
---"parent[.child][.subchild]" eg: "myProp.subProp.subSubProp"
---@return any|nil The value of the given field or nil if not found.
function util.table_get_field(t, fieldset)
local fields = util.split(fieldset, ".", "%.")
local field = fields[1]
local value = nil
if field and #fields > 1 and t[field] then
local sub_fields = table.concat(fields, ".", 2)
value = util.table_get_field(t[field], sub_fields)
elseif field and #fields > 0 and t[field] then
value = t[field]
end
return value
end
---Merge the content of the tables into a new one.
---Arguments from the later tables take precedence.
---Doesn't touch the original tables.
---`nil` arguments are ignored.
---@param ... table?
---@return table
function util.deep_merge(...)
local t = {}
local args = table.pack(...)
for i=1,args.n do
local other = args[i]
if other then
assert(type(other) == "table", string.format("Argument %d must be a table", i))
for k, v in pairs(other) do
if type(v) == "table" then
if type(t[k]) == "table" then
t[k] = util.deep_merge(t[k], v)
else
t[k] = util.deep_merge({}, v)
end
else
t[k] = v
end
end
end
end
return t
end
---Check if a table is really empty.
---@param t table
---@return boolean
function util.table_empty(t)
return next(t) == nil
end
---Convert markdown to plain text.
---@param text string
---@return string
function util.strip_markdown(text)
local clean_text = ""
local prev_line = ""
for match in (text.."\n"):gmatch("(.-)".."\n") do
match = match .. "\n"
-- strip markdown
local new_line = match
-- Block quotes
:gsub("^>+(%s*)", "%1")
-- headings
:gsub("^(%s*)######%s(.-)\n", "%1%2\n")
:gsub("^(%s*)#####%s(.-)\n", "%1%2\n")
:gsub("^(%s*)####%s(.-)\n", "%1%2\n")
:gsub("^(%s*)####%s(.-)\n", "%1%2\n")
:gsub("^(%s*)###%s(.-)\n", "%1%2\n")
:gsub("^(%s*)##%s(.-)\n", "%1%2\n")
:gsub("^(%s*)#%s(.-)\n", "%1%2\n")
-- heading custom id
:gsub("{#.-}", "")
-- emoji
:gsub(":[%w%-_]+:", "")
-- bold and italic
:gsub("%*%*%*(.-)%*%*%*", "%1")
:gsub("___(.-)___", "%1")
:gsub("%*%*_(.-)_%*%*", "%1")
:gsub("__%*(.-)%*__", "%1")
:gsub("___(.-)___", "%1")
-- bold
:gsub("%*%*(.-)%*%*", "%1")
:gsub("__(.-)__", "%1")
-- strikethrough
:gsub("%-%-(.-)%-%-", "%1")
-- italic
:gsub("%*(.-)%*", "%1")
:gsub("%s_(.-)_%s", "%1")
:gsub("\\_(.-)\\_", "_%1_")
:gsub("^_(.-)_", "%1")
-- code
:gsub("^%s*```(%w+)%s*\n", "")
:gsub("^%s*```%s*\n", "")
:gsub("``(.-)``", "%1")
:gsub("`(.-)`", "%1")
-- lines
:gsub("^%-%-%-%-*%s*\n", "")
:gsub("^%*%*%*%**%s*\n", "")
-- reference links
:gsub("^%[[^%^](.-)%]:.-\n", "")
-- footnotes
:gsub("^%[%^(.-)%]:%s+", "[%1]: ")
:gsub("%[%^(.-)%]", "[%1]")
-- Images
:gsub("!%[(.-)%]%((.-)%)", "")
-- links
:gsub("%s<(.-)>%s", "%1")
:gsub("%[(.-)%]%s*%[(.-)%]", "%1")
:gsub("%[(.-)%]%((.-)%)", "%1: %2")
-- remove escaped punctuations
:gsub("\\(%p)", "%1")
-- if paragraph put in same line
local is_paragraph = false
local prev_spaces = prev_line:match("^%g+")
local prev_endings = prev_line:match("[ \t\r\n]+$")
local new_spaces = new_line:match("^%g+")
if prev_spaces and new_spaces then
local new_lines = prev_endings ~= nil
and prev_endings:gsub("[ \t\r]+", "") or ""
if #new_lines == 1 then
is_paragraph = true
clean_text = clean_text:gsub("[%s\n]+$", "")
.. " " .. new_line:gsub("^%s+", "")
end
end
if not is_paragraph then
clean_text = clean_text .. new_line
end
prev_line = new_line
end
return clean_text
end
---@param text string
---@param font renderer.font
---@param max_width number
function util.wrap_text(text, font, max_width)
local lines = util.split(text, "\n")
local wrapped_text = ""
local longest_line = 0;
for _, line in ipairs(lines) do
local line_len = line:ulen() or 0
if line_len > longest_line then
longest_line = line_len
local line_width = font:get_width(line)
if line_width > max_width then
local words = util.split(line, " ")
local new_line = words[1] and words[1] or ""
wrapped_text = wrapped_text .. new_line
for w=2, #words do
if font:get_width(new_line .. " " .. words[w]) <= max_width then
new_line = new_line .. " " .. words[w]
wrapped_text = wrapped_text .. " " .. words[w]
else
wrapped_text = wrapped_text .. "\n" .. words[w]
new_line = words[w]
end
end
wrapped_text = wrapped_text .. "\n"
else
wrapped_text = wrapped_text .. line .. "\n"
end
else
wrapped_text = wrapped_text .. line .. "\n"
end
end
wrapped_text = wrapped_text:gsub("\n\n\n\n?", "\n\n"):gsub("%s*$", "")
return wrapped_text
end
return util

View file

@ -0,0 +1,12 @@
-- mod-version:3
local lspconfig = require "plugins.lsp.config"
local common = require "core.common"
local config = require "core.config"
local installed_path = USERDIR .. PATHSEP .. "plugins" .. PATHSEP .. "lsp_json" .. PATHSEP .. "vscode-json-languageserver" .. PATHSEP .. "dist" .. PATHSEP .. "index.js"
local node = require "libraries.nodejs"
lspconfig.jsonls.setup(common.merge({
command = { node.path_bin, installed_path, "--stdio" },
}, config.plugins.lsp_json or {}))

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 - present Microsoft Corporation
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.

View file

@ -0,0 +1,189 @@
@vscode/l10n
MIT
jsonc-parser
MIT
The MIT License (MIT)
Copyright (c) Microsoft
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.
request-light
MIT
The MIT License (MIT)
Copyright (c) Microsoft
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.
vscode-json-languageservice
MIT
The MIT License (MIT)
Copyright (c) Microsoft
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.
Glob matching is based on code from https://github.com/fitzgen/glob-to-regexp
Copyright (c) 2013, Nick Fitzgerald
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
vscode-jsonrpc
MIT
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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.
vscode-languageserver
MIT
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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.
vscode-languageserver-protocol
MIT
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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.
vscode-languageserver-textdocument
MIT
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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.
vscode-languageserver-types
MIT
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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.
vscode-uri
MIT
The MIT License (MIT)
Copyright (c) Microsoft
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.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,793 @@
-- mod-version:3
-- LSP style snippet parser
-- shamelessly 'inspired by' (stolen from) LuaSnip
-- https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/util/parser/neovim_parser.lua
local core = require 'core'
local common = require 'core.common'
local Doc = require 'core.doc'
local system = require 'system'
local regex = require 'regex'
local snippets = require 'plugins.snippets'
local json do
local ok, j
for _, p in ipairs {
'plugins.json', 'plugins.lsp.json', 'plugins.lintplus.json',
'libraries.json'
} do
ok, j = pcall(require, p)
if ok then json = j; break end
end
end
local B = snippets.builder
local LAST_CONVERTED_ID = { }
local THREAD_KEY = { }
-- node factories
local function doc_syntax(doc, k)
return doc.syntax and doc.syntax[k]
end
local variables = {
-- LSP
TM_SELECTED_TEXT = function(ctx) return ctx.selection end,
TM_CURRENT_LINE = function(ctx) return ctx.doc.lines[ctx.line] end,
TM_CURRENT_WORD = function(ctx) return ctx.partial end,
TM_LINE_INDEX = function(ctx) return ctx.line - 1 end,
TM_LINE_NUMBER = function(ctx) return ctx.line end,
TM_FILENAME = function(ctx) return ctx.doc.filename:match('[^/%\\]*$') or '' end,
TM_FILENAME_BASE = function(ctx) return ctx.doc.filename:match('([^/%\\]*)%.%w*$') or ctx.doc.filename end,
TM_DIRECTORY = function(ctx) return ctx.doc.filename:match('([^/%\\]*)[/%\\].*$') or '' end,
TM_FILEPATH = function(ctx) return common.dirname(ctx.doc.abs_filename) or '' end,
-- VSCode
RELATIVE_FILEPATH = function(ctx) return core.normalize_to_project_dir(ctx.doc.filename) end,
CLIPBOARD = function() return system.get_clipboard() end,
-- https://github.com/lite-xl/lite-xl/pull/1455
WORKSPACE_NAME = function(ctx) return end,
WORKSPACE_FOLDER = function(ctx) return end,
CURSOR_INDEX = function(ctx) return ctx.col - 1 end,
CURSOR_NUMBER = function(ctx) return ctx.col end,
CURRENT_YEAR = function() return os.date('%G') end,
CURRENT_YEAR_SHORT = function() return os.date('%g') end,
CURRENT_MONTH = function() return os.date('%m') end,
CURRENT_MONTH_NAME = function() return os.date('%B') end,
CURRENT_MONTH_NAME_SHORT = function() return os.date('%b') end,
CURRENT_DATE = function() return os.date('%d') end,
CURRENT_DAY_NAME = function() return os.date('%A') end,
CURRENT_DAY_NAME_SHORT = function() return os.date('%a') end,
CURRENT_HOUR = function() return os.date('%H') end,
CURRENT_MINUTE = function() return os.date('%M') end,
CURRENT_SECOND = function() return os.date('%S') end,
CURRENT_SECONDS_UNIX = function() return os.time() end,
RANDOM = function() return string.format('%06d', math.random(999999)) end,
RANDOM_HEX = function() return string.format('%06x', math.random(0xFFFFFF)) end,
BLOCK_COMMENT_START = function(ctx) return (doc_syntax(ctx.doc, 'block_comment') or { })[1] end,
BLOCK_COMMENT_END = function(ctx) return (doc_syntax(ctx.doc, 'block_comment') or { })[2] end,
LINE_COMMENT = function(ctx) return doc_syntax(ctx.doc, 'comment') end
-- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
-- UUID
}
local formatters; formatters = {
downcase = string.lower,
upcase = string.upper,
capitalize = function(str)
return str:sub(1, 1):upper() .. str:sub(2)
end,
pascalcase = function(str)
local t = { }
for s in str:gmatch('%w+') do
table.insert(t, formatters.capitalize(s))
end
return table.concat(t)
end,
camelcase = function(str)
str = formatters.pascalcase(str)
return str:sub(1, 1):lower() .. str:sub(2)
end
}
local function to_text(v, _s)
return v.esc
end
local function format_fn(v, _s)
local id = tonumber(v[2])
-- $1 | ${1}
if #v < 4 then
return function(captures)
return captures[id] or ''
end
end
-- ${1:...}
local t = v[3][2][1] -- token after the ':' | (else when no token)
local i = v[3][2][2] -- formatter | if | (else when no if)
local e = v[3][2][4] -- (else when if)
if t == '/' then
local f = formatters[i]
return function(captures)
local c = captures[id]
return c and f(c) or ''
end
elseif t == '+' then
return function(captures)
return captures[id] and i or ''
end
elseif t == '?' then
return function(captures)
return captures[id] and i or e
end
elseif t == '-' then
return function(captures)
return captures[id] or i
end
else
return function(captures)
return captures[id] or t
end
end
end
local function transform_fn(v, _s)
local reg = regex.compile(v[2], v[#v])
local fmt = v[4]
if type(fmt) ~= 'table' then
return function(str)
return reg:gsub(str, '')
end
end
local t = { }
for _, f in ipairs(fmt) do
if type(f) == 'string' then
table.insert(t, f)
else
break
end
end
if #t == #fmt then
t = table.concat(t)
return function(str)
return reg:gsub(str, t)
end
end
return function(str)
local captures = { reg:match(str) }
for k, v in ipairs(captures) do
if type(v) ~= 'string' then
captures[k] = nil
end
end
local t = { }
for _, f in ipairs(fmt) do
if type(f) == 'string' then
table.insert(t, f)
else
table.insert(t, f(captures))
end
end
return table.concat(t)
end
end
local function text_node(v, _s)
return B.static(v.esc)
end
local function variable_node(v, _s)
local name = v[2]
local var = variables[name]
local id
if not var then
if not _s._converted_variables then
id = os.time()
_s._converted_variables = { [name] = id, [LAST_CONVERTED_ID] = id }
else
id = _s._converted_variables[name]
if not id then
id = _s._converted_variables[LAST_CONVERTED_ID] + 1
_s._converted_variables[name] = id
_s._converted_variables[LAST_CONVERTED_ID] = id
end
end
end
if #v ~= 4 then
return var and B.static(var) or B.user(id, name)
end
if type(v[3]) == 'table' then
-- vscode accepts empty default -> var name
return var and B.static(var) or B.user(id, v[3][2] or name)
end
if not var then
return B.user(id, nil, v[3])
end
return type(var) ~= 'function' and B.static(var) or B.static(function(ctx)
return v[3](var(ctx))
end)
end
local function tabstop_node(v, _s)
local t = v[3] and v[3] ~= '}' and v[3] or nil
return B.user(tonumber(v[2]), nil, t)
end
local function choice_node(v, _s)
local id = tonumber(v[2])
local c = { [v[4]] = true }
if #v == 6 then
for _, _c in ipairs(v[5]) do
c[_c[2]] = true
end
end
_s:choice(id, c)
return B.user(id)
end
local function placeholder_node(v, _s)
local id = tonumber(v[2])
_s:default(id, v[4])
return B.user(id)
end
local function build_snippet(v, _s)
for _, n in ipairs(v) do _s:add(n) end
return _s:ok()
end
-- parser metatable
local P do
local mt = {
__call = function(mt, parser, converter)
return setmetatable({ parser = parser, converter = converter }, mt)
end,
-- allows 'lazy arguments'
-- i.e can use a yet to be defined rule in a previous rule
__index = function(t, k)
return function(...) return t[k](...) end
end
}
P = setmetatable({
__call = function(t, str, at, _s)
local r = t.parser(str, at, _s)
if r.ok and t.converter then
r.value = t.converter(r.value, _s)
end
return r
end
}, mt)
end
-- utils
local function toset(t)
local r = { }
for _, v in pairs(t or { }) do
r[v] = true
end
return r
end
local function fail(at)
return { at = at }
end
local function ok(at, v)
return { ok = true, at = at, value = v }
end
-- base + combinators
local function token(t)
return function(str, at)
local to = at + #t
return t == str:sub(at, to - 1) and ok(to, t) or fail(at)
end
end
local function consume(stops, escapes)
stops, escapes = toset(stops), toset(escapes)
return function(str, at)
local to = at
local raw, esc = { }, { }
local c = str:sub(to, to)
while to <= #str and not stops[c] do
if c == '\\' then
table.insert(raw, c)
to = to + 1
c = str:sub(to, to)
if not stops[c] and not escapes[c] then
table.insert(esc, '\\')
end
end
table.insert(raw, c)
table.insert(esc, c)
to = to + 1
c = str:sub(to, to)
end
return to ~= at
and ok(to, { raw = table.concat(raw), esc = table.concat(esc) })
or fail(at)
end
end
local function pattern(p)
return function(str, at)
local r = str:match('^' .. p, at)
return r and ok(at + #r, r) or fail(at)
end
end
local function maybe(p)
return function(str, at, ...)
local r = p(str, at, ...)
return ok(r.at, r.value)
end
end
local function rep(p)
return function(str, at, ...)
local v, to, r = { }, at, ok(at)
while to <= #str and r.ok do
table.insert(v, r.value)
to = r.at
r = p(str, to, ...)
end
return #v > 0 and ok(to, v) or fail(at)
end
end
local function any(...)
local t = { ... }
return function(str, at, ...)
for _, p in ipairs(t) do
local r = p(str, at, ...)
if r.ok then return r end
end
return fail(at)
end
end
local function seq(...)
local t = { ... }
return function(str, at, ...)
local v, to = { }, at
for _, p in ipairs(t) do
local r = p(str, to, ...)
if r.ok then
table.insert(v, r.value)
to = r.at
else
return fail(at)
end
end
return ok(to, v)
end
end
-- grammar rules
-- token cache
local t = setmetatable({ },
{
__index = function(t, k)
local fn = token(k)
rawset(t, k, fn)
return fn
end
}
)
P.int = pattern('%d+')
P.var = pattern('[%a_][%w_]*')
-- '}' needs to be escaped in normal text (i.e #0)
local __text0 = consume({ '$' }, { '\\', '}' })
local __text1 = consume({ '}' }, { '\\' })
local __text2 = consume({ ':' }, { '\\' })
local __text3 = consume({ '/' }, { '\\' })
local __text4 = consume({ '$', '}' }, { '\\' })
local __text5 = consume({ ',', '|' }, { '\\' })
local __text6 = consume({ "$", "/" }, { "\\" })
P._if1 = P(__text1, to_text)
P._if2 = P(__text2, to_text)
P._else = P(__text1, to_text)
P.options = pattern('%l*')
P.regex = P(__text3, to_text)
P.format = P(any(
seq(t['$'], P.int),
seq(t['${'], P.int, maybe(seq(t[':'], any(
seq(t['/'], any(t['upcase'], t['downcase'], t['capitalize'], t['pascalcase'], t['camelcase'])),
seq(t['+'], P._if1),
seq(t['?'], P._if2, t[':'], P._else),
seq(t['-'], P._else),
P._else
))), t['}'])
), format_fn)
P.transform_text = P(__text6, to_text)
P.transform = P(
seq(t['/'], P.regex, t['/'], rep(any(P.format, P.transform_text)), t['/'], P.options),
transform_fn
)
P.variable_text = P(__text4, text_node)
P.variable = P(any(
seq(t['$'], P.var),
seq(t['${'], P.var, maybe(any(
-- grammar says a single mandatory 'any' for default, vscode seems to accept any*
seq(t[':'], maybe(rep(any(P.dollars, P.variable_text)))),
P.transform
)), t['}'])
), variable_node)
P.choice_text = P(__text5, to_text)
P.choice = P(
seq(t['${'], P.int, t['|'], P.choice_text, maybe(rep(seq(t[','], P.choice_text))), t['|}']),
choice_node
)
P.placeholder_text = P(__text4, text_node)
P.placeholder = P(
seq(t['${'], P.int, t[':'], maybe(rep(any(P.dollars, P.placeholder_text))), t['}']),
placeholder_node
)
P.tabstop = P(any(
seq(t['$'], P.int),
-- transform isnt specified in the grammar but seems to be supported by vscode
seq(t['${'], P.int, maybe(P.transform), t['}'])
), tabstop_node)
P.dollars = any(P.tabstop, P.placeholder, P.choice, P.variable)
P.text = P(__text0, text_node)
P.any = any(P.dollars, P.text)
P.snippet = P(rep(P.any), build_snippet)
-- JSON files
-- defined at the end of the file
local extensions
local fstate = { NOT_DONE = 'not done', QUEUED = 'queued', DONE = 'done' }
local queue = { }
local files = { }
local files2exts = { }
local exts2files = { }
local function parse_file(file)
if files[file] == fstate.DONE then return end
files[file] = fstate.DONE
local _f = io.open(file)
if not _f then
core.error('[LSP snippets] Could not open \'%s\'', file)
return
end
local ok, r = pcall(json.decode, _f:read('a'))
_f:close()
if not ok then
core.error('[LSP snippets] %s: %s', file, r:match('%d+:%s+(.*)'))
return false
end
local exts = file:match('%.json$') and files2exts[file]
for i, s in pairs(r) do
-- apparently body can be a single string
local template = type(s.body) == 'table'
and table.concat(s.body, '\n')
or s.body
if not template or template == '' then
core.warn('[LSP snippets] missing \'body\' for %s (%s)', i, file)
goto continue
end
-- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_language-snippet-scope
local scope
if not exts and s.scope then
local tmp = { }
for _, l in ipairs(s.scope) do
for _, e in ipairs(extensions[l:lower()]) do
tmp[e] = true
end
end
scope = { }
for l in pairs(tmp) do
table.insert(scope, l)
end
end
-- prefix may be an array
local triggers = type(s.prefix) ~= 'table' and { s.prefix } or s.prefix
if #triggers == 0 then
core.warn('[LSP snippets] missing \'prefix\' for %s (%s)', i, file)
goto continue
end
for _, t in ipairs(triggers) do
snippets.add {
trigger = t,
format = 'lsp',
files = exts or scope,
info = i,
desc = s.description,
template = template
}
end
::continue::
end
return true
end
local function pop()
while #queue > 0 do
repeat until parse_file(table.remove(queue)) ~= nil
if #queue > 0 then coroutine.yield() end
end
end
local function enqueue(filename)
if not core.threads[THREAD_KEY] then
core.add_thread(pop, THREAD_KEY)
end
files[filename] = fstate.QUEUED
table.insert(queue, filename)
end
local function add_file(filename, exts)
if files[filename] then return end
if filename:match('%.code%-snippets$') then
enqueue(filename)
return
end
if not filename:match('%.json$') then return end
if not exts then
local lang_name = filename:match('([^/%\\]*)%.%w*$'):lower()
exts = extensions[lang_name]
if not exts then return end
end
files[filename] = fstate.NOT_DONE
exts = type(exts) == 'string' and { exts } or exts
for _, e in ipairs(exts) do
files2exts[filename] = files2exts[filename] or { }
table.insert(files2exts[filename], '%.' .. e .. '$')
exts2files[e] = exts2files[e] or { }
table.insert(exts2files[e], filename)
end
end
local function for_filename(name)
if not name then return end
local ext = name:match('%.(.*)$')
if not ext then return end
local _files = exts2files[ext]
if not _files then return end
for _, f in ipairs(_files) do
if files[f] == fstate.NOT_DONE then
enqueue(f)
end
end
end
local doc_new = Doc.new
function Doc:new(filename, ...)
doc_new(self, filename, ...)
for_filename(filename)
end
local doc_set_filename = Doc.set_filename
function Doc:set_filename(filename, ...)
doc_set_filename(self, filename, ...)
for_filename(filename)
end
-- API
local M = { }
function M.parse(template)
local _s = B.new()
local r = P.snippet(template, 1, _s)
if not r.ok then
return B.new():s(template):ok()
elseif r.at == #template + 1 then
return r.value
else
return _s:s(template:sub(r.at + 1)):ok()
end
end
snippets.parsers.lsp = M.parse
local warned = false
function M.add_paths(paths)
if not json then
if not warned then
core.error(
'[LSP snippets] Could not add snippet file(s):' ..
'JSON plugin not found'
)
warned = true
end
return
end
paths = type(paths) ~= 'table' and { paths } or paths
for _, p in ipairs(paths) do
-- non absolute paths are treated as relative from USERDIR
p = not common.is_absolute_path(p) and (USERDIR .. PATHSEP .. p) or p
local finfo = system.get_file_info(p)
-- if path of a directory, add every file it contains and directories
-- whose name is that of a lang
if finfo and finfo.type == 'dir' then
for _, f in ipairs(system.list_dir(p)) do
f = p .. PATHSEP .. f
finfo = system.get_file_info(f)
if not finfo or finfo.type == 'file' then
add_file(f)
else
-- only if the directory's name matches a language
local lang_name = f:match('[^/%\\]*$'):lower()
local exts = extensions[lang_name]
for _, f2 in ipairs(system.list_dir(f)) do
f2 = f .. PATHSEP .. f2
finfo = system.get_file_info(f2)
if not finfo or finfo.type == 'file' then
add_file(f2, exts)
end
end
end
end
-- if path of a file, add the file
else
add_file(p)
end
end
end
-- arbitrarily cleaned up extension dump from https://gist.github.com/ppisarczyk/43962d06686722d26d176fad46879d41
-- nothing after this
-- 90% of these are still useless but cba
extensions = {
['ats'] = { 'dats', 'hats', 'sats', },
['ada'] = { 'adb', 'ada', 'ads', },
['agda'] = { 'agda', },
['asciidoc'] = { 'asciidoc', 'adoc', 'asc', },
['assembly'] = { 'asm', 'nasm', },
['autohotkey'] = { 'ahk', 'ahkl', },
['awk'] = { 'awk', 'auk', 'gawk', 'mawk', 'nawk', },
['batchfile'] = { 'bat', 'cmd', },
['c'] = { 'c', 'h', },
['c#'] = { 'cs', 'cake', 'cshtml', 'csx', },
['c++'] = { 'cpp', 'c++', 'cc', 'cp', 'cxx', 'h', 'h++', 'hh', 'hpp', 'hxx', },
['cmake'] = { 'cmake', 'cmake.in', },
['cobol'] = { 'cob', 'cbl', 'ccp', 'cobol', 'cpy', },
['css'] = { 'css', },
['clean'] = { 'icl', 'dcl', },
['clojure'] = { 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', },
['common lisp'] = { 'lisp', 'asd', 'cl', 'l', 'lsp', 'ny', 'podsl', 'sexp', },
['component pascal'] = { 'cp', 'cps', },
['coq'] = { 'coq', 'v', },
['crystal'] = { 'cr', },
['cuda'] = { 'cu', 'cuh', },
['d'] = { 'd', 'di', },
['dart'] = { 'dart', },
['dockerfile'] = { 'dockerfile', },
['eiffel'] = { 'e', },
['elixir'] = { 'ex', 'exs', },
['elm'] = { 'elm', },
['emacs lisp'] = { 'el', 'emacs', 'emacs.desktop', },
['erlang'] = { 'erl', 'es', 'escript', 'hrl', 'xrl', 'yrl', },
['f#'] = { 'fs', 'fsi', 'fsx', },
['fortran'] = { 'f90', 'f', 'f03', 'f08', 'f77', 'f95', 'for', 'fpp', },
['factor'] = { 'factor', },
['forth'] = { 'fth', '4th', 'f', 'for', 'forth', 'fr', 'frt', 'fs', },
['go'] = { 'go', },
['groff'] = { 'man', '1', '1in', '1m', '1x', '2', '3', '3in', '3m', '3qt', '3x', '4', '5', '6', '7', '8', '9', 'l', 'me', 'ms', 'n', 'rno', 'roff', },
['groovy'] = { 'groovy', 'grt', 'gtpl', 'gvy', },
['html'] = { 'html', 'htm', 'html.hl', 'xht', 'xhtml', },
['haskell'] = { 'hs', 'hsc', },
['idris'] = { 'idr', 'lidr', },
['jsx'] = { 'jsx', },
['java'] = { 'java', },
['javascript'] = { 'js', },
['julia'] = { 'jl', },
['jupyter notebook'] = { 'ipynb', },
['kotlin'] = { 'kt', 'ktm', 'kts', },
['lean'] = { 'lean', 'hlean', },
['less'] = { 'less', },
['lua'] = { 'lua', 'fcgi', 'nse', 'pd_lua', 'rbxs', 'wlua', },
['markdown'] = { 'md', 'markdown', 'mkd', 'mkdn', 'mkdown', 'ron', },
['modula-2'] = { 'mod', },
['moonscript'] = { 'moon', },
['ocaml'] = { 'ml', 'eliom', 'eliomi', 'ml4', 'mli', 'mll', 'mly', },
['objective-c'] = { 'm', 'h', },
['objective-c++'] = { 'mm', },
['oz'] = { 'oz', },
['php'] = { 'php', 'aw', 'ctp', 'fcgi', 'inc', 'php3', 'php4', 'php5', 'phps', 'phpt', },
['plsql'] = { 'pls', 'pck', 'pkb', 'pks', 'plb', 'plsql', 'sql', },
['plpgsql'] = { 'sql', },
['pascal'] = { 'pas', 'dfm', 'dpr', 'inc', 'lpr', 'pp', },
['perl'] = { 'pl', 'al', 'cgi', 'fcgi', 'perl', 'ph', 'plx', 'pm', 'pod', 'psgi', 't', },
['perl6'] = { '6pl', '6pm', 'nqp', 'p6', 'p6l', 'p6m', 'pl', 'pl6', 'pm', 'pm6', 't', },
['picolisp'] = { 'l', },
['pike'] = { 'pike', 'pmod', },
['pony'] = { 'pony', },
['postscript'] = { 'ps', 'eps', },
['powershell'] = { 'ps1', 'psd1', 'psm1', },
['prolog'] = { 'pl', 'pro', 'prolog', 'yap', },
['python'] = { 'py', 'bzl', 'cgi', 'fcgi', 'gyp', 'lmi', 'pyde', 'pyp', 'pyt', 'pyw', 'rpy', 'tac', 'wsgi', 'xpy', },
['racket'] = { 'rkt', 'rktd', 'rktl', 'scrbl', },
['rebol'] = { 'reb', 'r', 'r2', 'r3', 'rebol', },
['ruby'] = { 'rb', 'builder', 'fcgi', 'gemspec', 'god', 'irbrc', 'jbuilder', 'mspec', 'pluginspec', 'podspec', 'rabl', 'rake', 'rbuild', 'rbw', 'rbx', 'ru', 'ruby', 'thor', 'watchr', },
['rust'] = { 'rs', 'rs.in', },
['scss'] = { 'scss', },
['sql'] = { 'sql', 'cql', 'ddl', 'inc', 'prc', 'tab', 'udf', 'viw', },
['sqlpl'] = { 'sql', 'db2', },
['scala'] = { 'scala', 'sbt', 'sc', },
['scheme'] = { 'scm', 'sld', 'sls', 'sps', 'ss', },
['self'] = { 'self', },
['shell'] = { 'sh', 'bash', 'bats', 'cgi', 'command', 'fcgi', 'ksh', 'sh.in', 'tmux', 'tool', 'zsh', },
['smalltalk'] = { 'st', 'cs', },
['standard ml'] = { 'ML', 'fun', 'sig', 'sml', },
['swift'] = { 'swift', },
['tcl'] = { 'tcl', 'adp', 'tm', },
['tex'] = { 'tex', 'aux', 'bbx', 'bib', 'cbx', 'cls', 'dtx', 'ins', 'lbx', 'ltx', 'mkii', 'mkiv', 'mkvi', 'sty', 'toc', },
['typescript'] = { 'ts', 'tsx', },
['vala'] = { 'vala', 'vapi', },
['verilog'] = { 'v', 'veo', },
['visual basic'] = { 'vb', 'bas', 'cls', 'frm', 'frx', 'vba', 'vbhtml', 'vbs', },
}
extensions.cpp = extensions['c++']
extensions.csharp = extensions['c#']
extensions.latex = extensions.tex
extensions.objc = extensions['objective-c']
M.extensions = extensions
return M

View file

@ -0,0 +1,12 @@
-- mod-version:3
local lspconfig = require "plugins.lsp.config"
local common = require "core.common"
local config = require "core.config"
local installed_path = USERDIR .. PATHSEP .. "plugins" .. PATHSEP .. "lsp_yaml" .. PATHSEP .. "yaml-language-server" .. PATHSEP .. "dist" .. PATHSEP .. "index.js"
local node = require "libraries.nodejs"
lspconfig.yamlls.setup(common.merge({
command = { node.path_bin, installed_path, "--stdio" },
}, config.plugins.lsp_yaml or {}))

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Red Hat Inc. and others.
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.

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,633 @@
-- mod-version:3
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 DocView = require "core.docview"
local Highlighter = require "core.doc.highlighter"
local Object = require "core.object"
local Scrollbar = require "core.scrollbar"
-- Sample configurations:
-- full width:
-- config.plugins.minimap.highlight_width = 100
-- config.plugins.minimap.gutter_width = 0
-- left side:
-- config.plugins.minimap.highlight_align = 'left'
-- config.plugins.minimap.highlight_width = 3
-- config.plugins.minimap.gutter_width = 4
-- right side:
-- config.plugins.minimap.highlight_align = 'right'
-- config.plugins.minimap.highlight_width = 5
-- config.plugins.minimap.gutter_width = 0
-- General plugin settings
config.plugins.minimap = common.merge({
enabled = true,
width = 100,
instant_scroll = false,
syntax_highlight = true,
scale = 1,
-- number of spaces needed to split a token
spaces_to_split = 2,
-- hide on small docs (can be true, false or min number of lines)
avoid_small_docs = false,
-- how many spaces one tab is equivalent to
tab_width = 4,
draw_background = true,
-- you can override these colors
selection_color = nil,
caret_color = nil,
-- If other plugins provide per-line highlights,
-- this controls the placement. (e.g. gitdiff_highlight)
highlight_align = 'left',
highlight_width = 3,
gutter_width = 5,
-- The config specification used by the settings gui
config_spec = {
name = "Mini Map",
{
label = "Enabled",
description = "Activate the minimap by default.",
path = "enabled",
type = "toggle",
default = true
},
{
label = "Width",
description = "Width of the minimap in pixels.",
path = "width",
type = "number",
default = 100,
min = 50,
max = 1000
},
{
label = "Instant Scroll",
description = "When enabled disables the scrolling animation.",
path = "instant_scroll",
type = "toggle",
default = false
},
{
label = "Syntax Highlighting",
description = "Disable to improve performance.",
path = "syntax_highlight",
type = "toggle",
default = true
},
{
label = "Scale",
description = "Size of the minimap using a scaling factor.",
path = "scale",
type = "number",
default = 1,
min = 0.5,
max = 10,
step = 0.1
},
{
label = "Spaces to split",
description = "Number of spaces needed to split a token.",
path = "spaces_to_split",
type = "number",
default = 2,
min = 1
},
{
label = "Hide for small Docs",
description = "Hide the minimap when a Doc is small enough.",
path = "avoid_small_docs",
type = "toggle",
default = false
},
{
label = "Small Docs definition",
description = "Size of a Doc to be considered small. Use 0 to automatically decide.",
path = "avoid_small_docs_len",
type = "number",
default = 0,
min = 0,
on_apply = function(value)
if value == 0 then
config.plugins.minimap.avoid_small_docs = true
else
config.plugins.minimap.avoid_small_docs = value
end
end
},
{
label = "Tabs Width",
description = "The amount of spaces that represent a tab.",
path = "tab_width",
type = "number",
default = 4,
min = 1,
max = 8
},
{
label = "Draw Background",
description = "When disabled makes the minimap transparent.",
path = "draw_background",
type = "toggle",
default = true
},
{
label = "Selection Color",
description = "Background color of selected text.",
path = "selection_color",
type = "color",
default = string.format("#%02X%02X%02X%02X",
style.dim[1], style.dim[2], style.dim[3], style.dim[4]
)
},
{
label = "Caret Color",
description = "Background color of active line.",
path = "caret_color",
type = "color",
default = string.format("#%02X%02X%02X%02X",
style.caret[1], style.caret[2], style.caret[3], style.caret[4]
)
},
{
label = "Highlight Alignment",
path = "highlight_align",
type = "selection",
default = "left",
values = {
{"Left", "left"},
{"Right", "right"}
}
},
{
label = "Highlight Width",
path = "highlight_width",
type = "number",
default = 3,
min = 0,
max = 50
},
{
label = "Gutter Width",
description = "Left padding of the minimap.",
path = "gutter_width",
type = "number",
default = 5,
min = 0,
max = 50
},
}
}, config.plugins.minimap)
-- contains the settings values that require a cache reset if changed
local cached_settings = {
color_scheme_canary = nil,
syntax_highlight = nil,
spaces_to_split = nil,
scale = nil,
width = nil,
}
-- Configure size for rendering each char in the minimap
local char_spacing
local char_height
local line_spacing
-- cache for the location of the rects for each Doc
local highlighter_cache
local function reset_cache()
highlighter_cache = setmetatable({}, { __mode = "k" })
cached_settings = {
color_scheme_canary = style.syntax["normal"],
syntax_highlight = config.plugins.minimap.syntax_highlight,
spaces_to_split = config.plugins.minimap.spaces_to_split,
scale = config.plugins.minimap.scale,
width = config.plugins.minimap.width,
}
char_spacing = 0.8 * SCALE * config.plugins.minimap.scale
-- keep y aligned to pixels
char_height = math.max(1, math.floor(1 * SCALE * config.plugins.minimap.scale + 0.5))
line_spacing = math.max(1, math.floor(2 * SCALE * config.plugins.minimap.scale + 0.5))
end
reset_cache()
local function reset_cache_if_needed()
if
cached_settings.color_scheme_canary ~= style.syntax["normal"]
or cached_settings.syntax_highlight ~= config.plugins.minimap.syntax_highlight
or cached_settings.spaces_to_split ~= config.plugins.minimap.spaces_to_split
or cached_settings.scale ~= config.plugins.minimap.scale
or cached_settings.width ~= config.plugins.minimap.width
then
reset_cache()
end
end
-- Move cache to make space for new lines
local prev_insert_notify = Highlighter.insert_notify
function Highlighter:insert_notify(line, n, ...)
prev_insert_notify(self, line, n, ...)
local blanks = { }
if not highlighter_cache[self] then
highlighter_cache[self] = {}
else
local blanks = { }
for i = 1, n do
blanks[i] = false
end
common.splice(highlighter_cache[self], line, 0, blanks)
end
end
-- Close the cache gap created by removed lines
local prev_remove_notify = Highlighter.remove_notify
function Highlighter:remove_notify(line, n, ...)
prev_remove_notify(self, line, n, ...)
if not highlighter_cache[self] then
highlighter_cache[self] = {}
else
common.splice(highlighter_cache[self], line, n)
end
end
-- Remove changed lines from the cache
local prev_tokenize_line = Highlighter.tokenize_line
function Highlighter:tokenize_line(idx, state, ...)
local res = prev_tokenize_line(self, idx, state, ...)
if not highlighter_cache[self] then
highlighter_cache[self] = {}
end
highlighter_cache[self][idx] = false
return res
end
-- Ask the Highlighter to retokenize the lines we have in cache
local prev_invalidate = Highlighter.invalidate
function Highlighter:invalidate(idx, ...)
local cache = highlighter_cache[self]
if cache then
self.max_wanted_line = math.max(self.max_wanted_line, #cache)
end
return prev_invalidate(self, idx, ...)
end
-- Remove cache on Highlighter reset (for example on syntax change)
local prev_soft_reset = Highlighter.soft_reset
function Highlighter:soft_reset(...)
prev_soft_reset(self, ...)
highlighter_cache[self] = {}
end
local MiniMap = Scrollbar:extend()
function MiniMap:new(dv, original_v_scrollbar)
MiniMap.super.new(self, { direction = "v", alignment = "e",
force_status = "expanded",
expanded_size = cached_settings.width,
expanded_margin = 0 })
self.original_force_status = original_v_scrollbar.force_status
self.original_expanded_size = original_v_scrollbar.expanded_size
self.original_expanded_margin = original_v_scrollbar.expanded_margin
self.dv = dv
self.enabled = nil
self.was_enabled = true
end
function MiniMap:swap_to_status()
local enabled = self:is_minimap_enabled()
if not enabled and self.was_enabled then
self.force_status = self.original_force_status
self.expanded_size = self.original_expanded_size
self.expanded_margin = self.original_expanded_margin
self.was_enabled = false
elseif enabled and not self.was_enabled then
self.force_status = "expanded"
self.expanded_size = cached_settings.width
self.expanded_margin = 0
self.was_enabled = true
end
end
function MiniMap:update()
self:swap_to_status()
if self:is_minimap_enabled() then
reset_cache_if_needed()
self.expanded_size = cached_settings.width
local lh = self.dv:get_line_height()
local nlines = self.dv.size.y / lh
self.minimum_thumb_size = nlines * line_spacing
end
MiniMap.super.update(self)
end
function MiniMap:line_highlight_color(line_index, docview)
-- other plugins can override this, and return a color
end
function MiniMap:is_minimap_enabled()
if self.enabled ~= nil then return self.enabled end
if not config.plugins.minimap.enabled then return false end
if config.plugins.minimap.avoid_small_docs then
local last_line = #self.dv.doc.lines
if type(config.plugins.minimap.avoid_small_docs) == "number" then
return last_line > config.plugins.minimap.avoid_small_docs
else
local docview = self.dv
local _, y = docview:get_line_screen_position(last_line, #docview.doc.lines[last_line])
y = y + docview.scroll.y - docview.position.y + docview:get_line_height()
return y > docview.size.y
end
end
return true
end
function MiniMap:_on_mouse_pressed_normal(button, x, y, clicks)
local overlaps = self:_overlaps_normal(x, y)
local percent = MiniMap.super._on_mouse_pressed_normal(self, button, x, y, clicks)
if overlaps == "track" then
-- We need to adjust the percentage to scroll to the line in the minimap
-- that was "clicked"
local minimap_line, _ = self:get_minimap_lines()
local _, track_y, _, _ = self:_get_track_rect_normal()
local line = minimap_line + (y - track_y) // line_spacing
local _, y = self.dv:get_line_screen_position(line)
local _, oy = self.dv:get_content_offset()
local nr = self.normal_rect
percent = common.clamp((y - oy - (self.dv.size.y) / 2) / (nr.scrollable - self.dv.size.y), 0, 1)
end
return percent
end
local function get_visible_minline(dv)
local _, y, _, _ = dv:get_content_bounds()
local lh = dv:get_line_height()
local minline = math.max(0, y / lh + 1)
return minline
end
function MiniMap:get_minimap_lines()
local _, track_y, _, h = self:_get_track_rect_normal()
local _, thumb_y, _, _ = self:_get_thumb_rect_normal()
local nlines = h // line_spacing
local minline = get_visible_minline(self.dv)
local top_lines = (thumb_y - track_y) / line_spacing
local lines_start, offset = math.modf(minline - top_lines)
if lines_start <= 1 and nlines >= #self.dv.doc.lines then
offset = 0
end
return common.clamp(lines_start, 1, #self.dv.doc.lines), common.clamp(nlines, 1, #self.dv.doc.lines), offset * line_spacing
end
function MiniMap:set_size(x, y, w, h, scrollable)
if not self:is_minimap_enabled() then return MiniMap.super.set_size(self, x, y, w, h, scrollable) end
-- If possible, use the size needed to only manage the visible minimap lines.
-- This allows us to let Scrollbar manage the thumb.
h = math.min(h, line_spacing * (scrollable // self.dv:get_line_height()))
MiniMap.super.set_size(self, x, y, w, h, scrollable)
end
function MiniMap:draw()
if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end
local dv = self.dv
local x, y, w, h = self:get_track_rect()
local highlight = dv.hovered_scrollbar or dv.dragging_scrollbar
local visual_color = highlight and style.scrollbar2 or style.scrollbar
if config.plugins.minimap.draw_background then
renderer.draw_rect(x, y, w, self.dv.size.y, style.minimap_background or style.background)
end
self:draw_thumb()
local minimap_lines_start, minimap_lines_count, y_offset = self:get_minimap_lines()
local line_selection_offset = line_spacing - char_height
y = y - y_offset + line_selection_offset
-- highlight the selected lines, and the line with the caret on it
local selection_color = config.plugins.minimap.selection_color or style.dim
local caret_color = config.plugins.minimap.caret_color or style.caret
for _, line1, _, line2, _ in dv.doc:get_selections() do
local selection1_y = y + (line1 - minimap_lines_start) * line_spacing - line_selection_offset
local selection2_y = y + (line2 - minimap_lines_start) * line_spacing - line_selection_offset
local selection_min_y = math.min(selection1_y, selection2_y)
local selection_h = math.abs(selection2_y - selection1_y) + 1 + line_selection_offset
renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color)
renderer.draw_rect(x, selection1_y, w, line_spacing + line_selection_offset, caret_color)
end
local highlight_align = config.plugins.minimap.highlight_align
local highlight_width = config.plugins.minimap.highlight_width
local gutter_width = config.plugins.minimap.gutter_width
-- time to draw the actual code, setup some local vars that are used in both highlighted and plain rendering.
local line_y = y
-- when not using syntax highlighted rendering, just use the normal color but dim it 50%.
local color = style.syntax["normal"]
color = {color[1], color[2], color[3], color[4] * 0.5}
-- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each.
local batch_width = 0
local batch_start = x
local last_batch_end = -1
local minimap_cutoff_x = config.plugins.minimap.width * SCALE
local batch_syntax_type = nil
local function flush_batch(type, cache)
if batch_width > 0 then
local lastidx = #cache
local old_color = color
color = style.syntax[type]
if config.plugins.minimap.syntax_highlight and color ~= nil then
-- fetch and dim colors
color = {color[1], color[2], color[3], (color[4] or 255) * 0.5}
else
color = old_color
end
if #cache >= 3 then
local last_color = cache[lastidx]
if
last_batch_end == batch_start -- no space skipped
and (
batch_syntax_type == type -- and same syntax
or ( -- or same color
last_color[1] == color[1]
and last_color[2] == color[2]
and last_color[3] == color[3]
and last_color[4] == color[4]
)
)
then
batch_start = cache[lastidx - 2]
batch_width = cache[lastidx - 1] + batch_width
lastidx = lastidx - 3
end
end
cache[lastidx + 1] = batch_start
cache[lastidx + 2] = batch_width
cache[lastidx + 3] = color
end
batch_syntax_type = type
batch_start = batch_start + batch_width
last_batch_end = batch_start
batch_width = 0
end
local highlight_x
if highlight_align == 'left' then
highlight_x = x
else
highlight_x = x + w - highlight_width
end
local function render_highlight(idx, line_y)
local highlight_color = self:line_highlight_color(idx, self.dv)
if highlight_color then
renderer.draw_rect(highlight_x, line_y - line_selection_offset,
highlight_width, line_spacing + line_selection_offset, highlight_color)
end
end
local endidx = math.min(minimap_lines_start + minimap_lines_count, #self.dv.doc.lines)
if not highlighter_cache[dv.doc.highlighter] then
highlighter_cache[dv.doc.highlighter] = {}
end
-- per line
for idx = minimap_lines_start, endidx do
batch_syntax_type = nil
batch_start = 0
batch_width = 0
last_batch_end = -1
render_highlight(idx, line_y)
local cache = highlighter_cache[dv.doc.highlighter][idx]
if not highlighter_cache[dv.doc.highlighter][idx] then -- need to cache
highlighter_cache[dv.doc.highlighter][idx] = {}
cache = highlighter_cache[dv.doc.highlighter][idx]
-- per token
for _, type, text in dv.doc.highlighter:each_token(idx) do
if not config.plugins.minimap.syntax_highlight then
type = nil
end
local start = 1
while true do
-- find text followed spaces followed by newline
local s, e, w, eol = string.ufind(text, "[^%s]*()[ \t]*()\n?", start)
if not s then break end
local nchars = w - s
start = e + 1
batch_width = batch_width + char_spacing * nchars
local nspaces = 0
for i=w,e do
local whitespace = string.sub(text, i, i)
if whitespace == "\t" then
nspaces = nspaces + config.plugins.minimap.tab_width
elseif whitespace == " " then
nspaces = nspaces + 1
end
end
-- not enough spaces; consider them part of the batch
if nspaces < config.plugins.minimap.spaces_to_split then
batch_width = batch_width + nspaces * char_spacing
end
-- line has ended or no more space in the minimap;
-- we can go to the next line
if eol <= w or batch_start + batch_width > minimap_cutoff_x then
if batch_width > 0 then
flush_batch(type, cache)
end
break
end
-- enough spaces to split the batch
if nspaces >= config.plugins.minimap.spaces_to_split then
flush_batch(type, cache)
batch_start = batch_start + nspaces * char_spacing
end
end
end
end
-- draw from cache
for i=1,#cache,3 do
local batch_start = cache[i ] + x + gutter_width
local batch_width = cache[i + 1]
local color = cache[i + 2]
renderer.draw_rect(batch_start, line_y, batch_width, char_height, color)
end
line_y = line_y + line_spacing
end
end
local old_docview_new = DocView.new
function DocView:new(doc)
old_docview_new(self, doc)
if self:is(DocView) then
self.v_scrollbar = MiniMap(self, self.v_scrollbar)
end
end
local function get_all_docviews(node, t)
t = t or {}
if not node then return end
if node.type == "leaf" then
for i,v in ipairs(node.views) do
if v:is(DocView) then
table.insert(t, v)
end
end
end
get_all_docviews(node.a, t)
get_all_docviews(node.b, t)
return t
end
command.add(nil, {
["minimap:toggle-visibility"] = function()
config.plugins.minimap.enabled = not config.plugins.minimap.enabled
for i,v in ipairs(get_all_docviews(core.root_view.root_node)) do
v.v_scrollbar.enabled = nil
end
end,
["minimap:toggle-syntax-highlighting"] = function()
config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight
end
})
command.add("core.docview!", {
["minimap:toggle-visibility-for-current-view"] = function(dv)
local sb = dv.v_scrollbar
if sb.enabled ~= nil then
sb.enabled = not sb.enabled
else
sb.enabled = not config.plugins.minimap.enabled
end
end
})
return MiniMap

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff