| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- import {
- CompletionContext,
- CompletionResult,
- CompletionSource,
- ifIn,
- } from '@codemirror/autocomplete'
- import { customEndCompletions } from './completions/environments'
- import { customCommandCompletions } from './completions/doc-commands'
- import {
- customEnvironmentCompletions,
- findEnvironmentsInDoc,
- } 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'
- import { sendMBSampled } from '@/infrastructure/event-tracking'
- import getMeta from '@/utils/meta'
- 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 ? splitExistingKeys(existing) : []
- 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)
- }
- const splitExistingKeys = (text: string) =>
- text
- .split(',')
- .map(key => key.trim())
- .filter(Boolean)
- export const makeMultipleArgumentCompletionSource = (
- ifInSpec: string[],
- builder: (
- builderOptions: Pick<
- CompletionBuilderOptions,
- 'completions' | 'context' | 'existingKeys' | 'from' | 'validFor'
- >
- ) => ReturnType<CompletionSource>
- ): CompletionSource => {
- const completionSource: CompletionSource = (context: CompletionContext) => {
- const token = context.tokenBefore(ifInSpec)
- if (!token) {
- return null
- }
- // match multiple comma-separated arguments, up to the last separator
- const existing = token.text.match(/^\{(.+\s*,\s*)?.*$/)?.[1] ?? ''
- return builder({
- completions: blankCompletions(),
- context,
- existingKeys: splitExistingKeys(existing),
- from: token.from + 1 + existing.length,
- validFor: /[^}\s]*/,
- })
- }
- return ifIn(ifInSpec, completionSource)
- }
- export const bibKeyArgumentCompletionSource: CompletionSource =
- makeMultipleArgumentCompletionSource(
- ['BibKeyArgument'],
- ({ completions, context, from, validFor, existingKeys }) => {
- buildReferenceCompletions(completions, context)
- return {
- from,
- validFor,
- options: completions.references.filter(
- item => !existingKeys.includes(item.label)
- ),
- }
- }
- )
- const debouncedCounter = (
- debounceTime: number
- ): {
- debounceTime: number
- counter: number
- increment: () => void
- reset: () => void
- } => {
- let timeoutId = 0
- let _counter = 0
- return {
- debounceTime,
- get counter() {
- return _counter
- },
- increment() {
- if (timeoutId !== 0) {
- clearTimeout(timeoutId)
- }
- timeoutId = window.setTimeout(() => {
- _counter += 1
- timeoutId = 0
- }, debounceTime)
- },
- reset() {
- clearTimeout(timeoutId)
- _counter = 0
- timeoutId = 0
- },
- }
- }
- const CITE_ANALYTICS_REPORT_TIMEOUT = 4000
- const analyticsSourceBuilder = (debounceTimes: number[]) => {
- const user = getMeta('ol-user')
- let timeoutId = 0
- const counters = debounceTimes.map(debounceTime => {
- if (debounceTime >= CITE_ANALYTICS_REPORT_TIMEOUT) {
- throw new Error(
- `Debounce time ${debounceTime} is greater than the report timeout ${CITE_ANALYTICS_REPORT_TIMEOUT}`
- )
- }
- return debouncedCounter(debounceTime)
- })
- const incrementCounters = () => {
- counters.forEach(counter => counter.increment())
- }
- const resetCounters = () => {
- counters.forEach(counter => counter.reset())
- }
- const delayedReport = () => {
- if (timeoutId !== 0) {
- clearTimeout(timeoutId)
- }
- timeoutId = window.setTimeout(() => {
- const result: Record<string, number | boolean | undefined> = {
- mendeley: Boolean(
- user?.features?.mendeley && user?.refProviders?.mendeley
- ),
- zotero: Boolean(user?.features?.zotero && user?.refProviders?.zotero),
- }
- counters.forEach(debouncedCounter => {
- result[`${debouncedCounter.debounceTime}ms`] = debouncedCounter.counter
- })
- sendMBSampled('cite-key-search', result, 0.05)
- timeoutId = 0
- resetCounters()
- }, CITE_ANALYTICS_REPORT_TIMEOUT)
- }
- return () => {
- incrementCounters()
- delayedReport()
- return null
- }
- }
- const citeKeyAnalyticsSource = makeMultipleArgumentCompletionSource(
- ['BibKeyArgument'],
- analyticsSourceBuilder([0, 100, 250, 500])
- )
- export const refArgumentCompletionSource: CompletionSource =
- makeMultipleArgumentCompletionSource(
- ['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 =
- makeMultipleArgumentCompletionSource(
- ['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,
- citeKeyAnalyticsSource,
- ]
- 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
- }
- const envName = envNameGroup.getChild('$EnvName')
- if (!envName) {
- return null
- }
- const name = context.state.sliceDoc(envName.from, envName.to)
- // if not directly after `\begin{…}`, exclude known environments
- if (context.pos !== envNameGroup.to) {
- const existingEnvironmentNames = findEnvironmentsInDoc(context)
- if (existingEnvironmentNames.has(name)) {
- return null
- }
- }
- const completion = {
- label: `\\begin{${name}} …`,
- apply: applySnippet(snippet(name)),
- extend: extendOverUnpairedClosingBrace,
- boost: -99,
- }
- return {
- from: beginEnvToken.from,
- options: [completion],
- }
- }
|