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