ipython-postfix-completion 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/CHANGELOG.md +15 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/MANIFEST.in +1 -0
- ipython_postfix_completion-0.2.1/PKG-INFO +115 -0
- ipython_postfix_completion-0.2.1/README.md +80 -0
- ipython_postfix_completion-0.2.1/docs/REFERENCE.md +150 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/ipython_postfix_completion/__init__.py +141 -16
- ipython_postfix_completion-0.2.1/ipython_postfix_completion.egg-info/PKG-INFO +115 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/ipython_postfix_completion.egg-info/SOURCES.txt +1 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/pyproject.toml +1 -1
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/tests/test_postfix_completion.py +79 -7
- ipython_postfix_completion-0.2.0/PKG-INFO +0 -282
- ipython_postfix_completion-0.2.0/README.md +0 -247
- ipython_postfix_completion-0.2.0/ipython_postfix_completion.egg-info/PKG-INFO +0 -282
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/LICENSE +0 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/ipython_postfix_completion.egg-info/dependency_links.txt +0 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/ipython_postfix_completion.egg-info/requires.txt +0 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/ipython_postfix_completion.egg-info/top_level.txt +0 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/setup.cfg +0 -0
- {ipython_postfix_completion-0.2.0 → ipython_postfix_completion-0.2.1}/tests/conftest.py +0 -0
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.1] - 2026-10-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added built-in `.for`, `.fori`, `.ef`, `.type`, and `.range` templates.
|
|
10
|
+
- Added editable, sequential Tab stops for loop and conditional templates,
|
|
11
|
+
with Shift+Tab navigation to previous stops.
|
|
12
|
+
- Wrapped integer literals in `range(...)` for `.for` and `.fori` templates.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Updated the built-in `.if` template to include an editable `pass` body.
|
|
17
|
+
- Simplified the README and moved detailed usage and maintenance notes to
|
|
18
|
+
`docs/REFERENCE.md`.
|
|
19
|
+
|
|
5
20
|
## [0.2.0] - 2026-08-09
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ipython-postfix-completion
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Configurable postfix completion extension for IPython.
|
|
5
|
+
Author: IPython Postfix Completion Contributors
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/fishandsheep/ipython-postfix-completion
|
|
8
|
+
Project-URL: Repository, https://github.com/fishandsheep/ipython-postfix-completion
|
|
9
|
+
Project-URL: Issues, https://github.com/fishandsheep/ipython-postfix-completion/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/fishandsheep/ipython-postfix-completion/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: ipython,completion,postfix,extension
|
|
12
|
+
Classifier: Framework :: IPython
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: ipython<10,>=9.0
|
|
25
|
+
Requires-Dist: traitlets>=5.13
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
|
+
Requires-Dist: ipython-postfix-completion[test]; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# IPython Postfix Completion
|
|
37
|
+
|
|
38
|
+
Complete Python expressions with postfix templates in IPython: type an
|
|
39
|
+
expression, a dot, and a template name, then press Tab.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install ipython --with ipython-postfix-completion
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Start IPython and load the extension:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
%load_ext ipython_postfix_completion
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Common Templates
|
|
54
|
+
|
|
55
|
+
`items.for<Tab>` creates a loop. Tab selects `item`, then `pass`; type to
|
|
56
|
+
replace a selection.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
for item in items:
|
|
60
|
+
pass
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An integer literal is wrapped in `range(...)`, so `10.for<Tab>` produces
|
|
64
|
+
`for item in range(10):`.
|
|
65
|
+
|
|
66
|
+
`items.fori<Tab>` creates an indexed loop. Tab selects `i`, `value`, then
|
|
67
|
+
`pass`. For an integer literal, it uses `enumerate(range(n))`.
|
|
68
|
+
|
|
69
|
+
`condition.if<Tab>` creates an `if` block with a selected `pass`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
if condition:
|
|
73
|
+
pass
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`condition.ef<Tab>` creates `if`/`elif`/`else` branches. Tab selects each
|
|
77
|
+
placeholder in order: `pass`, `cond`, `pass`, `pass`. The `cond` selection
|
|
78
|
+
does not include its colon.
|
|
79
|
+
|
|
80
|
+
Other built-ins: `print`, `len`, `not`, `par`, `var`, `await`, `return`,
|
|
81
|
+
`while`, `raise`, `yield`, `str`, `list`, `set`, `dict`, `tuple`, `type`, and
|
|
82
|
+
`range`. Use `%postfix_template list` to see their expansions.
|
|
83
|
+
|
|
84
|
+
Shift+Tab moves back to the previous placeholder. Custom Tab navigation is
|
|
85
|
+
supported in terminal IPython.
|
|
86
|
+
|
|
87
|
+
## Load Automatically
|
|
88
|
+
|
|
89
|
+
The default configuration file is:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
~/.ipython/profile_default/ipython_config.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Create it with:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ipython profile create
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Add the extension and any persistent templates there:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
c.InteractiveShellApp.extensions = ["ipython_postfix_completion"]
|
|
105
|
+
c.PostfixCompletionConfig.templates = {
|
|
106
|
+
"debug": "print({expr}=)",
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`IPYTHONDIR` and `--ipython-dir` can change the configuration directory. Use
|
|
111
|
+
`ipython locate profile default` to find the active profile. See the [official
|
|
112
|
+
IPython configuration guide](https://ipython.readthedocs.io/en/stable/development/config.html).
|
|
113
|
+
|
|
114
|
+
For runtime template commands, detailed behavior, and contributor or release
|
|
115
|
+
steps, see [the extended guide](docs/REFERENCE.md).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# IPython Postfix Completion
|
|
2
|
+
|
|
3
|
+
Complete Python expressions with postfix templates in IPython: type an
|
|
4
|
+
expression, a dot, and a template name, then press Tab.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv tool install ipython --with ipython-postfix-completion
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Start IPython and load the extension:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
%load_ext ipython_postfix_completion
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Common Templates
|
|
19
|
+
|
|
20
|
+
`items.for<Tab>` creates a loop. Tab selects `item`, then `pass`; type to
|
|
21
|
+
replace a selection.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
for item in items:
|
|
25
|
+
pass
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
An integer literal is wrapped in `range(...)`, so `10.for<Tab>` produces
|
|
29
|
+
`for item in range(10):`.
|
|
30
|
+
|
|
31
|
+
`items.fori<Tab>` creates an indexed loop. Tab selects `i`, `value`, then
|
|
32
|
+
`pass`. For an integer literal, it uses `enumerate(range(n))`.
|
|
33
|
+
|
|
34
|
+
`condition.if<Tab>` creates an `if` block with a selected `pass`:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
if condition:
|
|
38
|
+
pass
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`condition.ef<Tab>` creates `if`/`elif`/`else` branches. Tab selects each
|
|
42
|
+
placeholder in order: `pass`, `cond`, `pass`, `pass`. The `cond` selection
|
|
43
|
+
does not include its colon.
|
|
44
|
+
|
|
45
|
+
Other built-ins: `print`, `len`, `not`, `par`, `var`, `await`, `return`,
|
|
46
|
+
`while`, `raise`, `yield`, `str`, `list`, `set`, `dict`, `tuple`, `type`, and
|
|
47
|
+
`range`. Use `%postfix_template list` to see their expansions.
|
|
48
|
+
|
|
49
|
+
Shift+Tab moves back to the previous placeholder. Custom Tab navigation is
|
|
50
|
+
supported in terminal IPython.
|
|
51
|
+
|
|
52
|
+
## Load Automatically
|
|
53
|
+
|
|
54
|
+
The default configuration file is:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
~/.ipython/profile_default/ipython_config.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Create it with:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ipython profile create
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Add the extension and any persistent templates there:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
c.InteractiveShellApp.extensions = ["ipython_postfix_completion"]
|
|
70
|
+
c.PostfixCompletionConfig.templates = {
|
|
71
|
+
"debug": "print({expr}=)",
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`IPYTHONDIR` and `--ipython-dir` can change the configuration directory. Use
|
|
76
|
+
`ipython locate profile default` to find the active profile. See the [official
|
|
77
|
+
IPython configuration guide](https://ipython.readthedocs.io/en/stable/development/config.html).
|
|
78
|
+
|
|
79
|
+
For runtime template commands, detailed behavior, and contributor or release
|
|
80
|
+
steps, see [the extended guide](docs/REFERENCE.md).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Extended Reference
|
|
2
|
+
|
|
3
|
+
This guide contains detailed template, configuration, Tab behavior, development,
|
|
4
|
+
and release information. For installation and common usage, see the [README](../README.md).
|
|
5
|
+
|
|
6
|
+
## Template Reference
|
|
7
|
+
|
|
8
|
+
Templates take the expression to the left of the dot as `{expr}`. `{indent}` is
|
|
9
|
+
the leading indentation of the input line. These are the built-in templates:
|
|
10
|
+
|
|
11
|
+
| Name | Expansion |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `print` | `print({expr})` |
|
|
14
|
+
| `len` | `len({expr})` |
|
|
15
|
+
| `not` | `not {expr}` |
|
|
16
|
+
| `par` | `({expr})` |
|
|
17
|
+
| `var` | `key = {expr}`; selects `key` |
|
|
18
|
+
| `await` | `await {expr}` |
|
|
19
|
+
| `return` | `return {expr}` |
|
|
20
|
+
| `if` | `if {expr}:` with an indented `pass` |
|
|
21
|
+
| `while` | `while {expr}:` with an indented blank body |
|
|
22
|
+
| `for` | `for item in {expr}:` with an indented `pass` |
|
|
23
|
+
| `fori` | `for i, value in enumerate({expr}):` with an indented `pass` |
|
|
24
|
+
| `ef` | `if {expr}:`, `elif cond:`, and `else:` branches, each with `pass` |
|
|
25
|
+
| `raise` | `raise {expr}` |
|
|
26
|
+
| `yield` | `yield {expr}` |
|
|
27
|
+
| `str` | `str({expr})` |
|
|
28
|
+
| `list` | `list({expr})` |
|
|
29
|
+
| `set` | `set({expr})` |
|
|
30
|
+
| `dict` | `dict({expr})` |
|
|
31
|
+
| `tuple` | `tuple({expr})` |
|
|
32
|
+
| `type` | `type({expr})` |
|
|
33
|
+
| `range` | `range({expr})` |
|
|
34
|
+
|
|
35
|
+
Integer literals used with `.for` or `.fori` are wrapped in `range(...)`.
|
|
36
|
+
Signed and underscored integer literals are recognized. A variable such as `n`
|
|
37
|
+
is left unchanged because completion cannot know its runtime type. Thus
|
|
38
|
+
`10.fori` uses `enumerate(range(10))`, while `items.fori` uses
|
|
39
|
+
`enumerate(items)`.
|
|
40
|
+
|
|
41
|
+
When a partial name matches multiple templates, the completion menu lists each
|
|
42
|
+
match. In particular, `.f` can show both `for` and `fori`; Tab on the exact
|
|
43
|
+
`.if` suffix expands `if`.
|
|
44
|
+
|
|
45
|
+
## Placeholder and Tab Behavior
|
|
46
|
+
|
|
47
|
+
For built-in `.var`, `.for`, `.if`, `.fori`, and `.ef` templates, Tab accepts
|
|
48
|
+
the selected placeholder and advances to the next one. Shift+Tab selects the
|
|
49
|
+
previous placeholder. Typing replaces the selected text. After the final stop,
|
|
50
|
+
Tab accepts it and places the cursor after the expansion. Enter accepts the
|
|
51
|
+
selected `.var` name; press Enter again to submit the input.
|
|
52
|
+
|
|
53
|
+
The built-in placeholder behavior is enabled only when the corresponding
|
|
54
|
+
template retains its built-in definition. Overriding a built-in template
|
|
55
|
+
disables its special selection behavior.
|
|
56
|
+
|
|
57
|
+
### Smart Tab Jump
|
|
58
|
+
|
|
59
|
+
Smart Tab jump is enabled by default in terminal IPython. When the cursor is
|
|
60
|
+
immediately before a valid Python closing token, Tab moves over it without
|
|
61
|
+
changing source text. Repeated presses exit nested constructs:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
"hello|" -> "hello"|
|
|
65
|
+
print("hello|") -> print("hello"|) -> print("hello")|
|
|
66
|
+
print(f"{name|}") -> print(f"{name}|") -> print(f"{name}"|) -> print(f"{name}")|
|
|
67
|
+
items[index|] -> items[index]|
|
|
68
|
+
list[dict[str, int|]] -> list[dict[str, int]|] -> list[dict[str, int]]|
|
|
69
|
+
{"name": value|} -> {"name": value}|
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`|` marks the cursor and is not typed. Supported closers are single and triple
|
|
73
|
+
quotes plus `)`, `]`, and `}`. Detection follows Python tokens, including
|
|
74
|
+
multiline input, string prefixes, and f-string expressions. Tab processes an
|
|
75
|
+
active placeholder and an exact postfix template first, then falls back to
|
|
76
|
+
IPython completion, closer movement, or indentation. Ambiguous `< >`, colon,
|
|
77
|
+
and comma are not treated as closers.
|
|
78
|
+
|
|
79
|
+
Disable smart Tab jump while retaining postfix completion with:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
c.PostfixCompletionConfig.smart_tab_jump = False
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Runtime Template Commands
|
|
86
|
+
|
|
87
|
+
Add or override a template for the current IPython session:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
%postfix_template add debug "print({expr}=)"
|
|
91
|
+
%postfix_template add forin "for item in {expr}:\n{indent} pass"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Disable, reset one, or reset all runtime changes:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
%postfix_template remove tuple
|
|
98
|
+
%postfix_template reset forin
|
|
99
|
+
%postfix_template reset --all
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Runtime changes last only for the current session. Use
|
|
103
|
+
`%postfix_template list` to see built-in, custom, and disabled entries.
|
|
104
|
+
|
|
105
|
+
Persistent configuration belongs in
|
|
106
|
+
`~/.ipython/profile_default/ipython_config.py` by default. Create the profile
|
|
107
|
+
with `ipython profile create`; locate the active path with
|
|
108
|
+
`ipython locate profile default`. `IPYTHONDIR` or `--ipython-dir` can change
|
|
109
|
+
the IPython directory. See the [official IPython configuration guide](https://ipython.readthedocs.io/en/stable/development/config.html).
|
|
110
|
+
|
|
111
|
+
Template names must match `[A-Za-z_][A-Za-z0-9_]*`. Templates must include
|
|
112
|
+
`{expr}` and may also use `{indent}`. No other fields, conversions, or format
|
|
113
|
+
specifiers are allowed.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
Run tests and style checks:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv run --extra test pytest -q
|
|
121
|
+
uv run --extra dev ruff check .
|
|
122
|
+
uv run --extra dev ruff format --check .
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Run IPython against the current checkout without installing into system Python:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv run --with ipython --with-editable . ipython
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then load `ipython_postfix_completion` in that IPython session.
|
|
132
|
+
|
|
133
|
+
Build and validate release artifacts:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv run --extra dev python -m build
|
|
137
|
+
uv run --extra dev python -m twine check dist/*
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Publishing
|
|
141
|
+
|
|
142
|
+
Releases use GitHub Actions and PyPI Trusted Publishing. Configure the PyPI
|
|
143
|
+
publisher for the `fishandsheep/ipython-postfix-completion` repository, the
|
|
144
|
+
`publish.yml` workflow, and the `pypi` environment. Update the version in
|
|
145
|
+
`pyproject.toml`, push the change, then create and push the matching `v` tag.
|
|
146
|
+
The workflow checks the tag against the project version, runs tests, builds and
|
|
147
|
+
checks distributions, and publishes them. Published PyPI versions and release
|
|
148
|
+
tags are immutable; use a new version for fixes.
|
|
149
|
+
|
|
150
|
+
See [CHANGELOG.md](../CHANGELOG.md) for release history.
|
|
@@ -35,6 +35,19 @@ _NO_MAGIC_ATTR = object()
|
|
|
35
35
|
_ACTIVE_STATE: PostfixState | None = None
|
|
36
36
|
_VAR_TEMPLATE = "key = {expr}"
|
|
37
37
|
_VAR_PLACEHOLDER = "key"
|
|
38
|
+
_FOR_TEMPLATE = "for item in {expr}:\n{indent} pass"
|
|
39
|
+
_FORI_TEMPLATE = "for i, value in enumerate({expr}):\n{indent} pass"
|
|
40
|
+
_IF_TEMPLATE = "if {expr}:\n{indent} pass"
|
|
41
|
+
_EF_TEMPLATE = (
|
|
42
|
+
"if {expr}:\n{indent} pass\n{indent}elif cond:\n{indent} pass"
|
|
43
|
+
"\n{indent}else:\n{indent} pass"
|
|
44
|
+
)
|
|
45
|
+
_TEMPLATE_PLACEHOLDERS = {
|
|
46
|
+
"for": (_FOR_TEMPLATE, ("item", "pass")),
|
|
47
|
+
"fori": (_FORI_TEMPLATE, ("i", "value", "pass")),
|
|
48
|
+
"if": (_IF_TEMPLATE, ("pass",)),
|
|
49
|
+
"ef": (_EF_TEMPLATE, ("pass", "cond", "pass", "pass")),
|
|
50
|
+
}
|
|
38
51
|
_PLACEHOLDER_ATTR = "_postfix_completion_placeholder"
|
|
39
52
|
_OPENING_BRACKETS = {"(": ")", "[": "]", "{": "}"}
|
|
40
53
|
_CLOSING_BRACKETS = {closer: opener for opener, closer in _OPENING_BRACKETS.items()}
|
|
@@ -59,8 +72,11 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
59
72
|
"var": _VAR_TEMPLATE,
|
|
60
73
|
"await": "await {expr}",
|
|
61
74
|
"return": "return {expr}",
|
|
62
|
-
"if":
|
|
75
|
+
"if": _IF_TEMPLATE,
|
|
63
76
|
"while": "while {expr}:\n{indent} ",
|
|
77
|
+
"for": _FOR_TEMPLATE,
|
|
78
|
+
"fori": _FORI_TEMPLATE,
|
|
79
|
+
"ef": _EF_TEMPLATE,
|
|
64
80
|
"raise": "raise {expr}",
|
|
65
81
|
"yield": "yield {expr}",
|
|
66
82
|
"str": "str({expr})",
|
|
@@ -68,6 +84,8 @@ DEFAULT_TEMPLATES: dict[str, str] = {
|
|
|
68
84
|
"set": "set({expr})",
|
|
69
85
|
"dict": "dict({expr})",
|
|
70
86
|
"tuple": "tuple({expr})",
|
|
87
|
+
"type": "type({expr})",
|
|
88
|
+
"range": "range({expr})",
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
|
|
@@ -143,6 +161,30 @@ def _render_template(template: str, expr: str, indent: str = "") -> str:
|
|
|
143
161
|
return _FORMATTER.format(template, expr=expr, indent=indent)
|
|
144
162
|
|
|
145
163
|
|
|
164
|
+
def _is_integer_literal(expr: str) -> bool:
|
|
165
|
+
try:
|
|
166
|
+
node = ast.parse(expr, mode="eval").body
|
|
167
|
+
except SyntaxError:
|
|
168
|
+
return False
|
|
169
|
+
if isinstance(node, ast.Constant):
|
|
170
|
+
return type(node.value) is int
|
|
171
|
+
return (
|
|
172
|
+
isinstance(node, ast.UnaryOp)
|
|
173
|
+
and isinstance(node.op, (ast.UAdd, ast.USub))
|
|
174
|
+
and _is_integer_literal(ast.unparse(node.operand))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _render_candidate(name: str, template: str, expr: str, indent: str = "") -> str:
|
|
179
|
+
wraps_integer = _is_integer_literal(expr) and (
|
|
180
|
+
(name == "for" and template == _FOR_TEMPLATE)
|
|
181
|
+
or (name == "fori" and template == _FORI_TEMPLATE)
|
|
182
|
+
)
|
|
183
|
+
if wraps_integer:
|
|
184
|
+
expr = f"range({expr})"
|
|
185
|
+
return _render_template(template, expr, indent)
|
|
186
|
+
|
|
187
|
+
|
|
146
188
|
@dataclass
|
|
147
189
|
class PostfixState:
|
|
148
190
|
matcher: Callable[[CompletionContext], dict]
|
|
@@ -244,7 +286,7 @@ def _effective_templates(state: PostfixState | None = None) -> dict[str, str]:
|
|
|
244
286
|
def _apply_template(
|
|
245
287
|
name: str, expr: str, indent: str = "", state: PostfixState | None = None
|
|
246
288
|
) -> str:
|
|
247
|
-
return
|
|
289
|
+
return _render_candidate(name, _effective_templates(state)[name], expr, indent)
|
|
248
290
|
|
|
249
291
|
|
|
250
292
|
def _postfix_candidates(
|
|
@@ -264,7 +306,7 @@ def _postfix_candidates(
|
|
|
264
306
|
if (name == prefix if exact else name.startswith(prefix))
|
|
265
307
|
]
|
|
266
308
|
return f"{expr}.{prefix}", [
|
|
267
|
-
|
|
309
|
+
_render_candidate(name, templates[name], expr, indent) for name in names
|
|
268
310
|
]
|
|
269
311
|
|
|
270
312
|
|
|
@@ -278,7 +320,7 @@ def _template_name(
|
|
|
278
320
|
for name, template in _effective_templates(state).items():
|
|
279
321
|
if not name.startswith(prefix):
|
|
280
322
|
continue
|
|
281
|
-
if
|
|
323
|
+
if _render_candidate(name, template, expr, indent) == expansion:
|
|
282
324
|
return name
|
|
283
325
|
return None
|
|
284
326
|
|
|
@@ -306,7 +348,28 @@ def _expand_buffer(buffer) -> bool:
|
|
|
306
348
|
buffer.delete_before_cursor(len(matched_fragment))
|
|
307
349
|
expansion_start = buffer.cursor_position
|
|
308
350
|
buffer.insert_text(candidates[0])
|
|
351
|
+
placeholder_config = (
|
|
352
|
+
_TEMPLATE_PLACEHOLDERS.get(parsed[2]) if parsed is not None else None
|
|
353
|
+
)
|
|
309
354
|
if (
|
|
355
|
+
parsed is not None
|
|
356
|
+
and placeholder_config is not None
|
|
357
|
+
and _effective_templates().get(parsed[2]) == placeholder_config[0]
|
|
358
|
+
):
|
|
359
|
+
search_from = 0
|
|
360
|
+
ranges = []
|
|
361
|
+
for placeholder_text in placeholder_config[1]:
|
|
362
|
+
start = candidates[0].index(placeholder_text, search_from)
|
|
363
|
+
end = start + len(placeholder_text)
|
|
364
|
+
ranges.append((start, end))
|
|
365
|
+
search_from = end
|
|
366
|
+
_select_placeholders(
|
|
367
|
+
buffer,
|
|
368
|
+
expansion_start,
|
|
369
|
+
ranges,
|
|
370
|
+
buffer.cursor_position,
|
|
371
|
+
)
|
|
372
|
+
elif (
|
|
310
373
|
parsed is not None
|
|
311
374
|
and parsed[2] == "var"
|
|
312
375
|
and _effective_templates().get("var") == _VAR_TEMPLATE
|
|
@@ -321,35 +384,93 @@ def _expand_buffer(buffer) -> bool:
|
|
|
321
384
|
|
|
322
385
|
|
|
323
386
|
def _select_placeholder(buffer, start: int, end: int, final_cursor: int) -> None:
|
|
387
|
+
_select_placeholders(buffer, start, [(start, end)], final_cursor, absolute=True)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _select_placeholders(
|
|
391
|
+
buffer,
|
|
392
|
+
start: int,
|
|
393
|
+
ranges: list[tuple[int, int]],
|
|
394
|
+
final_cursor: int,
|
|
395
|
+
*,
|
|
396
|
+
absolute: bool = False,
|
|
397
|
+
index: int = 0,
|
|
398
|
+
) -> None:
|
|
324
399
|
from prompt_toolkit.selection import SelectionState, SelectionType
|
|
325
400
|
|
|
326
|
-
|
|
327
|
-
|
|
401
|
+
if not absolute:
|
|
402
|
+
ranges = [(start + left, start + right) for left, right in ranges]
|
|
403
|
+
setattr(buffer, _PLACEHOLDER_ATTR, (ranges, index, final_cursor))
|
|
404
|
+
left, right = ranges[index]
|
|
405
|
+
buffer.cursor_position = right
|
|
406
|
+
buffer.selection_state = SelectionState(left, SelectionType.CHARACTERS)
|
|
328
407
|
buffer.selection_state.enter_shift_mode()
|
|
329
|
-
setattr(buffer, _PLACEHOLDER_ATTR, (start, end, final_cursor))
|
|
330
408
|
|
|
331
409
|
|
|
332
410
|
def _has_active_placeholder(buffer) -> bool:
|
|
333
411
|
placeholder = getattr(buffer, _PLACEHOLDER_ATTR, None)
|
|
334
412
|
selection = buffer.selection_state
|
|
335
|
-
if placeholder is None
|
|
413
|
+
if placeholder is None:
|
|
336
414
|
return False
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
415
|
+
ranges, index, _ = placeholder
|
|
416
|
+
start, end = ranges[index]
|
|
417
|
+
return (selection is None and len(ranges) > 1) or (
|
|
418
|
+
selection is not None
|
|
419
|
+
and selection.shift_mode
|
|
340
420
|
and selection.original_cursor_position == start
|
|
341
421
|
and buffer.cursor_position == end
|
|
342
|
-
and buffer.text[start:end] == _VAR_PLACEHOLDER
|
|
422
|
+
and (len(ranges) > 1 or buffer.text[start:end] == _VAR_PLACEHOLDER)
|
|
343
423
|
)
|
|
344
424
|
|
|
345
425
|
|
|
346
426
|
def _accept_placeholder(buffer) -> bool:
|
|
347
427
|
if not _has_active_placeholder(buffer):
|
|
348
428
|
return False
|
|
349
|
-
|
|
350
|
-
buffer.
|
|
351
|
-
|
|
352
|
-
|
|
429
|
+
ranges, index, final_cursor = _update_placeholder_after_edit(buffer)
|
|
430
|
+
if buffer.selection_state is not None:
|
|
431
|
+
buffer.exit_selection()
|
|
432
|
+
if index + 1 < len(ranges):
|
|
433
|
+
_select_placeholders(
|
|
434
|
+
buffer, 0, ranges, final_cursor, absolute=True, index=index + 1
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
delattr(buffer, _PLACEHOLDER_ATTR)
|
|
438
|
+
buffer.cursor_position = final_cursor
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _update_placeholder_after_edit(buffer):
|
|
443
|
+
ranges, index, final_cursor = getattr(buffer, _PLACEHOLDER_ATTR)
|
|
444
|
+
if buffer.selection_state is None:
|
|
445
|
+
start, old_end = ranges[index]
|
|
446
|
+
new_end = buffer.cursor_position
|
|
447
|
+
delta = new_end - old_end
|
|
448
|
+
final_cursor += delta
|
|
449
|
+
ranges = [
|
|
450
|
+
(start, new_end)
|
|
451
|
+
if stop == index
|
|
452
|
+
else (left + delta, right + delta)
|
|
453
|
+
if stop > index
|
|
454
|
+
else (left, right)
|
|
455
|
+
for stop, (left, right) in enumerate(ranges)
|
|
456
|
+
]
|
|
457
|
+
setattr(buffer, _PLACEHOLDER_ATTR, (ranges, index, final_cursor))
|
|
458
|
+
return ranges, index, final_cursor
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _reverse_placeholder(buffer) -> bool:
|
|
462
|
+
if not _has_active_placeholder(buffer):
|
|
463
|
+
return False
|
|
464
|
+
ranges, index, final_cursor = _update_placeholder_after_edit(buffer)
|
|
465
|
+
previous_index = max(0, index - 1)
|
|
466
|
+
_select_placeholders(
|
|
467
|
+
buffer,
|
|
468
|
+
0,
|
|
469
|
+
ranges,
|
|
470
|
+
final_cursor,
|
|
471
|
+
absolute=True,
|
|
472
|
+
index=previous_index,
|
|
473
|
+
)
|
|
353
474
|
return True
|
|
354
475
|
|
|
355
476
|
|
|
@@ -657,6 +778,10 @@ def _make_key_bindings(state: PostfixState | None = None):
|
|
|
657
778
|
def accept_placeholder(event) -> None:
|
|
658
779
|
_accept_placeholder(event.current_buffer)
|
|
659
780
|
|
|
781
|
+
@key_bindings.add("s-tab", filter=has_active_placeholder)
|
|
782
|
+
def reverse_placeholder(event) -> None:
|
|
783
|
+
_reverse_placeholder(event.current_buffer)
|
|
784
|
+
|
|
660
785
|
@key_bindings.add("enter", filter=has_active_placeholder)
|
|
661
786
|
def accept_placeholder_with_enter(event) -> None:
|
|
662
787
|
_accept_placeholder(event.current_buffer)
|