| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- import {
- CompletionContext,
- CompletionResult,
- CompletionSource,
- ifIn,
- } from '@codemirror/autocomplete'
- import { customEndCompletions } from './completions/environments'
- import { customCommandCompletions } from './completions/doc-commands'
- import { customEnvironmentCompletions } from './completions/doc-environments'
- import { Completions } from './completions/types'
- import { buildReferenceCompletions } from './completions/references'
- import { buildPackageCompletions } from './completions/packages'
- import { buildLabelCompletions } from './completions/labels'
- import { buildIncludeCompletions } from './completions/include'
- import { buildBibliographyStyleCompletions } from './completions/bibliography-styles'
- import { buildClassCompletions } from './completions/classes'
- import { buildAllCompletions } from './completions'
- import {
- ifInType,
- cursorIsAtBeginEnvironment,
- cursorIsAtEndEnvironment,
- } from '../../utils/tree-query'
- import {
- applySnippet,
- extendOverUnpairedClosingBrace,
- } from './completions/apply'
- import { snippet } from './completions/data/environments'
- import { syntaxTree } from '@codemirror/language'
- function blankCompletions(): Completions {
- return {
- bibliographies: [],
- bibliographyStyles: [],
- classes: [],
- commands: [],
- graphics: [],
- includes: [],
- labels: [],
- packages: [],
- references: [],
- }
- }
- export function getCompletionMatches(context: CompletionContext) {
- // NOTE: [^\\] is needed to match commands inside the parameters of other commands
- const matchBefore = context.explicit
- ? context.matchBefore(/(?:^|\\)[^\\]*(\[[^\]]*])?[^\\]*/) // don't require a backslash if opening on explicit "startCompletion" keypress
- : context.matchBefore(/\\?\\[^\\]*(\[[^\]]*])?[^\\]*/)
- if (!matchBefore) {
- return null
- }
- // ignore some matches when not opening on explicit "startCompletion" keypress
- if (!context.explicit) {
- // ignore matches that end with two backslashes. \\ shouldn't show the autocomplete as it's used for line break.
- if (/\\\\$/.test(matchBefore.text)) {
- return null
- }
- // ignore matches that end with whitespace, unless after a comma
- // e.g. \item with a trailing space shouldn't show the autocomplete.
- if (/[^,\s]\s+$/.test(matchBefore.text)) {
- return null
- }
- }
- const multipleArgumentMatcher =
- /^(?<before>\\(?<command>\w+)\*?(?<arguments>(\[[^\]]*?]|\{[^}]*?})+)?{)(?<existing>([^}]+\s*,\s*)+)?(?<prefix>[^}]+)?$/
- // If this is a command with multiple comma-separated arguments, show deduplicated available completions
- const match = matchBefore.text.match(multipleArgumentMatcher)
- return { match, matchBefore }
- }
- export function getCompletionDetails(
- match: RegExpMatchArray,
- matchBefore: {
- from: number
- to: number
- text: string
- }
- ) {
- let { before, command, existing } = match.groups as {
- before?: string
- command: string
- existing?: string
- }
- command = command.toLowerCase()
- const existingKeys = existing
- ? existing
- .split(',')
- .map(key => key.trim())
- .filter(Boolean)
- : []
- const from =
- matchBefore.from + (before?.length || 0) + (existing?.length || 0)
- const validFor = /[^}\s]*/
- return { command, existingKeys, from, validFor }
- }
- export type CompletionBuilderOptions = {
- context: CompletionContext
- completions: Completions
- match: RegExpMatchArray
- matchBefore: { from: number; to: number; text: string }
- existingKeys: string[]
- from: number
- validFor: RegExp
- before: string
- }
- export const makeArgumentCompletionSource = (
- ifInSpec: string[],
- builder: (builderOptions: CompletionBuilderOptions) => CompletionResult | null
- ): CompletionSource => {
- const completionSource: CompletionSource = (context: CompletionContext) => {
- const completionMatches = getCompletionMatches(context)
- if (!completionMatches) {
- return null
- }
- const completions: Completions = blankCompletions()
- const { match, matchBefore } = completionMatches
- if (!match) {
- return null
- }
- const { before } = match.groups as {
- before: string
- }
- const { existingKeys, from, validFor } = getCompletionDetails(
- match,
- matchBefore
- )
- return builder({
- completions,
- context,
- match,
- matchBefore,
- before,
- existingKeys,
- from,
- validFor,
- })
- }
- return ifIn(ifInSpec, completionSource)
- }
- export const bibKeyArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['BibKeyArgument'],
- ({ completions, context, from, validFor, existingKeys }) => {
- buildReferenceCompletions(completions, context)
- return {
- from,
- validFor,
- options: completions.references.filter(
- item => !existingKeys.includes(item.label)
- ),
- }
- }
- )
- export const refArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['RefArgument'],
- ({ completions, context, from, validFor, existingKeys }) => {
- buildLabelCompletions(completions, context)
- return {
- from,
- validFor,
- options: completions.labels.filter(
- item => !existingKeys.includes(item.label)
- ),
- }
- }
- )
- export const packageArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['PackageArgument'],
- ({ completions, context, from, validFor, existingKeys }) => {
- buildPackageCompletions(completions, context)
- return {
- from,
- validFor,
- options: completions.packages.filter(
- item => !existingKeys.includes(item.label)
- ),
- }
- }
- )
- export const inputArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['InputArgument'],
- ({ completions, context, from }) => {
- buildIncludeCompletions(completions, context)
- return {
- from,
- validFor: /^[^}]*/,
- options: completions.includes,
- }
- }
- )
- export const includeArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['IncludeArgument'],
- ({ completions, context, from }) => {
- buildIncludeCompletions(completions, context)
- return {
- from,
- validFor: /^[^}]*/,
- options: completions.includes,
- }
- }
- )
- export const includeGraphicsArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['IncludeGraphicsArgument'],
- ({ completions, context, from }) => {
- buildIncludeCompletions(completions, context)
- return {
- from,
- validFor: /^[^}]*/,
- options: completions.graphics,
- }
- }
- )
- export const environmentNameCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['EnvNameGroup'],
- ({ completions, context, matchBefore, before }) => {
- if (cursorIsAtBeginEnvironment(context.state, context.pos)) {
- buildAllCompletions(completions, context)
- return {
- from: matchBefore.from,
- validFor: /^\\begin{\S*/,
- options: [
- ...completions.commands,
- ...customEnvironmentCompletions(context),
- ],
- }
- } else if (cursorIsAtEndEnvironment(context.state, context.pos)) {
- return {
- from: matchBefore.from + before.length,
- validFor: /^[^}]*/,
- options: customEndCompletions(context),
- }
- } else {
- return null
- }
- }
- )
- export const documentClassArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['DocumentClassArgument'],
- ({ completions, from }) => {
- buildClassCompletions(completions)
- return {
- from,
- validFor: /^[^}]*/,
- options: completions.classes,
- }
- }
- )
- export const bibliographyArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['BibliographyArgument'],
- ({ completions, context, from }) => {
- buildIncludeCompletions(completions, context)
- return {
- from,
- validFor: /^[^}]*/,
- options: completions.bibliographies,
- }
- }
- )
- export const bibliographyStyleArgumentCompletionSource: CompletionSource =
- makeArgumentCompletionSource(
- ['BibliographyStyleArgument'],
- ({ completions, from }) => {
- buildBibliographyStyleCompletions(completions)
- return {
- from,
- validFor: /^[^}]*/,
- options: completions.bibliographyStyles,
- }
- }
- )
- export const argumentCompletionSources: CompletionSource[] = [
- bibKeyArgumentCompletionSource,
- refArgumentCompletionSource,
- packageArgumentCompletionSource,
- inputArgumentCompletionSource,
- includeArgumentCompletionSource,
- includeGraphicsArgumentCompletionSource,
- environmentNameCompletionSource,
- documentClassArgumentCompletionSource,
- bibliographyArgumentCompletionSource,
- bibliographyStyleArgumentCompletionSource,
- ]
- const commandCompletionSource = (context: CompletionContext) => {
- const completionMatches = getCompletionMatches(context)
- if (!completionMatches) {
- return null
- }
- const { match, matchBefore } = completionMatches
- if (match) {
- // We're already in a command argument, bail out
- return null
- }
- const completions: Completions = blankCompletions()
- buildAllCompletions(completions, context)
- // Unknown commands
- const prefixMatcher = /^\\[^{\s]*$/
- const prefixMatch = matchBefore.text.match(prefixMatcher)
- if (prefixMatch) {
- return {
- from: matchBefore.from,
- validFor: prefixMatcher,
- options: [
- ...completions.commands,
- ...customCommandCompletions(context, completions.commands),
- ...customEnvironmentCompletions(context),
- ],
- }
- }
- // anything else (no validFor)
- return {
- from: matchBefore.to,
- options: [
- ...completions.commands,
- ...customCommandCompletions(context, completions.commands),
- ],
- }
- }
- export const inCommandCompletionSource: CompletionSource = ifInType(
- '$CtrlSeq',
- context => {
- return context.explicit ? null : commandCompletionSource(context)
- }
- )
- export const explicitCommandCompletionSource: CompletionSource = context => {
- return context.explicit ? commandCompletionSource(context) : null
- }
- /**
- * An additional completion source that handles two situations:
- *
- * 1. Typing the environment name within an already-complete `\begin{…}` command.
- * 2. After typing the closing brace of a complete `\begin{foo}` command, where the environment
- * isn't previously known, leaving the cursor after the closing brace.
- */
- export const beginEnvironmentCompletionSource: CompletionSource = context => {
- const beginEnvToken = context.tokenBefore(['BeginEnv'])
- if (!beginEnvToken) {
- return null
- }
- const beginEnv = syntaxTree(context.state).resolveInner(
- beginEnvToken.from,
- 1
- ).parent
- if (!beginEnv?.type.is('BeginEnv')) {
- return null
- }
- const envNameGroup = beginEnv.getChild('EnvNameGroup')
- if (!envNameGroup) {
- return null
- }
- // this completion source must only be active when the \begin command has a closing brace
- const closeBrace = envNameGroup.getChild('CloseBrace')
- if (!closeBrace) {
- return null
- }
- const envName = envNameGroup.getChild('$EnvName')
- if (!envName) {
- return null
- }
- const name = context.state.sliceDoc(envName.from, envName.to)
- const completion = {
- label: `\\begin{${name}} …`,
- apply: applySnippet(snippet(name)),
- extend: extendOverUnpairedClosingBrace,
- }
- return {
- from: beginEnvToken.from,
- options: [completion],
- }
- }
|