|
| 1 | +import {NodeWeakMap, SyntaxNodeRef, SyntaxNode, IterMode} from "@lezer/common" |
| 2 | +import {Completion, CompletionContext, CompletionResult, completeFromList, ifNotIn, |
| 3 | + snippetCompletion as snip} from "@codemirror/autocomplete" |
| 4 | +import {syntaxTree} from "@codemirror/language" |
| 5 | +import {Text} from "@codemirror/state" |
| 6 | + |
| 7 | +const cache = new NodeWeakMap<readonly Completion[]>() |
| 8 | + |
| 9 | +const ScopeNodes = new Set([ |
| 10 | + "Script", "Body", |
| 11 | + "FunctionDefinition", "ClassDefinition", "LambdaExpression", |
| 12 | + "ForStatement", "MatchClause" |
| 13 | +]) |
| 14 | + |
| 15 | +function defID(type: string) { |
| 16 | + return (node: SyntaxNodeRef, def: (node: SyntaxNodeRef, type: string) => void, outer: boolean) => { |
| 17 | + if (outer) return false |
| 18 | + let id = node.node.getChild("VariableName") |
| 19 | + if (id) def(id, type) |
| 20 | + return true |
| 21 | + } |
| 22 | +} |
| 23 | + |
| 24 | +const gatherCompletions: { |
| 25 | + [node: string]: (node: SyntaxNodeRef, def: (node: SyntaxNodeRef, type: string) => void, outer: boolean) => void | boolean |
| 26 | +} = { |
| 27 | + FunctionDefinition: defID("function"), |
| 28 | + ClassDefinition: defID("class"), |
| 29 | + ForStatement(node, def, outer) { |
| 30 | + if (outer) for (let child = node.node.firstChild; child; child = child.nextSibling) { |
| 31 | + if (child.name == "VariableName") def(child, "variable") |
| 32 | + else if (child.name == "in") break |
| 33 | + } |
| 34 | + }, |
| 35 | + ImportStatement(_node, def) { |
| 36 | + let {node} = _node |
| 37 | + let isFrom = node.firstChild?.name == "from" |
| 38 | + for (let ch = node.getChild("import"); ch; ch = ch.nextSibling) { |
| 39 | + if (ch.name == "VariableName" && ch.nextSibling?.name != "as") |
| 40 | + def(ch, isFrom ? "variable" : "namespace") |
| 41 | + } |
| 42 | + }, |
| 43 | + AssignStatement(node, def) { |
| 44 | + for (let child = node.node.firstChild; child; child = child.nextSibling) { |
| 45 | + if (child.name == "VariableName") def(child, "variable") |
| 46 | + else if (child.name == ":" || child.name == "AssignOp") break |
| 47 | + } |
| 48 | + }, |
| 49 | + ParamList(node, def) { |
| 50 | + for (let prev = null, child = node.node.firstChild; child; child = child.nextSibling) { |
| 51 | + if (child.name == "VariableName" && (!prev || !/\*|AssignOp/.test(prev.name))) |
| 52 | + def(child, "variable") |
| 53 | + prev = child |
| 54 | + } |
| 55 | + }, |
| 56 | + CapturePattern: defID("variable"), |
| 57 | + AsPattern: defID("variable"), |
| 58 | + __proto__: null as any |
| 59 | +} |
| 60 | + |
| 61 | +function getScope(doc: Text, node: SyntaxNode) { |
| 62 | + let cached = cache.get(node) |
| 63 | + if (cached) return cached |
| 64 | + |
| 65 | + console.log("get scope for", node.name) |
| 66 | + let completions: Completion[] = [], top = true |
| 67 | + function def(node: SyntaxNodeRef, type: string) { |
| 68 | + let name = doc.sliceString(node.from, node.to) |
| 69 | + completions.push({label: name, type}) |
| 70 | + } |
| 71 | + node.cursor(IterMode.IncludeAnonymous).iterate(node => { |
| 72 | + if (node.name) { |
| 73 | + let gather = gatherCompletions[node.name] |
| 74 | + if (gather && gather(node, def, top) || !top && ScopeNodes.has(node.name)) return console.log("bail for", node.name), false |
| 75 | + top = false |
| 76 | + } else if (node.to - node.from > 8192) { |
| 77 | + // Allow caching for bigger internal nodes |
| 78 | + for (let c of getScope(doc, node.node)) completions.push(c) |
| 79 | + return false |
| 80 | + } |
| 81 | + }) |
| 82 | + cache.set(node, completions) |
| 83 | + return completions |
| 84 | +} |
| 85 | + |
| 86 | +const Identifier = /^[\w\xa1-\uffff][\w\d\xa1-\uffff]*$/ |
| 87 | + |
| 88 | +const dontComplete = ["String", "FormatString", "Comment", "PropertyName"] |
| 89 | + |
| 90 | +/// Completion source that looks up locally defined names in |
| 91 | +/// Python code. |
| 92 | +export function localCompletionSource(context: CompletionContext): CompletionResult | null { |
| 93 | + let inner = syntaxTree(context.state).resolveInner(context.pos, -1) |
| 94 | + if (dontComplete.indexOf(inner.name) > -1) return null |
| 95 | + let isWord = inner.name == "VariableName" || |
| 96 | + inner.to - inner.from < 20 && Identifier.test(context.state.sliceDoc(inner.from, inner.to)) |
| 97 | + if (!isWord && !context.explicit) return null |
| 98 | + let options: Completion[] = [] |
| 99 | + for (let pos: SyntaxNode | null = inner; pos; pos = pos.parent) { |
| 100 | + if (ScopeNodes.has(pos.name)) options = options.concat(getScope(context.state.doc, pos)) |
| 101 | + } |
| 102 | + return { |
| 103 | + options, |
| 104 | + from: isWord ? inner.from : context.pos, |
| 105 | + validFor: Identifier |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +const globals: readonly Completion[] = [ |
| 110 | + "__annotations__", "__builtins__", "__debug__", "__doc__", "__import__", "__name__", |
| 111 | + "__loader__", "__package__", "__spec__", |
| 112 | + "False", "None", "True" |
| 113 | +].map(n => ({label: n, type: "constant"})).concat([ |
| 114 | + "ArithmeticError", "AssertionError", "AttributeError", "BaseException", "BlockingIOError", |
| 115 | + "BrokenPipeError", "BufferError", "BytesWarning", "ChildProcessError", "ConnectionAbortedError", |
| 116 | + "ConnectionError", "ConnectionRefusedError", "ConnectionResetError", "DeprecationWarning", |
| 117 | + "EOFError", "Ellipsis", "EncodingWarning", "EnvironmentError", "Exception", "FileExistsError", |
| 118 | + "FileNotFoundError", "FloatingPointError", "FutureWarning", "GeneratorExit", "IOError", |
| 119 | + "ImportError", "ImportWarning", "IndentationError", "IndexError", "InterruptedError", |
| 120 | + "IsADirectoryError", "KeyError", "KeyboardInterrupt", "LookupError", "MemoryError", |
| 121 | + "ModuleNotFoundError", "NameError", "NotADirectoryError", "NotImplemented", "NotImplementedError", |
| 122 | + "OSError", "OverflowError", "PendingDeprecationWarning", "PermissionError", "ProcessLookupError", |
| 123 | + "RecursionError", "ReferenceError", "ResourceWarning", "RuntimeError", "RuntimeWarning", |
| 124 | + "StopAsyncIteration", "StopIteration", "SyntaxError", "SyntaxWarning", "SystemError", |
| 125 | + "SystemExit", "TabError", "TimeoutError", "TypeError", "UnboundLocalError", "UnicodeDecodeError", |
| 126 | + "UnicodeEncodeError", "UnicodeError", "UnicodeTranslateError", "UnicodeWarning", "UserWarning", |
| 127 | + "ValueError", "Warning", "ZeroDivisionError" |
| 128 | +].map(n => ({label: n, type: "type"}))).concat([ |
| 129 | + "bool", "bytearray", "bytes", "classmethod", "complex", "float", "frozenset", "int", "list", |
| 130 | + "map", "memoryview", "object", "range", "set", "staticmethod", "str", "super", "tuple", "type" |
| 131 | +].map(n => ({label: n, type: "class"}))).concat([ |
| 132 | + "abs", "aiter", "all", "anext", "any", "ascii", "bin", "breakpoint", "callable", "chr", |
| 133 | + "compile", "delattr", "dict", "dir", "divmod", "enumerate", "eval", "exec", "exit", "filter", |
| 134 | + "format", "getattr", "globals", "hasattr", "hash", "help", "hex", "id", "input", "isinstance", |
| 135 | + "issubclass", "iter", "len", "license", "locals", "max", "min", "next", "oct", "open", |
| 136 | + "ord", "pow", "print", "property", "quit", "repr", "reversed", "round", "setattr", "slice", |
| 137 | + "sorted", "sum", "vars", "zip" |
| 138 | +].map(n => ({label: n, type: "function"}))) |
| 139 | + |
| 140 | +export const snippets: readonly Completion[] = [ |
| 141 | + snip("def ${name}(${params}):\n\t${}", { |
| 142 | + label: "def", |
| 143 | + detail: "function", |
| 144 | + type: "keyword" |
| 145 | + }), |
| 146 | + snip("for ${name} in ${collection}:\n\t${}", { |
| 147 | + label: "for", |
| 148 | + detail: "loop", |
| 149 | + type: "keyword" |
| 150 | + }), |
| 151 | + snip("while ${}:\n\t${}", { |
| 152 | + label: "while", |
| 153 | + detail: "loop", |
| 154 | + type: "keyword" |
| 155 | + }), |
| 156 | + snip("try:\n\t${}\nexcept ${error}:\n\t${}", { |
| 157 | + label: "try", |
| 158 | + detail: "/ except block", |
| 159 | + type: "keyword" |
| 160 | + }), |
| 161 | + snip("if ${}:\n\t\n", { |
| 162 | + label: "if", |
| 163 | + detail: "block", |
| 164 | + type: "keyword" |
| 165 | + }), |
| 166 | + snip("if ${}:\n\t${}\nelse:\n\t${}", { |
| 167 | + label: "if", |
| 168 | + detail: "/ else block", |
| 169 | + type: "keyword" |
| 170 | + }), |
| 171 | + snip("class ${name}:\n\tdef __init__(self, ${params}):\n\t\t\t${}", { |
| 172 | + label: "class", |
| 173 | + detail: "definition", |
| 174 | + type: "keyword" |
| 175 | + }), |
| 176 | + snip("import ${module}", { |
| 177 | + label: "import", |
| 178 | + detail: "statement", |
| 179 | + type: "keyword" |
| 180 | + }), |
| 181 | + snip("from ${module} import ${names}", { |
| 182 | + label: "from", |
| 183 | + detail: "import", |
| 184 | + type: "keyword" |
| 185 | + }) |
| 186 | +] |
| 187 | + |
| 188 | +/// Autocompletion for built-in Python globals and keywords. |
| 189 | +export const globalCompletion = ifNotIn(dontComplete, completeFromList(globals.concat(snippets))) |
0 commit comments