detect.lua (67646B)
1 -- Contains filetype detection functions for use in filetype.lua that are either: 2 -- * used more than once or 3 -- * complex (e.g. check more than one line or use conditionals). 4 -- Simple one-line checks, such as a check for a string in the first line are better inlined in filetype.lua. 5 6 -- A few guidelines to follow when porting a new function: 7 -- * Sort the function alphabetically and omit 'ft' or 'check' from the new function name. 8 -- * Use ':find' instead of ':match' / ':sub' if possible. 9 -- * When '=~' is used to match a pattern, there are two possibilities: 10 -- - If the pattern only contains lowercase characters, treat the comparison as case-insensitive. 11 -- - Otherwise, treat it as case-sensitive. 12 -- (Basically, we apply 'smartcase': if upper case characters are used in the original pattern, then 13 -- it's likely that case does matter). 14 -- * When '\k', '\<' or '\>' is used in a pattern, use the 'matchregex' function. 15 -- Note that vim.regex is case-sensitive by default, so add the '\c' flag if only lowercase letters 16 -- are present in the pattern: 17 -- Example: 18 -- `if line =~ '^\s*unwind_protect\>'` => `if matchregex(line, [[\c^\s*unwind_protect\>]])` 19 20 local fn = vim.fn 21 local fs = vim.fs 22 23 local M = {} 24 25 local getlines = vim.filetype._getlines 26 local getline = vim.filetype._getline 27 local findany = vim.filetype._findany 28 local nextnonblank = vim.filetype._nextnonblank 29 local matchregex = vim.filetype._matchregex 30 31 -- luacheck: push no unused args 32 -- luacheck: push ignore 122 33 34 -- Erlang Application Resource Files (*.app.src is matched by extension) 35 -- See: https://erlang.org/doc/system/applications 36 --- @type vim.filetype.mapfn 37 function M.app(path, bufnr) 38 if vim.g.filetype_app then 39 return vim.g.filetype_app 40 end 41 for lnum, line in ipairs(getlines(bufnr, 1, 100)) do 42 -- skip Erlang comments, might be something else 43 if not findany(line, { '^%s*%%', '^%s*$' }) then 44 if line:find('^%s*{') then 45 local name = fn.fnamemodify(path, ':t:r:r') 46 local lines = vim 47 .iter(getlines(bufnr, lnum, lnum + 9)) 48 :filter(function(v) 49 return not v:find('^%s*%%') 50 end) 51 :join(' ') 52 if 53 findany(lines, { 54 [[^%s*{%s*application%s*,%s*']] .. name .. [['%s*,]], 55 [[^%s*{%s*application%s*,%s*]] .. name .. [[%s*,]], 56 }) 57 then 58 return 'erlang' 59 end 60 end 61 return 62 end 63 end 64 end 65 66 -- This function checks for the kind of assembly that is wanted by the user, or 67 -- can be detected from the beginning of the file. 68 --- @type vim.filetype.mapfn 69 function M.asm(path, bufnr) 70 local syntax = vim.b[bufnr].asmsyntax 71 if not syntax or syntax == '' then 72 syntax = M.asm_syntax(path, bufnr) 73 end 74 75 -- If b:asmsyntax still isn't set, default to asmsyntax or GNU 76 if not syntax or syntax == '' then 77 if vim.g.asmsyntax and vim.g.asmsyntax ~= 0 then 78 syntax = vim.g.asmsyntax 79 else 80 syntax = 'asm' 81 end 82 end 83 return syntax, function(b) 84 vim.b[b].asmsyntax = syntax 85 end 86 end 87 88 -- Checks the first lines for a asmsyntax=foo override. 89 -- Only whitespace characters can be present immediately before or after this statement. 90 --- @type vim.filetype.mapfn 91 function M.asm_syntax(_, bufnr) 92 local lines = ' ' .. table.concat(getlines(bufnr, 1, 5), ' '):lower() .. ' ' 93 local match = lines:match('%sasmsyntax=([a-zA-Z0-9]+)%s') 94 if match then 95 return match 96 end 97 local is_slash_star_encountered = false 98 for _, line in ipairs(getlines(bufnr, 1, 50)) do 99 if line:find('^/%*') then 100 is_slash_star_encountered = true 101 end 102 if 103 line:find('^; Listing generated by Microsoft') 104 or matchregex( 105 line, 106 [[\c^\%(\%(CONST\|_BSS\|_DATA\|_TEXT\)\s\+SEGMENT\>\)\|\s*\.[2-6]86P\?\>\|\s*\.XMM\>]] 107 ) 108 then 109 return 'masm' 110 elseif 111 line:find('Texas Instruments Incorporated') 112 -- tiasm uses `* comment`, but detection is unreliable if '/*' is seen 113 or (line:find('^%*') and not is_slash_star_encountered) 114 then 115 return 'tiasm' 116 elseif matchregex(line, [[\c\.title\>\|\.ident\>\|\.macro\>\|\.subtitle\>\|\.library\>]]) then 117 return 'vmasm' 118 end 119 end 120 end 121 122 --- Active Server Pages (with Perl or Visual Basic Script) 123 --- @type vim.filetype.mapfn 124 function M.asp(_, bufnr) 125 if vim.g.filetype_asp then 126 return vim.g.filetype_asp 127 elseif table.concat(getlines(bufnr, 1, 3)):lower():find('perlscript') then 128 return 'aspperl' 129 end 130 return 'aspvbs' 131 end 132 133 local visual_basic_content = 134 [[\c^\s*\%(Attribute\s\+VB_Name\|Begin\s\+\%(VB\.\|{\%(\x\+-\)\+\x\+}\)\)]] 135 136 -- See frm() for Visual Basic form file detection 137 --- @type vim.filetype.mapfn 138 function M.bas(_, bufnr) 139 if vim.g.filetype_bas then 140 return vim.g.filetype_bas 141 end 142 143 -- Most frequent FreeBASIC-specific keywords in distro files 144 local fb_keywords = 145 [[\c^\s*\%(extern\|var\|enum\|private\|scope\|union\|byref\|operator\|constructor\|delete\|namespace\|public\|property\|with\|destructor\|using\)\>\%(\s*[:=(]\)\@!]] 146 local fb_preproc = 147 [[\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\)\>\)]] 148 149 local fb_comment = "^%s*/'" 150 -- OPTION EXPLICIT, without the leading underscore, is common to many dialects 151 local qb64_preproc = [[\c^\s*\%($\a\+\|option\s\+\%(_explicit\|_\=explicitarray\)\>\)]] 152 153 for _, line in ipairs(getlines(bufnr, 1, 100)) do 154 if matchregex(line, visual_basic_content) then 155 return 'vb' 156 elseif 157 line:find(fb_comment) 158 or matchregex(line, fb_preproc) 159 or matchregex(line, fb_keywords) 160 then 161 return 'freebasic' 162 elseif matchregex(line, qb64_preproc) then 163 return 'qb64' 164 end 165 end 166 return 'basic' 167 end 168 169 --- @type vim.filetype.mapfn 170 function M.bindzone(_, bufnr) 171 local lines = table.concat(getlines(bufnr, 1, 4)) 172 if findany(lines, { '^; <<>> DiG [0-9%.]+.* <<>>', '%$ORIGIN', '%$TTL', 'IN%s+SOA' }) then 173 return 'bindzone' 174 end 175 end 176 177 -- Returns true if file content looks like RAPID 178 --- @param bufnr integer 179 --- @param extension? string 180 --- @return string|boolean? 181 local function is_rapid(bufnr, extension) 182 if extension == 'cfg' then 183 local line = getline(bufnr, 1):lower() 184 return findany(line, { 'eio:cfg', 'mmc:cfg', 'moc:cfg', 'proc:cfg', 'sio:cfg', 'sys:cfg' }) 185 end 186 local line = nextnonblank(bufnr, 1) 187 if line then 188 -- Called from mod, prg or sys functions 189 return matchregex(line:lower(), [[\c\v^\s*%(\%{3}|module\s+\k+\s*%(\(|$))]]) 190 end 191 return false 192 end 193 194 --- @type vim.filetype.mapfn 195 function M.cfg(_, bufnr) 196 if vim.g.filetype_cfg then 197 return vim.g.filetype_cfg --[[@as string]] 198 elseif is_rapid(bufnr, 'cfg') then 199 return 'rapid' 200 end 201 return 'cfg' 202 end 203 204 --- This function checks if one of the first ten lines start with a '@'. In 205 --- that case it is probably a change file. 206 --- If the first line starts with # or ! it's probably a ch file. 207 --- If a line has "main", "include", "//" or "/*" it's probably ch. 208 --- Otherwise CHILL is assumed. 209 --- @type vim.filetype.mapfn 210 function M.change(_, bufnr) 211 local first_line = getline(bufnr, 1) 212 if findany(first_line, { '^#', '^!' }) then 213 return 'ch' 214 end 215 for _, line in ipairs(getlines(bufnr, 1, 10)) do 216 if line:find('^@') then 217 return 'change' 218 end 219 if line:find('MODULE') then 220 return 'chill' 221 elseif findany(line:lower(), { 'main%s*%(', '#%s*include', '//' }) then 222 return 'ch' 223 end 224 end 225 return 'chill' 226 end 227 228 --- @type vim.filetype.mapfn 229 function M.changelog(_, bufnr) 230 local line = getline(bufnr, 1):lower() 231 if line:find('; urgency=') then 232 return 'debchangelog' 233 end 234 return 'changelog' 235 end 236 237 --- @type vim.filetype.mapfn 238 function M.cl(_, bufnr) 239 local lines = table.concat(getlines(bufnr, 1, 4)) 240 if lines:match('/%*') then 241 return 'opencl' 242 else 243 return 'lisp' 244 end 245 end 246 247 --- @type vim.filetype.mapfn 248 function M.class(_, bufnr) 249 -- Check if not a Java class (starts with '\xca\xfe\xba\xbe') 250 if not getline(bufnr, 1):find('^\202\254\186\190') then 251 return 'stata' 252 end 253 end 254 255 --- @type vim.filetype.mapfn 256 function M.cls(_, bufnr) 257 if vim.g.filetype_cls then 258 return vim.g.filetype_cls 259 end 260 local line1 = getline(bufnr, 1) 261 if matchregex(line1, [[^#!.*\<\%(rexx\|regina\)\>]]) then 262 return 'rexx' 263 elseif line1 == 'VERSION 1.0 CLASS' then 264 return 'vb' 265 end 266 267 local nonblank1 = nextnonblank(bufnr, 1) 268 if nonblank1 and nonblank1:find('^[%%\\]') then 269 return 'tex' 270 elseif nonblank1 and findany(nonblank1, { '^%s*/%*', '^%s*::%w' }) then 271 return 'rexx' 272 end 273 return 'st' 274 end 275 276 --- *.cmd is close to a Batch file, but on OS/2 Rexx files and TI linker command files also use *.cmd. 277 --- lnk: `/* comment */`, `// comment`, and `--linker-option=value` 278 --- rexx: `/* comment */`, `-- comment` 279 --- @type vim.filetype.mapfn 280 function M.cmd(_, bufnr) 281 local lines = table.concat(getlines(bufnr, 1, 20)) 282 if matchregex(lines, [[MEMORY\|SECTIONS\|\%(^\|\n\)--\S\|\%(^\|\n\)//]]) then 283 return 'lnk' 284 else 285 local line1 = getline(bufnr, 1) 286 if line1:find('^/%*') then 287 return 'rexx' 288 else 289 return 'dosbatch' 290 end 291 end 292 end 293 294 --- @type vim.filetype.mapfn 295 function M.conf(path, bufnr) 296 if fn.did_filetype() ~= 0 or path:find(vim.g.ft_ignore_pat) then 297 return 298 end 299 if path:find('%.conf$') then 300 return 'conf' 301 end 302 for _, line in ipairs(getlines(bufnr, 1, 5)) do 303 if line:find('^#') then 304 return 'conf' 305 end 306 end 307 end 308 309 --- Debian Control 310 --- @type vim.filetype.mapfn 311 function M.control(_, bufnr) 312 local line1 = getline(bufnr, 1) 313 if line1 and findany(line1, { '^Source:', '^Package:' }) then 314 return 'debcontrol' 315 elseif line1 and findany(line1, { '^Tests:', '^Test%-Command:' }) then 316 return 'autopkgtest' 317 end 318 end 319 320 --- Debian Copyright 321 --- @type vim.filetype.mapfn 322 function M.copyright(_, bufnr) 323 if getline(bufnr, 1):find('^Format:') then 324 return 'debcopyright' 325 end 326 end 327 328 --- @type vim.filetype.mapfn 329 function M.cpp(_, _) 330 return vim.g.cynlib_syntax_for_cpp and 'cynlib' or 'cpp' 331 end 332 333 --- @type vim.filetype.mapfn 334 function M.csh(path, bufnr) 335 if fn.did_filetype() ~= 0 then 336 -- Filetype was already detected 337 return 338 end 339 local contents = getlines(bufnr) 340 if vim.g.filetype_csh then 341 return M.shell(path, contents, vim.g.filetype_csh) 342 elseif string.find(vim.o.shell, 'tcsh') then 343 return M.shell(path, contents, 'tcsh') 344 else 345 return M.shell(path, contents, 'csh') 346 end 347 end 348 349 --- @param path string 350 --- @param contents string[] 351 --- @return string? 352 local function cvs_diff(path, contents) 353 for _, line in ipairs(contents) do 354 if not line:find('^%? ') then 355 if matchregex(line, [[^Index:\s\+\f\+$]]) then 356 -- CVS diff 357 return 'diff' 358 elseif 359 -- Locale input files: Formal Definitions of Cultural Conventions 360 -- Filename must be like en_US, fr_FR@euro or en_US.UTF-8 361 findany(path, { 362 '%a%a_%a%a$', 363 '%a%a_%a%a[%.@]', 364 '%a%a_%a%ai18n$', 365 '%a%a_%a%aPOSIX$', 366 '%a%a_%a%atranslit_', 367 }) 368 then 369 -- Only look at the first 100 lines 370 for line_nr = 1, 100 do 371 if not contents[line_nr] then 372 break 373 elseif 374 findany(contents[line_nr], { 375 '^LC_IDENTIFICATION$', 376 '^LC_CTYPE$', 377 '^LC_COLLATE$', 378 '^LC_MONETARY$', 379 '^LC_NUMERIC$', 380 '^LC_TIME$', 381 '^LC_MESSAGES$', 382 '^LC_PAPER$', 383 '^LC_TELEPHONE$', 384 '^LC_MEASUREMENT$', 385 '^LC_NAME$', 386 '^LC_ADDRESS$', 387 }) 388 then 389 return 'fdcc' 390 end 391 end 392 end 393 end 394 end 395 end 396 397 --- @type vim.filetype.mapfn 398 function M.dat(path, bufnr) 399 local file_name = fs.basename(path):lower() 400 -- Innovation data processing 401 if findany(file_name, { '^upstream%.dat$', '^upstream%..*%.dat$', '^.*%.upstream%.dat$' }) then 402 return 'upstreamdat' 403 end 404 if vim.g.filetype_dat then 405 return vim.g.filetype_dat 406 end 407 -- Determine if a *.dat file is Kuka Robot Language 408 local line = nextnonblank(bufnr, 1) 409 if matchregex(line, [[\c\v^\s*%(\&\w+|defdat>)]]) then 410 return 'krl' 411 end 412 end 413 414 --- @type vim.filetype.mapfn 415 function M.decl(_, bufnr) 416 for _, line in ipairs(getlines(bufnr, 1, 3)) do 417 if line:lower():find('^<!sgml') then 418 return 'sgmldecl' 419 end 420 end 421 end 422 423 -- This function is called for all files under */debian/patches/*, make sure not 424 -- to non-dep3patch files, such as README and other text files. 425 --- @type vim.filetype.mapfn 426 function M.dep3patch(path, bufnr) 427 local file_name = fs.basename(path) 428 if file_name == 'series' then 429 return 430 end 431 432 for _, line in ipairs(getlines(bufnr, 1, 100)) do 433 if 434 findany(line, { 435 '^Description:', 436 '^Subject:', 437 '^Origin:', 438 '^Bug:', 439 '^Forwarded:', 440 '^Author:', 441 '^From:', 442 '^Reviewed%-by:', 443 '^Acked%-by:', 444 '^Last%-Updated:', 445 '^Applied%-Upstream:', 446 }) 447 then 448 return 'dep3patch' 449 elseif line:find('^%-%-%-') then 450 -- End of headers found. stop processing 451 return 452 end 453 end 454 end 455 456 local function diff(contents) 457 if 458 contents[1]:find('^%-%-%- ') and contents[2]:find('^%+%+%+ ') 459 or contents[1]:find('^%* looking for ') and contents[2]:find('^%* comparing to ') 460 or contents[1]:find('^%*%*%* ') and contents[2]:find('^%-%-%- ') 461 or contents[1]:find('^=== ') and ((contents[2]:find('^' .. string.rep('=', 66)) and contents[3]:find( 462 '^%-%-% ' 463 ) and contents[4]:find('^%+%+%+')) or (contents[2]:find('^%-%-%- ') and contents[3]:find( 464 '^%+%+%+ ' 465 ))) 466 or findany(contents[1], { '^=== removed', '^=== added', '^=== renamed', '^=== modified' }) 467 then 468 return 'diff' 469 end 470 end 471 472 local function dns_zone(contents) 473 if 474 findany( 475 contents[1] .. contents[2] .. contents[3] .. contents[4], 476 { '^; <<>> DiG [0-9%.]+.* <<>>', '%$ORIGIN', '%$TTL', 'IN%s+SOA' } 477 ) 478 then 479 return 'bindzone' 480 end 481 -- BAAN 482 if -- Check for 1 to 80 '*' characters 483 contents[1]:find('|%*' .. string.rep('%*?', 79)) and contents[2]:find('VRC ') 484 or contents[2]:find('|%*' .. string.rep('%*?', 79)) and contents[3]:find('VRC ') 485 then 486 return 'baan' 487 end 488 end 489 490 --- @type vim.filetype.mapfn 491 function M.dtrace(_, bufnr) 492 if fn.did_filetype() ~= 0 then 493 -- Filetype was already detected 494 return 495 end 496 for _, line in ipairs(getlines(bufnr, 1, 100)) do 497 if matchregex(line, [[\c^module\>\|^import\>]]) then 498 -- D files often start with a module and/or import statement. 499 return 'd' 500 elseif findany(line, { '^#!%S+dtrace', '#pragma%s+D%s+option', ':%S-:%S-:' }) then 501 return 'dtrace' 502 end 503 end 504 return 'd' 505 end 506 507 --- @param bufnr integer 508 --- @return boolean 509 local function is_modula2(bufnr) 510 return matchregex(nextnonblank(bufnr, 1), [[\<MODULE\s\+\w\+\s*\%(\[.*]\s*\)\=;\|^\s*(\*]]) 511 end 512 513 --- @param bufnr integer 514 --- @return string, fun(b: integer) 515 local function modula2(bufnr) 516 local dialect = vim.g.modula2_default_dialect or 'pim' 517 local extension = vim.g.modula2_default_extension or '' 518 519 -- ignore unknown dialects or badly formatted tags 520 for _, line in ipairs(getlines(bufnr, 1, 200)) do 521 local matched_dialect, matched_extension = line:match('%(%*!m2(%w+)%+(%w+)%*%)') 522 if not matched_dialect then 523 matched_dialect = line:match('%(%*!m2(%w+)%*%)') 524 end 525 if matched_dialect then 526 if vim.tbl_contains({ 'iso', 'pim', 'r10' }, matched_dialect) then 527 dialect = matched_dialect 528 end 529 if vim.tbl_contains({ 'gm2' }, matched_extension) then 530 extension = matched_extension 531 end 532 break 533 end 534 end 535 536 return 'modula2', 537 function(b) 538 vim._with({ buf = b }, function() 539 fn['modula2#SetDialect'](dialect, extension) 540 end) 541 end 542 end 543 544 --- @type vim.filetype.mapfn 545 function M.def(_, bufnr) 546 if getline(bufnr, 1):find('%%%%') then 547 return 'tex' 548 end 549 if vim.g.filetype_def == 'modula2' or is_modula2(bufnr) then 550 return modula2(bufnr) 551 end 552 553 if vim.g.filetype_def then 554 return vim.g.filetype_def 555 end 556 return 'def' 557 end 558 559 --- @type vim.filetype.mapfn 560 function M.dsp(path, bufnr) 561 if vim.g.filetype_dsp then 562 return vim.g.filetype_dsp 563 end 564 565 -- Test the filename 566 local file_name = fs.basename(path) 567 if file_name:find('^[mM]akefile.*$') then 568 return 'make' 569 end 570 571 -- Test the file contents 572 for _, line in ipairs(getlines(bufnr, 1, 200)) do 573 if 574 findany(line, { 575 -- Check for comment style 576 [[#.*]], 577 -- Check for common lines 578 [[^.*Microsoft Developer Studio Project File.*$]], 579 [[^!MESSAGE This is not a valid makefile\..+$]], 580 -- Check for keywords 581 [[^!(IF,ELSEIF,ENDIF).*$]], 582 -- Check for common assignments 583 [[^SOURCE=.*$]], 584 }) 585 then 586 return 'make' 587 end 588 end 589 590 -- Otherwise, assume we have a Faust file 591 return 'faust' 592 end 593 594 --- @type vim.filetype.mapfn 595 function M.e(_, bufnr) 596 if vim.g.filetype_euphoria then 597 return vim.g.filetype_euphoria 598 end 599 for _, line in ipairs(getlines(bufnr, 1, 100)) do 600 if findany(line, { "^%s*<'%s*$", "^%s*'>%s*$" }) then 601 return 'specman' 602 end 603 end 604 return 'eiffel' 605 end 606 607 --- @type vim.filetype.mapfn 608 function M.edn(_, bufnr) 609 local line = getline(bufnr, 1) 610 if matchregex(line, [[\c^\s*(\s*edif\>]]) then 611 return 'edif' 612 else 613 return 'clojure' 614 end 615 end 616 617 -- This function checks for valid cl syntax in the first five lines. 618 -- Look for either an opening comment, '#', or a block start, '{'. 619 -- If not found, assume SGML. 620 --- @type vim.filetype.mapfn 621 function M.ent(_, bufnr) 622 for _, line in ipairs(getlines(bufnr, 1, 5)) do 623 if line:find('^%s*[#{]') then 624 return 'cl' 625 elseif not line:find('^%s*$') then 626 -- Not a blank line, not a comment, and not a block start, 627 -- so doesn't look like valid cl code. 628 break 629 end 630 end 631 return 'dtd' 632 end 633 634 --- @type vim.filetype.mapfn 635 function M.euphoria(_, _) 636 return vim.g.filetype_euphoria or 'euphoria3' 637 end 638 639 --- @type vim.filetype.mapfn 640 function M.ex(_, bufnr) 641 if vim.g.filetype_euphoria then 642 return vim.g.filetype_euphoria 643 else 644 for _, line in ipairs(getlines(bufnr, 1, 100)) do 645 if matchregex(line, [[\c^--\|^ifdef\>\|^include\>]]) then 646 return 'euphoria3' 647 end 648 end 649 return 'elixir' 650 end 651 end 652 653 --- @param bufnr integer 654 --- @return boolean 655 local function is_forth(bufnr) 656 local first_line = nextnonblank(bufnr, 1) 657 658 -- SwiftForth block comment (line is usually filled with '-' or '=') or 659 -- OPTIONAL (sometimes precedes the header comment) 660 if first_line and findany(first_line:lower(), { '^%{%s', '^%{$', '^optional%s' }) then 661 return true 662 end 663 664 for _, line in ipairs(getlines(bufnr, 1, 100)) do 665 -- Forth comments and colon definitions 666 if line:find('^[:(\\] ') then 667 return true 668 end 669 end 670 return false 671 end 672 673 -- Distinguish between Forth and Fortran 674 --- @type vim.filetype.mapfn 675 function M.f(_, bufnr) 676 if vim.g.filetype_f then 677 return vim.g.filetype_f 678 end 679 if is_forth(bufnr) then 680 return 'forth' 681 end 682 return 'fortran' 683 end 684 685 -- This function checks the first 15 lines for appearance of 'FoamFile' 686 -- and then 'object' in a following line. 687 -- In that case, it's probably an OpenFOAM file 688 --- @type vim.filetype.mapfn 689 function M.foam(_, bufnr) 690 local foam_file = false 691 for _, line in ipairs(getlines(bufnr, 1, 15)) do 692 if line:find('^FoamFile') then 693 foam_file = true 694 elseif foam_file and line:find('^%s*object') then 695 return 'foam' 696 end 697 end 698 end 699 700 --- @type vim.filetype.mapfn 701 function M.frm(_, bufnr) 702 if vim.g.filetype_frm then 703 return vim.g.filetype_frm 704 end 705 if getline(bufnr, 1) == 'VERSION 5.00' then 706 return 'vb' 707 end 708 for _, line in ipairs(getlines(bufnr, 1, 5)) do 709 if matchregex(line, visual_basic_content) then 710 return 'vb' 711 end 712 end 713 return 'form' 714 end 715 716 --- @type vim.filetype.mapfn 717 function M.fvwm_v1(_, _) 718 return 'fvwm', function(bufnr) 719 vim.b[bufnr].fvwm_version = 1 720 end 721 end 722 723 --- @type vim.filetype.mapfn 724 function M.fvwm_v2(_, _) 725 return 'fvwm', function(bufnr) 726 vim.b[bufnr].fvwm_version = 2 727 end 728 end 729 730 -- Distinguish between Forth and F# 731 --- @type vim.filetype.mapfn 732 function M.fs(_, bufnr) 733 if vim.g.filetype_fs then 734 return vim.g.filetype_fs 735 end 736 if is_forth(bufnr) then 737 return 'forth' 738 end 739 return 'fsharp' 740 end 741 742 --- @type vim.filetype.mapfn 743 function M.git(_, bufnr) 744 local line = getline(bufnr, 1) 745 if matchregex(line, [[^\x\{40,\}\>\|^ref: ]]) then 746 return 'git' 747 end 748 end 749 750 --- @type vim.filetype.mapfn 751 function M.header(_, bufnr) 752 for _, line in ipairs(getlines(bufnr, 1, 200)) do 753 if findany(line:lower(), { '^@interface', '^@end', '^@class' }) then 754 if vim.g.c_syntax_for_h then 755 return 'objc' 756 else 757 return 'objcpp' 758 end 759 end 760 end 761 if vim.g.c_syntax_for_h then 762 return 'c' 763 elseif vim.g.ch_syntax_for_h then 764 return 'ch' 765 else 766 return 'cpp' 767 end 768 end 769 770 --- Recursively search for Hare source files in a directory and any 771 --- subdirectories, up to a given depth. 772 --- @param dir string 773 --- @param depth number 774 --- @return boolean 775 local function is_hare_module(dir, depth) 776 depth = math.max(depth, 0) 777 for name, _ in fs.dir(dir, { depth = depth + 1 }) do 778 if name:find('%.ha$') then 779 return true 780 end 781 end 782 return false 783 end 784 785 --- @type vim.filetype.mapfn 786 function M.haredoc(path, _) 787 if vim.g.filetype_haredoc then 788 if is_hare_module(fs.dirname(path), vim.g.haredoc_search_depth or 1) then 789 return 'haredoc' 790 end 791 end 792 end 793 794 --- @type vim.filetype.mapfn 795 function M.html(_, bufnr) 796 -- Disabled for the reasons mentioned here: 797 -- https://github.com/vim/vim/pull/13594#issuecomment-1834465890 798 -- local filename = fn.fnamemodify(path, ':t') 799 -- if filename:find('%.component%.html$') then 800 -- return 'htmlangular' 801 -- end 802 803 for _, line in ipairs(getlines(bufnr, 1, 40)) do 804 if 805 matchregex( 806 line, 807 [[@\(if\|for\|defer\|switch\)\|\*\(ngIf\|ngFor\|ngSwitch\|ngTemplateOutlet\)\|ng-template\|ng-content]] 808 ) 809 then 810 return 'htmlangular' 811 elseif matchregex(line, [[\<DTD\s\+XHTML\s]]) then 812 return 'xhtml' 813 elseif 814 matchregex( 815 line, 816 [[\c{%\s*\(autoescape\|block\|comment\|csrf_token\|cycle\|debug\|extends\|filter\|firstof\|for\|if\|ifchanged\|include\|load\|lorem\|now\|query_string\|regroup\|resetcycle\|spaceless\|templatetag\|url\|verbatim\|widthratio\|with\)\>\|{#\s\+]] 817 ) 818 then 819 return 'htmldjango' 820 elseif findany(line, { '<extend', '<super>' }) then 821 return 'superhtml' 822 end 823 end 824 return 'html' 825 end 826 827 -- Virata Config Script File or Drupal module 828 --- @type vim.filetype.mapfn 829 function M.hw(_, bufnr) 830 if getline(bufnr, 1):lower():find('<%?php') then 831 return 'php' 832 end 833 return 'virata' 834 end 835 836 -- This function checks for an assembly comment or a SWIG keyword or verbatim 837 -- block in the first 50 lines. 838 -- If not found, assume Progress. 839 --- @type vim.filetype.mapfn 840 function M.i(path, bufnr) 841 if vim.g.filetype_i then 842 return vim.g.filetype_i 843 end 844 845 -- These include the leading '%' sign 846 local ft_swig_keywords = 847 [[^\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\)]] 848 -- This is the start/end of a block that is copied literally to the processor file (C/C++) 849 local ft_swig_verbatim_block_start = '^%s*%%{' 850 851 for _, line in ipairs(getlines(bufnr, 1, 50)) do 852 if line:find('^%s*;') or line:find('^%*') then 853 return M.asm(path, bufnr) 854 elseif matchregex(line, ft_swig_keywords) or line:find(ft_swig_verbatim_block_start) then 855 return 'swig' 856 end 857 end 858 return 'progress' 859 end 860 861 --- @type vim.filetype.mapfn 862 function M.idl(_, bufnr) 863 for _, line in ipairs(getlines(bufnr, 1, 50)) do 864 if findany(line:lower(), { '^%s*import%s+"unknwn"%.idl', '^%s*import%s+"objidl"%.idl' }) then 865 return 'msidl' 866 end 867 end 868 return 'idl' 869 end 870 871 local pascal_comments = { '^%s*{', '^%s*%(%*', '^%s*//' } 872 local pascal_keywords = 873 [[\c^\s*\%(program\|unit\|library\|uses\|begin\|procedure\|function\|const\|type\|var\)\>]] 874 875 --- @type vim.filetype.mapfn 876 function M.inc(path, bufnr) 877 if vim.g.filetype_inc then 878 return vim.g.filetype_inc 879 end 880 for _, line in ipairs(getlines(bufnr, 1, 20)) do 881 if line:lower():find('perlscript') then 882 return 'aspperl' 883 elseif line:find('<%%') then 884 return 'aspvbs' 885 elseif line:find('<%?') then 886 return 'php' 887 -- Pascal supports // comments but they're vary rarely used for file 888 -- headers so assume POV-Ray 889 elseif findany(line, { '^%s{', '^%s%(%*' }) or matchregex(line, pascal_keywords) then 890 return 'pascal' 891 elseif 892 findany(line, { '^%s*inherit ', '^%s*require ', '^%s*%u[%w_:${}/]*%s+%??[?:+.]?=.? ' }) 893 then 894 return 'bitbake' 895 end 896 end 897 local syntax = M.asm_syntax(path, bufnr) 898 if not syntax or syntax == '' then 899 return 'pov' 900 end 901 return syntax, function(b) 902 vim.b[b].asmsyntax = syntax 903 end 904 end 905 906 --- @type vim.filetype.mapfn 907 function M.inp(_, bufnr) 908 if getline(bufnr, 1):find('%%%%') then 909 return 'tex' 910 elseif getline(bufnr, 1):find('^%*') then 911 return 'abaqus' 912 else 913 for _, line in ipairs(getlines(bufnr, 1, 500)) do 914 if line:lower():find('^header surface data') then 915 return 'trasys' 916 end 917 end 918 end 919 end 920 921 --- @type vim.filetype.mapfn 922 function M.install(path, bufnr) 923 if getline(bufnr, 1):lower():find('<%?php') then 924 return 'php' 925 end 926 return M.bash(path, bufnr) 927 end 928 929 --- Innovation Data Processing 930 --- (refactor of filetype.vim since the patterns are case-insensitive) 931 --- @type vim.filetype.mapfn 932 function M.log(path, _) 933 path = path:lower() --- @type string LuaLS bug 934 if 935 findany( 936 path, 937 { 'upstream%.log', 'upstream%..*%.log', '.*%.upstream%.log', 'upstream%-.*%.log' } 938 ) 939 then 940 return 'upstreamlog' 941 elseif 942 findany( 943 path, 944 { 'upstreaminstall%.log', 'upstreaminstall%..*%.log', '.*%.upstreaminstall%.log' } 945 ) 946 then 947 return 'upstreaminstalllog' 948 elseif findany(path, { 'usserver%.log', 'usserver%..*%.log', '.*%.usserver%.log' }) then 949 return 'usserverlog' 950 elseif findany(path, { 'usw2kagt%.log', 'usw2kagt%..*%.log', '.*%.usw2kagt%.log' }) then 951 return 'usw2kagtlog' 952 end 953 end 954 955 --- @type vim.filetype.mapfn 956 function M.ll(_, bufnr) 957 local first_line = getline(bufnr, 1) 958 if matchregex(first_line, [[;\|\<source_filename\>\|\<target\>]]) then 959 return 'llvm' 960 end 961 for _, line in ipairs(getlines(bufnr, 1, 100)) do 962 if line:find('^%s*%%') then 963 return 'lex' 964 end 965 end 966 return 'lifelines' 967 end 968 969 --- @type vim.filetype.mapfn 970 function M.lpc(_, bufnr) 971 if vim.g.lpc_syntax_for_c then 972 for _, line in ipairs(getlines(bufnr, 1, 12)) do 973 if 974 findany(line, { 975 '^//', 976 '^inherit', 977 '^private', 978 '^protected', 979 '^nosave', 980 '^string', 981 '^object', 982 '^mapping', 983 '^mixed', 984 }) 985 then 986 return 'lpc' 987 end 988 end 989 end 990 return 'c' 991 end 992 993 --- @type vim.filetype.mapfn 994 function M.lsl(_, bufnr) 995 if vim.g.filetype_lsl then 996 return vim.g.filetype_lsl 997 end 998 999 local line = nextnonblank(bufnr, 1) 1000 if findany(line, { '^%s*%%', ':%s*trait%s*$' }) then 1001 return 'larch' 1002 else 1003 return 'lsl' 1004 end 1005 end 1006 1007 --- @type vim.filetype.mapfn 1008 function M.m(_, bufnr) 1009 if vim.g.filetype_m then 1010 return vim.g.filetype_m 1011 end 1012 1013 -- Excluding end(for|function|if|switch|while) common to Murphi 1014 local octave_block_terminators = 1015 [[\<end\%(_try_catch\|classdef\|enumeration\|events\|methods\|parfor\|properties\)\>]] 1016 local objc_preprocessor = 1017 [[\c^\s*#\s*\%(import\|include\|define\|if\|ifn\=def\|undef\|line\|error\|pragma\)\>]] 1018 1019 -- Whether we've seen a multiline comment leader 1020 local saw_comment = false 1021 for _, line in ipairs(getlines(bufnr, 1, 100)) do 1022 if line:find('^%s*/%*') then 1023 -- /* ... */ is a comment in Objective C and Murphi, so we can't conclude 1024 -- it's either of them yet, but track this as a hint in case we don't see 1025 -- anything more definitive. 1026 saw_comment = true 1027 end 1028 if 1029 line:find('^%s*//') 1030 or matchregex(line, [[\c^\s*@import\>]]) 1031 or matchregex(line, objc_preprocessor) 1032 then 1033 return 'objc' 1034 end 1035 if 1036 findany(line, { '^%s*#', '^%s*%%!' }) 1037 or matchregex(line, [[\c^\s*unwind_protect\>]]) 1038 or matchregex(line, [[\c\%(^\|;\)\s*]] .. octave_block_terminators) 1039 then 1040 return 'octave' 1041 elseif line:find('^%s*%%') then 1042 return 'matlab' 1043 elseif line:find('^%s*%(%*') then 1044 return 'mma' 1045 elseif matchregex(line, [[\c^\s*\(\(type\|var\)\>\|--\)]]) then 1046 return 'murphi' 1047 end 1048 end 1049 1050 if saw_comment then 1051 -- We didn't see anything definitive, but this looks like either Objective C 1052 -- or Murphi based on the comment leader. Assume the former as it is more 1053 -- common. 1054 return 'objc' 1055 else 1056 -- Default is Matlab 1057 return 'matlab' 1058 end 1059 end 1060 1061 --- For files ending in *.m4, distinguish: 1062 --- – *.html.m4 files 1063 --- - *fvwm2rc*.m4 files 1064 --- – files in the Autoconf M4 dialect 1065 --- – files in POSIX M4 1066 --- @type vim.filetype.mapfn 1067 function M.m4(path, bufnr) 1068 local fname = fs.basename(path) 1069 path = fs.dirname(fs.abspath(path)) 1070 1071 if fname:find('html%.m4$') then 1072 return 'htmlm4' 1073 end 1074 1075 if fname:find('fvwm2rc') then 1076 return 'fvwm2m4' 1077 end 1078 1079 -- Canonical Autoconf file 1080 if fname == 'aclocal.m4' then 1081 return 'config' 1082 end 1083 1084 -- Repo heuristic for Autoconf M4 (nearby configure.ac) 1085 if 1086 fn.filereadable(path .. '/../configure.ac') ~= 0 1087 or fn.filereadable(path .. '/configure.ac') ~= 0 1088 then 1089 return 'config' 1090 end 1091 1092 -- Content heuristic for Autoconf M4 (scan first ~200 lines) 1093 -- Signals: 1094 -- - Autoconf macro prefixes: AC_/AM_/AS_/AU_/AT_ 1095 for _, line in ipairs(getlines(bufnr, 1, 200)) do 1096 if line:find('^%s*A[CMSUT]_') then 1097 return 'config' 1098 end 1099 end 1100 1101 -- Default to POSIX M4 1102 return 'm4' 1103 end 1104 1105 --- @param contents string[] 1106 --- @return string? 1107 local function m4(contents) 1108 for _, line in ipairs(contents) do 1109 if matchregex(line, [[^\s*dnl\>]]) then 1110 return 'm4' 1111 end 1112 end 1113 if vim.env.TERM == 'amiga' and findany(assert(contents[1]):lower(), { '^;', '^%.bra' }) then 1114 -- AmigaDos scripts 1115 return 'amiga' 1116 end 1117 end 1118 1119 --- Check if it is a Microsoft Makefile 1120 --- @type vim.filetype.mapfn 1121 function M.make(path, bufnr) 1122 vim.b.make_flavor = nil 1123 1124 -- 1. filename 1125 local file_name = fs.basename(path) 1126 if file_name == 'BSDmakefile' then 1127 vim.b.make_flavor = 'bsd' 1128 return 'make' 1129 elseif file_name == 'GNUmakefile' then 1130 vim.b.make_flavor = 'gnu' 1131 return 'make' 1132 end 1133 1134 -- 2. user's setting 1135 if vim.g.make_flavor ~= nil then 1136 vim.b.make_flavor = vim.g.make_flavor 1137 return 'make' 1138 elseif vim.g.make_microsoft ~= nil then 1139 vim._truncated_echo_once( 1140 "make_microsoft is deprecated; try g:make_flavor = 'microsoft' instead" 1141 ) 1142 vim.b.make_flavor = 'microsoft' 1143 return 'make' 1144 end 1145 1146 -- 3. try to detect a flavor from file content 1147 for _, line in ipairs(getlines(bufnr, 1, 1000)) do 1148 if matchregex(line, [[\c^\s*!\s*\(ifn\=\(def\)\=\|include\|message\|error\)\>]]) then 1149 vim.b.make_flavor = 'microsoft' 1150 break 1151 elseif 1152 matchregex(line, [[^\.\%(export\|error\|for\|if\%(n\=\%(def\|make\)\)\=\|info\|warning\)\>]]) 1153 then 1154 vim.b.make_flavor = 'bsd' 1155 break 1156 elseif 1157 matchregex(line, [[^ *\%(ifn\=\%(eq\|def\)\|define\|override\)\>]]) 1158 or line:find('%$[({][a-z-]+%s+%S+') -- a function call, e.g. $(shell pwd) 1159 then 1160 vim.b.make_flavor = 'gnu' 1161 break 1162 end 1163 end 1164 return 'make' 1165 end 1166 1167 --- @type vim.filetype.mapfn 1168 function M.markdown(_, _) 1169 return vim.g.filetype_md or 'markdown' 1170 end 1171 1172 --- Rely on the file to start with a comment. 1173 --- MS message text files use ';', Sendmail files use '#' or 'dnl' 1174 --- @type vim.filetype.mapfn 1175 function M.mc(_, bufnr) 1176 for _, line in ipairs(getlines(bufnr, 1, 20)) do 1177 if findany(line:lower(), { '^%s*#', '^%s*dnl' }) then 1178 -- Sendmail .mc file 1179 return 'm4' 1180 elseif line:find('^%s*;') then 1181 return 'msmessages' 1182 end 1183 end 1184 -- Default: Sendmail .mc file 1185 return 'm4' 1186 end 1187 1188 --- @param path string 1189 --- @return string? 1190 function M.me(path) 1191 local filename = fs.basename(path):lower() 1192 if filename ~= 'read.me' and filename ~= 'click.me' then 1193 return 'nroff' 1194 end 1195 end 1196 1197 --- @type vim.filetype.mapfn 1198 function M.mm(_, bufnr) 1199 for _, line in ipairs(getlines(bufnr, 1, 20)) do 1200 if matchregex(line, [[\c^\s*\(#\s*\(include\|import\)\>\|@import\>\|/\*\)]]) then 1201 return 'objcpp' 1202 end 1203 end 1204 return 'nroff' 1205 end 1206 1207 --- @type vim.filetype.mapfn 1208 function M.mms(_, bufnr) 1209 for _, line in ipairs(getlines(bufnr, 1, 20)) do 1210 if findany(line, { '^%s*%%', '^%s*//', '^%*' }) then 1211 return 'mmix' 1212 elseif line:find('^%s*#') then 1213 return 'make' 1214 end 1215 end 1216 return 'mmix' 1217 end 1218 1219 --- Returns true if file content looks like LambdaProlog 1220 --- @param bufnr integer 1221 --- @return boolean 1222 local function is_lprolog(bufnr) 1223 -- Skip apparent comments and blank lines, what looks like 1224 -- LambdaProlog comment may be RAPID header 1225 for _, line in ipairs(getlines(bufnr)) do 1226 -- The second pattern matches a LambdaProlog comment 1227 if not findany(line, { '^%s*$', '^%s*%%' }) then 1228 -- The pattern must not catch a go.mod file 1229 return matchregex(line, [[\c\<module\s\+\w\+\s*\.\s*\(%\|$\)]]) 1230 end 1231 end 1232 return false 1233 end 1234 1235 --- Determine if *.mod is ABB RAPID, LambdaProlog, Modula-2, Modsim III or go.mod 1236 --- @type vim.filetype.mapfn 1237 function M.mod(path, bufnr) 1238 if vim.g.filetype_mod == 'modula2' or is_modula2(bufnr) then 1239 return modula2(bufnr) 1240 end 1241 1242 if vim.g.filetype_mod then 1243 return vim.g.filetype_mod 1244 elseif matchregex(path, [[\c\<go\.mod$]]) then 1245 return 'gomod' 1246 elseif is_lprolog(bufnr) then 1247 return 'lprolog' 1248 elseif is_rapid(bufnr) then 1249 return 'rapid' 1250 end 1251 -- Nothing recognized, assume modsim3 1252 return 'modsim3' 1253 end 1254 1255 --- Determine if *.mod is ABB RAPID, LambdaProlog, Modula-2, Modsim III or go.mod 1256 --- @type vim.filetype.mapfn 1257 function M.mp(_, _) 1258 return 'mp', function(b) 1259 vim.b[b].mp_metafun = 1 1260 end 1261 end 1262 1263 --- @type vim.filetype.mapfn 1264 function M.news(_, bufnr) 1265 if getline(bufnr, 1):lower():find('; urgency=') then 1266 return 'debchangelog' 1267 end 1268 end 1269 1270 --- This function checks if one of the first five lines start with a typical 1271 --- nroff pattern in man files. In that case it is probably an nroff file. 1272 --- @type vim.filetype.mapfn 1273 function M.nroff(_, bufnr) 1274 for _, line in ipairs(getlines(bufnr, 1, 5)) do 1275 if 1276 matchregex( 1277 line, 1278 [[^\%([.']\s*\%(TH\|D[dt]\|S[Hh]\|d[es]1\?\|so\)\s\+\S\|[.'']\s*ig\>\|\%([.'']\s*\)\?\\"\)]] 1279 ) 1280 then 1281 return 'nroff' 1282 end 1283 end 1284 end 1285 1286 --- @type vim.filetype.mapfn 1287 function M.patch(_, bufnr) 1288 local firstline = getline(bufnr, 1) 1289 if string.find(firstline, '^From ' .. string.rep('%x', 40) .. '+ Mon Sep 17 00:00:00 2001$') then 1290 return 'gitsendemail' 1291 end 1292 return 'diff' 1293 end 1294 1295 --- If the file has an extension of 't' and is in a directory 't' or 'xt' then 1296 --- it is almost certainly a Perl test file. 1297 --- If the first line starts with '#' and contains 'perl' it's probably a Perl file. 1298 --- (Slow test) If a file contains a 'use' statement then it is almost certainly a Perl file. 1299 --- @type vim.filetype.mapfn 1300 function M.perl(path, bufnr) 1301 local dir_name = fs.dirname(path) 1302 if fn.fnamemodify(path, '%:e') == 't' and (dir_name == 't' or dir_name == 'xt') then 1303 return 'perl' 1304 end 1305 local first_line = getline(bufnr, 1) 1306 if first_line:find('^#') and first_line:lower():find('perl') then 1307 return 'perl' 1308 end 1309 for _, line in ipairs(getlines(bufnr, 1, 30)) do 1310 if matchregex(line, [[\c^use\s\s*\k]]) then 1311 return 'perl' 1312 end 1313 end 1314 end 1315 1316 local prolog_patterns = { '^%s*:%-', '^%s*%%+%s', '^%s*%%+$', '^%s*/%*', '%.%s*$' } 1317 1318 --- @type vim.filetype.mapfn 1319 function M.pl(_, bufnr) 1320 if vim.g.filetype_pl then 1321 return vim.g.filetype_pl 1322 end 1323 -- Recognize Prolog by specific text in the first non-empty line; 1324 -- require a blank after the '%' because Perl uses "%list" and "%translate" 1325 local line = nextnonblank(bufnr, 1) 1326 if line and matchregex(line, [[\c\<prolog\>]]) or findany(line, prolog_patterns) then 1327 return 'prolog' 1328 else 1329 return 'perl' 1330 end 1331 end 1332 1333 --- @type vim.filetype.mapfn 1334 function M.pm(_, bufnr) 1335 local line = getline(bufnr, 1) 1336 if line:find('XPM2') then 1337 return 'xpm2' 1338 elseif line:find('XPM') then 1339 return 'xpm' 1340 else 1341 return 'perl' 1342 end 1343 end 1344 1345 --- @type vim.filetype.mapfn 1346 function M.pp(_, bufnr) 1347 if vim.g.filetype_pp then 1348 return vim.g.filetype_pp 1349 end 1350 local line = nextnonblank(bufnr, 1) 1351 if findany(line, pascal_comments) or matchregex(line, pascal_keywords) then 1352 return 'pascal' 1353 else 1354 return 'puppet' 1355 end 1356 end 1357 1358 --- @type vim.filetype.mapfn 1359 function M.prg(_, bufnr) 1360 if vim.g.filetype_prg then 1361 return vim.g.filetype_prg 1362 elseif is_rapid(bufnr) then 1363 return 'rapid' 1364 else 1365 -- Nothing recognized, assume Clipper 1366 return 'clipper' 1367 end 1368 end 1369 1370 function M.printcap(ptcap_type) 1371 if fn.did_filetype() == 0 then 1372 return 'ptcap', function(bufnr) 1373 vim.b[bufnr].ptcap_type = ptcap_type 1374 end 1375 end 1376 end 1377 1378 --- @type vim.filetype.mapfn 1379 function M.progress_cweb(_, bufnr) 1380 if vim.g.filetype_w then 1381 return vim.g.filetype_w 1382 else 1383 if 1384 getline(bufnr, 1):lower():find('^&analyze') 1385 or getline(bufnr, 3):lower():find('^&global%-define') 1386 then 1387 return 'progress' 1388 else 1389 return 'cweb' 1390 end 1391 end 1392 end 1393 1394 -- This function checks for valid Pascal syntax in the first 10 lines. 1395 -- Look for either an opening comment or a program start. 1396 -- If not found, assume Progress. 1397 --- @type vim.filetype.mapfn 1398 function M.progress_pascal(_, bufnr) 1399 if vim.g.filetype_p then 1400 return vim.g.filetype_p 1401 end 1402 for _, line in ipairs(getlines(bufnr, 1, 10)) do 1403 if findany(line, pascal_comments) or matchregex(line, pascal_keywords) then 1404 return 'pascal' 1405 elseif not line:find('^%s*$') or line:find('^/%*') then 1406 -- Not an empty line: Doesn't look like valid Pascal code. 1407 -- Or it looks like a Progress /* comment 1408 break 1409 end 1410 end 1411 return 'progress' 1412 end 1413 1414 --- Distinguish between "default", Prolog and Cproto prototype file. 1415 --- @type vim.filetype.mapfn 1416 function M.proto(_, bufnr) 1417 if getline(bufnr, 2):find('/%* Generated automatically %*/') then 1418 return 'c' 1419 elseif getline(bufnr, 2):find('.;$') then 1420 -- Cproto files have a comment in the first line and a function prototype in 1421 -- the second line, it always ends in ";". Indent files may also have 1422 -- comments, thus we can't match comments to see the difference. 1423 -- IDL files can have a single ';' in the second line, require at least one 1424 -- character before the ';'. 1425 return 'cpp' 1426 end 1427 -- Recognize Prolog by specific text in the first non-empty line; 1428 -- require a blank after the '%' because Perl uses "%list" and "%translate" 1429 local line = nextnonblank(bufnr, 1) 1430 if line and matchregex(line, [[\c\<prolog\>]]) or findany(line, prolog_patterns) then 1431 return 'prolog' 1432 end 1433 end 1434 1435 -- Software Distributor Product Specification File (POSIX 1387.2-1995) 1436 --- @type vim.filetype.mapfn 1437 function M.psf(_, bufnr) 1438 local line = getline(bufnr, 1):lower() 1439 if 1440 findany(line, { 1441 '^%s*distribution%s*$', 1442 '^%s*installed_software%s*$', 1443 '^%s*root%s*$', 1444 '^%s*bundle%s*$', 1445 '^%s*product%s*$', 1446 }) 1447 then 1448 return 'psf' 1449 end 1450 end 1451 1452 --- @type vim.filetype.mapfn 1453 function M.r(_, bufnr) 1454 local lines = getlines(bufnr, 1, 50) 1455 -- Rebol is easy to recognize, check for that first 1456 if matchregex(table.concat(lines), [[\c\<rebol\>]]) then 1457 return 'rebol' 1458 end 1459 1460 for _, line in ipairs(lines) do 1461 -- R has # comments 1462 if line:find('^%s*#') then 1463 return 'r' 1464 end 1465 -- Rexx has /* comments */ 1466 if line:find('^%s*/%*') then 1467 return 'rexx' 1468 end 1469 end 1470 1471 -- Nothing recognized, use user default or assume R 1472 if vim.g.filetype_r then 1473 return vim.g.filetype_r 1474 end 1475 -- Rexx used to be the default, but R appears to be much more popular. 1476 return 'r' 1477 end 1478 1479 --- @type vim.filetype.mapfn 1480 function M.redif(_, bufnr) 1481 for _, line in ipairs(getlines(bufnr, 1, 5)) do 1482 if line:lower():find('^template%-type:') then 1483 return 'redif' 1484 end 1485 end 1486 end 1487 1488 --- @type vim.filetype.mapfn 1489 function M.reg(_, bufnr) 1490 local line = getline(bufnr, 1):lower() 1491 if 1492 line:find('^regedit[0-9]*%s*$') or line:find('^windows registry editor version %d*%.%d*%s*$') 1493 then 1494 return 'registry' 1495 end 1496 end 1497 1498 -- Diva (with Skill) or InstallShield 1499 --- @type vim.filetype.mapfn 1500 function M.rul(_, bufnr) 1501 if table.concat(getlines(bufnr, 1, 6)):lower():find('installshield') then 1502 return 'ishd' 1503 end 1504 return 'diva' 1505 end 1506 1507 local udev_rules_pattern = '^%s*udev_rules%s*=%s*"([%^"]+)/*".*' 1508 --- @type vim.filetype.mapfn 1509 function M.rules(path) 1510 path = path:lower() --- @type string LuaLS bug 1511 if 1512 findany(path, { 1513 '/etc/udev/.*%.rules$', 1514 '/etc/udev/rules%.d/.*$.rules$', 1515 '/usr/lib/udev/.*%.rules$', 1516 '/usr/lib/udev/rules%.d/.*%.rules$', 1517 '/lib/udev/.*%.rules$', 1518 '/lib/udev/rules%.d/.*%.rules$', 1519 }) 1520 then 1521 return 'udevrules' 1522 elseif path:find('^/etc/ufw/') then 1523 -- Better than hog 1524 return 'conf' 1525 elseif findany(path, { '^/etc/polkit%-1/rules%.d', '/usr/share/polkit%-1/rules%.d' }) then 1526 return 'javascript' 1527 else 1528 local ok, config_lines = pcall(fn.readfile, '/etc/udev/udev.conf') 1529 if not ok then 1530 return 'hog' 1531 end 1532 --- @cast config_lines -string 1533 local dir = fs.dirname(path) 1534 for _, line in ipairs(config_lines) do 1535 local match = line:match(udev_rules_pattern) 1536 if match then 1537 local udev_rules = line:gsub(udev_rules_pattern, match, 1) 1538 if dir == udev_rules then 1539 return 'udevrules' 1540 end 1541 end 1542 end 1543 return 'hog' 1544 end 1545 end 1546 1547 -- LambdaProlog and Standard ML signature files 1548 --- @type vim.filetype.mapfn 1549 function M.sig(_, bufnr) 1550 if vim.g.filetype_sig then 1551 return vim.g.filetype_sig 1552 end 1553 1554 local line = nextnonblank(bufnr, 1) 1555 1556 -- LambdaProlog comment or keyword 1557 if findany(line, { '^%s*/%*', '^%s*%%', '^%s*sig%s+%a' }) then 1558 return 'lprolog' 1559 -- SML comment or keyword 1560 elseif findany(line, { '^%s*%(%*', '^%s*signature%s+%a', '^%s*structure%s+%a' }) then 1561 return 'sml' 1562 end 1563 end 1564 1565 --- @type vim.filetype.mapfn 1566 function M.sa(_, bufnr) 1567 local lines = table.concat(getlines(bufnr, 1, 4), '\n') 1568 if findany(lines, { '^;', '\n;' }) then 1569 return 'tiasm' 1570 end 1571 return 'sather' 1572 end 1573 1574 -- This function checks the first 25 lines of file extension "sc" to resolve 1575 -- detection between scala and SuperCollider 1576 --- @type vim.filetype.mapfn 1577 function M.sc(_, bufnr) 1578 for _, line in ipairs(getlines(bufnr, 1, 25)) do 1579 if 1580 findany(line, { 1581 'var%s<', 1582 'classvar%s<', 1583 '%^this.*', 1584 '|%w+|', 1585 '%+%s%w*%s{', 1586 '%*ar%s', 1587 }) 1588 then 1589 return 'supercollider' 1590 end 1591 end 1592 return 'scala' 1593 end 1594 1595 -- This function checks the first line of file extension "scd" to resolve 1596 -- detection between scdoc and SuperCollider 1597 --- @type vim.filetype.mapfn 1598 function M.scd(_, bufnr) 1599 local first = '^%S+%(%d[0-9A-Za-z]*%)' 1600 local opt = [[%s+"[^"]*"]] 1601 local line = getline(bufnr, 1) 1602 if findany(line, { first .. '$', first .. opt .. '$', first .. opt .. opt .. '$' }) then 1603 return 'scdoc' 1604 end 1605 return 'supercollider' 1606 end 1607 1608 --- @type vim.filetype.mapfn 1609 function M.sgml(_, bufnr) 1610 local lines = table.concat(getlines(bufnr, 1, 5)) 1611 if lines:find('linuxdoc') then 1612 return 'sgmllnx' 1613 elseif lines:find('<!DOCTYPE.*DocBook') then 1614 return 'docbk', 1615 function(b) 1616 vim.b[b].docbk_type = 'sgml' 1617 vim.b[b].docbk_ver = 4 1618 end 1619 else 1620 return 'sgml' 1621 end 1622 end 1623 1624 --- @param path string 1625 --- @param contents string[] 1626 --- @param name? string 1627 --- @return string?, fun(b: integer)? 1628 local function sh(path, contents, name) 1629 -- Path may be nil, do not fail in that case 1630 if fn.did_filetype() ~= 0 or (path or ''):find(vim.g.ft_ignore_pat) then 1631 -- Filetype was already detected or detection should be skipped 1632 return 1633 end 1634 1635 -- Get the name from the first line if not specified 1636 name = name or contents[1] or '' 1637 if name:find('^csh$') or matchregex(name, [[^#!.\{-2,}\<csh\>]]) then 1638 -- Some .sh scripts contain #!/bin/csh. 1639 return M.shell(path, contents, 'csh') 1640 elseif name:find('^tcsh$') or matchregex(name, [[^#!.\{-2,}\<tcsh\>]]) then 1641 -- Some .sh scripts contain #!/bin/tcsh. 1642 return M.shell(path, contents, 'tcsh') 1643 elseif name:find('^zsh$') or matchregex(name, [[^#!.\{-2,}\<zsh\>]]) then 1644 -- Some .sh scripts contain #!/bin/zsh. 1645 return M.shell(path, contents, 'zsh') 1646 end 1647 1648 local on_detect --- @type fun(b: integer)? 1649 1650 if name:find('^ksh$') or matchregex(name, [[^#!.\{-2,}\<ksh\>]]) then 1651 on_detect = function(b) 1652 vim.b[b].is_kornshell = 1 1653 vim.b[b].is_bash = nil 1654 vim.b[b].is_sh = nil 1655 end 1656 elseif 1657 vim.g.bash_is_sh 1658 or name:find('^bash2?$') 1659 or matchregex(name, [[^#!.\{-2,}\<bash2\=\>]]) 1660 then 1661 on_detect = function(b) 1662 vim.b[b].is_bash = 1 1663 vim.b[b].is_kornshell = nil 1664 vim.b[b].is_sh = nil 1665 end 1666 elseif findany(name, { '^sh$', '^dash$' }) or matchregex(name, [[^#!.\{-2,}\<\%(da\)\=sh\>]]) then -- Ubuntu links "sh" to "dash" 1667 on_detect = function(b) 1668 vim.b[b].is_sh = 1 1669 vim.b[b].is_kornshell = nil 1670 vim.b[b].is_bash = nil 1671 end 1672 end 1673 return M.shell(path, contents, 'sh'), on_detect 1674 end 1675 1676 --- @param name? string 1677 --- @return vim.filetype.mapfn 1678 local function sh_with(name) 1679 return function(path, bufnr) 1680 return sh(path, getlines(bufnr), name) 1681 end 1682 end 1683 1684 M.sh = sh_with() 1685 M.bash = sh_with('bash') 1686 M.ksh = sh_with('ksh') 1687 M.tcsh = sh_with('tcsh') 1688 1689 --- For shell-like file types, check for an "exec" command hidden in a comment, as used for Tcl. 1690 --- @param path string 1691 --- @param contents string[] 1692 --- @param name? string 1693 --- @return string? 1694 function M.shell(path, contents, name) 1695 if fn.did_filetype() ~= 0 or matchregex(path, vim.g.ft_ignore_pat) then 1696 -- Filetype was already detected or detection should be skipped 1697 return 1698 end 1699 1700 local prev_line = '' 1701 for line_nr, line in ipairs(contents) do 1702 -- Skip the first line 1703 if line_nr ~= 1 then 1704 --- @type string 1705 line = line:lower() 1706 if line:find('%s*exec%s') and not prev_line:find('^%s*#.*\\$') then 1707 -- Found an "exec" line after a comment with continuation 1708 local n = line:gsub('%s*exec%s+([^ ]*/)?', '', 1) 1709 if matchregex(n, [[\c\<tclsh\|\<wish]]) then 1710 return 'tcl' 1711 end 1712 end 1713 prev_line = line 1714 end 1715 end 1716 return name 1717 end 1718 1719 -- Swift Intermediate Language or SILE 1720 --- @type vim.filetype.mapfn 1721 function M.sil(_, bufnr) 1722 for _, line in ipairs(getlines(bufnr, 1, 100)) do 1723 if line:find('^%s*[\\%%]') then 1724 return 'sile' 1725 elseif line:find('^%s*%S') then 1726 return 'sil' 1727 end 1728 end 1729 -- No clue, default to "sil" 1730 return 'sil' 1731 end 1732 1733 -- SMIL or SNMP MIB file 1734 --- @type vim.filetype.mapfn 1735 function M.smi(_, bufnr) 1736 local line = getline(bufnr, 1) 1737 if matchregex(line, [[\c\<smil\>]]) then 1738 return 'smil' 1739 else 1740 return 'mib' 1741 end 1742 end 1743 1744 --- @type vim.filetype.mapfn 1745 function M.sql(_, _) 1746 return vim.g.filetype_sql and vim.g.filetype_sql or 'sql' 1747 end 1748 1749 -- Determine if a *.src file is Kuka Robot Language 1750 --- @type vim.filetype.mapfn 1751 function M.src(_, bufnr) 1752 if vim.g.filetype_src then 1753 return vim.g.filetype_src 1754 end 1755 local line = nextnonblank(bufnr, 1) 1756 if matchregex(line, [[\c\v^\s*%(\&\w+|%(global\s+)?def%(fct)?>)]]) then 1757 return 'krl' 1758 end 1759 end 1760 1761 --- @type vim.filetype.mapfn 1762 function M.sys(_, bufnr) 1763 if vim.g.filetype_sys then 1764 return vim.g.filetype_sys 1765 elseif is_rapid(bufnr) then 1766 return 'rapid' 1767 end 1768 return 'bat' 1769 end 1770 1771 -- Choose context, plaintex, or tex (LaTeX) based on these rules: 1772 -- 1. Check the first line of the file for "%&<format>". 1773 -- 2. Check the first 1000 non-comment lines for LaTeX or ConTeXt keywords. 1774 -- 3. Default to "plain" or to g:tex_flavor, can be set in user's vimrc. 1775 --- @type vim.filetype.mapfn 1776 function M.tex(path, bufnr) 1777 local matched, _, format = getline(bufnr, 1):find('^%%&%s*(%a+)') 1778 if matched and format then 1779 --- @type string 1780 format = format:lower():gsub('pdf', '', 1) 1781 elseif path:lower():find('tex/context/.*/.*%.tex') then 1782 return 'context' 1783 else 1784 -- Default value, may be changed later: 1785 format = vim.g.tex_flavor or 'plaintex' 1786 1787 local lpat = [[documentclass\>\|usepackage\>\|begin{\|newcommand\>\|renewcommand\>]] 1788 local cpat = 1789 [[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\>]] 1790 1791 for i, l in ipairs(getlines(bufnr, 1, 1000)) do 1792 -- Find first non-comment line 1793 if not l:find('^%s*%%%S') then 1794 -- Check the next thousand lines for a LaTeX or ConTeXt keyword. 1795 for _, line in ipairs(getlines(bufnr, i, i + 1000)) do 1796 if matchregex(line, [[\c^\s*\\\%(]] .. lpat .. [[\)]]) then 1797 return 'tex' 1798 elseif matchregex(line, [[\c^\s*\\\%(]] .. cpat .. [[\)]]) then 1799 return 'context' 1800 end 1801 end 1802 end 1803 end 1804 end -- if matched 1805 1806 -- Translation from formats to file types. TODO: add AMSTeX, RevTex, others? 1807 if format == 'plain' then 1808 return 'plaintex' 1809 elseif format == 'plaintex' or format == 'context' then 1810 return format 1811 else 1812 -- Probably LaTeX 1813 return 'tex' 1814 end 1815 end 1816 1817 -- Determine if a *.tf file is TF (TinyFugue) mud client or terraform 1818 --- @type vim.filetype.mapfn 1819 function M.tf(_, bufnr) 1820 for _, line in ipairs(getlines(bufnr)) do 1821 -- Assume terraform file on a non-empty line (not whitespace-only) 1822 -- and when the first non-whitespace character is not a ; or / 1823 if not line:find('^%s*$') and not line:find('^%s*[;/]') then 1824 return 'terraform' 1825 end 1826 end 1827 return 'tf' 1828 end 1829 1830 --- @type vim.filetype.mapfn 1831 function M.ttl(_, bufnr) 1832 local line = getline(bufnr, 1):lower() 1833 if line:find('^@?prefix') or line:find('^@?base') then 1834 return 'turtle' 1835 end 1836 return 'teraterm' 1837 end 1838 1839 --- @type vim.filetype.mapfn 1840 function M.txt(_, bufnr) 1841 -- helpfiles match *.txt, but should have a modeline as last line 1842 if not getline(bufnr, -1):find('vim:.*ft=help') then 1843 return 'text' 1844 end 1845 end 1846 1847 --- @type vim.filetype.mapfn 1848 function M.typ(_, bufnr) 1849 if vim.g.filetype_typ then 1850 return vim.g.filetype_typ 1851 end 1852 1853 for _, line in ipairs(getlines(bufnr, 1, 200)) do 1854 if 1855 findany(line, { 1856 '^CASE[%s]?=[%s]?SAME$', 1857 '^CASE[%s]?=[%s]?LOWER$', 1858 '^CASE[%s]?=[%s]?UPPER$', 1859 '^CASE[%s]?=[%s]?OPPOSITE$', 1860 '^TYPE%s', 1861 }) 1862 then 1863 return 'sql' 1864 end 1865 end 1866 1867 return 'typst' 1868 end 1869 1870 --- @type vim.filetype.mapfn 1871 function M.uci(_, bufnr) 1872 -- Return "uci" iff the file has a config or package statement near the 1873 -- top of the file and all preceding lines were comments or blank. 1874 for _, line in ipairs(getlines(bufnr, 1, 3)) do 1875 -- Match a config or package statement at the start of the line. 1876 if 1877 line:find('^%s*[cp]%s+%S') 1878 or line:find('^%s*config%s+%S') 1879 or line:find('^%s*package%s+%S') 1880 then 1881 return 'uci' 1882 end 1883 -- Match a line that is either all blank or blank followed by a comment 1884 if not (line:find('^%s*$') or line:find('^%s*#')) then 1885 break 1886 end 1887 end 1888 end 1889 1890 -- Determine if a .v file is Verilog, V, or Coq 1891 --- @type vim.filetype.mapfn 1892 function M.v(_, bufnr) 1893 if fn.did_filetype() ~= 0 then 1894 -- Filetype was already detected 1895 return 1896 end 1897 if vim.g.filetype_v then 1898 return vim.g.filetype_v 1899 end 1900 local in_comment = 0 1901 for _, line in ipairs(getlines(bufnr, 1, 500)) do 1902 if line:find('^%s*/%*') then 1903 in_comment = 1 1904 end 1905 if in_comment == 1 then 1906 if line:find('%*/') then 1907 in_comment = 0 1908 end 1909 elseif not line:find('^%s*//') then 1910 if 1911 line:find('%.%s*$') and not line:find('/[/*]') 1912 or line:find('%(%*') and not line:find('/[/*].*%(%*') 1913 then 1914 return 'coq' 1915 elseif findany(line, { ';%s*$', ';%s*/[/*]', '^%s*module%s+%w+%s*%(' }) then 1916 return 'verilog' 1917 end 1918 end 1919 end 1920 return 'v' 1921 end 1922 1923 --- @type vim.filetype.mapfn 1924 function M.vba(_, bufnr) 1925 if getline(bufnr, 1):find('^["#] Vimball Archiver') then 1926 return 'vim' 1927 end 1928 return 'vb' 1929 end 1930 1931 -- WEB (*.web is also used for Winbatch: Guess, based on expecting "%" comment 1932 -- lines in a WEB file). 1933 --- @type vim.filetype.mapfn 1934 function M.web(_, bufnr) 1935 for _, line in ipairs(getlines(bufnr, 1, 5)) do 1936 if line:find('^%%') then 1937 return 'web' 1938 end 1939 end 1940 return 'winbatch' 1941 end 1942 1943 -- XFree86 config 1944 --- @type vim.filetype.mapfn 1945 function M.xfree86_v3(_, _) 1946 return 'xf86conf', 1947 function(bufnr) 1948 local line = getline(bufnr, 1) 1949 if matchregex(line, [[\<XConfigurator\>]]) then 1950 vim.b[bufnr].xf86conf_xfree86_version = 3 1951 end 1952 end 1953 end 1954 1955 -- XFree86 config 1956 --- @type vim.filetype.mapfn 1957 function M.xfree86_v4(_, _) 1958 return 'xf86conf', function(b) 1959 vim.b[b].xf86conf_xfree86_version = 4 1960 end 1961 end 1962 1963 --- @type vim.filetype.mapfn 1964 function M.xml(_, bufnr) 1965 for _, line in ipairs(getlines(bufnr, 1, 100)) do 1966 local is_docbook4 = line:find('<!DOCTYPE.*DocBook') 1967 line = line:lower() 1968 local is_docbook5 = line:find([[ xmlns="http://docbook.org/ns/docbook"]]) 1969 if is_docbook4 or is_docbook5 then 1970 return 'docbk', 1971 function(b) 1972 vim.b[b].docbk_type = 'xml' 1973 vim.b[b].docbk_ver = is_docbook4 and 4 or 5 1974 end 1975 end 1976 if line:find([[xmlns:xbl="http://www.mozilla.org/xbl"]]) then 1977 return 'xbl' 1978 end 1979 end 1980 return 'xml' 1981 end 1982 1983 --- @type vim.filetype.mapfn 1984 function M.y(_, bufnr) 1985 for _, line in ipairs(getlines(bufnr, 1, 100)) do 1986 if line:find('^%s*%%') then 1987 return 'yacc' 1988 end 1989 if matchregex(line, [[\c^\s*\(#\|class\>\)]]) and not line:lower():find('^%s*#%s*include') then 1990 return 'racc' 1991 end 1992 end 1993 return 'yacc' 1994 end 1995 1996 -- luacheck: pop 1997 -- luacheck: pop 1998 1999 local patterns_hashbang = { 2000 ['^zsh\\>'] = { 'zsh', { vim_regex = true } }, 2001 ['^\\(tclsh\\|wish\\|expectk\\|itclsh\\|itkwish\\)\\>'] = { 'tcl', { vim_regex = true } }, 2002 ['^expect\\>'] = { 'expect', { vim_regex = true } }, 2003 ['^gnuplot\\>'] = { 'gnuplot', { vim_regex = true } }, 2004 ['make\\>'] = { 'make', { vim_regex = true } }, 2005 ['^pike\\%(\\>\\|[0-9]\\)'] = { 'pike', { vim_regex = true } }, 2006 lua = 'lua', 2007 perl = 'perl', 2008 php = 'php', 2009 python = 'python', 2010 ['^groovy\\>'] = { 'groovy', { vim_regex = true } }, 2011 raku = 'raku', 2012 ruby = 'ruby', 2013 ['node\\(js\\)\\=\\>\\|js\\>'] = { 'javascript', { vim_regex = true } }, 2014 ['rhino\\>'] = { 'javascript', { vim_regex = true } }, 2015 just = 'just', 2016 -- BC calculator 2017 ['^bc\\>'] = { 'bc', { vim_regex = true } }, 2018 ['sed\\>'] = { 'sed', { vim_regex = true } }, 2019 ocaml = 'ocaml', 2020 -- Awk scripts; also finds "gawk" 2021 ['awk\\>'] = { 'awk', { vim_regex = true } }, 2022 wml = 'wml', 2023 scheme = 'scheme', 2024 cfengine = 'cfengine', 2025 escript = 'erlang', 2026 haskell = 'haskell', 2027 clojure = 'clojure', 2028 ['scala\\>'] = { 'scala', { vim_regex = true } }, 2029 -- Free Pascal 2030 ['instantfpc\\>'] = { 'pascal', { vim_regex = true } }, 2031 ['fennel\\>'] = { 'fennel', { vim_regex = true } }, 2032 -- MikroTik RouterOS script 2033 ['rsc\\>'] = { 'routeros', { vim_regex = true } }, 2034 ['fish\\>'] = { 'fish', { vim_regex = true } }, 2035 ['gforth\\>'] = { 'forth', { vim_regex = true } }, 2036 ['icon\\>'] = { 'icon', { vim_regex = true } }, 2037 guile = 'scheme', 2038 ['nix%-shell'] = 'nix', 2039 ['^crystal\\>'] = { 'crystal', { vim_regex = true } }, 2040 ['^\\%(rexx\\|regina\\)\\>'] = { 'rexx', { vim_regex = true } }, 2041 ['^janet\\>'] = { 'janet', { vim_regex = true } }, 2042 ['^dart\\>'] = { 'dart', { vim_regex = true } }, 2043 ['^execlineb\\>'] = { 'execline', { vim_regex = true } }, 2044 ['^bpftrace\\>'] = { 'bpftrace', { vim_regex = true } }, 2045 ['^vim\\>'] = { 'vim', { vim_regex = true } }, 2046 } 2047 2048 --- File starts with "#!". 2049 --- @param contents string[] 2050 --- @param path string 2051 --- @param dispatch_extension fun(name: string): string?, fun(b: integer)? 2052 --- @return string? 2053 --- @return fun(b: integer)? 2054 local function match_from_hashbang(contents, path, dispatch_extension) 2055 local first_line = assert(contents[1]) 2056 -- Check for a line like "#!/usr/bin/env {options} bash". Turn it into 2057 -- "#!/usr/bin/bash" to make matching easier. 2058 -- Recognize only a few {options} that are commonly used. 2059 if matchregex(first_line, [[^#!\s*\S*\<env\s]]) then 2060 first_line = fn.substitute(first_line, [[\s\zs--split-string\(\s\|=\)]], '', '') 2061 first_line = fn.substitute(first_line, [[\s\zs[A-Za-z0-9_]\+=\S*\ze\s]], '', 'g') 2062 first_line = 2063 fn.substitute(first_line, [[\s\zs\%(-[iS]\+\|--ignore-environment\)\ze\s]], '', 'g') 2064 first_line = fn.substitute(first_line, [[\<env\s\+]], '', '') 2065 end 2066 2067 -- Get the program name. 2068 -- Only accept spaces in PC style paths: "#!c:/program files/perl [args]". 2069 -- If the word env is used, use the first word after the space: 2070 -- "#!/usr/bin/env perl [path/args]" 2071 -- If there is no path use the first word: "#!perl [path/args]". 2072 -- Otherwise get the last word after a slash: "#!/usr/bin/perl [path/args]". 2073 local name --- @type string 2074 if first_line:find('^#!%s*%a:[/\\]') then 2075 name = fn.substitute(first_line, [[^#!.*[/\\]\(\i\+\).*]], '\\1', '') 2076 elseif matchregex(first_line, [[^#!.*\<env\>]]) then 2077 name = fn.substitute(first_line, [[^#!.*\<env\>\s\+\(\i\+\).*]], '\\1', '') 2078 elseif matchregex(first_line, [[^#!\s*[^/\\ ]*\>\([^/\\]\|$\)]]) then 2079 name = fn.substitute(first_line, [[^#!\s*\([^/\\ ]*\>\).*]], '\\1', '') 2080 else 2081 name = fn.substitute(first_line, [[^#!\s*\S*[/\\]\(\f\+\).*]], '\\1', '') 2082 end 2083 2084 -- tcl scripts may have #!/bin/sh in the first line and "exec wish" in the 2085 -- third line. Suggested by Steven Atkinson. 2086 if contents[3] and contents[3]:find('^exec wish') then 2087 name = 'wish' 2088 end 2089 2090 if matchregex(name, [[^\(bash\d*\|dash\|ksh\d*\|sh\)\>]]) then 2091 -- Bourne-like shell scripts: bash bash2 dash ksh ksh93 sh 2092 return sh(path, contents, first_line) 2093 elseif matchregex(name, [[^csh\>]]) then 2094 return M.shell(path, contents, vim.g.filetype_csh or 'csh') 2095 elseif matchregex(name, [[^tcsh\>]]) then 2096 return M.shell(path, contents, 'tcsh') 2097 end 2098 2099 for k, v in pairs(patterns_hashbang) do 2100 local ft = type(v) == 'table' and v[1] or v --[[@as string]] 2101 local opts = type(v) == 'table' and v[2] or {} 2102 if opts.vim_regex and matchregex(name, k) or name:find(k) then 2103 return ft 2104 end 2105 end 2106 2107 -- If nothing matched, check the extension table. For a hashbang like 2108 -- '#!/bin/env foo', this will set the filetype to 'fooscript' assuming 2109 -- the filetype for the 'foo' extension is 'fooscript' in the extension table. 2110 return dispatch_extension(name) 2111 end 2112 2113 --- @class vim.filetype.detect.PatternOpts 2114 --- @field vim_regex? true? use Vim regexes instead of Lua patterns. 2115 --- @field start_lnum? integer? Start line number for matching, defaults to 1. 2116 --- @field end_lnum? integer? End line number for matching, defaults to -1 (last line). 2117 --- @field ignore_case? true ignore case when matching. 2118 2119 -- TODO(lewis6991): split this table into two tables, one for patterns and one for functions. 2120 local patterns_text = { 2121 ['^#compdef\\>'] = { 'zsh', { vim_regex = true } }, 2122 ['^#autoload\\>'] = { 'zsh', { vim_regex = true } }, 2123 -- ELM Mail files 2124 ['^From [a-zA-Z][a-zA-Z_0-9%.=%-]*(@[^ ]*)? .* 19%d%d$'] = 'mail', 2125 ['^From [a-zA-Z][a-zA-Z_0-9%.=%-]*(@[^ ]*)? .* 20%d%d$'] = 'mail', 2126 ['^From %- .* 19%d%d$'] = 'mail', 2127 ['^From %- .* 20%d%d$'] = 'mail', 2128 ['^[Rr][Ee][Tt][Uu][Rr][Nn]%-[Pp][Aa][Tt][Hh]:%s<.*@.*>$'] = 'mail', 2129 -- Mason 2130 ['^<[%%&].*>'] = 'mason', 2131 -- Vim scripts (must have '" vim' as the first line to trigger this) 2132 ['^" *[vV]im$['] = 'vim', 2133 -- libcxx and libstdc++ standard library headers like ["iostream["] do not have 2134 -- an extension, recognize the Emacs file mode. 2135 ['%-%*%-.*[cC]%+%+.*%-%*%-'] = 'cpp', 2136 ['^\\*\\* LambdaMOO Database, Format Version \\%([1-3]\\>\\)\\@!\\d\\+ \\*\\*$'] = { 2137 'moo', 2138 { vim_regex = true }, 2139 }, 2140 -- Diff file: 2141 -- - "diff" in first line (context diff) 2142 -- - "Only in " in first line 2143 -- - "34,35c34,35" normal diff format output 2144 -- - "--- " in first line and "+++ " in second line (unified diff). 2145 -- - "*** " in first line and "--- " in second line (context diff). 2146 -- - "# It was generated by makepatch " in the second line (makepatch diff). 2147 -- - "Index: <filename>" in the first line (CVS file) 2148 -- - "=== ", line of "=", "---", "+++ " (SVK diff) 2149 -- - "=== ", "--- ", "+++ " (bzr diff, common case) 2150 -- - "=== (removed|added|renamed|modified)" (bzr diff, alternative) 2151 -- - "# HG changeset patch" in first line (Mercurial export format) 2152 ['^\\(diff\\>\\|Only in \\|\\d\\+\\(,\\d\\+\\)\\=[cda]\\d\\+\\(,\\d\\+\\)\\=\\>$\\|# It was generated by makepatch \\|Index:\\s\\+\\f\\+\\r\\=$\\|===== \\f\\+ \\d\\+\\.\\d\\+ vs edited\\|==== //\\f\\+#\\d\\+\\|# HG changeset patch\\)'] = { 2153 'diff', 2154 { vim_regex = true }, 2155 }, 2156 function(contents) 2157 return diff(contents) 2158 end, 2159 -- PostScript Files (must have %!PS as the first line, like a2ps output) 2160 ['^%%![ \t]*PS'] = 'postscr', 2161 function(contents) 2162 return m4(contents) 2163 end, 2164 -- SiCAD scripts (must have procn or procd as the first line to trigger this) 2165 ['^ *proc[nd] *$'] = { 'sicad', { ignore_case = true } }, 2166 ['^%*%*%*%* Purify'] = 'purifylog', 2167 -- XML 2168 ['<%?%s*xml.*%?>'] = 'xml', 2169 -- XHTML (e.g.: PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN") 2170 ['\\<DTD\\s\\+XHTML\\s'] = 'xhtml', 2171 -- HTML (e.g.: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN") 2172 -- Avoid "doctype html", used by slim. 2173 ['\\c<!DOCTYPE\\s\\+html\\>'] = { 'html', { vim_regex = true } }, 2174 -- PDF 2175 ['^%%PDF%-'] = 'pdf', 2176 -- XXD output 2177 ['^%x%x%x%x%x%x%x: %x%x ?%x%x ?%x%x ?%x%x '] = 'xxd', 2178 -- RCS/CVS log output 2179 ['^RCS file:'] = { 'rcslog', { start_lnum = 1, end_lnum = 2 } }, 2180 -- CVS commit 2181 ['^CVS:'] = { 'cvs', { start_lnum = 2 } }, 2182 ['^CVS: '] = { 'cvs', { start_lnum = -1 } }, 2183 -- Prescribe 2184 ['^!R!'] = 'prescribe', 2185 -- Send-pr 2186 ['^SEND%-PR:'] = 'sendpr', 2187 -- SNNS files 2188 ['^SNNS network definition file'] = 'snnsnet', 2189 ['^SNNS pattern definition file'] = 'snnspat', 2190 ['^SNNS result file'] = 'snnsres', 2191 ['^%%.-[Vv]irata'] = { 'virata', { start_lnum = 1, end_lnum = 5 } }, 2192 function(lines) 2193 if 2194 -- inaccurate fast match first, then use accurate slow match 2195 (lines[1]:find('execve%(') and lines[1]:find('^[0-9:%. ]*execve%(')) 2196 or lines[1]:find('^__libc_start_main') 2197 then 2198 return 'strace' 2199 end 2200 end, 2201 -- VSE JCL 2202 ['^\\* $$ JOB\\>'] = { 'vsejcl', { vim_regex = true } }, 2203 ['^// *JOB\\>'] = { 'vsejcl', { vim_regex = true } }, 2204 -- TAK and SINDA 2205 ['K & K Associates'] = { 'takout', { start_lnum = 4 } }, 2206 ['TAK 2000'] = { 'takout', { start_lnum = 2 } }, 2207 ['S Y S T E M S I M P R O V E D '] = { 'syndaout', { start_lnum = 3 } }, 2208 ['Run Date: '] = { 'takcmp', { start_lnum = 6 } }, 2209 ['Node File 1'] = { 'sindacmp', { start_lnum = 9 } }, 2210 dns_zone, 2211 -- Valgrind 2212 ['^==%d+== valgrind'] = 'valgrind', 2213 ['^==%d+== Using valgrind'] = { 'valgrind', { start_lnum = 3 } }, 2214 -- Go docs 2215 ['PACKAGE DOCUMENTATION$'] = 'godoc', 2216 -- Renderman Interface Bytestream 2217 ['^##RenderMan'] = 'rib', 2218 -- Scheme scripts 2219 ['exec%s%+%S*scheme'] = { 'scheme', { start_lnum = 1, end_lnum = 2 } }, 2220 -- Git output 2221 ['^\\(commit\\|tree\\|object\\) \\x\\{40,\\}\\>\\|^tag \\S\\+$'] = { 2222 'git', 2223 { vim_regex = true }, 2224 }, 2225 function(lines) 2226 -- Gprof (gnu profiler) 2227 if 2228 lines[1] == 'Flat profile:' 2229 and lines[2] == '' 2230 and lines[3]:find('^Each sample counts as .* seconds%.$') 2231 then 2232 return 'gprof' 2233 end 2234 end, 2235 -- Erlang terms 2236 -- (See also: http://www.gnu.org/software/emacs/manual/html_node/emacs/Choosing-Modes.html#Choosing-Modes) 2237 ['%-%*%-.*erlang.*%-%*%-'] = { 'erlang', { ignore_case = true } }, 2238 -- YAML 2239 ['^%%YAML'] = 'yaml', 2240 -- MikroTik RouterOS script 2241 ['^#.*by RouterOS'] = 'routeros', 2242 -- Sed scripts 2243 -- #ncomment is allowed but most likely a false positive so require a space before any trailing comment text 2244 ['^#n%s'] = 'sed', 2245 ['^#n$'] = 'sed', 2246 ['^#%s+Reconstructed via infocmp from file:'] = 'terminfo', 2247 ['^File: .*%.info, Node: .*, Next: .*, Up: '] = 'info', 2248 ['^File: .*%.info, Node: .*, Prev: .*, Up: '] = 'info', 2249 ['This is the top of the INFO tree.'] = 'info', 2250 } 2251 2252 --- File does not start with "#!". 2253 --- @param contents string[] 2254 --- @param path string 2255 --- @return string? 2256 --- @return fun(b: integer)? 2257 local function match_from_text(contents, path) 2258 if assert(contents[1]):find('^:$') then 2259 -- Bourne-like shell scripts: sh ksh bash bash2 2260 return sh(path, contents) 2261 elseif 2262 matchregex( 2263 '\n' .. table.concat(contents, '\n'), 2264 [[\n\s*emulate\s\+\%(-[LR]\s\+\)\=[ckz]\=sh\>]] 2265 ) 2266 then 2267 -- Z shell scripts 2268 return 'zsh' 2269 end 2270 2271 for k, v in pairs(patterns_text) do 2272 if type(v) == 'string' then 2273 -- Check the first line only 2274 if assert(contents[1]):find(k) then 2275 return v 2276 end 2277 elseif type(v) == 'function' then 2278 -- If filetype detection fails, continue with the next pattern 2279 local ok, ft = pcall(v, contents) 2280 if ok and ft then 2281 return ft 2282 end 2283 else 2284 --- @cast k string 2285 local opts = type(v) == 'table' and v[2] or {} 2286 --- @cast opts vim.filetype.detect.PatternOpts 2287 if opts.start_lnum and opts.end_lnum then 2288 assert( 2289 not opts.ignore_case, 2290 'ignore_case=true is ignored when start_lnum is also present, needs refactor' 2291 ) 2292 for i = opts.start_lnum, opts.end_lnum do 2293 local line = contents[i] 2294 if not line then 2295 break 2296 elseif line:find(k) then 2297 return v[1] 2298 end 2299 end 2300 else 2301 local line_nr = opts.start_lnum == -1 and #contents or opts.start_lnum or 1 2302 local contents_line_nr = contents[line_nr] 2303 if contents_line_nr then 2304 local line = opts.ignore_case and contents_line_nr:lower() or contents_line_nr 2305 if opts.vim_regex and matchregex(line, k) or line:find(k) then 2306 return v[1] 2307 end 2308 end 2309 end 2310 end 2311 end 2312 return cvs_diff(path, contents) 2313 end 2314 2315 --- @param contents string[] 2316 --- @param path string 2317 --- @param dispatch_extension fun(name: string): string?, fun(b: integer)? 2318 --- @return string? 2319 --- @return fun(b: integer)? 2320 function M.match_contents(contents, path, dispatch_extension) 2321 local first_line = assert(contents[1]) 2322 if first_line:find('^#!') then 2323 return match_from_hashbang(contents, path, dispatch_extension) 2324 else 2325 return match_from_text(contents, path) 2326 end 2327 end 2328 2329 return M