8000 Add autocompletion support · codemirror/lang-python@3926dd8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3926dd8

Browse files
committed
Add autocompletion support
FEATURE: The `globalCompletion` completion source (included in the language support returned from `python()`) completes standard Python globals and keywords. FEATURE: Export a `localCompletionSource` function that completes locally defined variables. Included in the support extensions returned from `python()`.
1 parent bbb33ac commit 3926dd8

File tree

3 files changed

+196
-1
lines changed

3 files changed

+196
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"sideEffects": false,
2727
"license": "MIT",
2828
"dependencies": {
29+
"@codemirror/autocomplete": "^6.3.2",
2930
"@codemirror/language": "^6.0.0",
3031
"@lezer/python": "^1.0.0"
3132
},

src/complete.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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)))

src/python.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {parser} from "@lezer/python"
22
import {SyntaxNode} from "@lezer/common"
33
import {delimitedIndent, indentNodeProp, TreeIndentContext,
44
foldNodeProp, foldInside, LRLanguage, LanguageSupport} from "@codemirror/language"
5+
import {globalCompletion, localCompletionSource} from "./complete"
6+
export {globalCompletion, localCompletionSource}
57

68
function indentBody(context: TreeIndentContext, node: SyntaxNode) {
79
let base = context.lineIndent(node.from)
@@ -71,5 +73,8 @@ export const pythonLanguage = LRLanguage.define({
7173

7274
/// Python language support.
7375
export function python() {
74-
return new LanguageSupport(pythonLanguage)
76+
return new LanguageSupport(pythonLanguage, [
77+
pythonLanguage.data.of({autocomplete: localCompletionSource}),
78+
pythonLanguage.data.of({autocomplete: globalCompletion}),
79+
])
7580
}

0 commit comments

Comments
 (0)
0