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.
Files changed (90) hide show
  1. jvim-0.7.2/.claude/settings.local.json +13 -0
  2. {jvim-0.7.0/.worktrees/codex-fix-subst → jvim-0.7.2}/.gitignore +1 -0
  3. jvim-0.7.2/.swp +0 -0
  4. {jvim-0.7.0 → jvim-0.7.2}/PKG-INFO +12 -1
  5. {jvim-0.7.0 → jvim-0.7.2}/README.kr.md +11 -0
  6. {jvim-0.7.0 → jvim-0.7.2}/README.md +11 -0
  7. jvim-0.7.2/docs/vim-compatibility-report.md +192 -0
  8. {jvim-0.7.0 → jvim-0.7.2}/pyproject.toml +1 -1
  9. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/__init__.py +1 -1
  10. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/__init__.py +6 -0
  11. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/clipboard.py +3 -4
  12. jvim-0.7.2/src/jvim/action/content.py +238 -0
  13. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/folding.py +17 -0
  14. jvim-0.7.2/src/jvim/action/jsonpath_locator.py +216 -0
  15. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/navigation.py +14 -0
  16. jvim-0.7.2/src/jvim/action/render.py +735 -0
  17. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/substitute.py +45 -23
  18. jvim-0.7.2/src/jvim/action/undo.py +72 -0
  19. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/visual.py +1 -5
  20. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/data/help.json +1 -0
  21. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/differ.py +24 -13
  22. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/editor.py +1 -0
  23. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/command.py +129 -86
  24. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/normal.py +198 -139
  25. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/search.py +15 -204
  26. jvim-0.7.2/src/jvim/widget.py +366 -0
  27. {jvim-0.7.0 → jvim-0.7.2}/tests/test_editor.py +113 -0
  28. jvim-0.7.2/tests/test_jsonpath_locator.py +52 -0
  29. {jvim-0.7.0 → jvim-0.7.2}/uv.lock +1 -1
  30. jvim-0.7.0/.claude/CLAUDE.md +0 -7
  31. jvim-0.7.0/.claude/settings.local.json +0 -40
  32. jvim-0.7.0/src/jvim/widget.py +0 -1276
  33. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/.git +0 -0
  34. {jvim-0.7.0 → jvim-0.7.2/.worktrees/codex-fix-fold}/.gitignore +0 -0
  35. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/CLAUDE.md +0 -0
  36. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/README.kr.md +0 -0
  37. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/README.md +0 -0
  38. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/pyproject.toml +0 -0
  39. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/__init__.py +0 -0
  40. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/__main__.py +0 -0
  41. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/data/help.json +0 -0
  42. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/data/sample.json +0 -0
  43. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/diff.py +0 -0
  44. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/differ.py +0 -0
  45. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/differ.tcss +0 -0
  46. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/editor.py +0 -0
  47. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/editor.tcss +0 -0
  48. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/py.typed +0 -0
  49. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/src/jvim/widget.py +0 -0
  50. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/tests/__init__.py +0 -0
  51. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/tests/test_diff.py +0 -0
  52. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-fold/tests/test_editor.py +0 -0
  53. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/.git +0 -0
  54. {jvim-0.7.0/.worktrees/codex-fix-fold → jvim-0.7.2/.worktrees/codex-fix-subst}/.gitignore +0 -0
  55. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/CLAUDE.md +0 -0
  56. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/README.kr.md +0 -0
  57. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/README.md +0 -0
  58. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/docs/jvim.svg +0 -0
  59. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/docs/jvimdiff.svg +0 -0
  60. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/pyproject.toml +0 -0
  61. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/__init__.py +0 -0
  62. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/__main__.py +0 -0
  63. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/data/help.json +0 -0
  64. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/data/sample.json +0 -0
  65. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/diff.py +0 -0
  66. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/differ.py +0 -0
  67. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/differ.tcss +0 -0
  68. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/editor.py +0 -0
  69. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/editor.tcss +0 -0
  70. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/py.typed +0 -0
  71. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/src/jvim/widget.py +0 -0
  72. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/tests/__init__.py +0 -0
  73. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/tests/test_diff.py +0 -0
  74. {jvim-0.7.0 → jvim-0.7.2}/.worktrees/codex-fix-subst/tests/test_editor.py +0 -0
  75. {jvim-0.7.0 → jvim-0.7.2}/CLAUDE.md +0 -0
  76. {jvim-0.7.0 → jvim-0.7.2}/docs/jvim.svg +0 -0
  77. {jvim-0.7.0 → jvim-0.7.2}/docs/jvimdiff.svg +0 -0
  78. {jvim-0.7.0 → jvim-0.7.2}/scripts/benchmark.py +0 -0
  79. {jvim-0.7.0 → jvim-0.7.2}/scripts/screenshots.py +0 -0
  80. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/__main__.py +0 -0
  81. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/action/jsonpath.py +0 -0
  82. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/data/sample.json +0 -0
  83. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/diff.py +0 -0
  84. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/__init__.py +0 -0
  85. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/mode/insert.py +0 -0
  86. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/py.typed +0 -0
  87. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/styles/differ.tcss +0 -0
  88. {jvim-0.7.0 → jvim-0.7.2}/src/jvim/styles/editor.tcss +0 -0
  89. {jvim-0.7.0 → jvim-0.7.2}/tests/__init__.py +0 -0
  90. {jvim-0.7.0 → jvim-0.7.2}/tests/test_diff.py +0 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:news.ycombinator.com)",
5
+ "WebFetch(domain:github.com)",
6
+ "Bash(python scripts/benchmark.py:*)",
7
+ "Bash(git:*)",
8
+ "Bash(gh repo:*)",
9
+ "Bash(gh api:*)",
10
+ "Bash(python -m build)"
11
+ ]
12
+ }
13
+ }
@@ -10,3 +10,4 @@ build/
10
10
  .pytest_cache/
11
11
  .mypy_cache/
12
12
  .ruff_cache/
13
+ CLAUDE.local.md
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.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jvim"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "JSON editor with vim-style keybindings"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,4 +3,4 @@
3
3
  from .widget import EditorMode, JsonEditor
4
4
 
5
5
  __all__ = ["EditorMode", "JsonEditor"]
6
- __version__ = "0.7.0"
6
+ __version__ = "0.7.2"
@@ -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._folds.get(self.cursor_row)
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._folds.get(self.cursor_row)
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
- del self._folds[self.cursor_row]
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: