editorconfig.lua 11 KB

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