From c1ba83529c864f742db0bedb03e381217d311099 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Sun, 3 Nov 2024 13:46:52 +0100 Subject: [PATCH] Allow backspacing to the start of the completion for implicit completions enabled by the char before FIX: Backspacing to the start of the completed range will no longer close the completion tooltip when it was triggered implicitly by typing the character before that range. See https://discuss.codemirror.net/t/how-to-ensure-all-possible-completions-are-shown-again-after-backspacing-until-the-trigger-character/8766 --- src/index.ts | 2 +- src/state.ts | 46 +++++++++++++++++++++++----------------------- src/view.ts | 18 ++++++++++-------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/index.ts b/src/index.ts index f810fce..33b4d9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,7 +56,7 @@ const completionKeymapExt = Prec.highest(keymap.computeN([completionConfig], sta /// returns `null`. export function completionStatus(state: EditorState): null | "active" | "pending" { let cState = state.field(completionState, false) - return cState && cState.active.some(a => a.state == State.Pending) ? "pending" + return cState && cState.active.some(a => a.isPending) ? "pending" : cState && cState.active.some(a => a.state != State.Inactive) ? "active" : null } diff --git a/src/state.ts b/src/state.ts index 1165720..c05af7c 100644 --- a/src/state.ts +++ b/src/state.ts @@ -92,11 +92,11 @@ class CompletionDialog { conf: Required, didSetActive: boolean ): CompletionDialog | null { - if (prev && !didSetActive && active.some(s => s.state == State.Pending)) + if (prev && !didSetActive && active.some(s => s.isPending)) return prev.setDisabled() let options = sortOptions(active, state) if (!options.length) - return prev && active.some(a => a.state == State.Pending) ? prev.setDisabled() : null + return prev && active.some(a => a.isPending) ? prev.setDisabled() : null let selected = state.facet(completionConfig).selectOnOpen ? 0 : -1 if (prev && prev.selected != selected && prev.selected != -1) { let selectedValue = prev.options[prev.selected].completion @@ -147,10 +147,10 @@ export class CompletionState { if (tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) || !sameResults(active, this.active) || didSet) open = CompletionDialog.build(active, state, this.id, open, conf, didSet) - else if (open && open.disabled && !active.some(a => a.state == State.Pending)) + else if (open && open.disabled && !active.some(a => a.isPending)) open = null - if (!open && active.every(a => a.state != State.Pending) && active.some(a => a.hasResult())) + if (!open && active.every(a => !a.isPending) && active.some(a => a.hasResult())) active = active.map(a => a.hasResult() ? new ActiveSource(a.source, State.Inactive) : a) for (let effect of tr.effects) if (effect.is(setSelectedEffect)) open = open && open.setSelected(effect.value, this.id) @@ -165,8 +165,8 @@ export class CompletionState { function sameResults(a: readonly ActiveSource[], b: readonly ActiveSource[]) { if (a == b) return true for (let iA = 0, iB = 0;;) { - while (iA < a.length && !a[iA].hasResult) iA++ - while (iB < b.length && !b[iB].hasResult) iB++ + while (iA < a.length && !a[iA].hasResult()) iA++ + while (iB < b.length && !b[iB].hasResult()) iB++ let endA = iA == a.length, endB = iB == b.length if (endA || endB) return endA == endB if ((a[iA++] as ActiveResult).result != (b[iB++] as ActiveResult).result) return false @@ -191,7 +191,7 @@ function makeAttrs(id: string, selected: number) { const none: readonly any[] = [] -export const enum State { Inactive = 0, Pending = 1, Result = 2 } +export const enum State { Inactive = 0, Pending = 1, Result = 3 } export const enum UpdateType { None = 0, @@ -219,10 +219,12 @@ export function getUpdateType(tr: Transaction, conf: Required) export class ActiveSource { constructor(readonly source: CompletionSource, readonly state: State, - readonly explicitPos: number = -1) {} + readonly explicit: boolean = false) {} hasResult(): this is ActiveResult { return false } + get isPending() { return this.state == State.Pending } + update(tr: Transaction, conf: Required): ActiveSource { let type = getUpdateType(tr, conf), value: ActiveSource = this if ((type & UpdateType.Reset) || (type & UpdateType.ResetIfTouching) && this.touches(tr)) @@ -233,7 +235,7 @@ export class ActiveSource { for (let effect of tr.effects) { if (effect.is(startCompletionEffect)) - value = new ActiveSource(value.source, State.Pending, effect.value ? cur(tr.state) : -1) + value = new ActiveSource(value.source, State.Pending, effect.value) else if (effect.is(closeCompletionEffect)) value = new ActiveSource(value.source, State.Inactive) else if (effect.is(setActiveEffect)) @@ -244,9 +246,7 @@ export class ActiveSource { updateFor(tr: Transaction, type: UpdateType): ActiveSource { return this.map(tr.changes) } - map(changes: ChangeDesc) { - return changes.empty || this.explicitPos < 0 ? this : new ActiveSource(this.source, this.state, changes.mapPos(this.explicitPos)) - } + map(changes: ChangeDesc): ActiveSource { return this } touches(tr: Transaction) { return tr.changes.touchesRange(cur(tr.state)) @@ -255,11 +255,12 @@ export class ActiveSource { export class ActiveResult extends ActiveSource { constructor(source: CompletionSource, - explicitPos: number, + explicit: boolean, + readonly limit: number, readonly result: CompletionResult, readonly from: number, readonly to: number) { - super(source, State.Result, explicitPos) + super(source, State.Result, explicit) } hasResult(): this is ActiveResult { return true } @@ -270,24 +271,23 @@ export class ActiveResult extends ActiveSource { if (result!.map && !tr.changes.empty) result = result!.map(result!, tr.changes) let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1) let pos = cur(tr.state) - if ((this.explicitPos < 0 ? pos <= from : pos < this.from) || - pos > to || !result || - (type & UpdateType.Backspacing) && cur(tr.startState) == this.from) + if (pos > to || !result || + (type & UpdateType.Backspacing) && (cur(tr.startState) == this.from || pos < this.limit)) return new ActiveSource(this.source, type & UpdateType.Activate ? State.Pending : State.Inactive) - let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos) + let limit = tr.changes.mapPos(this.limit) if (checkValid(result.validFor, tr.state, from, to)) - return new ActiveResult(this.source, explicitPos, result, from, to) + return new ActiveResult(this.source, this.explicit, limit, result, from, to) if (result.update && - (result = result.update(result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0)))) - return new ActiveResult(this.source, explicitPos, result, result.from, result.to ?? cur(tr.state)) - return new ActiveSource(this.source, State.Pending, explicitPos) + (result = result.update(result, from, to, new CompletionContext(tr.state, pos, false)))) + return new ActiveResult(this.source, this.explicit, limit, result, result.from, result.to ?? cur(tr.state)) + return new ActiveSource(this.source, State.Pending, this.explicit) } map(mapping: ChangeDesc) { if (mapping.empty) return this let result = this.result.map ? this.result.map(this.result, mapping) : this.result if (!result) return new ActiveSource(this.source, State.Inactive) - return new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, + return new ActiveResult(this.source, this.explicit, mapping.mapPos(this.limit), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1)) } diff --git a/src/view.ts b/src/view.ts index 946b842..f6892c4 100644 --- a/src/view.ts +++ b/src/view.ts @@ -76,7 +76,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu constructor(readonly view: EditorView) { for (let active of view.state.field(completionState).active) - if (active.state == State.Pending) this.startQuery(active) + if (active.isPending) this.startQuery(active) } update(update: ViewUpdate) { @@ -107,7 +107,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu if (this.debounceUpdate > -1) clearTimeout(this.debounceUpdate) if (update.transactions.some(tr => tr.effects.some(e => e.is(startCompletionEffect)))) this.pendingStart = true let delay = this.pendingStart ? 50 : conf.activateOnTypingDelay - this.debounceUpdate = cState.active.some(a => a.state == State.Pending && !this.running.some(q => q.active.source == a.source)) + this.debounceUpdate = cState.active.some(a => a.isPending && !this.running.some(q => q.active.source == a.source)) ? setTimeout(() => this.startUpdate(), delay) : -1 if (this.composing != CompositionState.None) for (let tr of update.transactions) { @@ -123,7 +123,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu this.pendingStart = false let {state} = this.view, cState = state.field(completionState) for (let active of cState.active) { - if (active.state == State.Pending && !this.running.some(r => r.active.source == active.source)) + if (active.isPending && !this.running.some(r => r.active.source == active.source)) this.startQuery(active) } if (this.running.length && cState.open && cState.open.disabled) @@ -133,7 +133,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu startQuery(active: ActiveSource) { let {state} = this.view, pos = cur(state) - let context = new CompletionContext(state, pos, active.explicitPos == pos, this.view) + let context = new CompletionContext(state, pos, active.explicit, this.view) let pending = new RunningQuery(active, context) this.running.push(pending) Promise.resolve(active.source(context)).then(result => { @@ -169,9 +169,11 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu this.running.splice(i--, 1) if (query.done) { + let pos = cur(query.updates.length ? query.updates[0].startState : this.view.state) + let limit = Math.min(pos, query.done.from + (query.active.explicit ? 0 : 1)) let active: ActiveSource = new ActiveResult( - query.active.source, query.active.explicitPos, query.done, query.done.from, - query.done.to ?? cur(query.updates.length ? query.updates[0].startState : this.view.state)) + query.active.source, query.active.explicit, limit, query.done, query.done.from, + query.done.to ?? pos) // Replay the transactions that happened since the start of // the request and see if that preserves the result for (let tr of query.updates) active = active.update(tr, conf) @@ -182,13 +184,13 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu } let current = cState.active.find(a => a.source == query.active.source) - if (current && current.state == State.Pending) { + if (current && current.isPending) { if (query.done == null) { // Explicitly failed. Should clear the pending status if it // hasn't been re-set in the meantime. let active = new ActiveSource(query.active.source, State.Inactive) for (let tr of query.updates) active = active.update(tr, conf) - if (active.state != State.Pending) updated.push(active) + if (!active.isPending) updated.push(active) } else { // Cleared by subsequent transactions. Restart. this.startQuery(current)