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 | |
parent | 0313aba77a447558c3b373370b60eb78067e1c4d (diff) | |
download | rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.tar.gz rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.tar.bz2 rneovim-acb7a902812a064fced5ef7d389bd94cb45764bb.zip |
refactor(runtime): port scripts.vim to lua (#18710)
-rw-r--r-- | runtime/doc/lua.txt | 54 | ||||
-rw-r--r-- | runtime/lua/vim/filetype.lua | 134 | ||||
-rw-r--r-- | runtime/lua/vim/filetype/detect.lua | 403 | ||||
-rw-r--r-- | runtime/scripts.vim | 6 |
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 |