|
'use strict'; |
|
|
|
var state = require('@codemirror/state'); |
|
var view = require('@codemirror/view'); |
|
var language = require('@codemirror/language'); |
|
|
|
|
|
|
|
|
|
class CompletionContext { |
|
|
|
|
|
|
|
|
|
|
|
constructor( |
|
|
|
|
|
|
|
state, |
|
|
|
|
|
|
|
pos, |
|
|
|
|
|
|
|
|
|
|
|
|
|
explicit) { |
|
this.state = state; |
|
this.pos = pos; |
|
this.explicit = explicit; |
|
|
|
|
|
|
|
this.abortListeners = []; |
|
} |
|
|
|
|
|
|
|
|
|
tokenBefore(types) { |
|
let token = language.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; |
|
} |
|
|
|
|
|
|
|
|
|
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) }; |
|
} |
|
|
|
|
|
|
|
|
|
get aborted() { return this.abortListeners == null; } |
|
|
|
|
|
|
|
|
|
|
|
addEventListener(type, listener) { |
|
if (type == "abort" && this.abortListeners) |
|
this.abortListeners.push(listener); |
|
} |
|
} |
|
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)]; |
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
function ifIn(nodes, source) { |
|
return (context) => { |
|
for (let pos = language.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; |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
function ifNotIn(nodes, source) { |
|
return (context) => { |
|
for (let pos = language.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; } |
|
|
|
|
|
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" : "")); |
|
} |
|
|
|
|
|
|
|
|
|
const pickedCompletion = state.Annotation.define(); |
|
|
|
|
|
|
|
|
|
|
|
function insertCompletionText(state$1, text, from, to) { |
|
let { main } = state$1.selection, fromOff = from - main.from, toOff = to - main.from; |
|
return Object.assign(Object.assign({}, state$1.changeByRange(range => { |
|
if (range != main && from != to && |
|
state$1.sliceDoc(range.from + fromOff, range.from + toOff) != state$1.sliceDoc(from, to)) |
|
return { range }; |
|
return { |
|
changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: text }, |
|
range: state.EditorSelection.cursor(range.from + fromOff + text.length) |
|
}; |
|
})), { scrollIntoView: true, userEvent: "input.complete" }); |
|
} |
|
const SourceCache = 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 = state.StateEffect.define(); |
|
const closeCompletionEffect = state.StateEffect.define(); |
|
|
|
|
|
|
|
|
|
class FuzzyMatcher { |
|
constructor(pattern) { |
|
this.pattern = pattern; |
|
this.chars = []; |
|
this.folded = []; |
|
|
|
|
|
this.any = []; |
|
this.precise = []; |
|
this.byWord = []; |
|
this.score = 0; |
|
this.matched = []; |
|
for (let p = 0; p < pattern.length;) { |
|
let char = state.codePointAt(pattern, p), size = state.codePointSize(char); |
|
this.chars.push(char); |
|
let part = pattern.slice(p, p + size), upper = part.toUpperCase(); |
|
this.folded.push(state.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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
match(word) { |
|
if (this.pattern.length == 0) |
|
return this.ret(-100 , []); |
|
if (word.length < this.pattern.length) |
|
return null; |
|
let { chars, folded, any, precise, byWord } = this; |
|
|
|
|
|
if (chars.length == 1) { |
|
let first = state.codePointAt(word, 0), firstSize = state.codePointSize(first); |
|
let score = firstSize == word.length ? 0 : -100 ; |
|
if (first == chars[0]) ; |
|
else if (first == folded[0]) |
|
score += -200 ; |
|
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 , [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 = state.codePointAt(word, i); |
|
if (next == chars[anyTo] || next == folded[anyTo]) |
|
any[anyTo++] = i; |
|
i += state.codePointSize(next); |
|
} |
|
|
|
if (anyTo < len) |
|
return null; |
|
} |
|
|
|
|
|
let preciseTo = 0; |
|
|
|
|
|
|
|
let byWordTo = 0, byWordFolded = false; |
|
|
|
let adjacentTo = 0, adjacentStart = -1, adjacentEnd = -1; |
|
let hasLower = /[a-z]/.test(word), wordAdjacent = true; |
|
|
|
for (let i = 0, e = Math.min(word.length, 200), prevType = 0 ; i < e && byWordTo < len;) { |
|
let next = state.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 : next >= 65 && next <= 90 ? 1 : 0 ) |
|
: ((ch = state.fromCodePoint(next)) != ch.toLowerCase() ? 1 : ch != ch.toUpperCase() ? 2 : 0 ); |
|
if (!i || type == 1 && hasLower || prevType == 0 && type != 0 ) { |
|
if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true))) |
|
byWord[byWordTo++] = i; |
|
else if (byWord.length) |
|
wordAdjacent = false; |
|
} |
|
prevType = type; |
|
i += state.codePointSize(next); |
|
} |
|
if (byWordTo == len && byWord[0] == 0 && wordAdjacent) |
|
return this.result(-100 + (byWordFolded ? -200 : 0), byWord, word); |
|
if (adjacentTo == len && adjacentStart == 0) |
|
return this.ret(-200 - word.length + (adjacentEnd == word.length ? 0 : -100 ), [0, adjacentEnd]); |
|
if (direct > -1) |
|
return this.ret(-700 - word.length, [direct, direct + this.pattern.length]); |
|
if (adjacentTo == len) |
|
return this.ret(-200 + -700 - word.length, [adjacentStart, adjacentEnd]); |
|
if (byWordTo == len) |
|
return this.result(-100 + (byWordFolded ? -200 : 0) + -700 + |
|
(wordAdjacent ? 0 : -1100 ), byWord, word); |
|
return chars.length == 2 ? null |
|
: this.result((any[0] ? -700 : 0) + -200 + -1100 , any, word); |
|
} |
|
result(score, positions, word) { |
|
let result = [], i = 0; |
|
for (let pos of positions) { |
|
let to = pos + (this.astral ? state.codePointSize(state.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 : null; |
|
if (match == null) |
|
return null; |
|
this.matched = [0, start.length]; |
|
this.score = match + (word.length == this.pattern.length ? 0 : -100 ); |
|
return this; |
|
} |
|
} |
|
|
|
const completionConfig = state.Facet.define({ |
|
combine(configs) { |
|
return state.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$1, list, option, info, space, tooltip) { |
|
let rtl = view$1.textDirection == view.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 , left ? spaceLeft : spaceRight); |
|
} |
|
else { |
|
narrow = true; |
|
maxWidth = Math.min(400 , (rtl ? list.right : space.right - list.left) - 30 ); |
|
let spaceBelow = space.bottom - list.bottom; |
|
if (spaceBelow >= infoHeight || spaceBelow > list.top) { |
|
offset = option.bottom - list.top; |
|
} |
|
else { |
|
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); |
|
} |
|
if (this.updateSelectedOption(open.selected)) { |
|
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 => view.logException(this.view.state, e, "completion info")); |
|
} |
|
else { |
|
this.addInfoPane(infoResult, completion); |
|
} |
|
} |
|
} |
|
addInfoPane(content, completion) { |
|
this.destroyInfo(); |
|
let wrap = this.info = document.createElement("div"); |
|
wrap.className = "cm-tooltip cm-completionInfo"; |
|
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--; |
|
} |
|
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"); |
|
} |
|
} |
|
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 win = this.dom.ownerDocument.defaultView || window; |
|
space = { left: 0, top: 0, right: win.innerWidth, bottom: win.innerHeight }; |
|
} |
|
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")); |
|
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; |
|
} |
|
|
|
|
|
|
|
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; |
|
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) : []; |
|
addOption(new Option(option, a.source, matched, match.score + (option.boost || 0))); |
|
} |
|
} |
|
} |
|
if (sections) { |
|
let sectionOrder = Object.create(null), pos = 0; |
|
let cmp = (a, b) => { var _a, _b; return ((_a = a.rank) !== null && _a !== void 0 ? _a : 1e9) - ((_b = b.rank) !== null && _b !== void 0 ? _b : 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) { |
|
let options = sortOptions(active, state); |
|
if (!options.length) { |
|
return prev && active.some(a => a.state == 1 ) ? |
|
new CompletionDialog(prev.options, prev.attrs, prev.tooltip, prev.timestamp, prev.selected, true) : 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, Object.assign(Object.assign({}, this.tooltip), { pos: changes.mapPos(this.tooltip.pos) }), this.timestamp, this.selected, this.disabled); |
|
} |
|
} |
|
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 ) ? 1 : 0 ); |
|
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; |
|
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)) |
|
open = CompletionDialog.build(active, state, this.id, open, conf); |
|
else if (open && open.disabled && !active.some(a => a.state == 1 )) |
|
open = null; |
|
if (!open && active.every(a => a.state != 1 ) && active.some(a => a.hasResult())) |
|
active = active.map(a => a.hasResult() ? new ActiveSource(a.source, 0 ) : 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 : baseAttrs; } |
|
} |
|
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" |
|
}; |
|
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 getUserEvent(tr, conf) { |
|
if (tr.isUserEvent("input.complete")) { |
|
let completion = tr.annotation(pickedCompletion); |
|
if (completion && conf.activateOnCompletion(completion)) |
|
return "input"; |
|
} |
|
return tr.isUserEvent("input.type") ? "input" : tr.isUserEvent("delete.backward") ? "delete" : null; |
|
} |
|
class ActiveSource { |
|
constructor(source, state, explicitPos = -1) { |
|
this.source = source; |
|
this.state = state; |
|
this.explicitPos = explicitPos; |
|
} |
|
hasResult() { return false; } |
|
update(tr, conf) { |
|
let event = getUserEvent(tr, conf), value = this; |
|
if (event) |
|
value = value.handleUserEvent(tr, event, conf); |
|
else if (tr.docChanged) |
|
value = value.handleChange(tr); |
|
else if (tr.selection && value.state != 0 ) |
|
value = new ActiveSource(value.source, 0 ); |
|
for (let effect of tr.effects) { |
|
if (effect.is(startCompletionEffect)) |
|
value = new ActiveSource(value.source, 1 , effect.value ? cur(tr.state) : -1); |
|
else if (effect.is(closeCompletionEffect)) |
|
value = new ActiveSource(value.source, 0 ); |
|
else if (effect.is(setActiveEffect)) |
|
for (let active of effect.value) |
|
if (active.source == value.source) |
|
value = active; |
|
} |
|
return value; |
|
} |
|
handleUserEvent(tr, type, conf) { |
|
return type == "delete" || !conf.activateOnTyping ? this.map(tr.changes) : new ActiveSource(this.source, 1 ); |
|
} |
|
handleChange(tr) { |
|
return tr.changes.touchesRange(cur(tr.startState)) ? new ActiveSource(this.source, 0 ) : this.map(tr.changes); |
|
} |
|
map(changes) { |
|
return changes.empty || this.explicitPos < 0 ? this : new ActiveSource(this.source, this.state, changes.mapPos(this.explicitPos)); |
|
} |
|
} |
|
class ActiveResult extends ActiveSource { |
|
constructor(source, explicitPos, result, from, to) { |
|
super(source, 2 , explicitPos); |
|
this.result = result; |
|
this.from = from; |
|
this.to = to; |
|
} |
|
hasResult() { return true; } |
|
handleUserEvent(tr, type, conf) { |
|
var _a; |
|
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 ((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 ? 1 : 0 ); |
|
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); |
|
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, (_a = result.to) !== null && _a !== void 0 ? _a : cur(tr.state)); |
|
return new ActiveSource(this.source, 1 , explicitPos); |
|
} |
|
handleChange(tr) { |
|
return tr.changes.touchesRange(this.from, this.to) ? new ActiveSource(this.source, 0 ) : this.map(tr.changes); |
|
} |
|
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 ); |
|
return new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1)); |
|
} |
|
} |
|
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 = state.StateEffect.define({ |
|
map(sources, mapping) { return sources.map(s => s.map(mapping)); } |
|
}); |
|
const setSelectedEffect = state.StateEffect.define(); |
|
const completionState = state.StateField.define({ |
|
create() { return CompletionState.start(); }, |
|
update(value, tr) { return value.update(tr); }, |
|
provide: f => [ |
|
view.showTooltip.from(f, val => val.tooltip), |
|
view.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(Object.assign(Object.assign({}, 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 = completionTooltip(completionState, applyCompletion); |
|
|
|
|
|
|
|
|
|
|
|
function moveCompletionSelection(forward, by = "option") { |
|
return (view$1) => { |
|
let cState = view$1.state.field(completionState, false); |
|
if (!cState || !cState.open || cState.open.disabled || |
|
Date.now() - cState.open.timestamp < view$1.state.facet(completionConfig).interactionDelay) |
|
return false; |
|
let step = 1, tooltip; |
|
if (by == "page" && (tooltip = view.getTooltip(view$1, 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$1.dispatch({ effects: setSelectedEffect.of(selected) }); |
|
return true; |
|
}; |
|
} |
|
|
|
|
|
|
|
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]); |
|
}; |
|
|
|
|
|
|
|
const startCompletion = (view) => { |
|
let cState = view.state.field(completionState, false); |
|
if (!cState) |
|
return false; |
|
view.dispatch({ effects: startCompletionEffect.of(true) }); |
|
return true; |
|
}; |
|
|
|
|
|
|
|
const closeCompletion = (view) => { |
|
let cState = view.state.field(completionState, false); |
|
if (!cState || !cState.active.some(a => a.state != 0 )) |
|
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 = []; |
|
|
|
|
|
this.done = undefined; |
|
} |
|
} |
|
const MaxUpdateCount = 50, MinAbortTime = 1000; |
|
const completionPlugin = view.ViewPlugin.fromClass(class { |
|
constructor(view) { |
|
this.view = view; |
|
this.debounceUpdate = -1; |
|
this.running = []; |
|
this.debounceAccept = -1; |
|
this.pendingStart = false; |
|
this.composing = 0 ; |
|
for (let active of view.state.field(completionState).active) |
|
if (active.state == 1 ) |
|
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 => { |
|
return (tr.selection || tr.docChanged) && !getUserEvent(tr, conf); |
|
}); |
|
for (let i = 0; i < this.running.length; i++) { |
|
let query = this.running[i]; |
|
if (doesReset || |
|
query.updates.length + update.transactions.length > MaxUpdateCount && Date.now() - query.time > MinAbortTime) { |
|
for (let handler of query.context.abortListeners) { |
|
try { |
|
handler(); |
|
} |
|
catch (e) { |
|
view.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.state == 1 && !this.running.some(q => q.active.source == a.source)) |
|
? setTimeout(() => this.startUpdate(), delay) : -1; |
|
if (this.composing != 0 ) |
|
for (let tr of update.transactions) { |
|
if (getUserEvent(tr, conf) == "input") |
|
this.composing = 2 ; |
|
else if (this.composing == 2 && tr.selection) |
|
this.composing = 3 ; |
|
} |
|
} |
|
startUpdate() { |
|
this.debounceUpdate = -1; |
|
this.pendingStart = false; |
|
let { state } = this.view, cState = state.field(completionState); |
|
for (let active of cState.active) { |
|
if (active.state == 1 && !this.running.some(r => r.active.source == active.source)) |
|
this.startQuery(active); |
|
} |
|
} |
|
startQuery(active) { |
|
let { state } = this.view, pos = cur(state); |
|
let context = new CompletionContext(state, pos, active.explicitPos == pos); |
|
let pending = new RunningQuery(active, context); |
|
this.running.push(pending); |
|
Promise.resolve(active.source(context)).then(result => { |
|
if (!pending.context.aborted) { |
|
pending.done = result || null; |
|
this.scheduleAccept(); |
|
} |
|
}, err => { |
|
this.view.dispatch({ effects: closeCompletionEffect.of(null) }); |
|
view.logException(this.view.state, err); |
|
}); |
|
} |
|
scheduleAccept() { |
|
if (this.running.every(q => q.done !== undefined)) |
|
this.accept(); |
|
else if (this.debounceAccept < 0) |
|
this.debounceAccept = setTimeout(() => this.accept(), this.view.state.facet(completionConfig).updateSyncTime); |
|
} |
|
|
|
|
|
accept() { |
|
var _a; |
|
if (this.debounceAccept > -1) |
|
clearTimeout(this.debounceAccept); |
|
this.debounceAccept = -1; |
|
let updated = []; |
|
let conf = this.view.state.facet(completionConfig); |
|
for (let i = 0; i < this.running.length; i++) { |
|
let query = this.running[i]; |
|
if (query.done === undefined) |
|
continue; |
|
this.running.splice(i--, 1); |
|
if (query.done) { |
|
let active = new ActiveResult(query.active.source, query.active.explicitPos, query.done, query.done.from, (_a = query.done.to) !== null && _a !== void 0 ? _a : cur(query.updates.length ? query.updates[0].startState : this.view.state)); |
|
|
|
|
|
for (let tr of query.updates) |
|
active = active.update(tr, conf); |
|
if (active.hasResult()) { |
|
updated.push(active); |
|
continue; |
|
} |
|
} |
|
let current = this.view.state.field(completionState).active.find(a => a.source == query.active.source); |
|
if (current && current.state == 1 ) { |
|
if (query.done == null) { |
|
|
|
|
|
let active = new ActiveSource(query.active.source, 0 ); |
|
for (let tr of query.updates) |
|
active = active.update(tr, conf); |
|
if (active.state != 1 ) |
|
updated.push(active); |
|
} |
|
else { |
|
|
|
this.startQuery(current); |
|
} |
|
} |
|
} |
|
if (updated.length) |
|
this.view.dispatch({ effects: setActiveEffect.of(updated) }); |
|
} |
|
}, { |
|
eventHandlers: { |
|
blur(event) { |
|
let state = this.view.state.field(completionState, false); |
|
if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur) { |
|
let dialog = state.open && view.getTooltip(this.view, state.open.tooltip); |
|
if (!dialog || !dialog.dom.contains(event.relatedTarget)) |
|
setTimeout(() => this.view.dispatch({ effects: closeCompletionEffect.of(null) }), 10); |
|
} |
|
}, |
|
compositionstart() { |
|
this.composing = 1 ; |
|
}, |
|
compositionend() { |
|
if (this.composing == 3 ) { |
|
|
|
|
|
setTimeout(() => this.view.dispatch({ effects: startCompletionEffect.of(false) }), 20); |
|
} |
|
this.composing = 0 ; |
|
} |
|
} |
|
}); |
|
const windows = typeof navigator == "object" && /Win/.test(navigator.platform); |
|
const commitCharacters = state.Prec.highest(view.EditorView.domEventHandlers({ |
|
keydown(event, view) { |
|
let field = view.state.field(completionState, false); |
|
if (!field || !field.open || field.open.disabled || field.open.selected < 0 || |
|
event.key.length > 1 || event.ctrlKey && !(windows && event.altKey) || event.metaKey) |
|
return false; |
|
let option = field.open.options[field.open.selected]; |
|
let result = field.active.find(a => a.source == option.source); |
|
let commitChars = option.completion.commitCharacters || result.result.commitCharacters; |
|
if (commitChars && commitChars.indexOf(event.key) > -1) |
|
applyCompletion(view, option); |
|
return false; |
|
} |
|
})); |
|
|
|
const baseTheme = view.EditorView.baseTheme({ |
|
".cm-tooltip.cm-tooltip-autocomplete": { |
|
"& > ul": { |
|
fontFamily: "monospace", |
|
whiteSpace: "nowrap", |
|
overflow: "hidden auto", |
|
maxWidth_fallback: "700px", |
|
maxWidth: "min(700px, 95vw)", |
|
minWidth: "250px", |
|
maxHeight: "10em", |
|
height: "100%", |
|
listStyle: "none", |
|
margin: 0, |
|
padding: 0, |
|
"& > li, & > completion-section": { |
|
padding: "1px 3px", |
|
lineHeight: 1.2 |
|
}, |
|
"& > li": { |
|
overflowX: "hidden", |
|
textOverflow: "ellipsis", |
|
cursor: "pointer" |
|
}, |
|
"& > completion-section": { |
|
display: "list-item", |
|
borderBottom: "1px solid silver", |
|
paddingLeft: "0.5em", |
|
opacity: 0.7 |
|
} |
|
} |
|
}, |
|
"&light .cm-tooltip-autocomplete ul li[aria-selected]": { |
|
background: "#17c", |
|
color: "white", |
|
}, |
|
"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]": { |
|
background: "#777", |
|
}, |
|
"&dark .cm-tooltip-autocomplete ul li[aria-selected]": { |
|
background: "#347", |
|
color: "white", |
|
}, |
|
"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]": { |
|
background: "#444", |
|
}, |
|
".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after": { |
|
content: '"···"', |
|
opacity: 0.5, |
|
display: "block", |
|
textAlign: "center" |
|
}, |
|
".cm-tooltip.cm-completionInfo": { |
|
position: "absolute", |
|
padding: "3px 9px", |
|
width: "max-content", |
|
maxWidth: `${400 /* Info.Width */}px`, |
|
boxSizing: "border-box" |
|
}, |
|
".cm-completionInfo.cm-completionInfo-left": { right: "100%" }, |
|
".cm-completionInfo.cm-completionInfo-right": { left: "100%" }, |
|
".cm-completionInfo.cm-completionInfo-left-narrow": { right: `${30 /* Info.Margin */}px` }, |
|
".cm-completionInfo.cm-completionInfo-right-narrow": { left: `${30 /* Info.Margin */}px` }, |
|
"&light .cm-snippetField": { backgroundColor: "#00000022" }, |
|
"&dark .cm-snippetField": { backgroundColor: "#ffffff22" }, |
|
".cm-snippetFieldPosition": { |
|
verticalAlign: "text-top", |
|
width: 0, |
|
height: "1.15em", |
|
display: "inline-block", |
|
margin: "0 -0.7px -.7em", |
|
borderLeft: "1.4px dotted #888" |
|
}, |
|
".cm-completionMatchedText": { |
|
textDecoration: "underline" |
|
}, |
|
".cm-completionDetail": { |
|
marginLeft: "0.5em", |
|
fontStyle: "italic" |
|
}, |
|
".cm-completionIcon": { |
|
fontSize: "90%", |
|
width: ".8em", |
|
display: "inline-block", |
|
textAlign: "center", |
|
paddingRight: ".6em", |
|
opacity: "0.6", |
|
boxSizing: "content-box" |
|
}, |
|
".cm-completionIcon-function, .cm-completionIcon-method": { |
|
"&:after": { content: "'ƒ'" } |
|
}, |
|
".cm-completionIcon-class": { |
|
"&:after": { content: "'○'" } |
|
}, |
|
".cm-completionIcon-interface": { |
|
"&:after": { content: "'◌'" } |
|
}, |
|
".cm-completionIcon-variable": { |
|
"&:after": { content: "'𝑥'" } |
|
}, |
|
".cm-completionIcon-constant": { |
|
"&:after": { content: "'𝐶'" } |
|
}, |
|
".cm-completionIcon-type": { |
|
"&:after": { content: "'𝑡'" } |
|
}, |
|
".cm-completionIcon-enum": { |
|
"&:after": { content: "'∪'" } |
|
}, |
|
".cm-completionIcon-property": { |
|
"&:after": { content: "'□'" } |
|
}, |
|
".cm-completionIcon-keyword": { |
|
"&:after": { content: "'🔑\uFE0E'" } |
|
}, |
|
".cm-completionIcon-namespace": { |
|
"&:after": { content: "'▢'" } |
|
}, |
|
".cm-completionIcon-text": { |
|
"&:after": { content: "'abc'", fontSize: "50%", verticalAlign: "middle" } |
|
} |
|
}); |
|
|
|
class FieldPos { |
|
constructor(field, line, from, to) { |
|
this.field = field; |
|
this.line = line; |
|
this.from = from; |
|
this.to = to; |
|
} |
|
} |
|
class FieldRange { |
|
constructor(field, from, to) { |
|
this.field = field; |
|
this.from = from; |
|
this.to = to; |
|
} |
|
map(changes) { |
|
let from = changes.mapPos(this.from, -1, state.MapMode.TrackDel); |
|
let to = changes.mapPos(this.to, 1, state.MapMode.TrackDel); |
|
return from == null || to == null ? null : new FieldRange(this.field, from, to); |
|
} |
|
} |
|
class Snippet { |
|
constructor(lines, fieldPositions) { |
|
this.lines = lines; |
|
this.fieldPositions = fieldPositions; |
|
} |
|
instantiate(state, pos) { |
|
let text = [], lineStart = [pos]; |
|
let lineObj = state.doc.lineAt(pos), baseIndent = /^\s*/.exec(lineObj.text)[0]; |
|
for (let line of this.lines) { |
|
if (text.length) { |
|
let indent = baseIndent, tabs = /^\t*/.exec(line)[0].length; |
|
for (let i = 0; i < tabs; i++) |
|
indent += state.facet(language.indentUnit); |
|
lineStart.push(pos + indent.length - tabs); |
|
line = indent + line.slice(tabs); |
|
} |
|
text.push(line); |
|
pos += line.length + 1; |
|
} |
|
let ranges = this.fieldPositions.map(pos => new FieldRange(pos.field, lineStart[pos.line] + pos.from, lineStart[pos.line] + pos.to)); |
|
return { text, ranges }; |
|
} |
|
static parse(template) { |
|
let fields = []; |
|
let lines = [], positions = [], m; |
|
for (let line of template.split(/\r\n?|\n/)) { |
|
while (m = /[#$]\{(?:(\d+)(?::([^}]*))?|((?:\\[{}]|[^}])*))\}/.exec(line)) { |
|
let seq = m[1] ? +m[1] : null, rawName = m[2] || m[3] || "", found = -1; |
|
let name = rawName.replace(/\\[{}]/g, m => m[1]); |
|
for (let i = 0; i < fields.length; i++) { |
|
if (seq != null ? fields[i].seq == seq : name ? fields[i].name == name : false) |
|
found = i; |
|
} |
|
if (found < 0) { |
|
let i = 0; |
|
while (i < fields.length && (seq == null || (fields[i].seq != null && fields[i].seq < seq))) |
|
i++; |
|
fields.splice(i, 0, { seq, name }); |
|
found = i; |
|
for (let pos of positions) |
|
if (pos.field >= found) |
|
pos.field++; |
|
} |
|
positions.push(new FieldPos(found, lines.length, m.index, m.index + name.length)); |
|
line = line.slice(0, m.index) + rawName + line.slice(m.index + m[0].length); |
|
} |
|
line = line.replace(/\\([{}])/g, (_, brace, index) => { |
|
for (let pos of positions) |
|
if (pos.line == lines.length && pos.from > index) { |
|
pos.from--; |
|
pos.to--; |
|
} |
|
return brace; |
|
}); |
|
lines.push(line); |
|
} |
|
return new Snippet(lines, positions); |
|
} |
|
} |
|
let fieldMarker = view.Decoration.widget({ widget: new class extends view.WidgetType { |
|
toDOM() { |
|
let span = document.createElement("span"); |
|
span.className = "cm-snippetFieldPosition"; |
|
return span; |
|
} |
|
ignoreEvent() { return false; } |
|
} }); |
|
let fieldRange = view.Decoration.mark({ class: "cm-snippetField" }); |
|
class ActiveSnippet { |
|
constructor(ranges, active) { |
|
this.ranges = ranges; |
|
this.active = active; |
|
this.deco = view.Decoration.set(ranges.map(r => (r.from == r.to ? fieldMarker : fieldRange).range(r.from, r.to))); |
|
} |
|
map(changes) { |
|
let ranges = []; |
|
for (let r of this.ranges) { |
|
let mapped = r.map(changes); |
|
if (!mapped) |
|
return null; |
|
ranges.push(mapped); |
|
} |
|
return new ActiveSnippet(ranges, this.active); |
|
} |
|
selectionInsideField(sel) { |
|
return sel.ranges.every(range => this.ranges.some(r => r.field == this.active && r.from <= range.from && r.to >= range.to)); |
|
} |
|
} |
|
const setActive = state.StateEffect.define({ |
|
map(value, changes) { return value && value.map(changes); } |
|
}); |
|
const moveToField = state.StateEffect.define(); |
|
const snippetState = state.StateField.define({ |
|
create() { return null; }, |
|
update(value, tr) { |
|
for (let effect of tr.effects) { |
|
if (effect.is(setActive)) |
|
return effect.value; |
|
if (effect.is(moveToField) && value) |
|
return new ActiveSnippet(value.ranges, effect.value); |
|
} |
|
if (value && tr.docChanged) |
|
value = value.map(tr.changes); |
|
if (value && tr.selection && !value.selectionInsideField(tr.selection)) |
|
value = null; |
|
return value; |
|
}, |
|
provide: f => view.EditorView.decorations.from(f, val => val ? val.deco : view.Decoration.none) |
|
}); |
|
function fieldSelection(ranges, field) { |
|
return state.EditorSelection.create(ranges.filter(r => r.field == field).map(r => state.EditorSelection.range(r.from, r.to))); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function snippet(template) { |
|
let snippet = Snippet.parse(template); |
|
return (editor, completion, from, to) => { |
|
let { text, ranges } = snippet.instantiate(editor.state, from); |
|
let spec = { |
|
changes: { from, to, insert: state.Text.of(text) }, |
|
scrollIntoView: true, |
|
annotations: completion ? [pickedCompletion.of(completion), state.Transaction.userEvent.of("input.complete")] : undefined |
|
}; |
|
if (ranges.length) |
|
spec.selection = fieldSelection(ranges, 0); |
|
if (ranges.some(r => r.field > 0)) { |
|
let active = new ActiveSnippet(ranges, 0); |
|
let effects = spec.effects = [setActive.of(active)]; |
|
if (editor.state.field(snippetState, false) === undefined) |
|
effects.push(state.StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme])); |
|
} |
|
editor.dispatch(editor.state.update(spec)); |
|
}; |
|
} |
|
function moveField(dir) { |
|
return ({ state, dispatch }) => { |
|
let active = state.field(snippetState, false); |
|
if (!active || dir < 0 && active.active == 0) |
|
return false; |
|
let next = active.active + dir, last = dir > 0 && !active.ranges.some(r => r.field == next + dir); |
|
dispatch(state.update({ |
|
selection: fieldSelection(active.ranges, next), |
|
effects: setActive.of(last ? null : new ActiveSnippet(active.ranges, next)), |
|
scrollIntoView: true |
|
})); |
|
return true; |
|
}; |
|
} |
|
|
|
|
|
|
|
const clearSnippet = ({ state, dispatch }) => { |
|
let active = state.field(snippetState, false); |
|
if (!active) |
|
return false; |
|
dispatch(state.update({ effects: setActive.of(null) })); |
|
return true; |
|
}; |
|
|
|
|
|
|
|
const nextSnippetField = moveField(1); |
|
|
|
|
|
|
|
const prevSnippetField = moveField(-1); |
|
|
|
|
|
|
|
|
|
function hasNextSnippetField(state) { |
|
let active = state.field(snippetState, false); |
|
return !!(active && active.ranges.some(r => r.field == active.active + 1)); |
|
} |
|
|
|
|
|
|
|
|
|
function hasPrevSnippetField(state) { |
|
let active = state.field(snippetState, false); |
|
return !!(active && active.active > 0); |
|
} |
|
const defaultSnippetKeymap = [ |
|
{ key: "Tab", run: nextSnippetField, shift: prevSnippetField }, |
|
{ key: "Escape", run: clearSnippet } |
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const snippetKeymap = state.Facet.define({ |
|
combine(maps) { return maps.length ? maps[0] : defaultSnippetKeymap; } |
|
}); |
|
const addSnippetKeymap = state.Prec.highest(view.keymap.compute([snippetKeymap], state => state.facet(snippetKeymap))); |
|
|
|
|
|
|
|
|
|
|
|
function snippetCompletion(template, completion) { |
|
return Object.assign(Object.assign({}, completion), { apply: snippet(template) }); |
|
} |
|
const snippetPointerHandler = view.EditorView.domEventHandlers({ |
|
mousedown(event, view) { |
|
let active = view.state.field(snippetState, false), pos; |
|
if (!active || (pos = view.posAtCoords({ x: event.clientX, y: event.clientY })) == null) |
|
return false; |
|
let match = active.ranges.find(r => r.from <= pos && r.to >= pos); |
|
if (!match || match.field == active.active) |
|
return false; |
|
view.dispatch({ |
|
selection: fieldSelection(active.ranges, match.field), |
|
effects: setActive.of(active.ranges.some(r => r.field > match.field) |
|
? new ActiveSnippet(active.ranges, match.field) : null), |
|
scrollIntoView: true |
|
}); |
|
return true; |
|
} |
|
}); |
|
|
|
function wordRE(wordChars) { |
|
let escaped = wordChars.replace(/[\]\-\\]/g, "\\$&"); |
|
try { |
|
return new RegExp(`[\\p{Alphabetic}\\p{Number}_${escaped}]+`, "ug"); |
|
} |
|
catch (_a) { |
|
return new RegExp(`[\w${escaped}]`, "g"); |
|
} |
|
} |
|
function mapRE(re, f) { |
|
return new RegExp(f(re.source), re.unicode ? "u" : ""); |
|
} |
|
const wordCaches = Object.create(null); |
|
function wordCache(wordChars) { |
|
return wordCaches[wordChars] || (wordCaches[wordChars] = new WeakMap); |
|
} |
|
function storeWords(doc, wordRE, result, seen, ignoreAt) { |
|
for (let lines = doc.iterLines(), pos = 0; !lines.next().done;) { |
|
let { value } = lines, m; |
|
wordRE.lastIndex = 0; |
|
while (m = wordRE.exec(value)) { |
|
if (!seen[m[0]] && pos + m.index != ignoreAt) { |
|
result.push({ type: "text", label: m[0] }); |
|
seen[m[0]] = true; |
|
if (result.length >= 2000 ) |
|
return; |
|
} |
|
} |
|
pos += value.length + 1; |
|
} |
|
} |
|
function collectWords(doc, cache, wordRE, to, ignoreAt) { |
|
let big = doc.length >= 1000 ; |
|
let cached = big && cache.get(doc); |
|
if (cached) |
|
return cached; |
|
let result = [], seen = Object.create(null); |
|
if (doc.children) { |
|
let pos = 0; |
|
for (let ch of doc.children) { |
|
if (ch.length >= 1000 ) { |
|
for (let c of collectWords(ch, cache, wordRE, to - pos, ignoreAt - pos)) { |
|
if (!seen[c.label]) { |
|
seen[c.label] = true; |
|
result.push(c); |
|
} |
|
} |
|
} |
|
else { |
|
storeWords(ch, wordRE, result, seen, ignoreAt - pos); |
|
} |
|
pos += ch.length + 1; |
|
} |
|
} |
|
else { |
|
storeWords(doc, wordRE, result, seen, ignoreAt); |
|
} |
|
if (big && result.length < 2000 ) |
|
cache.set(doc, result); |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const completeAnyWord = context => { |
|
let wordChars = context.state.languageDataAt("wordChars", context.pos).join(""); |
|
let re = wordRE(wordChars); |
|
let token = context.matchBefore(mapRE(re, s => s + "$")); |
|
if (!token && !context.explicit) |
|
return null; |
|
let from = token ? token.from : context.pos; |
|
let options = collectWords(context.state.doc, wordCache(wordChars), re, 50000 , from); |
|
return { from, options, validFor: mapRE(re, s => "^" + s) }; |
|
}; |
|
|
|
const defaults = { |
|
brackets: ["(", "[", "{", "'", '"'], |
|
before: ")]}:;>", |
|
stringPrefixes: [] |
|
}; |
|
const closeBracketEffect = state.StateEffect.define({ |
|
map(value, mapping) { |
|
let mapped = mapping.mapPos(value, -1, state.MapMode.TrackAfter); |
|
return mapped == null ? undefined : mapped; |
|
} |
|
}); |
|
const closedBracket = new class extends state.RangeValue { |
|
}; |
|
closedBracket.startSide = 1; |
|
closedBracket.endSide = -1; |
|
const bracketState = state.StateField.define({ |
|
create() { return state.RangeSet.empty; }, |
|
update(value, tr) { |
|
value = value.map(tr.changes); |
|
if (tr.selection) { |
|
let line = tr.state.doc.lineAt(tr.selection.main.head); |
|
value = value.update({ filter: from => from >= line.from && from <= line.to }); |
|
} |
|
for (let effect of tr.effects) |
|
if (effect.is(closeBracketEffect)) |
|
value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] }); |
|
return value; |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function closeBrackets() { |
|
return [inputHandler, bracketState]; |
|
} |
|
const definedClosing = "()[]{}<>"; |
|
function closing(ch) { |
|
for (let i = 0; i < definedClosing.length; i += 2) |
|
if (definedClosing.charCodeAt(i) == ch) |
|
return definedClosing.charAt(i + 1); |
|
return state.fromCodePoint(ch < 128 ? ch : ch + 1); |
|
} |
|
function config(state, pos) { |
|
return state.languageDataAt("closeBrackets", pos)[0] || defaults; |
|
} |
|
const android = typeof navigator == "object" && /Android\b/.test(navigator.userAgent); |
|
const inputHandler = view.EditorView.inputHandler.of((view, from, to, insert) => { |
|
if ((android ? view.composing : view.compositionStarted) || view.state.readOnly) |
|
return false; |
|
let sel = view.state.selection.main; |
|
if (insert.length > 2 || insert.length == 2 && state.codePointSize(state.codePointAt(insert, 0)) == 1 || |
|
from != sel.from || to != sel.to) |
|
return false; |
|
let tr = insertBracket(view.state, insert); |
|
if (!tr) |
|
return false; |
|
view.dispatch(tr); |
|
return true; |
|
}); |
|
|
|
|
|
|
|
|
|
const deleteBracketPair = ({ state: state$1, dispatch }) => { |
|
if (state$1.readOnly) |
|
return false; |
|
let conf = config(state$1, state$1.selection.main.head); |
|
let tokens = conf.brackets || defaults.brackets; |
|
let dont = null, changes = state$1.changeByRange(range => { |
|
if (range.empty) { |
|
let before = prevChar(state$1.doc, range.head); |
|
for (let token of tokens) { |
|
if (token == before && nextChar(state$1.doc, range.head) == closing(state.codePointAt(token, 0))) |
|
return { changes: { from: range.head - token.length, to: range.head + token.length }, |
|
range: state.EditorSelection.cursor(range.head - token.length) }; |
|
} |
|
} |
|
return { range: dont = range }; |
|
}); |
|
if (!dont) |
|
dispatch(state$1.update(changes, { scrollIntoView: true, userEvent: "delete.backward" })); |
|
return !dont; |
|
}; |
|
|
|
|
|
|
|
|
|
const closeBracketsKeymap = [ |
|
{ key: "Backspace", run: deleteBracketPair } |
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function insertBracket(state$1, bracket) { |
|
let conf = config(state$1, state$1.selection.main.head); |
|
let tokens = conf.brackets || defaults.brackets; |
|
for (let tok of tokens) { |
|
let closed = closing(state.codePointAt(tok, 0)); |
|
if (bracket == tok) |
|
return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok + tok) > -1, conf) |
|
: handleOpen(state$1, tok, closed, conf.before || defaults.before); |
|
if (bracket == closed && closedBracketAt(state$1, state$1.selection.main.from)) |
|
return handleClose(state$1, tok, closed); |
|
} |
|
return null; |
|
} |
|
function closedBracketAt(state, pos) { |
|
let found = false; |
|
state.field(bracketState).between(0, state.doc.length, from => { |
|
if (from == pos) |
|
found = true; |
|
}); |
|
return found; |
|
} |
|
function nextChar(doc, pos) { |
|
let next = doc.sliceString(pos, pos + 2); |
|
return next.slice(0, state.codePointSize(state.codePointAt(next, 0))); |
|
} |
|
function prevChar(doc, pos) { |
|
let prev = doc.sliceString(pos - 2, pos); |
|
return state.codePointSize(state.codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1); |
|
} |
|
function handleOpen(state$1, open, close, closeBefore) { |
|
let dont = null, changes = state$1.changeByRange(range => { |
|
if (!range.empty) |
|
return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }], |
|
effects: closeBracketEffect.of(range.to + open.length), |
|
range: state.EditorSelection.range(range.anchor + open.length, range.head + open.length) }; |
|
let next = nextChar(state$1.doc, range.head); |
|
if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) |
|
return { changes: { insert: open + close, from: range.head }, |
|
effects: closeBracketEffect.of(range.head + open.length), |
|
range: state.EditorSelection.cursor(range.head + open.length) }; |
|
return { range: dont = range }; |
|
}); |
|
return dont ? null : state$1.update(changes, { |
|
scrollIntoView: true, |
|
userEvent: "input.type" |
|
}); |
|
} |
|
function handleClose(state$1, _open, close) { |
|
let dont = null, changes = state$1.changeByRange(range => { |
|
if (range.empty && nextChar(state$1.doc, range.head) == close) |
|
return { changes: { from: range.head, to: range.head + close.length, insert: close }, |
|
range: state.EditorSelection.cursor(range.head + close.length) }; |
|
return dont = { range }; |
|
}); |
|
return dont ? null : state$1.update(changes, { |
|
scrollIntoView: true, |
|
userEvent: "input.type" |
|
}); |
|
} |
|
|
|
|
|
function handleSame(state$1, token, allowTriple, config) { |
|
let stringPrefixes = config.stringPrefixes || defaults.stringPrefixes; |
|
let dont = null, changes = state$1.changeByRange(range => { |
|
if (!range.empty) |
|
return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }], |
|
effects: closeBracketEffect.of(range.to + token.length), |
|
range: state.EditorSelection.range(range.anchor + token.length, range.head + token.length) }; |
|
let pos = range.head, next = nextChar(state$1.doc, pos), start; |
|
if (next == token) { |
|
if (nodeStart(state$1, pos)) { |
|
return { changes: { insert: token + token, from: pos }, |
|
effects: closeBracketEffect.of(pos + token.length), |
|
range: state.EditorSelection.cursor(pos + token.length) }; |
|
} |
|
else if (closedBracketAt(state$1, pos)) { |
|
let isTriple = allowTriple && state$1.sliceDoc(pos, pos + token.length * 3) == token + token + token; |
|
let content = isTriple ? token + token + token : token; |
|
return { changes: { from: pos, to: pos + content.length, insert: content }, |
|
range: state.EditorSelection.cursor(pos + content.length) }; |
|
} |
|
} |
|
else if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token && |
|
(start = canStartStringAt(state$1, pos - 2 * token.length, stringPrefixes)) > -1 && |
|
nodeStart(state$1, start)) { |
|
return { changes: { insert: token + token + token + token, from: pos }, |
|
effects: closeBracketEffect.of(pos + token.length), |
|
range: state.EditorSelection.cursor(pos + token.length) }; |
|
} |
|
else if (state$1.charCategorizer(pos)(next) != state.CharCategory.Word) { |
|
if (canStartStringAt(state$1, pos, stringPrefixes) > -1 && !probablyInString(state$1, pos, token, stringPrefixes)) |
|
return { changes: { insert: token + token, from: pos }, |
|
effects: closeBracketEffect.of(pos + token.length), |
|
range: state.EditorSelection.cursor(pos + token.length) }; |
|
} |
|
return { range: dont = range }; |
|
}); |
|
return dont ? null : state$1.update(changes, { |
|
scrollIntoView: true, |
|
userEvent: "input.type" |
|
}); |
|
} |
|
function nodeStart(state, pos) { |
|
let tree = language.syntaxTree(state).resolveInner(pos + 1); |
|
return tree.parent && tree.from == pos; |
|
} |
|
function probablyInString(state, pos, quoteToken, prefixes) { |
|
let node = language.syntaxTree(state).resolveInner(pos, -1); |
|
let maxPrefix = prefixes.reduce((m, p) => Math.max(m, p.length), 0); |
|
for (let i = 0; i < 5; i++) { |
|
let start = state.sliceDoc(node.from, Math.min(node.to, node.from + quoteToken.length + maxPrefix)); |
|
let quotePos = start.indexOf(quoteToken); |
|
if (!quotePos || quotePos > -1 && prefixes.indexOf(start.slice(0, quotePos)) > -1) { |
|
let first = node.firstChild; |
|
while (first && first.from == node.from && first.to - first.from > quoteToken.length + quotePos) { |
|
if (state.sliceDoc(first.to - quoteToken.length, first.to) == quoteToken) |
|
return false; |
|
first = first.firstChild; |
|
} |
|
return true; |
|
} |
|
let parent = node.to == pos && node.parent; |
|
if (!parent) |
|
break; |
|
node = parent; |
|
} |
|
return false; |
|
} |
|
function canStartStringAt(state$1, pos, prefixes) { |
|
let charCat = state$1.charCategorizer(pos); |
|
if (charCat(state$1.sliceDoc(pos - 1, pos)) != state.CharCategory.Word) |
|
return pos; |
|
for (let prefix of prefixes) { |
|
let start = pos - prefix.length; |
|
if (state$1.sliceDoc(start, pos) == prefix && charCat(state$1.sliceDoc(start - 1, start)) != state.CharCategory.Word) |
|
return start; |
|
} |
|
return -1; |
|
} |
|
|
|
|
|
|
|
|
|
function autocompletion(config = {}) { |
|
return [ |
|
commitCharacters, |
|
completionState, |
|
completionConfig.of(config), |
|
completionPlugin, |
|
completionKeymapExt, |
|
baseTheme |
|
]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const completionKeymap = [ |
|
{ key: "Ctrl-Space", run: startCompletion }, |
|
{ key: "Escape", run: closeCompletion }, |
|
{ key: "ArrowDown", run: moveCompletionSelection(true) }, |
|
{ key: "ArrowUp", run: moveCompletionSelection(false) }, |
|
{ key: "PageDown", run: moveCompletionSelection(true, "page") }, |
|
{ key: "PageUp", run: moveCompletionSelection(false, "page") }, |
|
{ key: "Enter", run: acceptCompletion } |
|
]; |
|
const completionKeymapExt = state.Prec.highest(view.keymap.computeN([completionConfig], state => state.facet(completionConfig).defaultKeymap ? [completionKeymap] : [])); |
|
|
|
|
|
|
|
|
|
|
|
|
|
function completionStatus(state) { |
|
let cState = state.field(completionState, false); |
|
return cState && cState.active.some(a => a.state == 1 ) ? "pending" |
|
: cState && cState.active.some(a => a.state != 0 ) ? "active" : null; |
|
} |
|
const completionArrayCache = new WeakMap; |
|
|
|
|
|
|
|
function currentCompletions(state) { |
|
var _a; |
|
let open = (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.open; |
|
if (!open || open.disabled) |
|
return []; |
|
let completions = completionArrayCache.get(open.options); |
|
if (!completions) |
|
completionArrayCache.set(open.options, completions = open.options.map(o => o.completion)); |
|
return completions; |
|
} |
|
|
|
|
|
|
|
function selectedCompletion(state) { |
|
var _a; |
|
let open = (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.open; |
|
return open && !open.disabled && open.selected >= 0 ? open.options[open.selected].completion : null; |
|
} |
|
|
|
|
|
|
|
|
|
function selectedCompletionIndex(state) { |
|
var _a; |
|
let open = (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.open; |
|
return open && !open.disabled && open.selected >= 0 ? open.selected : null; |
|
} |
|
|
|
|
|
|
|
|
|
function setSelectedCompletion(index) { |
|
return setSelectedEffect.of(index); |
|
} |
|
|
|
exports.CompletionContext = CompletionContext; |
|
exports.acceptCompletion = acceptCompletion; |
|
exports.autocompletion = autocompletion; |
|
exports.clearSnippet = clearSnippet; |
|
exports.closeBrackets = closeBrackets; |
|
exports.closeBracketsKeymap = closeBracketsKeymap; |
|
exports.closeCompletion = closeCompletion; |
|
exports.completeAnyWord = completeAnyWord; |
|
exports.completeFromList = completeFromList; |
|
exports.completionKeymap = completionKeymap; |
|
exports.completionStatus = completionStatus; |
|
exports.currentCompletions = currentCompletions; |
|
exports.deleteBracketPair = deleteBracketPair; |
|
exports.hasNextSnippetField = hasNextSnippetField; |
|
exports.hasPrevSnippetField = hasPrevSnippetField; |
|
exports.ifIn = ifIn; |
|
exports.ifNotIn = ifNotIn; |
|
exports.insertBracket = insertBracket; |
|
exports.insertCompletionText = insertCompletionText; |
|
exports.moveCompletionSelection = moveCompletionSelection; |
|
exports.nextSnippetField = nextSnippetField; |
|
exports.pickedCompletion = pickedCompletion; |
|
exports.prevSnippetField = prevSnippetField; |
|
exports.selectedCompletion = selectedCompletion; |
|
exports.selectedCompletionIndex = selectedCompletionIndex; |
|
exports.setSelectedCompletion = setSelectedCompletion; |
|
exports.snippet = snippet; |
|
exports.snippetCompletion = snippetCompletion; |
|
exports.snippetKeymap = snippetKeymap; |
|
exports.startCompletion = startCompletion; |
|
|