From 82893f890f37dc182e0dd1e585a62a35e8819cfc Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 21 Jun 2024 12:53:10 +0200 Subject: [PATCH] Simplify completion source updates FIX: Fix an issue where completions weren't properly reset when starting a new completion through `activateOnCompletion`. Closes https://github.com/codemirror/autocomplete/pull/23 --- src/state.ts | 59 ++++++++++++++++++++++++++++++++-------------------- src/view.ts | 7 ++++--- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/state.ts b/src/state.ts index 3d14a43..57be788 100644 --- a/src/state.ts +++ b/src/state.ts @@ -188,12 +188,27 @@ const none: readonly any[] = [] export const enum State { Inactive = 0, Pending = 1, Result = 2 } -export function getUserEvent(tr: Transaction, conf: Required): "input" | "delete" | null { +export const enum UpdateType { + None = 0, + Typing = 1, + Backspacing = 2, + SimpleInteraction = Typing | Backspacing, + Activate = 4, + Reset = 8, + ResetIfTouching = 16 +} + +export function getUpdateType(tr: Transaction, conf: Required): UpdateType { if (tr.isUserEvent("input.complete")) { let completion = tr.annotation(pickedCompletion) - if (completion && conf.activateOnCompletion(completion)) return "input" + if (completion && conf.activateOnCompletion(completion)) return UpdateType.Activate | UpdateType.Reset } - return tr.isUserEvent("input.type") ? "input" : tr.isUserEvent("delete.backward") ? "delete" : null + let typing = tr.isUserEvent("input.type") + return typing && conf.activateOnTyping ? UpdateType.Activate | UpdateType.Typing + : typing ? UpdateType.Typing + : tr.isUserEvent("delete.backward") ? UpdateType.Backspacing + : tr.selection ? UpdateType.Reset + : tr.docChanged ? UpdateType.ResetIfTouching : UpdateType.None } export class ActiveSource { @@ -204,13 +219,12 @@ export class ActiveSource { hasResult(): this is ActiveResult { return false } update(tr: Transaction, conf: Required): ActiveSource { - let event = getUserEvent(tr, conf), value: ActiveSource = this - if (event) - value = value.handleUserEvent(tr, event, conf) - else if (tr.docChanged) - value = value.handleChange(tr) - else if (tr.selection && value.state != State.Inactive) + let type = getUpdateType(tr, conf), value: ActiveSource = this + if ((type & UpdateType.Reset) || (type & UpdateType.ResetIfTouching) && this.touches(tr)) value = new ActiveSource(value.source, State.Inactive) + if ((type & UpdateType.Activate) && value.state == State.Inactive) + value = new ActiveSource(this.source, State.Pending) + value = value.updateFor(tr, type) for (let effect of tr.effects) { if (effect.is(startCompletionEffect)) @@ -223,17 +237,15 @@ export class ActiveSource { return value } - handleUserEvent(tr: Transaction, type: "input" | "delete", conf: Required): ActiveSource { - return type == "delete" || !conf.activateOnTyping ? this.map(tr.changes) : new ActiveSource(this.source, State.Pending) - } - - handleChange(tr: Transaction): ActiveSource { - return tr.changes.touchesRange(cur(tr.startState)) ? new ActiveSource(this.source, State.Inactive) : this.map(tr.changes) - } + 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)) } + + touches(tr: Transaction) { + return tr.changes.touchesRange(cur(tr.state)) + } } export class ActiveResult extends ActiveSource { @@ -247,15 +259,16 @@ export class ActiveResult extends ActiveSource { hasResult(): this is ActiveResult { return true } - handleUserEvent(tr: Transaction, type: "input" | "delete", conf: Required): ActiveSource { + updateFor(tr: Transaction, type: UpdateType) { + if (!(type & UpdateType.SimpleInteraction)) return this.map(tr.changes) let result = this.result as CompletionResult | null 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 == "delete" && cur(tr.startState) == this.from) - return new ActiveSource(this.source, type == "input" && conf.activateOnTyping ? State.Pending : State.Inactive) + (type & UpdateType.Backspacing) && cur(tr.startState) == this.from) + return new ActiveSource(this.source, type & UpdateType.Activate ? State.Pending : State.Inactive) let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos) if (checkValid(result.validFor, tr.state, from, to)) return new ActiveResult(this.source, explicitPos, result, from, to) @@ -265,10 +278,6 @@ export class ActiveResult extends ActiveSource { return new ActiveSource(this.source, State.Pending, explicitPos) } - handleChange(tr: Transaction): ActiveSource { - return tr.changes.touchesRange(this.from, this.to) ? new ActiveSource(this.source, State.Inactive) : this.map(tr.changes) - } - map(mapping: ChangeDesc) { if (mapping.empty) return this let result = this.result.map ? this.result.map(this.result, mapping) : this.result @@ -276,6 +285,10 @@ export class ActiveResult extends ActiveSource { return new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1)) } + + touches(tr: Transaction) { + return tr.changes.touchesRange(this.from, this.to) + } } function checkValid(validFor: undefined | RegExp | ((text: string, from: number, to: number, state: EditorState) => boolean), diff --git a/src/view.ts b/src/view.ts index b6f48d1..692ac18 100644 --- a/src/view.ts +++ b/src/view.ts @@ -2,7 +2,7 @@ import {EditorView, Command, ViewPlugin, PluginValue, ViewUpdate, logException, getTooltip, TooltipView} from "@codemirror/view" import {Transaction, Prec} from "@codemirror/state" import {completionState, setSelectedEffect, setActiveEffect, State, - ActiveSource, ActiveResult, getUserEvent, applyCompletion} from "./state" + ActiveSource, ActiveResult, getUpdateType, UpdateType, applyCompletion} from "./state" import {completionConfig} from "./config" import {cur, CompletionResult, CompletionContext, startCompletionEffect, closeCompletionEffect} from "./completion" @@ -86,7 +86,8 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu if (!update.selectionSet && !update.docChanged && update.startState.field(completionState) == cState) return let doesReset = update.transactions.some(tr => { - return (tr.selection || tr.docChanged) && !getUserEvent(tr, conf) + let type = getUpdateType(tr, conf) + return (type & UpdateType.Reset) || (tr.selection || tr.docChanged) && !(type & UpdateType.SimpleInteraction) }) for (let i = 0; i < this.running.length; i++) { let query = this.running[i] @@ -110,7 +111,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu ? setTimeout(() => this.startUpdate(), delay) : -1 if (this.composing != CompositionState.None) for (let tr of update.transactions) { - if (getUserEvent(tr, conf) == "input") + if (tr.isUserEvent("input.type")) this.composing = CompositionState.Changed else if (this.composing == CompositionState.Changed && tr.selection) this.composing = CompositionState.ChangedAndMoved