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: ~
{filetypes} (table) A table containing new filetype maps
(see example).
match({arg}) *vim.filetype.match()*
match({args}) *vim.filetype.match()*
Perform filetype detection.
The filetype can be detected using one of three methods:
@@ -2096,22 +2116,22 @@ match({arg}) *vim.filetype.match()*
<
Parameters: ~
{arg} (table) Table specifying which matching strategy to
use. Accepted keys are:
• buf (number): Buffer number to use for matching.
Mutually exclusive with {contents}
• filename (string): Filename to use for matching.
When {buf} is given, defaults to the filename of
the given buffer number. The file need not
actually exist in the filesystem. When used
without {buf} only the name of the file is used
for filetype matching. This may result in failure
to detect the filetype in cases where the
filename alone is not enough to disambiguate the
filetype.
• contents (table): An array of lines representing
file contents to use for matching. Can be used
with {filename}. Mutually exclusive with {buf}.
{args} (table) Table specifying which matching strategy
to use. Accepted keys are:
• buf (number): Buffer number to use for matching.
Mutually exclusive with {contents}
• filename (string): Filename to use for matching.
When {buf} is given, defaults to the filename of
the given buffer number. The file need not
actually exist in the filesystem. When used
without {buf} only the name of the file is used
for filetype matching. This may result in
failure to detect the filetype in cases where
the filename alone is not enough to disambiguate
the filetype.
• contents (table): An array of lines representing
file contents to use for matching. Can be used
with {filename}. Mutually exclusive with {buf}.
Return: ~
(string|nil) If a match was found, the matched filetype.

View File

@@ -24,18 +24,26 @@ local function starsetf(ft, opts)
end
---@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 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)
---@return table<string>|string Array of lines, or string when end_lnum is omitted
function M.getlines(bufnr, start_lnum, end_lnum)
if not end_lnum then
-- Return a single line as a string
return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or ''
if end_lnum then
-- Return a line range
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
return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum, false)
end
---@private
@@ -600,7 +608,8 @@ local extension = {
end,
quake = 'm3quake',
['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,
eml = 'mail',
mk = 'make',
@@ -847,22 +856,22 @@ local extension = {
sed = 'sed',
sexp = 'sexplib',
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,
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,
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,
env = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
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,
sh = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
sieve = 'sieve',
siv = 'sieve',
@@ -1090,7 +1099,7 @@ local extension = {
return require('vim.filetype.detect').scd(bufnr)
end,
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,
sql = function(path, bufnr)
return vim.g.filetype_sql and vim.g.filetype_sql or 'sql'
@@ -1510,40 +1519,40 @@ local filename = {
['/etc/serial.conf'] = 'setserial',
['/etc/udev/cdsymlinks.conf'] = 'sh',
['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,
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,
['.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,
['.env'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
['.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,
['.profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
['/etc/profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
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,
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,
['.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,
['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,
['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,
['/etc/slp.conf'] = 'slpconf',
['/etc/slp.reg'] = 'slpreg',
@@ -1934,28 +1943,28 @@ local pattern = {
['.*/etc/serial%.conf'] = 'setserial',
['.*/etc/udev/cdsymlinks%.conf'] = 'sh',
['%.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,
['%.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,
['%.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,
['%.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,
['%.profile.*'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
['.*/etc/profile'] = function(path, bufnr)
return require('vim.filetype.detect').sh(path, bufnr)
return require('vim.filetype.detect').sh(path, M.getlines(bufnr))
end,
['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,
['%.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,
['.*/etc/sudoers%.d/.*'] = starsetf('sudoers'),
['.*%._sst%.meta'] = 'sisu',
@@ -2165,6 +2174,25 @@ end
--- })
--- </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).
function M.add(filetypes)
for k, v in pairs(filetypes.extension or {}) do
@@ -2253,7 +2281,7 @@ end
--- vim.filetype.match({ contents = {'#!/usr/bin/env bash'} })
--- </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
--- {contents}
--- * 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
--- filetype specific buffer variables). The function accepts a buffer number as
--- its only argument.
function M.match(arg)
function M.match(args)
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')
end
if arg.buf and arg.contents then
error('Only one of "buf" or "contents" must be given')
end
local bufnr = arg.buf
local name = arg.filename
local contents = arg.contents
local bufnr = args.buf
local name = args.filename
local contents = args.contents
if bufnr and not name then
name = api.nvim_buf_get_name(bufnr)
@@ -2297,13 +2321,6 @@ function M.match(arg)
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
local path = vim.fn.resolve(vim.fn.fnamemodify(name, ':p'))
ft, on_detect = dispatch(filename[path], path, bufnr)
@@ -2345,7 +2362,7 @@ function M.match(arg)
return ft, on_detect
end
-- Finally, check patterns with negative priority
-- Next, check patterns with negative priority
for i = j, #pattern_sorted do
local v = pattern_sorted[i]
local k = next(v)
@@ -2359,6 +2376,19 @@ function M.match(arg)
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
return M

View File

@@ -206,12 +206,52 @@ function M.csh(path, bufnr)
-- Filetype was already detected
return
end
local contents = getlines(bufnr)
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
return M.shell(path, bufnr, 'tcsh')
return M.shell(path, contents, 'tcsh')
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
@@ -270,6 +310,38 @@ function M.dep3patch(path, bufnr)
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)
if vim.fn.did_filetype() ~= 0 then
-- Filetype was already detected
@@ -483,7 +555,7 @@ function M.install(path, bufnr)
if getlines(bufnr, 1):lower():find('<%?php') then
return 'php'
end
return M.sh(path, bufnr, 'bash')
return M.sh(path, getlines(bufnr), 'bash')
end
-- Innovation Data Processing
@@ -572,10 +644,15 @@ function M.m(bufnr)
end
end
function M.m4(path)
path = path:lower()
if not path:find('html%.m4$') and not path:find('fvwm2rc') then
return 'm4'
local function m4(contents)
for _, line in ipairs(contents) do
if matchregex(line, [[^\s*dnl\>]]) then
return 'm4'
end
end
if vim.env.TERM == 'amiga' and findany(contents[1]:lower(), { '^;', '^%.bra' }) then
-- AmigaDos scripts
return 'amiga'
end
end
@@ -625,7 +702,7 @@ end
local function is_lprolog(bufnr)
-- Skip apparent comments and blank lines, what looks like
-- 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
if not findany(line, { '^%s*$', '^%s*%%' }) then
-- The pattern must not catch a go.mod file
@@ -982,24 +1059,26 @@ function M.sgml(bufnr)
end
end
function M.sh(path, bufnr, name)
if vim.fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then
function M.sh(path, contents, name)
-- 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
return
end
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
-- Some .sh scripts contain #!/bin/csh.
return M.shell(path, bufnr, 'csh')
return M.shell(path, contents, 'csh')
-- Some .sh scripts contain #!/bin/tcsh.
elseif matchregex(name, [[\<tcsh\>]]) then
return M.shell(path, bufnr, 'tcsh')
return M.shell(path, contents, 'tcsh')
-- Some .sh scripts contain #!/bin/zsh.
elseif matchregex(name, [[\<zsh\>]]) then
return M.shell(path, bufnr, 'zsh')
return M.shell(path, contents, 'zsh')
elseif matchregex(name, [[\<ksh\>]]) then
on_detect = function(b)
vim.b[b].is_kornshell = 1
@@ -1019,27 +1098,30 @@ function M.sh(path, bufnr, name)
vim.b[b].is_bash = nil
end
end
return M.shell(path, bufnr, 'sh'), on_detect
return M.shell(path, contents, 'sh'), on_detect
end
-- 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, bufnr, name)
function M.shell(path, contents, name)
if vim.fn.did_filetype() ~= 0 or matchregex(path, vim.g.ft_ignore_pat) then
-- Filetype was already detected or detection should be skipped
return
end
local prev_line = ''
for _, line in ipairs(getlines(bufnr, 2, -1)) do
line = line:lower()
if line:find('%s*exec%s') and not prev_line:find('^%s*#.*\\$') then
-- Found an "exec" line after a comment with continuation
local n = line:gsub('%s*exec%s+([^ ]*/)?', '', 1)
if matchregex(n, [[\c\<tclsh\|\<wish]]) then
return 'tcl'
for line_nr, line in ipairs(contents) do
-- Skip the first line
if line_nr ~= 1 then
line = line:lower()
if line:find('%s*exec%s') and not prev_line:find('^%s*#.*\\$') then
-- Found an "exec" line after a comment with continuation
local n = line:gsub('%s*exec%s+([^ ]*/)?', '', 1)
if matchregex(n, [[\c\<tclsh\|\<wish]]) then
return 'tcl'
end
end
prev_line = line
end
prev_line = line
end
return name
end
@@ -1123,7 +1205,7 @@ end
-- Determine if a *.tf file is TF mud client or terraform
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)
-- and when the first non-whitespace character is not a ; or /
if not line:find('^%s*$') and not line:find('^%s*[;/]') then
@@ -1204,4 +1286,271 @@ end
-- 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

View File

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