complete.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import {
  2. CompletionContext,
  3. CompletionResult,
  4. CompletionSource,
  5. ifIn,
  6. } from '@codemirror/autocomplete'
  7. import { customEndCompletions } from './completions/environments'
  8. import { customCommandCompletions } from './completions/doc-commands'
  9. import { customEnvironmentCompletions } from './completions/doc-environments'
  10. import { Completions } from './completions/types'
  11. import { buildReferenceCompletions } from './completions/references'
  12. import { buildPackageCompletions } from './completions/packages'
  13. import { buildLabelCompletions } from './completions/labels'
  14. import { buildIncludeCompletions } from './completions/include'
  15. import { buildBibliographyStyleCompletions } from './completions/bibliography-styles'
  16. import { buildClassCompletions } from './completions/classes'
  17. import { buildAllCompletions } from './completions'
  18. import {
  19. ifInType,
  20. cursorIsAtBeginEnvironment,
  21. cursorIsAtEndEnvironment,
  22. } from '../../utils/tree-query'
  23. function blankCompletions(): Completions {
  24. return {
  25. bibliographies: [],
  26. bibliographyStyles: [],
  27. classes: [],
  28. commands: [],
  29. graphics: [],
  30. includes: [],
  31. labels: [],
  32. packages: [],
  33. references: [],
  34. }
  35. }
  36. export function getCompletionMatches(context: CompletionContext) {
  37. // NOTE: [^\\] is needed to match commands inside the parameters of other commands
  38. const matchBefore = context.explicit
  39. ? context.matchBefore(/(?:^|\\)[^\\]*(\[[^\]]*])?[^\\]*/) // don't require a backslash if opening on explicit "startCompletion" keypress
  40. : context.matchBefore(/\\?\\[^\\]*(\[[^\]]*])?[^\\]*/)
  41. if (!matchBefore) {
  42. return null
  43. }
  44. // ignore some matches when not opening on explicit "startCompletion" keypress
  45. if (!context.explicit) {
  46. // ignore matches that end with two backslashes. \\ shouldn't show the autocomplete as it's used for line break.
  47. if (/\\\\$/.test(matchBefore.text)) {
  48. return null
  49. }
  50. // ignore matches that end with whitespace, unless after a comma
  51. // e.g. \item with a trailing space shouldn't show the autocomplete.
  52. if (/[^,\s]\s+$/.test(matchBefore.text)) {
  53. return null
  54. }
  55. }
  56. const multipleArgumentMatcher =
  57. /^(?<before>\\(?<command>\w+)\*?(?<arguments>(\[[^\]]*?]|\{[^}]*?})+)?{)(?<existing>([^}]+\s*,\s*)+)?(?<prefix>[^}]+)?$/
  58. // If this is a command with multiple comma-separated arguments, show deduplicated available completions
  59. const match = matchBefore.text.match(multipleArgumentMatcher)
  60. return { match, matchBefore }
  61. }
  62. export function getCompletionDetails(
  63. match: RegExpMatchArray,
  64. matchBefore: {
  65. from: number
  66. to: number
  67. text: string
  68. }
  69. ) {
  70. let { before, command, existing } = match.groups as {
  71. before?: string
  72. command: string
  73. existing?: string
  74. }
  75. command = command.toLowerCase()
  76. const existingKeys = existing
  77. ? existing
  78. .split(',')
  79. .map(key => key.trim())
  80. .filter(Boolean)
  81. : []
  82. const from =
  83. matchBefore.from + (before?.length || 0) + (existing?.length || 0)
  84. const validFor = /[^}\s]*/
  85. return { command, existingKeys, from, validFor }
  86. }
  87. export type CompletionBuilderOptions = {
  88. context: CompletionContext
  89. completions: Completions
  90. match: RegExpMatchArray
  91. matchBefore: { from: number; to: number; text: string }
  92. existingKeys: string[]
  93. from: number
  94. validFor: RegExp
  95. before: string
  96. }
  97. export const makeArgumentCompletionSource = (
  98. ifInSpec: string[],
  99. builder: (builderOptions: CompletionBuilderOptions) => CompletionResult | null
  100. ): CompletionSource => {
  101. const completionSource: CompletionSource = (context: CompletionContext) => {
  102. const completionMatches = getCompletionMatches(context)
  103. if (!completionMatches) {
  104. return null
  105. }
  106. const completions: Completions = blankCompletions()
  107. const { match, matchBefore } = completionMatches
  108. if (!match) {
  109. return null
  110. }
  111. const { before } = match.groups as {
  112. before: string
  113. }
  114. const { existingKeys, from, validFor } = getCompletionDetails(
  115. match,
  116. matchBefore
  117. )
  118. return builder({
  119. completions,
  120. context,
  121. match,
  122. matchBefore,
  123. before,
  124. existingKeys,
  125. from,
  126. validFor,
  127. })
  128. }
  129. return ifIn(ifInSpec, completionSource)
  130. }
  131. export const bibKeyArgumentCompletionSource: CompletionSource =
  132. makeArgumentCompletionSource(
  133. ['BibKeyArgument'],
  134. ({ completions, context, from, validFor, existingKeys }) => {
  135. buildReferenceCompletions(completions, context)
  136. return {
  137. from,
  138. validFor,
  139. options: completions.references.filter(
  140. item => !existingKeys.includes(item.label)
  141. ),
  142. }
  143. }
  144. )
  145. export const refArgumentCompletionSource: CompletionSource =
  146. makeArgumentCompletionSource(
  147. ['RefArgument'],
  148. ({ completions, context, from, validFor, existingKeys }) => {
  149. buildLabelCompletions(completions, context)
  150. return {
  151. from,
  152. validFor,
  153. options: completions.labels.filter(
  154. item => !existingKeys.includes(item.label)
  155. ),
  156. }
  157. }
  158. )
  159. export const packageArgumentCompletionSource: CompletionSource =
  160. makeArgumentCompletionSource(
  161. ['PackageArgument'],
  162. ({ completions, context, from, validFor, existingKeys }) => {
  163. buildPackageCompletions(completions, context)
  164. return {
  165. from,
  166. validFor,
  167. options: completions.packages.filter(
  168. item => !existingKeys.includes(item.label)
  169. ),
  170. }
  171. }
  172. )
  173. export const inputArgumentCompletionSource: CompletionSource =
  174. makeArgumentCompletionSource(
  175. ['InputArgument'],
  176. ({ completions, context, from }) => {
  177. buildIncludeCompletions(completions, context)
  178. return {
  179. from,
  180. validFor: /^[^}]*/,
  181. options: completions.includes,
  182. }
  183. }
  184. )
  185. export const includeArgumentCompletionSource: CompletionSource =
  186. makeArgumentCompletionSource(
  187. ['IncludeArgument'],
  188. ({ completions, context, from }) => {
  189. buildIncludeCompletions(completions, context)
  190. return {
  191. from,
  192. validFor: /^[^}]*/,
  193. options: completions.includes,
  194. }
  195. }
  196. )
  197. export const includeGraphicsArgumentCompletionSource: CompletionSource =
  198. makeArgumentCompletionSource(
  199. ['IncludeGraphicsArgument'],
  200. ({ completions, context, from }) => {
  201. buildIncludeCompletions(completions, context)
  202. return {
  203. from,
  204. validFor: /^[^}]*/,
  205. options: completions.graphics,
  206. }
  207. }
  208. )
  209. export const environmentNameCompletionSource: CompletionSource =
  210. makeArgumentCompletionSource(
  211. ['EnvNameGroup'],
  212. ({ completions, context, matchBefore, before }) => {
  213. if (cursorIsAtBeginEnvironment(context.state, context.pos)) {
  214. buildAllCompletions(completions, context)
  215. return {
  216. from: matchBefore.from,
  217. validFor: /^\\begin{\S*/,
  218. options: [
  219. ...completions.commands,
  220. ...customEnvironmentCompletions(context),
  221. ],
  222. }
  223. } else if (cursorIsAtEndEnvironment(context.state, context.pos)) {
  224. return {
  225. from: matchBefore.from + before.length,
  226. validFor: /^[^}]*/,
  227. options: customEndCompletions(context),
  228. }
  229. } else {
  230. return null
  231. }
  232. }
  233. )
  234. export const documentClassArgumentCompletionSource: CompletionSource =
  235. makeArgumentCompletionSource(
  236. ['DocumentClassArgument'],
  237. ({ completions, from }) => {
  238. buildClassCompletions(completions)
  239. return {
  240. from,
  241. validFor: /^[^}]*/,
  242. options: completions.classes,
  243. }
  244. }
  245. )
  246. export const bibliographyArgumentCompletionSource: CompletionSource =
  247. makeArgumentCompletionSource(
  248. ['BibliographyArgument'],
  249. ({ completions, context, from }) => {
  250. buildIncludeCompletions(completions, context)
  251. return {
  252. from,
  253. validFor: /^[^}]*/,
  254. options: completions.bibliographies,
  255. }
  256. }
  257. )
  258. export const bibliographyStyleArgumentCompletionSource: CompletionSource =
  259. makeArgumentCompletionSource(
  260. ['BibliographyStyleArgument'],
  261. ({ completions, from }) => {
  262. buildBibliographyStyleCompletions(completions)
  263. return {
  264. from,
  265. validFor: /^[^}]*/,
  266. options: completions.bibliographyStyles,
  267. }
  268. }
  269. )
  270. export const argumentCompletionSources: CompletionSource[] = [
  271. bibKeyArgumentCompletionSource,
  272. refArgumentCompletionSource,
  273. packageArgumentCompletionSource,
  274. inputArgumentCompletionSource,
  275. includeArgumentCompletionSource,
  276. includeGraphicsArgumentCompletionSource,
  277. environmentNameCompletionSource,
  278. documentClassArgumentCompletionSource,
  279. bibliographyArgumentCompletionSource,
  280. bibliographyStyleArgumentCompletionSource,
  281. ]
  282. const commandCompletionSource = (context: CompletionContext) => {
  283. const completionMatches = getCompletionMatches(context)
  284. if (!completionMatches) {
  285. return null
  286. }
  287. const { match, matchBefore } = completionMatches
  288. if (match) {
  289. // We're already in a command argument, bail out
  290. return null
  291. }
  292. const completions: Completions = blankCompletions()
  293. buildAllCompletions(completions, context)
  294. // ensure that there's only one completion for each label
  295. const uniqueCommandCompletions = Array.from(
  296. new Map(
  297. [
  298. ...completions.commands,
  299. ...customCommandCompletions(context, completions.commands),
  300. ].map(completion => [completion.label, completion])
  301. ).values()
  302. )
  303. // Unknown commands
  304. const prefixMatcher = /^\\[^{\s]*$/
  305. const prefixMatch = matchBefore.text.match(prefixMatcher)
  306. if (prefixMatch) {
  307. return {
  308. from: matchBefore.from,
  309. validFor: prefixMatcher,
  310. options: [
  311. ...uniqueCommandCompletions,
  312. ...customEnvironmentCompletions(context),
  313. ],
  314. }
  315. }
  316. // anything else (no validFor)
  317. return {
  318. from: matchBefore.to,
  319. options: uniqueCommandCompletions,
  320. }
  321. }
  322. export const inCommandCompletionSource: CompletionSource = ifInType(
  323. '$CtrlSeq',
  324. context => {
  325. return context.explicit ? null : commandCompletionSource(context)
  326. }
  327. )
  328. export const explicitCommandCompletionSource: CompletionSource = context => {
  329. return context.explicit ? commandCompletionSource(context) : null
  330. }