mirror of
https://github.com/neovim/neovim.git
synced 2025-12-11 17:12:40 +00:00
refactor(filetype)!: allow vim.filetype.match to use different strategies (#18895)
This enables vim.filetype.match to match based on a buffer (most
accurate) or simply a filename or file contents, which are less accurate
but may still be useful for some scenarios.
When matching based on a buffer, the buffer's name and contents are both
used to do full filetype matching. When using a filename, if the file
exists the file is loaded into a buffer and full filetype detection is
performed. If the file does not exist then filetype matching is only
performed against the filename itself. Content-based matching does the
equivalent of scripts.vim, and matches solely based on file contents
without any information from the name of the file itself (e.g. for
shebangs).
BREAKING CHANGE: use `vim.filetype.match({buf = bufnr})` instead
of `vim.filetype.match(name, bufnr)`
This commit is contained in:
@@ -2064,14 +2064,45 @@ add({filetypes}) *vim.filetype.add()*
|
|||||||
{filetypes} (table) A table containing new filetype maps
|
{filetypes} (table) A table containing new filetype maps
|
||||||
(see example).
|
(see example).
|
||||||
|
|
||||||
match({name}, {bufnr}) *vim.filetype.match()*
|
match({arg}) *vim.filetype.match()*
|
||||||
Find the filetype for the given filename and buffer.
|
Perform filetype detection.
|
||||||
|
|
||||||
|
The filetype can be detected using one of three methods:
|
||||||
|
1. Using an existing buffer
|
||||||
|
2. Using only a file name
|
||||||
|
3. Using only file contents
|
||||||
|
|
||||||
|
Of these, option 1 provides the most accurate result as it
|
||||||
|
uses both the buffer's filename and (optionally) the buffer
|
||||||
|
contents. Options 2 and 3 can be used without an existing
|
||||||
|
buffer, but may not always provide a match in cases where the
|
||||||
|
filename (or contents) cannot unambiguously determine the
|
||||||
|
filetype.
|
||||||
|
|
||||||
|
Each of the three options is specified using a key to the
|
||||||
|
single argument of this function. Example:
|
||||||
|
>
|
||||||
|
|
||||||
|
-- Using a buffer number
|
||||||
|
vim.filetype.match({ buf = 42 })
|
||||||
|
|
||||||
|
-- Using a filename
|
||||||
|
vim.filetype.match({ filename = "main.lua" })
|
||||||
|
|
||||||
|
-- Using file contents
|
||||||
|
vim.filetype.match({ contents = {"#!/usr/bin/env bash"} })
|
||||||
|
<
|
||||||
|
|
||||||
Parameters: ~
|
Parameters: ~
|
||||||
{name} (string) File name (can be an absolute or
|
{arg} (table) Table specifying which matching strategy to
|
||||||
relative path)
|
use. It is an error to provide more than one
|
||||||
{bufnr} (number|nil) The buffer to set the filetype for.
|
strategy. Accepted keys are:
|
||||||
Defaults to the current buffer.
|
• buf (number): Buffer number to use for matching
|
||||||
|
• filename (string): Filename to use for matching.
|
||||||
|
Note that the file need not actually exist in the
|
||||||
|
filesystem, only the name itself is used.
|
||||||
|
• contents (table): An array of lines representing
|
||||||
|
file contents to use for matching.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(string|nil) If a match was found, the matched filetype.
|
(string|nil) If a match was found, the matched filetype.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ vim.api.nvim_create_augroup('filetypedetect', { clear = false })
|
|||||||
vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, {
|
vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, {
|
||||||
group = 'filetypedetect',
|
group = 'filetypedetect',
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
local ft, on_detect = vim.filetype.match(args.file, args.buf)
|
local ft, on_detect = vim.filetype.match({ buf = args.buf })
|
||||||
if ft then
|
if ft then
|
||||||
vim.api.nvim_buf_set_option(args.buf, 'filetype', ft)
|
vim.api.nvim_buf_set_option(args.buf, 'filetype', ft)
|
||||||
if on_detect then
|
if on_detect then
|
||||||
|
|||||||
@@ -2047,7 +2047,7 @@ local pattern = {
|
|||||||
end
|
end
|
||||||
end, { priority = -math.huge + 1 }),
|
end, { priority = -math.huge + 1 }),
|
||||||
['XF86Config.*'] = starsetf(function(path, bufnr)
|
['XF86Config.*'] = starsetf(function(path, bufnr)
|
||||||
return require('vim.filetype.detect').xfree86(bufnr)
|
return require('vim.filetype.detect').xfree86()
|
||||||
end),
|
end),
|
||||||
['%.zcompdump.*'] = starsetf('zsh'),
|
['%.zcompdump.*'] = starsetf('zsh'),
|
||||||
-- .zlog* and zlog*
|
-- .zlog* and zlog*
|
||||||
@@ -2185,17 +2185,24 @@ end
|
|||||||
local function dispatch(ft, path, bufnr, ...)
|
local function dispatch(ft, path, bufnr, ...)
|
||||||
local on_detect
|
local on_detect
|
||||||
if type(ft) == 'function' then
|
if type(ft) == 'function' then
|
||||||
|
if bufnr then
|
||||||
ft, on_detect = ft(path, bufnr, ...)
|
ft, on_detect = ft(path, bufnr, ...)
|
||||||
|
else
|
||||||
|
-- If bufnr is nil (meaning we are matching only against the filename), set it to an invalid
|
||||||
|
-- value (-1) and catch any errors from the filetype detection function. If the function tries
|
||||||
|
-- to use the buffer then it will fail, but this enables functions which do not need a buffer
|
||||||
|
-- to still work.
|
||||||
|
local ok
|
||||||
|
ok, ft, on_detect = pcall(ft, path, -1, ...)
|
||||||
|
if not ok then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if type(ft) == 'string' then
|
if type(ft) == 'string' then
|
||||||
return ft, on_detect
|
return ft, on_detect
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Any non-falsey value (that is, anything other than 'nil' or 'false') will
|
|
||||||
-- end filetype matching. This is useful for e.g. the dist#ft functions that
|
|
||||||
-- return 0, but set the buffer's filetype themselves
|
|
||||||
return ft
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
@@ -2214,29 +2221,74 @@ local function match_pattern(name, path, tail, pat)
|
|||||||
return matches
|
return matches
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Find the filetype for the given filename and buffer.
|
--- Perform filetype detection.
|
||||||
---
|
---
|
||||||
---@param name string File name (can be an absolute or relative path)
|
--- The filetype can be detected using one of three methods:
|
||||||
---@param bufnr number|nil The buffer to set the filetype for. Defaults to the current buffer.
|
--- 1. Using an existing buffer
|
||||||
|
--- 2. Using only a file name
|
||||||
|
--- 3. Using only file contents
|
||||||
|
---
|
||||||
|
--- Of these, option 1 provides the most accurate result as it uses both the buffer's filename and
|
||||||
|
--- (optionally) the buffer contents. Options 2 and 3 can be used without an existing buffer, but
|
||||||
|
--- may not always provide a match in cases where the filename (or contents) cannot unambiguously
|
||||||
|
--- determine the filetype.
|
||||||
|
---
|
||||||
|
--- Each of the three options is specified using a key to the single argument of this function.
|
||||||
|
--- Example:
|
||||||
|
---
|
||||||
|
--- <pre>
|
||||||
|
--- -- Using a buffer number
|
||||||
|
--- vim.filetype.match({ buf = 42 })
|
||||||
|
---
|
||||||
|
--- -- Using a filename
|
||||||
|
--- vim.filetype.match({ filename = "main.lua" })
|
||||||
|
---
|
||||||
|
--- -- Using file contents
|
||||||
|
--- vim.filetype.match({ contents = {"#!/usr/bin/env bash"} })
|
||||||
|
--- </pre>
|
||||||
|
---
|
||||||
|
---@param arg table Table specifying which matching strategy to use. It is an error to provide more
|
||||||
|
--- than one strategy. Accepted keys are:
|
||||||
|
--- * buf (number): Buffer number to use for matching
|
||||||
|
--- * filename (string): Filename to use for matching. Note that the file need not
|
||||||
|
--- actually exist in the filesystem, only the name itself is
|
||||||
|
--- used.
|
||||||
|
--- * contents (table): An array of lines representing file contents to use for
|
||||||
|
--- matching.
|
||||||
---@return string|nil If a match was found, the matched filetype.
|
---@return string|nil If a match was found, the matched filetype.
|
||||||
---@return function|nil A function that modifies buffer state when called (for example, to set some
|
---@return function|nil A function that modifies buffer state when called (for example, to set some
|
||||||
--- filetype specific buffer variables). The function accepts a buffer number as
|
--- filetype specific buffer variables). The function accepts a buffer number as
|
||||||
--- its only argument.
|
--- its only argument.
|
||||||
function M.match(name, bufnr)
|
function M.match(arg)
|
||||||
vim.validate({
|
vim.validate({
|
||||||
name = { name, 's' },
|
arg = { arg, 't' },
|
||||||
bufnr = { bufnr, 'n', true },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
-- When fired from the main filetypedetect autocommand the {bufnr} argument is omitted, so we use
|
if not (arg.buf or arg.filename or arg.contents) then
|
||||||
-- the current buffer. The {bufnr} argument is provided to allow extensibility in case callers
|
error('One of "buf", "filename", or "contents" must be given')
|
||||||
-- wish to perform filetype detection on buffers other than the current one.
|
end
|
||||||
bufnr = bufnr or api.nvim_get_current_buf()
|
|
||||||
|
|
||||||
|
if (arg.buf and arg.filename) or (arg.buf and arg.contents) or (arg.filename and arg.contents) then
|
||||||
|
error('Only one of "buf", "filename", or "contents" must be given')
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufnr = arg.buf
|
||||||
|
local name = bufnr and api.nvim_buf_get_name(bufnr) or arg.filename
|
||||||
|
local contents = arg.contents
|
||||||
|
|
||||||
|
if name then
|
||||||
name = normalize_path(name)
|
name = normalize_path(name)
|
||||||
|
end
|
||||||
|
|
||||||
local ft, on_detect
|
local ft, on_detect
|
||||||
|
|
||||||
|
if not (bufnr or name) then
|
||||||
|
-- Sanity check: this should not happen
|
||||||
|
assert(contents, 'contents should be non-nil when bufnr and filename are nil')
|
||||||
|
-- TODO: "scripts.lua" content matching
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- First check for the simple case where the full path exists as a key
|
-- First check for the simple case where the full path exists as a key
|
||||||
local path = vim.fn.resolve(vim.fn.fnamemodify(name, ':p'))
|
local path = vim.fn.resolve(vim.fn.fnamemodify(name, ':p'))
|
||||||
ft, on_detect = dispatch(filename[path], path, bufnr)
|
ft, on_detect = dispatch(filename[path], path, bufnr)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ local exec_lua = helpers.exec_lua
|
|||||||
local eq = helpers.eq
|
local eq = helpers.eq
|
||||||
local clear = helpers.clear
|
local clear = helpers.clear
|
||||||
local pathroot = helpers.pathroot
|
local pathroot = helpers.pathroot
|
||||||
|
local command = helpers.command
|
||||||
|
|
||||||
local root = pathroot()
|
local root = pathroot()
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ describe('vim.filetype', function()
|
|||||||
rs = 'radicalscript',
|
rs = 'radicalscript',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return vim.filetype.match('main.rs')
|
return vim.filetype.match({ filename = 'main.rs' })
|
||||||
]])
|
]])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ describe('vim.filetype', function()
|
|||||||
['main.rs'] = 'somethingelse',
|
['main.rs'] = 'somethingelse',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return vim.filetype.match('main.rs')
|
return vim.filetype.match({ filename = 'main.rs' })
|
||||||
]])
|
]])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ describe('vim.filetype', function()
|
|||||||
['s_O_m_e_F_i_l_e'] = 'nim',
|
['s_O_m_e_F_i_l_e'] = 'nim',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return vim.filetype.match('s_O_m_e_F_i_l_e')
|
return vim.filetype.match({ filename = 's_O_m_e_F_i_l_e' })
|
||||||
]])
|
]])
|
||||||
|
|
||||||
eq('dosini', exec_lua([[
|
eq('dosini', exec_lua([[
|
||||||
@@ -59,7 +60,7 @@ describe('vim.filetype', function()
|
|||||||
[root .. '/.config/fun/config'] = 'dosini',
|
[root .. '/.config/fun/config'] = 'dosini',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return vim.filetype.match(root .. '/.config/fun/config')
|
return vim.filetype.match({ filename = root .. '/.config/fun/config' })
|
||||||
]], root))
|
]], root))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -72,11 +73,13 @@ describe('vim.filetype', function()
|
|||||||
['~/blog/.*%.txt'] = 'markdown',
|
['~/blog/.*%.txt'] = 'markdown',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return vim.filetype.match('~/blog/why_neovim_is_awesome.txt')
|
return vim.filetype.match({ filename = '~/blog/why_neovim_is_awesome.txt' })
|
||||||
]], root))
|
]], root))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('works with functions', function()
|
it('works with functions', function()
|
||||||
|
command('new')
|
||||||
|
command('file relevant_to_me')
|
||||||
eq('foss', exec_lua [[
|
eq('foss', exec_lua [[
|
||||||
vim.filetype.add({
|
vim.filetype.add({
|
||||||
pattern = {
|
pattern = {
|
||||||
@@ -87,7 +90,7 @@ describe('vim.filetype', function()
|
|||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return vim.filetype.match('relevant_to_me')
|
return vim.filetype.match({ buf = 0 })
|
||||||
]])
|
]])
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user