refactor(runtime): port scripts.vim to lua (#18710)

This commit is contained in:
Jonas Strittmatter
2022-07-03 15:31:56 +02:00
committed by GitHub
parent 0313aba77a
commit acb7a90281
4 changed files with 499 additions and 100 deletions

View File

@@ -2059,11 +2059,31 @@ add({filetypes}) *vim.filetype.add()*
}) })
< <
To add a fallback match on contents (see
|new-filetype-scripts|), use >
vim.filetype.add {
pattern = {
['.*'] = {
priority = -math.huge,
function(path, bufnr)
local content = vim.filetype.getlines(bufnr, 1)
if vim.filetype.matchregex(content, { [[^#!.*\<mine\>]] }) then
return 'mine'
elseif vim.filetype.matchregex(content, { [[\<drawing\>]] }) then
return 'drawing'
end
end,
},
},
}
<
Parameters: ~ Parameters: ~
{filetypes} (table) A table containing new filetype maps {filetypes} (table) A table containing new filetype maps
(see example). (see example).
match({arg}) *vim.filetype.match()* match({args}) *vim.filetype.match()*
Perform filetype detection. Perform filetype detection.
The filetype can be detected using one of three methods: The filetype can be detected using one of three methods:
@@ -2096,22 +2116,22 @@ match({arg}) *vim.filetype.match()*
< <
Parameters: ~ Parameters: ~
{arg} (table) Table specifying which matching strategy to {args} (table) Table specifying which matching strategy
use. Accepted keys are: to use. Accepted keys are:
• buf (number): Buffer number to use for matching. • buf (number): Buffer number to use for matching.
Mutually exclusive with {contents} Mutually exclusive with {contents}
• filename (string): Filename to use for matching. • filename (string): Filename to use for matching.
When {buf} is given, defaults to the filename of When {buf} is given, defaults to the filename of
the given buffer number. The file need not the given buffer number. The file need not
actually exist in the filesystem. When used actually exist in the filesystem. When used
without {buf} only the name of the file is used without {buf} only the name of the file is used
for filetype matching. This may result in failure for filetype matching. This may result in
to detect the filetype in cases where the failure to detect the filetype in cases where
filename alone is not enough to disambiguate the the filename alone is not enough to disambiguate
filetype. the filetype.
• contents (table): An array of lines representing • contents (table): An array of lines representing
file contents to use for matching. Can be used file contents to use for matching. Can be used
with {filename}. Mutually exclusive with {buf}. with {filename}. Mutually exclusive with {buf}.
Return: ~ Return: ~
(string|nil) If a match was found, the matched filetype. (string|nil) If a match was found, the matched filetype.

View File

@@ -24,18 +24,26 @@ local function starsetf(ft, opts)
end end
---@private ---@private
--- Get a single line or line-range from the buffer. --- Get a single line or line range from the buffer.
--- If only start_lnum is specified, return a single line as a string.
--- If both start_lnum and end_lnum are omitted, return all lines from the buffer.
--- ---
---@param bufnr number|nil The buffer to get the lines from ---@param bufnr number|nil The buffer to get the lines from
---@param start_lnum number The line number of the first line (inclusive, 1-based) ---@param start_lnum number|nil The line number of the first line (inclusive, 1-based)
---@param end_lnum number|nil The line number of the last line (inclusive, 1-based) ---@param end_lnum number|nil The line number of the last line (inclusive, 1-based)
---@return table<string>|string Array of lines, or string when end_lnum is omitted ---@return table<string>|string Array of lines, or string when end_lnum is omitted
function M.getlines(bufnr, start_lnum, end_lnum) function M.getlines(bufnr, start_lnum, end_lnum)
if not end_lnum then if end_lnum then
-- Return a single line as a string -- Return a line range
return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or '' return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum, false)
end
if start_lnum then
-- Return a single line
return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or ''
else
-- Return all lines
return api.nvim_buf_get_lines(bufnr, 0, -1, false)
end end
return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum, false)
end end
---@private ---@private
@@ -600,7 +608,8 @@ local extension = {
end, end,
quake = 'm3quake', quake = 'm3quake',
['m4'] = function(path, bufnr) ['m4'] = function(path, bufnr)
return require('vim.filetype.detect').m4(path) path = path:lower()
return not (path:find('html%.m4$') or path:find('fvwm2rc')) and 'm4'
end, end,
eml = 'mail', eml = 'mail',
mk = 'make', mk = 'make',
@@ -847,22 +856,22 @@ local extension = {
sed = 'sed', sed = 'sed',
sexp = 'sexplib', sexp = 'sexplib',
bash = function(path, bufnr) bash = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
ebuild = function(path, bufnr) ebuild = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
eclass = function(path, bufnr) eclass = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
env = function(path, bufnr) env = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
ksh = function(path, bufnr) ksh = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'ksh') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'ksh')
end, end,
sh = function(path, bufnr) sh = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
sieve = 'sieve', sieve = 'sieve',
siv = 'sieve', siv = 'sieve',
@@ -1090,7 +1099,7 @@ local extension = {
return require('vim.filetype.detect').scd(bufnr) return require('vim.filetype.detect').scd(bufnr)
end, end,
tcsh = function(path, bufnr) tcsh = function(path, bufnr)
return require('vim.filetype.detect').shell(path, bufnr, 'tcsh') return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh')
end, end,
sql = function(path, bufnr) sql = function(path, bufnr)
return vim.g.filetype_sql and vim.g.filetype_sql or 'sql' return vim.g.filetype_sql and vim.g.filetype_sql or 'sql'
@@ -1510,40 +1519,40 @@ local filename = {
['/etc/serial.conf'] = 'setserial', ['/etc/serial.conf'] = 'setserial',
['/etc/udev/cdsymlinks.conf'] = 'sh', ['/etc/udev/cdsymlinks.conf'] = 'sh',
['bash.bashrc'] = function(path, bufnr) ['bash.bashrc'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
bashrc = function(path, bufnr) bashrc = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['.bashrc'] = function(path, bufnr) ['.bashrc'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['.env'] = function(path, bufnr) ['.env'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
['.kshrc'] = function(path, bufnr) ['.kshrc'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'ksh') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'ksh')
end, end,
['.profile'] = function(path, bufnr) ['.profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
['/etc/profile'] = function(path, bufnr) ['/etc/profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
APKBUILD = function(path, bufnr) APKBUILD = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
PKGBUILD = function(path, bufnr) PKGBUILD = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['.tcshrc'] = function(path, bufnr) ['.tcshrc'] = function(path, bufnr)
return require('vim.filetype.detect').shell(path, bufnr, 'tcsh') return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh')
end, end,
['tcsh.login'] = function(path, bufnr) ['tcsh.login'] = function(path, bufnr)
return require('vim.filetype.detect').shell(path, bufnr, 'tcsh') return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh')
end, end,
['tcsh.tcshrc'] = function(path, bufnr) ['tcsh.tcshrc'] = function(path, bufnr)
return require('vim.filetype.detect').shell(path, bufnr, 'tcsh') return require('vim.filetype.detect').shell(path, M.getlines(bufnr), 'tcsh')
end, end,
['/etc/slp.conf'] = 'slpconf', ['/etc/slp.conf'] = 'slpconf',
['/etc/slp.reg'] = 'slpreg', ['/etc/slp.reg'] = 'slpreg',
@@ -1934,28 +1943,28 @@ local pattern = {
['.*/etc/serial%.conf'] = 'setserial', ['.*/etc/serial%.conf'] = 'setserial',
['.*/etc/udev/cdsymlinks%.conf'] = 'sh', ['.*/etc/udev/cdsymlinks%.conf'] = 'sh',
['%.bash[_%-]aliases'] = function(path, bufnr) ['%.bash[_%-]aliases'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['%.bash[_%-]logout'] = function(path, bufnr) ['%.bash[_%-]logout'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['%.bash[_%-]profile'] = function(path, bufnr) ['%.bash[_%-]profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['%.kshrc.*'] = function(path, bufnr) ['%.kshrc.*'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'ksh') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'ksh')
end, end,
['%.profile.*'] = function(path, bufnr) ['%.profile.*'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
['.*/etc/profile'] = function(path, bufnr) ['.*/etc/profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr) return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end, end,
['bash%-fc[%-%.]'] = function(path, bufnr) ['bash%-fc[%-%.]'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr, 'bash') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'bash')
end, end,
['%.tcshrc.*'] = function(path, bufnr) ['%.tcshrc.*'] = function(path, bufnr)
return require('vim.filetype.detect').shell(path, bufnr, 'tcsh') return require('vim.filetype.detect').sh(path, M.getlines(bufnr), 'tcsh')
end, end,
['.*/etc/sudoers%.d/.*'] = starsetf('sudoers'), ['.*/etc/sudoers%.d/.*'] = starsetf('sudoers'),
['.*%._sst%.meta'] = 'sisu', ['.*%._sst%.meta'] = 'sisu',
@@ -2165,6 +2174,25 @@ end
--- }) --- })
--- </pre> --- </pre>
--- ---
--- To add a fallback match on contents (see |new-filetype-scripts|), use
--- <pre>
--- vim.filetype.add {
--- pattern = {
--- ['.*'] = {
--- priority = -math.huge,
--- function(path, bufnr)
--- local content = vim.filetype.getlines(bufnr, 1)
--- if vim.filetype.matchregex(content, { [[^#!.*\\<mine\\>]] }) then
--- return 'mine'
--- elseif vim.filetype.matchregex(content, { [[\\<drawing\\>]] }) then
--- return 'drawing'
--- end
--- end,
--- },
--- },
--- }
--- </pre>
---
---@param filetypes table A table containing new filetype maps (see example). ---@param filetypes table A table containing new filetype maps (see example).
function M.add(filetypes) function M.add(filetypes)
for k, v in pairs(filetypes.extension or {}) do for k, v in pairs(filetypes.extension or {}) do
@@ -2253,7 +2281,7 @@ end
--- vim.filetype.match({ contents = {'#!/usr/bin/env bash'} }) --- vim.filetype.match({ contents = {'#!/usr/bin/env bash'} })
--- </pre> --- </pre>
--- ---
---@param arg table Table specifying which matching strategy to use. Accepted keys are: ---@param args table Table specifying which matching strategy to use. Accepted keys are:
--- * buf (number): Buffer number to use for matching. Mutually exclusive with --- * buf (number): Buffer number to use for matching. Mutually exclusive with
--- {contents} --- {contents}
--- * filename (string): Filename to use for matching. When {buf} is given, --- * filename (string): Filename to use for matching. When {buf} is given,
@@ -2270,22 +2298,18 @@ end
---@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(arg) function M.match(args)
vim.validate({ vim.validate({
arg = { arg, 't' }, arg = { args, 't' },
}) })
if not (arg.buf or arg.filename or arg.contents) then if not (args.buf or args.filename or args.contents) then
error('At least one of "buf", "filename", or "contents" must be given') error('At least one of "buf", "filename", or "contents" must be given')
end end
if arg.buf and arg.contents then local bufnr = args.buf
error('Only one of "buf" or "contents" must be given') local name = args.filename
end local contents = args.contents
local bufnr = arg.buf
local name = arg.filename
local contents = arg.contents
if bufnr and not name then if bufnr and not name then
name = api.nvim_buf_get_name(bufnr) name = api.nvim_buf_get_name(bufnr)
@@ -2297,13 +2321,6 @@ function M.match(arg)
local ft, on_detect local ft, on_detect
if contents then
-- Sanity check: this should not happen
assert(not bufnr, '"buf" and "contents" are mutually exclusive')
-- 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)
@@ -2345,7 +2362,7 @@ function M.match(arg)
return ft, on_detect return ft, on_detect
end end
-- Finally, check patterns with negative priority -- Next, check patterns with negative priority
for i = j, #pattern_sorted do for i = j, #pattern_sorted do
local v = pattern_sorted[i] local v = pattern_sorted[i]
local k = next(v) local k = next(v)
@@ -2359,6 +2376,19 @@ function M.match(arg)
end end
end end
end end
-- Finally, check file contents
if contents or bufnr then
contents = contents or M.getlines(bufnr)
-- If name is nil, catch any errors from the contents filetype detection function.
-- If the function tries to use the filename that is nil then it will fail,
-- but this enables checks which do not need a filename to still work.
local ok
ok, ft = pcall(require('vim.filetype.detect').match_contents, contents, name)
if ok and ft then
return ft
end
end
end end
return M return M

View File

@@ -206,12 +206,52 @@ function M.csh(path, bufnr)
-- Filetype was already detected -- Filetype was already detected
return return
end end
local contents = getlines(bufnr)
if vim.g.filetype_csh then if vim.g.filetype_csh then
return M.shell(path, bufnr, vim.g.filetype_csh) return M.shell(path, contents, vim.g.filetype_csh)
elseif string.find(vim.o.shell, 'tcsh') then elseif string.find(vim.o.shell, 'tcsh') then
return M.shell(path, bufnr, 'tcsh') return M.shell(path, contents, 'tcsh')
else else
return M.shell(path, bufnr, 'csh') return M.shell(path, contents, 'csh')
end
end
local function cvs_diff(path, contents)
for _, line in ipairs(contents) do
if not line:find('^%? ') then
if matchregex(line, [[^Index:\s\+\f\+$]]) then
-- CVS diff
return 'diff'
elseif
-- Locale input files: Formal Definitions of Cultural Conventions
-- Filename must be like en_US, fr_FR@euro or en_US.UTF-8
findany(path, { '%a%a_%a%a$', '%a%a_%a%a[%.@]', '%a%a_%a%ai18n$', '%a%a_%a%aPOSIX$', '%a%a_%a%atranslit_' })
then
-- Only look at the first 100 lines
for line_nr = 1, 100 do
if not contents[line_nr] then
break
elseif
findany(contents[line_nr], {
'^LC_IDENTIFICATION$',
'^LC_CTYPE$',
'^LC_COLLATE$',
'^LC_MONETARY$',
'^LC_NUMERIC$',
'^LC_TIME$',
'^LC_MESSAGES$',
'^LC_PAPER$',
'^LC_TELEPHONE$',
'^LC_MEASUREMENT$',
'^LC_NAME$',
'^LC_ADDRESS$',
})
then
return 'fdcc'
end
end
end
end
end end
end end
@@ -270,6 +310,38 @@ function M.dep3patch(path, bufnr)
end end
end end
local function diff(contents)
if
contents[1]:find('^%-%-%- ') and contents[2]:find('^%+%+%+ ')
or contents[1]:find('^%* looking for ') and contents[2]:find('^%* comparing to ')
or contents[1]:find('^%*%*%* ') and contents[2]:find('^%-%-%- ')
or contents[1]:find('^=== ') and ((contents[2]:find('^' .. string.rep('=', 66)) and contents[3]:find('^%-%-% ') and contents[4]:find(
'^%+%+%+'
)) or (contents[2]:find('^%-%-%- ') and contents[3]:find('^%+%+%+ ')))
or findany(contents[1], { '^=== removed', '^=== added', '^=== renamed', '^=== modified' })
then
return 'diff'
end
end
function M.dns_zone(contents)
if
findany(
contents[1] .. contents[2] .. contents[3] .. contents[4],
{ '^; <<>> DiG [0-9%.]+.* <<>>', '%$ORIGIN', '%$TTL', 'IN%s+SOA' }
)
then
return 'bindzone'
end
-- BAAN
if -- Check for 1 to 80 '*' characters
contents[1]:find('|%*' .. string.rep('%*?', 79)) and contents[2]:find('VRC ')
or contents[2]:find('|%*' .. string.rep('%*?', 79)) and contents[3]:find('VRC ')
then
return 'baan'
end
end
function M.dtrace(bufnr) function M.dtrace(bufnr)
if vim.fn.did_filetype() ~= 0 then if vim.fn.did_filetype() ~= 0 then
-- Filetype was already detected -- Filetype was already detected
@@ -483,7 +555,7 @@ function M.install(path, bufnr)
if getlines(bufnr, 1):lower():find('<%?php') then if getlines(bufnr, 1):lower():find('<%?php') then
return 'php' return 'php'
end end
return M.sh(path, bufnr, 'bash') return M.sh(path, getlines(bufnr), 'bash')
end end
-- Innovation Data Processing -- Innovation Data Processing
@@ -572,10 +644,15 @@ function M.m(bufnr)
end end
end end
function M.m4(path) local function m4(contents)
path = path:lower() for _, line in ipairs(contents) do
if not path:find('html%.m4$') and not path:find('fvwm2rc') then if matchregex(line, [[^\s*dnl\>]]) then
return 'm4' return 'm4'
end
end
if vim.env.TERM == 'amiga' and findany(contents[1]:lower(), { '^;', '^%.bra' }) then
-- AmigaDos scripts
return 'amiga'
end end
end end
@@ -625,7 +702,7 @@ end
local function is_lprolog(bufnr) local function is_lprolog(bufnr)
-- Skip apparent comments and blank lines, what looks like -- Skip apparent comments and blank lines, what looks like
-- LambdaProlog comment may be RAPID header -- LambdaProlog comment may be RAPID header
for _, line in ipairs(getlines(bufnr, 1, -1)) do for _, line in ipairs(getlines(bufnr)) do
-- The second pattern matches a LambdaProlog comment -- The second pattern matches a LambdaProlog comment
if not findany(line, { '^%s*$', '^%s*%%' }) then if not findany(line, { '^%s*$', '^%s*%%' }) then
-- The pattern must not catch a go.mod file -- The pattern must not catch a go.mod file
@@ -982,24 +1059,26 @@ function M.sgml(bufnr)
end end
end end
function M.sh(path, bufnr, name) function M.sh(path, contents, name)
if vim.fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then -- Path may be nil, do not fail in that case
if vim.fn.did_filetype() ~= 0 or (path or ''):find(vim.g.ft_ignore_pat) then
-- Filetype was already detected or detection should be skipped -- Filetype was already detected or detection should be skipped
return return
end end
local on_detect local on_detect
name = name or getlines(bufnr, 1) -- Get the name from the first line if not specified
name = name or contents[1]
if matchregex(name, [[\<csh\>]]) then if matchregex(name, [[\<csh\>]]) then
-- Some .sh scripts contain #!/bin/csh. -- Some .sh scripts contain #!/bin/csh.
return M.shell(path, bufnr, 'csh') return M.shell(path, contents, 'csh')
-- Some .sh scripts contain #!/bin/tcsh. -- Some .sh scripts contain #!/bin/tcsh.
elseif matchregex(name, [[\<tcsh\>]]) then elseif matchregex(name, [[\<tcsh\>]]) then
return M.shell(path, bufnr, 'tcsh') return M.shell(path, contents, 'tcsh')
-- Some .sh scripts contain #!/bin/zsh. -- Some .sh scripts contain #!/bin/zsh.
elseif matchregex(name, [[\<zsh\>]]) then elseif matchregex(name, [[\<zsh\>]]) then
return M.shell(path, bufnr, 'zsh') return M.shell(path, contents, 'zsh')
elseif matchregex(name, [[\<ksh\>]]) then elseif matchregex(name, [[\<ksh\>]]) then
on_detect = function(b) on_detect = function(b)
vim.b[b].is_kornshell = 1 vim.b[b].is_kornshell = 1
@@ -1019,27 +1098,30 @@ function M.sh(path, bufnr, name)
vim.b[b].is_bash = nil vim.b[b].is_bash = nil
end end
end end
return M.shell(path, bufnr, 'sh'), on_detect return M.shell(path, contents, 'sh'), on_detect
end end
-- For shell-like file types, check for an "exec" command hidden in a comment, as used for Tcl. -- For shell-like file types, check for an "exec" command hidden in a comment, as used for Tcl.
-- Also called from scripts.vim, thus can't be local to this script. [TODO] function M.shell(path, contents, name)
function M.shell(path, bufnr, name)
if vim.fn.did_filetype() ~= 0 or matchregex(path, vim.g.ft_ignore_pat) then if vim.fn.did_filetype() ~= 0 or matchregex(path, vim.g.ft_ignore_pat) then
-- Filetype was already detected or detection should be skipped -- Filetype was already detected or detection should be skipped
return return
end end
local prev_line = '' local prev_line = ''
for _, line in ipairs(getlines(bufnr, 2, -1)) do for line_nr, line in ipairs(contents) do
line = line:lower() -- Skip the first line
if line:find('%s*exec%s') and not prev_line:find('^%s*#.*\\$') then if line_nr ~= 1 then
-- Found an "exec" line after a comment with continuation line = line:lower()
local n = line:gsub('%s*exec%s+([^ ]*/)?', '', 1) if line:find('%s*exec%s') and not prev_line:find('^%s*#.*\\$') then
if matchregex(n, [[\c\<tclsh\|\<wish]]) then -- Found an "exec" line after a comment with continuation
return 'tcl' local n = line:gsub('%s*exec%s+([^ ]*/)?', '', 1)
if matchregex(n, [[\c\<tclsh\|\<wish]]) then
return 'tcl'
end
end end
prev_line = line
end end
prev_line = line
end end
return name return name
end end
@@ -1123,7 +1205,7 @@ end
-- Determine if a *.tf file is TF mud client or terraform -- Determine if a *.tf file is TF mud client or terraform
function M.tf(bufnr) function M.tf(bufnr)
for _, line in ipairs(getlines(bufnr, 1, -1)) do for _, line in ipairs(getlines(bufnr)) do
-- Assume terraform file on a non-empty line (not whitespace-only) -- Assume terraform file on a non-empty line (not whitespace-only)
-- and when the first non-whitespace character is not a ; or / -- and when the first non-whitespace character is not a ; or /
if not line:find('^%s*$') and not line:find('^%s*[;/]') then if not line:find('^%s*$') and not line:find('^%s*[;/]') then
@@ -1204,4 +1286,271 @@ end
-- luacheck: pop -- luacheck: pop
-- luacheck: pop -- luacheck: pop
local patterns_hashbang = {
['^zsh\\>'] = { 'zsh', { vim_regex = true } },
['^\\(tclsh\\|wish\\|expectk\\|itclsh\\|itkwish\\)\\>'] = { 'tcl', { vim_regex = true } },
['^expect\\>'] = { 'expect', { vim_regex = true } },
['^gnuplot\\>'] = { 'gnuplot', { vim_regex = true } },
['make\\>'] = { 'make', { vim_regex = true } },
['^pike\\%(\\>\\|[0-9]\\)'] = { 'pike', { vim_regex = true } },
lua = 'lua',
perl = 'perl',
php = 'php',
python = 'python',
['^groovy\\>'] = { 'groovy', { vim_regex = true } },
raku = 'raku',
ruby = 'ruby',
['node\\(js\\)\\=\\>\\|js\\>'] = { 'javascript', { vim_regex = true } },
['rhino\\>'] = { 'javascript', { vim_regex = true } },
-- BC calculator
['^bc\\>'] = { 'bc', { vim_regex = true } },
['sed\\>'] = { 'sed', { vim_regex = true } },
ocaml = 'ocaml',
-- Awk scripts; also finds "gawk"
['awk\\>'] = { 'awk', { vim_regex = true } },
wml = 'wml',
scheme = 'scheme',
cfengine = 'cfengine',
escript = 'erlang',
haskell = 'haskell',
clojure = 'clojure',
['scala\\>'] = { 'scala', { vim_regex = true } },
-- Free Pascal
['instantfpc\\>'] = { 'pascal', { vim_regex = true } },
['fennel\\>'] = { 'fennel', { vim_regex = true } },
-- MikroTik RouterOS script
['rsc\\>'] = { 'routeros', { vim_regex = true } },
['fish\\>'] = { 'fish', { vim_regex = true } },
['gforth\\>'] = { 'forth', { vim_regex = true } },
['icon\\>'] = { 'icon', { vim_regex = true } },
}
---@private
-- File starts with "#!".
local function match_from_hashbang(contents, path)
local first_line = contents[1]
-- Check for a line like "#!/usr/bin/env {options} bash". Turn it into
-- "#!/usr/bin/bash" to make matching easier.
-- Recognize only a few {options} that are commonly used.
if matchregex(first_line, [[^#!\s*\S*\<env\s]]) then
first_line = first_line:gsub('%S+=%S+', '')
first_line = first_line
:gsub('%-%-ignore%-environment', '', 1)
:gsub('%-%-split%-string', '', 1)
:gsub('%-[iS]', '', 1)
first_line = vim.fn.substitute(first_line, [[\<env\s\+]], '', '')
end
-- Get the program name.
-- Only accept spaces in PC style paths: "#!c:/program files/perl [args]".
-- If the word env is used, use the first word after the space:
-- "#!/usr/bin/env perl [path/args]"
-- If there is no path use the first word: "#!perl [path/args]".
-- Otherwise get the last word after a slash: "#!/usr/bin/perl [path/args]".
local name
if first_line:find('^#!%s*%a:[/\\]') then
name = vim.fn.substitute(first_line, [[^#!.*[/\\]\(\i\+\).*]], '\\1', '')
elseif matchregex(first_line, [[^#!.*\<env\>]]) then
name = vim.fn.substitute(first_line, [[^#!.*\<env\>\s\+\(\i\+\).*]], '\\1', '')
elseif matchregex(first_line, [[^#!\s*[^/\\ ]*\>\([^/\\]\|$\)]]) then
name = vim.fn.substitute(first_line, [[^#!\s*\([^/\\ ]*\>\).*]], '\\1', '')
else
name = vim.fn.substitute(first_line, [[^#!\s*\S*[/\\]\(\i\+\).*]], '\\1', '')
end
-- tcl scripts may have #!/bin/sh in the first line and "exec wish" in the
-- third line. Suggested by Steven Atkinson.
if contents[3] and contents[3]:find('^exec wish') then
name = 'wish'
end
if matchregex(name, [[^\(bash\d*\|\|ksh\d*\|sh\)\>]]) then
-- Bourne-like shell scripts: bash bash2 ksh ksh93 sh
return require('vim.filetype.detect').sh(path, contents, first_line)
elseif matchregex(name, [[^csh\>]]) then
return require('vim.filetype.detect').shell(path, contents, vim.g.filetype_csh or 'csh')
elseif matchregex(name, [[^tcsh\>]]) then
return require('vim.filetype.detect').shell(path, contents, 'tcsh')
end
for k, v in pairs(patterns_hashbang) do
local ft = type(v) == 'table' and v[1] or v
local opts = type(v) == 'table' and v[2] or {}
if opts.vim_regex and matchregex(name, k) or name:find(k) then
return ft
end
end
end
local patterns_text = {
['^#compdef\\>'] = { 'zsh', { vim_regex = true } },
['^#autoload\\>'] = { 'zsh', { vim_regex = true } },
-- ELM Mail files
['^From [a-zA-Z][a-zA-Z_0-9%.=%-]*(@[^ ]*)? .* 19%d%d$'] = 'mail',
['^From [a-zA-Z][a-zA-Z_0-9%.=%-]*(@[^ ]*)? .* 20%d%d$'] = 'mail',
['^From %- .* 19%d%d$'] = 'mail',
['^From %- .* 20%d%d$'] = 'mail',
-- Mason
['^<[%%&].*>'] = 'mason',
-- Vim scripts (must have '" vim' as the first line to trigger this)
['^" *[vV]im$['] = 'vim',
-- libcxx and libstdc++ standard library headers like ["iostream["] do not have
-- an extension, recognize the Emacs file mode.
['%-%*%-.*[cC]%+%+.*%-%*%-'] = 'cpp',
['^\\*\\* LambdaMOO Database, Format Version \\%([1-3]\\>\\)\\@!\\d\\+ \\*\\*$'] = {
'moo',
{ vim_regex = true },
},
-- Diff file:
-- - "diff" in first line (context diff)
-- - "Only in " in first line
-- - "--- " in first line and "+++ " in second line (unified diff).
-- - "*** " in first line and "--- " in second line (context diff).
-- - "# It was generated by makepatch " in the second line (makepatch diff).
-- - "Index: <filename>" in the first line (CVS file)
-- - "=== ", line of "=", "---", "+++ " (SVK diff)
-- - "=== ", "--- ", "+++ " (bzr diff, common case)
-- - "=== (removed|added|renamed|modified)" (bzr diff, alternative)
-- - "# HG changeset patch" in first line (Mercurial export format)
['^\\(diff\\>\\|Only in \\|\\d\\+\\(,\\d\\+\\)\\=[cda]\\d\\+\\>\\|# It was generated by makepatch \\|Index:\\s\\+\\f\\+\\r\\=$\\|===== \\f\\+ \\d\\+\\.\\d\\+ vs edited\\|==== //\\f\\+#\\d\\+\\|# HG changeset patch\\)'] = {
'diff',
{ vim_regex = true },
},
function(contents)
return diff(contents)
end,
-- PostScript Files (must have %!PS as the first line, like a2ps output)
['^%%![ \t]*PS'] = 'postscr',
function(contents)
return m4(contents)
end,
-- SiCAD scripts (must have procn or procd as the first line to trigger this)
['^ *proc[nd] *$'] = { 'sicad', { ignore_case = true } },
['^%*%*%*%* Purify'] = 'purifylog',
-- XML
['<%?%s*xml.*%?>'] = 'xml',
-- XHTML (e.g.: PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN")
['\\<DTD\\s\\+XHTML\\s'] = 'xhtml',
-- HTML (e.g.: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN")
-- Avoid "doctype html", used by slim.
['\\c<!DOCTYPE\\s\\+html\\>'] = { 'html', { vim_regex = true } },
-- PDF
['^%%PDF%-'] = 'pdf',
-- XXD output
['^%x%x%x%x%x%x%x: %x%x ?%x%x ?%x%x ?%x%x '] = 'xxd',
-- RCS/CVS log output
['^RCS file:'] = { 'rcslog', { start_lnum = 1, end_lnum = 2 } },
-- CVS commit
['^CVS:'] = { 'cvs', { start_lnum = 2 } },
['^CVS: '] = { 'cvs', { start_lnum = -1 } },
-- Prescribe
['^!R!'] = 'prescribe',
-- Send-pr
['^SEND%-PR:'] = 'sendpr',
-- SNNS files
['^SNNS network definition file'] = 'snnsnet',
['^SNNS pattern definition file'] = 'snnspat',
['^SNNS result file'] = 'snnsres',
['^%%.-[Vv]irata'] = { 'virata', { start_lnum = 1, end_lnum = 5 } },
['[0-9:%.]* *execve%('] = 'strace',
['^__libc_start_main'] = 'strace',
-- VSE JCL
['^\\* $$ JOB\\>'] = { 'vsejcl', { vim_regex = true } },
['^// *JOB\\>'] = { 'vsejcl', { vim_regex = true } },
-- TAK and SINDA
['K & K Associates'] = { 'takout', { start_lnum = 4 } },
['TAK 2000'] = { 'takout', { start_lnum = 2 } },
['S Y S T E M S I M P R O V E D '] = { 'syndaout', { start_lnum = 3 } },
['Run Date: '] = { 'takcmp', { start_lnum = 6 } },
['Node File 1'] = { 'sindacmp', { start_lnum = 9 } },
function(contents)
require('vim.filetype.detect').dns_zone(contents)
end,
-- Valgrind
['^==%d+== valgrind'] = 'valgrind',
['^==%d+== Using valgrind'] = { 'valgrind', { start_lnum = 3 } },
-- Go docs
['PACKAGE DOCUMENTATION$'] = 'godoc',
-- Renderman Interface Bytestream
['^##RenderMan'] = 'rib',
-- Scheme scripts
['exec%s%+%S*scheme'] = { 'scheme', { start_lnum = 1, end_lnum = 2 } },
-- Git output
['^\\(commit\\|tree\\|object\\) \\x\\{40,\\}\\>\\|^tag \\S\\+$'] = { 'git', { vim_regex = true } },
function(lines)
-- Gprof (gnu profiler)
if lines[1] == 'Flat profile:' and lines[2] == '' and lines[3]:find('^Each sample counts as .* seconds%.$') then
return 'gprof'
end
end,
-- Erlang terms
-- (See also: http://www.gnu.org/software/emacs/manual/html_node/emacs/Choosing-Modes.html#Choosing-Modes)
['%-%*%-.*erlang.*%-%*%-'] = { 'erlang', { ignore_case = true } },
-- YAML
['^%%YAML'] = 'yaml',
-- MikroTik RouterOS script
['^#.*by RouterOS'] = 'routeros',
-- Sed scripts
-- #ncomment is allowed but most likely a false positive so require a space before any trailing comment text
['^#n%s'] = 'sed',
['^#n$'] = 'sed',
}
---@private
-- File does not start with "#!".
local function match_from_text(contents, path)
if contents[1]:find('^:$') then
-- Bourne-like shell scripts: sh ksh bash bash2
return M.sh(path, contents)
elseif matchregex('\n' .. table.concat(contents, '\n'), [[\n\s*emulate\s\+\%(-[LR]\s\+\)\=[ckz]\=sh\>]]) then
-- Z shell scripts
return 'zsh'
end
for k, v in pairs(patterns_text) do
if type(v) == 'string' then
-- Check the first line only
if contents[1]:find(k) then
return v
end
elseif type(v) == 'function' then
-- If filetype detection fails, continue with the next pattern
local ok, ft = pcall(v, contents)
if ok and ft then
return ft
end
else
local opts = type(v) == 'table' and v[2] or {}
if opts.start_lnum and opts.end_lnum then
assert(not opts.ignore_case, 'ignore_case=true is ignored when start_lnum is also present, needs refactor')
for i = opts.start_lnum, opts.end_lnum do
if not contents[i] then
break
elseif contents[i]:find(k) then
return v[1]
end
end
else
local line_nr = opts.start_lnum == -1 and #contents or opts.start_lnum or 1
if contents[line_nr] then
local line = opts.ignore_case and contents[line_nr]:lower() or contents[line_nr]
if opts.vim_regex and matchregex(line, k) or line:find(k) then
return v[1]
end
end
end
end
end
return cvs_diff(path, contents)
end
M.match_contents = function(contents, path)
local first_line = contents[1]
if first_line:find('^#!') then
return match_from_hashbang(contents, path)
else
return match_from_text(contents, path)
end
end
return M return M

View File

@@ -11,9 +11,9 @@
" 'ignorecase' option making a difference. Where case is to be ignored use " 'ignorecase' option making a difference. Where case is to be ignored use
" =~? instead. Do not use =~ anywhere. " =~? instead. Do not use =~ anywhere.
" Only do the rest when not using Lua filetype detection
" Only do the rest when the FileType autocommand has not been triggered yet. " and the FileType autocommand has not been triggered yet.
if did_filetype() if exists("g:do_filetype_lua") && g:do_filetype_lua || did_filetype()
finish finish
endif endif