Skip to content

Commit

Permalink
Allow backspacing to the start of the completion for implicit complet…
Browse files Browse the repository at this point in the history
…ions 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
  • Loading branch information
marijnh committed Nov 3, 2024
1 parent d0c97d3 commit c1ba835
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 32 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
46 changes: 23 additions & 23 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ class CompletionDialog {
conf: Required<CompletionConfig>,
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
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -219,10 +219,12 @@ export function getUpdateType(tr: Transaction, conf: Required<CompletionConfig>)
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<CompletionConfig>): ActiveSource {
let type = getUpdateType(tr, conf), value: ActiveSource = this
if ((type & UpdateType.Reset) || (type & UpdateType.ResetIfTouching) && this.touches(tr))
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -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 }
Expand All @@ -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))
}

Expand Down
18 changes: 10 additions & 8 deletions src/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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 => {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit c1ba835

Please sign in to comment.