UNPKG

@codemirror/autocomplete

Version:

Autocompletion for the CodeMirror code editor

1,176 lines (1,168 loc) 89.1 kB
import { Annotation, StateEffect, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateField, Prec, Text, Transaction, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state'; import { Direction, logException, showTooltip, EditorView, ViewPlugin, getTooltip, Decoration, WidgetType, keymap } from '@codemirror/view'; import { syntaxTree, indentUnit } from '@codemirror/language'; /** An instance of this is passed to completion source functions. */ class CompletionContext { /** Create a new completion context. (Mostly useful for testing completion sources—in the editor, the extension will create these for you.) */ constructor( /** The editor state that the completion happens in. */ state, /** The position at which the completion is happening. */ pos, /** Indicates whether completion was activated explicitly, or implicitly by typing. The usual way to respond to this is to only return completions when either there is part of a completable entity before the cursor, or `explicit` is true. */ explicit, /** The editor view. May be undefined if the context was created in a situation where there is no such view available, such as in synchronous updates via [`CompletionResult.update`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.update) or when called by test code. */ view) { this.state = state; this.pos = pos; this.explicit = explicit; this.view = view; /** @internal */ this.abortListeners = []; /** @internal */ this.abortOnDocChange = false; } /** Get the extent, content, and (if there is a token) type of the token before `this.pos`. */ tokenBefore(types) { let token = syntaxTree(this.state).resolveInner(this.pos, -1); while (token && types.indexOf(token.name) < 0) token = token.parent; return token ? { from: token.from, to: this.pos, text: this.state.sliceDoc(token.from, this.pos), type: token.type } : null; } /** Get the match of the given expression directly before the cursor. */ matchBefore(expr) { let line = this.state.doc.lineAt(this.pos); let start = Math.max(line.from, this.pos - 250); let str = line.text.slice(start - line.from, this.pos - line.from); let found = str.search(ensureAnchor(expr, false)); return found < 0 ? null : { from: start + found, to: this.pos, text: str.slice(found) }; } /** Yields true when the query has been aborted. Can be useful in asynchronous queries to avoid doing work that will be ignored. */ get aborted() { return this.abortListeners == null; } /** Allows you to register abort handlers, which will be called when the query is [aborted](https://codemirror.net/6/docs/ref/#autocomplete.CompletionContext.aborted). By default, running queries will not be aborted for regular typing or backspacing, on the assumption that they are likely to return a result with a [`validFor`](https://codemirror.net/6/docs/ref/#autocomplete.CompletionResult.validFor) field that allows the result to be used after all. Passing `onDocChange: true` will cause this query to be aborted for any document change. */ addEventListener(type, listener, options) { if (type == "abort" && this.abortListeners) { this.abortListeners.push(listener); if (options && options.onDocChange) this.abortOnDocChange = true; } } } function toSet(chars) { let flat = Object.keys(chars).join(""); let words = /\w/.test(flat); if (words) flat = flat.replace(/\w/g, ""); return `[${words ? "\\w" : ""}${flat.replace(/[^\w\s]/g, "\\$&")}]`; } function prefixMatch(options) { let first = Object.create(null), rest = Object.create(null); for (let { label } of options) { first[label[0]] = true; for (let i = 1; i < label.length; i++) rest[label[i]] = true; } let source = toSet(first) + toSet(rest) + "*$"; return [new RegExp("^" + source), new RegExp(source)]; } /** Given a a fixed array of options, return an autocompleter that completes them. */ function completeFromList(list) { let options = list.map(o => typeof o == "string" ? { label: o } : o); let [validFor, match] = options.every(o => /^\w+$/.test(o.label)) ? [/\w*$/, /\w+$/] : prefixMatch(options); return (context) => { let token = context.matchBefore(match); return token || context.explicit ? { from: token ? token.from : context.pos, options, validFor } : null; }; } /** Wrap the given completion source so that it will only fire when the cursor is in a syntax node with one of the given names. */ function ifIn(nodes, source) { return (context) => { for (let pos = syntaxTree(context.state).resolveInner(context.pos, -1); pos; pos = pos.parent) { if (nodes.indexOf(pos.name) > -1) return source(context); if (pos.type.isTop) break; } return null; }; } /** Wrap the given completion source so that it will not fire when the cursor is in a syntax node with one of the given names. */ function ifNotIn(nodes, source) { return (context) => { for (let pos = syntaxTree(context.state).resolveInner(context.pos, -1); pos; pos = pos.parent) { if (nodes.indexOf(pos.name) > -1) return null; if (pos.type.isTop) break; } return source(context); }; } class Option { constructor(completion, source, match, score) { this.completion = completion; this.source = source; this.match = match; this.score = score; } } function cur(state) { return state.selection.main.from; } // Make sure the given regexp has a $ at its end and, if `start` is // true, a ^ at its start. function ensureAnchor(expr, start) { var _a; let { source } = expr; let addStart = start && source[0] != "^", addEnd = source[source.length - 1] != "$"; if (!addStart && !addEnd) return expr; return new RegExp(`${addStart ? "^" : ""}(?:${source})${addEnd ? "$" : ""}`, (_a = expr.flags) !== null && _a !== void 0 ? _a : (expr.ignoreCase ? "i" : "")); } /** This annotation is added to transactions that are produced by picking a completion. */ const pickedCompletion = /*@__PURE__*/Annotation.define(); /** Helper function that returns a transaction spec which inserts a completion's text in the main selection range, and any other selection range that has the same text in front of it. */ function insertCompletionText(state, text, from, to) { let { main } = state.selection, fromOff = from - main.from, toOff = to - main.from; return { ...state.changeByRange(range => { if (range != main && from != to && state.sliceDoc(range.from + fromOff, range.from + toOff) != state.sliceDoc(from, to)) return { range }; let lines = state.toText(text); return { changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: lines }, range: EditorSelection.cursor(range.from + fromOff + lines.length) }; }), scrollIntoView: true, userEvent: "input.complete" }; } const SourceCache = /*@__PURE__*/new WeakMap(); function asSource(source) { if (!Array.isArray(source)) return source; let known = SourceCache.get(source); if (!known) SourceCache.set(source, known = completeFromList(source)); return known; } const startCompletionEffect = /*@__PURE__*/StateEffect.define(); const closeCompletionEffect = /*@__PURE__*/StateEffect.define(); // A pattern matcher for fuzzy completion matching. Create an instance // once for a pattern, and then use that to match any number of // completions. class FuzzyMatcher { constructor(pattern) { this.pattern = pattern; this.chars = []; this.folded = []; // Buffers reused by calls to `match` to track matched character // positions. this.any = []; this.precise = []; this.byWord = []; this.score = 0; this.matched = []; for (let p = 0; p < pattern.length;) { let char = codePointAt(pattern, p), size = codePointSize(char); this.chars.push(char); let part = pattern.slice(p, p + size), upper = part.toUpperCase(); this.folded.push(codePointAt(upper == part ? part.toLowerCase() : upper, 0)); p += size; } this.astral = pattern.length != this.chars.length; } ret(score, matched) { this.score = score; this.matched = matched; return this; } // Matches a given word (completion) against the pattern (input). // Will return a boolean indicating whether there was a match and, // on success, set `this.score` to the score, `this.matched` to an // array of `from, to` pairs indicating the matched parts of `word`. // // The score is a number that is more negative the worse the match // is. See `Penalty` above. match(word) { if (this.pattern.length == 0) return this.ret(-100 /* Penalty.NotFull */, []); if (word.length < this.pattern.length) return null; let { chars, folded, any, precise, byWord } = this; // For single-character queries, only match when they occur right // at the start if (chars.length == 1) { let first = codePointAt(word, 0), firstSize = codePointSize(first); let score = firstSize == word.length ? 0 : -100 /* Penalty.NotFull */; if (first == chars[0]) ; else if (first == folded[0]) score += -200 /* Penalty.CaseFold */; else return null; return this.ret(score, [0, firstSize]); } let direct = word.indexOf(this.pattern); if (direct == 0) return this.ret(word.length == this.pattern.length ? 0 : -100 /* Penalty.NotFull */, [0, this.pattern.length]); let len = chars.length, anyTo = 0; if (direct < 0) { for (let i = 0, e = Math.min(word.length, 200); i < e && anyTo < len;) { let next = codePointAt(word, i); if (next == chars[anyTo] || next == folded[anyTo]) any[anyTo++] = i; i += codePointSize(next); } // No match, exit immediately if (anyTo < len) return null; } // This tracks the extent of the precise (non-folded, not // necessarily adjacent) match let preciseTo = 0; // Tracks whether there is a match that hits only characters that // appear to be starting words. `byWordFolded` is set to true when // a case folded character is encountered in such a match let byWordTo = 0, byWordFolded = false; // If we've found a partial adjacent match, these track its state let adjacentTo = 0, adjacentStart = -1, adjacentEnd = -1; let hasLower = /[a-z]/.test(word), wordAdjacent = true; // Go over the option's text, scanning for the various kinds of matches for (let i = 0, e = Math.min(word.length, 200), prevType = 0 /* Tp.NonWord */; i < e && byWordTo < len;) { let next = codePointAt(word, i); if (direct < 0) { if (preciseTo < len && next == chars[preciseTo]) precise[preciseTo++] = i; if (adjacentTo < len) { if (next == chars[adjacentTo] || next == folded[adjacentTo]) { if (adjacentTo == 0) adjacentStart = i; adjacentEnd = i + 1; adjacentTo++; } else { adjacentTo = 0; } } } let ch, type = next < 0xff ? (next >= 48 && next <= 57 || next >= 97 && next <= 122 ? 2 /* Tp.Lower */ : next >= 65 && next <= 90 ? 1 /* Tp.Upper */ : 0 /* Tp.NonWord */) : ((ch = fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Tp.Upper */ : ch != ch.toUpperCase() ? 2 /* Tp.Lower */ : 0 /* Tp.NonWord */); if (!i || type == 1 /* Tp.Upper */ && hasLower || prevType == 0 /* Tp.NonWord */ && type != 0 /* Tp.NonWord */) { if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true))) byWord[byWordTo++] = i; else if (byWord.length) wordAdjacent = false; } prevType = type; i += codePointSize(next); } if (byWordTo == len && byWord[0] == 0 && wordAdjacent) return this.result(-100 /* Penalty.ByWord */ + (byWordFolded ? -200 /* Penalty.CaseFold */ : 0), byWord, word); if (adjacentTo == len && adjacentStart == 0) return this.ret(-200 /* Penalty.CaseFold */ - word.length + (adjacentEnd == word.length ? 0 : -100 /* Penalty.NotFull */), [0, adjacentEnd]); if (direct > -1) return this.ret(-700 /* Penalty.NotStart */ - word.length, [direct, direct + this.pattern.length]); if (adjacentTo == len) return this.ret(-200 /* Penalty.CaseFold */ + -700 /* Penalty.NotStart */ - word.length, [adjacentStart, adjacentEnd]); if (byWordTo == len) return this.result(-100 /* Penalty.ByWord */ + (byWordFolded ? -200 /* Penalty.CaseFold */ : 0) + -700 /* Penalty.NotStart */ + (wordAdjacent ? 0 : -1100 /* Penalty.Gap */), byWord, word); return chars.length == 2 ? null : this.result((any[0] ? -700 /* Penalty.NotStart */ : 0) + -200 /* Penalty.CaseFold */ + -1100 /* Penalty.Gap */, any, word); } result(score, positions, word) { let result = [], i = 0; for (let pos of positions) { let to = pos + (this.astral ? codePointSize(codePointAt(word, pos)) : 1); if (i && result[i - 1] == pos) result[i - 1] = to; else { result[i++] = pos; result[i++] = to; } } return this.ret(score - word.length, result); } } class StrictMatcher { constructor(pattern) { this.pattern = pattern; this.matched = []; this.score = 0; this.folded = pattern.toLowerCase(); } match(word) { if (word.length < this.pattern.length) return null; let start = word.slice(0, this.pattern.length); let match = start == this.pattern ? 0 : start.toLowerCase() == this.folded ? -200 /* Penalty.CaseFold */ : null; if (match == null) return null; this.matched = [0, start.length]; this.score = match + (word.length == this.pattern.length ? 0 : -100 /* Penalty.NotFull */); return this; } } const completionConfig = /*@__PURE__*/Facet.define({ combine(configs) { return combineConfig(configs, { activateOnTyping: true, activateOnCompletion: () => false, activateOnTypingDelay: 100, selectOnOpen: true, override: null, closeOnBlur: true, maxRenderedOptions: 100, defaultKeymap: true, tooltipClass: () => "", optionClass: () => "", aboveCursor: false, icons: true, addToOptions: [], positionInfo: defaultPositionInfo, filterStrict: false, compareCompletions: (a, b) => a.label.localeCompare(b.label), interactionDelay: 75, updateSyncTime: 100 }, { defaultKeymap: (a, b) => a && b, closeOnBlur: (a, b) => a && b, icons: (a, b) => a && b, tooltipClass: (a, b) => c => joinClass(a(c), b(c)), optionClass: (a, b) => c => joinClass(a(c), b(c)), addToOptions: (a, b) => a.concat(b), filterStrict: (a, b) => a || b, }); } }); function joinClass(a, b) { return a ? b ? a + " " + b : a : b; } function defaultPositionInfo(view, list, option, info, space, tooltip) { let rtl = view.textDirection == Direction.RTL, left = rtl, narrow = false; let side = "top", offset, maxWidth; let spaceLeft = list.left - space.left, spaceRight = space.right - list.right; let infoWidth = info.right - info.left, infoHeight = info.bottom - info.top; if (left && spaceLeft < Math.min(infoWidth, spaceRight)) left = false; else if (!left && spaceRight < Math.min(infoWidth, spaceLeft)) left = true; if (infoWidth <= (left ? spaceLeft : spaceRight)) { offset = Math.max(space.top, Math.min(option.top, space.bottom - infoHeight)) - list.top; maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight); } else { narrow = true; maxWidth = Math.min(400 /* Info.Width */, (rtl ? list.right : space.right - list.left) - 30 /* Info.Margin */); let spaceBelow = space.bottom - list.bottom; if (spaceBelow >= infoHeight || spaceBelow > list.top) { // Below the completion offset = option.bottom - list.top; } else { // Above it side = "bottom"; offset = list.bottom - option.top; } } let scaleY = (list.bottom - list.top) / tooltip.offsetHeight; let scaleX = (list.right - list.left) / tooltip.offsetWidth; return { style: `${side}: ${offset / scaleY}px; max-width: ${maxWidth / scaleX}px`, class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right") }; } function optionContent(config) { let content = config.addToOptions.slice(); if (config.icons) content.push({ render(completion) { let icon = document.createElement("div"); icon.classList.add("cm-completionIcon"); if (completion.type) icon.classList.add(...completion.type.split(/\s+/g).map(cls => "cm-completionIcon-" + cls)); icon.setAttribute("aria-hidden", "true"); return icon; }, position: 20 }); content.push({ render(completion, _s, _v, match) { let labelElt = document.createElement("span"); labelElt.className = "cm-completionLabel"; let label = completion.displayLabel || completion.label, off = 0; for (let j = 0; j < match.length;) { let from = match[j++], to = match[j++]; if (from > off) labelElt.appendChild(document.createTextNode(label.slice(off, from))); let span = labelElt.appendChild(document.createElement("span")); span.appendChild(document.createTextNode(label.slice(from, to))); span.className = "cm-completionMatchedText"; off = to; } if (off < label.length) labelElt.appendChild(document.createTextNode(label.slice(off))); return labelElt; }, position: 50 }, { render(completion) { if (!completion.detail) return null; let detailElt = document.createElement("span"); detailElt.className = "cm-completionDetail"; detailElt.textContent = completion.detail; return detailElt; }, position: 80 }); return content.sort((a, b) => a.position - b.position).map(a => a.render); } function rangeAroundSelected(total, selected, max) { if (total <= max) return { from: 0, to: total }; if (selected < 0) selected = 0; if (selected <= (total >> 1)) { let off = Math.floor(selected / max); return { from: off * max, to: (off + 1) * max }; } let off = Math.floor((total - selected) / max); return { from: total - (off + 1) * max, to: total - off * max }; } class CompletionTooltip { constructor(view, stateField, applyCompletion) { this.view = view; this.stateField = stateField; this.applyCompletion = applyCompletion; this.info = null; this.infoDestroy = null; this.placeInfoReq = { read: () => this.measureInfo(), write: (pos) => this.placeInfo(pos), key: this }; this.space = null; this.currentClass = ""; let cState = view.state.field(stateField); let { options, selected } = cState.open; let config = view.state.facet(completionConfig); this.optionContent = optionContent(config); this.optionClass = config.optionClass; this.tooltipClass = config.tooltipClass; this.range = rangeAroundSelected(options.length, selected, config.maxRenderedOptions); this.dom = document.createElement("div"); this.dom.className = "cm-tooltip-autocomplete"; this.updateTooltipClass(view.state); this.dom.addEventListener("mousedown", (e) => { let { options } = view.state.field(stateField).open; for (let dom = e.target, match; dom && dom != this.dom; dom = dom.parentNode) { if (dom.nodeName == "LI" && (match = /-(\d+)$/.exec(dom.id)) && +match[1] < options.length) { this.applyCompletion(view, options[+match[1]]); e.preventDefault(); return; } } }); this.dom.addEventListener("focusout", (e) => { let state = view.state.field(this.stateField, false); if (state && state.tooltip && view.state.facet(completionConfig).closeOnBlur && e.relatedTarget != view.contentDOM) view.dispatch({ effects: closeCompletionEffect.of(null) }); }); this.showOptions(options, cState.id); } mount() { this.updateSel(); } showOptions(options, id) { if (this.list) this.list.remove(); this.list = this.dom.appendChild(this.createListBox(options, id, this.range)); this.list.addEventListener("scroll", () => { if (this.info) this.view.requestMeasure(this.placeInfoReq); }); } update(update) { var _a; let cState = update.state.field(this.stateField); let prevState = update.startState.field(this.stateField); this.updateTooltipClass(update.state); if (cState != prevState) { let { options, selected, disabled } = cState.open; if (!prevState.open || prevState.open.options != options) { this.range = rangeAroundSelected(options.length, selected, update.state.facet(completionConfig).maxRenderedOptions); this.showOptions(options, cState.id); } this.updateSel(); if (disabled != ((_a = prevState.open) === null || _a === void 0 ? void 0 : _a.disabled)) this.dom.classList.toggle("cm-tooltip-autocomplete-disabled", !!disabled); } } updateTooltipClass(state) { let cls = this.tooltipClass(state); if (cls != this.currentClass) { for (let c of this.currentClass.split(" ")) if (c) this.dom.classList.remove(c); for (let c of cls.split(" ")) if (c) this.dom.classList.add(c); this.currentClass = cls; } } positioned(space) { this.space = space; if (this.info) this.view.requestMeasure(this.placeInfoReq); } updateSel() { let cState = this.view.state.field(this.stateField), open = cState.open; if (open.selected > -1 && open.selected < this.range.from || open.selected >= this.range.to) { this.range = rangeAroundSelected(open.options.length, open.selected, this.view.state.facet(completionConfig).maxRenderedOptions); this.showOptions(open.options, cState.id); } let newSel = this.updateSelectedOption(open.selected); if (newSel) { this.destroyInfo(); let { completion } = open.options[open.selected]; let { info } = completion; if (!info) return; let infoResult = typeof info === "string" ? document.createTextNode(info) : info(completion); if (!infoResult) return; if ("then" in infoResult) { infoResult.then(obj => { if (obj && this.view.state.field(this.stateField, false) == cState) this.addInfoPane(obj, completion); }).catch(e => logException(this.view.state, e, "completion info")); } else { this.addInfoPane(infoResult, completion); newSel.setAttribute("aria-describedby", this.info.id); } } } addInfoPane(content, completion) { this.destroyInfo(); let wrap = this.info = document.createElement("div"); wrap.className = "cm-tooltip cm-completionInfo"; wrap.id = "cm-completionInfo-" + Math.floor(Math.random() * 0xffff).toString(16); if (content.nodeType != null) { wrap.appendChild(content); this.infoDestroy = null; } else { let { dom, destroy } = content; wrap.appendChild(dom); this.infoDestroy = destroy || null; } this.dom.appendChild(wrap); this.view.requestMeasure(this.placeInfoReq); } updateSelectedOption(selected) { let set = null; for (let opt = this.list.firstChild, i = this.range.from; opt; opt = opt.nextSibling, i++) { if (opt.nodeName != "LI" || !opt.id) { i--; // A section header } else if (i == selected) { if (!opt.hasAttribute("aria-selected")) { opt.setAttribute("aria-selected", "true"); set = opt; } } else { if (opt.hasAttribute("aria-selected")) { opt.removeAttribute("aria-selected"); opt.removeAttribute("aria-describedby"); } } } if (set) scrollIntoView(this.list, set); return set; } measureInfo() { let sel = this.dom.querySelector("[aria-selected]"); if (!sel || !this.info) return null; let listRect = this.dom.getBoundingClientRect(); let infoRect = this.info.getBoundingClientRect(); let selRect = sel.getBoundingClientRect(); let space = this.space; if (!space) { let docElt = this.dom.ownerDocument.documentElement; space = { left: 0, top: 0, right: docElt.clientWidth, bottom: docElt.clientHeight }; } if (selRect.top > Math.min(space.bottom, listRect.bottom) - 10 || selRect.bottom < Math.max(space.top, listRect.top) + 10) return null; return this.view.state.facet(completionConfig).positionInfo(this.view, listRect, selRect, infoRect, space, this.dom); } placeInfo(pos) { if (this.info) { if (pos) { if (pos.style) this.info.style.cssText = pos.style; this.info.className = "cm-tooltip cm-completionInfo " + (pos.class || ""); } else { this.info.style.cssText = "top: -1e6px"; } } } createListBox(options, id, range) { const ul = document.createElement("ul"); ul.id = id; ul.setAttribute("role", "listbox"); ul.setAttribute("aria-expanded", "true"); ul.setAttribute("aria-label", this.view.state.phrase("Completions")); ul.addEventListener("mousedown", e => { // Prevent focus change when clicking the scrollbar if (e.target == ul) e.preventDefault(); }); let curSection = null; for (let i = range.from; i < range.to; i++) { let { completion, match } = options[i], { section } = completion; if (section) { let name = typeof section == "string" ? section : section.name; if (name != curSection && (i > range.from || range.from == 0)) { curSection = name; if (typeof section != "string" && section.header) { ul.appendChild(section.header(section)); } else { let header = ul.appendChild(document.createElement("completion-section")); header.textContent = name; } } } const li = ul.appendChild(document.createElement("li")); li.id = id + "-" + i; li.setAttribute("role", "option"); let cls = this.optionClass(completion); if (cls) li.className = cls; for (let source of this.optionContent) { let node = source(completion, this.view.state, this.view, match); if (node) li.appendChild(node); } } if (range.from) ul.classList.add("cm-completionListIncompleteTop"); if (range.to < options.length) ul.classList.add("cm-completionListIncompleteBottom"); return ul; } destroyInfo() { if (this.info) { if (this.infoDestroy) this.infoDestroy(); this.info.remove(); this.info = null; } } destroy() { this.destroyInfo(); } } function completionTooltip(stateField, applyCompletion) { return (view) => new CompletionTooltip(view, stateField, applyCompletion); } function scrollIntoView(container, element) { let parent = container.getBoundingClientRect(); let self = element.getBoundingClientRect(); let scaleY = parent.height / container.offsetHeight; if (self.top < parent.top) container.scrollTop -= (parent.top - self.top) / scaleY; else if (self.bottom > parent.bottom) container.scrollTop += (self.bottom - parent.bottom) / scaleY; } // Used to pick a preferred option when two options with the same // label occur in the result. function score(option) { return (option.boost || 0) * 100 + (option.apply ? 10 : 0) + (option.info ? 5 : 0) + (option.type ? 1 : 0); } function sortOptions(active, state) { let options = []; let sections = null, dynamicSectionScore = null; let addOption = (option) => { options.push(option); let { section } = option.completion; if (section) { if (!sections) sections = []; let name = typeof section == "string" ? section : section.name; if (!sections.some(s => s.name == name)) sections.push(typeof section == "string" ? { name } : section); } }; let conf = state.facet(completionConfig); for (let a of active) if (a.hasResult()) { let getMatch = a.result.getMatch; if (a.result.filter === false) { for (let option of a.result.options) { addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length)); } } else { let pattern = state.sliceDoc(a.from, a.to), match; let matcher = conf.filterStrict ? new StrictMatcher(pattern) : new FuzzyMatcher(pattern); for (let option of a.result.options) if (match = matcher.match(option.label)) { let matched = !option.displayLabel ? match.matched : getMatch ? getMatch(option, match.matched) : []; let score = match.score + (option.boost || 0); addOption(new Option(option, a.source, matched, score)); if (typeof option.section == "object" && option.section.rank === "dynamic") { let { name } = option.section; if (!dynamicSectionScore) dynamicSectionScore = Object.create(null); dynamicSectionScore[name] = Math.max(score, dynamicSectionScore[name] || -1e9); } } } } if (sections) { let sectionOrder = Object.create(null), pos = 0; let cmp = (a, b) => { return (a.rank === "dynamic" && b.rank === "dynamic" ? dynamicSectionScore[b.name] - dynamicSectionScore[a.name] : 0) || (typeof a.rank == "number" ? a.rank : 1e9) - (typeof b.rank == "number" ? b.rank : 1e9) || (a.name < b.name ? -1 : 1); }; for (let s of sections.sort(cmp)) { pos -= 1e5; sectionOrder[s.name] = pos; } for (let option of options) { let { section } = option.completion; if (section) option.score += sectionOrder[typeof section == "string" ? section : section.name]; } } let result = [], prev = null; let compare = conf.compareCompletions; for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) { let cur = opt.completion; if (!prev || prev.label != cur.label || prev.detail != cur.detail || (prev.type != null && cur.type != null && prev.type != cur.type) || prev.apply != cur.apply || prev.boost != cur.boost) result.push(opt); else if (score(opt.completion) > score(prev)) result[result.length - 1] = opt; prev = opt.completion; } return result; } class CompletionDialog { constructor(options, attrs, tooltip, timestamp, selected, disabled) { this.options = options; this.attrs = attrs; this.tooltip = tooltip; this.timestamp = timestamp; this.selected = selected; this.disabled = disabled; } setSelected(selected, id) { return selected == this.selected || selected >= this.options.length ? this : new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected, this.disabled); } static build(active, state, id, prev, conf, didSetActive) { 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.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; for (let i = 0; i < options.length; i++) if (options[i].completion == selectedValue) { selected = i; break; } } return new CompletionDialog(options, makeAttrs(id, selected), { pos: active.reduce((a, b) => b.hasResult() ? Math.min(a, b.from) : a, 1e8), create: createTooltip, above: conf.aboveCursor, }, prev ? prev.timestamp : Date.now(), selected, false); } map(changes) { return new CompletionDialog(this.options, this.attrs, { ...this.tooltip, pos: changes.mapPos(this.tooltip.pos) }, this.timestamp, this.selected, this.disabled); } setDisabled() { return new CompletionDialog(this.options, this.attrs, this.tooltip, this.timestamp, this.selected, true); } } class CompletionState { constructor(active, id, open) { this.active = active; this.id = id; this.open = open; } static start() { return new CompletionState(none, "cm-ac-" + Math.floor(Math.random() * 2e6).toString(36), null); } update(tr) { let { state } = tr, conf = state.facet(completionConfig); let sources = conf.override || state.languageDataAt("autocomplete", cur(state)).map(asSource); let active = sources.map(source => { let value = this.active.find(s => s.source == source) || new ActiveSource(source, this.active.some(a => a.state != 0 /* State.Inactive */) ? 1 /* State.Pending */ : 0 /* State.Inactive */); return value.update(tr, conf); }); if (active.length == this.active.length && active.every((a, i) => a == this.active[i])) active = this.active; let open = this.open, didSet = tr.effects.some(e => e.is(setActiveEffect)); if (open && tr.docChanged) open = open.map(tr.changes); 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.isPending)) open = null; if (!open && active.every(a => !a.isPending) && active.some(a => a.hasResult())) active = active.map(a => a.hasResult() ? new ActiveSource(a.source, 0 /* State.Inactive */) : a); for (let effect of tr.effects) if (effect.is(setSelectedEffect)) open = open && open.setSelected(effect.value, this.id); return active == this.active && open == this.open ? this : new CompletionState(active, this.id, open); } get tooltip() { return this.open ? this.open.tooltip : null; } get attrs() { return this.open ? this.open.attrs : this.active.length ? baseAttrs : noAttrs; } } function sameResults(a, b) { 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++; let endA = iA == a.length, endB = iB == b.length; if (endA || endB) return endA == endB; if (a[iA++].result != b[iB++].result) return false; } } const baseAttrs = { "aria-autocomplete": "list" }; const noAttrs = {}; function makeAttrs(id, selected) { let result = { "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-controls": id }; if (selected > -1) result["aria-activedescendant"] = id + "-" + selected; return result; } const none = []; function getUpdateType(tr, conf) { if (tr.isUserEvent("input.complete")) { let completion = tr.annotation(pickedCompletion); if (completion && conf.activateOnCompletion(completion)) return 4 /* UpdateType.Activate */ | 8 /* UpdateType.Reset */; } let typing = tr.isUserEvent("input.type"); return typing && conf.activateOnTyping ? 4 /* UpdateType.Activate */ | 1 /* UpdateType.Typing */ : typing ? 1 /* UpdateType.Typing */ : tr.isUserEvent("delete.backward") ? 2 /* UpdateType.Backspacing */ : tr.selection ? 8 /* UpdateType.Reset */ : tr.docChanged ? 16 /* UpdateType.ResetIfTouching */ : 0 /* UpdateType.None */; } class ActiveSource { constructor(source, state, explicit = false) { this.source = source; this.state = state; this.explicit = explicit; } hasResult() { return false; } get isPending() { return this.state == 1 /* State.Pending */; } update(tr, conf) { let type = getUpdateType(tr, conf), value = this; if ((type & 8 /* UpdateType.Reset */) || (type & 16 /* UpdateType.ResetIfTouching */) && this.touches(tr)) value = new ActiveSource(value.source, 0 /* State.Inactive */); if ((type & 4 /* UpdateType.Activate */) && value.state == 0 /* State.Inactive */) value = new ActiveSource(this.source, 1 /* State.Pending */); value = value.updateFor(tr, type); for (let effect of tr.effects) { if (effect.is(startCompletionEffect)) value = new ActiveSource(value.source, 1 /* State.Pending */, effect.value); else if (effect.is(closeCompletionEffect)) value = new ActiveSource(value.source, 0 /* State.Inactive */); else if (effect.is(setActiveEffect)) for (let active of effect.value) if (active.source == value.source) value = active; } return value; } updateFor(tr, type) { return this.map(tr.changes); } map(changes) { return this; } touches(tr) { return tr.changes.touchesRange(cur(tr.state)); } } class ActiveResult extends ActiveSource { constructor(source, explicit, limit, result, from, to) { super(source, 3 /* State.Result */, explicit); this.limit = limit; this.result = result; this.from = from; this.to = to; } hasResult() { return true; } updateFor(tr, type) { var _a; if (!(type & 3 /* UpdateType.SimpleInteraction */)) return this.map(tr.changes); let result = this.result; 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 (pos > to || !result || (type & 2 /* UpdateType.Backspacing */) && (cur(tr.startState) == this.from || pos < this.limit)) return new ActiveSource(this.source, type & 4 /* UpdateType.Activate */ ? 1 /* State.Pending */ : 0 /* State.Inactive */); let limit = tr.changes.mapPos(this.limit); if (checkValid(result.validFor, tr.state, 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, false)))) return new ActiveResult(this.source, this.explicit, limit, result, result.from, (_a = result.to) !== null && _a !== void 0 ? _a : cur(tr.state)); return new ActiveSource(this.source, 1 /* State.Pending */, this.explicit); } map(mapping) { 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, 0 /* State.Inactive */); return new ActiveResult(this.source, this.explicit, mapping.mapPos(this.limit), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1)); } touches(tr) { return tr.changes.touchesRange(this.from, this.to); } } function checkValid(validFor, state, from, to) { if (!validFor) return false; let text = state.sliceDoc(from, to); return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text); } const setActiveEffect = /*@__PURE__*/StateEffect.define({ map(sources, mapping) { return sources.map(s => s.map(mapping)); } }); const setSelectedEffect = /*@__PURE__*/StateEffect.define(); const completionState = /*@__PURE__*/StateField.define({ create() { return CompletionState.start(); }, update(value, tr) { return value.update(tr); }, provide: f => [ showTooltip.from(f, val => val.tooltip), EditorView.contentAttributes.from(f, state => state.attrs) ] }); function applyCompletion(view, option) { const apply = option.completion.apply || option.completion.label; let result = view.state.field(completionState).active.find(a => a.source == option.source); if (!(result instanceof ActiveResult)) return false; if (typeof apply == "string") view.dispatch({ ...insertCompletionText(view.state, apply, result.from, result.to), annotations: pickedCompletion.of(option.completion) }); else apply(view, option.completion, result.from, result.to); return true; } const createTooltip = /*@__PURE__*/completionTooltip(completionState, applyCompletion); /** Returns a command that moves the completion selection forward or backward by the given amount. */ function moveCompletionSelection(forward, by = "option") { return (view) => { let cState = view.state.field(completionState, false); if (!cState || !cState.open || cState.open.disabled || Date.now() - cState.open.timestamp < view.state.facet(completionConfig).interactionDelay) return false; let step = 1, tooltip; if (by == "page" && (tooltip = getTooltip(view, cState.open.tooltip))) step = Math.max(2, Math.floor(tooltip.dom.offsetHeight / tooltip.dom.querySelector("li").offsetHeight) - 1); let { length } = cState.open.options; let selected = cState.open.selected > -1 ? cState.open.selected + step * (forward ? 1 : -1) : forward ? 0 : length - 1; if (selected < 0) selected = by == "page" ? 0 : length - 1; else if (selected >= length) selected = by == "page" ? length - 1 : 0; view.dispatch({ effects: setSelectedEffect.of(selected) }); return true; }; } /** Accept the current completion. */ const acceptCompletion = (view) => { let cState = view.state.field(completionState, false); if (view.state.readOnly || !cState || !cState.open || cState.open.selected < 0 || cState.open.disabled || Date.now() - cState.open.timestamp < view.state.facet(completionConfig).interactionDelay) return false; return applyCompletion(view, cState.open.options[cState.open.selected]); }; /** Explicitly start autocompletion. */ const startCompletion = (view) => { let cState = view.state.field(completionState, false); if (!cState) return false; view.dispatch({ effects: startCompletionEffect.of(true) }); return true; }; /** Close the currently active completion. */ const closeCompletion = (view) => { let cState = view.state.field(completionState, false); if (!cState || !cState.active.some(a => a.state != 0 /* State.Inactive */)) return false; view.dispatch({ effects: closeCompletionEffect.of(null) }); return true; }; class RunningQuery { constructor(active, context) { this.active = active; this.context = context; this.time = Date.now(); this.updates = []; // Note that 'undefined' means 'not done yet', whereas 'null' means // 'query returned null'. this.done = undefined; } } const MaxUpdateCount = 50, MinAbortTime = 1000; const completionPlugin = /*@__PURE__*/ViewPlugin.fromClass(class { constructor(view) { this.view = view; this.debounceUpdate = -1; this.running = []; this.debounceAccept = -1; this.pendingStart = false; this.composing = 0 /* CompositionState.None */; for (let active of view.state.field(completionState).active) if (active.isPending) this.startQuery(active); } update(update) { let cState = update.state.field(completionState); let conf = update.state.facet(completionConfig); if (!update.selectionSet && !update.docChanged && update.startState.field(completionState) == cState) return; let doesReset = update.transactions.some(tr => { let type = getUpdateType(tr, conf); return (type & 8 /* UpdateType.Reset */) || (tr.selection || tr.docChanged) && !(type & 3 /* UpdateType.SimpleInteraction */); }); for (let i = 0; i < this.running.length; i++) { let query = this.running[i]; if (doesReset || query.context.abortOnDocChange && update.docChanged || query.updates.length + update.transactions.length > MaxUpdateCount && Date.now() - query.time > MinAbortTime) { for (let handler of query.context.abortListeners) { try { handler(); } catch (e) { logException(this.view.state, e); } } query.context.abortListeners = null; this.running.splice(i--, 1); } else { query.updates.push(...update.transactions); } } 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.isPending && !this.running.some(q => q.active.source == a.source)) ? setTimeout(() => this.startUpdate(), delay) : -1; if (this.composing != 0 /* CompositionState.None */) for (let tr of update.transactions) { if (tr.isUserEvent("input.type"))