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:
Gregory Anders
2022-06-26 10:41:20 -06:00
committed by GitHub
parent ae3e371303
commit f3ce06cfa1
4 changed files with 117 additions and 31 deletions

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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)