aboutsummaryrefslogtreecommitdiff
path: root/plugins/editorconfig
diff options
context:
space:
mode:
authorJefferson González <jgmdev@gmail.com>2022-12-24 23:06:24 -0400
committerGitHub <noreply@github.com>2022-12-24 23:06:24 -0400
commit363c3eb859ddbe1efad5949ad087c8e7db6cee41 (patch)
treedf6ae14c1b360d710ce0d4b5c11950e91a2d0517 /plugins/editorconfig
parent7bb53600a33c2f630f2a0850a543c356a856f172 (diff)
downloadlite-xl-plugins-363c3eb859ddbe1efad5949ad087c8e7db6cee41.tar.gz
lite-xl-plugins-363c3eb859ddbe1efad5949ad087c8e7db6cee41.zip
added editorconfig plugin (#163)
* properly ignore comments on declarations * strip newlines when insert_final_newline set to false * force match up to the end * support unset property * make property values case insensitive * handle indent_size when set to tab * handle escaped comment chars * also lowercase property names * support utf-8 as on spec * apply rules to unsaved unamed/named docs * annotation fix * added test suite and fixes for 100% pass rate * do not force match from start, breaks 2 tests * allow starting wild cards to match everything * disabled print buffering * make last line visible if insert_final_newline true * use log_quiet for debug * adapted to changes on lite-xl/lite-xl#1232 * properly return from add/remove_project_directory overrides * Use new trimwhitespace functionality if possible
Diffstat (limited to 'plugins/editorconfig')
-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
28 files changed, 1997 insertions, 0 deletions
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