mirror of
https://github.com/neovim/neovim.git
synced 2026-01-04 10:26:42 +10:00
Merge pull request #21633 from gpanders/editorconfig
Builtin EditorConfig support
This commit is contained in:
89
runtime/doc/editorconfig.txt
Normal file
89
runtime/doc/editorconfig.txt
Normal file
@@ -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:
|
||||
@@ -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
|
||||
|
||||
246
runtime/lua/editorconfig.lua
Normal file
246
runtime/lua/editorconfig.lua
Normal file
@@ -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
|
||||
@@ -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',
|
||||
|
||||
11
runtime/plugin/editorconfig.lua
Normal file
11
runtime/plugin/editorconfig.lua
Normal file
@@ -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,
|
||||
})
|
||||
18
runtime/syntax/editorconfig.vim
Normal file
18
runtime/syntax/editorconfig.vim
Normal file
@@ -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'
|
||||
Reference in New Issue
Block a user