diff options
| author | Jonas Strittmatter <40792180+smjonas@users.noreply.github.com> | 2022-07-03 15:31:56 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-07-03 15:31:56 +0200 |
| commit | acb7a902812a064fced5ef7d389bd94cb45764bb (patch) | |
| tree | 0e3a40a07f8223e1144e0d2e7d61810b0711a718 /runtime/lua/vim/filetype | |
| parent | 0313aba77a447558c3b373370b60eb78067e1c4d (diff) | |
| download | rneovim-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.lua | 403 |
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 |