Skip to content

Commit 2056c3a

Browse files
Merge branch 'codemirror:main' into main-overleaf
2 parents 03ea9e4 + b26ff38 commit 2056c3a

File tree

11 files changed

+160
-76
lines changed

11 files changed

+160
-76
lines changed

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
1+
## 6.10.2 (2023-10-13)
2+
3+
### Bug fixes
4+
5+
Fix a bug that caused `updateSyncTime` to always delay the initial population of the tooltip.
6+
7+
## 6.10.1 (2023-10-11)
8+
9+
### Bug fixes
10+
11+
Fix a bug where picking a selection with the mouse could use the wrong completion if the completion list was updated after being opened.
12+
13+
## 6.10.0 (2023-10-11)
14+
15+
### New features
16+
17+
The new autocompletion configuration option `updateSyncTime` allows control over how long fast sources are held back waiting for slower completion sources.
18+
19+
## 6.9.2 (2023-10-06)
20+
21+
### Bug fixes
22+
23+
Fix a bug in `completeAnyWord` that could cause it to generate invalid regular expressions and crash.
24+
25+
## 6.9.1 (2023-09-14)
26+
27+
### Bug fixes
28+
29+
Make sure the cursor is scrolled into view after inserting completion text.
30+
31+
Make sure scrolling completions into view doesn't get confused when the tooltip is scaled.
32+
33+
## 6.9.0 (2023-07-18)
34+
35+
### New features
36+
37+
Completions may now provide a `displayLabel` property that overrides the way they are displayed in the completion list.
38+
39+
## 6.8.1 (2023-06-23)
40+
41+
### Bug fixes
42+
43+
`acceptCompletion` now returns false (allowing other handlers to take effect) when the completion popup is open but disabled.
44+
145
## 6.8.0 (2023-06-12)
246

347
### New features

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codemirror/autocomplete",
3-
"version": "6.8.0",
3+
"version": "6.10.2",
44
"description": "Autocompletion for the CodeMirror code editor",
55
"scripts": {
66
"test": "cm-runtests",
@@ -28,7 +28,7 @@
2828
"dependencies": {
2929
"@codemirror/language": "^6.0.0",
3030
"@codemirror/state": "^6.0.0",
31-
"@codemirror/view": "^6.6.0",
31+
"@codemirror/view": "^6.17.0",
3232
"@lezer/common": "^1.0.0"
3333
},
3434
"peerDependencies": {

src/completion.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ import {SyntaxNode} from "@lezer/common"
66
/// Objects type used to represent individual completions.
77
export interface Completion {
88
/// The label to show in the completion picker. This is what input
9-
/// is matched agains to determine whether a completion matches (and
9+
/// is matched against to determine whether a completion matches (and
1010
/// how well it matches).
1111
label: string
12+
/// An optional override for the completion's visible label. When
13+
/// using this, matched characters will only be highlighted if you
14+
/// provide a [`getMatch`](#autocomplete.CompletionResult.getMatch)
15+
/// function.
16+
displayLabel?: string
1217
/// An optional short piece of information to show (with a different
1318
/// style) after the label.
1419
detail?: string
@@ -211,11 +216,14 @@ export interface CompletionResult {
211216
/// is `false`, because it only works when filtering.
212217
filter?: boolean
213218
/// When [`filter`](#autocomplete.CompletionResult.filter) is set to
214-
/// `false`, this may be provided to compute the ranges on the label
215-
/// that match the input. Should return an array of numbers where
216-
/// each pair of adjacent numbers provide the start and end of a
217-
/// range.
218-
getMatch?: (completion: Completion) => readonly number[]
219+
/// `false` or a completion has a
220+
/// [`displayLabel`](#autocomplete.Completion.displayLabel), this
221+
/// may be provided to compute the ranges on the label that match
222+
/// the input. Should return an array of numbers where each pair of
223+
/// adjacent numbers provide the start and end of a range. The
224+
/// second argument, the match found by the library, is only passed
225+
/// when `filter` isn't `false`.
226+
getMatch?: (completion: Completion, matched?: readonly number[]) => readonly number[]
219227
/// Synchronously update the completion result after typing or
220228
/// deletion. If given, this should not do any expensive work, since
221229
/// it will be called during editor state updates. The function
@@ -277,6 +285,7 @@ export function insertCompletionText(state: EditorState, text: string | Text, fr
277285
range: EditorSelection.cursor(change.from + change.insert.length)
278286
}
279287
}),
288+
scrollIntoView: true,
280289
userEvent: "input.complete"
281290
}
282291
}

src/config.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ export interface CompletionConfig {
5454
/// 80.
5555
addToOptions?: {render: (completion: Completion, state: EditorState) => Node | null,
5656
position: number}[]
57-
/// By default, [info](#autocomplet.Completion.info) tooltips are
58-
/// placed to the side of the selected. This option can be used to
59-
/// override that. It will be given rectangles for the list of
60-
/// completions, the selected option, the info element, and the
61-
/// availble [tooltip space](#view.tooltips^config.tooltipSpace),
62-
/// and should return style and/or class strings for the info
63-
/// element.
57+
/// By default, [info](#autocomplete.Completion.info) tooltips are
58+
/// placed to the side of the selected completion. This option can
59+
/// be used to override that. It will be given rectangles for the
60+
/// list of completions, the selected option, the info element, and
61+
/// the availble [tooltip
62+
/// space](#view.tooltips^config.tooltipSpace), and should return
63+
/// style and/or class strings for the info element.
6464
positionInfo?: (view: EditorView, list: Rect, option: Rect, info: Rect, space: Rect) => {style?: string, class?: string}
6565
/// The comparison function to use when sorting completions with the same
6666
/// match score. Defaults to using
@@ -71,6 +71,11 @@ export interface CompletionConfig {
7171
/// presses made before the user is aware of the tooltip don't go to
7272
/// the tooltip. This option can be used to configure that delay.
7373
interactionDelay?: number
74+
/// When there are multiple asynchronous completion sources, this
75+
/// controls how long the extension waits for a slow source before
76+
/// displaying results from faster sources. Defaults to 100
77+
/// milliseconds.
78+
updateSyncTime?: number
7479
}
7580

7681
export const completionConfig = Facet.define<CompletionConfig, Required<CompletionConfig>>({
@@ -87,9 +92,10 @@ export const completionConfig = Facet.define<CompletionConfig, Required<Completi
8792
aboveCursor: false,
8893
icons: true,
8994
addToOptions: [],
90-
positionInfo: defaultPositionInfo,
95+
positionInfo: defaultPositionInfo as any,
9196
compareCompletions: (a, b) => a.label.localeCompare(b.label),
92-
interactionDelay: 75
97+
interactionDelay: 75,
98+
updateSyncTime: 100
9399
}, {
94100
defaultKeymap: (a, b) => a && b,
95101
closeOnBlur: (a, b) => a && b,
@@ -105,7 +111,7 @@ function joinClass(a: string, b: string) {
105111
return a ? b ? a + " " + b : a : b
106112
}
107113

108-
function defaultPositionInfo(view: EditorView, list: Rect, option: Rect, info: Rect, space: Rect) {
114+
function defaultPositionInfo(view: EditorView, list: Rect, option: Rect, info: Rect, space: Rect, tooltip: HTMLElement) {
109115
let rtl = view.textDirection == Direction.RTL, left = rtl, narrow = false
110116
let side = "top", offset, maxWidth
111117
let spaceLeft = list.left - space.left, spaceRight = space.right - list.right
@@ -126,8 +132,10 @@ function defaultPositionInfo(view: EditorView, list: Rect, option: Rect, info: R
126132
offset = list.bottom - option.top
127133
}
128134
}
135+
let scaleY = (list.bottom - list.top) / tooltip.offsetHeight
136+
let scaleX = (list.right - list.left) / tooltip.offsetWidth
129137
return {
130-
style: `${side}: ${offset}px; max-width: ${maxWidth}px`,
138+
style: `${side}: ${offset / scaleY}px; max-width: ${maxWidth / scaleX}px`,
131139
class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right")
132140
}
133141
}

src/filter.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export class FuzzyMatcher {
2626
precise: number[] = []
2727
byWord: number[] = []
2828

29+
score = 0
30+
matched: readonly number[] = []
31+
2932
constructor(readonly pattern: string) {
3033
for (let p = 0; p < pattern.length;) {
3134
let char = codePointAt(pattern, p), size = codePointSize(char)
@@ -37,16 +40,22 @@ export class FuzzyMatcher {
3740
this.astral = pattern.length != this.chars.length
3841
}
3942

43+
ret(score: number, matched: readonly number[]) {
44+
this.score = score
45+
this.matched = matched
46+
return true
47+
}
48+
4049
// Matches a given word (completion) against the pattern (input).
41-
// Will return null for no match, and otherwise an array that starts
42-
// with the match score, followed by any number of `from, to` pairs
43-
// indicating the matched parts of `word`.
50+
// Will return a boolean indicating whether there was a match and,
51+
// on success, set `this.score` to the score, `this.matched` to an
52+
// array of `from, to` pairs indicating the matched parts of `word`.
4453
//
4554
// The score is a number that is more negative the worse the match
4655
// is. See `Penalty` above.
47-
match(word: string): number[] | null {
48-
if (this.pattern.length == 0) return [Penalty.NotFull]
49-
if (word.length < this.pattern.length) return null
56+
match(word: string): boolean {
57+
if (this.pattern.length == 0) return this.ret(Penalty.NotFull, [])
58+
if (word.length < this.pattern.length) return false
5059
let {chars, folded, any, precise, byWord} = this
5160
// For single-character queries, only match when they occur right
5261
// at the start
@@ -55,11 +64,11 @@ export class FuzzyMatcher {
5564
let score = firstSize == word.length ? 0 : Penalty.NotFull
5665
if (first == chars[0]) {}
5766
else if (first == folded[0]) score += Penalty.CaseFold
58-
else return null
59-
return [score, 0, firstSize]
67+
else return false
68+
return this.ret(score, [0, firstSize])
6069
}
6170
let direct = word.indexOf(this.pattern)
62-
if (direct == 0) return [word.length == this.pattern.length ? 0 : Penalty.NotFull, 0, this.pattern.length]
71+
if (direct == 0) return this.ret(word.length == this.pattern.length ? 0 : Penalty.NotFull, [0, this.pattern.length])
6372

6473
let len = chars.length, anyTo = 0
6574
if (direct < 0) {
@@ -69,7 +78,7 @@ export class FuzzyMatcher {
6978
i += codePointSize(next)
7079
}
7180
// No match, exit immediately
72-
if (anyTo < len) return null
81+
if (anyTo < len) return false
7382
}
7483

7584
// This tracks the extent of the precise (non-folded, not
@@ -112,24 +121,25 @@ export class FuzzyMatcher {
112121
if (byWordTo == len && byWord[0] == 0 && wordAdjacent)
113122
return this.result(Penalty.ByWord + (byWordFolded ? Penalty.CaseFold : 0), byWord, word)
114123
if (adjacentTo == len && adjacentStart == 0)
115-
return [Penalty.CaseFold - word.length + (adjacentEnd == word.length ? 0 : Penalty.NotFull), 0, adjacentEnd]
124+
return this.ret(Penalty.CaseFold - word.length + (adjacentEnd == word.length ? 0 : Penalty.NotFull), [0, adjacentEnd])
116125
if (direct > -1)
117-
return [Penalty.NotStart - word.length, direct, direct + this.pattern.length]
126+
return this.ret(Penalty.NotStart - word.length, [direct, direct + this.pattern.length])
118127
if (adjacentTo == len)
119-
return [Penalty.CaseFold + Penalty.NotStart - word.length, adjacentStart, adjacentEnd]
128+
return this.ret(Penalty.CaseFold + Penalty.NotStart - word.length, [adjacentStart, adjacentEnd])
120129
if (byWordTo == len)
121130
return this.result(Penalty.ByWord + (byWordFolded ? Penalty.CaseFold : 0) + Penalty.NotStart +
122131
(wordAdjacent ? 0 : Penalty.Gap), byWord, word)
123-
return chars.length == 2 ? null : this.result((any[0] ? Penalty.NotStart : 0) + Penalty.CaseFold + Penalty.Gap, any, word)
132+
return chars.length == 2 ? false
133+
: this.result((any[0] ? Penalty.NotStart : 0) + Penalty.CaseFold + Penalty.Gap, any, word)
124134
}
125135

126136
result(score: number, positions: number[], word: string) {
127-
let result = [score - word.length], i = 1
137+
let result: number[] = [], i = 0
128138
for (let pos of positions) {
129139
let to = pos + (this.astral ? codePointSize(codePointAt(word, pos)) : 1)
130-
if (i > 1 && result[i - 1] == pos) result[i - 1] = to
140+
if (i && result[i - 1] == pos) result[i - 1] = to
131141
else { result[i++] = pos; result[i++] = to }
132142
}
133-
return result
143+
return this.ret(score - word.length, result)
134144
}
135145
}

src/snippet.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ function moveField(dir: 1 | -1): StateCommand {
221221
let next = active.active + dir, last = dir > 0 && !active.ranges.some(r => r.field == next + dir)
222222
dispatch(state.update({
223223
selection: fieldSelection(active.ranges, next),
224-
effects: setActive.of(last ? null : new ActiveSnippet(active.ranges, next))
224+
effects: setActive.of(last ? null : new ActiveSnippet(active.ranges, next)),
225+
scrollIntoView: true
225226
}))
226227
return true
227228
}
@@ -286,7 +287,9 @@ const snippetPointerHandler = EditorView.domEventHandlers({
286287
if (!match || match.field == active.active) return false
287288
view.dispatch({
288289
selection: fieldSelection(active.ranges, match.field),
289-
effects: setActive.of(active.ranges.some(r => r.field > match!.field) ? new ActiveSnippet(active.ranges, match.field) : null)
290+
effects: setActive.of(active.ranges.some(r => r.field > match!.field)
291+
? new ActiveSnippet(active.ranges, match.field) : null),
292+
scrollIntoView: true
290293
})
291294
return true
292295
}

src/state.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,16 @@ function sortOptions(active: readonly ActiveSource[], state: EditorState) {
2929
}
3030

3131
for (let a of active) if (a.hasResult()) {
32+
let getMatch = a.result.getMatch
3233
if (a.result.filter === false) {
33-
let getMatch = a.result.getMatch
3434
for (let option of a.result.options) {
35-
let match = [1e9 - options.length]
36-
if (getMatch) for (let n of getMatch(option)) match.push(n)
37-
addOption(new Option(option, a.source, match, match[0]))
35+
addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length))
3836
}
3937
} else {
40-
let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)), match
41-
for (let option of a.result.options) if (match = matcher.match(option.label)) {
42-
addOption(new Option(option, a.source, match, match[0] + (option.boost || 0)))
38+
let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to))
39+
for (let option of a.result.options) if (matcher.match(option.label)) {
40+
let matched = !option.displayLabel ? matcher.matched : getMatch ? getMatch(option, matcher.matched) : []
41+
addOption(new Option(option, a.source, matched, matcher.score + (option.boost || 0)))
4342
}
4443
}
4544
}
@@ -107,7 +106,7 @@ class CompletionDialog {
107106
}
108107
return new CompletionDialog(options, makeAttrs(id, selected), {
109108
pos: active.reduce((a, b) => b.hasResult() ? Math.min(a, b.from) : a, 1e8),
110-
create: completionTooltip(completionState, applyCompletion),
109+
create: createTooltip,
111110
above: conf.aboveCursor,
112111
}, prev ? prev.timestamp : Date.now(), selected, false)
113112
}
@@ -307,3 +306,5 @@ export function applyCompletion(view: EditorView, option: Option) {
307306
apply(view, option.completion, result.from, result.to)
308307
return true
309308
}
309+
310+
const createTooltip = completionTooltip(completionState, applyCompletion)

0 commit comments

Comments
 (0)