mirror of
https://github.com/vim/vim.git
synced 2026-05-06 12:26:58 -04:00
aa5c9310f5
Problem: runtime(typeset) does not escape the detected directory Solution: Use fnameescape() (Michał Majchrowicz) fyi @lifepillar Signed-off-by: Christian Brabandt <cb@256bit.org>
293 lines
7.9 KiB
VimL
293 lines
7.9 KiB
VimL
vim9script
|
|
|
|
# Language: Generic TeX typesetting engine
|
|
# Maintainer: Nicola Vitacolonna <nvitacolonna@gmail.com>
|
|
# Latest Revision: 2026 Feb 19
|
|
# Last Change:
|
|
# 2026 Mar 30 by Vim project: Use fnameescape for the ProcessOutput command
|
|
|
|
# Constants and helpers {{{
|
|
const SLASH = !exists("+shellslash") || &shellslash ? '/' : '\'
|
|
|
|
def Echo(msg: string, mode: string, label: string)
|
|
redraw
|
|
echo "\r"
|
|
execute 'echohl' mode
|
|
echomsg $'[{label}] {msg}'
|
|
echohl None
|
|
enddef
|
|
|
|
def EchoMsg(msg: string, label = gettext('Notice'))
|
|
Echo(msg, 'ModeMsg', label)
|
|
enddef
|
|
|
|
def EchoWarn(msg: string, label = gettext('Warning'))
|
|
Echo(msg, 'WarningMsg', label)
|
|
enddef
|
|
|
|
def EchoErr(msg: string, label = gettext('Error'))
|
|
Echo(msg, 'ErrorMsg', label)
|
|
enddef
|
|
# }}}
|
|
|
|
# Track jobs {{{
|
|
var running_jobs: dict<list<job>> = {}
|
|
|
|
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(gettext("Quickfix list not found, stopping the job"))
|
|
job_stop(ch_getjob(ch))
|
|
return
|
|
endif
|
|
|
|
# Make sure the working directory is correct
|
|
silent execute "lcd" .. fnameescape(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(gettext('Success!'), label)
|
|
elseif exitStatus < 0
|
|
EchoWarn(gettext('Job terminated'), label)
|
|
else
|
|
botright copen
|
|
wincmd p
|
|
EchoWarn(gettext('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" specifying 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.
|
|
export def FindRootDocument(bufname: string = bufname("%")): string
|
|
var docpath = fnamemodify(bufname, ":p")
|
|
var bufnr = bufnr(bufname)
|
|
var header: list<string>
|
|
var rootpath = docpath
|
|
|
|
if bufexists(bufnr)
|
|
header = getbufline(bufnr, 1, 10)
|
|
elseif filereadable(bufname)
|
|
header = readfile(bufname, "", 10)
|
|
else
|
|
return simplify(rootpath)
|
|
endif
|
|
|
|
# Search for magic line `% !TEX root = ...` in the first ten lines
|
|
var idx = match(header, '^\s*%\s\+!TEX\s\+root\s*=\s*\S')
|
|
|
|
if idx > -1
|
|
rootpath = matchstr(header[idx], '!TEX\s\+root\s*=\s*\zs.*$')
|
|
|
|
if !isabsolutepath(rootpath) # Path is relative to the buffer's path
|
|
rootpath = fnamemodify(docpath, ":h") .. SLASH .. rootpath
|
|
endif
|
|
endif
|
|
|
|
return simplify(rootpath)
|
|
enddef
|
|
|
|
# ConTeXt documents may specify an output directory in a comment using the
|
|
# following syntax:
|
|
#
|
|
# runpath=texruns:<output directory>
|
|
#
|
|
# This function looks for such a comment in the first ten lines of the given
|
|
# buffer, and returns the full path of the output directory. If the comment is
|
|
# not found then the output directory coincides with the directory of the
|
|
# buffer.
|
|
export def GetOutputDirectory(bufname: string = bufname("%")): string
|
|
var basedir = fnamemodify(bufname, ':p:h')
|
|
var bufnr = bufnr(bufname)
|
|
var header: list<string>
|
|
var outdir = basedir
|
|
|
|
if bufexists(bufnr)
|
|
header = getbufline(bufnr, 1, 10)
|
|
elseif filereadable(bufname)
|
|
header = readfile(bufname, "", 10)
|
|
else
|
|
return simplify(outdir)
|
|
endif
|
|
|
|
# Search for output path in the first ten lines
|
|
var idx = match(header, '^\s*%.*\<runpath\s*=\s*texruns\s*:\s*\S')
|
|
|
|
if idx > -1
|
|
outdir = matchstr(header[idx], '\<runpath\s*=\s*texruns\s*:\s*\zs.*$')
|
|
|
|
if !isabsolutepath(outdir) # Path is relative to the buffer's directory
|
|
outdir = basedir .. SLASH .. outdir
|
|
endif
|
|
endif
|
|
|
|
return simplify(outdir)
|
|
enddef
|
|
|
|
export def LogPath(bufname: string): string
|
|
var rootdoc = FindRootDocument(bufname)
|
|
var docname = fnamemodify(rootdoc, ":t:r")
|
|
var outdir = GetOutputDirectory(rootdoc)
|
|
|
|
if empty(docname) # Set an arbitrary name to avoid returning a dotfile (.log)
|
|
docname = '[NotFound]'
|
|
endif
|
|
|
|
return $'{outdir}{SLASH}{docname}.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 name = fnamemodify(fp, ':t')
|
|
var wd = fnamemodify(fp, ':h')
|
|
var qfid = NewQuickfixList(fp)
|
|
|
|
if qfid == -1
|
|
EchoErr(gettext('Could not create quickfix list'), label)
|
|
return false
|
|
endif
|
|
|
|
if !filereadable(fp)
|
|
var msg = gettext('File not readable:')
|
|
EchoErr($'{msg} {fp}', label)
|
|
return false
|
|
endif
|
|
|
|
# Make sure to pass only the base name of the path to Cmd as this usually
|
|
# works better with TeX commands (note that the command is executed inside
|
|
# the file's directory). For instance, ConTeXt writes the path in .synctex
|
|
# files, and full paths break syncing from the editor to the viewer.
|
|
var jobid = job_start(Cmd(name), {
|
|
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(gettext("Failed to start job"), label)
|
|
return false
|
|
endif
|
|
|
|
AddJob(label, jobid)
|
|
|
|
EchoMsg(gettext('Typesetting...'), label)
|
|
|
|
return true
|
|
enddef
|
|
|
|
export def JobStatus(label: string)
|
|
var msg = gettext('Jobs still running:')
|
|
EchoMsg($'{msg} {len(GetRunningJobs(label))}', label)
|
|
enddef
|
|
|
|
export def StopJobs(label: string)
|
|
for job in GetRunningJobs(label)
|
|
job_stop(job)
|
|
endfor
|
|
|
|
EchoMsg(gettext('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 = gettext('Typeset')
|
|
): bool
|
|
var bufname = bufname(name)
|
|
|
|
if empty(bufname)
|
|
EchoErr(gettext('Please save the buffer first.'), label)
|
|
return false
|
|
endif
|
|
|
|
var efm = getbufvar(bufnr(bufname), "&efm")
|
|
var rootpath = FindRootDocument(bufname)
|
|
|
|
return Typeset('ConTeXt', Cmd, rootpath, efm, env)
|
|
enddef
|
|
# }}}
|
|
|
|
# vim: sw=2 fdm=marker
|