complete.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  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 {
  10. customEnvironmentCompletions,
  11. findEnvironmentsInDoc,
  12. } from './completions/doc-environments'
  13. import { Completions } from './completions/types'
  14. import { buildReferenceCompletions } from './completions/references'
  15. import { buildPackageCompletions } from './completions/packages'
  16. import { buildLabelCompletions } from './completions/labels'
  17. import { buildIncludeCompletions } from './completions/include'
  18. import { buildBibliographyStyleCompletions } from './completions/bibliography-styles'
  19. import { buildClassCompletions } from './completions/classes'
  20. import { buildAllCompletions } from './completions'
  21. import {
  22. ifInType,
  23. cursorIsAtBeginEnvironment,
  24. cursorIsAtEndEnvironment,
  25. } from '../../utils/tree-query'
  26. import {
  27. applySnippet,
  28. extendOverUnpairedClosingBrace,
  29. } from './completions/apply'
  30. import { snippet } from './completions/data/environments'
  31. import { syntaxTree } from '@codemirror/language'
  32. function blankCompletions(): Completions {
  33. return {
  34. bibliographies: [],
  35. bibliographyStyles: [],
  36. classes: [],
  37. commands: [],
  38. graphics: [],
  39. includes: [],
  40. labels: [],
  41. packages: [],
  42. references: [],
  43. }
  44. }
  45. export function getCompletionMatches(context: CompletionContext) {
  46. // NOTE: [^\\] is needed to match commands inside the parameters of other commands
  47. const matchBefore = context.explicit
  48. ? context.matchBefore(/(?:^|\\)[^\\]*(\[[^\]]*])?[^\\]*/) // don't require a backslash if opening on explicit "startCompletion" keypress
  49. : context.matchBefore(/\\?\\[^\\]*(\[[^\]]*])?[^\\]*/)
  50. if (!matchBefore) {
  51. return null
  52. }
  53. // ignore some matches when not opening on explicit "startCompletion" keypress
  54. if (!context.explicit) {
  55. // ignore matches that end with two backslashes. \\ shouldn't show the autocomplete as it's used for line break.
  56. if (/\\\\$/.test(matchBefore.text)) {
  57. return null
  58. }
  59. // ignore matches that end with whitespace, unless after a comma
  60. // e.g. \item with a trailing space shouldn't show the autocomplete.
  61. if (/[^,\s]\s+$/.test(matchBefore.text)) {
  62. return null
  63. }
  64. }
  65. const multipleArgumentMatcher =
  66. /^(?<before>\\(?<command>\w+)\*?(?<arguments>(\[[^\]]*?]|\{[^}]*?})+)?{)(?<existing>([^}]+\s*,\s*)+)?(?<prefix>[^}]+)?$/
  67. // If this is a command with multiple comma-separated arguments, show deduplicated available completions
  68. const match = matchBefore.text.match(multipleArgumentMatcher)
  69. return { match, matchBefore }
  70. }
  71. export function getCompletionDetails(
  72. match: RegExpMatchArray,
  73. matchBefore: {
  74. from: number
  75. to: number
  76. text: string
  77. }
  78. ) {
  79. let { before, command, existing } = match.groups as {
  80. before?: string
  81. command: string
  82. existing?: string
  83. }
  84. command = command.toLowerCase()
  85. const existingKeys = existing
  86. ? existing
  87. .split(',')
  88. .map(key => key.trim())
  89. .filter(Boolean)
  90. : []
  91. const from =
  92. matchBefore.from + (before?.length || 0) + (existing?.length || 0)
  93. const validFor = /[^}\s]*/
  94. return { command, existingKeys, from, validFor }
  95. }
  96. export type CompletionBuilderOptions = {
  97. context: CompletionContext
  98. completions: Completions
  99. match: RegExpMatchArray
  100. matchBefore: { from: number; to: number; text: string }
  101. existingKeys: string[]
  102. from: number
  103. validFor: RegExp
  104. before: string
  105. }
  106. export const makeArgumentCompletionSource = (
  107. ifInSpec: string[],
  108. builder: (builderOptions: CompletionBuilderOptions) => CompletionResult | null
  109. ): CompletionSource => {
  110. const completionSource: CompletionSource = (context: CompletionContext) => {
  111. const completionMatches = getCompletionMatches(context)
  112. if (!completionMatches) {
  113. return null
  114. }
  115. const completions: Completions = blankCompletions()
  116. const { match, matchBefore } = completionMatches
  117. if (!match) {
  118. return null
  119. }
  120. const { before } = match.groups as {
  121. before: string
  122. }
  123. const { existingKeys, from, validFor } = getCompletionDetails(
  124. match,
  125. matchBefore
  126. )
  127. return builder({
  128. completions,
  129. context,
  130. match,
  131. matchBefore,
  132. before,
  133. existingKeys,
  134. from,
  135. validFor,
  136. })
  137. }
  138. return ifIn(ifInSpec, completionSource)
  139. }
  140. export const bibKeyArgumentCompletionSource: CompletionSource =
  141. makeArgumentCompletionSource(
  142. ['BibKeyArgument'],
  143. ({ completions, context, from, validFor, existingKeys }) => {
  144. buildReferenceCompletions(completions, context)
  145. return {
  146. from,
  147. validFor,
  148. options: completions.references.filter(
  149. item => !existingKeys.includes(item.label)
  150. ),
  151. }
  152. }
  153. )
  154. export const refArgumentCompletionSource: CompletionSource =
  155. makeArgumentCompletionSource(
  156. ['RefArgument'],
  157. ({ completions, context, from, validFor, existingKeys }) => {
  158. buildLabelCompletions(completions, context)
  159. return {
  160. from,
  161. validFor,
  162. options: completions.labels.filter(
  163. item => !existingKeys.includes(item.label)
  164. ),
  165. }
  166. }
  167. )
  168. export const packageArgumentCompletionSource: CompletionSource =
  169. makeArgumentCompletionSource(
  170. ['PackageArgument'],
  171. ({ completions, context, from, validFor, existingKeys }) => {
  172. buildPackageCompletions(completions, context)
  173. return {
  174. from,
  175. validFor,
  176. options: completions.packages.filter(
  177. item => !existingKeys.includes(item.label)
  178. ),
  179. }
  180. }
  181. )
  182. export const inputArgumentCompletionSource: CompletionSource =
  183. makeArgumentCompletionSource(
  184. ['InputArgument'],
  185. ({ completions, context, from }) => {
  186. buildIncludeCompletions(completions, context)
  187. return {
  188. from,
  189. validFor: /^[^}]*/,
  190. options: completions.includes,
  191. }
  192. }
  193. )
  194. export const includeArgumentCompletionSource: CompletionSource =
  195. makeArgumentCompletionSource(
  196. ['IncludeArgument'],
  197. ({ completions, context, from }) => {
  198. buildIncludeCompletions(completions, context)
  199. return {
  200. from,
  201. validFor: /^[^}]*/,
  202. options: completions.includes,
  203. }
  204. }
  205. )
  206. export const includeGraphicsArgumentCompletionSource: CompletionSource =
  207. makeArgumentCompletionSource(
  208. ['IncludeGraphicsArgument'],
  209. ({ completions, context, from }) => {
  210. buildIncludeCompletions(completions, context)
  211. return {
  212. from,
  213. validFor: /^[^}]*/,
  214. options: completions.graphics,
  215. }
  216. }
  217. )
  218. export const environmentNameCompletionSource: CompletionSource =
  219. makeArgumentCompletionSource(
  220. ['EnvNameGroup'],
  221. ({ completions, context, matchBefore, before }) => {
  222. if (cursorIsAtBeginEnvironment(context.state, context.pos)) {
  223. buildAllCompletions(completions, context)
  224. return {
  225. from: matchBefore.from,
  226. validFor: /^\\begin{\S*/,
  227. options: [
  228. ...completions.commands,
  229. ...customEnvironmentCompletions(context),
  230. ],
  231. }
  232. } else if (cursorIsAtEndEnvironment(context.state, context.pos)) {
  233. return {
  234. from: matchBefore.from + before.length,
  235. validFor: /^[^}]*/,
  236. options: customEndCompletions(context),
  237. }
  238. } else {
  239. return null
  240. }
  241. }
  242. )
  243. export const documentClassArgumentCompletionSource: CompletionSource =
  244. makeArgumentCompletionSource(
  245. ['DocumentClassArgument'],
  246. ({ completions, from }) => {
  247. buildClassCompletions(completions)
  248. return {
  249. from,
  250. validFor: /^[^}]*/,
  251. options: completions.classes,
  252. }
  253. }
  254. )
  255. export const bibliographyArgumentCompletionSource: CompletionSource =
  256. makeArgumentCompletionSource(
  257. ['BibliographyArgument'],
  258. ({ completions, context, from }) => {
  259. buildIncludeCompletions(completions, context)
  260. return {
  261. from,
  262. validFor: /^[^}]*/,
  263. options: completions.bibliographies,
  264. }
  265. }
  266. )
  267. export const bibliographyStyleArgumentCompletionSource: CompletionSource =
  268. makeArgumentCompletionSource(
  269. ['BibliographyStyleArgument'],
  270. ({ completions, from }) => {
  271. buildBibliographyStyleCompletions(completions)
  272. return {
  273. from,
  274. validFor: /^[^}]*/,
  275. options: completions.bibliographyStyles,
  276. }
  277. }
  278. )
  279. export const argumentCompletionSources: CompletionSource[] = [
  280. bibKeyArgumentCompletionSource,
  281. refArgumentCompletionSource,
  282. packageArgumentCompletionSource,
  283. inputArgumentCompletionSource,
  284. includeArgumentCompletionSource,
  285. includeGraphicsArgumentCompletionSource,
  286. environmentNameCompletionSource,
  287. documentClassArgumentCompletionSource,
  288. bibliographyArgumentCompletionSource,
  289. bibliographyStyleArgumentCompletionSource,
  290. ]
  291. const commandCompletionSource = (context: CompletionContext) => {
  292. const completionMatches = getCompletionMatches(context)
  293. if (!completionMatches) {
  294. return null
  295. }
  296. const { match, matchBefore } = completionMatches
  297. if (match) {
  298. // We're already in a command argument, bail out
  299. return null
  300. }
  301. const completions: Completions = blankCompletions()
  302. buildAllCompletions(completions, context)
  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. ...completions.commands,
  312. ...customCommandCompletions(context, completions.commands),
  313. ...customEnvironmentCompletions(context),
  314. ],
  315. }
  316. }
  317. // anything else (no validFor)
  318. return {
  319. from: matchBefore.to,
  320. options: [
  321. ...completions.commands,
  322. ...customCommandCompletions(context, completions.commands),
  323. ],
  324. }
  325. }
  326. export const inCommandCompletionSource: CompletionSource = ifInType(
  327. '$CtrlSeq',
  328. context => {
  329. return context.explicit ? null : commandCompletionSource(context)
  330. }
  331. )
  332. export const explicitCommandCompletionSource: CompletionSource = context => {
  333. return context.explicit ? commandCompletionSource(context) : null
  334. }
  335. /**
  336. * An additional completion source that handles two situations:
  337. *
  338. * 1. Typing the environment name within an already-complete `\begin{…}` command.
  339. * 2. After typing the closing brace of a complete `\begin{foo}` command, where the environment
  340. * isn't previously known, leaving the cursor after the closing brace.
  341. */
  342. export const beginEnvironmentCompletionSource: CompletionSource = context => {
  343. const beginEnvToken = context.tokenBefore(['BeginEnv'])
  344. if (!beginEnvToken) {
  345. return null
  346. }
  347. const beginEnv = syntaxTree(context.state).resolveInner(
  348. beginEnvToken.from,
  349. 1
  350. ).parent
  351. if (!beginEnv?.type.is('BeginEnv')) {
  352. return null
  353. }
  354. const envNameGroup = beginEnv.getChild('EnvNameGroup')
  355. if (!envNameGroup) {
  356. return null
  357. }
  358. const envName = envNameGroup.getChild('$EnvName')
  359. if (!envName) {
  360. return null
  361. }
  362. const name = context.state.sliceDoc(envName.from, envName.to)
  363. // if not directly after `\begin{…}`, exclude known environments
  364. if (context.pos !== envNameGroup.to) {
  365. const existingEnvironmentNames = findEnvironmentsInDoc(context)
  366. if (existingEnvironmentNames.has(name)) {
  367. return null
  368. }
  369. }
  370. const completion = {
  371. label: `\\begin{${name}} …`,
  372. apply: applySnippet(snippet(name)),
  373. extend: extendOverUnpairedClosingBrace,
  374. boost: -99,
  375. }
  376. return {
  377. from: beginEnvToken.from,
  378. options: [completion],
  379. }
  380. }