mirror of
https://github.com/neovim/neovim
synced 2025-07-16 09:11:51 +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
|
||||
(see example).
|
||||
|
||||
match({name}, {bufnr}) *vim.filetype.match()*
|
||||
Find the filetype for the given filename and buffer.
|
||||
match({arg}) *vim.filetype.match()*
|
||||
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: ~
|
||||
{name} (string) File name (can be an absolute or
|
||||
relative path)
|
||||
{bufnr} (number|nil) The buffer to set the filetype for.
|
||||
Defaults to the current buffer.
|
||||
{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.
|
||||
|
@ -12,7 +12,7 @@ vim.api.nvim_create_augroup('filetypedetect', { clear = false })
|
||||
vim.api.nvim_create_autocmd({ 'BufRead', 'BufNewFile' }, {
|
||||
group = 'filetypedetect',
|
||||
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
|
||||
vim.api.nvim_buf_set_option(args.buf, 'filetype', ft)
|
||||
if on_detect then
|
||||
|
@ -2047,7 +2047,7 @@ local pattern = {
|
||||
end
|
||||
end, { priority = -math.huge + 1 }),
|
||||
['XF86Config.*'] = starsetf(function(path, bufnr)
|
||||
return require('vim.filetype.detect').xfree86(bufnr)
|
||||
return require('vim.filetype.detect').xfree86()
|
||||
end),
|
||||
['%.zcompdump.*'] = starsetf('zsh'),
|
||||
-- .zlog* and zlog*
|
||||
@ -2185,17 +2185,24 @@ end
|
||||
local function dispatch(ft, path, bufnr, ...)
|
||||
local on_detect
|
||||
if type(ft) == 'function' then
|
||||
ft, on_detect = ft(path, bufnr, ...)
|
||||
if bufnr then
|
||||
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
|
||||
|
||||
if type(ft) == 'string' then
|
||||
return ft, on_detect
|
||||
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
|
||||
|
||||
---@private
|
||||
@ -2214,29 +2221,74 @@ local function match_pattern(name, path, tail, pat)
|
||||
return matches
|
||||
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)
|
||||
---@param bufnr number|nil The buffer to set the filetype for. Defaults to the current buffer.
|
||||
--- 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:
|
||||
---
|
||||
--- <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 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
|
||||
--- its only argument.
|
||||
function M.match(name, bufnr)
|
||||
function M.match(arg)
|
||||
vim.validate({
|
||||
name = { name, 's' },
|
||||
bufnr = { bufnr, 'n', true },
|
||||
arg = { arg, 't' },
|
||||
})
|
||||
|
||||
-- When fired from the main filetypedetect autocommand the {bufnr} argument is omitted, so we use
|
||||
-- the current buffer. The {bufnr} argument is provided to allow extensibility in case callers
|
||||
-- wish to perform filetype detection on buffers other than the current one.
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
if not (arg.buf or arg.filename or arg.contents) then
|
||||
error('One of "buf", "filename", or "contents" must be given')
|
||||
end
|
||||
|
||||
name = normalize_path(name)
|
||||
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)
|
||||
end
|
||||
|
||||
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
|
||||
local path = vim.fn.resolve(vim.fn.fnamemodify(name, ':p'))
|
||||
ft, on_detect = dispatch(filename[path], path, bufnr)
|
||||
|
@ -3,6 +3,7 @@ local exec_lua = helpers.exec_lua
|
||||
local eq = helpers.eq
|
||||
local clear = helpers.clear
|
||||
local pathroot = helpers.pathroot
|
||||
local command = helpers.command
|
||||
|
||||
local root = pathroot()
|
||||
|
||||
@ -23,7 +24,7 @@ describe('vim.filetype', function()
|
||||
rs = 'radicalscript',
|
||||
},
|
||||
})
|
||||
return vim.filetype.match('main.rs')
|
||||
return vim.filetype.match({ filename = 'main.rs' })
|
||||
]])
|
||||
end)
|
||||
|
||||
@ -37,7 +38,7 @@ describe('vim.filetype', function()
|
||||
['main.rs'] = 'somethingelse',
|
||||
},
|
||||
})
|
||||
return vim.filetype.match('main.rs')
|
||||
return vim.filetype.match({ filename = 'main.rs' })
|
||||
]])
|
||||
end)
|
||||
|
||||
@ -48,7 +49,7 @@ describe('vim.filetype', function()
|
||||
['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([[
|
||||
@ -59,7 +60,7 @@ describe('vim.filetype', function()
|
||||
[root .. '/.config/fun/config'] = 'dosini',
|
||||
},
|
||||
})
|
||||
return vim.filetype.match(root .. '/.config/fun/config')
|
||||
return vim.filetype.match({ filename = root .. '/.config/fun/config' })
|
||||
]], root))
|
||||
end)
|
||||
|
||||
@ -72,11 +73,13 @@ describe('vim.filetype', function()
|
||||
['~/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))
|
||||
end)
|
||||
|
||||
it('works with functions', function()
|
||||
command('new')
|
||||
command('file relevant_to_me')
|
||||
eq('foss', exec_lua [[
|
||||
vim.filetype.add({
|
||||
pattern = {
|
||||
@ -87,7 +90,7 @@ describe('vim.filetype', function()
|
||||
end,
|
||||
}
|
||||
})
|
||||
return vim.filetype.match('relevant_to_me')
|
||||
return vim.filetype.match({ buf = 0 })
|
||||
]])
|
||||
end)
|
||||
end)
|
||||
|
Reference in New Issue
Block a user