local api = vim.api
local fn = vim.fn

local M = {}

--- @alias vim.filetype.mapfn fun(path:string,bufnr:integer, ...):string?, fun(b:integer)?
--- @alias vim.filetype.mapopts { priority: number }
--- @alias vim.filetype.maptbl [string|vim.filetype.mapfn, vim.filetype.mapopts]
--- @alias vim.filetype.mapping.value string|vim.filetype.mapfn|vim.filetype.maptbl
--- @alias vim.filetype.mapping table<string,vim.filetype.mapping.value>

--- @class vim.filetype.mapping.sorted
--- @nodoc
--- @field [1] string parent pattern
--- @field [2] string pattern
--- @field [3] string|vim.filetype.mapfn
--- @field [4] integer priority

--- @param ft string|vim.filetype.mapfn
--- @param priority? integer
--- @return vim.filetype.maptbl
local function starsetf(ft, priority)
  return {
    function(path, bufnr)
      -- Note: when `ft` is a function its return value may be nil.
      local f = type(ft) ~= 'function' and ft or ft(path, bufnr)
      if not vim.g.ft_ignore_pat then
        return f
      end

      local re = vim.regex(vim.g.ft_ignore_pat)
      if not re:match_str(path) then
        return f
      end
    end,
    {
      -- Starset matches should have lowest priority by default
      priority = priority or -math.huge,
    },
  }
end

---@private
--- Get a line range from the buffer.
---@param bufnr integer The buffer to get the lines from
---@param start_lnum integer|nil The line number of the first line (inclusive, 1-based)
---@param end_lnum integer|nil The line number of the last line (inclusive, 1-based)
---@return string[] # Array of lines
function M._getlines(bufnr, start_lnum, end_lnum)
  if start_lnum then
    return api.nvim_buf_get_lines(bufnr, start_lnum - 1, end_lnum or start_lnum, false)
  end

  -- Return all lines
  return api.nvim_buf_get_lines(bufnr, 0, -1, false)
end

---@private
--- Get a single line from the buffer.
---@param bufnr integer The buffer to get the lines from
---@param start_lnum integer The line number of the first line (inclusive, 1-based)
---@return string
function M._getline(bufnr, start_lnum)
  -- Return a single line
  return api.nvim_buf_get_lines(bufnr, start_lnum - 1, start_lnum, false)[1] or ''
end

---@private
--- Check whether a string matches any of the given Lua patterns.
---
---@param s string? The string to check
---@param patterns string[] A list of Lua patterns
---@return boolean `true` if s matched a pattern, else `false`
function M._findany(s, patterns)
  if not s then
    return false
  end
  for _, v in ipairs(patterns) do
    if s:find(v) then
      return true
    end
  end
  return false
end

---@private
--- Get the next non-whitespace line in the buffer.
---
---@param bufnr integer The buffer to get the line from
---@param start_lnum integer The line number of the first line to start from (inclusive, 1-based)
---@return string|nil The first non-blank line if found or `nil` otherwise
function M._nextnonblank(bufnr, start_lnum)
  for _, line in ipairs(M._getlines(bufnr, start_lnum, -1)) do
    if not line:find('^%s*$') then
      return line
    end
  end
  return nil
end

do
  --- @type table<string,vim.regex>
  local regex_cache = {}

  ---@private
  --- Check whether the given string matches the Vim regex pattern.
  --- @param s string?
  --- @param pattern string
  --- @return boolean
  function M._matchregex(s, pattern)
    if not s then
      return false
    end
    if not regex_cache[pattern] then
      regex_cache[pattern] = vim.regex(pattern)
    end
    return regex_cache[pattern]:match_str(s) ~= nil
  end
end

--- @module 'vim.filetype.detect'
local detect = setmetatable({}, {
  --- @param k string
  --- @param t table<string,function>
  --- @return function
  __index = function(t, k)
    t[k] = function(...)
      return require('vim.filetype.detect')[k](...)
    end
    return t[k]
  end,
})

--- @param ... string|vim.filetype.mapfn
--- @return vim.filetype.mapfn
local function detect_seq(...)
  local candidates = { ... }
  return function(...)
    for _, c in ipairs(candidates) do
      if type(c) == 'string' then
        return c
      end
      if type(c) == 'function' then
        local r = c(...)
        if r then
          return r
        end
      end
    end
  end
end

local function detect_noext(path, bufnr)
  local root = fn.fnamemodify(path, ':r')
  if root == path then
    return
  end
  return M.match({ buf = bufnr, filename = root })
end

--- @param pat string
--- @param a string?
--- @param b string?
--- @return vim.filetype.mapfn
local function detect_line1(pat, a, b)
  return function(_path, bufnr)
    if M._getline(bufnr, 1):find(pat) then
      return a
    end
    return b
  end
end

--- @type vim.filetype.mapfn
local detect_rc = function(path, _bufnr)
  if not path:find('/etc/Muttrc%.d/') then
    return 'rc'
  end
end

-- luacheck: push no unused args
-- luacheck: push ignore 122

-- Filetype detection logic is encoded in three tables:
-- 1. `extension` for literal extension lookup
-- 2. `filename` for literal full path or basename lookup;
-- 3. `pattern` for matching filenames or paths against Lua patterns,
--     optimized for fast lookup.
-- See `:h dev-vimpatch-filetype` for guidance when porting Vim filetype patches.

---@diagnostic disable: unused-local
---@type vim.filetype.mapping
local extension = {
  -- BEGIN EXTENSION
  ['8th'] = '8th',
  a65 = 'a65',
  aap = 'aap',
  abap = 'abap',
  abc = 'abc',
  abl = 'abel',
  abnf = 'abnf',
  wrm = 'acedb',
  ads = 'ada',
  ada = 'ada',
  gpr = 'ada',
  adb = 'ada',
  tdf = 'ahdl',
  aidl = 'aidl',
  aml = 'aml',
  run = 'ampl',
  g4 = 'antlr4',
  scpt = 'applescript',
  ino = 'arduino',
  pde = 'arduino',
  art = 'art',
  asciidoc = 'asciidoc',
  adoc = 'asciidoc',
  asa = function(path, bufnr)
    if vim.g.filetype_asa then
      return vim.g.filetype_asa
    end
    return 'aspvbs'
  end,
  asm = detect.asm,
  s = detect.asm,
  S = detect.asm,
  a = detect.asm,
  A = detect.asm,
  lst = detect.asm,
  mac = detect.asm,
  asn1 = 'asn',
  asn = 'asn',
  asp = detect.asp,
  astro = 'astro',
  asy = 'asy',
  atl = 'atlas',
  as = 'atlas',
  zed = 'authzed',
  ahk = 'autohotkey',
  au3 = 'autoit',
  ave = 'ave',
  gawk = 'awk',
  awk = 'awk',
  ref = 'b',
  imp = 'b',
  mch = 'b',
  bas = detect.bas,
  bass = 'bass',
  bi = detect.bas,
  bm = detect.bas,
  bc = 'bc',
  bdf = 'bdf',
  beancount = 'beancount',
  bib = 'bib',
  com = detect_seq(detect.bindzone, 'dcl'),
  db = detect.bindzone,
  bicep = 'bicep',
  bicepparam = 'bicep',
  zone = 'bindzone',
  bb = 'bitbake',
  bbappend = 'bitbake',
  bbclass = 'bitbake',
  bl = 'blank',
  blp = 'blueprint',
  bp = 'bp',
  bsd = 'bsdl',
  bsdl = 'bsdl',
  bst = 'bst',
  btm = function(path, bufnr)
    return (vim.g.dosbatch_syntax_for_btm and vim.g.dosbatch_syntax_for_btm ~= 0) and 'dosbatch'
      or 'btm'
  end,
  bzl = 'bzl',
  bazel = 'bzl',
  BUILD = 'bzl',
  mdh = 'c',
  epro = 'c',
  qc = 'c',
  c3 = 'c3',
  c3i = 'c3',
  c3t = 'c3',
  cabal = 'cabal',
  cairo = 'cairo',
  capnp = 'capnp',
  cdc = 'cdc',
  cdl = 'cdl',
  toc = detect_line1('\\contentsline', 'tex', 'cdrtoc'),
  cedar = 'cedar',
  cfc = 'cf',
  cfm = 'cf',
  cfi = 'cf',
  hgrc = 'cfg',
  cfg = detect.cfg,
  Cfg = detect.cfg,
  CFG = detect.cfg,
  chf = 'ch',
  chai = 'chaiscript',
  ch = detect.change,
  chs = 'chaskell',
  chatito = 'chatito',
  chopro = 'chordpro',
  crd = 'chordpro',
  crdpro = 'chordpro',
  cho = 'chordpro',
  chordpro = 'chordpro',
  ck = 'chuck',
  cl = detect.cl,
  eni = 'cl',
  icl = 'clean',
  cljx = 'clojure',
  clj = 'clojure',
  cljc = 'clojure',
  cljs = 'clojure',
  cook = 'cook',
  cmake = 'cmake',
  cmod = 'cmod',
  cob = 'cobol',
  cbl = 'cobol',
  atg = 'coco',
  recipe = 'conaryrecipe',
  ctags = 'conf',
  hook = function(path, bufnr)
    return M._getline(bufnr, 1) == '[Trigger]' and 'confini' or nil
  end,
  nmconnection = 'confini',
  mklx = 'context',
  mkiv = 'context',
  mkii = 'context',
  mkxl = 'context',
  mkvi = 'context',
  control = detect.control,
  copyright = detect.copyright,
  corn = 'corn',
  csh = detect.csh,
  cpon = 'cpon',
  moc = 'cpp',
  hh = 'cpp',
  tlh = 'cpp',
  inl = 'cpp',
  ipp = 'cpp',
  ['c++'] = 'cpp',
  C = 'cpp',
  cxx = 'cpp',
  H = 'cpp',
  tcc = 'cpp',
  hxx = 'cpp',
  hpp = 'cpp',
  ixx = 'cpp',
  mpp = 'cpp',
  ccm = 'cpp',
  cppm = 'cpp',
  cxxm = 'cpp',
  ['c++m'] = 'cpp',
  cpp = detect.cpp,
  cc = detect.cpp,
  cql = 'cqlang',
  crm = 'crm',
  cr = 'crystal',
  cake = 'cs',
  csx = 'cs',
  cs = 'cs',
  csc = 'csc',
  csdl = 'csdl',
  cshtml = 'html',
  fdr = 'csp',
  csp = 'csp',
  css = 'css',
  csv = 'csv',
  con = 'cterm',
  feature = 'cucumber',
  cuh = 'cuda',
  cu = 'cuda',
  cue = 'cue',
  pld = 'cupl',
  si = 'cuplsim',
  cyn = 'cynpp',
  cypher = 'cypher',
  dfy = 'dafny',
  dart = 'dart',
  drt = 'dart',
  ds = 'datascript',
  dat = detect.dat,
  Dat = detect.dat,
  DAT = detect.dat,
  dax = 'dax',
  dcd = 'dcd',
  decl = detect.decl,
  dec = detect.decl,
  dcl = detect_seq(detect.decl, 'clean'),
  def = detect.def,
  desc = 'desc',
  directory = 'desktop',
  desktop = 'desktop',
  dhall = 'dhall',
  diff = 'diff',
  rej = 'diff',
  Dockerfile = 'dockerfile',
  dockerfile = 'dockerfile',
  bat = 'dosbatch',
  wrap = 'dosini',
  ini = 'dosini',
  INI = 'dosini',
  vbp = 'dosini',
  dot = 'dot',
  gv = 'dot',
  drac = 'dracula',
  drc = 'dracula',
  lvs = 'dracula',
  lpe = 'dracula',
  dsp = detect.dsp,
  dtd = 'dtd',
  d = detect.dtrace,
  dts = 'dts',
  dtsi = 'dts',
  dtso = 'dts',
  its = 'dts',
  keymap = 'dts',
  overlay = 'dts',
  dylan = 'dylan',
  intr = 'dylanintr',
  lid = 'dylanlid',
  e = detect.e,
  E = detect.e,
  ecd = 'ecd',
  edf = 'edif',
  edif = 'edif',
  edo = 'edif',
  edn = detect.edn,
  eex = 'eelixir',
  leex = 'eelixir',
  am = 'elf',
  exs = 'elixir',
  elm = 'elm',
  lc = 'elsa',
  elv = 'elvish',
  ent = detect.ent,
  epp = 'epuppet',
  erl = 'erlang',
  hrl = 'erlang',
  yaws = 'erlang',
  erb = 'eruby',
  rhtml = 'eruby',
  esdl = 'esdl',
  ec = 'esqlc',
  EC = 'esqlc',
  strl = 'esterel',
  eu = detect.euphoria,
  EU = detect.euphoria,
  ew = detect.euphoria,
  EW = detect.euphoria,
  EX = detect.euphoria,
  exu = detect.euphoria,
  EXU = detect.euphoria,
  exw = detect.euphoria,
  EXW = detect.euphoria,
  ex = detect.ex,
  exp = 'expect',
  f = detect.f,
  factor = 'factor',
  fal = 'falcon',
  fan = 'fan',
  fwt = 'fan',
  lib = 'faust',
  fnl = 'fennel',
  m4gl = 'fgl',
  ['4gl'] = 'fgl',
  ['4gh'] = 'fgl',
  fir = 'firrtl',
  fish = 'fish',
  focexec = 'focexec',
  fex = 'focexec',
  ft = 'forth',
  fth = 'forth',
  ['4th'] = 'forth',
  FOR = 'fortran',
  f77 = 'fortran',
  f03 = 'fortran',
  fortran = 'fortran',
  F95 = 'fortran',
  f90 = 'fortran',
  F03 = 'fortran',
  fpp = 'fortran',
  FTN = 'fortran',
  ftn = 'fortran',
  ['for'] = 'fortran',
  F90 = 'fortran',
  F77 = 'fortran',
  f95 = 'fortran',
  FPP = 'fortran',
  F = 'fortran',
  F08 = 'fortran',
  f08 = 'fortran',
  fpc = 'fpcmake',
  fsl = 'framescript',
  frm = detect.frm,
  fb = 'freebasic',
  fs = detect.fs,
  fsh = 'fsh',
  fsi = 'fsharp',
  fsx = 'fsharp',
  fc = 'func',
  fusion = 'fusion',
  gdb = 'gdb',
  gdmo = 'gdmo',
  mo = 'gdmo',
  tscn = 'gdresource',
  tres = 'gdresource',
  gd = 'gdscript',
  gdshader = 'gdshader',
  shader = 'gdshader',
  ged = 'gedcom',
  gel = 'gel',
  gmi = 'gemtext',
  gemini = 'gemtext',
  gift = 'gift',
  prettierignore = 'gitignore',
  gleam = 'gleam',
  vert = 'glsl',
  tesc = 'glsl',
  tese = 'glsl',
  glsl = 'glsl',
  geom = 'glsl',
  frag = 'glsl',
  comp = 'glsl',
  rgen = 'glsl',
  rmiss = 'glsl',
  rchit = 'glsl',
  rahit = 'glsl',
  rint = 'glsl',
  rcall = 'glsl',
  gn = 'gn',
  gni = 'gn',
  gnuplot = 'gnuplot',
  gpi = 'gnuplot',
  go = 'go',
  gp = 'gp',
  gs = 'grads',
  gql = 'graphql',
  graphql = 'graphql',
  graphqls = 'graphql',
  gretl = 'gretl',
  gradle = 'groovy',
  groovy = 'groovy',
  gsp = 'gsp',
  gjs = 'javascript.glimmer',
  gts = 'typescript.glimmer',
  gyp = 'gyp',
  gypi = 'gyp',
  hack = 'hack',
  hackpartial = 'hack',
  haml = 'haml',
  hsm = 'hamster',
  hbs = 'handlebars',
  ha = 'hare',
  ['hs-boot'] = 'haskell',
  hsig = 'haskell',
  hsc = 'haskell',
  hs = 'haskell',
  persistentmodels = 'haskellpersistent',
  ht = 'haste',
  htpp = 'hastepreproc',
  hcl = 'hcl',
  hb = 'hb',
  h = detect.header,
  sum = 'hercules',
  errsum = 'hercules',
  ev = 'hercules',
  vc = 'hercules',
  heex = 'heex',
  hex = 'hex',
  ['a43'] = 'hex',
  ['a90'] = 'hex',
  ['h32'] = 'hex',
  ['h80'] = 'hex',
  ['h86'] = 'hex',
  ihex = 'hex',
  ihe = 'hex',
  ihx = 'hex',
  int = 'hex',
  mcs = 'hex',
  hjson = 'hjson',
  m3u = 'hlsplaylist',
  m3u8 = 'hlsplaylist',
  hog = 'hog',
  hws = 'hollywood',
  hoon = 'hoon',
  cpt = detect.html,
  dtml = detect.html,
  htm = detect.html,
  html = detect.html,
  pt = detect.html,
  shtml = detect.html,
  stm = detect.html,
  htt = 'httest',
  htb = 'httest',
  http = 'http',
  hurl = 'hurl',
  hw = detect.hw,
  module = detect.hw,
  pkg = detect.hw,
  hy = 'hy',
  iba = 'ibasic',
  ibi = 'ibasic',
  icn = 'icon',
  idl = detect.idl,
  idr = 'idris2',
  inc = detect.inc,
  inf = 'inform',
  INF = 'inform',
  ii = 'initng',
  inko = 'inko',
  inp = detect.inp,
  ms = detect_seq(detect.nroff, 'xmath'),
  ipkg = 'ipkg',
  iss = 'iss',
  mst = 'ist',
  ist = 'ist',
  ijs = 'j',
  JAL = 'jal',
  jal = 'jal',
  jpr = 'jam',
  jpl = 'jam',
  janet = 'janet',
  jav = 'java',
  java = 'java',
  jsh = 'java',
  jj = 'javacc',
  jjt = 'javacc',
  es = 'javascript',
  mjs = 'javascript',
  javascript = 'javascript',
  js = 'javascript',
  jsm = 'javascript',
  cjs = 'javascript',
  jsx = 'javascriptreact',
  clp = 'jess',
  jgr = 'jgraph',
  jinja = 'jinja',
  jjdescription = 'jjdescription',
  j73 = 'jovial',
  jov = 'jovial',
  jovial = 'jovial',
  properties = 'jproperties',
  jq = 'jq',
  slnf = 'json',
  json = 'json',
  jsonp = 'json',
  geojson = 'json',
  mcmeta = 'json',
  webmanifest = 'json',
  ipynb = 'json',
  ['jupyterlab-settings'] = 'json',
  ['sublime-project'] = 'json',
  ['sublime-settings'] = 'json',
  ['sublime-workspace'] = 'json',
  ['json-patch'] = 'json',
  bd = 'json',
  bda = 'json',
  xci = 'json',
  json5 = 'json5',
  jsonc = 'jsonc',
  jsonl = 'jsonl',
  jsonnet = 'jsonnet',
  libsonnet = 'jsonnet',
  jsp = 'jsp',
  jl = 'julia',
  just = 'just',
  Just = 'just',
  JUST = 'just',
  kl = 'karel',
  KL = 'karel',
  kdl = 'kdl',
  kv = 'kivy',
  kix = 'kix',
  kts = 'kotlin',
  kt = 'kotlin',
  ktm = 'kotlin',
  sub = 'krl',
  Sub = 'krl',
  SUB = 'krl',
  ks = 'kscript',
  k = 'kwt',
  ACE = 'lace',
  ace = 'lace',
  lalrpop = 'lalrpop',
  latte = 'latte',
  lte = 'latte',
  ld = 'ld',
  ldif = 'ldif',
  lean = 'lean',
  journal = 'ledger',
  ldg = 'ledger',
  ledger = 'ledger',
  leo = 'leo',
  less = 'less',
  lex = 'lex',
  lxx = 'lex',
  ['l++'] = 'lex',
  l = 'lex',
  lhs = 'lhaskell',
  lidr = 'lidris2',
  ly = 'lilypond',
  ily = 'lilypond',
  liquid = 'liquid',
  liq = 'liquidsoap',
  L = 'lisp',
  lisp = 'lisp',
  el = 'lisp',
  lsp = 'lisp',
  asd = 'lisp',
  lt = 'lite',
  lite = 'lite',
  livemd = 'livebook',
  ll = detect.ll,
  log = detect.log,
  Log = detect.log,
  LOG = detect.log,
  lgt = 'logtalk',
  lotos = 'lotos',
  lot = detect_line1('\\contentsline', 'tex', 'lotos'),
  lout = 'lout',
  lou = 'lout',
  ulpc = 'lpc',
  lpc = 'lpc',
  c = detect.lpc,
  lsl = detect.lsl,
  lss = 'lss',
  nse = 'lua',
  rockspec = 'lua',
  lua = 'lua',
  tlu = 'lua',
  luau = 'luau',
  lrc = 'lyrics',
  m = detect.m,
  at = 'm4',
  mc = detect.mc,
  quake = 'm3quake',
  m4 = function(path, bufnr)
    local pathl = path:lower()
    return not (pathl:find('html%.m4$') or pathl:find('fvwm2rc')) and 'm4' or nil
  end,
  eml = 'mail',
  mk = detect.make,
  mak = detect.make,
  page = 'mallard',
  map = detect_line1('^%*+$', 'lnkmap', 'map'),
  mws = 'maple',
  mpl = 'maple',
  mv = 'maple',
  mkdn = detect.markdown,
  md = detect.markdown,
  mdwn = detect.markdown,
  mkd = detect.markdown,
  markdown = detect.markdown,
  mdown = detect.markdown,
  masm = 'masm',
  mhtml = 'mason',
  mason = 'mason',
  master = 'master',
  mas = 'master',
  demo = 'maxima',
  dm1 = 'maxima',
  dm2 = 'maxima',
  dm3 = 'maxima',
  dmt = 'maxima',
  wxm = 'maxima',
  mbsyncrc = 'mbsync',
  mw = 'mediawiki',
  wiki = 'mediawiki',
  mel = 'mel',
  mmd = 'mermaid',
  mmdc = 'mermaid',
  mermaid = 'mermaid',
  mf = 'mf',
  mgl = 'mgl',
  mgp = 'mgp',
  my = 'mib',
  mib = 'mib',
  mix = 'mix',
  mixal = 'mix',
  mlir = 'mlir',
  mm = detect.mm,
  nb = 'mma',
  wl = 'mma',
  mmp = 'mmp',
  mms = detect.mms,
  mod = detect.mod,
  Mod = detect.mod,
  MOD = detect.mod,
  DEF = 'modula2',
  m3 = 'modula3',
  i3 = 'modula3',
  mg = 'modula3',
  ig = 'modula3',
  lm3 = 'modula3',
  mojo = 'mojo',
  ['🔥'] = 'mojo', -- 🙄
  ssc = 'monk',
  monk = 'monk',
  tsc = 'monk',
  isc = 'monk',
  moo = 'moo',
  moon = 'moonscript',
  move = 'move',
  mp = 'mp',
  mpiv = detect.mp,
  mpvi = detect.mp,
  mpxl = detect.mp,
  mof = 'msidl',
  odl = 'msidl',
  msql = 'msql',
  mss = 'mss',
  mu = 'mupad',
  mush = 'mush',
  mustache = 'mustache',
  mysql = 'mysql',
  n1ql = 'n1ql',
  nql = 'n1ql',
  nanorc = 'nanorc',
  nasm = 'nasm',
  NSA = 'natural',
  NSC = 'natural',
  NSG = 'natural',
  NSL = 'natural',
  NSM = 'natural',
  NSN = 'natural',
  NSP = 'natural',
  NSS = 'natural',
  ncf = 'ncf',
  nginx = 'nginx',
  nim = 'nim',
  nims = 'nim',
  nimble = 'nim',
  ninja = 'ninja',
  nix = 'nix',
  norg = 'norg',
  nqc = 'nqc',
  ['0'] = detect.nroff,
  ['1'] = detect.nroff,
  ['2'] = detect.nroff,
  ['3'] = detect.nroff,
  ['4'] = detect.nroff,
  ['5'] = detect.nroff,
  ['6'] = detect.nroff,
  ['7'] = detect.nroff,
  ['8'] = detect.nroff,
  ['9'] = detect.nroff,
  ['0p'] = detect.nroff,
  ['1p'] = detect.nroff,
  ['3p'] = detect.nroff,
  ['1x'] = detect.nroff,
  ['2x'] = detect.nroff,
  ['3x'] = detect.nroff,
  ['4x'] = detect.nroff,
  ['5x'] = detect.nroff,
  ['6x'] = detect.nroff,
  ['7x'] = detect.nroff,
  ['8x'] = detect.nroff,
  ['3am'] = detect.nroff,
  ['3perl'] = detect.nroff,
  ['3pm'] = detect.nroff,
  ['3posix'] = detect.nroff,
  ['3type'] = detect.nroff,
  n = detect.nroff,
  roff = 'nroff',
  tmac = 'nroff',
  man = 'nroff',
  mom = 'nroff',
  nr = 'nroff',
  tr = 'nroff',
  nsi = 'nsis',
  nsh = 'nsis',
  nt = 'ntriples',
  nu = 'nu',
  obj = 'obj',
  objdump = 'objdump',
  cppobjdump = 'objdump',
  obl = 'obse',
  obse = 'obse',
  oblivion = 'obse',
  obscript = 'obse',
  mlt = 'ocaml',
  mly = 'ocaml',
  mll = 'ocaml',
  mlp = 'ocaml',
  mlip = 'ocaml',
  mli = 'ocaml',
  ml = 'ocaml',
  occ = 'occam',
  odin = 'odin',
  xom = 'omnimark',
  xin = 'omnimark',
  opam = 'opam',
  ['or'] = 'openroad',
  scad = 'openscad',
  ovpn = 'openvpn',
  opl = 'opl',
  opL = 'opl',
  oPl = 'opl',
  oPL = 'opl',
  Opl = 'opl',
  OpL = 'opl',
  OPl = 'opl',
  OPL = 'opl',
  ora = 'ora',
  org = 'org',
  org_archive = 'org',
  pandoc = 'pandoc',
  pdk = 'pandoc',
  pd = 'pandoc',
  pdc = 'pandoc',
  pxsl = 'papp',
  papp = 'papp',
  pxml = 'papp',
  pas = 'pascal',
  lpr = detect_line1('<%?xml', 'xml', 'pascal'),
  dpr = 'pascal',
  txtpb = 'pbtxt',
  textproto = 'pbtxt',
  textpb = 'pbtxt',
  pbtxt = 'pbtxt',
  g = 'pccts',
  pcmk = 'pcmk',
  pdf = 'pdf',
  pem = 'pem',
  cer = 'pem',
  crt = 'pem',
  csr = 'pem',
  plx = 'perl',
  prisma = 'prisma',
  psgi = 'perl',
  al = 'perl',
  ctp = 'php',
  php = 'php',
  phpt = 'php',
  php0 = 'php',
  php1 = 'php',
  php2 = 'php',
  php3 = 'php',
  php4 = 'php',
  php5 = 'php',
  php6 = 'php',
  php7 = 'php',
  php8 = 'php',
  php9 = 'php',
  phtml = 'php',
  theme = 'php',
  pike = 'pike',
  pmod = 'pike',
  rcp = 'pilrc',
  PL = detect.pl,
  pli = 'pli',
  pl1 = 'pli',
  p36 = 'plm',
  plm = 'plm',
  pac = 'plm',
  plp = 'plp',
  pls = 'plsql',
  plsql = 'plsql',
  po = 'po',
  pot = 'po',
  pod = 'pod',
  filter = 'poefilter',
  pk = 'poke',
  pony = 'pony',
  ps = 'postscr',
  epsi = 'postscr',
  afm = 'postscr',
  epsf = 'postscr',
  eps = 'postscr',
  pfa = 'postscr',
  ai = 'postscr',
  pov = 'pov',
  ppd = 'ppd',
  it = 'ppwiz',
  ih = 'ppwiz',
  pq = 'pq',
  action = 'privoxy',
  prg = detect.prg,
  Prg = detect.prg,
  PRG = detect.prg,
  pc = 'proc',
  pdb = 'prolog',
  pml = 'promela',
  proto = 'proto',
  prql = 'prql',
  psd1 = 'ps1',
  psm1 = 'ps1',
  ps1 = 'ps1',
  pssc = 'ps1',
  ps1xml = 'ps1xml',
  psf = 'psf',
  psl = 'psl',
  ptx = 'ptx',
  pug = 'pug',
  purs = 'purescript',
  arr = 'pyret',
  pxd = 'pyrex',
  pxi = 'pyrex',
  pyx = 'pyrex',
  ['pyx+'] = 'pyrex',
  pyw = 'python',
  py = 'python',
  pyi = 'python',
  ptl = 'python',
  ipy = 'python',
  ql = 'ql',
  qll = 'ql',
  qml = 'qml',
  qbs = 'qml',
  qmd = 'quarto',
  R = detect.r,
  rkt = 'racket',
  rktd = 'racket',
  rktl = 'racket',
  rad = 'radiance',
  mat = 'radiance',
  pod6 = 'raku',
  rakudoc = 'raku',
  rakutest = 'raku',
  rakumod = 'raku',
  pm6 = 'raku',
  raku = 'raku',
  t6 = 'raku',
  p6 = 'raku',
  raml = 'raml',
  sysx = 'rapid',
  sysX = 'rapid',
  Sysx = 'rapid',
  SysX = 'rapid',
  SYSX = 'rapid',
  SYSx = 'rapid',
  modx = 'rapid',
  modX = 'rapid',
  Modx = 'rapid',
  ModX = 'rapid',
  MODX = 'rapid',
  MODx = 'rapid',
  rasi = 'rasi',
  rbs = 'rbs',
  rego = 'rego',
  rem = 'remind',
  remind = 'remind',
  pip = 'requirements',
  res = 'rescript',
  resi = 'rescript',
  frt = 'reva',
  testUnit = 'rexx',
  rex = 'rexx',
  orx = 'rexx',
  rexx = 'rexx',
  jrexx = 'rexx',
  rxj = 'rexx',
  rexxj = 'rexx',
  testGroup = 'rexx',
  rxo = 'rexx',
  Rd = 'rhelp',
  rd = 'rhelp',
  rib = 'rib',
  Rmd = 'rmd',
  rmd = 'rmd',
  smd = 'rmd',
  Smd = 'rmd',
  rnc = 'rnc',
  rng = 'rng',
  rnw = 'rnoweb',
  snw = 'rnoweb',
  Rnw = 'rnoweb',
  Snw = 'rnoweb',
  robot = 'robot',
  resource = 'robot',
  roc = 'roc',
  ron = 'ron',
  rsc = 'routeros',
  x = 'rpcgen',
  rpgle = 'rpgle',
  rpgleinc = 'rpgle',
  rpl = 'rpl',
  Srst = 'rrst',
  srst = 'rrst',
  Rrst = 'rrst',
  rrst = 'rrst',
  rst = 'rst',
  rtf = 'rtf',
  rjs = 'ruby',
  rxml = 'ruby',
  rb = 'ruby',
  rant = 'ruby',
  ru = 'ruby',
  rbw = 'ruby',
  gemspec = 'ruby',
  builder = 'ruby',
  rake = 'ruby',
  rs = 'rust',
  sa = detect.sa,
  sage = 'sage',
  sls = 'salt',
  sas = 'sas',
  sass = 'sass',
  sbt = 'sbt',
  scala = 'scala',
  mill = 'scala',
  ss = 'scheme',
  scm = 'scheme',
  sld = 'scheme',
  stsg = 'scheme',
  sce = 'scilab',
  sci = 'scilab',
  scss = 'scss',
  sd = 'sd',
  sdc = 'sdc',
  pr = 'sdl',
  sdl = 'sdl',
  sed = 'sed',
  sexp = 'sexplib',
  bash = detect.bash,
  bats = detect.bash,
  cygport = detect.bash,
  ebuild = detect.bash,
  eclass = detect.bash,
  env = detect.sh,
  envrc = detect.sh,
  ksh = detect.ksh,
  sh = detect.sh,
  lo = 'sh',
  la = 'sh',
  lai = 'sh',
  mdd = 'sh',
  slang = 'shaderslang',
  sieve = 'sieve',
  siv = 'sieve',
  sig = detect.sig,
  sil = detect.sil,
  sim = 'simula',
  s85 = 'sinda',
  sin = 'sinda',
  ssm = 'sisu',
  sst = 'sisu',
  ssi = 'sisu',
  ['_sst'] = 'sisu',
  ['-sst'] = 'sisu',
  il = 'skill',
  ils = 'skill',
  cdf = 'skill',
  sl = 'slang',
  ice = 'slice',
  slint = 'slint',
  score = 'slrnsc',
  sol = 'solidity',
  smali = 'smali',
  tpl = 'smarty',
  ihlp = 'smcl',
  smcl = 'smcl',
  hlp = 'smcl',
  smith = 'smith',
  smt = 'smith',
  smithy = 'smithy',
  sml = 'sml',
  smk = 'snakemake',
  spt = 'snobol4',
  sno = 'snobol4',
  sln = 'solution',
  sparql = 'sparql',
  rq = 'sparql',
  spec = 'spec',
  spice = 'spice',
  sp = 'spice',
  spd = 'spup',
  spdata = 'spup',
  speedup = 'spup',
  spi = 'spyce',
  spy = 'spyce',
  tyc = 'sql',
  typ = detect.typ,
  pkb = 'sql',
  tyb = 'sql',
  pks = 'sql',
  sqlj = 'sqlj',
  sqi = 'sqr',
  sqr = 'sqr',
  nut = 'squirrel',
  src = detect.src,
  Src = detect.src,
  SRC = detect.src,
  s28 = 'srec',
  s37 = 'srec',
  srec = 'srec',
  mot = 'srec',
  s19 = 'srec',
  srt = 'srt',
  ssa = 'ssa',
  ass = 'ssa',
  st = 'st',
  ipd = 'starlark',
  star = 'starlark',
  starlark = 'starlark',
  imata = 'stata',
  ['do'] = 'stata',
  mata = 'stata',
  ado = 'stata',
  stp = 'stp',
  styl = 'stylus',
  stylus = 'stylus',
  quark = 'supercollider',
  sface = 'surface',
  svelte = 'svelte',
  svg = 'svg',
  sw = 'sway',
  swift = 'swift',
  swiftinterface = 'swift',
  swig = 'swig',
  swg = 'swig',
  sys = detect.sys,
  Sys = detect.sys,
  SYS = detect.sys,
  svh = 'systemverilog',
  sv = 'systemverilog',
  cmm = 'trace32',
  cmmt = 'trace32',
  t32 = 'trace32',
  td = 'tablegen',
  tak = 'tak',
  tal = 'tal',
  task = 'taskedit',
  tm = 'tcl',
  tcl = 'tcl',
  itk = 'tcl',
  itcl = 'tcl',
  tk = 'tcl',
  jacl = 'tcl',
  tl = 'teal',
  templ = 'templ',
  tmpl = 'template',
  tera = 'tera',
  ti = 'terminfo',
  dtx = 'tex',
  ltx = 'tex',
  bbl = 'tex',
  latex = 'tex',
  sty = 'tex',
  pgf = 'tex',
  nlo = 'tex',
  nls = 'tex',
  thm = 'tex',
  eps_tex = 'tex',
  pdf_tex = 'tex',
  pygtex = 'tex',
  pygstyle = 'tex',
  clo = 'tex',
  aux = 'tex',
  brf = 'tex',
  ind = 'tex',
  lof = 'tex',
  loe = 'tex',
  nav = 'tex',
  vrb = 'tex',
  ins = 'tex',
  tikz = 'tex',
  bbx = 'tex',
  cbx = 'tex',
  beamer = 'tex',
  cls = detect.cls,
  texi = 'texinfo',
  txi = 'texinfo',
  texinfo = 'texinfo',
  text = 'text',
  tfvars = 'terraform-vars',
  thrift = 'thrift',
  tla = 'tla',
  tli = 'tli',
  toml = 'toml',
  tpp = 'tpp',
  treetop = 'treetop',
  trig = 'trig',
  slt = 'tsalt',
  tsscl = 'tsscl',
  tssgm = 'tssgm',
  tssop = 'tssop',
  tsv = 'tsv',
  tutor = 'tutor',
  twig = 'twig',
  ts = detect_line1('<%?xml', 'xml', 'typescript'),
  mts = 'typescript',
  cts = 'typescript',
  tsx = 'typescriptreact',
  tsp = 'typespec',
  uc = 'uc',
  uit = 'uil',
  uil = 'uil',
  ungram = 'ungrammar',
  u = 'unison',
  uu = 'unison',
  url = 'urlshortcut',
  usd = 'usd',
  usda = 'usd',
  v = detect.v,
  vsh = 'v',
  vv = 'v',
  ctl = 'vb',
  dob = 'vb',
  dsm = 'vb',
  dsr = 'vb',
  pag = 'vb',
  sba = 'vb',
  vb = 'vb',
  vbs = 'vb',
  vba = detect.vba,
  vdf = 'vdf',
  vdmpp = 'vdmpp',
  vpp = 'vdmpp',
  vdmrt = 'vdmrt',
  vdmsl = 'vdmsl',
  vdm = 'vdmsl',
  vto = 'vento',
  vr = 'vera',
  vri = 'vera',
  vrh = 'vera',
  va = 'verilogams',
  vams = 'verilogams',
  vhdl = 'vhdl',
  vst = 'vhdl',
  vhd = 'vhdl',
  hdl = 'vhdl',
  vho = 'vhdl',
  vbe = 'vhdl',
  tape = 'vhs',
  vim = 'vim',
  mar = 'vmasm',
  cm = 'voscm',
  wrl = 'vrml',
  vroom = 'vroom',
  vue = 'vue',
  wast = 'wat',
  wat = 'wat',
  wdl = 'wdl',
  wm = 'webmacro',
  wgsl = 'wgsl',
  wbt = 'winbatch',
  wit = 'wit',
  wml = 'wml',
  wsf = 'wsh',
  wsc = 'wsh',
  wsml = 'wsml',
  ad = 'xdefaults',
  xhtml = 'xhtml',
  xht = 'xhtml',
  msc = 'xmath',
  msf = 'xmath',
  psc1 = 'xml',
  tpm = 'xml',
  xliff = 'xml',
  atom = 'xml',
  xul = 'xml',
  cdxml = 'xml',
  mpd = 'xml',
  rss = 'xml',
  fsproj = 'xml',
  ui = 'xml',
  vbproj = 'xml',
  xlf = 'xml',
  wsdl = 'xml',
  csproj = 'xml',
  wpl = 'xml',
  xmi = 'xml',
  xpr = 'xml',
  xpfm = 'xml',
  spfm = 'xml',
  bxml = 'xml',
  mmi = 'xml',
  xcu = 'xml',
  xlb = 'xml',
  xlc = 'xml',
  xba = 'xml',
  slnx = 'xml',
  xpm = detect_line1('XPM2', 'xpm2', 'xpm'),
  xpm2 = 'xpm2',
  xqy = 'xquery',
  xqm = 'xquery',
  xquery = 'xquery',
  xq = 'xquery',
  xql = 'xquery',
  xs = 'xs',
  xsd = 'xsd',
  xsl = 'xslt',
  xslt = 'xslt',
  yy = 'yacc',
  ['y++'] = 'yacc',
  yxx = 'yacc',
  yml = 'yaml',
  yaml = 'yaml',
  eyaml = 'yaml',
  mplstyle = 'yaml',
  yang = 'yang',
  yuck = 'yuck',
  z8a = 'z8a',
  zig = 'zig',
  zon = 'zig',
  ziggy = 'ziggy',
  ['ziggy-schema'] = 'ziggy_schema',
  zu = 'zimbu',
  zut = 'zimbutempl',
  zs = 'zserio',
  zsh = 'zsh',
  zunit = 'zsh',
  ['zsh-theme'] = 'zsh',
  vala = 'vala',
  web = detect.web,
  pl = detect.pl,
  pp = detect.pp,
  i = detect.i,
  w = detect.progress_cweb,
  p = detect.progress_pascal,
  pro = detect_seq(detect.proto, 'idlang'),
  patch = detect.patch,
  r = detect.r,
  rdf = detect.redif,
  rules = detect.rules,
  sc = detect.sc,
  scd = detect.scd,
  tcsh = function(path, bufnr)
    return require('vim.filetype.detect').shell(path, M._getlines(bufnr), 'tcsh')
  end,
  sql = detect.sql,
  zsql = detect.sql,
  tex = detect.tex,
  tf = detect.tf,
  txt = detect.txt,
  xml = detect.xml,
  y = detect.y,
  cmd = detect.cmd,
  rul = detect.rul,
  cpy = detect_line1('^##', 'python', 'cobol'),
  dsl = detect_line1('^%s*<!', 'dsl', 'structurizr'),
  smil = detect_line1('<%?%s*xml.*%?>', 'xml', 'smil'),
  smi = detect.smi,
  install = detect.install,
  pm = detect.pm,
  me = detect.me,
  reg = detect.reg,
  ttl = detect.ttl,
  rc = detect_rc,
  rch = detect_rc,
  class = detect.class,
  sgml = detect.sgml,
  sgm = detect.sgml,
  t = detect_seq(detect.nroff, detect.perl, 'tads'),
  -- Ignored extensions
  bak = detect_noext,
  ['dpkg-bak'] = detect_noext,
  ['dpkg-dist'] = detect_noext,
  ['dpkg-old'] = detect_noext,
  ['dpkg-new'] = detect_noext,
  ['in'] = function(path, bufnr)
    if vim.fs.basename(path) ~= 'configure.in' then
      return detect_noext(path, bufnr)
    end
  end,
  new = detect_noext,
  old = detect_noext,
  orig = detect_noext,
  pacsave = detect_noext,
  pacnew = detect_noext,
  rpmsave = detect_noext,
  rmpnew = detect_noext,
  -- END EXTENSION
}

---@type vim.filetype.mapping
local filename = {
  -- BEGIN FILENAME
  ['a2psrc'] = 'a2ps',
  ['/etc/a2ps.cfg'] = 'a2ps',
  ['.a2psrc'] = 'a2ps',
  ['.asoundrc'] = 'alsaconf',
  ['/usr/share/alsa/alsa.conf'] = 'alsaconf',
  ['/etc/asound.conf'] = 'alsaconf',
  ['build.xml'] = 'ant',
  ['.htaccess'] = 'apache',
  APKBUILD = 'apkbuild',
  ['apt.conf'] = 'aptconf',
  ['/.aptitude/config'] = 'aptconf',
  ['=tagging-method'] = 'arch',
  ['.arch-inventory'] = 'arch',
  ['makefile.am'] = 'automake',
  ['Makefile.am'] = 'automake',
  ['GNUmakefile.am'] = 'automake',
  ['.bash_aliases'] = detect.bash,
  ['.bash-aliases'] = detect.bash,
  ['.bash_history'] = detect.bash,
  ['.bash-history'] = detect.bash,
  ['.bash_logout'] = detect.bash,
  ['.bash-logout'] = detect.bash,
  ['.bash_profile'] = detect.bash,
  ['.bash-profile'] = detect.bash,
  ['named.root'] = 'bindzone',
  WORKSPACE = 'bzl',
  ['WORKSPACE.bzlmod'] = 'bzl',
  BUCK = 'bzl',
  BUILD = 'bzl',
  ['cabal.project'] = 'cabalproject',
  ['cabal.config'] = 'cabalconfig',
  calendar = 'calendar',
  catalog = 'catalog',
  ['/etc/cdrdao.conf'] = 'cdrdaoconf',
  ['.cdrdao'] = 'cdrdaoconf',
  ['/etc/default/cdrdao'] = 'cdrdaoconf',
  ['/etc/defaults/cdrdao'] = 'cdrdaoconf',
  ['cfengine.conf'] = 'cfengine',
  cgdbrc = 'cgdbrc',
  ['init.trans'] = 'clojure',
  ['.trans'] = 'clojure',
  ['CMakeLists.txt'] = 'cmake',
  ['CMakeCache.txt'] = 'cmakecache',
  ['.cling_history'] = 'cpp',
  ['.alias'] = detect.csh,
  ['.cshrc'] = detect.csh,
  ['.login'] = detect.csh,
  ['csh.cshrc'] = detect.csh,
  ['csh.login'] = detect.csh,
  ['csh.logout'] = detect.csh,
  ['auto.master'] = 'conf',
  ['texdoc.cnf'] = 'conf',
  ['.x11vncrc'] = 'conf',
  ['.chktexrc'] = 'conf',
  ['.ripgreprc'] = 'conf',
  ripgreprc = 'conf',
  ['configure.in'] = 'config',
  ['configure.ac'] = 'config',
  crontab = 'crontab',
  ['.cvsrc'] = 'cvsrc',
  ['/debian/changelog'] = 'debchangelog',
  ['changelog.dch'] = 'debchangelog',
  ['changelog.Debian'] = 'debchangelog',
  ['NEWS.dch'] = 'debchangelog',
  ['NEWS.Debian'] = 'debchangelog',
  ['/debian/control'] = 'debcontrol',
  ['/DEBIAN/control'] = 'debcontrol',
  ['/debian/copyright'] = 'debcopyright',
  ['/etc/apt/sources.list'] = 'debsources',
  ['denyhosts.conf'] = 'denyhosts',
  ['dict.conf'] = 'dictconf',
  ['.dictrc'] = 'dictconf',
  ['/etc/DIR_COLORS'] = 'dircolors',
  ['.dir_colors'] = 'dircolors',
  ['.dircolors'] = 'dircolors',
  ['/etc/dnsmasq.conf'] = 'dnsmasq',
  Containerfile = 'dockerfile',
  dockerfile = 'dockerfile',
  Dockerfile = 'dockerfile',
  npmrc = 'dosini',
  ['/etc/yum.conf'] = 'dosini',
  ['.npmrc'] = 'dosini',
  ['pip.conf'] = 'dosini',
  ['setup.cfg'] = 'dosini',
  ['pudb.cfg'] = 'dosini',
  ['.coveragerc'] = 'dosini',
  ['.pypirc'] = 'dosini',
  ['.pylintrc'] = 'dosini',
  ['pylintrc'] = 'dosini',
  ['.replyrc'] = 'dosini',
  ['.gitlint'] = 'dosini',
  ['.oelint.cfg'] = 'dosini',
  ['psprint.conf'] = 'dosini',
  sofficerc = 'dosini',
  ['mimeapps.list'] = 'dosini',
  ['.wakatime.cfg'] = 'dosini',
  ['nfs.conf'] = 'dosini',
  ['nfsmount.conf'] = 'dosini',
  ['.notmuch-config'] = 'dosini',
  ['.alsoftrc'] = 'dosini',
  ['alsoft.conf'] = 'dosini',
  ['alsoft.ini'] = 'dosini',
  ['alsoftrc.sample'] = 'dosini',
  ['pacman.conf'] = 'confini',
  ['paru.conf'] = 'confini',
  ['mpv.conf'] = 'confini',
  dune = 'dune',
  jbuild = 'dune',
  ['dune-workspace'] = 'dune',
  ['dune-project'] = 'dune',
  ['dune-file'] = 'dune',
  Earthfile = 'earthfile',
  ['.editorconfig'] = 'editorconfig',
  ['elinks.conf'] = 'elinks',
  ['mix.lock'] = 'elixir',
  ['filter-rules'] = 'elmfilt',
  ['exim.conf'] = 'exim',
  exports = 'exports',
  fennelrc = 'fennel',
  ['.fennelrc'] = 'fennel',
  ['.fetchmailrc'] = 'fetchmail',
  fvSchemes = detect.foam,
  fvSolution = detect.foam,
  fvConstraints = detect.foam,
  fvModels = detect.foam,
  fstab = 'fstab',
  mtab = 'fstab',
  ['.gdbinit'] = 'gdb',
  gdbinit = 'gdb',
  ['.gdbearlyinit'] = 'gdb',
  gdbearlyinit = 'gdb',
  ['lltxxxxx.txt'] = 'gedcom',
  TAG_EDITMSG = 'gitcommit',
  MERGE_MSG = 'gitcommit',
  COMMIT_EDITMSG = 'gitcommit',
  NOTES_EDITMSG = 'gitcommit',
  EDIT_DESCRIPTION = 'gitcommit',
  ['.gitconfig'] = 'gitconfig',
  ['.gitmodules'] = 'gitconfig',
  ['.gitattributes'] = 'gitattributes',
  ['.gitignore'] = 'gitignore',
  ['.ignore'] = 'gitignore',
  ['.containerignore'] = 'gitignore',
  ['.dockerignore'] = 'gitignore',
  ['.fdignore'] = 'gitignore',
  ['.npmignore'] = 'gitignore',
  ['.rgignore'] = 'gitignore',
  ['.vscodeignore'] = 'gitignore',
  ['gitolite.conf'] = 'gitolite',
  ['git-rebase-todo'] = 'gitrebase',
  gkrellmrc = 'gkrellmrc',
  ['.gnashrc'] = 'gnash',
  ['.gnashpluginrc'] = 'gnash',
  gnashpluginrc = 'gnash',
  gnashrc = 'gnash',
  ['.gnuplot_history'] = 'gnuplot',
  ['goaccess.conf'] = 'goaccess',
  ['go.sum'] = 'gosum',
  ['go.work.sum'] = 'gosum',
  ['go.work'] = 'gowork',
  ['.gprc'] = 'gp',
  ['/.gnupg/gpg.conf'] = 'gpg',
  ['/.gnupg/options'] = 'gpg',
  Jenkinsfile = 'groovy',
  ['/var/backups/gshadow.bak'] = 'group',
  ['/etc/gshadow'] = 'group',
  ['/etc/group-'] = 'group',
  ['/etc/gshadow.edit'] = 'group',
  ['/etc/gshadow-'] = 'group',
  ['/etc/group'] = 'group',
  ['/var/backups/group.bak'] = 'group',
  ['/etc/group.edit'] = 'group',
  ['/boot/grub/menu.lst'] = 'grub',
  ['/etc/grub.conf'] = 'grub',
  ['/boot/grub/grub.conf'] = 'grub',
  ['.gtkrc'] = 'gtkrc',
  gtkrc = 'gtkrc',
  ['snort.conf'] = 'hog',
  ['vision.conf'] = 'hog',
  ['/etc/host.conf'] = 'hostconf',
  ['/etc/hosts.allow'] = 'hostsaccess',
  ['/etc/hosts.deny'] = 'hostsaccess',
  ['.hy-history'] = 'hy',
  ['hyprland.conf'] = 'hyprlang',
  ['hyprpaper.conf'] = 'hyprlang',
  ['hypridle.conf'] = 'hyprlang',
  ['hyprlock.conf'] = 'hyprlang',
  ['/.icewm/menu'] = 'icemenu',
  ['.indent.pro'] = 'indent',
  indentrc = 'indent',
  inittab = 'inittab',
  ['ipf.conf'] = 'ipfilter',
  ['ipf6.conf'] = 'ipfilter',
  ['ipf.rules'] = 'ipfilter',
  ['.bun_repl_history'] = 'javascript',
  ['.node_repl_history'] = 'javascript',
  ['deno_history.txt'] = 'javascript',
  ['Pipfile.lock'] = 'json',
  ['.firebaserc'] = 'json',
  ['.prettierrc'] = 'json',
  ['.stylelintrc'] = 'json',
  ['.lintstagedrc'] = 'json',
  ['deno.lock'] = 'json',
  ['flake.lock'] = 'json',
  ['.swcrc'] = 'json',
  ['.babelrc'] = 'jsonc',
  ['.eslintrc'] = 'jsonc',
  ['.hintrc'] = 'jsonc',
  ['.jscsrc'] = 'jsonc',
  ['.jsfmtrc'] = 'jsonc',
  ['.jshintrc'] = 'jsonc',
  ['.luaurc'] = 'jsonc',
  ['.swrc'] = 'jsonc',
  ['.vsconfig'] = 'jsonc',
  ['bun.lock'] = 'jsonc',
  ['.justfile'] = 'just',
  ['.Justfile'] = 'just',
  ['.JUSTFILE'] = 'just',
  ['justfile'] = 'just',
  ['Justfile'] = 'just',
  ['JUSTFILE'] = 'just',
  Kconfig = 'kconfig',
  ['Kconfig.debug'] = 'kconfig',
  ['Config.in'] = 'kconfig',
  ['ldaprc'] = 'ldapconf',
  ['.ldaprc'] = 'ldapconf',
  ['ldap.conf'] = 'ldapconf',
  ['lfrc'] = 'lf',
  ['lftp.conf'] = 'lftp',
  ['.lftprc'] = 'lftp',
  ['/.libao'] = 'libao',
  ['/etc/libao.conf'] = 'libao',
  ['lilo.conf'] = 'lilo',
  ['/etc/limits'] = 'limits',
  ['.emacs'] = 'lisp',
  sbclrc = 'lisp',
  ['.sbclrc'] = 'lisp',
  ['.sawfishrc'] = 'lisp',
  ['/etc/login.access'] = 'loginaccess',
  ['/etc/login.defs'] = 'logindefs',
  ['.lsl'] = detect.lsl,
  ['.busted'] = 'lua',
  ['.luacheckrc'] = 'lua',
  ['.lua_history'] = 'lua',
  ['config.ld'] = 'lua',
  ['rock_manifest'] = 'lua',
  ['lynx.cfg'] = 'lynx',
  ['m3overrides'] = 'm3build',
  ['m3makefile'] = 'm3build',
  ['cm3.cfg'] = 'm3quake',
  ['.m4_history'] = 'm4',
  ['.followup'] = 'mail',
  ['.article'] = 'mail',
  ['.letter'] = 'mail',
  ['/etc/aliases'] = 'mailaliases',
  ['/etc/mail/aliases'] = 'mailaliases',
  mailcap = 'mailcap',
  ['.mailcap'] = 'mailcap',
  Kbuild = 'make',
  ['/etc/man.conf'] = 'manconf',
  ['man.config'] = 'manconf',
  ['maxima-init.mac'] = 'maxima',
  isyncrc = 'mbsync',
  ['meson.build'] = 'meson',
  ['meson.options'] = 'meson',
  ['meson_options.txt'] = 'meson',
  ['/etc/conf.modules'] = 'modconf',
  ['/etc/modules'] = 'modconf',
  ['/etc/modules.conf'] = 'modconf',
  ['/.mplayer/config'] = 'mplayerconf',
  ['mplayer.conf'] = 'mplayerconf',
  mrxvtrc = 'mrxvtrc',
  ['.mrxvtrc'] = 'mrxvtrc',
  ['.msmtprc'] = 'msmtp',
  ['Muttngrc'] = 'muttrc',
  ['Muttrc'] = 'muttrc',
  ['.mysql_history'] = 'mysql',
  ['/etc/nanorc'] = 'nanorc',
  Neomuttrc = 'neomuttrc',
  ['.netrc'] = 'netrc',
  NEWS = detect.news,
  ['.ocamlinit'] = 'ocaml',
  ['.octaverc'] = 'octave',
  octaverc = 'octave',
  ['octave.conf'] = 'octave',
  ['.ondirrc'] = 'ondir',
  opam = 'opam',
  ['opam.locked'] = 'opam',
  ['/etc/pam.conf'] = 'pamconf',
  ['pam_env.conf'] = 'pamenv',
  ['.pam_environment'] = 'pamenv',
  ['/var/backups/passwd.bak'] = 'passwd',
  ['/var/backups/shadow.bak'] = 'passwd',
  ['/etc/passwd'] = 'passwd',
  ['/etc/passwd-'] = 'passwd',
  ['/etc/shadow.edit'] = 'passwd',
  ['/etc/shadow-'] = 'passwd',
  ['/etc/shadow'] = 'passwd',
  ['/etc/passwd.edit'] = 'passwd',
  ['.gitolite.rc'] = 'perl',
  ['gitolite.rc'] = 'perl',
  ['example.gitolite.rc'] = 'perl',
  ['latexmkrc'] = 'perl',
  ['.latexmkrc'] = 'perl',
  ['pf.conf'] = 'pf',
  ['main.cf'] = 'pfmain',
  ['main.cf.proto'] = 'pfmain',
  pinerc = 'pine',
  ['.pinercex'] = 'pine',
  ['.pinerc'] = 'pine',
  pinercex = 'pine',
  ['/etc/pinforc'] = 'pinfo',
  ['/.pinforc'] = 'pinfo',
  ['.povrayrc'] = 'povini',
  printcap = function(path, bufnr)
    return 'ptcap', function(b)
      vim.b[b].ptcap_type = 'print'
    end
  end,
  termcap = function(path, bufnr)
    return 'ptcap', function(b)
      vim.b[b].ptcap_type = 'term'
    end
  end,
  ['.procmailrc'] = 'procmail',
  ['.procmail'] = 'procmail',
  ['indent.pro'] = detect_seq(detect.proto, 'indent'),
  ['/etc/protocols'] = 'protocols',
  INDEX = detect.psf,
  INFO = detect.psf,
  ['MANIFEST.in'] = 'pymanifest',
  ['.pythonstartup'] = 'python',
  ['.pythonrc'] = 'python',
  ['.python_history'] = 'python',
  ['.jline-jython.history'] = 'python',
  SConstruct = 'python',
  qmldir = 'qmldir',
  ['.Rhistory'] = 'r',
  ['.Rprofile'] = 'r',
  Rprofile = 'r',
  ['Rprofile.site'] = 'r',
  ratpoisonrc = 'ratpoison',
  ['.ratpoisonrc'] = 'ratpoison',
  inputrc = 'readline',
  ['.inputrc'] = 'readline',
  ['.reminders'] = 'remind',
  ['requirements.txt'] = 'requirements',
  ['constraints.txt'] = 'requirements',
  ['requirements.in'] = 'requirements',
  ['resolv.conf'] = 'resolv',
  ['robots.txt'] = 'robots',
  Gemfile = 'ruby',
  Puppetfile = 'ruby',
  ['.irbrc'] = 'ruby',
  irbrc = 'ruby',
  ['.irb_history'] = 'ruby',
  irb_history = 'ruby',
  ['rakefile'] = 'ruby',
  ['Rakefile'] = 'ruby',
  ['rantfile'] = 'ruby',
  ['Rantfile'] = 'ruby',
  Vagrantfile = 'ruby',
  ['smb.conf'] = 'samba',
  ['.lips_repl_history'] = 'scheme',
  ['.guile'] = 'scheme',
  screenrc = 'screen',
  ['.screenrc'] = 'screen',
  ['/etc/sensors3.conf'] = 'sensors',
  ['/etc/sensors.conf'] = 'sensors',
  ['/etc/services'] = 'services',
  ['/etc/serial.conf'] = 'setserial',
  ['/etc/udev/cdsymlinks.conf'] = 'sh',
  ['.ash_history'] = 'sh',
  ['.devscripts'] = 'sh',
  ['devscripts.conf'] = 'sh',
  ['makepkg.conf'] = 'sh',
  ['.makepkg.conf'] = 'sh',
  ['user-dirs.dirs'] = 'sh',
  ['user-dirs.defaults'] = 'sh',
  ['.xprofile'] = 'sh',
  ['bash.bashrc'] = detect.bash,
  bashrc = detect.bash,
  ['.bashrc'] = detect.bash,
  ['.kshrc'] = detect.ksh,
  ['.profile'] = detect.sh,
  ['/etc/profile'] = detect.sh,
  PKGBUILD = detect.bash,
  ['.tcshrc'] = detect.tcsh,
  ['tcsh.login'] = detect.tcsh,
  ['tcsh.tcshrc'] = detect.tcsh,
  ['/etc/slp.conf'] = 'slpconf',
  ['/etc/slp.reg'] = 'slpreg',
  ['/etc/slp.spi'] = 'slpspi',
  ['.slrnrc'] = 'slrnrc',
  ['sendmail.cf'] = 'sm',
  Snakefile = 'snakemake',
  ['.sqlite_history'] = 'sql',
  ['squid.conf'] = 'squid',
  ['ssh_config'] = 'sshconfig',
  ['sshd_config'] = 'sshdconfig',
  ['/etc/sudoers'] = 'sudoers',
  ['sudoers.tmp'] = 'sudoers',
  ['/etc/sysctl.conf'] = 'sysctl',
  tags = 'tags',
  ['pending.data'] = 'taskdata',
  ['completed.data'] = 'taskdata',
  ['undo.data'] = 'taskdata',
  ['.tclshrc'] = 'tcl',
  ['.wishrc'] = 'tcl',
  ['.tclsh-history'] = 'tcl',
  ['tclsh.rc'] = 'tcl',
  ['.xsctcmdhistory'] = 'tcl',
  ['.xsdbcmdhistory'] = 'tcl',
  ['texmf.cnf'] = 'texmf',
  COPYING = 'text',
  README = detect_seq(detect.haredoc, 'text'),
  LICENSE = 'text',
  AUTHORS = 'text',
  tfrc = 'tf',
  ['.tfrc'] = 'tf',
  ['tidy.conf'] = 'tidy',
  tidyrc = 'tidy',
  ['.tidyrc'] = 'tidy',
  ['.tmux.conf'] = 'tmux',
  ['Cargo.lock'] = 'toml',
  ['/.cargo/config'] = 'toml',
  ['/.cargo/credentials'] = 'toml',
  Pipfile = 'toml',
  ['Gopkg.lock'] = 'toml',
  ['uv.lock'] = 'toml',
  ['.black'] = 'toml',
  black = detect_line1('tool%.black', 'toml', nil),
  ['trustees.conf'] = 'trustees',
  ['.ts_node_repl_history'] = 'typescript',
  ['/etc/udev/udev.conf'] = 'udevconf',
  ['/etc/updatedb.conf'] = 'updatedb',
  ['fdrupstream.log'] = 'upstreamlog',
  vgrindefs = 'vgrindefs',
  ['.exrc'] = 'vim',
  ['_exrc'] = 'vim',
  ['.netrwhist'] = 'vim',
  ['_viminfo'] = 'viminfo',
  ['.viminfo'] = 'viminfo',
  ['.wgetrc'] = 'wget',
  ['.wget2rc'] = 'wget2',
  wgetrc = 'wget',
  wget2rc = 'wget2',
  ['.wvdialrc'] = 'wvdial',
  ['wvdial.conf'] = 'wvdial',
  ['.XCompose'] = 'xcompose',
  ['Compose'] = 'xcompose',
  ['.Xresources'] = 'xdefaults',
  ['.Xpdefaults'] = 'xdefaults',
  ['xdm-config'] = 'xdefaults',
  ['.Xdefaults'] = 'xdefaults',
  ['xorg.conf'] = detect.xfree86_v4,
  ['xorg.conf-4'] = detect.xfree86_v4,
  ['XF86Config'] = detect.xfree86_v3,
  ['/etc/xinetd.conf'] = 'xinetd',
  fglrxrc = 'xml',
  ['/etc/blkid.tab'] = 'xml',
  ['/etc/blkid.tab.old'] = 'xml',
  ['fonts.conf'] = 'xml',
  ['Directory.Packages.props'] = 'xml',
  ['Directory.Build.props'] = 'xml',
  ['Directory.Build.targets'] = 'xml',
  ['.clangd'] = 'yaml',
  ['.clang-format'] = 'yaml',
  ['.clang-tidy'] = 'yaml',
  ['pixi.lock'] = 'yaml',
  ['yarn.lock'] = 'yaml',
  matplotlibrc = 'yaml',
  ['.condarc'] = 'yaml',
  condarc = 'yaml',
  zathurarc = 'zathurarc',
  ['/etc/zprofile'] = 'zsh',
  ['.zlogin'] = 'zsh',
  ['.zlogout'] = 'zsh',
  ['.zshrc'] = 'zsh',
  ['.zprofile'] = 'zsh',
  ['.zcompdump'] = 'zsh',
  ['.zsh_history'] = 'zsh',
  ['.zshenv'] = 'zsh',
  ['.zfbfmarks'] = 'zsh',
  -- END FILENAME
}

-- Re-use closures as much as possible
local detect_apache = starsetf('apache')
local detect_muttrc = starsetf('muttrc')
local detect_neomuttrc = starsetf('neomuttrc')
local detect_xkb = starsetf('xkb')

---@type table<string,vim.filetype.mapping>
local pattern = {
  -- BEGIN PATTERN
  ['/debian/'] = {
    ['/debian/changelog$'] = 'debchangelog',
    ['/debian/control$'] = 'debcontrol',
    ['/debian/copyright$'] = 'debcopyright',
    ['/debian/patches/'] = detect.dep3patch,
  },
  ['/etc/'] = {
    ['/etc/a2ps/.*%.cfg$'] = 'a2ps',
    ['/etc/a2ps%.cfg$'] = 'a2ps',
    ['/etc/asound%.conf$'] = 'alsaconf',
    ['/etc/apache2/sites%-.*/.*%.com$'] = 'apache',
    ['/etc/httpd/.*%.conf$'] = 'apache',
    ['/etc/apache2/.*%.conf'] = detect_apache,
    ['/etc/apache2/conf%..*/'] = detect_apache,
    ['/etc/apache2/mods%-.*/'] = detect_apache,
    ['/etc/apache2/sites%-.*/'] = detect_apache,
    ['/etc/httpd/conf%..*/'] = detect_apache,
    ['/etc/httpd/conf%.d/.*%.conf'] = detect_apache,
    ['/etc/httpd/mods%-.*/'] = detect_apache,
    ['/etc/httpd/sites%-.*/'] = detect_apache,
    ['/etc/proftpd/.*%.conf'] = starsetf('apachestyle'),
    ['/etc/proftpd/conf%..*/'] = starsetf('apachestyle'),
    ['/etc/cdrdao%.conf$'] = 'cdrdaoconf',
    ['/etc/default/cdrdao$'] = 'cdrdaoconf',
    ['/etc/defaults/cdrdao$'] = 'cdrdaoconf',
    ['/etc/translate%-shell$'] = 'clojure',
    ['/etc/hostname%.'] = starsetf('config'),
    ['/etc/cron%.d/'] = starsetf('crontab'),
    ['/etc/apt/sources%.list%.d/.*%.sources$'] = 'deb822sources',
    ['/etc/apt/sources%.list%.d/.*%.list$'] = 'debsources',
    ['/etc/apt/sources%.list$'] = 'debsources',
    ['/etc/DIR_COLORS$'] = 'dircolors',
    ['/etc/dnsmasq%.conf$'] = 'dnsmasq',
    ['/etc/dnsmasq%.d/'] = starsetf('dnsmasq'),
    ['/etc/yum%.conf$'] = 'dosini',
    ['/etc/yum%.repos%.d/'] = starsetf('dosini'),
    ['/etc/gitconfig%.d/'] = starsetf('gitconfig'),
    ['/etc/gitconfig$'] = 'gitconfig',
    ['/etc/gitattributes$'] = 'gitattributes',
    ['/etc/group$'] = 'group',
    ['/etc/group%-$'] = 'group',
    ['/etc/group%.edit$'] = 'group',
    ['/etc/gshadow%-$'] = 'group',
    ['/etc/gshadow%.edit$'] = 'group',
    ['/etc/gshadow$'] = 'group',
    ['/etc/grub%.conf$'] = 'grub',
    ['/etc/host%.conf$'] = 'hostconf',
    ['/etc/hosts%.allow$'] = 'hostsaccess',
    ['/etc/hosts%.deny$'] = 'hostsaccess',
    ['/etc/initng/.*/.*%.i$'] = 'initng',
    ['/etc/libao%.conf$'] = 'libao',
    ['/etc/.*limits%.conf$'] = 'limits',
    ['/etc/.*limits%.d/.*%.conf$'] = 'limits',
    ['/etc/limits$'] = 'limits',
    ['/etc/logcheck/.*%.d.*/'] = starsetf('logcheck'),
    ['/etc/login%.access$'] = 'loginaccess',
    ['/etc/login%.defs$'] = 'logindefs',
    ['/etc/aliases$'] = 'mailaliases',
    ['/etc/mail/aliases$'] = 'mailaliases',
    ['/etc/man%.conf$'] = 'manconf',
    ['/etc/conf%.modules$'] = 'modconf',
    ['/etc/modprobe%.'] = starsetf('modconf'),
    ['/etc/modules%.conf$'] = 'modconf',
    ['/etc/modules$'] = 'modconf',
    ['/etc/modutils/'] = starsetf(function(path, bufnr)
      if fn.executable(fn.expand(path)) ~= 1 then
        return 'modconf'
      end
    end),
    ['/etc/Muttrc%.d/'] = starsetf('muttrc'),
    ['/etc/nanorc$'] = 'nanorc',
    ['/etc/nginx/'] = 'nginx',
    ['/etc/pam%.conf$'] = 'pamconf',
    ['/etc/pam%.d/'] = starsetf('pamconf'),
    ['/etc/passwd%-$'] = 'passwd',
    ['/etc/shadow$'] = 'passwd',
    ['/etc/shadow%.edit$'] = 'passwd',
    ['/etc/passwd$'] = 'passwd',
    ['/etc/passwd%.edit$'] = 'passwd',
    ['/etc/shadow%-$'] = 'passwd',
    ['/etc/pinforc$'] = 'pinfo',
    ['/etc/protocols$'] = 'protocols',
    ['/etc/sensors%.d/[^.]'] = starsetf('sensors'),
    ['/etc/sensors%.conf$'] = 'sensors',
    ['/etc/sensors3%.conf$'] = 'sensors',
    ['/etc/services$'] = 'services',
    ['/etc/serial%.conf$'] = 'setserial',
    ['/etc/udev/cdsymlinks%.conf$'] = 'sh',
    ['/etc/profile$'] = detect.sh,
    ['/etc/slp%.conf$'] = 'slpconf',
    ['/etc/slp%.reg$'] = 'slpreg',
    ['/etc/slp%.spi$'] = 'slpspi',
    ['/etc/sudoers%.d/'] = starsetf('sudoers'),
    ['/etc/ssh/ssh_config%.d/.*%.conf$'] = 'sshconfig',
    ['/etc/ssh/sshd_config%.d/.*%.conf$'] = 'sshdconfig',
    ['/etc/sudoers$'] = 'sudoers',
    ['/etc/sysctl%.conf$'] = 'sysctl',
    ['/etc/sysctl%.d/.*%.conf$'] = 'sysctl',
    ['/etc/systemd/.*%.conf%.d/.*%.conf$'] = 'systemd',
    ['/etc/systemd/system/.*%.d/.*%.conf$'] = 'systemd',
    ['/etc/systemd/system/.*%.d/%.#'] = 'systemd',
    ['/etc/systemd/system/%.#'] = 'systemd',
    ['/etc/config/'] = starsetf(detect.uci),
    ['/etc/udev/udev%.conf$'] = 'udevconf',
    ['/etc/udev/permissions%.d/.*%.permissions$'] = 'udevperm',
    ['/etc/updatedb%.conf$'] = 'updatedb',
    ['/etc/init/.*%.conf$'] = 'upstart',
    ['/etc/init/.*%.override$'] = 'upstart',
    ['/etc/xinetd%.conf$'] = 'xinetd',
    ['/etc/xinetd%.d/'] = starsetf('xinetd'),
    ['/etc/blkid%.tab%.old$'] = 'xml',
    ['/etc/blkid%.tab$'] = 'xml',
    ['/etc/xdg/menus/.*%.menu$'] = 'xml',
    ['/etc/zprofile$'] = 'zsh',
  },
  ['/log/'] = {
    ['/log/auth%.crit$'] = 'messages',
    ['/log/auth%.err$'] = 'messages',
    ['/log/auth%.info$'] = 'messages',
    ['/log/auth%.log$'] = 'messages',
    ['/log/auth%.notice$'] = 'messages',
    ['/log/auth%.warn$'] = 'messages',
    ['/log/auth$'] = 'messages',
    ['/log/cron%.crit$'] = 'messages',
    ['/log/cron%.err$'] = 'messages',
    ['/log/cron%.info$'] = 'messages',
    ['/log/cron%.log$'] = 'messages',
    ['/log/cron%.notice$'] = 'messages',
    ['/log/cron%.warn$'] = 'messages',
    ['/log/cron$'] = 'messages',
    ['/log/daemon%.crit$'] = 'messages',
    ['/log/daemon%.err$'] = 'messages',
    ['/log/daemon%.info$'] = 'messages',
    ['/log/daemon%.log$'] = 'messages',
    ['/log/daemon%.notice$'] = 'messages',
    ['/log/daemon%.warn$'] = 'messages',
    ['/log/daemon$'] = 'messages',
    ['/log/debug%.crit$'] = 'messages',
    ['/log/debug%.err$'] = 'messages',
    ['/log/debug%.info$'] = 'messages',
    ['/log/debug%.log$'] = 'messages',
    ['/log/debug%.notice$'] = 'messages',
    ['/log/debug%.warn$'] = 'messages',
    ['/log/debug$'] = 'messages',
    ['/log/kern%.crit$'] = 'messages',
    ['/log/kern%.err$'] = 'messages',
    ['/log/kern%.info$'] = 'messages',
    ['/log/kern%.log$'] = 'messages',
    ['/log/kern%.notice$'] = 'messages',
    ['/log/kern%.warn$'] = 'messages',
    ['/log/kern$'] = 'messages',
    ['/log/lpr%.crit$'] = 'messages',
    ['/log/lpr%.err$'] = 'messages',
    ['/log/lpr%.info$'] = 'messages',
    ['/log/lpr%.log$'] = 'messages',
    ['/log/lpr%.notice$'] = 'messages',
    ['/log/lpr%.warn$'] = 'messages',
    ['/log/lpr$'] = 'messages',
    ['/log/mail%.crit$'] = 'messages',
    ['/log/mail%.err$'] = 'messages',
    ['/log/mail%.info$'] = 'messages',
    ['/log/mail%.log$'] = 'messages',
    ['/log/mail%.notice$'] = 'messages',
    ['/log/mail%.warn$'] = 'messages',
    ['/log/mail$'] = 'messages',
    ['/log/messages%.crit$'] = 'messages',
    ['/log/messages%.err$'] = 'messages',
    ['/log/messages%.info$'] = 'messages',
    ['/log/messages%.log$'] = 'messages',
    ['/log/messages%.notice$'] = 'messages',
    ['/log/messages%.warn$'] = 'messages',
    ['/log/messages$'] = 'messages',
    ['/log/news/news%.crit$'] = 'messages',
    ['/log/news/news%.err$'] = 'messages',
    ['/log/news/news%.info$'] = 'messages',
    ['/log/news/news%.log$'] = 'messages',
    ['/log/news/news%.notice$'] = 'messages',
    ['/log/news/news%.warn$'] = 'messages',
    ['/log/news/news$'] = 'messages',
    ['/log/syslog%.crit$'] = 'messages',
    ['/log/syslog%.err$'] = 'messages',
    ['/log/syslog%.info$'] = 'messages',
    ['/log/syslog%.log$'] = 'messages',
    ['/log/syslog%.notice$'] = 'messages',
    ['/log/syslog%.warn$'] = 'messages',
    ['/log/syslog$'] = 'messages',
    ['/log/user%.crit$'] = 'messages',
    ['/log/user%.err$'] = 'messages',
    ['/log/user%.info$'] = 'messages',
    ['/log/user%.log$'] = 'messages',
    ['/log/user%.notice$'] = 'messages',
    ['/log/user%.warn$'] = 'messages',
    ['/log/user$'] = 'messages',
  },
  ['/systemd/'] = {
    ['/%.config/systemd/user/%.#'] = 'systemd',
    ['/%.config/systemd/user/.*%.d/%.#'] = 'systemd',
    ['/%.config/systemd/user/.*%.d/.*%.conf$'] = 'systemd',
    ['/systemd/.*%.automount$'] = 'systemd',
    ['/systemd/.*%.dnssd$'] = 'systemd',
    ['/systemd/.*%.link$'] = 'systemd',
    ['/systemd/.*%.mount$'] = 'systemd',
    ['/systemd/.*%.netdev$'] = 'systemd',
    ['/systemd/.*%.network$'] = 'systemd',
    ['/systemd/.*%.nspawn$'] = 'systemd',
    ['/systemd/.*%.path$'] = 'systemd',
    ['/systemd/.*%.service$'] = 'systemd',
    ['/systemd/.*%.slice$'] = 'systemd',
    ['/systemd/.*%.socket$'] = 'systemd',
    ['/systemd/.*%.swap$'] = 'systemd',
    ['/systemd/.*%.target$'] = 'systemd',
    ['/systemd/.*%.timer$'] = 'systemd',
  },
  ['/usr/'] = {
    ['/usr/share/alsa/alsa%.conf$'] = 'alsaconf',
    ['/usr/.*/gnupg/options%.skel$'] = 'gpg',
    ['/usr/share/upstart/.*%.conf$'] = 'upstart',
    ['/usr/share/upstart/.*%.override$'] = 'upstart',
  },
  ['/var/'] = {
    ['/var/backups/group%.bak$'] = 'group',
    ['/var/backups/gshadow%.bak$'] = 'group',
    ['/var/backups/passwd%.bak$'] = 'passwd',
    ['/var/backups/shadow%.bak$'] = 'passwd',
  },
  ['/conf'] = {
    ['/%.aptitude/config$'] = 'aptconf',
    ['/build/conf/.*%.conf$'] = 'bitbake',
    ['/meta%-.*/conf/.*%.conf$'] = 'bitbake',
    ['/meta/conf/.*%.conf$'] = 'bitbake',
    ['/project%-spec/configs/.*%.conf$'] = 'bitbake',
    ['/%.cabal/config$'] = 'cabalconfig',
    ['/cabal/config$'] = 'cabalconfig',
    ['/%.aws/config$'] = 'confini',
    ['/bpython/config$'] = 'dosini',
    ['/flatpak/repo/config$'] = 'dosini',
    ['/mypy/config$'] = 'dosini',
    ['^${HOME}/%.config/notmuch/.*/config$'] = 'dosini',
    ['^${XDG_CONFIG_HOME}/notmuch/.*/config$'] = 'dosini',
    ['^${XDG_CONFIG_HOME}/git/config$'] = 'gitconfig',
    ['%.git/config%.worktree$'] = 'gitconfig',
    ['%.git/config$'] = 'gitconfig',
    ['%.git/modules/.*/config$'] = 'gitconfig',
    ['%.git/modules/config$'] = 'gitconfig',
    ['%.git/worktrees/.*/config%.worktree$'] = 'gitconfig',
    ['/%.config/git/config$'] = 'gitconfig',
    ['/gitolite%-admin/conf/'] = starsetf('gitolite'),
    ['/%.i3/config$'] = 'i3config',
    ['/i3/config$'] = 'i3config',
    ['/%.mplayer/config$'] = 'mplayerconf',
    ['/supertux2/config$'] = 'scheme',
    ['/neofetch/config%.conf$'] = 'sh',
    ['/%.ssh/config$'] = 'sshconfig',
    ['/%.sway/config$'] = 'swayconfig',
    ['/sway/config$'] = 'swayconfig',
    ['/%.cargo/config$'] = 'toml',
    ['/%.bundle/config$'] = 'yaml',
    ['/%.kube/config$'] = 'yaml',
  },
  ['/%.'] = {
    ['/%.aws/credentials$'] = 'confini',
    ['/%.gitconfig%.d/'] = starsetf('gitconfig'),
    ['/%.gnupg/gpg%.conf$'] = 'gpg',
    ['/%.gnupg/options$'] = 'gpg',
    ['/%.icewm/menu$'] = 'icemenu',
    ['/%.libao$'] = 'libao',
    ['/%.pinforc$'] = 'pinfo',
    ['/%.cargo/credentials$'] = 'toml',
    ['/%.init/.*%.override$'] = 'upstart',
  },
  ['calendar/'] = {
    ['/%.calendar/'] = starsetf('calendar'),
    ['/share/calendar/.*/calendar%.'] = starsetf('calendar'),
    ['/share/calendar/calendar%.'] = starsetf('calendar'),
  },
  ['cmus/'] = {
    -- */cmus/*.theme and */.cmus/*.theme
    ['/%.?cmus/.*%.theme$'] = 'cmusrc',
    -- */cmus/rc and */.cmus/rc
    ['/%.?cmus/rc$'] = 'cmusrc',
    ['/%.cmus/autosave$'] = 'cmusrc',
    ['/%.cmus/command%-history$'] = 'cmusrc',
  },
  ['git/'] = {
    ['%.git/'] = {
      detect.git,
      -- Decrease priority to run after simple pattern checks
      { priority = -1 },
    },
    ['^${XDG_CONFIG_HOME}/git/attributes$'] = 'gitattributes',
    ['%.git/info/attributes$'] = 'gitattributes',
    ['/%.config/git/attributes$'] = 'gitattributes',
    ['^${XDG_CONFIG_HOME}/git/ignore$'] = 'gitignore',
    ['%.git/info/exclude$'] = 'gitignore',
    ['/%.config/git/ignore$'] = 'gitignore',
  },
  ['%.cfg'] = {
    ['enlightenment/.*%.cfg$'] = 'c',
    ['Eterm/.*%.cfg$'] = 'eterm',
    ['baseq[2-3]/.*%.cfg$'] = 'quake',
    ['id1/.*%.cfg$'] = 'quake',
    ['quake[1-3]/.*%.cfg$'] = 'quake',
    ['/tex/latex/.*%.cfg$'] = 'tex',
  },
  ['%.conf'] = {
    ['^proftpd%.conf'] = starsetf('apachestyle'),
    ['^access%.conf'] = detect_apache,
    ['^apache%.conf'] = detect_apache,
    ['^apache2%.conf'] = detect_apache,
    ['^httpd%.conf'] = detect_apache,
    ['^httpd%-.*%.conf'] = detect_apache,
    ['^proxy%-html%.conf'] = detect_apache,
    ['^srm%.conf'] = detect_apache,
    ['asterisk/.*%.conf'] = starsetf('asterisk'),
    ['asterisk.*/.*voicemail%.conf'] = starsetf('asteriskvm'),
    ['^dictd.*%.conf$'] = 'dictdconf',
    ['/lxqt/.*%.conf$'] = 'dosini',
    ['/screengrab/.*%.conf$'] = 'dosini',
    ['/%.config/fd/ignore$'] = 'gitignore',
    ['^${GNUPGHOME}/gpg%.conf$'] = 'gpg',
    ['/boot/grub/grub%.conf$'] = 'grub',
    ['/hypr/.*%.conf$'] = 'hyprlang',
    ['^lilo%.conf'] = starsetf('lilo'),
    ['^named.*%.conf$'] = 'named',
    ['^rndc.*%.conf$'] = 'named',
    ['/openvpn/.*/.*%.conf$'] = 'openvpn',
    ['/pipewire/.*%.conf$'] = 'spajson',
    ['/wireplumber/.*%.conf$'] = 'spajson',
    ['/%.ssh/.*%.conf$'] = 'sshconfig',
    ['^%.?tmux.*%.conf$'] = 'tmux',
    ['^%.?tmux.*%.conf'] = { 'tmux', { priority = -1 } },
    ['/containers/containers%.conf$'] = 'toml',
    ['/containers/containers%.conf%.d/.*%.conf$'] = 'toml',
    ['/containers/containers%.conf%.modules/.*%.conf$'] = 'toml',
    ['/containers/registries%.conf$'] = 'toml',
    ['/containers/registries%.conf%.d/.*%.conf$'] = 'toml',
    ['/containers/storage%.conf$'] = 'toml',
    ['/%.config/upstart/.*%.conf$'] = 'upstart',
    ['/%.config/upstart/.*%.override$'] = 'upstart',
    ['/%.init/.*%.conf$'] = 'upstart',
    ['/xorg%.conf%.d/.*%.conf$'] = detect.xfree86_v4,
  },
  ['sst%.meta'] = {
    ['%.%-sst%.meta$'] = 'sisu',
    ['%._sst%.meta$'] = 'sisu',
    ['%.sst%.meta$'] = 'sisu',
  },
  ['file'] = {
    ['^Containerfile%.'] = starsetf('dockerfile'),
    ['^Dockerfile%.'] = starsetf('dockerfile'),
    ['[mM]akefile$'] = detect.make,
    ['^[mM]akefile'] = starsetf(detect.make),
    ['^[rR]akefile'] = starsetf('ruby'),
    ['^%.profile'] = detect.sh,
  },
  ['fvwm'] = {
    ['/%.fvwm/'] = starsetf('fvwm'),
    ['fvwmrc'] = starsetf(detect.fvwm_v1),
    ['fvwm95.*%.hook$'] = starsetf(detect.fvwm_v1),
    ['fvwm2rc'] = starsetf(detect.fvwm_v2),
  },
  ['nginx'] = {
    ['/nginx/.*%.conf$'] = 'nginx',
    ['/usr/local/nginx/conf/'] = 'nginx',
    ['nginx%.conf$'] = 'nginx',
    ['^nginx.*%.conf$'] = 'nginx',
  },
  ['require'] = {
    ['%-requirements%.txt$'] = 'requirements',
    ['^requirements/.*%.txt$'] = 'requirements',
    ['^requires/.*%.txt$'] = 'requirements',
  },
  ['s6'] = {
    ['s6.*/down$'] = 'execline',
    ['s6.*/finish$'] = 'execline',
    ['s6.*/run$'] = 'execline',
    ['s6.*/up$'] = 'execline',
    ['^s6%-'] = 'execline',
  },
  ['utt'] = {
    ['^mutt%-.*%-%w+$'] = 'mail',
    ['^mutt' .. string.rep('[%w_-]', 6) .. '$'] = 'mail',
    ['^muttng%-.*%-%w+$'] = 'mail',
    ['^neomutt%-.*%-%w+$'] = 'mail',
    ['^neomutt' .. string.rep('[%w_-]', 6) .. '$'] = 'mail',
    -- muttngrc* and .muttngrc*
    ['^%.?muttngrc'] = detect_muttrc,
    -- muttrc* and .muttrc*
    ['^%.?muttrc'] = detect_muttrc,
    ['/%.mutt/muttrc'] = detect_muttrc,
    ['/%.muttng/muttngrc'] = detect_muttrc,
    ['/%.muttng/muttrc'] = detect_muttrc,
    ['^Muttngrc'] = detect_muttrc,
    ['^Muttrc'] = detect_muttrc,
    -- neomuttrc* and .neomuttrc*
    ['^%.?neomuttrc'] = detect_neomuttrc,
    ['/%.neomutt/neomuttrc'] = detect_neomuttrc,
    ['^Neomuttrc'] = detect_neomuttrc,
    ['%.neomuttdebug'] = 'neomuttlog',
  },
  ['xkb/'] = {
    ['/%.?xkb/compat/'] = detect_xkb,
    ['/%.?xkb/geometry/'] = detect_xkb,
    ['/%.?xkb/keycodes/'] = detect_xkb,
    ['/%.?xkb/symbols/'] = detect_xkb,
    ['/%.?xkb/types/'] = detect_xkb,
  },
  ['m17n'] = {
    ['/m17n/.*%.ali$'] = 'm17ndb',
    ['/m17n/.*%.cs$'] = 'm17ndb',
    ['/m17n/.*%.dir$'] = 'm17ndb',
    ['/m17n/.*%.flt$'] = 'm17ndb',
    ['/m17n/.*%.fst$'] = 'm17ndb',
    ['/m17n/.*%.lnm$'] = 'm17ndb',
    ['/m17n/.*%.mic$'] = 'm17ndb',
    ['/m17n/.*%.mim$'] = 'm17ndb',
    ['/m17n/.*%.tbl$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.ali$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.cs$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.dir$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.flt$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.fst$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.lnm$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.mic$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.mim$'] = 'm17ndb',
    ['/%.m17n%.d/.*%.tbl$'] = 'm17ndb',
    ['/m17n%-db/.*%.ali$'] = 'm17ndb',
    ['/m17n%-db/.*%.cs$'] = 'm17ndb',
    ['/m17n%-db/.*%.dir$'] = 'm17ndb',
    ['/m17n%-db/.*%.flt$'] = 'm17ndb',
    ['/m17n%-db/.*%.fst$'] = 'm17ndb',
    ['/m17n%-db/.*%.lnm$'] = 'm17ndb',
    ['/m17n%-db/.*%.mic$'] = 'm17ndb',
    ['/m17n%-db/.*%.mim$'] = 'm17ndb',
    ['/m17n%-db/.*%.tbl$'] = 'm17ndb',
  },
  ['^%.'] = {
    ['^%.cshrc'] = detect.csh,
    ['^%.login'] = detect.csh,
    ['^%.notmuch%-config%.'] = 'dosini',
    ['^%.gitsendemail%.msg%.......$'] = 'gitsendemail',
    ['^%.kshrc'] = detect.ksh,
    ['^%.article%.%d+$'] = 'mail',
    ['^%.letter%.%d+$'] = 'mail',
    ['^%.reminders'] = starsetf('remind'),
    ['^%.tcshrc'] = detect.tcsh,
    ['^%.zcompdump'] = starsetf('zsh'),
  },
  ['proj%.user$'] = {
    ['%.csproj%.user$'] = 'xml',
    ['%.fsproj%.user$'] = 'xml',
    ['%.vbproj%.user$'] = 'xml',
  },
  [''] = {
    ['^bash%-fc[%-%.]'] = detect.bash,
    ['/bind/db%.'] = starsetf('bindzone'),
    ['/named/db%.'] = starsetf('bindzone'),
    ['%.blade%.php$'] = 'blade',
    ['^bzr_log%.'] = 'bzr',
    ['^cabal%.project%.'] = starsetf('cabalproject'),
    ['^sgml%.catalog'] = starsetf('catalog'),
    ['hgrc$'] = 'cfg',
    ['^[cC]hange[lL]og'] = starsetf(detect.changelog),
    ['%.%.ch$'] = 'chill',
    ['%.cmake%.in$'] = 'cmake',
    ['^crontab%.'] = starsetf('crontab'),
    ['^cvs%d+$'] = 'cvs',
    ['/DEBIAN/control$'] = 'debcontrol',
    ['^php%.ini%-'] = 'dosini',
    ['^php%-fpm%.conf'] = 'dosini',
    ['^www%.conf'] = 'dosini',
    ['^drac%.'] = starsetf('dracula'),
    ['/dtrace/.*%.d$'] = 'dtrace',
    ['esmtprc$'] = 'esmtprc',
    ['/0%.orig/'] = detect.foam,
    ['/0/'] = detect.foam,
    ['/constant/g$'] = detect.foam,
    ['Transport%.'] = detect.foam,
    ['^[a-zA-Z0-9].*Dict%.'] = detect.foam,
    ['^[a-zA-Z0-9].*Dict$'] = detect.foam,
    ['^[a-zA-Z].*Properties%.'] = detect.foam,
    ['^[a-zA-Z].*Properties$'] = detect.foam,
    ['/tmp/lltmp'] = starsetf('gedcom'),
    ['^gkrellmrc_.$'] = 'gkrellmrc',
    ['^${GNUPGHOME}/options$'] = 'gpg',
    ['/boot/grub/menu%.lst$'] = 'grub',
    -- gtkrc* and .gtkrc*
    ['^%.?gtkrc'] = starsetf('gtkrc'),
    ['/doc/.*%.txt$'] = function(_, bufnr)
      local line = M._getline(bufnr, -1)
      if
        M._findany(line, {
          '^vim:ft=help[:%s]',
          '^vim:ft=help$',
          '^vim:filetype=help[:%s]',
          '^vim:filetype=help$',
          '^vim:.*[:%s]ft=help[:%s]',
          '^vim:.*[:%s]ft=help$',
          '^vim:.*[:%s]filetype=help[:%s]',
          '^vim:.*[:%s]filetype=help$',
          '%svim:ft=help[:%s]',
          '%svim:ft=help$',
          '%svim:filetype=help[:%s]',
          '%svim:filetype=help$',
          '%svim:.*[:%s]ft=help[:%s]',
          '%svim:.*[:%s]ft=help$',
          '%svim:.*[:%s]filetype=help[:%s]',
          '%svim:.*[:%s]filetype=help$',
        })
      then
        return 'help'
      end
    end,
    ['^hg%-editor%-.*%.txt$'] = 'hgcommit',
    ['%.html%.m4$'] = 'htmlm4',
    ['^JAM.*%.'] = starsetf('jam'),
    ['^Prl.*%.'] = starsetf('jam'),
    ['^${HOME}/.*/Code/User/.*%.json$'] = 'jsonc',
    ['^${HOME}/.*/VSCodium/User/.*%.json$'] = 'jsonc',
    ['%.properties_..$'] = 'jproperties',
    ['%.properties_.._..$'] = 'jproperties',
    ['%.properties_.._.._'] = starsetf('jproperties'),
    ['^org%.eclipse%..*%.prefs$'] = 'jproperties',
    ['^[jt]sconfig.*%.json$'] = 'jsonc',
    ['^Config%.in%.'] = starsetf('kconfig'),
    ['^Kconfig%.'] = starsetf('kconfig'),
    ['/ldscripts/'] = 'ld',
    ['lftp/rc$'] = 'lftp',
    ['/LiteStep/.*/.*%.rc$'] = 'litestep',
    ['^/tmp/SLRN[0-9A-Z.]+$'] = 'mail',
    ['^ae%d+%.txt$'] = 'mail',
    ['^pico%.%d+$'] = 'mail',
    ['^reportbug%-'] = starsetf('mail'),
    ['^snd%.%d+$'] = 'mail',
    ['^rndc.*%.key$'] = 'named',
    ['^tmac%.'] = starsetf('nroff'),
    ['%.ml%.cppo$'] = 'ocaml',
    ['%.mli%.cppo$'] = 'ocaml',
    ['/octave/history$'] = 'octave',
    ['%.opam%.locked$'] = 'opam',
    ['%.opam%.template$'] = 'opam',
    ['^pacman%.log'] = starsetf(function(path, bufnr)
      return vim.uv.fs_stat(path) and 'pacmanlog' or nil
    end),
    ['printcap'] = starsetf(function(path, bufnr)
      return require('vim.filetype.detect').printcap('print')
    end),
    ['/queries/.*%.scm$'] = 'query', -- treesitter queries (Neovim only)
    [',v$'] = 'rcs',
    ['^svn%-commit.*%.tmp$'] = 'svn',
    ['%.swift%.gyb$'] = 'swiftgyb',
    ['termcap'] = starsetf(function(path, bufnr)
      return require('vim.filetype.detect').printcap('term')
    end),
    ['%.t%.html$'] = 'tilde',
    ['%.vhdl_[0-9]'] = starsetf('vhdl'),
    ['vimrc'] = starsetf('vim'),
    ['/Xresources/'] = starsetf('xdefaults'),
    ['/app%-defaults/'] = starsetf('xdefaults'),
    ['^Xresources'] = starsetf('xdefaults'),
    -- Increase priority to run before the pattern below
    ['^XF86Config%-4'] = starsetf(detect.xfree86_v4, -math.huge + 1),
    ['^XF86Config'] = starsetf(detect.xfree86_v3),
    ['Xmodmap$'] = 'xmodmap',
    ['xmodmap'] = starsetf('xmodmap'),
    -- .zlog* and zlog*
    ['^%.?zlog'] = starsetf('zsh'),
    -- .zsh* and zsh*
    ['^%.?zsh'] = starsetf('zsh'),
    -- Ignored extension
    ['~$'] = function(path, bufnr)
      local short = path:gsub('~+$', '', 1)
      if path ~= short and short ~= '' then
        return M.match({ buf = bufnr, filename = fn.fnameescape(short) })
      end
    end,
  },
  -- END PATTERN
}
-- luacheck: pop
-- luacheck: pop

--- Lookup table/cache for patterns
--- @alias vim.filetype.pattern_cache { has_env: boolean, has_slash: boolean }
--- @type table<string,vim.filetype.pattern_cache>
local pattern_lookup = {}

--- @param a vim.filetype.mapping.sorted
--- @param b vim.filetype.mapping.sorted
local function compare_by_priority(a, b)
  return a[4] > b[4]
end

--- @param pat string
--- @return { has_env: boolean, has_slash: boolean }
local function parse_pattern(pat)
  return { has_env = pat:find('%$%b{}') ~= nil, has_slash = pat:find('/') ~= nil }
end

--- @param t table<string,vim.filetype.mapping>
--- @return vim.filetype.mapping.sorted[]
--- @return vim.filetype.mapping.sorted[]
local function sort_by_priority(t)
  -- Separate patterns with non-negative and negative priority because they
  -- will be processed separately
  local pos = {} --- @type vim.filetype.mapping.sorted[]
  local neg = {} --- @type vim.filetype.mapping.sorted[]
  for parent, ft_map in pairs(t) do
    pattern_lookup[parent] = pattern_lookup[parent] or parse_pattern(parent)
    for pat, maptbl in pairs(ft_map) do
      local ft_or_fun = type(maptbl) == 'table' and maptbl[1] or maptbl
      assert(
        type(ft_or_fun) == 'string' or type(ft_or_fun) == 'function',
        'Expected string or function for filetype'
      )

      -- Parse pattern for common data and cache it once
      pattern_lookup[pat] = pattern_lookup[pat] or parse_pattern(pat)

      --- @type vim.filetype.mapopts?
      local opts = (type(maptbl) == 'table' and type(maptbl[2]) == 'table') and maptbl[2] or nil
      local priority = opts and opts.priority or 0

      table.insert(priority >= 0 and pos or neg, { parent, pat, ft_or_fun, priority })
    end
  end

  table.sort(pos, compare_by_priority)
  table.sort(neg, compare_by_priority)
  return pos, neg
end

local pattern_sorted_pos, pattern_sorted_neg = sort_by_priority(pattern)

--- @param path string
--- @param as_pattern? true
--- @return string
local function normalize_path(path, as_pattern)
  local normal = path:gsub('\\', '/')
  if normal:find('^~') then
    if as_pattern then
      -- Escape Lua's metacharacters when $HOME is used in a pattern.
      -- The rest of path should already be properly escaped.
      normal = vim.pesc(vim.env.HOME) .. normal:sub(2)
    else
      normal = vim.env.HOME .. normal:sub(2) --- @type string
    end
  end
  return normal
end

local abspath = function(x)
  return fn.fnamemodify(x, ':p')
end
if fn.has('win32') == 1 then
  abspath = function(x)
    return (fn.fnamemodify(x, ':p'):gsub('\\', '/'))
  end
end

--- @class vim.filetype.add.filetypes
--- @inlinedoc
--- @field pattern? vim.filetype.mapping
--- @field extension? vim.filetype.mapping
--- @field filename? vim.filetype.mapping

--- Add new filetype mappings.
---
--- Filetype mappings can be added either by extension or by filename (either
--- the "tail" or the full file path). The full file path is checked first,
--- followed by the file name. If a match is not found using the filename, then
--- the filename is matched against the list of |lua-pattern|s (sorted by priority)
--- until a match is found. Lastly, if pattern matching does not find a
--- filetype, then the file extension is used.
---
--- The filetype can be either a string (in which case it is used as the
--- filetype directly) or a function. If a function, it takes the full path and
--- buffer number of the file as arguments (along with captures from the matched
--- pattern, if any) and should return a string that will be used as the
--- buffer's filetype. Optionally, the function can return a second function
--- value which, when called, modifies the state of the buffer. This can be used
--- to, for example, set filetype-specific buffer variables. This function will
--- be called by Nvim before setting the buffer's filetype.
---
--- Filename patterns can specify an optional priority to resolve cases when a
--- file path matches multiple patterns. Higher priorities are matched first.
--- When omitted, the priority defaults to 0.
--- A pattern can contain environment variables of the form "${SOME_VAR}" that will
--- be automatically expanded. If the environment variable is not set, the pattern
--- won't be matched.
---
--- See $VIMRUNTIME/lua/vim/filetype.lua for more examples.
---
--- Example:
---
--- ```lua
--- vim.filetype.add({
---   extension = {
---     foo = 'fooscript',
---     bar = function(path, bufnr)
---       if some_condition() then
---         return 'barscript', function(bufnr)
---           -- Set a buffer variable
---           vim.b[bufnr].barscript_version = 2
---         end
---       end
---       return 'bar'
---     end,
---   },
---   filename = {
---     ['.foorc'] = 'toml',
---     ['/etc/foo/config'] = 'toml',
---   },
---   pattern = {
---     ['.*/etc/foo/.*'] = 'fooscript',
---     -- Using an optional priority
---     ['.*/etc/foo/.*%.conf'] = { 'dosini', { priority = 10 } },
---     -- A pattern containing an environment variable
---     ['${XDG_CONFIG_HOME}/foo/git'] = 'git',
---     ['.*README.(%a+)'] = function(path, bufnr, ext)
---       if ext == 'md' then
---         return 'markdown'
---       elseif ext == 'rst' then
---         return 'rst'
---       end
---     end,
---   },
--- })
--- ```
---
--- To add a fallback match on contents, use
---
--- ```lua
--- vim.filetype.add {
---   pattern = {
---     ['.*'] = {
---       function(path, bufnr)
---         local content = vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1] or ''
---         if vim.regex([[^#!.*\\<mine\\>]]):match_str(content) ~= nil then
---           return 'mine'
---         elseif vim.regex([[\\<drawing\\>]]):match_str(content) ~= nil then
---           return 'drawing'
---         end
---       end,
---       { priority = -math.huge },
---     },
---   },
--- }
--- ```
---
---@param filetypes vim.filetype.add.filetypes A table containing new filetype maps (see example).
function M.add(filetypes)
  for k, v in pairs(filetypes.extension or {}) do
    extension[k] = v
  end

  for k, v in pairs(filetypes.filename or {}) do
    filename[normalize_path(k)] = v
  end

  for k, v in pairs(filetypes.pattern or {}) do
    -- Add to "match all" parent pattern (might be better to optimize later or document
    -- supplying `opts.parent` directly)
    -- User patterns are assumed to be implicitly anchored (as in Vim)
    pattern['']['^' .. normalize_path(k, true) .. '$'] = v
  end

  if filetypes.pattern then
    -- TODO: full resorting might be expensive with a lot of separate `vim.filetype.add()` calls.
    -- Consider inserting new patterns precisely into already sorted lists of built-in patterns.
    pattern_sorted_pos, pattern_sorted_neg = sort_by_priority(pattern)
  end
end

--- @param ft vim.filetype.mapping.value
--- @param path? string
--- @param bufnr? integer
--- @param ... any
--- @return string?
--- @return fun(b: integer)?
local function dispatch(ft, path, bufnr, ...)
  if type(ft) == 'string' then
    return ft
  end

  if type(ft) ~= 'function' then
    return
  end

  assert(path)

  ---@type string|false?, fun(b: integer)?
  local ft0, on_detect
  if bufnr then
    ft0, on_detect = ft(path, bufnr, ...)
  else
    -- If bufnr is nil (meaning we are matching only against the filename), set it to an invalid
    -- value (-1) and catch any errors from the filetype detection function. If the function tries
    -- to use the buffer then it will fail, but this enables functions which do not need a buffer
    -- to still work.
    local ok
    ok, ft0, on_detect = pcall(ft, path, -1, ...)
    if not ok then
      return
    end
  end

  if not ft0 then
    return
  end

  return ft0, on_detect
end

--- @param pat string
--- @return boolean
--- @return string
local function expand_envvar_pattern(pat)
  local some_env_missing = false
  local expanded = pat:gsub('%${(%S-)}', function(env)
    local val = vim.env[env] --- @type string?
    some_env_missing = some_env_missing or val == nil
    return vim.pesc(val or '')
  end)
  return some_env_missing, expanded
end

--- @param name string
--- @param path string
--- @param tail string
--- @param pat string
--- @param try_all_candidates boolean
--- @return string?
local function match_pattern(name, path, tail, pat, try_all_candidates)
  local pat_cache = pattern_lookup[pat]
  local has_slash = pat_cache.has_slash

  if pat_cache.has_env then
    local some_env_missing, expanded = expand_envvar_pattern(pat)
    -- If any environment variable is present in the pattern but not set, there is no match
    if some_env_missing then
      return nil
    end
    pat = expanded
    has_slash = has_slash or expanded:find('/') ~= nil
  end

  -- Try all possible candidates to make parent patterns not depend on slash presence
  if try_all_candidates then
    return (path:match(pat) or name:match(pat) or tail:match(pat))
  end

  -- If the pattern contains a / match against the full path, otherwise just the tail
  if has_slash then
    -- Similar to |autocmd-pattern|, if the pattern contains a '/' then check for a match against
    -- both the short file name (as typed) and the full file name (after expanding to full path
    -- and resolving symlinks)
    return (name:match(pat) or path:match(pat))
  end

  return (tail:match(pat))
end

--- @param name string
--- @param path string
--- @param tail string
--- @param pattern_sorted vim.filetype.mapping.sorted[]
--- @param parent_matches table<string,boolean>
--- @param bufnr integer?
local function match_pattern_sorted(name, path, tail, pattern_sorted, parent_matches, bufnr)
  for _, p in ipairs(pattern_sorted) do
    local parent, pat, ft_or_fn = p[1], p[2], p[3]

    local parent_is_matched = parent_matches[parent]
    if parent_is_matched == nil then
      parent_matches[parent] = match_pattern(name, path, tail, parent, true) ~= nil
      parent_is_matched = parent_matches[parent]
    end

    if parent_is_matched then
      local matches = match_pattern(name, path, tail, pat, false)
      if matches then
        local ft, on_detect = dispatch(ft_or_fn, path, bufnr, matches)
        if ft then
          return ft, on_detect
        end
      end
    end
  end
end

--- @class vim.filetype.match.args
--- @inlinedoc
---
--- Buffer number to use for matching. Mutually exclusive with {contents}
--- @field buf? integer
---
--- Filename to use for matching. When {buf} is given,
--- defaults to the filename of the given buffer number. The
--- file need not actually exist in the filesystem. When used
--- without {buf} only the name of the file is used for
--- filetype matching. This may result in failure to detect
--- the filetype in cases where the filename alone is not
--- enough to disambiguate the filetype.
--- @field filename? string
---
--- An array of lines representing file contents to use for
--- matching. Can be used with {filename}. Mutually exclusive
--- with {buf}.
--- @field contents? string[]

--- Perform filetype detection.
---
--- The filetype can be detected using one of three methods:
--- 1. Using an existing buffer
--- 2. Using only a file name
--- 3. Using only file contents
---
--- Of these, option 1 provides the most accurate result as it uses both the buffer's filename and
--- (optionally) the buffer contents. Options 2 and 3 can be used without an existing buffer, but
--- may not always provide a match in cases where the filename (or contents) cannot unambiguously
--- determine the filetype.
---
--- Each of the three options is specified using a key to the single argument of this function.
--- Example:
---
--- ```lua
--- -- Using a buffer number
--- vim.filetype.match({ buf = 42 })
---
--- -- Override the filename of the given buffer
--- vim.filetype.match({ buf = 42, filename = 'foo.c' })
---
--- -- Using a filename without a buffer
--- vim.filetype.match({ filename = 'main.lua' })
---
--- -- Using file contents
--- vim.filetype.match({ contents = {'#!/usr/bin/env bash'} })
--- ```
---
---@param args vim.filetype.match.args Table specifying which matching strategy to use.
---                 Accepted keys are:
---@return string|nil # If a match was found, the matched filetype.
---@return function|nil # A function that modifies buffer state when called (for example, to set some
---                     filetype specific buffer variables). The function accepts a buffer number as
---                     its only argument.
function M.match(args)
  vim.validate('arg', args, 'table')

  if not (args.buf or args.filename or args.contents) then
    error('At least one of "buf", "filename", or "contents" must be given')
  end

  local bufnr = args.buf
  local name = args.filename
  local contents = args.contents

  if bufnr and not name then
    name = api.nvim_buf_get_name(bufnr)
  end

  --- @type string?, fun(b: integer)?
  local ft, on_detect

  if name then
    name = normalize_path(name)

    -- First check for the simple case where the full path exists as a key
    local path = abspath(name)
    ft, on_detect = dispatch(filename[path], path, bufnr)
    if ft then
      return ft, on_detect
    end

    -- Next check against just the file name
    local tail = fn.fnamemodify(name, ':t')
    ft, on_detect = dispatch(filename[tail], path, bufnr)
    if ft then
      return ft, on_detect
    end

    -- Next, check the file path against available patterns with non-negative priority
    -- Cache match results of all parent patterns to improve performance
    local parent_matches = {}
    ft, on_detect =
      match_pattern_sorted(name, path, tail, pattern_sorted_pos, parent_matches, bufnr)
    if ft then
      return ft, on_detect
    end

    -- Next, check file extension
    -- Don't use fnamemodify() with :e modifier here,
    -- as that's empty when there is only an extension.
    local ext = name:match('%.([^.]-)$') or ''
    ft, on_detect = dispatch(extension[ext], path, bufnr)
    if ft then
      return ft, on_detect
    end

    -- Next, check patterns with negative priority
    ft, on_detect =
      match_pattern_sorted(name, path, tail, pattern_sorted_neg, parent_matches, bufnr)
    if ft then
      return ft, on_detect
    end
  end

  -- Finally, check file contents
  if contents or bufnr then
    if contents == nil then
      assert(bufnr)
      if api.nvim_buf_line_count(bufnr) > 101 then
        -- only need first 100 and last line for current checks
        contents = M._getlines(bufnr, 1, 100)
        contents[#contents + 1] = M._getline(bufnr, -1)
      else
        contents = M._getlines(bufnr)
      end
    end

    -- Match based solely on content only if there is any content (for performance)
    if not (#contents == 1 and contents[1] == '') then
      -- If name is nil, catch any errors from the contents filetype detection function.
      -- If the function tries to use the filename that is nil then it will fail,
      -- but this enables checks which do not need a filename to still work.
      local ok
      ok, ft, on_detect = pcall(
        require('vim.filetype.detect').match_contents,
        contents,
        name,
        function(ext)
          return dispatch(extension[ext], name, bufnr)
        end
      )
      if ok then
        return ft, on_detect
      end
    end
  end
end

--- Get the default option value for a {filetype}.
---
--- The returned value is what would be set in a new buffer after 'filetype'
--- is set, meaning it should respect all FileType autocmds and ftplugin files.
---
--- Example:
---
--- ```lua
--- vim.filetype.get_option('vim', 'commentstring')
--- ```
---
--- Note: this uses |nvim_get_option_value()| but caches the result.
--- This means |ftplugin| and |FileType| autocommands are only
--- triggered once and may not reflect later changes.
--- @since 11
--- @param filetype string Filetype
--- @param option string Option name
--- @return string|boolean|integer: Option value
function M.get_option(filetype, option)
  return require('vim.filetype.options').get_option(filetype, option)
end

return M
