typeset.vim 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. vim9script
  2. # Language: Generic TeX typesetting engine
  3. # Maintainer: Nicola Vitacolonna <nvitacolonna@gmail.com>
  4. # Latest Revision: 2022 Aug 12
  5. # Constants and helpers {{{
  6. const SLASH = !exists("+shellslash") || &shellslash ? '/' : '\'
  7. def Echo(msg: string, mode: string, label: string)
  8. redraw
  9. echo "\r"
  10. execute 'echohl' mode
  11. echomsg printf('[%s] %s', label, msg)
  12. echohl None
  13. enddef
  14. def EchoMsg(msg: string, label = 'Notice')
  15. Echo(msg, 'ModeMsg', label)
  16. enddef
  17. def EchoWarn(msg: string, label = 'Warning')
  18. Echo(msg, 'WarningMsg', label)
  19. enddef
  20. def EchoErr(msg: string, label = 'Error')
  21. Echo(msg, 'ErrorMsg', label)
  22. enddef
  23. # }}}
  24. # Track jobs {{{
  25. var running_jobs = {} # Dictionary of job IDs of jobs currently executing
  26. def AddJob(label: string, j: job)
  27. if !has_key(running_jobs, label)
  28. running_jobs[label] = []
  29. endif
  30. add(running_jobs[label], j)
  31. enddef
  32. def RemoveJob(label: string, j: job)
  33. if has_key(running_jobs, label) && index(running_jobs[label], j) != -1
  34. remove(running_jobs[label], index(running_jobs[label], j))
  35. endif
  36. enddef
  37. def GetRunningJobs(label: string): list<job>
  38. return has_key(running_jobs, label) ? running_jobs[label] : []
  39. enddef
  40. # }}}
  41. # Callbacks {{{
  42. def ProcessOutput(qfid: number, wd: string, efm: string, ch: channel, msg: string)
  43. # Make sure the quickfix list still exists
  44. if getqflist({'id': qfid}).id != qfid
  45. EchoErr("Quickfix list not found, stopping the job")
  46. call job_stop(ch_getjob(ch))
  47. return
  48. endif
  49. # Make sure the working directory is correct
  50. silent execute "lcd" wd
  51. setqflist([], 'a', {'id': qfid, 'lines': [msg], 'efm': efm})
  52. silent lcd -
  53. enddef
  54. def CloseCb(ch: channel)
  55. job_status(ch_getjob(ch)) # Trigger exit_cb's callback
  56. enddef
  57. def ExitCb(label: string, jobid: job, exitStatus: number)
  58. RemoveJob(label, jobid)
  59. if exitStatus == 0
  60. botright cwindow
  61. EchoMsg('Success!', label)
  62. elseif exitStatus < 0
  63. EchoWarn('Job terminated', label)
  64. else
  65. botright copen
  66. wincmd p
  67. EchoWarn('There are errors.', label)
  68. endif
  69. enddef
  70. # }}}
  71. # Create a new empty quickfix list at the end of the stack and return its id {{{
  72. def NewQuickfixList(path: string): number
  73. if setqflist([], ' ', {'nr': '$', 'title': path}) == -1
  74. return -1
  75. endif
  76. return getqflist({'nr': '$', 'id': 0}).id
  77. enddef
  78. # }}}
  79. # Public interface {{{
  80. # When a TeX document is split into several source files, each source file
  81. # may contain a "magic line" specifiying the "root" file, e.g.:
  82. #
  83. # % !TEX root = main.tex
  84. #
  85. # Using this line, Vim can know which file to typeset even if the current
  86. # buffer is different from main.tex.
  87. #
  88. # This function searches for the magic line in the first ten lines of the
  89. # given buffer, and returns the full path of the root document.
  90. #
  91. # NOTE: the value of "% !TEX root" *must* be a relative path.
  92. export def FindRootDocument(bufname: string = bufname("%")): string
  93. const bufnr = bufnr(bufname)
  94. if !bufexists(bufnr)
  95. return bufname
  96. endif
  97. var rootpath = fnamemodify(bufname(bufnr), ':p')
  98. # Search for magic line `% !TEX root = ...` in the first ten lines
  99. const header = getbufline(bufnr, 1, 10)
  100. const idx = match(header, '^\s*%\s\+!TEX\s\+root\s*=\s*\S')
  101. if idx > -1
  102. const main = matchstr(header[idx], '!TEX\s\+root\s*=\s*\zs.*$')
  103. rootpath = simplify(fnamemodify(rootpath, ":h") .. SLASH .. main)
  104. endif
  105. return rootpath
  106. enddef
  107. export def LogPath(bufname: string): string
  108. const logfile = FindRootDocument(bufname)
  109. return fnamemodify(logfile, ":r") .. ".log"
  110. enddef
  111. # Typeset the specified path
  112. #
  113. # Parameters:
  114. # label: a descriptive string used in messages to identify the kind of job
  115. # Cmd: a function that takes the path of a document and returns the typesetting command
  116. # path: the path of the document to be typeset. To avoid ambiguities, pass a *full* path.
  117. # efm: the error format string to parse the output of the command.
  118. # env: environment variables for the process (passed to job_start())
  119. #
  120. # Returns:
  121. # true if the job is started successfully;
  122. # false otherwise.
  123. export def Typeset(
  124. label: string,
  125. Cmd: func(string): list<string>,
  126. path: string,
  127. efm: string,
  128. env: dict<string> = {}
  129. ): bool
  130. var fp = fnamemodify(path, ":p")
  131. var wd = fnamemodify(fp, ":h")
  132. var qfid = NewQuickfixList(fp)
  133. if qfid == -1
  134. EchoErr('Could not create quickfix list', label)
  135. return false
  136. endif
  137. if !filereadable(fp)
  138. EchoErr(printf('File not readable: %s', fp), label)
  139. return false
  140. endif
  141. var jobid = job_start(Cmd(path), {
  142. env: env,
  143. cwd: wd,
  144. in_io: "null",
  145. callback: (c, m) => ProcessOutput(qfid, wd, efm, c, m),
  146. close_cb: CloseCb,
  147. exit_cb: (j, e) => ExitCb(label, j, e),
  148. })
  149. if job_status(jobid) ==# "fail"
  150. EchoErr("Failed to start job", label)
  151. return false
  152. endif
  153. AddJob(label, jobid)
  154. EchoMsg('Typesetting...', label)
  155. return true
  156. enddef
  157. export def JobStatus(label: string)
  158. EchoMsg('Jobs still running: ' .. string(len(GetRunningJobs(label))), label)
  159. enddef
  160. export def StopJobs(label: string)
  161. for job in GetRunningJobs(label)
  162. job_stop(job)
  163. endfor
  164. EchoMsg('Done.', label)
  165. enddef
  166. # Typeset the specified buffer
  167. #
  168. # Parameters:
  169. # name: a buffer's name. this may be empty to indicate the current buffer.
  170. # cmd: a function that takes the path of a document and returns the typesetting command
  171. # label: a descriptive string used in messages to identify the kind of job
  172. # env: environment variables for the process (passed to job_start())
  173. #
  174. # Returns:
  175. # true if the job is started successfully;
  176. # false otherwise.
  177. export def TypesetBuffer(
  178. name: string,
  179. Cmd: func(string): list<string>,
  180. env = {},
  181. label = 'Typeset'
  182. ): bool
  183. const bufname = bufname(name)
  184. if empty(bufname)
  185. EchoErr('Please save the buffer first.', label)
  186. return false
  187. endif
  188. const efm = getbufvar(bufnr(bufname), "&efm")
  189. const rootpath = FindRootDocument(bufname)
  190. return Typeset('ConTeXt', Cmd, rootpath, efm, env)
  191. enddef
  192. # }}}
  193. # vim: sw=2 fdm=marker