Update runtime files
diff --git a/runtime/autoload/context.vim b/runtime/autoload/context.vim
index 254d710..e42b99e 100644
--- a/runtime/autoload/context.vim
+++ b/runtime/autoload/context.vim
@@ -1,184 +1,95 @@
-" Language:           ConTeXt typesetting engine
-" Maintainer:         Nicola Vitacolonna <nvitacolonna@gmail.com>
-" Latest Revision:    2016 Oct 21
+vim9script
 
-let s:keepcpo= &cpo
-set cpo&vim
+# Language:           ConTeXt typesetting engine
+# Maintainer:         Nicola Vitacolonna <nvitacolonna@gmail.com>
+# Former Maintainers: Nikolai Weibull <now@bitwi.se>
+# Latest Revision:    2022 Aug 12
 
-" Helper functions {{{
-function! s:context_echo(message, mode)
-  redraw
-  echo "\r"
-  execute 'echohl' a:mode
-  echomsg '[ConTeXt]' a:message
-  echohl None
-endf
+# Typesetting {{{
+import autoload './typeset.vim'
 
-function! s:sh()
-  return has('win32') || has('win64') || has('win16') || has('win95')
-        \ ? ['cmd.exe', '/C']
-        \ : ['/bin/sh', '-c']
-endfunction
+export def ConTeXtCmd(path: string): list<string>
+  return ['mtxrun', '--script', 'context', '--nonstopmode', '--autogenerate', path]
+enddef
 
-" For backward compatibility
-if exists('*win_getid')
+export def Typeset(bufname: string, env = {}, Cmd = ConTeXtCmd): bool
+  return typeset.TypesetBuffer(bufname, Cmd, env, 'ConTeXt')
+enddef
 
-  function! s:win_getid()
-    return win_getid()
-  endf
+export def JobStatus()
+  typeset.JobStatus('ConTeXt')
+enddef
 
-  function! s:win_id2win(winid)
-    return win_id2win(a:winid)
-  endf
+export def StopJobs()
+  typeset.StopJobs('ConTeXt')
+enddef
 
-else
+export def Log(bufname: string)
+  execute 'edit' typeset.LogPath(bufname)
+enddef
+# }}}
 
-  function! s:win_getid()
-    return winnr()
-  endf
+# Completion {{{
+def BinarySearch(base: string, keywords: list<string>): list<string>
+  const pat = '^' .. base
+  const len = len(keywords)
+  var res = []
+  var lft = 0
+  var rgt = len
 
-  function! s:win_id2win(winnr)
-    return a:winnr
-  endf
-
-endif
-" }}}
-
-" ConTeXt jobs {{{
-if has('job')
-
-  let g:context_jobs = []
-
-  " Print the status of ConTeXt jobs
-  function! context#job_status()
-    let l:jobs = filter(g:context_jobs, 'job_status(v:val) == "run"')
-    let l:n = len(l:jobs)
-    call s:context_echo(
-          \ 'There '.(l:n == 1 ? 'is' : 'are').' '.(l:n == 0 ? 'no' : l:n)
-          \ .' job'.(l:n == 1 ? '' : 's').' running'
-          \ .(l:n == 0 ? '.' : ' (' . join(l:jobs, ', ').').'),
-          \ 'ModeMsg')
-  endfunction
-
-  " Stop all ConTeXt jobs
-  function! context#stop_jobs()
-    let l:jobs = filter(g:context_jobs, 'job_status(v:val) == "run"')
-    for job in l:jobs
-      call job_stop(job)
-    endfor
-    sleep 1
-    let l:tmp = []
-    for job in l:jobs
-      if job_status(job) == "run"
-        call add(l:tmp, job)
-      endif
-    endfor
-    let g:context_jobs = l:tmp
-    if empty(g:context_jobs)
-      call s:context_echo('Done. No jobs running.', 'ModeMsg')
+  # Find the leftmost index matching base
+  while lft < rgt
+    var i = (lft + rgt) / 2
+    if keywords[i] < base
+      lft = i + 1
     else
-      call s:context_echo('There are still some jobs running. Please try again.', 'WarningMsg')
+      rgt = i
     endif
-  endfunction
+  endwhile
 
-  function! context#callback(path, job, status)
-    if index(g:context_jobs, a:job) != -1 && job_status(a:job) != 'run' " just in case
-      call remove(g:context_jobs, index(g:context_jobs, a:job))
+  while lft < len && keywords[lft] =~ pat
+    add(res, keywords[lft])
+    lft += 1
+  endwhile
+
+  return res
+enddef
+
+var isMetaPostBlock = false
+
+var MP_KEYWORDS:  list<string> = []
+var CTX_KEYWORDS: list<string> = []
+
+# Complete only MetaPost keywords in MetaPost blocks, and complete only
+# ConTeXt keywords otherwise.
+export def Complete(findstart: number, base: string): any
+  if findstart == 1
+    if len(synstack(line("."), 1)) > 0 && synIDattr(synstack(line("."), 1)[0], "name") ==# 'contextMPGraphic'
+      isMetaPostBlock = true
+      return match(getline('.'), '\S\+\%' .. col('.') .. 'c')
     endif
-    call s:callback(a:path, a:job, a:status)
-  endfunction
 
-  function! context#close_cb(channel)
-    call job_status(ch_getjob(a:channel)) " Trigger exit_cb's callback for faster feedback
-  endfunction
-
-  function! s:typeset(path)
-    call add(g:context_jobs,
-          \ job_start(add(s:sh(), context#command() . ' ' . shellescape(fnamemodify(a:path, ":t"))), {
-          \   'close_cb' : 'context#close_cb',
-          \   'exit_cb'  : function(get(b:, 'context_callback', get(g:, 'context_callback', 'context#callback')),
-          \                         [a:path]),
-          \   'in_io'    : 'null'
-          \ }))
-  endfunction
-
-else " No jobs
-
-  function! context#job_status()
-    call s:context_echo('Not implemented', 'WarningMsg')
-  endfunction!
-
-  function! context#stop_jobs()
-    call s:context_echo('Not implemented', 'WarningMsg')
-  endfunction
-
-  function! context#callback(path, job, status)
-    call s:callback(a:path, a:job, a:status)
-  endfunction
-
-  function! s:typeset(path)
-    execute '!' . context#command() . ' ' . shellescape(fnamemodify(a:path, ":t"))
-    call call(get(b:, 'context_callback', get(g:, 'context_callback', 'context#callback')),
-          \ [a:path, 0, v:shell_error])
-  endfunction
-
-endif " has('job')
-
-function! s:callback(path, job, status) abort
-  if a:status < 0 " Assume the job was terminated
-    return
+    # Complete only \commands starting with a backslash
+    isMetaPostBlock = false
+    var pos = match(getline('.'), '\\\zs\S\+\%' .. col('.') .. 'c')
+    return (pos == -1) ? -3 : pos
   endif
-  " Get info about the current window
-  let l:winid = s:win_getid()             " Save window id
-  let l:efm = &l:errorformat              " Save local errorformat
-  let l:cwd = fnamemodify(getcwd(), ":p") " Save local working directory
-  " Set errorformat to parse ConTeXt errors
-  execute 'setl efm=' . escape(b:context_errorformat, ' ')
-  try " Set cwd to expand error file correctly
-    execute 'lcd' fnameescape(fnamemodify(a:path, ':h'))
-  catch /.*/
-    execute 'setl efm=' . escape(l:efm, ' ')
-    throw v:exception
-  endtry
-  try
-    execute 'cgetfile' fnameescape(fnamemodify(a:path, ':r') . '.log')
-    botright cwindow
-  finally " Restore cwd and errorformat
-    execute s:win_id2win(l:winid) . 'wincmd w'
-    execute 'lcd ' . fnameescape(l:cwd)
-    execute 'setl efm=' . escape(l:efm, ' ')
-  endtry
-  if a:status == 0
-    call s:context_echo('Success!', 'ModeMsg')
-  else
-    call s:context_echo('There are errors. ', 'ErrorMsg')
+
+  if isMetaPostBlock
+    if empty(MP_KEYWORDS)
+      MP_KEYWORDS = sort(syntaxcomplete#OmniSyntaxList(['mf\w\+', 'mp\w\+']))
+    endif
+    return BinarySearch(base, MP_KEYWORDS)
   endif
-endfunction
 
-function! context#command()
-  return get(b:, 'context_mtxrun', get(g:, 'context_mtxrun', 'mtxrun'))
-        \ . ' --script context --autogenerate --nonstopmode'
-        \ . ' --synctex=' . (get(b:, 'context_synctex', get(g:, 'context_synctex', 0)) ? '1' : '0')
-        \ . ' ' . get(b:, 'context_extra_options', get(g:, 'context_extra_options', ''))
-endfunction
+  if empty(CTX_KEYWORDS)
+    CTX_KEYWORDS = sort(syntaxcomplete#OmniSyntaxList([
+      'context\w\+', 'texAleph', 'texEtex', 'texLuatex', 'texOmega',
+      'texPdftex', 'texTex', 'texXeTeX'
+    ]))
+  endif
+  return BinarySearch(base, CTX_KEYWORDS)
+enddef
+# }}}
 
-" Accepts an optional path (useful for big projects, when the file you are
-" editing is not the project's root document). If no argument is given, uses
-" the path of the current buffer.
-function! context#typeset(...) abort
-  let l:path = fnamemodify(strlen(a:000[0]) > 0 ? a:1 : expand("%"), ":p")
-  let l:cwd = fnamemodify(getcwd(), ":p") " Save local working directory
-  call s:context_echo('Typesetting...',  'ModeMsg')
-  execute 'lcd' fnameescape(fnamemodify(l:path, ":h"))
-  try
-    call s:typeset(l:path)
-  finally " Restore local working directory
-    execute 'lcd ' . fnameescape(l:cwd)
-  endtry
-endfunction!
-"}}}
-
-let &cpo = s:keepcpo
-unlet s:keepcpo
-
-" vim: sw=2 fdm=marker
+# vim: sw=2 fdm=marker
diff --git a/runtime/autoload/typeset.vim b/runtime/autoload/typeset.vim
new file mode 100644
index 0000000..35cf17b
--- /dev/null
+++ b/runtime/autoload/typeset.vim
@@ -0,0 +1,233 @@
+vim9script
+
+# Language:           Generic TeX typesetting engine
+# Maintainer:         Nicola Vitacolonna <nvitacolonna@gmail.com>
+# Latest Revision:    2022 Aug 12
+
+# Constants and helpers {{{
+const SLASH = !exists("+shellslash") || &shellslash ? '/' : '\'
+
+def Echo(msg: string, mode: string, label: string)
+  redraw
+  echo "\r"
+  execute 'echohl' mode
+  echomsg printf('[%s] %s', label, msg)
+  echohl None
+enddef
+
+def EchoMsg(msg: string, label = 'Notice')
+  Echo(msg, 'ModeMsg', label)
+enddef
+
+def EchoWarn(msg: string, label = 'Warning')
+  Echo(msg, 'WarningMsg', label)
+enddef
+
+def EchoErr(msg: string, label = 'Error')
+  Echo(msg, 'ErrorMsg', label)
+enddef
+# }}}
+
+# Track jobs {{{
+var running_jobs = {} # Dictionary of job IDs of jobs currently executing
+
+def AddJob(label: string, j: job)
+  if !has_key(running_jobs, label)
+    running_jobs[label] = []
+  endif
+
+  add(running_jobs[label], j)
+enddef
+
+def RemoveJob(label: string, j: job)
+  if has_key(running_jobs, label) && index(running_jobs[label], j) != -1
+    remove(running_jobs[label], index(running_jobs[label], j))
+  endif
+enddef
+
+def GetRunningJobs(label: string): list<job>
+  return has_key(running_jobs, label) ? running_jobs[label] : []
+enddef
+# }}}
+
+# Callbacks {{{
+def ProcessOutput(qfid: number, wd: string, efm: string, ch: channel, msg: string)
+  # Make sure the quickfix list still exists
+  if getqflist({'id': qfid}).id != qfid
+    EchoErr("Quickfix list not found, stopping the job")
+    call job_stop(ch_getjob(ch))
+    return
+  endif
+
+  # Make sure the working directory is correct
+  silent execute "lcd" wd
+  setqflist([], 'a', {'id': qfid, 'lines': [msg], 'efm': efm})
+  silent lcd -
+enddef
+
+def CloseCb(ch: channel)
+  job_status(ch_getjob(ch)) # Trigger exit_cb's callback
+enddef
+
+def ExitCb(label: string, jobid: job, exitStatus: number)
+  RemoveJob(label, jobid)
+
+  if exitStatus == 0
+    botright cwindow
+    EchoMsg('Success!', label)
+  elseif exitStatus < 0
+    EchoWarn('Job terminated', label)
+  else
+    botright copen
+    wincmd p
+    EchoWarn('There are errors.', label)
+  endif
+enddef
+# }}}
+
+# Create a new empty quickfix list at the end of the stack and return its id {{{
+def NewQuickfixList(path: string): number
+  if setqflist([], ' ', {'nr': '$', 'title': path}) == -1
+    return -1
+  endif
+
+  return getqflist({'nr': '$', 'id': 0}).id
+enddef
+# }}}
+
+# Public interface {{{
+# When a TeX document is split into several source files, each source file
+# may contain a "magic line" specifiying the "root" file, e.g.:
+#
+#   % !TEX root = main.tex
+#
+# Using this line, Vim can know which file to typeset even if the current
+# buffer is different from main.tex.
+#
+# This function searches for the magic line in the first ten lines of the
+# given buffer, and returns the full path of the root document.
+#
+# NOTE: the value of "% !TEX root" *must* be a relative path.
+export def FindRootDocument(bufname: string = bufname("%")): string
+  const bufnr = bufnr(bufname)
+
+  if !bufexists(bufnr)
+    return bufname
+  endif
+
+  var rootpath = fnamemodify(bufname(bufnr), ':p')
+
+  # Search for magic line `% !TEX root = ...` in the first ten lines
+  const header = getbufline(bufnr, 1, 10)
+  const idx = match(header, '^\s*%\s\+!TEX\s\+root\s*=\s*\S')
+  if idx > -1
+    const main = matchstr(header[idx], '!TEX\s\+root\s*=\s*\zs.*$')
+    rootpath = simplify(fnamemodify(rootpath, ":h") .. SLASH .. main)
+  endif
+
+  return rootpath
+enddef
+
+export def LogPath(bufname: string): string
+  const logfile = FindRootDocument(bufname)
+  return fnamemodify(logfile, ":r") .. ".log"
+enddef
+
+# Typeset the specified path
+#
+# Parameters:
+#   label: a descriptive string used in messages to identify the kind of job
+#   Cmd:   a function that takes the path of a document and returns the typesetting command
+#   path:  the path of the document to be typeset. To avoid ambiguities, pass a *full* path.
+#   efm:   the error format string to parse the output of the command.
+#   env:   environment variables for the process (passed to job_start())
+#
+# Returns:
+#   true if the job is started successfully;
+#   false otherwise.
+export def Typeset(
+  label: string,
+  Cmd:   func(string): list<string>,
+  path:  string,
+  efm:   string,
+  env:   dict<string> = {}
+): bool
+  var fp   = fnamemodify(path, ":p")
+  var wd   = fnamemodify(fp, ":h")
+  var qfid = NewQuickfixList(fp)
+
+  if qfid == -1
+    EchoErr('Could not create quickfix list', label)
+    return false
+  endif
+
+  if !filereadable(fp)
+    EchoErr(printf('File not readable: %s', fp), label)
+    return false
+  endif
+
+  var jobid = job_start(Cmd(path), {
+    env: env,
+    cwd: wd,
+    in_io: "null",
+    callback: (c, m) => ProcessOutput(qfid, wd, efm, c, m),
+    close_cb: CloseCb,
+    exit_cb: (j, e) => ExitCb(label, j, e),
+    })
+
+  if job_status(jobid) ==# "fail"
+    EchoErr("Failed to start job", label)
+    return false
+  endif
+
+  AddJob(label, jobid)
+
+  EchoMsg('Typesetting...', label)
+
+  return true
+enddef
+
+export def JobStatus(label: string)
+  EchoMsg('Jobs still running: ' .. string(len(GetRunningJobs(label))), label)
+enddef
+
+export def StopJobs(label: string)
+  for job in GetRunningJobs(label)
+    job_stop(job)
+  endfor
+
+  EchoMsg('Done.', label)
+enddef
+
+# Typeset the specified buffer
+#
+# Parameters:
+#   name:    a buffer's name. this may be empty to indicate the current buffer.
+#   cmd:     a function that takes the path of a document and returns the typesetting command
+#   label:   a descriptive string used in messages to identify the kind of job
+#   env:     environment variables for the process (passed to job_start())
+#
+# Returns:
+#   true if the job is started successfully;
+#   false otherwise.
+export def TypesetBuffer(
+  name: string,
+  Cmd: func(string): list<string>,
+  env = {},
+  label = 'Typeset'
+): bool
+  const bufname = bufname(name)
+
+  if empty(bufname)
+    EchoErr('Please save the buffer first.', label)
+    return false
+  endif
+
+  const efm = getbufvar(bufnr(bufname), "&efm")
+  const rootpath = FindRootDocument(bufname)
+
+  return Typeset('ConTeXt', Cmd, rootpath, efm, env)
+enddef
+# }}}
+
+# vim: sw=2 fdm=marker