aboutsummaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
authorGregory Anders <8965202+gpanders@users.noreply.github.com>2023-01-03 11:38:20 -0700
committerGitHub <noreply@github.com>2023-01-03 11:38:20 -0700
commitd56c603caf348ba97b8428c9fcb812f1182e07e9 (patch)
tree1d2b5e74ab33acb5eb9fc04934fea5a2f829feba /runtime
parenteb702273c4e862305c72b2b9354f7c3a32447135 (diff)
parentc951236d638af242626ffc1bc23df5a57549c0f5 (diff)
downloadrneovim-d56c603caf348ba97b8428c9fcb812f1182e07e9.tar.gz
rneovim-d56c603caf348ba97b8428c9fcb812f1182e07e9.tar.bz2
rneovim-d56c603caf348ba97b8428c9fcb812f1182e07e9.zip
Merge pull request #21633 from gpanders/editorconfig
Builtin EditorConfig support
Diffstat (limited to 'runtime')
-rw-r--r--runtime/doc/editorconfig.txt89
-rw-r--r--runtime/doc/news.txt7
-rw-r--r--runtime/lua/editorconfig.lua246
-rw-r--r--runtime/lua/vim/filetype.lua2
-rw-r--r--runtime/plugin/editorconfig.lua11
-rw-r--r--runtime/syntax/editorconfig.vim18
6 files changed, 372 insertions, 1 deletions
diff --git a/runtime/doc/editorconfig.txt b/runtime/doc/editorconfig.txt
new file mode 100644
index 0000000000..e93713e5ff
--- /dev/null
+++ b/runtime/doc/editorconfig.txt
@@ -0,0 +1,89 @@
+*editorconfig.txt* Nvim
+
+
+ NVIM REFERENCE MANUAL
+
+
+EditorConfig integration *editorconfig*
+
+Nvim natively supports EditorConfig. When a file is opened, Nvim searches
+upward through all of the parent directories of that file looking for
+".editorconfig" files. Each of these is parsed and any properties that match
+the opened file are applied.
+
+For more information on EditorConfig, see https://editorconfig.org/.
+
+ *g:editorconfig_enable*
+EditorConfig integration can be disabled by adding >lua
+
+ vim.g.editorconfig_enable = false
+<
+to the user's |init.lua| file (or the Vimscript equivalent to |init.vim|).
+
+ *b:editorconfig*
+When Nvim finds a valid .editorconfig file it will store the applied
+properties in the buffer variable |b:editorconfig|.
+
+ *editorconfig-properties*
+The following properties are supported by default:
+
+ *editorconfig_root*
+root If "true", then stop searching for .editorconfig files
+ in parent directories. This property must be at the
+ top-level of the .editorconfig file (i.e. it must not
+ be within a glob section).
+
+ *editorconfig_charset*
+charset One of "utf-8", "utf-8-bom", "latin1", "utf-16be", or
+ "utf-16le". Sets the 'fileencoding' and 'bomb'
+ options.
+
+ *editorconfig_end_of_line*
+end_of_line One of "lf", "crlf", or "cr". These correspond to
+ setting 'fileformat' to "unix", "dos", or "mac",
+ respectively.
+
+ *editorconfig_indent_style*
+indent_style One of "tab" or "space". Sets the 'expandtab' option.
+
+ *editorconfig_indent_size*
+indent_size A number indicating the size of a single indent.
+ Alternatively, use the value "tab" to use the value of
+ the tab_width property. Sets the 'shiftwidth' and
+ 'softtabstop'.
+
+ *editorconfig_insert_final_newline*
+insert_final_newline "true" or "false" to ensure the file always has a
+ trailing newline as its last byte. Sets the
+ 'fixendofline' and 'endofline' options.
+
+ *editorconfig_max_line_length*
+max_line_length A number indicating the maximum length of a single
+ line. Sets the 'textwidth' option.
+
+ *editorconfig_tab_width*
+tab_width The display size of a single tab character. Sets the
+ 'tabstop' option.
+
+ *editorconfig_trim_trailing_whitespace*
+trim_trailing_whitespace
+ When "true", trailing whitespace is automatically
+ removed when the buffer is written.
+
+ *editorconfig-custom-properties*
+New properties can be added by adding a new entry to the "properties" table.
+The table key is a property name and the value is a callback function which
+accepts the number of the buffer to be modified, the value of the property
+in the .editorconfig file, and (optionally) a table containing all of the
+other properties and their values (useful for properties which depend on other
+properties). The value is always a string and must be coerced if necessary.
+Example: >lua
+
+ require('editorconfig').properties.foo = function(bufnr, val, opts)
+ if opts.charset and opts.charset ~= "utf-8" then
+ error("foo can only be set when charset is utf-8", 0)
+ end
+ vim.b[bufnr].foo = val
+ end
+<
+ vim:tw=78:ts=8:noet:ft=help:norl:
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index ffc8ce2c88..03bbf1567b 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -50,6 +50,13 @@ NEW FEATURES *news-features*
The following new APIs or features were added.
+• EditorConfig support is now builtin. This is enabled by default and happens
+ automatically. To disable it, users should add >lua
+
+ vim.g.editorconfig_enable = false
+<
+ (or the Vimscript equivalent) to their |config| file.
+
• Added a |vim.lsp.codelens.clear()| function to clear codelenses.
• |vim.inspect_pos()|, |vim.show_pos()| and |:Inspect| allow a user to get or show items
diff --git a/runtime/lua/editorconfig.lua b/runtime/lua/editorconfig.lua
new file mode 100644
index 0000000000..2079006234
--- /dev/null
+++ b/runtime/lua/editorconfig.lua
@@ -0,0 +1,246 @@
+local M = {}
+
+M.properties = {}
+
+--- Modified version of the builtin assert that does not include error position information
+---
+---@param v any Condition
+---@param message string Error message to display if condition is false or nil
+---@return any v if not false or nil, otherwise an error is displayed
+---
+---@private
+local function assert(v, message)
+ return v or error(message, 0)
+end
+
+--- Show a warning message
+---
+---@param msg string Message to show
+---
+---@private
+local function warn(msg, ...)
+ vim.notify(string.format(msg, ...), vim.log.levels.WARN, {
+ title = 'editorconfig',
+ })
+end
+
+function M.properties.charset(bufnr, val)
+ assert(
+ vim.tbl_contains({ 'utf-8', 'utf-8-bom', 'latin1', 'utf-16be', 'utf-16le' }, val),
+ 'charset must be one of "utf-8", "utf-8-bom", "latin1", "utf-16be", or "utf-16le"'
+ )
+ if val == 'utf-8' or val == 'utf-8-bom' then
+ vim.bo[bufnr].fileencoding = 'utf-8'
+ vim.bo[bufnr].bomb = val == 'utf-8-bom'
+ elseif val == 'utf-16be' then
+ vim.bo[bufnr].fileencoding = 'utf-16'
+ else
+ vim.bo[bufnr].fileencoding = val
+ end
+end
+
+function M.properties.end_of_line(bufnr, val)
+ vim.bo[bufnr].fileformat = assert(
+ ({ lf = 'unix', crlf = 'dos', cr = 'mac' })[val],
+ 'end_of_line must be one of "lf", "crlf", or "cr"'
+ )
+end
+
+function M.properties.indent_style(bufnr, val, opts)
+ assert(val == 'tab' or val == 'space', 'indent_style must be either "tab" or "space"')
+ vim.bo[bufnr].expandtab = val == 'space'
+ if val == 'tab' and not opts.indent_size then
+ vim.bo[bufnr].shiftwidth = 0
+ vim.bo[bufnr].softtabstop = 0
+ end
+end
+
+function M.properties.indent_size(bufnr, val, opts)
+ if val == 'tab' then
+ vim.bo[bufnr].shiftwidth = 0
+ vim.bo[bufnr].softtabstop = 0
+ else
+ local n = assert(tonumber(val), 'indent_size must be a number')
+ vim.bo[bufnr].shiftwidth = n
+ vim.bo[bufnr].softtabstop = -1
+ if not opts.tab_width then
+ vim.bo[bufnr].tabstop = n
+ end
+ end
+end
+
+function M.properties.tab_width(bufnr, val)
+ vim.bo[bufnr].tabstop = assert(tonumber(val), 'tab_width must be a number')
+end
+
+function M.properties.max_line_length(bufnr, val)
+ local n = tonumber(val)
+ if n then
+ vim.bo[bufnr].textwidth = n
+ else
+ assert(val == 'off', 'max_line_length must be a number or "off"')
+ vim.bo[bufnr].textwidth = 0
+ end
+end
+
+function M.properties.trim_trailing_whitespace(bufnr, val)
+ assert(
+ val == 'true' or val == 'false',
+ 'trim_trailing_whitespace must be either "true" or "false"'
+ )
+ if val == 'true' then
+ vim.api.nvim_create_autocmd('BufWritePre', {
+ group = 'editorconfig',
+ buffer = bufnr,
+ callback = function()
+ local view = vim.fn.winsaveview()
+ vim.api.nvim_command('silent! undojoin')
+ vim.api.nvim_command('silent keepjumps keeppatterns %s/\\s\\+$//e')
+ vim.fn.winrestview(view)
+ end,
+ })
+ else
+ vim.api.nvim_clear_autocmds({
+ event = 'BufWritePre',
+ group = 'editorconfig',
+ buffer = bufnr,
+ })
+ end
+end
+
+function M.properties.insert_final_newline(bufnr, val)
+ assert(val == 'true' or val == 'false', 'insert_final_newline must be either "true" or "false"')
+ vim.bo[bufnr].fixendofline = val == 'true'
+ vim.bo[bufnr].endofline = val == 'true'
+end
+
+--- Modified version of |glob2regpat()| that does not match path separators on *.
+---
+--- This function replaces single instances of * with the regex pattern [^/]*. However, the star in
+--- the replacement pattern also gets interpreted by glob2regpat, so we insert a placeholder, pass
+--- it through glob2regpat, then replace the placeholder with the actual regex pattern.
+---
+---@param glob string Glob to convert into a regular expression
+---@return string Regular expression
+---
+---@private
+local function glob2regpat(glob)
+ local placeholder = '@@PLACEHOLDER@@'
+ return (
+ string.gsub(
+ vim.fn.glob2regpat(
+ vim.fn.substitute(
+ string.gsub(glob, '{(%d+)%.%.(%d+)}', '[%1-%2]'),
+ '\\*\\@<!\\*\\*\\@!',
+ placeholder,
+ 'g'
+ )
+ ),
+ placeholder,
+ '[^/]*'
+ )
+ )
+end
+
+--- Parse a single line in an EditorConfig file
+---
+---@param line string Line
+---@return string|nil If the line contains a pattern, the glob pattern
+---@return string|nil If the line contains a key-value pair, the key
+---@return string|nil If the line contains a key-value pair, the value
+---
+---@private
+local function parse_line(line)
+ if line:find('^%s*[^ #;]') then
+ local glob = (line:match('%b[]') or ''):match('^%s*%[(.*)%]%s*$')
+ if glob then
+ return glob, nil, nil
+ end
+
+ local key, val = line:match('^%s*([^:= ][^:=]-)%s*[:=]%s*(.-)%s*$')
+ if key ~= nil and val ~= nil then
+ return nil, key:lower(), val:lower()
+ end
+ end
+end
+
+--- Parse options from an .editorconfig file
+---
+---@param filepath string File path of the file to apply EditorConfig settings to
+---@param dir string Current directory
+---@return table Table of options to apply to the given file
+---
+---@private
+local function parse(filepath, dir)
+ local pat = nil
+ local opts = {}
+ local f = io.open(dir .. '/.editorconfig')
+ if f then
+ for line in f:lines() do
+ local glob, key, val = parse_line(line)
+ if glob then
+ glob = glob:find('/') and (dir .. '/' .. glob:gsub('^/', '')) or ('**/' .. glob)
+ local ok, regpat = pcall(glob2regpat, glob)
+ if ok then
+ pat = vim.regex(regpat)
+ else
+ pat = nil
+ warn('editorconfig: Error occurred while parsing glob pattern "%s": %s', glob, regpat)
+ end
+ elseif key ~= nil and val ~= nil then
+ if key == 'root' then
+ opts.root = val == 'true'
+ elseif pat and pat:match_str(filepath) then
+ opts[key] = val
+ end
+ end
+ end
+ f:close()
+ end
+ return opts
+end
+
+--- Configure the given buffer with options from an .editorconfig file
+---
+---@param bufnr number Buffer number to configure
+---
+---@private
+function M.config(bufnr)
+ bufnr = bufnr or vim.api.nvim_get_current_buf()
+ local path = vim.fs.normalize(vim.api.nvim_buf_get_name(bufnr))
+ if vim.bo[bufnr].buftype ~= '' or not vim.bo[bufnr].modifiable or path == '' then
+ return
+ end
+
+ local opts = {}
+ for parent in vim.fs.parents(path) do
+ for k, v in pairs(parse(path, parent)) do
+ if opts[k] == nil then
+ opts[k] = v
+ end
+ end
+
+ if opts.root then
+ break
+ end
+ end
+
+ local applied = {}
+ for opt, val in pairs(opts) do
+ if val ~= 'unset' then
+ local func = M.properties[opt]
+ if func then
+ local ok, err = pcall(func, bufnr, val, opts)
+ if ok then
+ applied[opt] = val
+ else
+ warn('editorconfig: invalid value for option %s: %s. %s', opt, val, err)
+ end
+ end
+ end
+ end
+
+ vim.b[bufnr].editorconfig = applied
+end
+
+return M
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index 7eec567b66..c3ab39a1a3 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -1378,13 +1378,13 @@ local filename = {
npmrc = 'dosini',
['/etc/yum.conf'] = 'dosini',
['.npmrc'] = 'dosini',
- ['.editorconfig'] = 'dosini',
['/etc/pacman.conf'] = 'confini',
['mpv.conf'] = 'confini',
dune = 'dune',
jbuild = 'dune',
['dune-workspace'] = 'dune',
['dune-project'] = 'dune',
+ ['.editorconfig'] = 'editorconfig',
['elinks.conf'] = 'elinks',
['mix.lock'] = 'elixir',
['filter-rules'] = 'elmfilt',
diff --git a/runtime/plugin/editorconfig.lua b/runtime/plugin/editorconfig.lua
new file mode 100644
index 0000000000..60eb861aaa
--- /dev/null
+++ b/runtime/plugin/editorconfig.lua
@@ -0,0 +1,11 @@
+if vim.g.editorconfig_enable == false or vim.g.editorconfig_enable == 0 then
+ return
+end
+
+local group = vim.api.nvim_create_augroup('editorconfig', {})
+vim.api.nvim_create_autocmd({ 'BufNewFile', 'BufRead', 'BufFilePost' }, {
+ group = group,
+ callback = function(args)
+ require('editorconfig').config(args.buf)
+ end,
+})
diff --git a/runtime/syntax/editorconfig.vim b/runtime/syntax/editorconfig.vim
new file mode 100644
index 0000000000..006b99cec5
--- /dev/null
+++ b/runtime/syntax/editorconfig.vim
@@ -0,0 +1,18 @@
+runtime! syntax/dosini.vim
+unlet! b:current_syntax
+
+syntax match editorconfigInvalidProperty "^\s*\zs\w\+\ze\s*="
+syntax keyword editorconfigProperty root
+
+lua<<
+local props = {}
+for k in pairs(require('editorconfig').properties) do
+ props[#props + 1] = k
+end
+vim.cmd(string.format('syntax keyword editorconfigProperty %s', table.concat(props, ' ')))
+.
+
+hi def link editorconfigInvalidProperty Error
+hi def link editorconfigProperty dosiniLabel
+
+let b:current_syntax = 'editorconfig'