feat: filetype.lua (#16600)

Adds a new vim.filetype module that provides support for filetype detection in
Lua.
This commit is contained in:
Gregory Anders
2022-01-04 07:28:29 -07:00
committed by GitHub
parent cc62f3d6cb
commit 3fd454bd4a
13 changed files with 1938 additions and 21 deletions

View File

@ -24,12 +24,21 @@ Each time a new or existing file is edited, Vim will try to recognize the type
of the file and set the 'filetype' option. This will trigger the FileType
event, which can be used to set the syntax highlighting, set options, etc.
Detail: The ":filetype on" command will load this file:
Detail: The ":filetype on" command will load these files:
$VIMRUNTIME/filetype.lua
$VIMRUNTIME/filetype.vim
This file is a Vim script that defines autocommands for the
BufNewFile and BufRead events. If the file type is not found by the
name, the file $VIMRUNTIME/scripts.vim is used to detect it from the
contents of the file.
filetype.lua creates an autocommand that fires for all BufNewFile and
BufRead events. It tries to detect the filetype based off of the
file's extension or name.
filetype.vim is a Vim script that defines autocommands for the
BufNewFile and BufRead events. In contrast to filetype.lua, this
file creates separate BufNewFile and BufRead events for each filetype
pattern.
If the file type is not found by the name, the file
$VIMRUNTIME/scripts.vim is used to detect it from the contents of the
file.
When the GUI is running or will start soon, the |menu.vim| script is
also sourced. See |'go-M'| about avoiding that.
@ -149,9 +158,10 @@ is used. The default value is set like this: >
This means that the contents of compressed files are not inspected.
*new-filetype*
If a file type that you want to use is not detected yet, there are four ways
to add it. In any way, it's better not to modify the $VIMRUNTIME/filetype.vim
file. It will be overwritten when installing a new version of Vim.
If a file type that you want to use is not detected yet, there are a few ways
to add it. In any way, it's better not to modify the $VIMRUNTIME/filetype.lua
or $VIMRUNTIME/filetype.vim files. They will be overwritten when installing a
new version of Nvim.
A. If you want to overrule all default file type checks.
This works by writing one file for each filetype. The disadvantage is that
@ -191,7 +201,7 @@ B. If you want to detect your file after the default file type checks.
au BufRead,BufNewFile * if &ft == 'pascal' | set ft=mypascal
| endif
C. If your file type can be detected by the file name.
C. If your file type can be detected by the file name or extension.
1. Create your user runtime directory. You would normally use the first
item of the 'runtimepath' option. Example for Unix: >
:!mkdir -p ~/.config/nvim
@ -206,9 +216,38 @@ C. If your file type can be detected by the file name.
au! BufRead,BufNewFile *.mine setfiletype mine
au! BufRead,BufNewFile *.xyz setfiletype drawing
augroup END
< Write this file as "filetype.vim" in your user runtime directory. For
<
Write this file as "filetype.vim" in your user runtime directory. For
example, for Unix: >
:w ~/.config/nvim/filetype.vim
<
Alternatively, create a file called "filetype.lua" that adds new
filetypes.
Example: >
vim.filetype.add({
extension = {
foo = "fooscript",
},
filename = {
[".foorc"] = "foorc",
},
pattern = {
[".*/etc/foo/.*%.conf"] = "foorc",
},
})
<
See |vim.filetype.add()|.
*g:do_filetype_lua*
For now, Lua filetype detection is opt-in. You can enable it by adding
the following to your |init.vim|: >
let g:do_filetype_lua = 1
< *g:did_load_filetypes*
In either case, the builtin filetype detection provided by Nvim can be
disabled by setting the did_load_filetypes global variable. If this
variable exists, $VIMRUNTIME/filetype.vim will not run.
Example: >
" Disable filetype.vim
let g:did_load_filetypes = 1
< 3. To use the new filetype detection you must restart Vim.
@ -245,9 +284,9 @@ D. If your filetype can only be detected by inspecting the contents of the
$VIMRUNTIME/scripts.vim.
*remove-filetype*
If a file type is detected that is wrong for you, install a filetype.vim or
scripts.vim to catch it (see above). You can set 'filetype' to a non-existing
name to avoid that it will be set later anyway: >
If a file type is detected that is wrong for you, install a filetype.lua,
filetype.vim or scripts.vim to catch it (see above). You can set 'filetype' to
a non-existing name to avoid that it will be set later anyway: >
:set filetype=ignored
If you are setting up a system with many users, and you don't want each user

View File

@ -1780,4 +1780,71 @@ select({items}, {opts}, {on_choice}) *vim.ui.select()*
1-based index of `item` within `item` . `nil`
if the user aborted the dialog.
==============================================================================
Lua module: filetype *lua-filetype*
add({filetypes}) *vim.filetype.add()*
Add new filetype mappings.
Filetype mappings can be added either by extension or by
filename (either the "tail" or the full file path). The full
file path is checked first, followed by the file name. If a
match is not found using the filename, then the filename is
matched against the list of patterns (sorted by priority)
until a match is found. Lastly, if pattern matching does not
find a filetype, then the file extension is used.
The filetype can be either a string (in which case it is used
as the filetype directly) or a function. If a function, it
takes the full path and buffer number of the file as arguments
(along with captures from the matched pattern, if any) and
should return a string that will be used as the buffer's
filetype.
Filename patterns can specify an optional priority to resolve
cases when a file path matches multiple patterns. Higher
priorities are matched first. When omitted, the priority
defaults to 0.
See $VIMRUNTIME/lua/vim/filetype.lua for more examples.
Note that Lua filetype detection is only enabled when
|g:do_filetype_lua| is set to 1.
Example: >
vim.filetype.add({
extension = {
foo = "fooscript",
bar = function(path, bufnr)
if some_condition() then
return "barscript"
end
return "bar"
end,
},
filename = {
[".foorc"] = "toml",
["/etc/foo/config"] = "toml",
},
pattern = {
[".*&zwj;/etc/foo/.*"] = "fooscript",
-- Using an optional priority
[".*&zwj;/etc/foo/.*%.conf"] = { "dosini", { priority = 10 } },
["README.(%a+)$"] = function(path, bufnr, ext)
if ext == "md" then
return "markdown"
elseif ext == "rst" then
return "rst"
end
end,
},
})
<
Parameters: ~
{filetypes} table A table containing new filetype maps
(see example).
vim:tw=78:ts=8:ft=help:norl:

22
runtime/filetype.lua Normal file
View File

@ -0,0 +1,22 @@
if vim.g.did_load_filetypes and vim.g.did_load_filetypes ~= 0 then
return
end
-- For now, make this opt-in with a global variable
if vim.g.do_filetype_lua ~= 1 then
return
end
vim.cmd [[
augroup filetypedetect
au BufRead,BufNewFile * call v:lua.vim.filetype.match(str2nr(expand('<abuf>')))
" These *must* be sourced after the autocommand above is created
runtime! ftdetect/*.vim
runtime! ftdetect/*.lua
" Set a marker so that the ftdetect scripts are not sourced a second time by filetype.vim
let g:did_load_ftdetect = 1
augroup END
]]

View File

@ -2407,10 +2407,12 @@ au BufNewFile,BufRead *.txt
\| setf text
\| endif
" Use the filetype detect plugins. They may overrule any of the previously
" detected filetypes.
runtime! ftdetect/*.vim
runtime! ftdetect/*.lua
if !exists('g:did_load_ftdetect')
" Use the filetype detect plugins. They may overrule any of the previously
" detected filetypes.
runtime! ftdetect/*.vim
runtime! ftdetect/*.lua
endif
" NOTE: The above command could have ended the filetypedetect autocmd group
" and started another one. Let's make sure it has ended to get to a consistent

1465
runtime/lua/vim/filetype.lua Normal file

File diff suppressed because it is too large Load Diff

201
scripts/gen_filetype.lua Normal file
View File

@ -0,0 +1,201 @@
local do_not_run = true
if do_not_run then
print([[
This script was used to bootstrap the filetype patterns in runtime/lua/vim/filetype.lua. It
should no longer be used except for testing purposes. New filetypes, or changes to existing
filetypes, should be ported manually as part of the vim-patch process.
]])
return
end
local filetype_vim = "runtime/filetype.vim"
local filetype_lua = "runtime/lua/vim/filetype.lua"
local keywords = {
["for"] = true,
["or"] = true,
["and"] = true,
["end"] = true,
["do"] = true,
["if"] = true,
["while"] = true,
["repeat"] = true,
}
local sections = {
extension = { str = {}, func = {} },
filename = { str = {}, func = {} },
pattern = { str = {}, func = {} },
}
local specialchars = "%*%?\\%$%[%]%{%}"
local function add_pattern(pat, ft)
local ok = true
-- Patterns that start or end with { or } confuse splitting on commas and make parsing harder, so just skip those
if not string.find(pat, "^%{") and not string.find(pat, "%}$") then
for part in string.gmatch(pat, "[^,]+") do
if not string.find(part, "[" .. specialchars .. "]") then
if type(ft) == "string" then
sections.filename.str[part] = ft
else
sections.filename.func[part] = ft
end
elseif string.match(part, "^%*%.[^%./" .. specialchars .. "]+$") then
if type(ft) == "string" then
sections.extension.str[part:sub(3)] = ft
else
sections.extension.func[part:sub(3)] = ft
end
else
if string.match(part, "^%*/[^" .. specialchars .. "]+$") then
-- For patterns matching */some/pattern we want to easily match files
-- with path /some/pattern, so include those in filename detection
if type(ft) == "string" then
sections.filename.str[part:sub(2)] = ft
else
sections.filename.func[part:sub(2)] = ft
end
end
if string.find(part, "^[%w-_.*?%[%]/]+$") then
local p = part:gsub("%.", "%%."):gsub("%*", ".*"):gsub("%?", ".")
-- Insert into array to maintain order rather than setting
-- key-value directly
if type(ft) == "string" then
sections.pattern.str[p] = ft
else
sections.pattern.func[p] = ft
end
else
ok = false
end
end
end
end
return ok
end
local function parse_line(line)
local pat, ft
pat, ft = line:match("^%s*au%a* Buf[%a,]+%s+(%S+)%s+setf%s+(%S+)")
if pat then
return add_pattern(pat, ft)
else
local func
pat, func = line:match("^%s*au%a* Buf[%a,]+%s+(%S+)%s+call%s+(%S+)")
if pat then
return add_pattern(pat, function() return func end)
end
end
end
local unparsed = {}
local full_line
for line in io.lines(filetype_vim) do
local cont = string.match(line, "^%s*\\%s*(.*)$")
if cont then
full_line = full_line .. " " .. cont
else
if full_line then
if not parse_line(full_line) and string.find(full_line, "^%s*au%a* Buf") then
table.insert(unparsed, full_line)
end
end
full_line = line
end
end
if #unparsed > 0 then
print("Failed to parse the following patterns:")
for _, v in ipairs(unparsed) do
print(v)
end
end
local function add_item(indent, key, ft)
if type(ft) == "string" then
if string.find(key, "%A") or keywords[key] then
key = string.format("[\"%s\"]", key)
end
return string.format([[%s%s = "%s",]], indent, key, ft)
elseif type(ft) == "function" then
local func = ft()
if string.find(key, "%A") or keywords[key] then
key = string.format("[\"%s\"]", key)
end
-- Right now only a single argument is supported, which covers
-- everything in filetype.vim as of this writing
local arg = string.match(func, "%((.*)%)$")
func = string.gsub(func, "%(.*$", "")
if arg == "" then
-- Function with no arguments, call the function directly
return string.format([[%s%s = function() vim.fn["%s"]() end,]], indent, key, func)
elseif string.match(arg, [[^(["']).*%1$]]) then
-- String argument
if func == "s:StarSetf" then
return string.format([[%s%s = starsetf(%s),]], indent, key, arg)
else
return string.format([[%s%s = function() vim.fn["%s"](%s) end,]], indent, key, func, arg)
end
elseif string.find(arg, "%(") then
-- Function argument
return string.format([[%s%s = function() vim.fn["%s"](vim.fn.%s) end,]], indent, key, func, arg)
else
assert(false, arg)
end
end
end
do
local lines = {}
local start = false
for line in io.lines(filetype_lua) do
if line:match("^%s+-- END [A-Z]+$") then
start = false
end
if not start then
table.insert(lines, line)
end
local indent, section = line:match("^(%s+)-- BEGIN ([A-Z]+)$")
if section then
start = true
local t = sections[string.lower(section)]
local sorted = {}
for k, v in pairs(t.str) do
table.insert(sorted, {[k] = v})
end
table.sort(sorted, function(a, b)
return a[next(a)] < b[next(b)]
end)
for _, v in ipairs(sorted) do
local k = next(v)
table.insert(lines, add_item(indent, k, v[k]))
end
sorted = {}
for k, v in pairs(t.func) do
table.insert(sorted, {[k] = v})
end
table.sort(sorted, function(a, b)
return next(a) < next(b)
end)
for _, v in ipairs(sorted) do
local k = next(v)
table.insert(lines, add_item(indent, k, v[k]))
end
end
end
local f = io.open(filetype_lua, "w")
f:write(table.concat(lines, "\n") .. "\n")
f:close()
end

View File

@ -128,12 +128,14 @@ CONFIG = {
'shared.lua',
'uri.lua',
'ui.lua',
'filetype.lua',
],
'files': ' '.join([
os.path.join(base_dir, 'src/nvim/lua/vim.lua'),
os.path.join(base_dir, 'runtime/lua/vim/shared.lua'),
os.path.join(base_dir, 'runtime/lua/vim/uri.lua'),
os.path.join(base_dir, 'runtime/lua/vim/ui.lua'),
os.path.join(base_dir, 'runtime/lua/vim/filetype.lua'),
]),
'file_patterns': '*.lua',
'fn_name_prefix': '',
@ -148,6 +150,7 @@ CONFIG = {
'shared': 'vim',
'uri': 'vim',
'ui': 'vim.ui',
'filetype': 'vim.filetype',
},
'append_only': [
'shared.lua',

View File

@ -62,6 +62,7 @@ set(LUA_SHARED_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/shared.lua)
set(LUA_INSPECT_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/inspect.lua)
set(LUA_F_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/F.lua)
set(LUA_META_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/_meta.lua)
set(LUA_FILETYPE_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/filetype.lua)
set(CHAR_BLOB_GENERATOR ${GENERATOR_DIR}/gen_char_blob.lua)
set(LINT_SUPPRESS_FILE ${PROJECT_BINARY_DIR}/errors.json)
set(LINT_SUPPRESS_URL_BASE "https://raw.githubusercontent.com/neovim/doc/gh-pages/reports/clint")
@ -334,6 +335,7 @@ add_custom_command(
${LUA_INSPECT_MODULE_SOURCE} inspect_module
${LUA_F_MODULE_SOURCE} lua_F_module
${LUA_META_MODULE_SOURCE} lua_meta_module
${LUA_FILETYPE_MODULE_SOURCE} lua_filetype_module
DEPENDS
${CHAR_BLOB_GENERATOR}
${LUA_VIM_MODULE_SOURCE}
@ -341,6 +343,7 @@ add_custom_command(
${LUA_INSPECT_MODULE_SOURCE}
${LUA_F_MODULE_SOURCE}
${LUA_META_MODULE_SOURCE}
${LUA_FILETYPE_MODULE_SOURCE}
VERBATIM
)

View File

@ -9551,15 +9551,18 @@ static void ex_filetype(exarg_T *eap)
void filetype_maybe_enable(void)
{
if (filetype_detect == kNone) {
source_runtime(FILETYPE_FILE, true);
// Normally .vim files are sourced before .lua files when both are
// supported, but we reverse the order here because we want the Lua
// autocommand to be defined first so that it runs first
source_runtime(FILETYPE_FILE, DIP_ALL);
filetype_detect = kTrue;
}
if (filetype_plugin == kNone) {
source_runtime(FTPLUGIN_FILE, true);
source_runtime(FTPLUGIN_FILE, DIP_ALL);
filetype_plugin = kTrue;
}
if (filetype_indent == kNone) {
source_runtime(INDENT_FILE, true);
source_runtime(INDENT_FILE, DIP_ALL);
filetype_indent = kTrue;
}
}

View File

@ -28,7 +28,7 @@
#endif
#ifndef FILETYPE_FILE
# define FILETYPE_FILE "filetype.vim"
# define FILETYPE_FILE "filetype.lua filetype.vim"
#endif
#ifndef FTPLUGIN_FILE

View File

@ -434,6 +434,15 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
// [package, loaded, module]
lua_setfield(lstate, -2, "vim.F"); // [package, loaded]
code = (char *)&lua_filetype_module[0];
if (luaL_loadbuffer(lstate, code, sizeof(lua_filetype_module) - 1, "@vim/filetype.lua")
|| nlua_pcall(lstate, 0, 1)) {
nlua_error(lstate, _("E5106: Error while creating vim.filetype module: %.*s"));
return 1;
}
// [package, loaded, module]
lua_setfield(lstate, -2, "vim.filetype"); // [package, loaded]
lua_pop(lstate, 2); // []
}

View File

@ -40,6 +40,9 @@ assert(vim)
vim.inspect = package.loaded['vim.inspect']
assert(vim.inspect)
vim.filetype = package.loaded['vim.filetype']
assert(vim.filetype)
local pathtrails = {}
vim._so_trails = {}
for s in (package.cpath..';'):gmatch('[^;]*;') do

View File

@ -0,0 +1,100 @@
local helpers = require('test.functional.helpers')(after_each)
local exec_lua = helpers.exec_lua
local eq = helpers.eq
local clear = helpers.clear
describe('vim.filetype', function()
before_each(function()
clear()
exec_lua [[
local bufnr = vim.api.nvim_create_buf(true, false)
vim.api.nvim_set_current_buf(bufnr)
]]
end)
it('works with extensions', function()
eq('radicalscript', exec_lua [[
vim.filetype.add({
extension = {
rs = 'radicalscript',
},
})
vim.api.nvim_buf_set_name(0, '/home/user/src/main.rs')
vim.filetype.match(0)
return vim.bo.filetype
]])
end)
it('prioritizes filenames over extensions', function()
eq('somethingelse', exec_lua [[
vim.filetype.add({
extension = {
rs = 'radicalscript',
},
filename = {
['main.rs'] = 'somethingelse',
},
})
vim.api.nvim_buf_set_name(0, '/home/usr/src/main.rs')
vim.filetype.match(0)
return vim.bo.filetype
]])
end)
it('works with filenames', function()
eq('nim', exec_lua [[
vim.filetype.add({
filename = {
['s_O_m_e_F_i_l_e'] = 'nim',
},
})
vim.api.nvim_buf_set_name(0, '/home/user/src/s_O_m_e_F_i_l_e')
vim.filetype.match(0)
return vim.bo.filetype
]])
eq('dosini', exec_lua [[
vim.filetype.add({
filename = {
['config'] = 'toml',
['~/.config/fun/config'] = 'dosini',
},
})
vim.api.nvim_buf_set_name(0, '~/.config/fun/config')
vim.filetype.match(0)
return vim.bo.filetype
]])
end)
it('works with patterns', function()
eq('markdown', exec_lua [[
vim.filetype.add({
pattern = {
['~/blog/.*%.txt'] = 'markdown',
}
})
vim.api.nvim_buf_set_name(0, '~/blog/why_neovim_is_awesome.txt')
vim.filetype.match(0)
return vim.bo.filetype
]])
end)
it('works with functions', function()
eq('foss', exec_lua [[
vim.filetype.add({
pattern = {
["relevant_to_(%a+)"] = function(path, bufnr, capture)
if capture == "me" then
return "foss"
end
end,
}
})
vim.api.nvim_buf_set_name(0, 'relevant_to_me')
vim.filetype.match(0)
return vim.bo.filetype
]])
end)
end)