jvim 0.7.0__tar.gz → 0.7.2__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.
- jvim-0.7.2/.claude/settings.local.json +13 -0
- {jvim-0.7.0/.worktrees/codex-fix-subst → jvim-0.7.2}/.gitignore +1 -0
- jvim-0.7.2/.swp +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/PKG-INFO +12 -1
- {jvim-0.7.0 → jvim-0.7.2}/README.kr.md +11 -0
- {jvim-0.7.0 → jvim-0.7.2}/README.md +11 -0
- jvim-0.7.2/docs/vim-compatibility-report.md +192 -0
- {jvim-0.7.0 → jvim-0.7.2}/pyproject.toml +1 -1
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/__init__.py +1 -1
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/__init__.py +6 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/clipboard.py +3 -4
- jvim-0.7.2/src/jvim/action/content.py +238 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/folding.py +17 -0
- jvim-0.7.2/src/jvim/action/jsonpath_locator.py +216 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/navigation.py +14 -0
- jvim-0.7.2/src/jvim/action/render.py +735 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/substitute.py +45 -23
- jvim-0.7.2/src/jvim/action/undo.py +72 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/visual.py +1 -5
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/data/help.json +1 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/differ.py +24 -13
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/editor.py +1 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/command.py +129 -86
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/normal.py +198 -139
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/search.py +15 -204
- jvim-0.7.2/src/jvim/widget.py +366 -0
- {jvim-0.7.0 → jvim-0.7.2}/tests/test_editor.py +113 -0
- jvim-0.7.2/tests/test_jsonpath_locator.py +52 -0
- {jvim-0.7.0 → jvim-0.7.2}/uv.lock +1 -1
- jvim-0.7.0/.claude/CLAUDE.md +0 -7
- jvim-0.7.0/.claude/settings.local.json +0 -40
- jvim-0.7.0/src/jvim/widget.py +0 -1276
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/.git +0 -0
- {jvim-0.7.0 → jvim-0.7.2/.worktrees/codex-fix-fold}/.gitignore +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/CLAUDE.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/README.kr.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/README.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/pyproject.toml +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/__init__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/__main__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/data/help.json +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/data/sample.json +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/diff.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/differ.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/differ.tcss +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/editor.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/editor.tcss +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/py.typed +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/widget.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/tests/__init__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/tests/test_diff.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/tests/test_editor.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/.git +0 -0
- {jvim-0.7.0/.worktrees/codex-fix-fold → jvim-0.7.2/.worktrees/codex-fix-subst}/.gitignore +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/CLAUDE.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/README.kr.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/README.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/docs/jvim.svg +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/docs/jvimdiff.svg +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/pyproject.toml +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/__init__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/__main__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/data/help.json +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/data/sample.json +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/diff.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/differ.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/differ.tcss +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/editor.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/editor.tcss +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/py.typed +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/widget.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/tests/__init__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/tests/test_diff.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/tests/test_editor.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/CLAUDE.md +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/docs/jvim.svg +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/docs/jvimdiff.svg +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/scripts/benchmark.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/scripts/screenshots.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/__main__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/jsonpath.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/data/sample.json +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/diff.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/__init__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/insert.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/py.typed +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/styles/differ.tcss +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/src/jvim/styles/editor.tcss +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/tests/__init__.py +0 -0
- {jvim-0.7.0 → jvim-0.7.2}/tests/test_diff.py +0 -0
jvim-0.7.2/.swp
ADDED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jvim
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: JSON editor with vim-style keybindings
|
|
5
5
|
Project-URL: Homepage, https://github.com/k2hyun/jvim
|
|
6
6
|
Project-URL: Repository, https://github.com/k2hyun/jvim
|
|
@@ -47,6 +47,7 @@ JSON editor with vim-style keybindings, built with [Textual](https://github.com/
|
|
|
47
47
|
- **Syntax highlighting** - JSON-aware colorization
|
|
48
48
|
- **JSON validation** - Real-time validation with error reporting
|
|
49
49
|
- **JSONPath search** - Search using JSONPath expressions (`$.foo.bar`)
|
|
50
|
+
- **Word search** - Search word under cursor with `*` (forward) and `#` (backward)
|
|
50
51
|
- **JSONL support** - Edit JSON Lines files with smart formatting
|
|
51
52
|
- **Embedded JSON editing** - Edit JSON strings within JSON with nested level support
|
|
52
53
|
- **Visual mode** - Character-wise (`v`) and line-wise (`V`) selection with `d`/`y`/`c` operators
|
|
@@ -340,6 +341,16 @@ Also available as `jvd` shortcut.
|
|
|
340
341
|
|
|
341
342
|
Use `:help` inside jvim to see the full keybinding reference.
|
|
342
343
|
|
|
344
|
+
## Vim Plugin
|
|
345
|
+
|
|
346
|
+
A native Vim plugin with the same JSON editing features (folding, JSONPath, embedded JSON, JSONL) is available as a standalone repository:
|
|
347
|
+
|
|
348
|
+
[vim-jvim](https://github.com/k2hyun/vim-jvim) — install with your favorite plugin manager:
|
|
349
|
+
|
|
350
|
+
```vim
|
|
351
|
+
Plug 'k2hyun/vim-jvim'
|
|
352
|
+
```
|
|
353
|
+
|
|
343
354
|
## License
|
|
344
355
|
|
|
345
356
|
MIT
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- **구문 강조** - JSON 문법에 맞는 색상 표시
|
|
17
17
|
- **JSON 검증** - 실시간 유효성 검사 및 오류 표시
|
|
18
18
|
- **JSONPath 검색** - JSONPath 표현식으로 검색 (`$.foo.bar`)
|
|
19
|
+
- **단어 검색** - 커서 위치 단어를 `*`(정방향) 및 `#`(역방향)으로 검색
|
|
19
20
|
- **JSONL 지원** - JSON Lines 파일을 스마트하게 포맷팅하여 편집
|
|
20
21
|
- **내장 JSON 편집** - JSON 내 문자열로 저장된 JSON을 중첩 레벨까지 편집
|
|
21
22
|
- **Visual 모드** - 문자 단위(`v`) 및 줄 단위(`V`) 선택 후 `d`/`y`/`c` 연산자 지원
|
|
@@ -309,6 +310,16 @@ jvimdiff --jsonl file1.json file2.json
|
|
|
309
310
|
|
|
310
311
|
jvim 내에서 `:help`를 입력하면 전체 키 바인딩을 확인할 수 있습니다.
|
|
311
312
|
|
|
313
|
+
## Vim 플러그인
|
|
314
|
+
|
|
315
|
+
동일한 JSON 편집 기능(폴딩, JSONPath, 내장 JSON, JSONL)을 제공하는 네이티브 Vim 플러그인이 별도 리포지토리로 제공됩니다:
|
|
316
|
+
|
|
317
|
+
[vim-jvim](https://github.com/k2hyun/vim-jvim) — 원하는 플러그인 매니저로 설치:
|
|
318
|
+
|
|
319
|
+
```vim
|
|
320
|
+
Plug 'k2hyun/vim-jvim'
|
|
321
|
+
```
|
|
322
|
+
|
|
312
323
|
## 라이선스
|
|
313
324
|
|
|
314
325
|
MIT
|
|
@@ -18,6 +18,7 @@ JSON editor with vim-style keybindings, built with [Textual](https://github.com/
|
|
|
18
18
|
- **Syntax highlighting** - JSON-aware colorization
|
|
19
19
|
- **JSON validation** - Real-time validation with error reporting
|
|
20
20
|
- **JSONPath search** - Search using JSONPath expressions (`$.foo.bar`)
|
|
21
|
+
- **Word search** - Search word under cursor with `*` (forward) and `#` (backward)
|
|
21
22
|
- **JSONL support** - Edit JSON Lines files with smart formatting
|
|
22
23
|
- **Embedded JSON editing** - Edit JSON strings within JSON with nested level support
|
|
23
24
|
- **Visual mode** - Character-wise (`v`) and line-wise (`V`) selection with `d`/`y`/`c` operators
|
|
@@ -311,6 +312,16 @@ Also available as `jvd` shortcut.
|
|
|
311
312
|
|
|
312
313
|
Use `:help` inside jvim to see the full keybinding reference.
|
|
313
314
|
|
|
315
|
+
## Vim Plugin
|
|
316
|
+
|
|
317
|
+
A native Vim plugin with the same JSON editing features (folding, JSONPath, embedded JSON, JSONL) is available as a standalone repository:
|
|
318
|
+
|
|
319
|
+
[vim-jvim](https://github.com/k2hyun/vim-jvim) — install with your favorite plugin manager:
|
|
320
|
+
|
|
321
|
+
```vim
|
|
322
|
+
Plug 'k2hyun/vim-jvim'
|
|
323
|
+
```
|
|
324
|
+
|
|
314
325
|
## License
|
|
315
326
|
|
|
316
327
|
MIT
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# jvim / jvimdiff — Vim Compatibility Report
|
|
2
|
+
|
|
3
|
+
> Generated: 2026-02-25 | Based on: v0.7.0 (commit 22eff5a)
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Category | Implemented | Key Missing | Sync Rate |
|
|
8
|
+
|----------|-------------|-------------|-----------|
|
|
9
|
+
| **Movement** | 16 | `e`, `f/t/F/T`, `H/M/L`, `W/B/E`, `{/}` | ~60% |
|
|
10
|
+
| **Search** | 6 | `g*`, `g#` | ~85% |
|
|
11
|
+
| **Editing (Normal)** | 18 | `X`, `s/S/C/D`, `R`, `~`, `>>/<<`, generic motion | ~50% |
|
|
12
|
+
| **Visual** | 5 | `Ctrl+v`, `>/<`, `~` | ~55% |
|
|
13
|
+
| **Folding** | 5 | `zj/zk`, `zO/zC` | ~55% |
|
|
14
|
+
| **Insert** | 5 | `Ctrl+w/u/o` | ~60% |
|
|
15
|
+
| **Command** | 14 | `:set`, `:noh`, `:!`, `:reg` | ~60% |
|
|
16
|
+
| **Diff** | 2 | `do/dp` | ~40% |
|
|
17
|
+
| **Overall** | | | **~55%** |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Normal Mode — Movement
|
|
22
|
+
|
|
23
|
+
| Vim Key | Description | jvim | Notes |
|
|
24
|
+
|---------|-------------|:----:|-------|
|
|
25
|
+
| `h j k l` | left/down/up/right | ✅ | count support |
|
|
26
|
+
| `w` | word forward | ✅ | count support |
|
|
27
|
+
| `b` | word backward | ✅ | count support |
|
|
28
|
+
| `e` | word end | ❌ | reserved as pending starter (`ej`) |
|
|
29
|
+
| `W B E` | WORD movement | ❌ | |
|
|
30
|
+
| `0` | line start | ✅ | |
|
|
31
|
+
| `$` | line end | ✅ | |
|
|
32
|
+
| `^` | first non-whitespace | ✅ | |
|
|
33
|
+
| `gg` | file start | ✅ | count support |
|
|
34
|
+
| `G` | file end / line N | ✅ | count support |
|
|
35
|
+
| `%` | matching bracket jump | ✅ | |
|
|
36
|
+
| `f t F T` | inline char search | ❌ | |
|
|
37
|
+
| `;` `,` | repeat/reverse f/t | ❌ | |
|
|
38
|
+
| `{` `}` | paragraph movement | ❌ | |
|
|
39
|
+
| `(` `)` | sentence movement | ❌ | |
|
|
40
|
+
| `H M L` | screen top/mid/bottom | ❌ | |
|
|
41
|
+
| `Ctrl+f / Ctrl+b` | page down/up | ✅ | fold-aware |
|
|
42
|
+
| `Ctrl+d / Ctrl+u` | half page down/up | ✅ | fold-aware |
|
|
43
|
+
| `Ctrl+e / Ctrl+y` | scroll 1 line | ✅ | fold-aware |
|
|
44
|
+
| `Ctrl+g` | file info | ✅ | |
|
|
45
|
+
| `zz` `zt` `zb` | cursor line to center/top/bottom | ❌ | |
|
|
46
|
+
|
|
47
|
+
## Normal Mode — Search
|
|
48
|
+
|
|
49
|
+
| Vim Key | Description | jvim | Notes |
|
|
50
|
+
|---------|-------------|:----:|-------|
|
|
51
|
+
| `/` | forward search | ✅ | regex, smartcase |
|
|
52
|
+
| `?` | backward search | ✅ | |
|
|
53
|
+
| `n` | next match | ✅ | |
|
|
54
|
+
| `N` | previous match | ✅ | |
|
|
55
|
+
| `*` | word forward search | ✅ | |
|
|
56
|
+
| `#` | word backward search | ✅ | |
|
|
57
|
+
| `g*` `g#` | partial word search | ❌ | |
|
|
58
|
+
|
|
59
|
+
## Normal Mode — Editing
|
|
60
|
+
|
|
61
|
+
| Vim Key | Description | jvim | Notes |
|
|
62
|
+
|---------|-------------|:----:|-------|
|
|
63
|
+
| `i I a A` | insert entry | ✅ | |
|
|
64
|
+
| `o O` | open line + insert | ✅ | fold-aware |
|
|
65
|
+
| `x` | delete char | ✅ | count support |
|
|
66
|
+
| `X` | delete char before cursor | ❌ | |
|
|
67
|
+
| `r` | replace char | ✅ | |
|
|
68
|
+
| `R` | REPLACE mode | ❌ | |
|
|
69
|
+
| `s` | delete char + insert | ❌ | |
|
|
70
|
+
| `S` | delete line + insert (= `cc`) | ❌ | |
|
|
71
|
+
| `C` | change to EOL (= `c$`) | ❌ | |
|
|
72
|
+
| `D` | delete to EOL (= `d$`) | ❌ | |
|
|
73
|
+
| `dd` | delete line | ✅ | count, fold-aware |
|
|
74
|
+
| `dw` | delete word | ✅ | |
|
|
75
|
+
| `d$` | delete to EOL | ✅ | |
|
|
76
|
+
| `d0` | delete to BOL | ✅ | |
|
|
77
|
+
| `d` + motion | generic delete motion | ❌ | only `dd/dw/d$/d0` |
|
|
78
|
+
| `cc` | change line | ✅ | |
|
|
79
|
+
| `cw` | change word | ✅ | |
|
|
80
|
+
| `c` + motion | generic change motion | ❌ | only `cc/cw` |
|
|
81
|
+
| `yy` | yank line | ✅ | count, fold-aware |
|
|
82
|
+
| `yw` `y$` etc. | generic yank motion | ❌ | only `yy` |
|
|
83
|
+
| `p` | paste after | ✅ | count support |
|
|
84
|
+
| `P` | paste before | ✅ | count support |
|
|
85
|
+
| `J` | join lines | ✅ | count, fold-aware |
|
|
86
|
+
| `u` | undo | ✅ | |
|
|
87
|
+
| `Ctrl+r` | redo | ✅ | |
|
|
88
|
+
| `.` | repeat last edit | ✅ | |
|
|
89
|
+
| `~` | toggle case | ❌ | |
|
|
90
|
+
| `>>` `<<` | indent/dedent | ❌ | |
|
|
91
|
+
| `gq` `gw` | text formatting | ❌ | |
|
|
92
|
+
|
|
93
|
+
## Visual Mode
|
|
94
|
+
|
|
95
|
+
| Vim Key | Description | jvim | Notes |
|
|
96
|
+
|---------|-------------|:----:|-------|
|
|
97
|
+
| `v` | character-wise select | ✅ | |
|
|
98
|
+
| `V` | line-wise select | ✅ | |
|
|
99
|
+
| `Ctrl+v` | block select | ❌ | |
|
|
100
|
+
| `d` | delete selection | ✅ | |
|
|
101
|
+
| `y` | yank selection | ✅ | |
|
|
102
|
+
| `c` | change selection | ✅ | |
|
|
103
|
+
| `>` `<` | indent/dedent selection | ❌ | |
|
|
104
|
+
| `~` `U` `u` | case transform | ❌ | |
|
|
105
|
+
|
|
106
|
+
## Folding
|
|
107
|
+
|
|
108
|
+
| Vim Key | Description | jvim | Notes |
|
|
109
|
+
|---------|-------------|:----:|-------|
|
|
110
|
+
| `za` | toggle fold | ✅ | |
|
|
111
|
+
| `zo` | open fold | ✅ | |
|
|
112
|
+
| `zc` | close fold | ✅ | |
|
|
113
|
+
| `zM` | fold all | ✅ | |
|
|
114
|
+
| `zR` | unfold all | ✅ | |
|
|
115
|
+
| `zj` `zk` | next/prev fold | ❌ | |
|
|
116
|
+
| `zO` `zC` | recursive open/close | ❌ | |
|
|
117
|
+
|
|
118
|
+
## Insert Mode
|
|
119
|
+
|
|
120
|
+
| Vim Key | Description | jvim | Notes |
|
|
121
|
+
|---------|-------------|:----:|-------|
|
|
122
|
+
| `Esc` | return to Normal | ✅ | |
|
|
123
|
+
| `Backspace` | delete char | ✅ | line merge support |
|
|
124
|
+
| `Enter` | newline | ✅ | smart indent |
|
|
125
|
+
| `Tab` | indent (4 spaces) | ✅ | |
|
|
126
|
+
| Arrow keys | movement | ✅ | |
|
|
127
|
+
| `Ctrl+w` | delete previous word | ❌ | |
|
|
128
|
+
| `Ctrl+u` | delete to line start | ❌ | |
|
|
129
|
+
| `Ctrl+o` | one-shot Normal cmd | ❌ | |
|
|
130
|
+
|
|
131
|
+
## Command Mode (`:`)
|
|
132
|
+
|
|
133
|
+
| Vim Command | Description | jvim | Notes |
|
|
134
|
+
|-------------|-------------|:----:|-------|
|
|
135
|
+
| `:w` `:w!` | save | ✅ | `!` skips validation |
|
|
136
|
+
| `:w {file}` | save as | ✅ | |
|
|
137
|
+
| `:e {file}` | open file | ✅ | Tab completion |
|
|
138
|
+
| `:e#` | alternate file | ✅ | |
|
|
139
|
+
| `:q` `:q!` | quit | ✅ | |
|
|
140
|
+
| `:wq` `:x` | save + quit | ✅ | |
|
|
141
|
+
| `:{N}` | go to line N | ✅ | |
|
|
142
|
+
| `:$` | last line | ✅ | |
|
|
143
|
+
| `:n` `:N` | next/prev file | ✅ | |
|
|
144
|
+
| `:s/old/new/g` | substitute | ✅ | range, regex, JSONPath |
|
|
145
|
+
| `:%s/old/new/g` | global substitute | ✅ | |
|
|
146
|
+
| `:help` | help | ✅ | |
|
|
147
|
+
| `:fmt` | format JSON | ✅ | jvim-specific |
|
|
148
|
+
| `:set` | set options | ❌ | |
|
|
149
|
+
| `:noh` | clear search highlight | ❌ | |
|
|
150
|
+
| `:reg` | show registers | ❌ | |
|
|
151
|
+
| `:!cmd` | external command | ❌ | |
|
|
152
|
+
| `:r {file}` | insert file content | ❌ | |
|
|
153
|
+
| `:buffers` `:ls` | buffer list | ❌ | |
|
|
154
|
+
|
|
155
|
+
## Diff (jvimdiff only)
|
|
156
|
+
|
|
157
|
+
| Vim Key | Description | jvimdiff | Notes |
|
|
158
|
+
|---------|-------------|:--------:|-------|
|
|
159
|
+
| `]c` | next hunk | ✅ | |
|
|
160
|
+
| `[c` | prev hunk | ✅ | |
|
|
161
|
+
| `do` | diff obtain | ❌ | |
|
|
162
|
+
| `dp` | diff put | ❌ | |
|
|
163
|
+
| `:diffget` `:diffput` | diff transfer | ❌ | |
|
|
164
|
+
| `Tab` | switch panel | ✅ | jvim-specific |
|
|
165
|
+
| `:ig` `:uig` | ignore path | ✅ | jvim-specific |
|
|
166
|
+
|
|
167
|
+
## jvim-specific Features (not in Vim)
|
|
168
|
+
|
|
169
|
+
| Key/Command | Description |
|
|
170
|
+
|-------------|-------------|
|
|
171
|
+
| `ej` | edit embedded JSON string |
|
|
172
|
+
| `/$. /$[` | JSONPath search |
|
|
173
|
+
| `\j` suffix | force JSONPath mode |
|
|
174
|
+
| `:s/$.key/new/g` | JSONPath-based substitution |
|
|
175
|
+
| `:fmt` | JSON formatting |
|
|
176
|
+
| `:ig` `:uig` | diff path ignore |
|
|
177
|
+
| `:l{N}` `:p{N}` | editor line / record jump |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Priority Improvements
|
|
182
|
+
|
|
183
|
+
Most impactful missing features, sorted by user benefit:
|
|
184
|
+
|
|
185
|
+
1. **`f` / `t` / `F` / `T`** — inline char search (very high Vim muscle-memory)
|
|
186
|
+
2. **`C` / `D` / `S`** — `c$`, `d$`, `cc` shortcuts (extremely common edits)
|
|
187
|
+
3. **`e`** — word end motion (conflicts with `ej` pending)
|
|
188
|
+
4. **`>>` / `<<`** — indent/dedent manipulation
|
|
189
|
+
5. **`do` / `dp`** — diff obtain/put for jvimdiff
|
|
190
|
+
6. **`:noh`** — clear search highlight
|
|
191
|
+
7. **`H` / `M` / `L`** — screen-relative cursor movement
|
|
192
|
+
8. **`X` / `s`** — minor but frequently used edits
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"""Action mixins and utilities for JsonEditor."""
|
|
2
2
|
|
|
3
3
|
from jvim.action.clipboard import ClipboardMixin
|
|
4
|
+
from jvim.action.content import ContentMixin
|
|
4
5
|
from jvim.action.folding import FoldMixin
|
|
5
6
|
from jvim.action.navigation import NavigationMixin
|
|
7
|
+
from jvim.action.render import RenderMixin
|
|
6
8
|
from jvim.action.substitute import SubstituteMixin
|
|
9
|
+
from jvim.action.undo import UndoMixin
|
|
7
10
|
from jvim.action.visual import VisualMixin
|
|
8
11
|
|
|
9
12
|
__all__ = [
|
|
10
13
|
"ClipboardMixin",
|
|
14
|
+
"ContentMixin",
|
|
11
15
|
"FoldMixin",
|
|
12
16
|
"NavigationMixin",
|
|
17
|
+
"RenderMixin",
|
|
13
18
|
"SubstituteMixin",
|
|
19
|
+
"UndoMixin",
|
|
14
20
|
"VisualMixin",
|
|
15
21
|
]
|
|
@@ -47,7 +47,7 @@ class ClipboardMixin:
|
|
|
47
47
|
return
|
|
48
48
|
# fold-aware: fold 헤더이면 fold end 뒤에 삽입
|
|
49
49
|
insert_after = self.cursor_row
|
|
50
|
-
fold_end = self.
|
|
50
|
+
fold_end = self._get_fold_end(self.cursor_row)
|
|
51
51
|
if fold_end is not None:
|
|
52
52
|
insert_after = fold_end
|
|
53
53
|
inserted = len(self.yank_buffer)
|
|
@@ -94,7 +94,7 @@ class ClipboardMixin:
|
|
|
94
94
|
if self.cursor_row >= len(self.lines) - 1:
|
|
95
95
|
return
|
|
96
96
|
# fold-aware: fold 헤더이면 fold 전체를 제거한 뒤 다음 보이는 줄과 join
|
|
97
|
-
fold_end = self.
|
|
97
|
+
fold_end = self._get_fold_end(self.cursor_row)
|
|
98
98
|
if fold_end is not None:
|
|
99
99
|
next_row = fold_end + 1
|
|
100
100
|
if next_row >= len(self.lines):
|
|
@@ -104,8 +104,7 @@ class ClipboardMixin:
|
|
|
104
104
|
# fold 내부 행 제거 (cursor_row+1 ~ fold_end)
|
|
105
105
|
del self.lines[self.cursor_row + 1 : next_row]
|
|
106
106
|
removed = fold_end - self.cursor_row
|
|
107
|
-
|
|
108
|
-
self._folded_lines_dirty = True
|
|
107
|
+
self._open_fold(self.cursor_row)
|
|
109
108
|
self._adjust_line_indices(self.cursor_row + 1, -removed)
|
|
110
109
|
# 이제 cursor_row+1 = 원래 next_row (fold 뒤 첫 줄)
|
|
111
110
|
nxt = self.lines[self.cursor_row + 1].lstrip()
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Content management mixin for JsonEditor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContentMixin:
|
|
9
|
+
"""Content get/set, validation, formatting, and embedded JSON methods for JsonEditor."""
|
|
10
|
+
|
|
11
|
+
# -- Public API --------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
def get_content(self) -> str:
|
|
14
|
+
return "\n".join(self.lines)
|
|
15
|
+
|
|
16
|
+
def _get_parseable_content(self) -> str:
|
|
17
|
+
"""JSON 파싱용 콘텐츠 반환. DiffEditor에서 filler 행 제외하도록 오버라이드."""
|
|
18
|
+
return self.get_content()
|
|
19
|
+
|
|
20
|
+
def set_content(self, content: str) -> None:
|
|
21
|
+
if self.jsonl and content:
|
|
22
|
+
content = self._jsonl_to_pretty(content)
|
|
23
|
+
self.lines = content.split("\n") if content else [""]
|
|
24
|
+
self.cursor_row = 0
|
|
25
|
+
self.cursor_col = 0
|
|
26
|
+
self._reset_fold_state()
|
|
27
|
+
# 초기 로드 시 긴 문자열 자동 접기
|
|
28
|
+
for i in range(len(self.lines)):
|
|
29
|
+
if self._find_long_string_at(i):
|
|
30
|
+
self._collapsed_strings.add(i)
|
|
31
|
+
self._invalidate_caches()
|
|
32
|
+
self.refresh()
|
|
33
|
+
|
|
34
|
+
def get_history(self) -> dict:
|
|
35
|
+
"""Get search and command history for persistence."""
|
|
36
|
+
return {
|
|
37
|
+
"search": self._search_history[:],
|
|
38
|
+
"command": self._command_history[:],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def set_history(self, history: dict) -> None:
|
|
42
|
+
"""Restore search and command history."""
|
|
43
|
+
if "search" in history:
|
|
44
|
+
self._search_history = history["search"][: self._search_history_max]
|
|
45
|
+
if "command" in history:
|
|
46
|
+
self._command_history = history["command"][: self._command_history_max]
|
|
47
|
+
|
|
48
|
+
# -- JSON operations ---------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def _check_content(self, content: str) -> tuple[bool, str]:
|
|
51
|
+
"""Validate content as JSON or JSONL. Returns (valid, error_msg)."""
|
|
52
|
+
if self.jsonl:
|
|
53
|
+
blocks = self._split_jsonl_blocks(content)
|
|
54
|
+
for i, block in enumerate(blocks, 1):
|
|
55
|
+
try:
|
|
56
|
+
json.loads(block)
|
|
57
|
+
except json.JSONDecodeError as e:
|
|
58
|
+
return False, f"JSONL error: record {i}: {e.msg}"
|
|
59
|
+
return True, ""
|
|
60
|
+
try:
|
|
61
|
+
json.loads(content)
|
|
62
|
+
return True, ""
|
|
63
|
+
except json.JSONDecodeError as e:
|
|
64
|
+
return False, f"JSON error: {e.msg} (line {e.lineno})"
|
|
65
|
+
|
|
66
|
+
def _validate_json(self) -> bool:
|
|
67
|
+
content = self.get_content()
|
|
68
|
+
valid, err = self._check_content(content)
|
|
69
|
+
if valid:
|
|
70
|
+
label = "JSONL" if self.jsonl else "JSON"
|
|
71
|
+
self.status_msg = f"{label} valid"
|
|
72
|
+
self.post_message(self.JsonValidated(content=content, valid=True))
|
|
73
|
+
return True
|
|
74
|
+
self.status_msg = err
|
|
75
|
+
self.post_message(self.JsonValidated(content=content, valid=False, error=err))
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _format_json(self) -> None:
|
|
79
|
+
if self.jsonl:
|
|
80
|
+
self._format_jsonl()
|
|
81
|
+
return
|
|
82
|
+
content = self.get_content()
|
|
83
|
+
try:
|
|
84
|
+
parsed = json.loads(content)
|
|
85
|
+
formatted = json.dumps(parsed, indent=4, ensure_ascii=False)
|
|
86
|
+
self._save_undo()
|
|
87
|
+
self.lines = formatted.split("\n")
|
|
88
|
+
self.cursor_row = 0
|
|
89
|
+
self.cursor_col = 0
|
|
90
|
+
self._reset_fold_state()
|
|
91
|
+
self.status_msg = "formatted"
|
|
92
|
+
except json.JSONDecodeError as e:
|
|
93
|
+
self.status_msg = f"cannot format: {e.msg} (line {e.lineno})"
|
|
94
|
+
|
|
95
|
+
def _format_jsonl(self) -> None:
|
|
96
|
+
content = self.get_content()
|
|
97
|
+
blocks = self._split_jsonl_blocks(content)
|
|
98
|
+
formatted: list[str] = []
|
|
99
|
+
for i, block in enumerate(blocks):
|
|
100
|
+
try:
|
|
101
|
+
parsed = json.loads(block)
|
|
102
|
+
formatted.append(json.dumps(parsed, indent=4, ensure_ascii=False))
|
|
103
|
+
except json.JSONDecodeError as e:
|
|
104
|
+
self.status_msg = f"cannot format: record {i + 1}: {e.msg}"
|
|
105
|
+
return
|
|
106
|
+
self._save_undo()
|
|
107
|
+
self.lines = "\n\n".join(formatted).split("\n")
|
|
108
|
+
self._reset_fold_state()
|
|
109
|
+
self.cursor_row = 0
|
|
110
|
+
self.cursor_col = 0
|
|
111
|
+
self.status_msg = "formatted"
|
|
112
|
+
|
|
113
|
+
def _find_string_at_cursor(self) -> tuple[int, int, str] | None:
|
|
114
|
+
"""Find a string value on the current line.
|
|
115
|
+
|
|
116
|
+
Returns (col_start, col_end, string_content) or None if no string value found.
|
|
117
|
+
col_start and col_end include the quotes.
|
|
118
|
+
"""
|
|
119
|
+
line = self.lines[self.cursor_row]
|
|
120
|
+
|
|
121
|
+
# Parse all strings on this line with their positions
|
|
122
|
+
strings: list[tuple[int, int, str]] = [] # (start, end, content)
|
|
123
|
+
i = 0
|
|
124
|
+
while i < len(line):
|
|
125
|
+
if line[i] == '"':
|
|
126
|
+
start = i
|
|
127
|
+
i += 1
|
|
128
|
+
while i < len(line):
|
|
129
|
+
if line[i] == '"' and line[i - 1] != "\\":
|
|
130
|
+
raw = line[start + 1 : i]
|
|
131
|
+
try:
|
|
132
|
+
content = json.loads(f'"{raw}"')
|
|
133
|
+
strings.append((start, i + 1, content))
|
|
134
|
+
except json.JSONDecodeError:
|
|
135
|
+
pass
|
|
136
|
+
break
|
|
137
|
+
i += 1
|
|
138
|
+
i += 1
|
|
139
|
+
|
|
140
|
+
if not strings:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
# Find string values (strings that follow a ':')
|
|
144
|
+
for start, end, content in strings:
|
|
145
|
+
before = line[:start].rstrip()
|
|
146
|
+
if before.endswith(":"):
|
|
147
|
+
return (start, end, content)
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def _edit_embedded_json(self) -> None:
|
|
152
|
+
"""Handle :ej command to edit embedded JSON string."""
|
|
153
|
+
result = self._find_string_at_cursor()
|
|
154
|
+
if result is None:
|
|
155
|
+
self.status_msg = "cursor not on a string value"
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
col_start, col_end, content = result
|
|
159
|
+
|
|
160
|
+
# Try to parse as JSON
|
|
161
|
+
try:
|
|
162
|
+
parsed = json.loads(content)
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
self.status_msg = "string is not valid JSON"
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Check if it's a list or dict
|
|
168
|
+
if not isinstance(parsed, (list, dict)):
|
|
169
|
+
self.status_msg = "string is not a list or dict"
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Format and send for editing
|
|
173
|
+
formatted = json.dumps(parsed, indent=4, ensure_ascii=False)
|
|
174
|
+
self.post_message(
|
|
175
|
+
self.EmbeddedEditRequested(
|
|
176
|
+
content=formatted,
|
|
177
|
+
source_row=self.cursor_row,
|
|
178
|
+
source_col_start=col_start,
|
|
179
|
+
source_col_end=col_end,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def update_embedded_string(
|
|
184
|
+
self, row: int, col_start: int, col_end: int, new_content: str
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Update a string value with new JSON content."""
|
|
187
|
+
self._save_undo()
|
|
188
|
+
# Escape the new content as a JSON string
|
|
189
|
+
escaped = json.dumps(new_content, ensure_ascii=False)
|
|
190
|
+
line = self.lines[row]
|
|
191
|
+
self.lines[row] = line[:col_start] + escaped + line[col_end:]
|
|
192
|
+
self.refresh()
|
|
193
|
+
|
|
194
|
+
# -- JSONL helpers -----------------------------------------------------
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _jsonl_to_pretty(content: str) -> str:
|
|
198
|
+
"""Convert JSONL (one-json-per-line) to pretty-printed blocks."""
|
|
199
|
+
blocks: list[str] = []
|
|
200
|
+
for line in content.split("\n"):
|
|
201
|
+
stripped = line.strip()
|
|
202
|
+
if not stripped:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
parsed = json.loads(stripped)
|
|
206
|
+
blocks.append(json.dumps(parsed, indent=4, ensure_ascii=False))
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
blocks.append(stripped)
|
|
209
|
+
return "\n\n".join(blocks)
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _split_jsonl_blocks(content: str) -> list[str]:
|
|
213
|
+
"""Split pretty-printed content into blocks separated by blank lines."""
|
|
214
|
+
blocks: list[str] = []
|
|
215
|
+
current: list[str] = []
|
|
216
|
+
for line in content.split("\n"):
|
|
217
|
+
if line.strip():
|
|
218
|
+
current.append(line)
|
|
219
|
+
else:
|
|
220
|
+
if current:
|
|
221
|
+
blocks.append("\n".join(current))
|
|
222
|
+
current = []
|
|
223
|
+
if current:
|
|
224
|
+
blocks.append("\n".join(current))
|
|
225
|
+
return blocks
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _pretty_to_jsonl(content: str) -> str:
|
|
229
|
+
"""Convert pretty-printed blocks back to JSONL (one-json-per-line)."""
|
|
230
|
+
blocks = ContentMixin._split_jsonl_blocks(content)
|
|
231
|
+
lines: list[str] = []
|
|
232
|
+
for block in blocks:
|
|
233
|
+
try:
|
|
234
|
+
parsed = json.loads(block)
|
|
235
|
+
lines.append(json.dumps(parsed, ensure_ascii=False))
|
|
236
|
+
except json.JSONDecodeError:
|
|
237
|
+
lines.append(" ".join(block.split()))
|
|
238
|
+
return "\n".join(lines)
|
|
@@ -6,6 +6,23 @@ from __future__ import annotations
|
|
|
6
6
|
class FoldMixin:
|
|
7
7
|
"""Fold and collapse related methods for JsonEditor."""
|
|
8
8
|
|
|
9
|
+
def _get_fold_end(self, line_idx: int) -> int | None:
|
|
10
|
+
"""Return fold end line if *line_idx* is a fold header."""
|
|
11
|
+
return self._folds.get(line_idx)
|
|
12
|
+
|
|
13
|
+
def _has_fold_header(self, line_idx: int) -> bool:
|
|
14
|
+
"""Return True when *line_idx* is a fold header."""
|
|
15
|
+
return line_idx in self._folds
|
|
16
|
+
|
|
17
|
+
def _reset_fold_state(self, clear_collapsed: bool = True) -> None:
|
|
18
|
+
"""Clear fold bookkeeping after structural content changes."""
|
|
19
|
+
self._folds.clear()
|
|
20
|
+
self._folded_lines.clear()
|
|
21
|
+
self._folded_lines_dirty = False
|
|
22
|
+
self._folded_lines_folds_len = 0
|
|
23
|
+
if clear_collapsed:
|
|
24
|
+
self._collapsed_strings.clear()
|
|
25
|
+
|
|
9
26
|
def _adjust_line_indices(self, from_line: int, delta: int) -> None:
|
|
10
27
|
"""라인 삽입(delta>0)/삭제(delta<0) 후 fold/collapse 인덱스 조정."""
|
|
11
28
|
if delta == 0:
|