-- Contains filetype detection functions for use in filetype.lua that are either: -- * used more than once or -- * complex (e.g. check more than one line or use conditionals). -- Simple one-line checks, such as a check for a string in the first line are better inlined in filetype.lua. -- A few guidelines to follow when porting a new function: -- * Sort the function alphabetically and omit 'ft' or 'check' from the new function name. -- * Use ':find' instead of ':match' / ':sub' if possible. -- * When '=~' is used to match a pattern, there are two possibilities: -- - If the pattern only contains lowercase characters, treat the comparison as case-insensitive. -- - Otherwise, treat it as case-sensitive. -- (Basically, we apply 'smartcase': if upper case characters are used in the original pattern, then -- it's likely that case does matter). -- * When '\k', '\<' or '\>' is used in a pattern, use the 'matchregex' function. -- Note that vim.regex is case-sensitive by default, so add the '\c' flag if only lowercase letters -- are present in the pattern: -- Example: -- `if line =~ '^\s*unwind_protect\>'` => `if matchregex(line, [[\c^\s*unwind_protect\>]])` local fn = vim.fn local M = {} local getlines = vim.filetype._getlines local getline = vim.filetype._getline local findany = vim.filetype._findany local nextnonblank = vim.filetype._nextnonblank local matchregex = vim.filetype._matchregex -- luacheck: push no unused args -- luacheck: push ignore 122 -- This function checks for the kind of assembly that is wanted by the user, or -- can be detected from the first five lines of the file. --- @type vim.filetype.mapfn function M.asm(path, bufnr) -- tiasm uses `* comment` local lines = table.concat(getlines(bufnr, 1, 10), '\n') if findany(lines, { '^%*', '\n%*', 'Texas Instruments Incorporated' }) then return 'tiasm' end local syntax = vim.b[bufnr].asmsyntax if not syntax or syntax == '' then syntax = M.asm_syntax(path, bufnr) end -- If b:asmsyntax still isn't set, default to asmsyntax or GNU if not syntax or syntax == '' then if vim.g.asmsyntax and vim.g.asmsyntax ~= 0 then syntax = vim.g.asmsyntax else syntax = 'asm' end end return syntax, function(b) vim.b[b].asmsyntax = syntax end end --- Active Server Pages (with Perl or Visual Basic Script) --- @type vim.filetype.mapfn function M.asp(_, bufnr) if vim.g.filetype_asp then return vim.g.filetype_asp elseif table.concat(getlines(bufnr, 1, 3)):lower():find('perlscript') then return 'aspperl' end return 'aspvbs' end -- Checks the first 5 lines for a asmsyntax=foo override. -- Only whitespace characters can be present immediately before or after this statement. --- @type vim.filetype.mapfn function M.asm_syntax(_, bufnr) local lines = ' ' .. table.concat(getlines(bufnr, 1, 5), ' '):lower() .. ' ' local match = lines:match('%sasmsyntax=([a-zA-Z0-9]+)%s') if match then return match elseif findany(lines, { '%.title', '%.ident', '%.macro', '%.subtitle', '%.library' }) then return 'vmasm' end end local visual_basic_content = [[\c^\s*\%(Attribute\s\+VB_Name\|Begin\s\+\%(VB\.\|{\%(\x\+-\)\+\x\+}\)\)]] -- See frm() for Visual Basic form file detection --- @type vim.filetype.mapfn function M.bas(_, bufnr) if vim.g.filetype_bas then return vim.g.filetype_bas end -- Most frequent FreeBASIC-specific keywords in distro files local fb_keywords = [[\c^\s*\%(extern\|var\|enum\|private\|scope\|union\|byref\|operator\|constructor\|delete\|namespace\|public\|property\|with\|destructor\|using\)\>\%(\s*[:=(]\)\@!]] local fb_preproc = [[\c^\s*\%(#\s*\a\+\|option\s\+\%(byval\|dynamic\|escape\|\%(no\)\=gosub\|nokeyword\|private\|static\)\>\|\%(''\|rem\)\s*\$lang\>\|def\%(byte\|longint\|short\|ubyte\|uint\|ulongint\|ushort\)\>\)]] local fb_comment = "^%s*/'" -- OPTION EXPLICIT, without the leading underscore, is common to many dialects local qb64_preproc = [[\c^\s*\%($\a\+\|option\s\+\%(_explicit\|_\=explicitarray\)\>\)]] for _, line in ipairs(getlines(bufnr, 1, 100)) do if matchregex(line, visual_basic_content) then return 'vb' elseif line:find(fb_comment) or matchregex(line, fb_preproc) or matchregex(line, fb_keywords) then return 'freebasic' elseif matchregex(line, qb64_preproc) then return 'qb64' end end return 'basic' end --- @type vim.filetype.mapfn function M.bindzone(_, bufnr) local lines = table.concat(getlines(bufnr, 1, 4)) if findany(lines, { '^; <<>> DiG [0-9%.]+.* <<>>', '%$ORIGIN', '%$TTL', 'IN%s+SOA' }) then return 'bindzone' end end -- Returns true if file content looks like RAPID --- @param bufnr integer --- @param extension? string --- @return string|boolean? local function is_rapid(bufnr, extension) if extension == 'cfg' then local line = getline(bufnr, 1):lower() return findany(line, { 'eio:cfg', 'mmc:cfg', 'moc:cfg', 'proc:cfg', 'sio:cfg', 'sys:cfg' }) end local line = nextnonblank(bufnr, 1) if line then -- Called from mod, prg or sys functions return matchregex(line:lower(), [[\c\v^\s*%(\%{3}|module\s+\k+\s*%(\(|$))]]) end return false end --- @type vim.filetype.mapfn function M.cfg(_, bufnr) if vim.g.filetype_cfg then return vim.g.filetype_cfg --[[@as string]] elseif is_rapid(bufnr, 'cfg') then return 'rapid' end return 'cfg' end --- This function checks if one of the first ten lines start with a '@'. In --- that case it is probably a change file. --- If the first line starts with # or ! it's probably a ch file. --- If a line has "main", "include", "//" or "/*" it's probably ch. --- Otherwise CHILL is assumed. --- @type vim.filetype.mapfn function M.change(_, bufnr) local first_line = getline(bufnr, 1) if findany(first_line, { '^#', '^!' }) then return 'ch' end for _, line in ipairs(getlines(bufnr, 1, 10)) do if line:find('^@') then return 'change' end if line:find('MODULE') then return 'chill' elseif findany(line:lower(), { 'main%s*%(', '#%s*include', '//' }) then return 'ch' end end return 'chill' end --- @type vim.filetype.mapfn function M.changelog(_, bufnr) local line = getline(bufnr, 1):lower() if line:find('; urgency=') then return 'debchangelog' end return 'changelog' end --- @type vim.filetype.mapfn function M.cl(_, bufnr) local lines = table.concat(getlines(bufnr, 1, 4)) if lines:match('/%*') then return 'opencl' else return 'lisp' end end --- @type vim.filetype.mapfn function M.class(_, bufnr) -- Check if not a Java class (starts with '\xca\xfe\xba\xbe') if not getline(bufnr, 1):find('^\202\254\186\190') then return 'stata' end end --- @type vim.filetype.mapfn function M.cls(_, bufnr) if vim.g.filetype_cls then return vim.g.filetype_cls end local line1 = getline(bufnr, 1) if matchregex(line1, [[^#!.*\<\%(rexx\|regina\)\>]]) then return 'rexx' elseif line1 == 'VERSION 1.0 CLASS' then return 'vb' end local nonblank1 = nextnonblank(bufnr, 1) if nonblank1 and nonblank1:find('^[%%\\]') then return 'tex' elseif nonblank1 and findany(nonblank1, { '^%s*/%*', '^%s*::%w' }) then return 'rexx' end return 'st' end --- *.cmd is close to a Batch file, but on OS/2 Rexx files and TI linker command files also use *.cmd. --- lnk: `/* comment */`, `// comment`, and `--linker-option=value` --- rexx: `/* comment */`, `-- comment` --- @type vim.filetype.mapfn function M.cmd(_, bufnr) local lines = table.concat(getlines(bufnr, 1, 20)) if matchregex(lines, [[MEMORY\|SECTIONS\|\%(^\|\n\)--\S\|\%(^\|\n\)//]]) then return 'lnk' else local line1 = getline(bufnr, 1) if line1:find('^/%*') then return 'rexx' else return 'dosbatch' end end end --- @type vim.filetype.mapfn function M.conf(path, bufnr) if fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then return end if path:find('%.conf$') then return 'conf' end for _, line in ipairs(getlines(bufnr, 1, 5)) do if line:find('^#') then return 'conf' end end end --- Debian Control --- @type vim.filetype.mapfn function M.control(_, bufnr) local line1 = getline(bufnr, 1) if line1 and findany(line1, { '^Source:', '^Package:' }) then return 'debcontrol' end end --- Debian Copyright --- @type vim.filetype.mapfn function M.copyright(_, bufnr) if getline(bufnr, 1):find('^Format:') then return 'debcopyright' end end --- @type vim.filetype.mapfn function M.cpp(_, _) return vim.g.cynlib_syntax_for_cpp and 'cynlib' or 'cpp' end --- @type vim.filetype.mapfn function M.csh(path, bufnr) if fn.did_filetype() ~= 0 then -- Filetype was already detected return end local contents = getlines(bufnr) if vim.g.filetype_csh then return M.shell(path, contents, vim.g.filetype_csh) elseif string.find(vim.o.shell, 'tcsh') then return M.shell(path, contents, 'tcsh') else return M.shell(path, contents, 'csh') end end --- @param path string --- @param contents string[] --- @return string? 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 --- @type vim.filetype.mapfn function M.dat(path, bufnr) local file_name = fn.fnamemodify(path, ':t'):lower() -- Innovation data processing if findany(file_name, { '^upstream%.dat$', '^upstream%..*%.dat$', '^.*%.upstream%.dat$' }) then return 'upstreamdat' end if vim.g.filetype_dat then return vim.g.filetype_dat end -- Determine if a *.dat file is Kuka Robot Language local line = nextnonblank(bufnr, 1) if matchregex(line, [[\c\v^\s*%(\&\w+|defdat>)]]) then return 'krl' end end --- @type vim.filetype.mapfn function M.decl(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 3)) do if line:lower():find('^> 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 --- @type vim.filetype.mapfn function M.dtrace(_, bufnr) if fn.did_filetype() ~= 0 then -- Filetype was already detected return end for _, line in ipairs(getlines(bufnr, 1, 100)) do if matchregex(line, [[\c^module\>\|^import\>]]) then -- D files often start with a module and/or import statement. return 'd' elseif findany(line, { '^#!%S+dtrace', '#pragma%s+D%s+option', ':%S-:%S-:' }) then return 'dtrace' end end return 'd' end --- @param bufnr integer --- @return boolean local function is_modula2(bufnr) return matchregex(nextnonblank(bufnr, 1), [[\%s*$" }) then return 'specman' end end return 'eiffel' end --- @type vim.filetype.mapfn function M.edn(_, bufnr) local line = getline(bufnr, 1) if matchregex(line, [[\c^\s*(\s*edif\>]]) then return 'edif' else return 'clojure' end end -- This function checks for valid cl syntax in the first five lines. -- Look for either an opening comment, '#', or a block start, '{'. -- If not found, assume SGML. --- @type vim.filetype.mapfn function M.ent(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 5)) do if line:find('^%s*[#{]') then return 'cl' elseif not line:find('^%s*$') then -- Not a blank line, not a comment, and not a block start, -- so doesn't look like valid cl code. break end end return 'dtd' end --- @type vim.filetype.mapfn function M.euphoria(_, _) return vim.g.filetype_euphoria or 'euphoria3' end --- @type vim.filetype.mapfn function M.ex(_, bufnr) if vim.g.filetype_euphoria then return vim.g.filetype_euphoria else for _, line in ipairs(getlines(bufnr, 1, 100)) do if matchregex(line, [[\c^--\|^ifdef\>\|^include\>]]) then return 'euphoria3' end end return 'elixir' end end --- @param bufnr integer --- @return boolean local function is_forth(bufnr) local first_line = nextnonblank(bufnr, 1) -- SwiftForth block comment (line is usually filled with '-' or '=') or -- OPTIONAL (sometimes precedes the header comment) if first_line and findany(first_line:lower(), { '^%{%s', '^%{$', '^optional%s' }) then return true end for _, line in ipairs(getlines(bufnr, 1, 100)) do -- Forth comments and colon definitions if line:find('^[:(\\] ') then return true end end return false end -- Distinguish between Forth and Fortran --- @type vim.filetype.mapfn function M.f(_, bufnr) if vim.g.filetype_f then return vim.g.filetype_f end if is_forth(bufnr) then return 'forth' end return 'fortran' end -- This function checks the first 15 lines for appearance of 'FoamFile' -- and then 'object' in a following line. -- In that case, it's probably an OpenFOAM file --- @type vim.filetype.mapfn function M.foam(_, bufnr) local foam_file = false for _, line in ipairs(getlines(bufnr, 1, 15)) do if line:find('^FoamFile') then foam_file = true elseif foam_file and line:find('^%s*object') then return 'foam' end end end --- @type vim.filetype.mapfn function M.frm(_, bufnr) if vim.g.filetype_frm then return vim.g.filetype_frm end if getline(bufnr, 1) == 'VERSION 5.00' then return 'vb' end for _, line in ipairs(getlines(bufnr, 1, 5)) do if matchregex(line, visual_basic_content) then return 'vb' end end return 'form' end --- @type vim.filetype.mapfn function M.fvwm_v1(_, _) return 'fvwm', function(bufnr) vim.b[bufnr].fvwm_version = 1 end end --- @type vim.filetype.mapfn function M.fvwm_v2(path, _) if fn.fnamemodify(path, ':e') == 'm4' then return 'fvwm2m4' end return 'fvwm', function(bufnr) vim.b[bufnr].fvwm_version = 2 end end -- Distinguish between Forth and F# --- @type vim.filetype.mapfn function M.fs(_, bufnr) if vim.g.filetype_fs then return vim.g.filetype_fs end if is_forth(bufnr) then return 'forth' end return 'fsharp' end --- @type vim.filetype.mapfn function M.git(_, bufnr) local line = getline(bufnr, 1) if matchregex(line, [[^\x\{40,\}\>\|^ref: ]]) then return 'git' end end --- @type vim.filetype.mapfn function M.header(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 200)) do if findany(line:lower(), { '^@interface', '^@end', '^@class' }) then if vim.g.c_syntax_for_h then return 'objc' else return 'objcpp' end end end if vim.g.c_syntax_for_h then return 'c' elseif vim.g.ch_syntax_for_h then return 'ch' else return 'cpp' end end --- Recursively search for Hare source files in a directory and any --- subdirectories, up to a given depth. --- @param dir string --- @param depth number --- @return boolean local function is_hare_module(dir, depth) depth = math.max(depth, 0) for name, _ in vim.fs.dir(dir, { depth = depth + 1 }) do if name:find('%.ha$') then return true end end return false end --- @type vim.filetype.mapfn function M.haredoc(path, _) if vim.g.filetype_haredoc then if is_hare_module(vim.fs.dirname(path), vim.g.haredoc_search_depth or 1) then return 'haredoc' end end end --- @type vim.filetype.mapfn function M.html(_, bufnr) -- Disabled for the reasons mentioned here: -- https://github.com/vim/vim/pull/13594#issuecomment-1834465890 -- local filename = fn.fnamemodify(path, ':t') -- if filename:find('%.component%.html$') then -- return 'htmlangular' -- end for _, line in ipairs(getlines(bufnr, 1, 40)) do if matchregex( line, [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content]] ) then return 'htmlangular' elseif matchregex(line, [[\\|{#\s\+]] ) then return 'htmldjango' elseif findany(line, { '' }) then return 'superhtml' end end return 'html' end -- Virata Config Script File or Drupal module --- @type vim.filetype.mapfn function M.hw(_, bufnr) if getline(bufnr, 1):lower():find('<%?php') then return 'php' end return 'virata' end -- This function checks for an assembly comment or a SWIG keyword or verbatim -- block in the first 50 lines. -- If not found, assume Progress. --- @type vim.filetype.mapfn function M.i(path, bufnr) if vim.g.filetype_i then return vim.g.filetype_i end -- These include the leading '%' sign local ft_swig_keywords = [[^\s*%\%(addmethods\|apply\|beginfile\|clear\|constant\|define\|echo\|enddef\|endoffile\|extend\|feature\|fragment\|ignore\|import\|importfile\|include\|includefile\|inline\|insert\|keyword\|module\|name\|namewarn\|native\|newobject\|parms\|pragma\|rename\|template\|typedef\|typemap\|types\|varargs\|warn\)]] -- This is the start/end of a block that is copied literally to the processor file (C/C++) local ft_swig_verbatim_block_start = '^%s*%%{' for _, line in ipairs(getlines(bufnr, 1, 50)) do if line:find('^%s*;') or line:find('^%*') then return M.asm(path, bufnr) elseif matchregex(line, ft_swig_keywords) or line:find(ft_swig_verbatim_block_start) then return 'swig' end end return 'progress' end --- @type vim.filetype.mapfn function M.idl(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 50)) do if findany(line:lower(), { '^%s*import%s+"unknwn"%.idl', '^%s*import%s+"objidl"%.idl' }) then return 'msidl' end end return 'idl' end local pascal_comments = { '^%s*{', '^%s*%(%*', '^%s*//' } local pascal_keywords = [[\c^\s*\%(program\|unit\|library\|uses\|begin\|procedure\|function\|const\|type\|var\)\>]] --- @type vim.filetype.mapfn function M.inc(path, bufnr) if vim.g.filetype_inc then return vim.g.filetype_inc end local lines = table.concat(getlines(bufnr, 1, 3)) if lines:lower():find('perlscript') then return 'aspperl' elseif lines:find('<%%') then return 'aspvbs' elseif lines:find('<%?') then return 'php' -- Pascal supports // comments but they're vary rarely used for file -- headers so assume POV-Ray elseif findany(lines, { '^%s{', '^%s%(%*' }) or matchregex(lines, pascal_keywords) then return 'pascal' elseif findany(lines, { '^%s*inherit ', '^%s*require ', '^%s*%u[%w_:${}]*%s+%??[?:+]?= ' }) then return 'bitbake' else local syntax = M.asm_syntax(path, bufnr) if not syntax or syntax == '' then return 'pov' end return syntax, function(b) vim.b[b].asmsyntax = syntax end end end --- @type vim.filetype.mapfn function M.inp(_, bufnr) if getline(bufnr, 1):find('%%%%') then return 'tex' elseif getline(bufnr, 1):find('^%*') then return 'abaqus' else for _, line in ipairs(getlines(bufnr, 1, 500)) do if line:lower():find('^header surface data') then return 'trasys' end end end end --- @type vim.filetype.mapfn function M.install(path, bufnr) if getline(bufnr, 1):lower():find('<%?php') then return 'php' end return M.bash(path, bufnr) end --- Innovation Data Processing --- (refactor of filetype.vim since the patterns are case-insensitive) --- @type vim.filetype.mapfn function M.log(path, _) path = path:lower() --- @type string LuaLS bug if findany( path, { 'upstream%.log', 'upstream%..*%.log', '.*%.upstream%.log', 'upstream%-.*%.log' } ) then return 'upstreamlog' elseif findany( path, { 'upstreaminstall%.log', 'upstreaminstall%..*%.log', '.*%.upstreaminstall%.log' } ) then return 'upstreaminstalllog' elseif findany(path, { 'usserver%.log', 'usserver%..*%.log', '.*%.usserver%.log' }) then return 'usserverlog' elseif findany(path, { 'usw2kagt%.log', 'usw2kagt%..*%.log', '.*%.usw2kagt%.log' }) then return 'usw2kagtlog' end end --- @type vim.filetype.mapfn function M.ll(_, bufnr) local first_line = getline(bufnr, 1) if matchregex(first_line, [[;\|\\|\]]) then return 'llvm' else return 'lifelines' end end --- @type vim.filetype.mapfn function M.lpc(_, bufnr) if vim.g.lpc_syntax_for_c then for _, line in ipairs(getlines(bufnr, 1, 12)) do if findany(line, { '^//', '^inherit', '^private', '^protected', '^nosave', '^string', '^object', '^mapping', '^mixed', }) then return 'lpc' end end end return 'c' end --- @type vim.filetype.mapfn function M.lsl(_, bufnr) if vim.g.filetype_lsl then return vim.g.filetype_lsl end local line = nextnonblank(bufnr, 1) if findany(line, { '^%s*%%', ':%s*trait%s*$' }) then return 'larch' else return 'lsl' end end --- @type vim.filetype.mapfn function M.m(_, bufnr) if vim.g.filetype_m then return vim.g.filetype_m end -- Excluding end(for|function|if|switch|while) common to Murphi local octave_block_terminators = [[\]] local objc_preprocessor = [[\c^\s*#\s*\%(import\|include\|define\|if\|ifn\=def\|undef\|line\|error\|pragma\)\>]] -- Whether we've seen a multiline comment leader local saw_comment = false for _, line in ipairs(getlines(bufnr, 1, 100)) do if line:find('^%s*/%*') then -- /* ... */ is a comment in Objective C and Murphi, so we can't conclude -- it's either of them yet, but track this as a hint in case we don't see -- anything more definitive. saw_comment = true end if line:find('^%s*//') or matchregex(line, [[\c^\s*@import\>]]) or matchregex(line, objc_preprocessor) then return 'objc' end if findany(line, { '^%s*#', '^%s*%%!' }) or matchregex(line, [[\c^\s*unwind_protect\>]]) or matchregex(line, [[\c\%(^\|;\)\s*]] .. octave_block_terminators) then return 'octave' elseif line:find('^%s*%%') then return 'matlab' elseif line:find('^%s*%(%*') then return 'mma' elseif matchregex(line, [[\c^\s*\(\(type\|var\)\>\|--\)]]) then return 'murphi' end end if saw_comment then -- We didn't see anything definitive, but this looks like either Objective C -- or Murphi based on the comment leader. Assume the former as it is more -- common. return 'objc' else -- Default is Matlab return 'matlab' end end --- @param contents string[] --- @return string? 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 --- Check if it is a Microsoft Makefile --- @type vim.filetype.mapfn function M.make(_, bufnr) vim.b.make_microsoft = nil for _, line in ipairs(getlines(bufnr, 1, 1000)) do if matchregex(line, [[\c^\s*!\s*\(ifn\=\(def\)\=\|include\|message\|error\)\>]]) then vim.b.make_microsoft = 1 break elseif matchregex(line, [[^ *ifn\=\(eq\|def\)\>]]) or findany(line, { '^ *[-s]?%s', '^ *%w+%s*[!?:+]=' }) then break end end return 'make' end --- @type vim.filetype.mapfn function M.markdown(_, _) return vim.g.filetype_md or 'markdown' end --- Rely on the file to start with a comment. --- MS message text files use ';', Sendmail files use '#' or 'dnl' --- @type vim.filetype.mapfn function M.mc(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 20)) do if findany(line:lower(), { '^%s*#', '^%s*dnl' }) then -- Sendmail .mc file return 'm4' elseif line:find('^%s*;') then return 'msmessages' end end -- Default: Sendmail .mc file return 'm4' end --- @param path string --- @return string? function M.me(path) local filename = fn.fnamemodify(path, ':t'):lower() if filename ~= 'read.me' and filename ~= 'click.me' then return 'nroff' end end --- @type vim.filetype.mapfn function M.mm(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 20)) do if matchregex(line, [[\c^\s*\(#\s*\(include\|import\)\>\|@import\>\|/\*\)]]) then return 'objcpp' end end return 'nroff' end --- @type vim.filetype.mapfn function M.mms(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 20)) do if findany(line, { '^%s*%%', '^%s*//', '^%*' }) then return 'mmix' elseif line:find('^%s*#') then return 'make' end end return 'mmix' end --- Returns true if file content looks like LambdaProlog --- @param bufnr integer --- @return boolean 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)) do -- The second pattern matches a LambdaProlog comment if not findany(line, { '^%s*$', '^%s*%%' }) then -- The pattern must not catch a go.mod file return matchregex(line, [[\c\]]) or findany(line, prolog_patterns) then return 'prolog' else return 'perl' end end --- @type vim.filetype.mapfn function M.pm(_, bufnr) local line = getline(bufnr, 1) if line:find('XPM2') then return 'xpm2' elseif line:find('XPM') then return 'xpm' else return 'perl' end end --- @type vim.filetype.mapfn function M.pp(_, bufnr) if vim.g.filetype_pp then return vim.g.filetype_pp end local line = nextnonblank(bufnr, 1) if findany(line, pascal_comments) or matchregex(line, pascal_keywords) then return 'pascal' else return 'puppet' end end --- @type vim.filetype.mapfn function M.prg(_, bufnr) if vim.g.filetype_prg then return vim.g.filetype_prg elseif is_rapid(bufnr) then return 'rapid' else -- Nothing recognized, assume Clipper return 'clipper' end end function M.printcap(ptcap_type) if fn.did_filetype() == 0 then return 'ptcap', function(bufnr) vim.b[bufnr].ptcap_type = ptcap_type end end end --- @type vim.filetype.mapfn function M.progress_cweb(_, bufnr) if vim.g.filetype_w then return vim.g.filetype_w else if getline(bufnr, 1):lower():find('^&analyze') or getline(bufnr, 3):lower():find('^&global%-define') then return 'progress' else return 'cweb' end end end -- This function checks for valid Pascal syntax in the first 10 lines. -- Look for either an opening comment or a program start. -- If not found, assume Progress. --- @type vim.filetype.mapfn function M.progress_pascal(_, bufnr) if vim.g.filetype_p then return vim.g.filetype_p end for _, line in ipairs(getlines(bufnr, 1, 10)) do if findany(line, pascal_comments) or matchregex(line, pascal_keywords) then return 'pascal' elseif not line:find('^%s*$') or line:find('^/%*') then -- Not an empty line: Doesn't look like valid Pascal code. -- Or it looks like a Progress /* comment break end end return 'progress' end --- Distinguish between "default", Prolog and Cproto prototype file. --- @type vim.filetype.mapfn function M.proto(_, bufnr) if getline(bufnr, 2):find('/%* Generated automatically %*/') then return 'c' elseif getline(bufnr, 2):find('.;$') then -- Cproto files have a comment in the first line and a function prototype in -- the second line, it always ends in ";". Indent files may also have -- comments, thus we can't match comments to see the difference. -- IDL files can have a single ';' in the second line, require at least one -- character before the ';'. return 'cpp' end -- Recognize Prolog by specific text in the first non-empty line; -- require a blank after the '%' because Perl uses "%list" and "%translate" local line = nextnonblank(bufnr, 1) if line and matchregex(line, [[\c\]]) or findany(line, prolog_patterns) then return 'prolog' end end -- Software Distributor Product Specification File (POSIX 1387.2-1995) --- @type vim.filetype.mapfn function M.psf(_, bufnr) local line = getline(bufnr, 1):lower() if findany(line, { '^%s*distribution%s*$', '^%s*installed_software%s*$', '^%s*root%s*$', '^%s*bundle%s*$', '^%s*product%s*$', }) then return 'psf' end end --- @type vim.filetype.mapfn function M.r(_, bufnr) local lines = getlines(bufnr, 1, 50) -- Rebol is easy to recognize, check for that first if matchregex(table.concat(lines), [[\c\]]) then return 'rebol' end for _, line in ipairs(lines) do -- R has # comments if line:find('^%s*#') then return 'r' end -- Rexx has /* comments */ if line:find('^%s*/%*') then return 'rexx' end end -- Nothing recognized, use user default or assume R if vim.g.filetype_r then return vim.g.filetype_r end -- Rexx used to be the default, but R appears to be much more popular. return 'r' end --- @type vim.filetype.mapfn function M.redif(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 5)) do if line:lower():find('^template%-type:') then return 'redif' end end end --- @type vim.filetype.mapfn function M.reg(_, bufnr) local line = getline(bufnr, 1):lower() if line:find('^regedit[0-9]*%s*$') or line:find('^windows registry editor version %d*%.%d*%s*$') then return 'registry' end end -- Diva (with Skill) or InstallShield --- @type vim.filetype.mapfn function M.rul(_, bufnr) if table.concat(getlines(bufnr, 1, 6)):lower():find('installshield') then return 'ishd' end return 'diva' end local udev_rules_pattern = '^%s*udev_rules%s*=%s*"([%^"]+)/*".*' --- @type vim.filetype.mapfn function M.rules(path) path = path:lower() --- @type string LuaLS bug if findany(path, { '/etc/udev/.*%.rules$', '/etc/udev/rules%.d/.*$.rules$', '/usr/lib/udev/.*%.rules$', '/usr/lib/udev/rules%.d/.*%.rules$', '/lib/udev/.*%.rules$', '/lib/udev/rules%.d/.*%.rules$', }) then return 'udevrules' elseif path:find('^/etc/ufw/') then -- Better than hog return 'conf' elseif findany(path, { '^/etc/polkit%-1/rules%.d', '/usr/share/polkit%-1/rules%.d' }) then return 'javascript' else local ok, config_lines = pcall(fn.readfile, '/etc/udev/udev.conf') --- @cast config_lines +string[] if not ok then return 'hog' end local dir = fn.fnamemodify(path, ':h') for _, line in ipairs(config_lines) do local match = line:match(udev_rules_pattern) if match then local udev_rules = line:gsub(udev_rules_pattern, match, 1) if dir == udev_rules then return 'udevrules' end end end return 'hog' end end -- LambdaProlog and Standard ML signature files --- @type vim.filetype.mapfn function M.sig(_, bufnr) if vim.g.filetype_sig then return vim.g.filetype_sig end local line = nextnonblank(bufnr, 1) -- LambdaProlog comment or keyword if findany(line, { '^%s*/%*', '^%s*%%', '^%s*sig%s+%a' }) then return 'lprolog' -- SML comment or keyword elseif findany(line, { '^%s*%(%*', '^%s*signature%s+%a', '^%s*structure%s+%a' }) then return 'sml' end end --- @type vim.filetype.mapfn function M.sa(_, bufnr) local lines = table.concat(getlines(bufnr, 1, 4), '\n') if findany(lines, { '^;', '\n;' }) then return 'tiasm' end return 'sather' end -- This function checks the first 25 lines of file extension "sc" to resolve -- detection between scala and SuperCollider --- @type vim.filetype.mapfn function M.sc(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 25)) do if findany(line, { 'var%s<', 'classvar%s<', '%^this.*', '|%w+|', '%+%s%w*%s{', '%*ar%s', }) then return 'supercollider' end end return 'scala' end -- This function checks the first line of file extension "scd" to resolve -- detection between scdoc and SuperCollider --- @type vim.filetype.mapfn function M.scd(_, bufnr) local first = '^%S+%(%d[0-9A-Za-z]*%)' local opt = [[%s+"[^"]*"]] local line = getline(bufnr, 1) if findany(line, { first .. '$', first .. opt .. '$', first .. opt .. opt .. '$' }) then return 'scdoc' end return 'supercollider' end --- @type vim.filetype.mapfn function M.sgml(_, bufnr) local lines = table.concat(getlines(bufnr, 1, 5)) if lines:find('linuxdoc') then return 'sgmllnx' elseif lines:find(']]) then -- Some .sh scripts contain #!/bin/csh. return M.shell(path, contents, 'csh') -- Some .sh scripts contain #!/bin/tcsh. elseif matchregex(name, [[\]]) then return M.shell(path, contents, 'tcsh') -- Some .sh scripts contain #!/bin/zsh. elseif matchregex(name, [[\]]) then return M.shell(path, contents, 'zsh') end local on_detect --- @type fun(b: integer)? if matchregex(name, [[\]]) then on_detect = function(b) vim.b[b].is_kornshell = 1 vim.b[b].is_bash = nil vim.b[b].is_sh = nil end elseif vim.g.bash_is_sh or matchregex(name, [[\<\(bash\|bash2\)\>]]) then on_detect = function(b) vim.b[b].is_bash = 1 vim.b[b].is_kornshell = nil vim.b[b].is_sh = nil end -- Ubuntu links sh to dash elseif matchregex(name, [[\<\(sh\|dash\)\>]]) then on_detect = function(b) vim.b[b].is_sh = 1 vim.b[b].is_kornshell = nil vim.b[b].is_bash = nil end end return M.shell(path, contents, 'sh'), on_detect end --- @param name? string --- @return vim.filetype.mapfn local function sh_with(name) return function(path, bufnr) return sh(path, getlines(bufnr), name) end end M.sh = sh_with() M.bash = sh_with('bash') M.ksh = sh_with('ksh') M.tcsh = sh_with('tcsh') --- For shell-like file types, check for an "exec" command hidden in a comment, as used for Tcl. --- @param path string --- @param contents string[] --- @param name? string --- @return string? function M.shell(path, contents, name) if 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_nr, line in ipairs(contents) do -- Skip the first line if line_nr ~= 1 then --- @type string 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\]]) then return 'smil' else return 'mib' end end --- @type vim.filetype.mapfn function M.sql(_, _) return vim.g.filetype_sql and vim.g.filetype_sql or 'sql' end -- Determine if a *.src file is Kuka Robot Language --- @type vim.filetype.mapfn function M.src(_, bufnr) if vim.g.filetype_src then return vim.g.filetype_src end local line = nextnonblank(bufnr, 1) if matchregex(line, [[\c\v^\s*%(\&\w+|%(global\s+)?def%(fct)?>)]]) then return 'krl' end end --- @type vim.filetype.mapfn function M.sys(_, bufnr) if vim.g.filetype_sys then return vim.g.filetype_sys elseif is_rapid(bufnr) then return 'rapid' end return 'bat' end -- Choose context, plaintex, or tex (LaTeX) based on these rules: -- 1. Check the first line of the file for "%&". -- 2. Check the first 1000 non-comment lines for LaTeX or ConTeXt keywords. -- 3. Default to "plain" or to g:tex_flavor, can be set in user's vimrc. --- @type vim.filetype.mapfn function M.tex(path, bufnr) local matched, _, format = getline(bufnr, 1):find('^%%&%s*(%a+)') if matched then --- @type string format = format:lower():gsub('pdf', '', 1) elseif path:lower():find('tex/context/.*/.*%.tex') then return 'context' else -- Default value, may be changed later: format = vim.g.tex_flavor or 'plaintex' local lpat = [[documentclass\>\|usepackage\>\|begin{\|newcommand\>\|renewcommand\>]] local cpat = [[start\a\+\|setup\a\+\|usemodule\|enablemode\|enableregime\|setvariables\|useencoding\|usesymbols\|stelle\a\+\|verwende\a\+\|stel\a\+\|gebruik\a\+\|usa\a\+\|imposta\a\+\|regle\a\+\|utilisemodule\>]] for i, l in ipairs(getlines(bufnr, 1, 1000)) do -- Find first non-comment line if not l:find('^%s*%%%S') then -- Check the next thousand lines for a LaTeX or ConTeXt keyword. for _, line in ipairs(getlines(bufnr, i, i + 1000)) do if matchregex(line, [[\c^\s*\\\%(]] .. lpat .. [[\)]]) then return 'tex' elseif matchregex(line, [[\c^\s*\\\%(]] .. cpat .. [[\)]]) then return 'context' end end end end end -- if matched -- Translation from formats to file types. TODO: add AMSTeX, RevTex, others? if format == 'plain' then return 'plaintex' elseif format == 'plaintex' or format == 'context' then return format else -- Probably LaTeX return 'tex' end end -- Determine if a *.tf file is TF (TinyFugue) mud client or terraform --- @type vim.filetype.mapfn function M.tf(_, bufnr) 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 return 'terraform' end end return 'tf' end --- @type vim.filetype.mapfn function M.ttl(_, bufnr) local line = getline(bufnr, 1):lower() if line:find('^@?prefix') or line:find('^@?base') then return 'turtle' end return 'teraterm' end --- @type vim.filetype.mapfn function M.txt(_, bufnr) -- helpfiles match *.txt, but should have a modeline as last line if not getline(bufnr, -1):find('vim:.*ft=help') then return 'text' end end --- @type vim.filetype.mapfn function M.typ(_, bufnr) if vim.g.filetype_typ then return vim.g.filetype_typ end for _, line in ipairs(getlines(bufnr, 1, 200)) do if findany(line, { '^CASE[%s]?=[%s]?SAME$', '^CASE[%s]?=[%s]?LOWER$', '^CASE[%s]?=[%s]?UPPER$', '^CASE[%s]?=[%s]?OPPOSITE$', '^TYPE%s', }) then return 'sql' end end return 'typst' end --- @type vim.filetype.mapfn function M.uci(_, bufnr) -- Return "uci" iff the file has a config or package statement near the -- top of the file and all preceding lines were comments or blank. for _, line in ipairs(getlines(bufnr, 1, 3)) do -- Match a config or package statement at the start of the line. if line:find('^%s*[cp]%s+%S') or line:find('^%s*config%s+%S') or line:find('^%s*package%s+%S') then return 'uci' end -- Match a line that is either all blank or blank followed by a comment if not (line:find('^%s*$') or line:find('^%s*#')) then break end end end -- Determine if a .v file is Verilog, V, or Coq --- @type vim.filetype.mapfn function M.v(_, bufnr) if fn.did_filetype() ~= 0 then -- Filetype was already detected return end if vim.g.filetype_v then return vim.g.filetype_v end local in_comment = 0 for _, line in ipairs(getlines(bufnr, 1, 500)) do if line:find('^%s*/%*') then in_comment = 1 end if in_comment == 1 then if line:find('%*/') then in_comment = 0 end elseif not line:find('^%s*//') then if line:find('%.%s*$') and not line:find('/[/*]') or line:find('%(%*') and not line:find('/[/*].*%(%*') then return 'coq' elseif findany(line, { ';%s*$', ';%s*/[/*]', '^%s*module%s+%w+%s*%(' }) then return 'verilog' end end end return 'v' end --- @type vim.filetype.mapfn function M.vba(_, bufnr) if getline(bufnr, 1):find('^["#] Vimball Archiver') then return 'vim' end return 'vb' end -- WEB (*.web is also used for Winbatch: Guess, based on expecting "%" comment -- lines in a WEB file). --- @type vim.filetype.mapfn function M.web(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 5)) do if line:find('^%%') then return 'web' end end return 'winbatch' end -- XFree86 config --- @type vim.filetype.mapfn function M.xfree86_v3(_, _) return 'xf86conf', function(bufnr) local line = getline(bufnr, 1) if matchregex(line, [[\]]) then vim.b[bufnr].xf86conf_xfree86_version = 3 end end end -- XFree86 config --- @type vim.filetype.mapfn function M.xfree86_v4(_, _) return 'xf86conf', function(b) vim.b[b].xf86conf_xfree86_version = 4 end end --- @type vim.filetype.mapfn function M.xml(_, bufnr) for _, line in ipairs(getlines(bufnr, 1, 100)) do local is_docbook4 = line:find('\)]]) and not line:lower():find('^%s*#%s*include') then return 'racc' end end return 'yacc' 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 } }, just = 'just', -- 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 } }, guile = 'scheme', ['nix%-shell'] = 'nix', ['^crystal\\>'] = { 'crystal', { vim_regex = true } }, ['^\\%(rexx\\|regina\\)\\>'] = { 'rexx', { vim_regex = true } }, ['^janet\\>'] = { 'janet', { vim_regex = true } }, ['^dart\\>'] = { 'dart', { vim_regex = true } }, ['^execlineb\\>'] = { 'execline', { vim_regex = true } }, ['^vim\\>'] = { 'vim', { vim_regex = true } }, } ---@private --- File starts with "#!". --- @param contents string[] --- @param path string --- @param dispatch_extension fun(name: string): string?, fun(b: integer)? --- @return string? --- @return fun(b: integer)? local function match_from_hashbang(contents, path, dispatch_extension) 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*\]]) then name = fn.substitute(first_line, [[^#!.*\\s\+\(\i\+\).*]], '\\1', '') elseif matchregex(first_line, [[^#!\s*[^/\\ ]*\>\([^/\\]\|$\)]]) then name = fn.substitute(first_line, [[^#!\s*\([^/\\ ]*\>\).*]], '\\1', '') else name = fn.substitute(first_line, [[^#!\s*\S*[/\\]\(\f\+\).*]], '\\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*\|dash\|ksh\d*\|sh\)\>]]) then -- Bourne-like shell scripts: bash bash2 dash ksh ksh93 sh return sh(path, contents, first_line) elseif matchregex(name, [[^csh\>]]) then return M.shell(path, contents, vim.g.filetype_csh or 'csh') elseif matchregex(name, [[^tcsh\>]]) then return M.shell(path, contents, 'tcsh') end for k, v in pairs(patterns_hashbang) do local ft = type(v) == 'table' and v[1] or v --[[@as string]] 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 -- If nothing matched, check the extension table. For a hashbang like -- '#!/bin/env foo', this will set the filetype to 'fooscript' assuming -- the filetype for the 'foo' extension is 'fooscript' in the extension table. return dispatch_extension(name) 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: " 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") ['\\'] = { '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 } }, function(lines) if -- inaccurate fast match first, then use accurate slow match (lines[1]:find('execve%(') and lines[1]:find('^[0-9:%. ]*execve%(')) or lines[1]:find('^__libc_start_main') then return 'strace' end end, -- 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 } }, dns_zone, -- 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 "#!". --- @param contents string[] --- @param path string --- @return string? --- @return fun(b: integer)? local function match_from_text(contents, path) if contents[1]:find('^:$') then -- Bourne-like shell scripts: sh ksh bash bash2 return 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 --- @cast k string 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 --- @param contents string[] --- @param path string --- @param dispatch_extension fun(name: string): string?, fun(b: integer)? --- @return string? --- @return fun(b: integer)? function M.match_contents(contents, path, dispatch_extension) local first_line = contents[1] if first_line:find('^#!') then return match_from_hashbang(contents, path, dispatch_extension) else return match_from_text(contents, path) end end return M