Skip to content

Commit c1ba835

Browse files
committed
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
1 parent d0c97d3 commit c1ba835

File tree

3 files changed

+34
-32
lines changed

3 files changed

+34
-32
lines changed

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const completionKeymapExt = Prec.highest(keymap.computeN([completionConfig], sta
5656
/// returns `null`.
5757
export function completionStatus(state: EditorState): null | "active" | "pending" {
5858
let cState = state.field(completionState, false)
59-
return cState && cState.active.some(a => a.state == State.Pending) ? "pending"
59+
return cState && cState.active.some(a => a.isPending) ? "pending"
6060
: cState && cState.active.some(a => a.state != State.Inactive) ? "active" : null
6161
}
6262

src/state.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,11 @@ class CompletionDialog {
9292
conf: Required<CompletionConfig>,
9393
didSetActive: boolean
9494
): CompletionDialog | null {
95-
if (prev && !didSetActive && active.some(s => s.state == State.Pending))
95+
if (prev && !didSetActive && active.some(s => s.isPending))
9696
return prev.setDisabled()
9797
let options = sortOptions(active, state)
9898
if (!options.length)
99-
return prev && active.some(a => a.state == State.Pending) ? prev.setDisabled() : null
99+
return prev && active.some(a => a.isPending) ? prev.setDisabled() : null
100100
let selected = state.facet(completionConfig).selectOnOpen ? 0 : -1
101101
if (prev && prev.selected != selected && prev.selected != -1) {
102102
let selectedValue = prev.options[prev.selected].completion
@@ -147,10 +147,10 @@ export class CompletionState {
147147
if (tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) ||
148148
!sameResults(active, this.active) || didSet)
149149
open = CompletionDialog.build(active, state, this.id, open, conf, didSet)
150-
else if (open && open.disabled && !active.some(a => a.state == State.Pending))
150+
else if (open && open.disabled && !active.some(a => a.isPending))
151151
open = null
152152

153-
if (!open && active.every(a => a.state != State.Pending) && active.some(a => a.hasResult()))
153+
if (!open && active.every(a => !a.isPending) && active.some(a => a.hasResult()))
154154
active = active.map(a => a.hasResult() ? new ActiveSource(a.source, State.Inactive) : a)
155155
for (let effect of tr.effects) if (effect.is(setSelectedEffect)) open = open && open.setSelected(effect.value, this.id)
156156

@@ -165,8 +165,8 @@ export class CompletionState {
165165
function sameResults(a: readonly ActiveSource[], b: readonly ActiveSource[]) {
166166
if (a == b) return true
167167
for (let iA = 0, iB = 0;;) {
168-
while (iA < a.length && !a[iA].hasResult) iA++
169-
while (iB < b.length && !b[iB].hasResult) iB++
168+
while (iA < a.length && !a[iA].hasResult()) iA++
169+
while (iB < b.length && !b[iB].hasResult()) iB++
170170
let endA = iA == a.length, endB = iB == b.length
171171
if (endA || endB) return endA == endB
172172
if ((a[iA++] as ActiveResult).result != (b[iB++] as ActiveResult).result) return false
@@ -191,7 +191,7 @@ function makeAttrs(id: string, selected: number) {
191191

192192
const none: readonly any[] = []
193193

194-
export const enum State { Inactive = 0, Pending = 1, Result = 2 }
194+
export const enum State { Inactive = 0, Pending = 1, Result = 3 }
195195

196196
export const enum UpdateType {
197197
None = 0,
@@ -219,10 +219,12 @@ export function getUpdateType(tr: Transaction, conf: Required<CompletionConfig>)
219219
export class ActiveSource {
220220
constructor(readonly source: CompletionSource,
221221
readonly state: State,
222-
readonly explicitPos: number = -1) {}
222+
readonly explicit: boolean = false) {}
223223

224224
hasResult(): this is ActiveResult { return false }
225225

226+
get isPending() { return this.state == State.Pending }
227+
226228
update(tr: Transaction, conf: Required<CompletionConfig>): ActiveSource {
227229
let type = getUpdateType(tr, conf), value: ActiveSource = this
228230
if ((type & UpdateType.Reset) || (type & UpdateType.ResetIfTouching) && this.touches(tr))
@@ -233,7 +235,7 @@ export class ActiveSource {
233235

234236
for (let effect of tr.effects) {
235237
if (effect.is(startCompletionEffect))
236-
value = new ActiveSource(value.source, State.Pending, effect.value ? cur(tr.state) : -1)
238+
value = new ActiveSource(value.source, State.Pending, effect.value)
237239
else if (effect.is(closeCompletionEffect))
238240
value = new ActiveSource(value.source, State.Inactive)
239241
else if (effect.is(setActiveEffect))
@@ -244,9 +246,7 @@ export class ActiveSource {
244246

245247
updateFor(tr: Transaction, type: UpdateType): ActiveSource { return this.map(tr.changes) }
246248

247-
map(changes: ChangeDesc) {
248-
return changes.empty || this.explicitPos < 0 ? this : new ActiveSource(this.source, this.state, changes.mapPos(this.explicitPos))
249-
}
249+
map(changes: ChangeDesc): ActiveSource { return this }
250250

251251
touches(tr: Transaction) {
252252
return tr.changes.touchesRange(cur(tr.state))
@@ -255,11 +255,12 @@ export class ActiveSource {
255255

256256
export class ActiveResult extends ActiveSource {
257257
constructor(source: CompletionSource,
258-
explicitPos: number,
258+
explicit: boolean,
259+
readonly limit: number,
259260
readonly result: CompletionResult,
260261
readonly from: number,
261262
readonly to: number) {
262-
super(source, State.Result, explicitPos)
263+
super(source, State.Result, explicit)
263264
}
264265

265266
hasResult(): this is ActiveResult { return true }
@@ -270,24 +271,23 @@ export class ActiveResult extends ActiveSource {
270271
if (result!.map && !tr.changes.empty) result = result!.map(result!, tr.changes)
271272
let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1)
272273
let pos = cur(tr.state)
273-
if ((this.explicitPos < 0 ? pos <= from : pos < this.from) ||
274-
pos > to || !result ||
275-
(type & UpdateType.Backspacing) && cur(tr.startState) == this.from)
274+
if (pos > to || !result ||
275+
(type & UpdateType.Backspacing) && (cur(tr.startState) == this.from || pos < this.limit))
276276
return new ActiveSource(this.source, type & UpdateType.Activate ? State.Pending : State.Inactive)
277-
let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos)
277+
let limit = tr.changes.mapPos(this.limit)
278278
if (checkValid(result.validFor, tr.state, from, to))
279-
return new ActiveResult(this.source, explicitPos, result, from, to)
279+
return new ActiveResult(this.source, this.explicit, limit, result, from, to)
280280
if (result.update &&
281-
(result = result.update(result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0))))
282-
return new ActiveResult(this.source, explicitPos, result, result.from, result.to ?? cur(tr.state))
283-
return new ActiveSource(this.source, State.Pending, explicitPos)
281+
(result = result.update(result, from, to, new CompletionContext(tr.state, pos, false))))
282+
return new ActiveResult(this.source, this.explicit, limit, result, result.from, result.to ?? cur(tr.state))
283+
return new ActiveSource(this.source, State.Pending, this.explicit)
284284
}
285285

286286
map(mapping: ChangeDesc) {
287287
if (mapping.empty) return this
288288
let result = this.result.map ? this.result.map(this.result, mapping) : this.result
289289
if (!result) return new ActiveSource(this.source, State.Inactive)
290-
return new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result,
290+
return new ActiveResult(this.source, this.explicit, mapping.mapPos(this.limit), this.result,
291291
mapping.mapPos(this.from), mapping.mapPos(this.to, 1))
292292
}
293293

src/view.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu
7676

7777
constructor(readonly view: EditorView) {
7878
for (let active of view.state.field(completionState).active)
79-
if (active.state == State.Pending) this.startQuery(active)
79+
if (active.isPending) this.startQuery(active)
8080
}
8181

8282
update(update: ViewUpdate) {
@@ -107,7 +107,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu
107107
if (this.debounceUpdate > -1) clearTimeout(this.debounceUpdate)
108108
if (update.transactions.some(tr => tr.effects.some(e => e.is(startCompletionEffect)))) this.pendingStart = true
109109
let delay = this.pendingStart ? 50 : conf.activateOnTypingDelay
110-
this.debounceUpdate = cState.active.some(a => a.state == State.Pending && !this.running.some(q => q.active.source == a.source))
110+
this.debounceUpdate = cState.active.some(a => a.isPending && !this.running.some(q => q.active.source == a.source))
111111
? setTimeout(() => this.startUpdate(), delay) : -1
112112

113113
if (this.composing != CompositionState.None) for (let tr of update.transactions) {
@@ -123,7 +123,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu
123123
this.pendingStart = false
124124
let {state} = this.view, cState = state.field(completionState)
125125
for (let active of cState.active) {
126-
if (active.state == State.Pending && !this.running.some(r => r.active.source == active.source))
126+
if (active.isPending && !this.running.some(r => r.active.source == active.source))
127127
this.startQuery(active)
128128
}
129129
if (this.running.length && cState.open && cState.open.disabled)
@@ -133,7 +133,7 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu
133133

134134
startQuery(active: ActiveSource) {
135135
let {state} = this.view, pos = cur(state)
136-
let context = new CompletionContext(state, pos, active.explicitPos == pos, this.view)
136+
let context = new CompletionContext(state, pos, active.explicit, this.view)
137137
let pending = new RunningQuery(active, context)
138138
this.running.push(pending)
139139
Promise.resolve(active.source(context)).then(result => {
@@ -169,9 +169,11 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu
169169
this.running.splice(i--, 1)
170170

171171
if (query.done) {
172+
let pos = cur(query.updates.length ? query.updates[0].startState : this.view.state)
173+
let limit = Math.min(pos, query.done.from + (query.active.explicit ? 0 : 1))
172174
let active: ActiveSource = new ActiveResult(
173-
query.active.source, query.active.explicitPos, query.done, query.done.from,
174-
query.done.to ?? cur(query.updates.length ? query.updates[0].startState : this.view.state))
175+
query.active.source, query.active.explicit, limit, query.done, query.done.from,
176+
query.done.to ?? pos)
175177
// Replay the transactions that happened since the start of
176178
// the request and see if that preserves the result
177179
for (let tr of query.updates) active = active.update(tr, conf)
@@ -182,13 +184,13 @@ export const completionPlugin = ViewPlugin.fromClass(class implements PluginValu
182184
}
183185

184186
let current = cState.active.find(a => a.source == query.active.source)
185-
if (current && current.state == State.Pending) {
187+
if (current && current.isPending) {
186188
if (query.done == null) {
187189
// Explicitly failed. Should clear the pending status if it
188190
// hasn't been re-set in the meantime.
189191
let active = new ActiveSource(query.active.source, State.Inactive)
190192
for (let tr of query.updates) active = active.update(tr, conf)
191-
if (active.state != State.Pending) updated.push(active)
193+
if (!active.isPending) updated.push(active)
192194
} else {
193195
// Cleared by subsequent transactions. Restart.
194196
this.startQuery(current)

0 commit comments

Comments
 (0)