import {NodeWeakMap, SyntaxNodeRef, SyntaxNode, IterMode} from "@lezer/common" import {Completion, CompletionContext, CompletionResult, CompletionSource} from "@codemirror/autocomplete" import {syntaxTree} from "@codemirror/language" import {Text} from "@codemirror/state" const cache = new NodeWeakMap() const ScopeNodes = new Set([ "Script", "Block", "FunctionExpression", "FunctionDeclaration", "ArrowFunction", "MethodDeclaration", "ForStatement" ]) function defID(type: string) { return (node: SyntaxNodeRef, def: (node: SyntaxNodeRef, type: string) => void) => { let id = node.node.getChild("VariableDefinition") if (id) def(id, type) return true } } const functionContext = ["FunctionDeclaration"] const gatherCompletions: { [node: string]: (node: SyntaxNodeRef, def: (node: SyntaxNodeRef, type: string) => void) => void | boolean } = { FunctionDeclaration: defID("function"), ClassDeclaration: defID("class"), ClassExpression: () => true, EnumDeclaration: defID("constant"), TypeAliasDeclaration: defID("type"), NamespaceDeclaration: defID("namespace"), VariableDefinition(node, def) { if (!node.matchContext(functionContext)) def(node, "variable") }, TypeDefinition(node, def) { def(node, "type") }, __proto__: null as any } function getScope(doc: Text, node: SyntaxNode) { let cached = cache.get(node) if (cached) return cached let completions: Completion[] = [], top = true function def(node: SyntaxNodeRef, type: string) { let name = doc.sliceString(node.from, node.to) completions.push({label: name, type}) } node.cursor(IterMode.IncludeAnonymous).iterate(node => { if (top) { top = false } else if (node.name) { let gather = gatherCompletions[node.name] if (gather && gather(node, def) || ScopeNodes.has(node.name)) return false } else if (node.to - node.from > 8192) { // Allow caching for bigger internal nodes for (let c of getScope(doc, node.node)) completions.push(c) return false } }) cache.set(node, completions) return completions } const Identifier = /^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/ export const dontComplete = [ "TemplateString", "String", "RegExp", "LineComment", "BlockComment", "VariableDefinition", "TypeDefinition", "Label", "PropertyDefinition", "PropertyName", "PrivatePropertyDefinition", "PrivatePropertyName", "JSXText", "JSXAttributeValue", "JSXOpenTag", "JSXCloseTag", "JSXSelfClosingTag", ".", "?." ] /// Completion source that looks up locally defined names in /// JavaScript code. export function localCompletionSource(context: CompletionContext): CompletionResult | null { let inner = syntaxTree(context.state).resolveInner(context.pos, -1) if (dontComplete.indexOf(inner.name) > -1) return null let isWord = inner.name == "VariableName" || inner.to - inner.from < 20 && Identifier.test(context.state.sliceDoc(inner.from, inner.to)) if (!isWord && !context.explicit) return null let options: Completion[] = [] for (let pos: SyntaxNode | null = inner; pos; pos = pos.parent) { if (ScopeNodes.has(pos.name)) options = options.concat(getScope(context.state.doc, pos)) } return { options, from: isWord ? inner.from : context.pos, validFor: Identifier } } function pathFor(read: (node: SyntaxNode) => string, member: SyntaxNode, name: string) { let path: string[] = [] for (;;) { let obj = member.firstChild, prop if (obj?.name == "VariableName") { path.push(read(obj)) return {path: path.reverse(), name} } else if (obj?.name == "MemberExpression" && (prop = obj.lastChild)?.name == "PropertyName") { path.push(read(prop!)) member = obj } else { return null } } } /// Helper function for defining JavaScript completion sources. It /// returns the completable name and object path for a completion /// context, or null if no name/property completion should happen at /// that position. For example, when completing after `a.b.c` it will /// return `{path: ["a", "b"], name: "c"}`. When completing after `x` /// it will return `{path: [], name: "x"}`. When not in a property or /// name, it will return null if `context.explicit` is false, and /// `{path: [], name: ""}` otherwise. export function completionPath(context: CompletionContext): {path: readonly string[], name: string} | null { let read = (node: SyntaxNode) => context.state.doc.sliceString(node.from, node.to) let inner = syntaxTree(context.state).resolveInner(context.pos, -1) if (inner.name == "PropertyName") { return pathFor(read, inner.parent!, read(inner)) } else if ((inner.name == "." || inner.name == "?.") && inner.parent!.name == "MemberExpression") { return pathFor(read, inner.parent!, "") } else if (dontComplete.indexOf(inner.name) > -1) { return null } else if (inner.name == "VariableName" || inner.to - inner.from < 20 && Identifier.test(read(inner))) { return {path: [], name: read(inner)} } else if (inner.name == "MemberExpression") { return pathFor(read, inner, "") } else { return context.explicit ? {path: [], name: ""} : null } } function enumeratePropertyCompletions(obj: any, top: boolean): readonly Completion[] { let options: Completion[] = [], seen: Set = new Set for (let depth = 0;; depth++) { for (let name of (Object.getOwnPropertyNames || Object.keys)(obj)) { if (!/^[a-zA-Z_$\xaa-\uffdc][\w$\xaa-\uffdc]*$/.test(name) || seen.has(name)) continue seen.add(name) let value try { value = obj[name] } catch(_) { continue } options.push({ label: name, type: typeof value == "function" ? (/^[A-Z]/.test(name) ? "class" : top ? "function" : "method") : top ? "variable" : "property", boost: -depth }) } let next = Object.getPrototypeOf(obj) if (!next) return options obj = next } } /// Defines a [completion source](#autocomplete.CompletionSource) that /// completes from the given scope object (for example `globalThis`). /// Will enter properties of the object when completing properties on /// a directly-named path. export function scopeCompletionSource(scope: any): CompletionSource { let cache: Map = new Map return (context: CompletionContext) => { let path = completionPath(context) if (!path) return null let target = scope for (let step of path.path) { target = target[step] if (!target) return null } let options = cache.get(target) if (!options) cache.set(target, options = enumeratePropertyCompletions(target, !path.path.length)) return { from: context.pos - path.name.length, options, validFor: Identifier } } }