aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStunxFS <56417208+StunxFS@users.noreply.github.com>2022-12-31 20:33:25 -0400
committerGitHub <noreply@github.com>2022-12-31 20:33:25 -0400
commit975243113fa52cc1f9549526f0a48d8b98179b37 (patch)
treee5ca4bcfa0e37bdb0838ffc1ee44c5c2d4e01c0c
parentfadbb0aae2dcbc5a418647f7ba7397b6380854f3 (diff)
parentbf3a3b7b75efc54869df6f62363b5b94191a85cc (diff)
downloadlite-xl-plugins-975243113fa52cc1f9549526f0a48d8b98179b37.tar.gz
lite-xl-plugins-975243113fa52cc1f9549526f0a48d8b98179b37.zip
Merge branch 'lite-xl:master' into master
-rw-r--r--README.md23
-rw-r--r--plugins/centerdoc.lua3
-rw-r--r--plugins/custom_caret.lua156
-rw-r--r--plugins/editorconfig/README.md61
-rw-r--r--plugins/editorconfig/init.lua441
-rw-r--r--plugins/editorconfig/parser.lua553
-rw-r--r--plugins/editorconfig/runtest.lua63
-rw-r--r--plugins/editorconfig/tests/glob/braces.in71
-rw-r--r--plugins/editorconfig/tests/glob/brackets.in51
-rw-r--r--plugins/editorconfig/tests/glob/init.lua241
-rw-r--r--plugins/editorconfig/tests/glob/question.in7
-rw-r--r--plugins/editorconfig/tests/glob/star.in12
-rw-r--r--plugins/editorconfig/tests/glob/star_star.in15
-rw-r--r--plugins/editorconfig/tests/glob/utf8char.in6
-rw-r--r--plugins/editorconfig/tests/init.lua143
-rw-r--r--plugins/editorconfig/tests/parser/basic.in16
-rw-r--r--plugins/editorconfig/tests/parser/bom.in6
-rw-r--r--plugins/editorconfig/tests/parser/comments.in47
-rw-r--r--plugins/editorconfig/tests/parser/comments_and_newlines.in4
-rw-r--r--plugins/editorconfig/tests/parser/comments_only.in1
-rw-r--r--plugins/editorconfig/tests/parser/crlf.in6
-rw-r--r--plugins/editorconfig/tests/parser/empty.in0
-rw-r--r--plugins/editorconfig/tests/parser/init.lua107
-rw-r--r--plugins/editorconfig/tests/parser/limits.in13
-rw-r--r--plugins/editorconfig/tests/parser/newlines_only.in2
-rw-r--r--plugins/editorconfig/tests/parser/whitespace.in48
-rw-r--r--plugins/editorconfig/tests/properties/indent_size_default.in11
-rw-r--r--plugins/editorconfig/tests/properties/init.lua42
-rw-r--r--plugins/editorconfig/tests/properties/lowercase_names.in6
-rw-r--r--plugins/editorconfig/tests/properties/lowercase_values.in15
-rw-r--r--plugins/editorconfig/tests/properties/tab_width_default.in9
-rw-r--r--plugins/eval.lua21
-rw-r--r--plugins/fontconfig.lua26
-rw-r--r--plugins/ipc.lua114
-rw-r--r--plugins/language_go.lua37
-rw-r--r--plugins/language_php.lua9
-rw-r--r--plugins/language_rust.lua33
-rw-r--r--plugins/language_wren.lua17
-rw-r--r--plugins/lfautoinsert.lua2
-rw-r--r--plugins/minimap.lua2
-rw-r--r--plugins/profiler/README.md48
-rw-r--r--plugins/profiler/init.lua99
-rw-r--r--plugins/profiler/profiler.lua311
-rw-r--r--plugins/rainbowparen.lua8
-rw-r--r--plugins/regexreplacepreview.lua66
-rw-r--r--plugins/settings.lua218
-rw-r--r--plugins/tab_switcher.lua60
47 files changed, 3077 insertions, 173 deletions
diff --git a/README.md b/README.md
index 7cd5ab5..4c4fc56 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,28 @@
# Lite XL plugins
-Plugins for the [Lite XL text editor](https://github.com/lite-xl/lite-xl), originally forked from the [lite plugins repository](https://github.com/rxi/lite-plugins).
+Plugins for the [Lite XL text editor](https://github.com/lite-xl/lite-xl),
+originally forked from the [lite plugins repository](https://github.com/rxi/lite-plugins).
-If you can't find a plugin that suits your needs, check if someone has already created an issue about it, otherwise feel free to create one yourself.
+If you can't find a plugin that suits your needs, check if someone has already
+created an issue about it, otherwise feel free to create one yourself.
## How to install
To install a plugin:
* If the plugin links to a repository, follow its `README`.
-* If the plugin is a single file, drop it directly in:
+* If the plugin is a single file or directory, drop it directly in:
* Linux `~/.config/lite-xl/plugins/`
* MacOS `~/.config/lite-xl/plugins/`
* Windows `C:\Users\(username)\.config\lite-xl\plugins\`
-*Note: if you make a pull request, the table should be updated and kept in alphabetical order. If your plugin is large (or you'd otherwise prefer it to have its own repo), the table can simply be updated to add a link to the repo; otherwise the plugin file itself can be submitted. If a plugin's link resolves to something other than a raw file it should be marked with an asterisk.*
+*Note: if you make a pull request, the table should be updated and kept in
+alphabetical order. If your plugin is large (or you'd otherwise prefer it to
+have its own repo), the table can simply be updated to add a link to the
+repo; otherwise the plugin file itself can be submitted. If a plugin's link
+resolves to something other than a raw file it should be marked with an
+asterisk.*
***
@@ -34,13 +41,16 @@ To install a plugin:
| [`console`](https://github.com/franko/console)\* | A console for running external commands and capturing their output *([gif](https://user-images.githubusercontent.com/3920290/81343656-49325a00-90ad-11ea-8647-ff39d8f1d730.gif))* |
| [`contextmenu`](https://github.com/takase1121/lite-contextmenu)\* | Simple context menu *([screenshot](https://github.com/takase1121/lite-contextmenu/blob/master/assets/screenshot.jpg?raw=true))* |
| [`copyfilelocation`](plugins/copyfilelocation.lua?raw=1) | Copy file location to clipboard |
+| [`custom_caret`](plugins/custom_caret.lua?raw=1) | Customize the caret in the editor |
| [`datetimestamps`](plugins/datetimestamps.lua?raw=1) | Insert date-, time- and date-time-stamps |
| [`discord-presence`](https://github.com/vincens2005/lite-xl-discord)\* | Adds the current workspace and file to your Discord Rich Presence |
| [`dragdropselected`](plugins/dragdropselected.lua?raw=1) | Provides basic drag and drop of selected text (in same document) |
+| [`editorconfig`](plugins/editorconfig) | [EditorConfig](https://editorconfig.org/) implementation for Lite XL |
| [`eofnewline`](https://github.com/bokunodev/lite_modules/blob/master/plugins/eofnewline-xl.lua?raw=1) | Make sure the file ends with one blank line. |
| [`ephemeral_tabs`](plugins/ephemeral_tabs.lua?raw=1) | Preview tabs. Opening a doc will replace the contents of the preview tab. Marks tabs as non-preview on any change or tab double clicking. |
| [`equationgrapher`](https://github.com/ThaCuber/equationgrapher?raw=1)\* | Graphs y=x equations. |
| [`eval`](plugins/eval.lua?raw=1) | Replaces selected Lua code with its evaluated result |
+| [`evergreen`](https://github.com/TorchedSammy/Evergreen.lxl)\* | Adds Treesitter syntax highlighting support |
| [`exec`](plugins/exec.lua?raw=1) | Runs selected text through shell command and replaces with result |
| [`extend_selection_line`](plugins/extend_selection_line.lua?raw=1) | When a selection crosses multiple lines, it is drawn to the end of the screen *([screenshot](https://user-images.githubusercontent.com/2798487/140995616-89a20b55-5917-4df8-8a7c-d7c53732fa8b.png))* |
| [`exterm`](https://github.com/ShadiestGoat/lite-xl-exterm)\* | Allows to open an external console in current project directory |
@@ -49,6 +59,7 @@ To install a plugin:
| [`force_syntax`](plugins/force_syntax.lua?raw=1) | Change the syntax used for a file. |
| [`formatter`](https://github.com/vincens2005/lite-formatters)\* | formatters for various languages |
| [`ghmarkdown`](plugins/ghmarkdown.lua?raw=1) | Opens a preview of the current markdown file in a browser window *([screenshot](https://user-images.githubusercontent.com/3920290/82754898-f7394600-9dc7-11ea-8278-2305363ed372.png))* |
+| [`gitblame`](https://github.com/juliardi/lite-xl-gitblame)\* | Shows "git blame" information of a line *([screenshot](https://raw.githubusercontent.com/juliardi/lite-xl-gitblame/main/screenshot_1.png))* |
| [`gitdiff_highlight`](https://github.com/vincens2005/lite-xl-gitdiff-highlight)\* | highlight changed lines from git *([screenshot](https://raw.githubusercontent.com/vincens2005/lite-xl-gitdiff-highlight/master/screenshot.png))* |
| [`gitstatus`](plugins/gitstatus.lua?raw=1) | Displays git branch and insert/delete count in status bar *([screenshot](https://user-images.githubusercontent.com/3920290/81107223-bcea3080-8f0e-11ea-8fc7-d03173f42e33.png))* |
| [`gofmt`](plugins/gofmt.lua?raw=1) | Auto-formats the current go file, adds the missing imports and the missing return cases |
@@ -149,9 +160,10 @@ To install a plugin:
| [`openselected`](plugins/openselected.lua?raw=1) | Opens the selected filename or url |
| [`pdfview`](plugins/pdfview.lua?raw=1) | PDF preview for TeX files |
| [`primary_selection`](plugins/primary_selection.lua?raw=1) | Adds middle mouse click copy/paste (primary selection). To use this plugin, `xclip` must be installed. |
+| [`profiler`](plugins/profiler) | Adds the ability to profile lite-xl with the [lua-profiler](https://github.com/charlesmallah/lua-profiler) |
| [`rainbowparen`](plugins/rainbowparen.lua?raw=1) | Show nesting of parentheses with rainbow colours |
| [`regexreplacepreview`](plugins/regexreplacepreview.lua?raw=1) | Allows for you to write a regex and its replacement in one go, and live preview the results. |
-| [`restoretabs`](plugins/restoretabs.lua?raw=1) | Keep a list of recently closed tabs, and restore the tab in order on cntrl+shift+t. |
+| [`restoretabs`](plugins/restoretabs.lua?raw=1) | Keep a list of recently closed tabs, and restore the tab in order on ctrl+shift+t. |
| [`scalestatus`](plugins/scalestatus.lua?raw=1) | Displays current scale (zoom) in status view (depends on scale plugin) |
| [`select_colorscheme`](plugins/select_colorscheme.lua?raw=1) | Select a color theme, like VScode, Sublime Text.(plugin saves changes) |
| [`selectionhighlight`](plugins/selectionhighlight.lua?raw=1) | Highlights regions of code that match the current selection *([screenshot](https://user-images.githubusercontent.com/3920290/80710883-5f597c80-8ae7-11ea-97f0-76dfacc08439.png))* |
@@ -161,6 +173,7 @@ To install a plugin:
| [`sort`](plugins/sort.lua?raw=1) | Sorts selected lines alphabetically |
| [`spellcheck`](plugins/spellcheck.lua?raw=1) | Underlines misspelt words *([screenshot](https://user-images.githubusercontent.com/3920290/79923973-9caa7400-842e-11ea-85d4-7a196a91ca50.png))* *-- note: on Windows a [`words.txt`](https://github.com/dwyl/english-words/blob/master/words.txt) dictionary file must be placed beside the exe* |
| [`statusclock`](plugins/statusclock.lua?raw=1) | Displays the current date and time in the corner of the status view |
+| [`tab_switcher`](plugins/tab_switcher.lua?raw=1) | Switch between open tabs by searching by name |
| [`tabnumbers`](plugins/tabnumbers.lua?raw=1) | Displays tab numbers from 1–9 next to their names \*([screenshot](https://user-images.githubusercontent.com/16415678/101285362-007a8500-37e5-11eb-869b-c10eb9d9d902.png)) |
| [`texcompile`](plugins/texcompile.lua?raw=1) | Compile Tex files into PDF |
| [`theme16`](https://github.com/monolifed/theme16)\* | Theme manager with base16 themes |
diff --git a/plugins/centerdoc.lua b/plugins/centerdoc.lua
index 980cbed..824ef2a 100644
--- a/plugins/centerdoc.lua
+++ b/plugins/centerdoc.lua
@@ -3,6 +3,7 @@ local core = require "core"
local config = require "core.config"
local common = require "core.common"
local command = require "core.command"
+local style = require "core.style"
local keymap = require "core.keymap"
local treeview = require "plugins.treeview"
local DocView = require "core.docview"
@@ -22,7 +23,7 @@ function DocView:draw_line_gutter(line, x, y, width)
lh = draw_line_gutter(self, line, x, y, width)
else
local real_gutter_width = self:get_font():get_width(#self.doc.lines)
- local offset = self:get_gutter_width() - real_gutter_width * 2
+ local offset = self:get_gutter_width() - real_gutter_width * 2 - style.padding.x
lh = draw_line_gutter(self, line, x + offset, y, real_gutter_width)
end
return lh
diff --git a/plugins/custom_caret.lua b/plugins/custom_caret.lua
new file mode 100644
index 0000000..f0358cd
--- /dev/null
+++ b/plugins/custom_caret.lua
@@ -0,0 +1,156 @@
+-- mod-version:3
+
+--[[
+ Author: techie-guy
+
+ Plugin to customize the caret in the editor
+ Thanks to @Guldoman for the initial example on Discord
+
+ Features
+ Change the Color and Opacity of the caret
+ Change the Shape of the caret, available shapes are Line, Block, Underline
+
+ Customizing the Caret: (this can be changed from the .config/lite-xl/init.lua
+ file or from the settings menu plugin)
+ config.plugins.custom_caret.shape - Change the shape of the caret [string]
+ style.caret - Change the rgba color of the caret [table]
+
+ Example Config(in the .config/lite-xl/init.lua)
+ style.caret = {0, 255, 255, 150}
+ config.plugins.custom_caret.shape = "block"
+]]
+
+local core = require "core"
+local style = require "core.style"
+local common = require "core.common"
+local config = require "core.config"
+local DocView = require "core.docview"
+
+config.plugins.custom_caret = common.merge({
+ shape = "line",
+ custom_color = true,
+ color_r = style.caret[1],
+ color_g = style.caret[2],
+ color_b = style.caret[3],
+ opacity = style.caret[4]
+}, config.plugins.custom_caret)
+
+-- Reference to plugin config
+local conf = config.plugins.custom_caret
+
+-- Get real default caret color after everything is loaded up
+core.add_thread(function()
+ if
+ conf.color_r == 147 and conf.color_g == 221
+ and
+ conf.color_b == 250 and conf.opacity == 255
+ and
+ (
+ style.caret[1] ~= conf.color_r or style.caret[2] ~= conf.color_g
+ or
+ style.caret[3] ~= conf.color_b or style.caret[4] ~= conf.opacity
+ )
+ then
+ conf.color_r = style.caret[1]
+ conf.color_g = style.caret[2]
+ conf.color_b = style.caret[3]
+ conf.opacity = style.caret[4]
+ end
+
+ local settings_loaded, settings = pcall(require, "plugins.settings")
+ if settings_loaded then
+ conf.config_spec = {
+ name = "Custom Caret",
+ {
+ label = "Shape",
+ description = "The Shape of the cursor.",
+ path = "shape",
+ type = "selection",
+ default = "line",
+ values = {
+ {"Line", "line"},
+ {"Block", "block"},
+ {"Underline", "underline"}
+ }
+ },
+ {
+ label = "Custom Color",
+ description = "Use a custom color for the caret as specified below.",
+ path = "custom_color",
+ type = "toggle",
+ default = true
+ },
+ {
+ label = "Red Component of Color",
+ description = "The color consists of 3 components RGB, "
+ .. "This modifies the 'R' component of the caret's color",
+ path = "color_r",
+ type = "number",
+ min = 0,
+ max = 255,
+ default = style.caret[1],
+ step = 1,
+ },
+ {
+ label = "Green Component of Color",
+ description = "The color consists of 3 components RGB, "
+ .. "This modifies the 'G' component of the caret's color",
+ path = "color_g",
+ type = "number",
+ min = 0,
+ max = 255,
+ default = style.caret[2],
+ step = 1,
+ },
+ {
+ label = "Blue Component of Color",
+ description = "The color consists of 3 components RGB, "
+ .. "This modifies the 'B' component of the caret's color",
+ path = "color_b",
+ type = "number",
+ min = 0,
+ max = 255,
+ default = style.caret[3],
+ step = 1,
+ },
+ {
+ label = "Opacity of the Cursor",
+ description = "The Opacity of the caret",
+ path = "opacity",
+ type = "number",
+ min = 0,
+ max = 255,
+ default = style.caret[4],
+ step = 1,
+ },
+ }
+
+ ---@cast settings plugins.settings
+ settings.ui:enable_plugin("custom_caret")
+ end
+end)
+
+function DocView:draw_caret(x, y)
+ local caret_width = style.caret_width
+ local caret_height = self:get_line_height()
+ local current_caret_shape = conf.shape
+ local caret_color = conf.custom_color and {
+ conf.color_r,
+ conf.color_g,
+ conf.color_b,
+ conf.opacity
+ } or style.caret
+
+ if (current_caret_shape == "block") then
+ caret_width = math.ceil(self:get_font():get_width("a"))
+ elseif (current_caret_shape == "underline") then
+ caret_width = math.ceil(self:get_font():get_width("a"))
+ caret_height = style.caret_width*2
+ y = y+self:get_line_height()
+ else
+ caret_width = style.caret_width
+ caret_height = self:get_line_height()
+ end
+
+ renderer.draw_rect(x, y, caret_width, caret_height, caret_color)
+end
diff --git a/plugins/editorconfig/README.md b/plugins/editorconfig/README.md
new file mode 100644
index 0000000..c3abd51
--- /dev/null
+++ b/plugins/editorconfig/README.md
@@ -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
+```
diff --git a/plugins/editorconfig/init.lua b/plugins/editorconfig/init.lua
new file mode 100644
index 0000000..a3df02c
--- /dev/null
+++ b/plugins/editorconfig/init.lua
@@ -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
diff --git a/plugins/editorconfig/parser.lua b/plugins/editorconfig/parser.lua
new file mode 100644
index 0000000..b0ec689
--- /dev/null
+++ b/plugins/editorconfig/parser.lua
@@ -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
diff --git a/plugins/editorconfig/runtest.lua b/plugins/editorconfig/runtest.lua
new file mode 100644
index 0000000..85378cd
--- /dev/null
+++ b/plugins/editorconfig/runtest.lua
@@ -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
diff --git a/plugins/editorconfig/tests/glob/braces.in b/plugins/editorconfig/tests/glob/braces.in
new file mode 100644
index 0000000..0400aeb
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/braces.in
@@ -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
diff --git a/plugins/editorconfig/tests/glob/brackets.in b/plugins/editorconfig/tests/glob/brackets.in
new file mode 100644
index 0000000..f44def2
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/brackets.in
@@ -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
diff --git a/plugins/editorconfig/tests/glob/init.lua b/plugins/editorconfig/tests/glob/init.lua
new file mode 100644
index 0000000..f1214c3
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/init.lua
@@ -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]*$")
diff --git a/plugins/editorconfig/tests/glob/question.in b/plugins/editorconfig/tests/glob/question.in
new file mode 100644
index 0000000..e2af52a
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/question.in
@@ -0,0 +1,7 @@
+; test ?
+
+root=true
+
+[som?.c]
+key=value
+
diff --git a/plugins/editorconfig/tests/glob/star.in b/plugins/editorconfig/tests/glob/star.in
new file mode 100644
index 0000000..c7d874f
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/star.in
@@ -0,0 +1,12 @@
+; test *
+
+root=true
+
+[a*e.c]
+key=value
+
+[Bar/*]
+keyb=valueb
+
+[*]
+keyc=valuec
diff --git a/plugins/editorconfig/tests/glob/star_star.in b/plugins/editorconfig/tests/glob/star_star.in
new file mode 100644
index 0000000..c8f2c99
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/star_star.in
@@ -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
diff --git a/plugins/editorconfig/tests/glob/utf8char.in b/plugins/editorconfig/tests/glob/utf8char.in
new file mode 100644
index 0000000..6fe89b0
--- /dev/null
+++ b/plugins/editorconfig/tests/glob/utf8char.in
@@ -0,0 +1,6 @@
+; test EditorConfig files with UTF-8 characters larger than 127
+
+root = true
+
+[中文.txt]
+key = value
diff --git a/plugins/editorconfig/tests/init.lua b/plugins/editorconfig/tests/init.lua
new file mode 100644
index 0000000..654067b
--- /dev/null
+++ b/plugins/editorconfig/tests/init.lua
@@ -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
diff --git a/plugins/editorconfig/tests/parser/basic.in b/plugins/editorconfig/tests/parser/basic.in
new file mode 100644
index 0000000..3033b9a
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/basic.in
@@ -0,0 +1,16 @@
+[*.a]
+option1=value1
+
+; repeat section
+[*.a]
+option2=value2
+
+[*.b]
+option1 = a
+option2 = a
+
+[b.b]
+option2 = b
+
+[*.b]
+option1 = c
diff --git a/plugins/editorconfig/tests/parser/bom.in b/plugins/editorconfig/tests/parser/bom.in
new file mode 100644
index 0000000..8bde201
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/bom.in
@@ -0,0 +1,6 @@
+; test EditorConfig files with BOM
+
+root = true
+
+[*]
+key = value
diff --git a/plugins/editorconfig/tests/parser/comments.in b/plugins/editorconfig/tests/parser/comments.in
new file mode 100644
index 0000000..c49fba8
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/comments.in
@@ -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
diff --git a/plugins/editorconfig/tests/parser/comments_and_newlines.in b/plugins/editorconfig/tests/parser/comments_and_newlines.in
new file mode 100644
index 0000000..35fc023
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/comments_and_newlines.in
@@ -0,0 +1,4 @@
+
+# Just comments
+
+# ... and newlines
diff --git a/plugins/editorconfig/tests/parser/comments_only.in b/plugins/editorconfig/tests/parser/comments_only.in
new file mode 100644
index 0000000..9592ed2
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/comments_only.in
@@ -0,0 +1 @@
+# Just a comment, nothing else \ No newline at end of file
diff --git a/plugins/editorconfig/tests/parser/crlf.in b/plugins/editorconfig/tests/parser/crlf.in
new file mode 100644
index 0000000..ec582d2
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/crlf.in
@@ -0,0 +1,6 @@
+; test EditorConfig files with CRLF line separators
+
+root = true
+
+[*]
+key = value
diff --git a/plugins/editorconfig/tests/parser/empty.in b/plugins/editorconfig/tests/parser/empty.in
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/empty.in
diff --git a/plugins/editorconfig/tests/parser/init.lua b/plugins/editorconfig/tests/parser/init.lua
new file mode 100644
index 0000000..cf473e5
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/init.lua
@@ -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]*$")
diff --git a/plugins/editorconfig/tests/parser/limits.in b/plugins/editorconfig/tests/parser/limits.in
new file mode 100644
index 0000000..d768a8c
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/limits.in
@@ -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
diff --git a/plugins/editorconfig/tests/parser/newlines_only.in b/plugins/editorconfig/tests/parser/newlines_only.in
new file mode 100644
index 0000000..139597f
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/newlines_only.in
@@ -0,0 +1,2 @@
+
+
diff --git a/plugins/editorconfig/tests/parser/whitespace.in b/plugins/editorconfig/tests/parser/whitespace.in
new file mode 100644
index 0000000..d1f3c5f
--- /dev/null
+++ b/plugins/editorconfig/tests/parser/whitespace.in
@@ -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
diff --git a/plugins/editorconfig/tests/properties/indent_size_default.in b/plugins/editorconfig/tests/properties/indent_size_default.in
new file mode 100644
index 0000000..809fc3f
--- /dev/null
+++ b/plugins/editorconfig/tests/properties/indent_size_default.in
@@ -0,0 +1,11 @@
+root = true
+
+[test.c]
+indent_style = tab
+
+[test2.c]
+indent_style = space
+
+[test3.c]
+indent_style = tab
+tab_width = 2
diff --git a/plugins/editorconfig/tests/properties/init.lua b/plugins/editorconfig/tests/properties/init.lua
new file mode 100644
index 0000000..4ae22d0
--- /dev/null
+++ b/plugins/editorconfig/tests/properties/init.lua
@@ -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]*$")
diff --git a/plugins/editorconfig/tests/properties/lowercase_names.in b/plugins/editorconfig/tests/properties/lowercase_names.in
new file mode 100644
index 0000000..253ea8b
--- /dev/null
+++ b/plugins/editorconfig/tests/properties/lowercase_names.in
@@ -0,0 +1,6 @@
+; test that property names are lowercased
+
+root = true
+
+[test.c]
+TestProperty = testvalue
diff --git a/plugins/editorconfig/tests/properties/lowercase_values.in b/plugins/editorconfig/tests/properties/lowercase_values.in
new file mode 100644
index 0000000..1730bb2
--- /dev/null
+++ b/plugins/editorconfig/tests/properties/lowercase_values.in
@@ -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
diff --git a/plugins/editorconfig/tests/properties/tab_width_default.in b/plugins/editorconfig/tests/properties/tab_width_default.in
new file mode 100644
index 0000000..3084607
--- /dev/null
+++ b/plugins/editorconfig/tests/properties/tab_width_default.in
@@ -0,0 +1,9 @@
+root = true
+
+[test.c]
+indent_style = space
+indent_size = 4
+
+[test2.c]
+indent_style = tab
+indent_size = tab
diff --git a/plugins/eval.lua b/plugins/eval.lua
index 1f507fb..da6e9be 100644
--- a/plugins/eval.lua
+++ b/plugins/eval.lua
@@ -1,6 +1,8 @@
-- mod-version:3
local core = require "core"
local command = require "core.command"
+local contextmenu = require "plugins.contextmenu"
+local keymap = require "core.keymap"
local function eval(str)
@@ -29,4 +31,23 @@ command.add("core.docview", {
end
})
end,
+
+ ["eval:selected"] = function(dv)
+ if dv.doc:has_selection() then
+ local text = dv.doc:get_text(dv.doc:get_selection())
+ dv.doc:text_input(eval(text))
+ else
+ local line = dv.doc:get_selection()
+ local text = dv.doc.lines[line]
+ dv.doc:insert(line+1, 0, "= " .. eval(text) .. "\n")
+ end
+ end,
})
+
+
+contextmenu:register("core.docview", {
+ { text = "Evaluate Selected", command = "eval:selected" }
+})
+
+
+keymap.add { ["ctrl+alt+return"] = "eval:selected" }
diff --git a/plugins/fontconfig.lua b/plugins/fontconfig.lua
index 8b713b5..03a2f95 100644
--- a/plugins/fontconfig.lua
+++ b/plugins/fontconfig.lua
@@ -75,6 +75,24 @@ function M.load_blocking(font_name, font_size, font_opt)
return result
end
+function M.load_group_blocking(group, font_size, font_opt)
+ local fonts = {}
+ for i in ipairs(group) do
+ local co = coroutine.create(function()
+ return M.load(group[i], font_size, font_opt)
+ end)
+ local result
+ while coroutine.status(co) ~= "dead" do
+ local ok, err = coroutine.resume(co)
+ if not ok then error(err) end
+ result = err
+ end
+ table.insert(fonts, result)
+ end
+
+ return renderer.font.group(fonts)
+end
+
function M.use(spec)
core.add_thread(function()
for key, value in pairs(spec) do
@@ -87,7 +105,13 @@ end
-- I'll leave this here
function M.use_blocking(spec)
for key, value in pairs(spec) do
- style[key] = M.load_blocking(value.name, value.size, value)
+ local font
+ if value.group then
+ font = M.load_group_blocking(value.group, value.size, value)
+ else
+ font = M.load_blocking(value.name, value.size, value)
+ end
+ style[key] = font
end
end
diff --git a/plugins/ipc.lua b/plugins/ipc.lua
index 8a68fc2..0f9a172 100644
--- a/plugins/ipc.lua
+++ b/plugins/ipc.lua
@@ -18,7 +18,7 @@ local MESSAGE_EXPIRATION=3
---@class config.plugins.ipc
---@field single_instance boolean
----@field dirs_instance string
+---@field dirs_instance '"new"' | '"add"' | '"change"'
config.plugins.ipc = common.merge({
single_instance = true,
dirs_instance = "new",
@@ -59,99 +59,71 @@ config.plugins.ipc = common.merge({
---| '"signal"'
---@class plugins.ipc.message
+---Id of the message
---@field id string
+---The id of process that sent the message
---@field sender string
+---Name of the message
---@field name string
+---Type of message.
---@field type plugins.ipc.messagetype | string
+---List with id of the instance that should receive the message.
---@field destinations table<integer,string>
+---A list of named values sent to receivers.
---@field data table<string,any>
+---Time in seconds when the message was sent for automatic expiration purposes.
---@field timestamp number
+---Optional callback executed by the receiver when the message is read.
---@field on_read plugins.ipc.onmessageread
+---Optional callback executed when a reply to the message is received.
---@field on_reply plugins.ipc.onreply
+---The received replies for the message.
---@field replies plugins.ipc.reply[]
-local IPCMessage = {
- ---Id of the message
- id = "",
- ---The id of process that sent the message
- sender = "",
- ---Name of the message
- name = "",
- ---Type of message.
- type = "",
- ---List with id of the instance that should receive the message.
- destinations = {},
- ---A list of named values sent to receivers.
- data = {},
- ---Time in seconds when the message was sent for automatic expiration purposes.
- timestamp = 0,
- ---Optional callback executed by the receiver when the message is read.
- on_read = function(message) end,
- ---Optional callback executed when a reply to the message is received.
- on_reply = function(reply) end,
- ---The received replies for the message.
- replies = {}
-}
---@class plugins.ipc.reply
+---Id of the message
---@field id string
+---The id of process that sent the message
---@field sender string
+---The id of the replier
---@field replier string
+---A list of named values sent back to sender.
---@field data table<string,any>
+---Time in seconds when the reply was sent for automatic expiration purposes.
---@field timestamp number
+---Optional callback executed by the sender when the reply is read.
---@field on_read plugins.ipc.onreplyread
-local IPCReply = {
- ---Id of the message
- id = "",
- ---The id of process that sent the message
- sender = "",
- ---The id of the replier
- replier = "",
- ---A list of named values sent back to sender.
- data = {},
- ---Time in seconds when the reply was sent for automatic expiration purposes.
- timestamp = 0,
- ---Optional callback executed by the sender when the reply is read.
- on_read = function(reply) end
-}
---@class plugins.ipc.instance
+---Process id of the instance.
---@field id string
+---The position in which the instance was launched.
---@field position integer
+---Flag that indicates if this instance was the first started.
+---@field primary boolean
+---Indicates the last time this instance updated its session file.
---@field last_update integer
+---The messages been broadcasted.
---@field messages plugins.ipc.message[]
+---The replies been broadcasted.
---@field replies plugins.ipc.reply[]
+---Table of properties associated with the instance. (NOT IMPLEMENTED)
---@field properties table
-local IPCInstance = {
- ---Process id of the instance.
- id = "",
- ---The position in which the instance was launched.
- position = 0,
- ---Flag that indicates if this instance was the first started.
- primary = false,
- ---Indicates the last time this instance updated its session file.
- last_update = 0,
- ---The messages been broadcasted.
- messages = {},
- ---The replies been broadcasted.
- replies = {},
- ---Table of properties associated with the instance.
- properties = {}
-}
-
----@class core.ipc : core.object
----@field private id string
----@field private user_dir string
----@field private running boolean
----@field private file string
----@field private primary boolean
----@field private position integer
----@field private messages plugins.ipc.message[]
----@field private replies plugins.ipc.reply[]
----@field private listeners table<string,table<integer,plugins.ipc.onmessage>>
----@field private signals table<string,integer>
----@field private methods table<string,integer>
----@field private signal_definitions table<integer,string>
----@field private method_definitions table<integer,string>
+
+---@class plugins.ipc : core.object
+---@field protected id string
+---@field protected user_dir string
+---@field protected running boolean
+---@field protected file string
+---@field protected primary boolean
+---@field protected position integer
+---@field protected messages plugins.ipc.message[]
+---@field protected replies plugins.ipc.reply[]
+---@field protected listeners table<string,table<integer,plugins.ipc.onmessage>>
+---@field protected signals table<string,integer>
+---@field protected methods table<string,integer>
+---@field protected signal_definitions table<integer,string>
+---@field protected method_definitions table<integer,string>
local IPC = Object:extend()
---@class plugins.ipc.threads
@@ -171,7 +143,7 @@ local function add_thread(f)
end
---Updates the session file of an IPC object.
----@param self core.ipc
+---@param self plugins.ipc
local function update_file(self)
local file, errmsg = io.open(self.file, "w+")
@@ -817,11 +789,11 @@ function IPC:call_async(destinations, name, callback, ...)
end
---Main ipc session for current instance.
----@type core.ipc
+---@type plugins.ipc
local ipc = IPC()
---Get the IPC session for the running lite-xl instance.
----@return core.ipc
+---@return plugins.ipc
function IPC.current()
return ipc
end
diff --git a/plugins/language_go.lua b/plugins/language_go.lua
index 93db0e4..e262af0 100644
--- a/plugins/language_go.lua
+++ b/plugins/language_go.lua
@@ -193,3 +193,40 @@ syntax.add {
},
}
+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 = {}
+}
+
diff --git a/plugins/language_php.lua b/plugins/language_php.lua
index 8b3371a..e0dc078 100644
--- a/plugins/language_php.lua
+++ b/plugins/language_php.lua
@@ -79,10 +79,10 @@ local inline_variables = {
{ pattern = "{()%$[%a_][%w_]*()}",
type = { "keyword", "keyword2", "keyword" }
},
- { pattern = "%$[%a_][%w_]*()%[()%w*()%]",
+ { pattern = "%$[%a_][%w_]*()%[()[%w_]*()%]",
type = { "keyword2", "keyword", "string", "keyword" }
},
- { pattern = "%$[%a_][%w_]*()%->()%w+",
+ { pattern = "%$[%a_][%w_]*()%->()%a[%w_]*",
type = { "keyword2", "keyword", "symbol" }
},
{ pattern = "%$[%a_][%w_]*", type = "keyword2" },
@@ -335,13 +335,12 @@ end
syntax.add {
name = "PHP",
files = { "%.php$", "%.phtml" },
- comment = "//",
- block_comment = {"/*", "*/"},
+ block_comment = { "<!--", "-->" },
patterns = {
{
regex = {
"<\\?php\\s+",
- "(\\?>|(?=`{3}))" -- end if inside markdown code tags
+ "(?:\\?>|(?=`{3}))" -- end if inside markdown code tags
},
syntax = ".phps",
type = "keyword2"
diff --git a/plugins/language_rust.lua b/plugins/language_rust.lua
index 848e8b1..fa5479d 100644
--- a/plugins/language_rust.lua
+++ b/plugins/language_rust.lua
@@ -6,21 +6,22 @@ syntax.add {
files = { "%.rs$" },
comment = "//",
patterns = {
- { pattern = "//.-\n", type = "comment" },
- { pattern = { "/%*", "%*/" }, type = "comment" },
- { pattern = { 'r#"', '"#', '\\' }, type = "string" },
- { pattern = { '"', '"', '\\' }, type = "string" },
- { pattern = "'.'", type = "string" },
- { pattern = "'%a", type = "keyword2" },
- { pattern = "0[oO_][0-7]+", type = "number" },
- { pattern = "-?0x[%x_]+", type = "number" },
- { pattern = "-?%d+_%d", type = "number" },
- { pattern = "-?%d+[%d%.eE]*f?", type = "number" },
- { pattern = "-?%.?%d+f?", type = "number" },
- { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
- { pattern = "[%a_][%w_]*!%f[%[(]", type = "function" },
- { pattern = "[%a_][%w_]*%f[(]", type = "function" },
- { pattern = "[%a_][%w_]*", type = "symbol" },
+ { pattern = "//.-\n", type = "comment" },
+ { pattern = { "/%*", "%*/" }, type = "comment" },
+ { pattern = { 'r#"', '"#', '\\' }, type = "string" },
+ { pattern = { '"', '"', '\\' }, type = "string" },
+ { pattern = "'.'", type = "string" },
+ { pattern = "'%a", type = "keyword2" },
+ { pattern = "0[oO_][0-7]+", type = "number" },
+ { pattern = "-?0x[%x_]+", type = "number" },
+ { pattern = "-?%d+_%d", type = "number" },
+ { pattern = "-?%d+[%d%.eE]*f?", type = "number" },
+ { pattern = "-?%.?%d+f?", type = "number" },
+ { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
+ { regex = "[[:alpha:]_][\\w]*(?=\\s*<[\\w\\s,']+>\\s*\\()", type = "function" },
+ { pattern = "[%a_][%w_]*!%f[%[(]", type = "function" },
+ { pattern = "[%a_][%w_]*%f[(]", type = "function" },
+ { pattern = "[%a_][%w_]*", type = "symbol" },
},
symbols = {
["as"] = "keyword",
@@ -85,5 +86,3 @@ syntax.add {
["Result"] = "literal",
},
}
-
-
diff --git a/plugins/language_wren.lua b/plugins/language_wren.lua
index 2022dbf..593cd46 100644
--- a/plugins/language_wren.lua
+++ b/plugins/language_wren.lua
@@ -5,22 +5,31 @@ syntax.add {
name = "Wren",
files = { "%.wren$" },
comment = "//",
+ block_comment = {"/*", "*/"},
patterns = {
{ pattern = "//.-\n", type = "comment" },
{ pattern = { "/%*", "%*/" }, type = "comment" },
{ pattern = { '"', '"', '\\' }, type = "string" },
- { pattern = { "'", "'", '\\' }, type = "string" },
- { pattern = "-?%.?%d+", type = "number" },
+ { pattern = "%d+%.%d+[Ee]%d+", type = "number" },
+ { pattern = "%d+%.%d+", type = "number" },
+ { pattern = "%d+[Ee]%d+", type = "number" },
+ { pattern = "0x%x+", type = "number" },
+ { pattern = "%d+", type = "number" },
{ pattern = "%.%.%.?", type = "operator" },
{ pattern = "[<>!=]=", type = "operator" },
+ { pattern = "|[^|]+|%s+", type = "string" },
{ pattern = "[%+%-=/%*%^%%<>!~|&?:]", type = "operator" },
- { pattern = "[%a_][%w_]*%s*%f[(\"{]", type = "function" },
- { pattern = "[%a_][%w_]*", type = "symbol" },
+ { pattern = "_[%w_]*", type = "keyword2" },
+ { pattern = "%a[%w_]*()%s+()is()%s+%a[%w_]*%s*%f[{]", type = {"function", "normal", "keyword", "normal"}},
+ { pattern = "%a[%w_]*%s*=()%s*%f[(]", type = {"function", "normal"} },
+ { pattern = "%a[%w_]*()%s*%f[({]", type = {"function", "normal"} },
+ { pattern = "%a[%w_]+", type = "symbol" },
},
symbols = {
["break"] = "keyword",
["class"] = "keyword",
["construct"] = "keyword",
+ ["continue"] = "keyword",
["else"] = "keyword",
["for"] = "keyword",
["foreign"] = "keyword",
diff --git a/plugins/lfautoinsert.lua b/plugins/lfautoinsert.lua
index fa7294e..492f323 100644
--- a/plugins/lfautoinsert.lua
+++ b/plugins/lfautoinsert.lua
@@ -12,7 +12,7 @@ config.plugins.lfautoinsert = common.merge({ map = {
["=%s*\n"] = false,
[":%s*\n"] = false,
["->%s*\n"] = false,
- ["^%s*<([^/][^%s>]*)[^>]*>%s*\n"] = "</$TEXT>",
+ ["^%s*<([^/!][^%s>]*)[^>]*>%s*\n"] = "</$TEXT>",
["^%s*{{#([^/][^%s}]*)[^}]*}}%s*\n"] = "{{/$TEXT}}",
["/%*%s*\n"] = "*/",
["c/c++"] = {
diff --git a/plugins/minimap.lua b/plugins/minimap.lua
index 603e173..75eb272 100644
--- a/plugins/minimap.lua
+++ b/plugins/minimap.lua
@@ -326,7 +326,7 @@ function MiniMap:is_minimap_enabled()
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])
+ 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
diff --git a/plugins/profiler/README.md b/plugins/profiler/README.md
new file mode 100644
index 0000000..b7afad0
--- /dev/null
+++ b/plugins/profiler/README.md
@@ -0,0 +1,48 @@
+# Profiler Plugin
+
+Profiling is mainly the runtime analysis of a program performance by counting
+the calls and duration for the various routines executed thru the lifecycle
+of the application. For more information view the [wikipedia] article.
+
+This plugin adds the ability to profile function calls while running Lite XL,
+becoming easier to investigate performance related issues and pinpoint what
+could be causing them. It integrates the [lua-profiler] which provides
+the functionality we need.
+
+## Usage
+
+Open Lite XL and access the command palette by pressing `ctrl+shift+p` and
+search for `profiler`. The command `Profiler: Toggle` will be shown to let you
+start or stop the profiler. You should start the profiler before triggering
+the events that are causing any performance issues.
+
+![command](https://user-images.githubusercontent.com/1702572/202113672-6ba593d9-03be-4462-9e82-e3339cf2722f.png)
+
+> **Note:** Starting the profiler will make the editor slower since it is
+> now accumulating metrics about every function call. Do not worry, this is
+> expected and shouldn't affect the end result, just be patience because
+> everything will be slower.
+
+There may be some situations when you would like to enable the profiler
+early on the startup process so we provided a configuration option for that.
+Also the profiler output is saved to a log file for easy sharing, its default
+path is also configurable as shown below:
+
+![settings](https://user-images.githubusercontent.com/1702572/202113713-7e932b4f-3283-42e6-af92-a1aa9ad09bde.png)
+
+> **Note:** since the profiler is not part of the core, but a plugin, it will
+> only start accumulating metrics once the plugin is loaded. The `priority`
+> tag of the profiler plugin was set to `0` to make it one of the first
+> plugins to start.
+
+Once you have profiled enough you can execute the `Profiler: Toggle` command
+to stop it, the log will be automatically open with the collected metrics
+as shown below:
+
+![metrics](https://user-images.githubusercontent.com/1702572/202113736-ef8d550c-130e-4372-b66c-694ee5f4c5c0.png)
+
+You can send Lite XL developers the output of `profiler.log` so it can be
+more easily diagnosed what could be causing any issue.
+
+[wikipedia]: https://en.wikipedia.org/wiki/Profiling_(computer_programming)
+[lua-profiler]: https://github.com/charlesmallah/lua-profiler
diff --git a/plugins/profiler/init.lua b/plugins/profiler/init.lua
new file mode 100644
index 0000000..8b4782b
--- /dev/null
+++ b/plugins/profiler/init.lua
@@ -0,0 +1,99 @@
+-- mod-version:3 --priority:0
+
+local core = require "core"
+local common = require "core.common"
+local config = require "core.config"
+local command = require "core.command"
+local profiler = require "plugins.profiler.profiler"
+
+--Keep track of profiler status.
+local RUNNING = false
+--The profiler runs before the settings plugin, we need to manually load them.
+local SETTINGS_PATH = USERDIR .. PATHSEP .. "user_settings.lua"
+-- Default location to store the profiler results.
+local DEFAULT_LOG_PATH = USERDIR .. PATHSEP .. "profiler.log"
+
+config.plugins.profiler = common.merge({
+ enable_on_startup = false,
+ log_file = DEFAULT_LOG_PATH,
+ config_spec = {
+ name = "Profiler",
+ {
+ label = "Enable on Startup",
+ description = "Enable profiler early on plugin startup process.",
+ path = "enable_on_startup",
+ type = "toggle",
+ default = false
+ },
+ {
+ label = "Log Path",
+ description = "Path to the file that will contain the profiler logged data.",
+ path = "log_file",
+ type = "file",
+ default = DEFAULT_LOG_PATH,
+ filters = {"%.log$"}
+ }
+ }
+}, config.plugins.profiler)
+
+---@class plugins.profiler
+local Profiler = {}
+
+function Profiler.start()
+ if RUNNING then return end
+ profiler.start()
+ RUNNING = true
+end
+
+function Profiler.stop()
+ if RUNNING then
+ profiler.stop()
+ profiler.report(config.plugins.profiler.log_file)
+ RUNNING = false
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Run profiler at startup if enabled.
+--------------------------------------------------------------------------------
+if system.get_file_info(SETTINGS_PATH) then
+ local ok, t = pcall(dofile, SETTINGS_PATH)
+ if ok and t.config and t.config.plugins and t.config.plugins.profiler then
+ local options = t.config.plugins.profiler
+ local profiler_ref = config.plugins.profiler
+ profiler_ref.enable_on_startup = options.enable_on_startup or false
+ profiler_ref.log_file = options.log_file or DEFAULT_LOG_PATH
+ end
+end
+
+if config.plugins.profiler.enable_on_startup then
+ Profiler.start()
+end
+
+--------------------------------------------------------------------------------
+-- Override core.run to stop profiler before exit if running.
+--------------------------------------------------------------------------------
+local core_run = core.run
+function core.run(...)
+ core_run(...)
+ Profiler.stop()
+end
+
+--------------------------------------------------------------------------------
+-- Add a profiler toggle command.
+--------------------------------------------------------------------------------
+command.add(nil, {
+ ["profiler:toggle"] = function()
+ if RUNNING then
+ Profiler.stop()
+ core.log("Profiler: stopped")
+ core.root_view:open_doc(core.open_doc(config.plugins.profiler.log_file))
+ else
+ Profiler.start()
+ core.log("Profiler: started")
+ end
+ end
+})
+
+
+return Profiler
diff --git a/plugins/profiler/profiler.lua b/plugins/profiler/profiler.lua
new file mode 100644
index 0000000..ca18f24
--- /dev/null
+++ b/plugins/profiler/profiler.lua
@@ -0,0 +1,311 @@
+--[[
+@title lua-profiler
+@version 1.1
+@description Code profiling for Lua based code;
+The output is a report file (text) and optionally to a console or other logger.
+
+The initial reason for this project was to reduce misinterpretations of code profiling
+caused by the lengthy measurement time of the 'ProFi' profiler v1.3;
+and then to remove the self-profiler functions from the output report.
+
+The profiler code has been substantially rewritten to remove dependence to the 'OO'
+class definitions, and repetitions in code;
+thus this profiler has a smaller code footprint and reduced execution time up to ~900% faster.
+
+The second purpose was to allow slight customisation of the output report,
+which I have parametrised the output report and rewritten.
+
+Caveats: I didn't include an 'inspection' function that ProFi had, also the RAM
+output is gone. Please configure the profiler output in top of the code, particularly the
+location of the profiler source file (if not in the 'main' root source directory).
+
+@authors Charles Mallah
+@copyright (c) 2018-2020 Charles Mallah
+@license MIT license
+
+@sample Output will be generated like this, all output here is ordered by time (seconds):
+`> TOTAL TIME = 0.030000 s
+`--------------------------------------------------------------------------------------
+`| FILE : FUNCTION : LINE : TIME : % : # |
+`--------------------------------------------------------------------------------------
+`| map : new : 301 : 0.1330 : 52.2 : 2 |
+`| map : unpackTileLayer : 197 : 0.0970 : 38.0 : 36 |
+`| engine : loadAtlas : 512 : 0.0780 : 30.6 : 1 |
+`| map : init : 292 : 0.0780 : 30.6 : 1 |
+`| map : setTile : 38 : 0.0500 : 19.6 : 20963|
+`| engine : new : 157 : 0.0220 : 8.6 : 1 |
+`| map : unpackObjectLayer : 281 : 0.0190 : 7.5 : 2 |
+`--------------------------------------------------------------------------------------
+`| ui : sizeCharLimit : 328 : ~ : ~ : 2 |
+`| modules/profiler : stop : 192 : ~ : ~ : 1 |
+`| ui : sizeWidthToScreenWidthHalf : 301 : ~ : ~ : 4 |
+`| map : setRectGridTo : 255 : ~ : ~ : 7 |
+`| ui : sizeWidthToScreenWidth : 295 : ~ : ~ : 11 |
+`| character : warp : 32 : ~ : ~ : 15 |
+`| panels : Anon : 0 : ~ : ~ : 1 |
+`--------------------------------------------------------------------------------------
+
+The partition splits the notable code that is running the slowest, all other code is running
+too fast to determine anything specific, instead of displaying "0.0000" the script will tidy
+this up as "~". Table headers % and # refer to percentage total time, and function call count.
+
+@example Print a profile report of a code block
+`local profiler = require("profiler")
+`profiler.start()
+`-- Code block and/or called functions to profile --
+`profiler.stop()
+`profiler.report("profiler.log")
+
+@example Profile a code block and allow mirror print to a custom print function
+`local profiler = require("profiler")
+`function exampleConsolePrint()
+` -- Custom function in your code-base to print to file or console --
+`end
+`profiler.attachPrintFunction(exampleConsolePrint, true)
+`profiler.start()
+`-- Code block and/or called functions to profile --
+`profiler.stop()
+`profiler.report("profiler.log") -- exampleConsolePrint will now be called from this
+
+@example Override a configuration parameter programmatically; insert your override values into a
+new table using the matched key names:
+
+`local overrides = {
+` fW = 100, -- Change the file column to 100 characters (from 20)
+` fnW = 120, -- Change the function column to 120 characters (from 28)
+` }
+`profiler.configuration(overrides)
+]]
+
+--[[ Configuration ]]--
+
+local config = {
+ outputFile = "profiler.lua", -- Name of this profiler (to remove itself from reports)
+ emptyToThis = "~", -- Rows with no time are set to this value
+ fW = 60, -- Width of the file column
+ fnW = 30, -- Width of the function name column
+ lW = 7, -- Width of the line column
+ tW = 7, -- Width of the time taken column
+ rW = 6, -- Width of the relative percentage column
+ cW = 5, -- Width of the call count column
+ reportSaved = "> Report saved to: ", -- Text for the file output confirmation
+}
+
+--[[ Locals ]]--
+
+local module = {}
+local getTime = os.clock
+local string, debug, table = string, debug, table
+local reportCache = {}
+local allReports = {}
+local reportCount = 0
+local startTime = 0
+local stopTime = 0
+local printFun = nil
+local verbosePrint = false
+
+local outputHeader, formatHeader, outputTitle, formatOutput, formatTotalTime
+local formatFunLine, formatFunTime, formatFunRelative, formatFunCount, divider, nilTime
+
+local function deepCopy(input)
+ if type(input) == "table" then
+ local output = {}
+ for i, o in next, input, nil do
+ output[deepCopy(i)] = deepCopy(o)
+ end
+ return output
+ else
+ return input
+ end
+end
+
+local function charRepetition(n, character)
+ local s = ""
+ character = character or " "
+ for _ = 1, n do
+ s = s..character
+ end
+ return s
+end
+
+local function singleSearchReturn(inputString, search)
+ for _ in string.gmatch(inputString, search) do -- luacheck: ignore
+ return true
+ end
+ return false
+end
+
+local function rebuildColumnPatterns()
+ local c = config
+ local str = "s: %-"
+ outputHeader = "| %-"..c.fW..str..c.fnW..str..c.lW..str..c.tW..str..c.rW..str..c.cW.."s|\n"
+ formatHeader = string.format(outputHeader, "FILE", "FUNCTION", "LINE", "TIME", "%", "#")
+ outputTitle = "%-"..c.fW.."."..c.fW..str..c.fnW.."."..c.fnW..str..c.lW.."s"
+ formatOutput = "| %s: %-"..c.tW..str..c.rW..str..c.cW.."s|\n"
+ formatTotalTime = "Total time: %f s\n"
+ formatFunLine = "%"..(c.lW - 2).."i"
+ formatFunTime = "%04.4f"
+ formatFunRelative = "%03.1f"
+ formatFunCount = "%"..(c.cW - 1).."i"
+ divider = charRepetition(#formatHeader - 1, "-").."\n"
+ -- nilTime = "0."..charRepetition(c.tW - 3, "0")
+ nilTime = "0.0000"
+end
+
+local function functionReport(information)
+ local src = information.short_src
+ if not src then
+ src = "<C>"
+ elseif string.sub(src, #src - 3, #src) == ".lua" then
+ src = string.sub(src, 1, #src - 4)
+ end
+ local name = information.name
+ if not name then
+ name = "Anon"
+ elseif string.sub(name, #name - 1, #name) == "_l" then
+ name = string.sub(name, 1, #name - 2)
+ end
+ local title = string.format(outputTitle, src, name,
+ string.format(formatFunLine, information.linedefined or 0))
+ local report = reportCache[title]
+ if not report then
+ report = {
+ title = string.format(outputTitle, src, name,
+ string.format(formatFunLine, information.linedefined or 0)),
+ count = 0, timer = 0,
+ }
+ reportCache[title] = report
+ reportCount = reportCount + 1
+ allReports[reportCount] = report
+ end
+ return report
+end
+
+local onDebugHook = function(hookType)
+ local information = debug.getinfo(2, "nS")
+ if hookType == "call" then
+ local funcReport = functionReport(information)
+ funcReport.callTime = getTime()
+ funcReport.count = funcReport.count + 1
+ elseif hookType == "return" then
+ local funcReport = functionReport(information)
+ if funcReport.callTime and funcReport.count > 0 then
+ funcReport.timer = funcReport.timer + (getTime() - funcReport.callTime)
+ end
+ end
+end
+
+--[[ Functions ]]--
+
+--[[Attach a print function to the profiler, to receive a single string parameter
+@param fn (function) <required>
+@param verbose (boolean) <default: false>
+]]
+function module.attachPrintFunction(fn, verbose)
+ printFun = fn
+ verbosePrint = verbose or false
+end
+
+--[[Start the profiling
+]]
+function module.start()
+ if not outputHeader then
+ rebuildColumnPatterns()
+ end
+ reportCache = {}
+ allReports = {}
+ reportCount = 0
+ startTime = getTime()
+ stopTime = nil
+ debug.sethook(onDebugHook, "cr", 0)
+end
+
+--[[Stop profiling
+]]
+function module.stop()
+ stopTime = getTime()
+ debug.sethook()
+end
+
+--[[Writes the profile report to file (will stop profiling if not stopped already)
+@param filename (string) <default: "profiler.log"> [File will be created and overwritten]
+]]
+function module.report(filename)
+ if not stopTime then
+ module.stop()
+ end
+ filename = filename or "profiler.log"
+ table.sort(allReports, function(a, b) return a.timer > b.timer end)
+ local fileWriter = io.open(filename, "w+")
+ local divide = false
+ local totalTime = stopTime - startTime
+ local totalTimeOutput = "> "..string.format(formatTotalTime, totalTime)
+ fileWriter:write(totalTimeOutput)
+ if printFun ~= nil then
+ printFun(totalTimeOutput)
+ end
+ fileWriter:write(divider)
+ fileWriter:write(formatHeader)
+ fileWriter:write(divider)
+ for i = 1, reportCount do
+ local funcReport = allReports[i]
+ if funcReport.count > 0 and funcReport.timer <= totalTime then
+ local printThis = true
+ if config.outputFile ~= "" then
+ if singleSearchReturn(funcReport.title, config.outputFile) then
+ printThis = false
+ end
+ end
+ if printThis then -- Remove lines that are not needed
+ if singleSearchReturn(funcReport.title, "[[C]]") then
+ printThis = false
+ end
+ end
+ if printThis then
+ local count = string.format(formatFunCount, funcReport.count)
+ local timer = string.format(formatFunTime, funcReport.timer)
+ local relTime = string.format(formatFunRelative, (funcReport.timer / totalTime) * 100)
+ if not divide and timer == nilTime then
+ fileWriter:write(divider)
+ divide = true
+ end
+ if timer == nilTime then
+ timer = config.emptyToThis
+ relTime = config.emptyToThis
+ end
+ -- Build final line
+ local output = string.format(formatOutput, funcReport.title, timer, relTime, count)
+ fileWriter:write(output)
+ -- This is a verbose print to the attached print function
+ if printFun ~= nil and verbosePrint then
+ printFun(output)
+ end
+ end
+ end
+ end
+ fileWriter:write(divider)
+ fileWriter:close()
+ if printFun ~= nil then
+ printFun(config.reportSaved.."'"..filename.."'")
+ end
+end
+
+--[[Modify the configuration of this module programmatically;
+Provide a table with keys that share the same name as the configuration parameters:
+@param overrides (table) <required> [Each key is from a valid name, the value is the override]
+@unpack config
+]]
+function module.configuration(overrides)
+ local safe = deepCopy(overrides)
+ for k, v in pairs(safe) do
+ if config[k] == nil then
+ print("error: override field '"..k.."' not found (configuration)")
+ else
+ config[k] = v
+ end
+ end
+ rebuildColumnPatterns()
+end
+
+--[[ End ]]--
+return module
diff --git a/plugins/rainbowparen.lua b/plugins/rainbowparen.lua
index 6ca4cb4..075667e 100644
--- a/plugins/rainbowparen.lua
+++ b/plugins/rainbowparen.lua
@@ -20,6 +20,7 @@ style.syntax.paren4 = style.syntax.paren4 or { common.color "#52dab2"}
style.syntax.paren5 = style.syntax.paren5 or { common.color "#5a98cf"}
local tokenize = tokenizer.tokenize
+local extract_subsyntaxes = tokenizer.extract_subsyntaxes
local closers = {
["("] = ")",
["["] = "]",
@@ -30,6 +31,13 @@ local function parenstyle(parenstack)
return "paren" .. ((#parenstack % config.plugins.rainbowparen.parens) + 1)
end
+function tokenizer.extract_subsyntaxes(base_syntax, state)
+ if not config.plugins.rainbowparen.enabled then
+ return extract_subsyntaxes(base_syntax, state)
+ end
+ return extract_subsyntaxes(base_syntax, state.istate)
+end
+
function tokenizer.tokenize(syntax, text, state)
if not config.plugins.rainbowparen.enabled then
return tokenize(syntax, text, state)
diff --git a/plugins/regexreplacepreview.lua b/plugins/regexreplacepreview.lua
index 747e9ba..7675d54 100644
--- a/plugins/regexreplacepreview.lua
+++ b/plugins/regexreplacepreview.lua
@@ -3,6 +3,68 @@ local core = require "core"
local keymap = require "core.keymap"
local command = require "core.command"
+-- Compatibility with latest lite-xl regex changes.
+local regex_match = regex.find_offsets or regex.match
+
+-- Will iterate back through any UTF-8 bytes so that we don't replace bits
+-- mid character.
+local function previous_character(str, index)
+ local byte
+ repeat
+ index = index - 1
+ byte = string.byte(str, index)
+ until byte < 128 or byte >= 192
+ return index
+end
+
+-- Moves to the end of the identified character.
+local function end_character(str, index)
+ local byte = string.byte(str, index + 1)
+ while byte and byte >= 128 and byte < 192 do
+ index = index + 1
+ byte = string.byte(str, index + 1)
+ end
+ return index
+end
+
+-- Build off matching. For now, only support basic replacements, but capture
+-- groupings should be doable. We can even have custom group replacements and
+-- transformations and stuff in lua. Currently, this takes group replacements
+-- as \1 - \9.
+-- Should work on UTF-8 text.
+local function substitute(pattern_string, str, replacement)
+ local pattern = type(pattern_string) == "table" and
+ pattern_string or regex.compile(pattern_string)
+ local result, indices = {}
+ local matches, replacements = {}, {}
+ local offset = 0
+ repeat
+ indices = { regex.cmatch(pattern, str, offset) }
+ if #indices > 0 then
+ table.insert(matches, indices)
+ local currentReplacement = replacement
+ if #indices > 2 then
+ for i = 1, (#indices/2 - 1) do
+ currentReplacement = string.gsub(
+ currentReplacement,
+ "\\" .. i,
+ str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1))
+ )
+ end
+ end
+ currentReplacement = string.gsub(currentReplacement, "\\%d", "")
+ table.insert(replacements, { indices[1], #currentReplacement+indices[1] })
+ if indices[1] > 1 then
+ table.insert(result, str:sub(offset, previous_character(str, indices[1])) .. currentReplacement)
+ else
+ table.insert(result, currentReplacement)
+ end
+ offset = indices[2]
+ end
+ until #indices == 0 or indices[1] == indices[2]
+ return table.concat(result) .. str:sub(offset), matches, replacements
+end
+
-- Workaround for bug in Lite XL 2.1
-- Remove this when b029f5993edb7dee5ccd2ba55faac1ec22e24609 is in a release
local function get_selection(doc, sort)
@@ -64,7 +126,7 @@ local function regex_replace_file(view, pattern, old_lines, raw, start_line, end
local old_text = old_lines[i] or doc.lines[i]
local old_length = #old_text
if replacement then
- new_text, matches, rmatches = regex.gsub(re, old_text, replacement)
+ new_text, matches, rmatches = substitute(re, old_text, replacement)
end
if matches and #matches > 0 then
old_lines[i] = old_text
@@ -78,7 +140,7 @@ local function regex_replace_file(view, pattern, old_lines, raw, start_line, end
old_lines[i] = nil
end
if not replacement then
- local s,e = regex.match(re, old_text)
+ local s,e = regex_match(re, old_text)
if s then
line_scroll = i
doc:set_selection(i, s, i, e)
diff --git a/plugins/settings.lua b/plugins/settings.lua
index 6d55c14..17ce998 100644
--- a/plugins/settings.lua
+++ b/plugins/settings.lua
@@ -5,6 +5,8 @@ local common = require "core.common"
local command = require "core.command"
local keymap = require "core.keymap"
local style = require "core.style"
+local View = require "core.view"
+local DocView = require "core.docview"
-- check if widget is installed before proceeding
local widget_found, Widget = pcall(require, "widget")
@@ -28,7 +30,9 @@ local ItemsList = require "widget.itemslist"
local KeybindingDialog = require "widget.keybinddialog"
local Fonts = require "widget.fonts"
local FilePicker = require "widget.filepicker"
+local MessageBox = require "widget.messagebox"
+---@class plugins.settings
local settings = {}
settings.core = {}
@@ -64,63 +68,43 @@ settings.type = {
---Represents a setting to render on a settings pane.
---@class settings.option
+---Title displayed to the user eg: "My Option"
---@field public label string
+---Description of the option eg: "Modifies the document indentation"
---@field public description string
+---Config path in the config table, eg: section.myoption, myoption, etc...
---@field public path string
+---Type of option that will be used to render an appropriate control
---@field public type settings.types | integer
+---Default value of the option
---@field public default string | number | boolean | table<integer, string> | table<integer, integer>
+---Used for NUMBER to indicate the minimum number allowed
---@field public min number
+---Used for NUMBER to indiciate the maximum number allowed
---@field public max number
+---Used for NUMBER to indiciate the increment/decrement amount
---@field public step number
+---Used in a SELECTION to provide the list of valid options
---@field public values table
+---Optionally used for FONT to store the generated font group.
---@field public fonts_list table<string, renderer.font>
+---Flag set to true when loading user defined fonts fail
---@field public font_error boolean
+---Optional function that is used to manipulate the current value on retrieval.
---@field public get_value nil | fun(value:any):any
+---Optional function that is used to manipulate the saved value on save.
---@field public set_value nil | fun(value:any):any
+---The icon set for a BUTTON
---@field public icon string
+---Command or function executed when a BUTTON is clicked
---@field public on_click nil | string | fun(button:string, x:integer, y:integer)
+---Optional function executed when the option value is applied.
---@field public on_apply nil | fun(value:any)
+---When FILE or DIRECTORY this flag tells the path should exist.
---@field public exists boolean
+---Lua patterns used on FILE or DIRECTORY to filter browser results and
+---also force the selection to match one of the filters.
---@field public filters table<integer,string>
-settings.option = {
- ---Title displayed to the user eg: "My Option"
- label = "",
- ---Description of the option eg: "Modifies the document indentation"
- description = "",
- ---Config path in the config table, eg: section.myoption, myoption, etc...
- path = "",
- ---Type of option that will be used to render an appropriate control
- type = "",
- ---Default value of the option
- default = "",
- ---Used for NUMBER to indiciate the minimum number allowed
- min = 0,
- ---Used for NUMBER to indiciate the maximum number allowed
- max = 0,
- ---Used for NUMBER to indiciate the increment/decrement amount
- step = 0,
- ---Used in a SELECTION to provide the list of valid options
- values = {},
- ---Optionally used for FONT to store the generated font group.
- fonts_list = {},
- ---Flag set to true when loading user defined fonts fail
- font_error = false,
- ---Optional function that is used to manipulate the current value on retrieval.
- get_value = nil,
- ---Optional function that is used to manipulate the saved value on save.
- set_value = nil,
- ---The icon set for a BUTTON
- icon = "",
- ---Command or function executed when a BUTTON is clicked
- on_click = nil,
- ---Optional function executed when the option value is applied.
- on_apply = nil,
- ---When FILE or DIRECTORY this flag tells the path should exist.
- exists = false,
- ---Lua patterns used on FILE or DIRECTORY to filter browser results and
- ---also force the selection to match one of the filters.
- filters = {}
-}
---Add a new settings section to the settings UI
---@param section string
@@ -181,7 +165,29 @@ settings.add("General",
type = settings.type.BUTTON,
icon = "C",
on_click = function()
- Fonts.clean_cache()
+ if Fonts.cache_is_building() then
+ MessageBox.warning(
+ "Clear Fonts Cache",
+ { "The font cache is already been built,\n"
+ .. "status will be logged on the core log."
+ }
+ )
+ else
+ MessageBox.info(
+ "Clear Fonts Cache",
+ { "Re-building the font cache can take some time,\n"
+ .. "it is needed when you have installed new fonts\n"
+ .. "which are not listed on the font picker tool.\n\n"
+ .. "Do you want to continue?"
+ },
+ function(_, button_id, _)
+ if button_id == 1 then
+ Fonts.clean_cache()
+ end
+ end,
+ MessageBox.BUTTONS_YES_NO
+ )
+ end
end
},
{
@@ -192,10 +198,8 @@ settings.add("General",
default = 2000,
min = 1,
max = 100000,
- on_apply = function(button, x, y)
- if button == "left" then
- core.rescan_project_directories()
- end
+ on_apply = function()
+ core.rescan_project_directories()
end
},
{
@@ -212,7 +216,16 @@ settings.add("General",
description = "List of lua patterns matching files to be ignored by the editor.",
path = "ignore_files",
type = settings.type.LIST_STRINGS,
- default = { "^%." },
+ default = {
+ -- folders
+ "^%.svn/", "^%.git/", "^%.hg/", "^CVS/", "^%.Trash/", "^%.Trash%-.*/",
+ "^node_modules/", "^%.cache/", "^__pycache__/",
+ -- files
+ "%.pyc$", "%.pyo$", "%.exe$", "%.dll$", "%.obj$", "%.o$",
+ "%.a$", "%.lib$", "%.so$", "%.dylib$", "%.ncb$", "%.sdf$",
+ "%.suo$", "%.pdb$", "%.idb$", "%.class$", "%.psd$", "%.db$",
+ "^desktop%.ini$", "^%.DS_Store$", "^%.directory$",
+ },
on_apply = function()
core.rescan_project_directories()
end
@@ -386,6 +399,56 @@ settings.add("User Interface",
end
},
{
+ label = "Force Scrollbar Status",
+ description = "Choose a fixed scrollbar state instead of resizing it on mouse hover.",
+ path = "force_scrollbar_status",
+ type = settings.type.SELECTION,
+ default = false,
+ values = {
+ {"Disabled", false},
+ {"Expanded", "expanded"},
+ {"Contracted", "contracted"}
+ },
+ on_apply = function(value)
+ local mode = config.force_scrollbar_status_mode or "global"
+ local globally = mode == "global"
+ local views = core.root_view.root_node:get_children()
+ for _, view in ipairs(views) do
+ if globally or view:extends(DocView) then
+ view.h_scrollbar:set_forced_status(value)
+ view.v_scrollbar:set_forced_status(value)
+ else
+ view.h_scrollbar:set_forced_status(false)
+ view.v_scrollbar:set_forced_status(false)
+ end
+ end
+ end
+ },
+ {
+ label = "Force Scrollbar Status Mode",
+ description = "Choose between applying globally or document views only.",
+ path = "force_scrollbar_status_mode",
+ type = settings.type.SELECTION,
+ default = "global",
+ values = {
+ {"Documents", "docview"},
+ {"Globally", "global"}
+ },
+ on_apply = function(value)
+ local globally = value == "global"
+ local views = core.root_view.root_node:get_children()
+ for _, view in ipairs(views) do
+ if globally or view:extends(DocView) then
+ view.h_scrollbar:set_forced_status(config.force_scrollbar_status)
+ view.v_scrollbar:set_forced_status(config.force_scrollbar_status)
+ else
+ view.h_scrollbar:set_forced_status(false)
+ view.v_scrollbar:set_forced_status(false)
+ end
+ end
+ end
+ },
+ {
label = "Disable Cursor Blinking",
description = "Disables cursor blinking on text input elements.",
path = "disable_blink",
@@ -903,6 +966,28 @@ local function merge_font_settings(option, path, saved_value)
end
end
+---Load the user_settings.lua stored options for a plugin into global config.
+---@param plugin_name string
+---@param options settings.option[]
+local function merge_plugin_settings(plugin_name, options)
+ for _, option in pairs(options) do
+ if type(option.path) == "string" then
+ local path = "plugins." .. plugin_name .. "." .. option.path
+ local saved_value = get_config_value(settings.config, path)
+ if type(saved_value) ~= "nil" then
+ if option.type == settings.type.FONT or option.type == "font" then
+ merge_font_settings(option, path, saved_value)
+ else
+ set_config_value(config, path, saved_value)
+ end
+ if option.on_apply then
+ option.on_apply(saved_value)
+ end
+ end
+ end
+ end
+end
+
---Merge previously saved settings without destroying the config table.
local function merge_settings()
if type(settings.config) ~= "table" then return end
@@ -932,22 +1017,7 @@ local function merge_settings()
for _, section in ipairs(settings.plugin_sections) do
local plugins = settings.plugins[section]
for plugin_name, options in pairs(plugins) do
- for _, option in pairs(options) do
- if type(option.path) == "string" then
- local path = "plugins." .. plugin_name .. "." .. option.path
- local saved_value = get_config_value(settings.config, path)
- if type(saved_value) ~= "nil" then
- if option.type == settings.type.FONT or option.type == "font" then
- merge_font_settings(option, path, saved_value)
- else
- set_config_value(config, path, saved_value)
- end
- if option.on_apply then
- option.on_apply(saved_value)
- end
- end
- end
- end
+ merge_plugin_settings(plugin_name, options)
end
end
@@ -1255,7 +1325,7 @@ function Settings:load_core_settings()
for _, section in ipairs(settings.sections) do
local options = settings.core[section]
- ---@type widget|widget.foldingbook.pane
+ ---@type widget|widget.foldingbook.pane|nil
local pane = self.core_sections:get_pane(section)
if not pane then
pane = self.core_sections:add_pane(section, section)
@@ -1379,7 +1449,7 @@ function Settings:enable_plugin(plugin)
for plugin_name, options in pairs(plugins) do
if plugin_name == plugin then
- ---@type widget
+ ---@type widget|widget.foldingbook.pane|nil
local pane = self.plugin_sections:get_pane(section)
if not pane then
pane = self.plugin_sections:add_pane(section, section)
@@ -1387,6 +1457,8 @@ function Settings:enable_plugin(plugin)
pane = pane.container
end
+ merge_plugin_settings(plugin, options)
+
for _, opt in ipairs(options) do
---@type settings.option
local option = opt
@@ -1417,7 +1489,7 @@ end
---Generate all the widgets for plugin settings.
function Settings:load_plugin_settings()
- ---@type widget
+ ---@type widget|widget.foldingbook.pane|nil
local pane = self.plugin_sections:get_pane("enable_disable")
if not pane then
pane = self.plugin_sections:add_pane("enable_disable", "Installed")
@@ -1474,7 +1546,7 @@ function Settings:load_plugin_settings()
local plugins = settings.plugins[section]
for plugin_name, options in pairs(plugins) do
- ---@type widget
+ ---@type widget|widget.foldingbook.pane|nil
local pane = self.plugin_sections:get_pane(section)
if not pane then
pane = self.plugin_sections:add_pane(section, section)
@@ -1861,5 +1933,21 @@ if config.plugins.toolbarview ~= false then
end
end
+--------------------------------------------------------------------------------
+-- Overwrite View:new to allow setting force scrollbar status globally
+--------------------------------------------------------------------------------
+local view_new = View.new
+function View:new()
+ view_new(self)
+ local mode = config.force_scrollbar_status_mode or "global"
+ local globally = mode == "global"
+ if globally then
+ --This is delayed to allow widgets to also apply it to child views/widgets
+ core.add_thread(function()
+ self.v_scrollbar:set_forced_status(config.force_scrollbar_status)
+ self.h_scrollbar:set_forced_status(config.force_scrollbar_status)
+ end)
+ end
+end
return settings;
diff --git a/plugins/tab_switcher.lua b/plugins/tab_switcher.lua
new file mode 100644
index 0000000..3d55eed
--- /dev/null
+++ b/plugins/tab_switcher.lua
@@ -0,0 +1,60 @@
+-- mod-version:3
+local core = require "core"
+local command = require "core.command"
+local keymap = require "core.keymap"
+local common = require "core.common"
+local DocView = require "core.docview"
+
+local tab_switcher = {}
+function tab_switcher.get_tab_list(base_node)
+ local raw_list = base_node:get_children()
+ local list = {}
+ local mt = {
+ -- fuzzy_match uses tostring to get the text to compare
+ __tostring = function(i) return i.text end
+ }
+ for _,v in pairs(raw_list) do
+ if v:is(DocView) then
+ table.insert(list, setmetatable({
+ text = v:get_name(),
+ view = v
+ }, mt))
+ end
+ end
+ return list
+end
+
+local function ask_selection(label, items)
+ if #items == 0 then
+ core.warn("No tabs available")
+ return
+ end
+ core.command_view:enter(label, {
+ submit = function(_, item)
+ local n = core.root_view.root_node:get_node_for_view(item.view)
+ if n then n:set_active_view(item.view) end
+ end,
+ suggest = function(text)
+ return common.fuzzy_match(items, text, true)
+ end,
+ validate = function(_, item)
+ return item
+ end
+ })
+end
+
+command.add(nil,{
+ ["tab-switcher:tab-list"] = function()
+ ask_selection("Switch to tab", tab_switcher.get_tab_list(core.root_view.root_node))
+ end,
+ ["tab-switcher:tab-list-current-split"] = function()
+ ask_selection("Switch to tab in current split", tab_switcher.get_tab_list(core.root_view:get_active_node()))
+ end
+})
+
+keymap.add({
+ ["alt+p"] = "tab-switcher:tab-list",
+ ["alt+shift+p"] = "tab-switcher:tab-list-current-split"
+})
+
+return tab_switcher