neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

editorconfig.lua (11695B)


      1 --- @brief
      2 --- EditorConfig is like 'modeline' for an entire (recursive) directory. When a file is opened,
      3 --- after running |ftplugin|s and |FileType| autocommands, the EditorConfig feature searches all
      4 --- parent directories of that file for `.editorconfig` files, parses them, and applies their
      5 --- properties. For more information see https://editorconfig.org/.
      6 ---
      7 --- Example `.editorconfig` file:
      8 --- ```ini
      9 --- root = true
     10 ---
     11 --- [*]
     12 --- charset = utf-8
     13 --- end_of_line = lf
     14 --- indent_size = 4
     15 --- indent_style = space
     16 --- max_line_length = 42
     17 --- trim_trailing_whitespace = true
     18 ---
     19 --- [*.{diff,md}]
     20 --- trim_trailing_whitespace = false
     21 --- ```
     22 
     23 --- @brief [g:editorconfig]() [b:editorconfig]()
     24 ---
     25 --- EditorConfig is enabled by default. To disable it, add to your config:
     26 --- ```lua
     27 --- vim.g.editorconfig = false
     28 --- ```
     29 ---
     30 --- (Vimscript: `let g:editorconfig = v:false`). It can also be disabled
     31 --- per-buffer by setting the [b:editorconfig] buffer-local variable to `false`.
     32 ---
     33 --- Nvim stores the applied properties in [b:editorconfig] if it is not `false`.
     34 
     35 --- @brief [editorconfig-custom-properties]()
     36 ---
     37 --- New properties can be added by adding a new entry to the "properties" table.
     38 --- The table key is a property name and the value is a callback function which
     39 --- accepts the number of the buffer to be modified, the value of the property
     40 --- in the `.editorconfig` file, and (optionally) a table containing all of the
     41 --- other properties and their values (useful for properties which depend on other
     42 --- properties). The value is always a string and must be coerced if necessary.
     43 --- Example:
     44 ---
     45 --- ```lua
     46 ---
     47 --- require('editorconfig').properties.foo = function(bufnr, val, opts)
     48 ---   if opts.charset and opts.charset ~= "utf-8" then
     49 ---     error("foo can only be set when charset is utf-8", 0)
     50 ---   end
     51 ---   vim.b[bufnr].foo = val
     52 --- end
     53 ---
     54 --- ```
     55 
     56 --- @brief [editorconfig-properties]()
     57 ---
     58 --- The following properties are supported by default:
     59 
     60 --- @type table<string,fun(bufnr: integer, val: string, opts?: table)>
     61 local properties = {}
     62 
     63 --- Modified version of the builtin assert that does not include error position information
     64 ---
     65 --- @param v any Condition
     66 --- @param message string Error message to display if condition is false or nil
     67 --- @return any v if not false or nil, otherwise an error is displayed
     68 local function assert(v, message)
     69  return v or error(message, 0)
     70 end
     71 
     72 --- Show a warning message
     73 --- @param msg string Message to show
     74 local function warn(msg, ...)
     75  vim.notify_once(msg:format(...), vim.log.levels.WARN, {
     76    title = 'editorconfig',
     77  })
     78 end
     79 
     80 --- If "true", then stop searching for `.editorconfig` files in parent
     81 --- directories. This property must be at the top-level of the
     82 --- `.editorconfig` file (i.e. it must not be within a glob section).
     83 function properties.root()
     84  -- Unused
     85 end
     86 
     87 --- One of `"utf-8"`, `"utf-8-bom"`, `"latin1"`, `"utf-16be"`, or `"utf-16le"`.
     88 --- Sets the 'fileencoding' and 'bomb' options.
     89 function properties.charset(bufnr, val)
     90  assert(
     91    vim.list_contains({ 'utf-8', 'utf-8-bom', 'latin1', 'utf-16be', 'utf-16le' }, val),
     92    'charset must be one of "utf-8", "utf-8-bom", "latin1", "utf-16be", or "utf-16le"'
     93  )
     94  if val == 'utf-8' or val == 'utf-8-bom' then
     95    vim.bo[bufnr].fileencoding = 'utf-8'
     96    vim.bo[bufnr].bomb = val == 'utf-8-bom'
     97  elseif val == 'utf-16be' then
     98    vim.bo[bufnr].fileencoding = 'utf-16'
     99  else
    100    vim.bo[bufnr].fileencoding = val
    101  end
    102 end
    103 
    104 --- One of `"lf"`, `"crlf"`, or `"cr"`.
    105 --- These correspond to setting 'fileformat' to "unix", "dos", or "mac",
    106 --- respectively.
    107 function properties.end_of_line(bufnr, val)
    108  vim.bo[bufnr].fileformat = assert(
    109    ({ lf = 'unix', crlf = 'dos', cr = 'mac' })[val],
    110    'end_of_line must be one of "lf", "crlf", or "cr"'
    111  )
    112 end
    113 
    114 --- One of `"tab"` or `"space"`. Sets the 'expandtab' option.
    115 function properties.indent_style(bufnr, val, opts)
    116  assert(val == 'tab' or val == 'space', 'indent_style must be either "tab" or "space"')
    117  vim.bo[bufnr].expandtab = val == 'space'
    118  if val == 'tab' and not opts.indent_size then
    119    vim.bo[bufnr].shiftwidth = 0
    120    vim.bo[bufnr].softtabstop = 0
    121  end
    122 end
    123 
    124 --- A number indicating the size of a single indent. Alternatively, use the
    125 --- value "tab" to use the value of the tab_width property. Sets the
    126 --- 'shiftwidth' and 'softtabstop' options. If this value is not "tab" and
    127 --- the tab_width property is not set, 'tabstop' is also set to this value.
    128 function properties.indent_size(bufnr, val, opts)
    129  if val == 'tab' then
    130    vim.bo[bufnr].shiftwidth = 0
    131    vim.bo[bufnr].softtabstop = 0
    132  else
    133    local n = assert(tonumber(val), 'indent_size must be a number')
    134    vim.bo[bufnr].shiftwidth = n
    135    vim.bo[bufnr].softtabstop = -1
    136    if not opts.tab_width then
    137      vim.bo[bufnr].tabstop = n
    138    end
    139  end
    140 end
    141 
    142 --- The display size of a single tab character. Sets the 'tabstop' option.
    143 function properties.tab_width(bufnr, val)
    144  vim.bo[bufnr].tabstop = assert(tonumber(val), 'tab_width must be a number')
    145 end
    146 
    147 --- A number indicating the maximum length of a single
    148 --- line. Sets the 'textwidth' option.
    149 function properties.max_line_length(bufnr, val)
    150  local n = tonumber(val)
    151  if n then
    152    vim.bo[bufnr].textwidth = n
    153  else
    154    assert(val == 'off', 'max_line_length must be a number or "off"')
    155    vim.bo[bufnr].textwidth = 0
    156  end
    157 end
    158 
    159 --- When `"true"`, trailing whitespace is automatically removed when the buffer is written.
    160 function properties.trim_trailing_whitespace(bufnr, val)
    161  assert(
    162    val == 'true' or val == 'false',
    163    'trim_trailing_whitespace must be either "true" or "false"'
    164  )
    165  if val == 'true' then
    166    vim.api.nvim_create_autocmd('BufWritePre', {
    167      group = 'nvim.editorconfig',
    168      buffer = bufnr,
    169      callback = function()
    170        local view = vim.fn.winsaveview()
    171        vim.api.nvim_command('silent! undojoin')
    172        vim.api.nvim_command('silent keepjumps keeppatterns %s/\\s\\+$//e')
    173        vim.fn.winrestview(view)
    174      end,
    175    })
    176  else
    177    vim.api.nvim_clear_autocmds({
    178      event = 'BufWritePre',
    179      group = 'nvim.editorconfig',
    180      buffer = bufnr,
    181    })
    182  end
    183 end
    184 
    185 --- `"true"` or `"false"` to ensure the file always has a trailing newline as its last byte.
    186 --- Sets the 'fixendofline' and 'endofline' options.
    187 function properties.insert_final_newline(bufnr, val)
    188  assert(val == 'true' or val == 'false', 'insert_final_newline must be either "true" or "false"')
    189  vim.bo[bufnr].fixendofline = val == 'true'
    190 
    191  -- 'endofline' can be read to detect if the file contains a final newline,
    192  -- so only change 'endofline' right before writing the file
    193  local endofline = val == 'true'
    194  if vim.bo[bufnr].endofline ~= endofline then
    195    vim.api.nvim_create_autocmd('BufWritePre', {
    196      group = 'nvim.editorconfig',
    197      buffer = bufnr,
    198      once = true,
    199      callback = function()
    200        vim.bo[bufnr].endofline = endofline
    201      end,
    202    })
    203  end
    204 end
    205 
    206 --- A code of the format ss or ss-TT, where ss is an ISO 639 language code and TT is an ISO 3166 territory identifier.
    207 --- Sets the 'spelllang' option.
    208 function properties.spelling_language(bufnr, val)
    209  local error_msg =
    210    'spelling_language must be of the format ss or ss-TT, where ss is an ISO 639 language code and TT is an ISO 3166 territory identifier.'
    211 
    212  assert(val:len() == 2 or val:len() == 5, error_msg)
    213 
    214  local language_code = val:sub(1, 2):lower()
    215  assert(language_code:match('%l%l'), error_msg)
    216  if val:len() == 2 then
    217    vim.bo[bufnr].spelllang = language_code
    218  else
    219    assert(val:sub(3, 3) == '-', error_msg)
    220 
    221    local territory_code = val:sub(4, 5):lower()
    222    assert(territory_code:match('%l%l'), error_msg)
    223    vim.bo[bufnr].spelllang = language_code .. '_' .. territory_code
    224  end
    225 end
    226 
    227 --- Modified version of [glob2regpat()] that does not match path separators on `*`.
    228 ---
    229 --- This function replaces single instances of `*` with the regex pattern `[^/]*`.
    230 --- However, the star in the replacement pattern also gets interpreted by glob2regpat,
    231 --- so we insert a placeholder, pass it through glob2regpat, then replace the
    232 --- placeholder with the actual regex pattern.
    233 ---
    234 --- @param glob string Glob to convert into a regular expression
    235 --- @return string regex Regular expression
    236 local function glob2regpat(glob)
    237  local placeholder = '@@PLACEHOLDER@@'
    238  local glob1 = vim.fn.substitute(
    239    glob:gsub('{(%d+)%.%.(%d+)}', '[%1-%2]'),
    240    '\\*\\@<!\\*\\*\\@!',
    241    placeholder,
    242    'g'
    243  )
    244  local regpat = vim.fn.glob2regpat(glob1)
    245  return (regpat:gsub(placeholder, '[^/]*'))
    246 end
    247 
    248 --- Parse a single line in an EditorConfig file
    249 --- @param line string Line
    250 --- @return string? glob pattern if the line contains a pattern
    251 --- @return string? key if the line contains a key-value pair
    252 --- @return string? value if the line contains a key-value pair
    253 local function parse_line(line)
    254  if not line:find('^%s*[^ #;]') then
    255    return
    256  end
    257 
    258  --- @type string?
    259  local glob = line:match('^%s*%[(.*)%]%s*$')
    260  if glob then
    261    return glob
    262  end
    263 
    264  local key, val = line:match('^%s*([^:= ][^:=]-)%s*[:=]%s*(.-)%s*$')
    265  if key ~= nil and val ~= nil then
    266    return nil, key:lower(), val:lower()
    267  end
    268 end
    269 
    270 --- Parse options from an `.editorconfig` file
    271 --- @param filepath string File path of the file to apply EditorConfig settings to
    272 --- @param dir string Current directory
    273 --- @return table<string,string|boolean> Table of options to apply to the given file
    274 local function parse(filepath, dir)
    275  local pat --- @type vim.regex?
    276  local opts = {} --- @type table<string,string|boolean>
    277  local f = io.open(dir .. '/.editorconfig')
    278  if f then
    279    for line in f:lines() do
    280      local glob, key, val = parse_line(line)
    281      if glob then
    282        glob = glob:find('/') and (dir .. '/' .. glob:gsub('^/', '')) or ('**/' .. glob)
    283        local ok, regpat = pcall(glob2regpat, glob)
    284        if ok then
    285          pat = vim.regex(regpat)
    286        else
    287          pat = nil
    288          warn('editorconfig: Error occurred while parsing glob pattern "%s": %s', glob, regpat)
    289        end
    290      elseif key ~= nil and val ~= nil then
    291        if key == 'root' then
    292          assert(val == 'true' or val == 'false', 'root must be either "true" or "false"')
    293          opts.root = val == 'true'
    294        elseif pat and pat:match_str(filepath) then
    295          opts[key] = val
    296        end
    297      end
    298    end
    299    f:close()
    300  end
    301  return opts
    302 end
    303 
    304 local M = {}
    305 
    306 -- Exposed for use in syntax/editorconfig.vim`
    307 M.properties = properties
    308 
    309 --- @private
    310 --- Configure the given buffer with options from an `.editorconfig` file
    311 --- @param bufnr integer Buffer number to configure
    312 function M.config(bufnr)
    313  bufnr = bufnr or vim.api.nvim_get_current_buf()
    314  if not vim.api.nvim_buf_is_valid(bufnr) then
    315    return
    316  end
    317 
    318  local path = vim.fs.normalize(vim.api.nvim_buf_get_name(bufnr))
    319  if vim.bo[bufnr].buftype ~= '' or not vim.bo[bufnr].modifiable or path == '' then
    320    return
    321  end
    322 
    323  local opts = {} --- @type table<string,string|boolean>
    324  for parent in vim.fs.parents(path) do
    325    for k, v in pairs(parse(path, parent)) do
    326      if opts[k] == nil then
    327        opts[k] = v
    328      end
    329    end
    330 
    331    if opts.root then
    332      break
    333    end
    334  end
    335 
    336  local applied = {} --- @type table<string,string|boolean>
    337  for opt, val in pairs(opts) do
    338    if val ~= 'unset' then
    339      local func = M.properties[opt]
    340      if func then
    341        --- @type boolean, string?
    342        local ok, err = pcall(func, bufnr, val, opts)
    343        if ok then
    344          applied[opt] = val
    345        else
    346          warn('editorconfig: invalid value for option %s: %s. %s', opt, val, err)
    347        end
    348      end
    349    end
    350  end
    351 
    352  vim.b[bufnr].editorconfig = applied
    353 end
    354 
    355 return M