diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml index 9552050..d050072 100644 --- a/.github/workflows/dispatch.yml +++ b/.github/workflows/dispatch.yml @@ -11,6 +11,6 @@ jobs: with: # You should create a personal access token and store it in your repository token: ${{ secrets.DISPATCH_AUTH }} - repo: codemirror.next + repo: dev owner: codemirror event_type: push diff --git a/.gitignore b/.gitignore index 01bff1b..eebad72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ /node_modules package-lock.json /dist -/src/*.js -/src/*.d.ts -/src/*.d.ts.map /test/*.js /test/*.d.ts /test/*.d.ts.map diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8ebf0..50f62e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,147 @@ +## 6.2.0 (2025-04-25) + +### Bug fixes + +Add an indentation rule for member expressions. + +### New features + +Allow multi-line strings to be code-folded. + +## 6.1.7 (2025-01-18) + +### Bug fixes + +Properly indent match/case statements. + +## 6.1.6 (2024-04-29) + +### Bug fixes + +Improve the way indentation for the current body is preserved when inenting new lines. + +## 6.1.5 (2024-03-28) + +### Bug fixes + +Properly indent `else:` when attached to a `for` or `while` statement. + +## 6.1.4 (2024-02-02) + +### Bug fixes + +Explicitly declare dependencies on @lezer/common and @codemirror/state + +## 6.1.3 (2023-06-12) + +### Bug fixes + +Fix a bug where blocks started after a wrapped argument list or similar construct were indented too far. + +## 6.1.2 (2023-03-01) + +### Bug fixes + +Don't indent lines after a dedented comment line. + +## 6.1.1 (2022-12-24) + +### Bug fixes + +Remove leftover log statements. + +## 6.1.0 (2022-11-18) + +### New features + +The `globalCompletion` completion source (included in the language support returned from `python()`) completes standard Python globals and keywords. + +Export a `localCompletionSource` function that completes locally defined variables. Included in the support extensions returned from `python()`. + +## 6.0.4 (2022-10-24) + +### Bug fixes + +Make sure the language object has a name. + +## 6.0.3 (2022-10-19) + +### Bug fixes + +Add proper indentation handling of `else` clauses in `try` statements. + +## 6.0.2 (2022-09-22) + +### Bug fixes + +Allow prefixed strings to be closed by `closeBrackets`. + +## 6.0.1 (2022-07-21) + +### Bug fixes + +Fix (non-)auto indentation in template strings and comments. + +## 6.0.0 (2022-06-08) + +### Breaking changes + +Update dependencies to 6.0.0 + +## 0.20.0 (2022-04-20) + +### Bug fixes + +Add folding information for set and tuple expressions. + +## 0.19.5 (2022-04-06) + +### Bug fixes + +Make sure * and ** modifiers are highlighted as such, add modifier tag for FormatSpec nodes. + +## 0.19.4 (2022-01-26) + +### Bug fixes + +Fix issue where folding body nodes folded away the newline after the body. + +## 0.19.3 (2022-01-20) + +### Bug fixes + +Fix the way block bodies are folded. + +## 0.19.2 (2021-08-11) + +### Bug fixes + +Make sure that indenting an else/elif/except/finally that's indented too deep moves it back up. + +## 0.19.1 (2021-08-11) + +### Bug fixes + +Fix incorrect versions for @lezer dependencies. + +## 0.19.0 (2021-08-11) + +### Bug fixes + +Improve indentation for dictionaries, arrays, and tuples. Fix delimitedIndent calls + +Indentation on deindented blank lines after a block will no longer return to the block's indentation level. + +## 0.18.1 (2021-07-20) + +### Bug fixes + +Fix highlighting of property names. Improve indentation support + +Don't deindent when pressing enter at the end of the document. + +Properly indent else/elif/except syntax. + ## 0.18.0 (2021-03-03) ### Breaking changes diff --git a/LICENSE b/LICENSE index 3af12e6..9a91f48 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2018-2021 by Marijn Haverbeke and others +Copyright (C) 2018-2021 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3059c4a..7356dae 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ # @codemirror/lang-python [![NPM version](https://img.shields.io/npm/v/@codemirror/lang-python.svg)](https://www.npmjs.org/package/@codemirror/lang-python) -[ [**WEBSITE**](https://codemirror.net/6/) | [**ISSUES**](https://github.com/codemirror/codemirror.next/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/lang-python/blob/main/CHANGELOG.md) ] +[ [**WEBSITE**](https://codemirror.net/) | [**ISSUES**](https://github.com/codemirror/dev/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/lang-python/blob/main/CHANGELOG.md) ] This package implements Python language support for the -[CodeMirror](https://codemirror.net/6/) code editor. +[CodeMirror](https://codemirror.net/) code editor. -The [project page](https://codemirror.net/6/) has more information, a -number of [examples](https://codemirror.net/6/examples/) and the -[documentation](https://codemirror.net/6/docs/). +The [project page](https://codemirror.net/) has more information, a +number of [examples](https://codemirror.net/examples/) and the +[documentation](https://codemirror.net/docs/). This code is released under an [MIT license](https://github.com/codemirror/lang-python/tree/main/LICENSE). @@ -19,18 +19,43 @@ we have a [code of conduct](http://contributor-covenant.org/version/1/1/0/) that applies to communication around the project. +## Usage + +```javascript +import {EditorView, basicSetup} from "codemirror" +import {python} from "@codemirror/lang-python" + +const view = new EditorView({ + parent: document.body, + doc: `print("Hello world")`, + extensions: [basicSetup, python()] +}) +``` + # API Reference +
- python() → LanguageSupport
+ python() → LanguageSupport

Python language support.

- pythonLanguage: LezerLanguage
+ pythonLanguage: LRLanguage

A language provider based on the Lezer Python parser, extended with highlighting and indentation information.

+
+ globalCompletion: CompletionSource
+ +

Autocompletion for built-in Python globals and keywords.

+
+
+ localCompletionSource(contextCompletionContext) → CompletionResult | null
+ +

Completion source that looks up locally defined names in +Python code.

+
diff --git a/package.json b/package.json index bc90561..c1ae028 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@codemirror/lang-python", - "version": "0.18.0", + "version": "6.2.0", "description": "Python language support for the CodeMirror code editor", "scripts": { - "test": "echo 'No tests'", - "prepare": "tsc -p tsconfig.local.json && rollup -c" + "test": "cm-runtests", + "prepare": "cm-buildhelper src/python.ts" }, "keywords": [ "editor", @@ -12,7 +12,7 @@ ], "author": { "name": "Marijn Haverbeke", - "email": "marijnh@gmail.com", + "email": "marijn@haverbeke.berlin", "url": "http://marijnhaverbeke.nl" }, "type": "module", @@ -26,14 +26,14 @@ "sideEffects": false, "license": "MIT", "dependencies": { - "@codemirror/highlight": "^0.18.0", - "@codemirror/language": "^0.18.0", - "lezer-python": "^0.13.0" + "@codemirror/state": "^6.0.0", + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" }, "devDependencies": { - "rollup": "^2.35.1", - "rollup-plugin-dts": "^2.0.1", - "typescript": "^4.1.3" + "@codemirror/buildhelper": "^1.0.0" }, "repository": { "type": "git", diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 096deef..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,22 +0,0 @@ -import dts from "rollup-plugin-dts" - -export default [{ - input: "./src/python.js", - external: id => id != "tslib" && !/^(\.?\/|\w:)/.test(id), - output: [{ - format: "esm", - file: "./dist/index.js", - externalLiveBindings: false - }, { - format: "cjs", - file: "./dist/index.cjs" - }] -}, { - input: "./src/python.d.ts", - output: { - format: "esm", - file: "./dist/index.d.ts", - }, - plugins: [dts()], - onwarn(warning, warn) { if (warning.code != "CIRCULAR_DEPENDENCY") warn(warning) } -}] diff --git a/src/README.md b/src/README.md index 4fbafab..1ae9876 100644 --- a/src/README.md +++ b/src/README.md @@ -2,14 +2,14 @@ # @codemirror/lang-python [![NPM version](https://img.shields.io/npm/v/@codemirror/lang-python.svg)](https://www.npmjs.org/package/@codemirror/lang-python) -[ [**WEBSITE**](https://codemirror.net/6/) | [**ISSUES**](https://github.com/codemirror/codemirror.next/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/lang-python/blob/main/CHANGELOG.md) ] +[ [**WEBSITE**](https://codemirror.net/) | [**ISSUES**](https://github.com/codemirror/dev/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/lang-python/blob/main/CHANGELOG.md) ] This package implements Python language support for the -[CodeMirror](https://codemirror.net/6/) code editor. +[CodeMirror](https://codemirror.net/) code editor. -The [project page](https://codemirror.net/6/) has more information, a -number of [examples](https://codemirror.net/6/examples/) and the -[documentation](https://codemirror.net/6/docs/). +The [project page](https://codemirror.net/) has more information, a +number of [examples](https://codemirror.net/examples/) and the +[documentation](https://codemirror.net/docs/). This code is released under an [MIT license](https://github.com/codemirror/lang-python/tree/main/LICENSE). @@ -19,8 +19,25 @@ we have a [code of conduct](http://contributor-covenant.org/version/1/1/0/) that applies to communication around the project. +## Usage + +```javascript +import {EditorView, basicSetup} from "codemirror" +import {python} from "@codemirror/lang-python" + +const view = new EditorView({ + parent: document.body, + doc: `print("Hello world")`, + extensions: [basicSetup, python()] +}) +``` + # API Reference @python -@pythonLanguage \ No newline at end of file +@pythonLanguage + +@globalCompletion + +@localCompletionSource diff --git a/src/complete.ts b/src/complete.ts new file mode 100644 index 0000000..8bcc566 --- /dev/null +++ b/src/complete.ts @@ -0,0 +1,188 @@ +import {NodeWeakMap, SyntaxNodeRef, SyntaxNode, IterMode} from "@lezer/common" +import {Completion, CompletionContext, CompletionResult, completeFromList, ifNotIn, + snippetCompletion as snip} from "@codemirror/autocomplete" +import {syntaxTree} from "@codemirror/language" +import {Text} from "@codemirror/state" + +const cache = new NodeWeakMap() + +const ScopeNodes = new Set([ + "Script", "Body", + "FunctionDefinition", "ClassDefinition", "LambdaExpression", + "ForStatement", "MatchClause" +]) + +function defID(type: string) { + return (node: SyntaxNodeRef, def: (node: SyntaxNodeRef, type: string) => void, outer: boolean) => { + if (outer) return false + let id = node.node.getChild("VariableName") + if (id) def(id, type) + return true + } +} + +const gatherCompletions: { + [node: string]: (node: SyntaxNodeRef, def: (node: SyntaxNodeRef, type: string) => void, outer: boolean) => void | boolean +} = { + FunctionDefinition: defID("function"), + ClassDefinition: defID("class"), + ForStatement(node, def, outer) { + if (outer) for (let child = node.node.firstChild; child; child = child.nextSibling) { + if (child.name == "VariableName") def(child, "variable") + else if (child.name == "in") break + } + }, + ImportStatement(_node, def) { + let {node} = _node + let isFrom = node.firstChild?.name == "from" + for (let ch = node.getChild("import"); ch; ch = ch.nextSibling) { + if (ch.name == "VariableName" && ch.nextSibling?.name != "as") + def(ch, isFrom ? "variable" : "namespace") + } + }, + AssignStatement(node, def) { + for (let child = node.node.firstChild; child; child = child.nextSibling) { + if (child.name == "VariableName") def(child, "variable") + else if (child.name == ":" || child.name == "AssignOp") break + } + }, + ParamList(node, def) { + for (let prev = null, child = node.node.firstChild; child; child = child.nextSibling) { + if (child.name == "VariableName" && (!prev || !/\*|AssignOp/.test(prev.name))) + def(child, "variable") + prev = child + } + }, + CapturePattern: defID("variable"), + AsPattern: defID("variable"), + __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 (node.name) { + let gather = gatherCompletions[node.name] + if (gather && gather(node, def, top) || !top && ScopeNodes.has(node.name)) return false + top = 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]*$/ + +const dontComplete = ["String", "FormatString", "Comment", "PropertyName"] + +/// Completion source that looks up locally defined names in +/// Python 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 + } +} + +const globals: readonly Completion[] = [ + "__annotations__", "__builtins__", "__debug__", "__doc__", "__import__", "__name__", + "__loader__", "__package__", "__spec__", + "False", "None", "True" +].map(n => ({label: n, type: "constant"})).concat([ + "ArithmeticError", "AssertionError", "AttributeError", "BaseException", "BlockingIOError", + "BrokenPipeError", "BufferError", "BytesWarning", "ChildProcessError", "ConnectionAbortedError", + "ConnectionError", "ConnectionRefusedError", "ConnectionResetError", "DeprecationWarning", + "EOFError", "Ellipsis", "EncodingWarning", "EnvironmentError", "Exception", "FileExistsError", + "FileNotFoundError", "FloatingPointError", "FutureWarning", "GeneratorExit", "IOError", + "ImportError", "ImportWarning", "IndentationError", "IndexError", "InterruptedError", + "IsADirectoryError", "KeyError", "KeyboardInterrupt", "LookupError", "MemoryError", + "ModuleNotFoundError", "NameError", "NotADirectoryError", "NotImplemented", "NotImplementedError", + "OSError", "OverflowError", "PendingDeprecationWarning", "PermissionError", "ProcessLookupError", + "RecursionError", "ReferenceError", "ResourceWarning", "RuntimeError", "RuntimeWarning", + "StopAsyncIteration", "StopIteration", "SyntaxError", "SyntaxWarning", "SystemError", + "SystemExit", "TabError", "TimeoutError", "TypeError", "UnboundLocalError", "UnicodeDecodeError", + "UnicodeEncodeError", "UnicodeError", "UnicodeTranslateError", "UnicodeWarning", "UserWarning", + "ValueError", "Warning", "ZeroDivisionError" +].map(n => ({label: n, type: "type"}))).concat([ + "bool", "bytearray", "bytes", "classmethod", "complex", "float", "frozenset", "int", "list", + "map", "memoryview", "object", "range", "set", "staticmethod", "str", "super", "tuple", "type" +].map(n => ({label: n, type: "class"}))).concat([ + "abs", "aiter", "all", "anext", "any", "ascii", "bin", "breakpoint", "callable", "chr", + "compile", "delattr", "dict", "dir", "divmod", "enumerate", "eval", "exec", "exit", "filter", + "format", "getattr", "globals", "hasattr", "hash", "help", "hex", "id", "input", "isinstance", + "issubclass", "iter", "len", "license", "locals", "max", "min", "next", "oct", "open", + "ord", "pow", "print", "property", "quit", "repr", "reversed", "round", "setattr", "slice", + "sorted", "sum", "vars", "zip" +].map(n => ({label: n, type: "function"}))) + +export const snippets: readonly Completion[] = [ + snip("def ${name}(${params}):\n\t${}", { + label: "def", + detail: "function", + type: "keyword" + }), + snip("for ${name} in ${collection}:\n\t${}", { + label: "for", + detail: "loop", + type: "keyword" + }), + snip("while ${}:\n\t${}", { + label: "while", + detail: "loop", + type: "keyword" + }), + snip("try:\n\t${}\nexcept ${error}:\n\t${}", { + label: "try", + detail: "/ except block", + type: "keyword" + }), + snip("if ${}:\n\t\n", { + label: "if", + detail: "block", + type: "keyword" + }), + snip("if ${}:\n\t${}\nelse:\n\t${}", { + label: "if", + detail: "/ else block", + type: "keyword" + }), + snip("class ${name}:\n\tdef __init__(self, ${params}):\n\t\t\t${}", { + label: "class", + detail: "definition", + type: "keyword" + }), + snip("import ${module}", { + label: "import", + detail: "statement", + type: "keyword" + }), + snip("from ${module} import ${names}", { + label: "from", + detail: "import", + type: "keyword" + }) +] + +/// Autocompletion for built-in Python globals and keywords. +export const globalCompletion = ifNotIn(dontComplete, completeFromList(globals.concat(snippets))) diff --git a/src/python.ts b/src/python.ts index 5bc1782..fa95cc5 100644 --- a/src/python.ts +++ b/src/python.ts @@ -1,61 +1,112 @@ -import {parser} from "lezer-python" -import {continuedIndent, indentNodeProp, foldNodeProp, foldInside, LezerLanguage, LanguageSupport} from "@codemirror/language" -import {styleTags, tags as t} from "@codemirror/highlight" +import {parser} from "@lezer/python" +import {SyntaxNode} from "@lezer/common" +import {delimitedIndent, indentNodeProp, TreeIndentContext, + foldNodeProp, foldInside, LRLanguage, LanguageSupport} from "@codemirror/language" +import {globalCompletion, localCompletionSource} from "./complete" +export {globalCompletion, localCompletionSource} + +function innerBody(context: TreeIndentContext) { + let {node, pos} = context + let lineIndent = context.lineIndent(pos, -1) + let found = null + for (;;) { + let before = node.childBefore(pos) + if (!before) { + break + } else if (before.name == "Comment") { + pos = before.from + } else if (before.name == "Body" || before.name == "MatchBody") { + if (context.baseIndentFor(before) + context.unit <= lineIndent) found = before + node = before + } else if (before.name == "MatchClause") { + node = before + } else if (before.type.is("Statement")) { + node = before + } else { + break + } + } + return found +} + +function indentBody(context: TreeIndentContext, node: SyntaxNode) { + let base = context.baseIndentFor(node) + let line = context.lineAt(context.pos, -1), to = line.from + line.text.length + // Don't consider blank, deindented lines at the end of the + // block part of the block + if (/^\s*($|#)/.test(line.text) && + context.node.to < to + 100 && + !/\S/.test(context.state.sliceDoc(to, context.node.to)) && + context.lineIndent(context.pos, -1) <= base) + return null + // A normally deindenting keyword that appears at a higher + // indentation than the block should probably be handled by the next + // level + if (/^\s*(else:|elif |except |finally:|case\s+[^=:]+:)/.test(context.textAfter) && context.lineIndent(context.pos, -1) > base) + return null + return base + context.unit +} /// A language provider based on the [Lezer Python /// parser](https://github.com/lezer-parser/python), extended with /// highlighting and indentation information. -export const pythonLanguage = LezerLanguage.define({ +export const pythonLanguage = LRLanguage.define({ + name: "python", parser: parser.configure({ props: [ indentNodeProp.add({ - Body: continuedIndent() + Body: context => { + let inner = innerBody(context) + return indentBody(context, inner || context.node) ?? context.continue() + }, + + MatchBody: context => { + let inner = innerBody(context) + return indentBody(context, inner || context.node) ?? context.continue() + }, + + IfStatement: cx => /^\s*(else:|elif )/.test(cx.textAfter) ? cx.baseIndent : cx.continue(), + "ForStatement WhileStatement": cx => /^\s*else:/.test(cx.textAfter) ? cx.baseIndent : cx.continue(), + TryStatement: cx => /^\s*(except |finally:|else:)/.test(cx.textAfter) ? cx.baseIndent : cx.continue(), + MatchStatement: cx => { + if (/^\s*case /.test(cx.textAfter)) return cx.baseIndent + cx.unit + return cx.continue() + }, + + "TupleExpression ComprehensionExpression ParamList ArgList ParenthesizedExpression": delimitedIndent({closing: ")"}), + "DictionaryExpression DictionaryComprehensionExpression SetExpression SetComprehensionExpression": delimitedIndent({closing: "}"}), + "ArrayExpression ArrayComprehensionExpression": delimitedIndent({closing: "]"}), + MemberExpression: cx => cx.baseIndent + cx.unit, + "String FormatString": () => null, + Script: context => { + let inner = innerBody(context) + return (inner && indentBody(context, inner)) ?? context.continue() + }, }), + foldNodeProp.add({ - "Body ArrayExpression DictionaryExpression": foldInside - }), - styleTags({ - "async '*' '**' FormatConversion": t.modifier, - "for while if elif else try except finally return raise break continue with pass assert await yield": t.controlKeyword, - "in not and or is del": t.operatorKeyword, - "import from def class global nonlocal lambda": t.definitionKeyword, - "with as print": t.keyword, - self: t.self, - Boolean: t.bool, - None: t.null, - VariableName: t.variableName, - "CallExpression/VariableName": t.function(t.variableName), - "FunctionDefinition/VariableName": t.function(t.definition(t.variableName)), - "ClassDefinition/VariableName": t.definition(t.className), - PropertyName: t.propertyName, - "CallExpression/MemberExpression/ProperyName": t.function(t.propertyName), - Comment: t.lineComment, - Number: t.number, - String: t.string, - FormatString: t.special(t.string), - UpdateOp: t.updateOperator, - ArithOp: t.arithmeticOperator, - BitOp: t.bitwiseOperator, - CompareOp: t.compareOperator, - AssignOp: t.definitionOperator, - Ellipsis: t.punctuation, - At: t.meta, - "( )": t.paren, - "[ ]": t.squareBracket, - "{ }": t.brace, - ".": t.derefOperator, - ", ;": t.separator + "ArrayExpression DictionaryExpression SetExpression TupleExpression": foldInside, + Body: (node, state) => ({from: node.from + 1, to: node.to - (node.to == state.doc.length ? 0 : 1)}), + "String FormatString": (node, state) => ({from: state.doc.lineAt(node.from).to, to: node.to}) }) ], }), languageData: { - closeBrackets: {brackets: ["(", "[", "{", "'", '"', "'''", '"""']}, + closeBrackets: { + brackets: ["(", "[", "{", "'", '"', "'''", '"""'], + stringPrefixes: ["f", "fr", "rf", "r", "u", "b", "br", "rb", + "F", "FR", "RF", "R", "U", "B", "BR", "RB"] + }, commentTokens: {line: "#"}, - indentOnInput: /^\s*[\}\]\)]$/ + // Indent logic logic are triggered upon below input patterns + indentOnInput: /^\s*([\}\]\)]|else:|elif |except |finally:|case\s+[^:]*:?)$/, } }) /// Python language support. export function python() { - return new LanguageSupport(pythonLanguage) + return new LanguageSupport(pythonLanguage, [ + pythonLanguage.data.of({autocomplete: localCompletionSource}), + pythonLanguage.data.of({autocomplete: globalCompletion}), + ]) } diff --git a/test/test-indent.ts b/test/test-indent.ts new file mode 100644 index 0000000..513e1d4 --- /dev/null +++ b/test/test-indent.ts @@ -0,0 +1,207 @@ +import ist from "ist" +import {EditorState} from "@codemirror/state" +import {getIndentation} from "@codemirror/language" +import {python} from "@codemirror/lang-python" + +function check(code: string) { + return () => { + code = /^\n*([^]*)/.exec(code)![1] + let state = EditorState.create({doc: code, extensions: [python().language]}) + for (let pos = 0, lines = code.split("\n"), i = 0; i < lines.length; i++) { + let line = lines[i], indent = /^\s*/.exec(line)![0].length + ist(`indent=${getIndentation(state, pos)} (line-numer=${i + 1})`, `indent=${indent} (line-numer=${i + 1})`) + pos += line.length + 1 + } + } +} + +describe("python indentation", () => { + it("indents bodies", check(` +def foo(): + bar + baz + +`)) + + it("indents function arg lists", check(` +foo( + bar, + baz +)`)) + + it("indents nested bodies", check(` +def foo(): + if True: + a + elif False: + b + else: + c +`)) + + it("dedents except", check(` +try: + foo() +except e: + bar() +`)) + + it("multi-line-block try-except", check(` +try: + foo() + fooz() +except e: + bar() + barz() +finally: + baz() + bazz() +`)) + + + it("multi-line-nested-block try-except", check(` +try: + foo() + fooz() + try: + inner() + inner2() + except e2: + f3() + f4() + else: + f5() + f6() + finally: + f7() + f8() +except e: + bar() + barz() +finally: + baz() + bazz() +`)) + + it("match-case", check(` +match x: + case 1: + foo() + case 2: + bar() + case _: + bar() +`)) + + it("match-case-multi-line-block", check(` +def func(): + match x: + case 1: + foo() + fooz() + case 2: + bar() + bar() + bar() + match y: + case 3: + bar() + case 4: + bar() + case _: + bar() +`)) + + it("class-with-decorators", check(` +@decorator1 +@decorator2( + param1, + param2 +) +class MyClass: + def method(self): + pass +`)) + + it("list-comprehension", check(` +result = [ + x * y + for x in range(10) + for y in range(5) + if x > y +] +`)) + + it("multi-line-expressions", check(` +result = ( + very_long_variable_name + + another_long_variable * + some_computation( + arg1, + arg2 + ) +) +`)) + + it("async-function-and-with", check(` +async def process_data(): + async with context() as ctx: + result = await ctx.fetch( + url, + timeout=30 + ) + return result +`)) + + it("nested-functions", check(` +def outer(): + x = 1 + def inner1(): + y = 2 + def inner2(): + z = 3 + return x + y + z + return inner2() + return inner1() +`)) + + it("type-hints-and-annotations", check(` +def process_data( + data: list[str], + config: dict[str, Any] +) -> tuple[int, str]: + result: Optional[str] = None + if data: + result = data[0] + return len(data), result +`)) + + it("multi-line-dict-comprehension", check(` +config = { + key: value + for key, value in items + if is_valid( + key, + value + ) +} +`)) + + it("multi-line-with-comments", check(` +def process( + x: int, # The input value + y: float # The coefficient +): + # Compute first step + result = x * y + + # Apply additional processing + if result > 0: + # Positive case + return result + else: + # Negative case + return -result +`)) + +}) diff --git a/tsconfig.local.json b/tsconfig.local.json deleted file mode 100644 index 39f3353..0000000 --- a/tsconfig.local.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es6", "dom", "scripthost"], - "types": ["mocha"], - "stripInternal": true, - "noUnusedLocals": true, - "strict": true, - "target": "es6", - "module": "es2020", - "newLine": "lf", - "declaration": true, - "declarationMap": true, - "moduleResolution": "node", - "paths": { - "@codemirror/lang-python": ["./src/python.ts"] - } - }, - "include": ["src/*.ts", "test/*.ts"] -}