aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonas Strittmatter <40792180+smjonas@users.noreply.github.com>2022-07-03 15:31:56 +0200
committerGitHub <noreply@github.com>2022-07-03 15:31:56 +0200
commitacb7a902812a064fced5ef7d389bd94cb45764bb (patch)
tree0e3a40a07f8223e1144e0d2e7d61810b0711a718
parent0313aba77a447558c3b373370b60eb78067e1c4d (diff)
downloadrneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.tar.gz
rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.tar.bz2
rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.zip
refactor(runtime): port scripts.vim to lua (#18710)
-rw-r--r--runtime/doc/lua.txt54
-rw-r--r--runtime/lua/vim/filetype.lua134
-rw-r--r--runtime/lua/vim/filetype/detect.lua403
-rw-r--r--runtime/scripts.vim6
4 files changed, 498 insertions, 99 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index 0e72e89672..3372bc90f7 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -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.
diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua
index 8fe631e7ed..2874ea45e7 100644
--- a/runtime/lua/vim/filetype.lua
+++ b/runtime/lua/vim/filetype.lua
@@ -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
+ 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
diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua
index 14a4381835..37922b4ebf 100644
--- a/runtime/lua/vim/filetype/detect.lua
+++ b/runtime/lua/vim/filetype/detect.lua
@@ -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
diff --git a/runtime/scripts.vim b/runtime/scripts.vim
index 36763a4a82..49e9e259a2 100644
--- a/runtime/scripts.vim
+++ b/runtime/scripts.vim
@@ -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