aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/filetype
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 /runtime/lua/vim/filetype
parent0313aba77a447558c3b373370b60eb78067e1c4d (diff)
downloadrneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.tar.gz
rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.tar.bz2
rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.zip
refactor(runtime): port scripts.vim to lua (#18710)
Diffstat (limited to 'runtime/lua/vim/filetype')
-rw-r--r--runtime/lua/vim/filetype/detect.lua403
1 files changed, 376 insertions, 27 deletions
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