jvim 0.5.0__tar.gz → 0.7.0__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 (81) hide show
  1. {jvim-0.5.0 → jvim-0.7.0}/.claude/CLAUDE.md +1 -0
  2. {jvim-0.5.0 → jvim-0.7.0}/.claude/settings.local.json +4 -1
  3. {jvim-0.5.0 → jvim-0.7.0}/PKG-INFO +5 -1
  4. {jvim-0.5.0 → jvim-0.7.0}/README.kr.md +4 -0
  5. {jvim-0.5.0 → jvim-0.7.0}/README.md +4 -0
  6. {jvim-0.5.0 → jvim-0.7.0}/pyproject.toml +1 -1
  7. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/__init__.py +1 -1
  8. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/clipboard.py +38 -11
  9. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/folding.py +31 -5
  10. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/substitute.py +3 -0
  11. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/visual.py +3 -0
  12. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/data/help.json +20 -0
  13. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/differ.py +486 -23
  14. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/mode/command.py +58 -0
  15. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/mode/normal.py +194 -43
  16. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/mode/search.py +133 -33
  17. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/styles/differ.tcss +6 -1
  18. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/widget.py +181 -39
  19. {jvim-0.5.0 → jvim-0.7.0}/tests/test_diff.py +565 -1
  20. {jvim-0.5.0 → jvim-0.7.0}/tests/test_editor.py +490 -0
  21. jvim-0.7.0/uv.lock +1170 -0
  22. {jvim-0.5.0 → jvim-0.7.0}/.gitignore +0 -0
  23. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/.git +0 -0
  24. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/.gitignore +0 -0
  25. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/CLAUDE.md +0 -0
  26. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/README.kr.md +0 -0
  27. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/README.md +0 -0
  28. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/pyproject.toml +0 -0
  29. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/__init__.py +0 -0
  30. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/__main__.py +0 -0
  31. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/data/help.json +0 -0
  32. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/data/sample.json +0 -0
  33. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/diff.py +0 -0
  34. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/differ.py +0 -0
  35. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/differ.tcss +0 -0
  36. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/editor.py +0 -0
  37. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/editor.tcss +0 -0
  38. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/py.typed +0 -0
  39. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/src/jvim/widget.py +0 -0
  40. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/tests/__init__.py +0 -0
  41. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/tests/test_diff.py +0 -0
  42. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-fold/tests/test_editor.py +0 -0
  43. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/.git +0 -0
  44. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/.gitignore +0 -0
  45. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/CLAUDE.md +0 -0
  46. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/README.kr.md +0 -0
  47. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/README.md +0 -0
  48. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/docs/jvim.svg +0 -0
  49. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/docs/jvimdiff.svg +0 -0
  50. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/pyproject.toml +0 -0
  51. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/__init__.py +0 -0
  52. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/__main__.py +0 -0
  53. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/data/help.json +0 -0
  54. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/data/sample.json +0 -0
  55. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/diff.py +0 -0
  56. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/differ.py +0 -0
  57. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/differ.tcss +0 -0
  58. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/editor.py +0 -0
  59. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/editor.tcss +0 -0
  60. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/py.typed +0 -0
  61. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/src/jvim/widget.py +0 -0
  62. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/tests/__init__.py +0 -0
  63. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/tests/test_diff.py +0 -0
  64. {jvim-0.5.0 → jvim-0.7.0}/.worktrees/codex-fix-subst/tests/test_editor.py +0 -0
  65. {jvim-0.5.0 → jvim-0.7.0}/CLAUDE.md +0 -0
  66. {jvim-0.5.0 → jvim-0.7.0}/docs/jvim.svg +0 -0
  67. {jvim-0.5.0 → jvim-0.7.0}/docs/jvimdiff.svg +0 -0
  68. {jvim-0.5.0 → jvim-0.7.0}/scripts/benchmark.py +0 -0
  69. {jvim-0.5.0 → jvim-0.7.0}/scripts/screenshots.py +0 -0
  70. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/__main__.py +0 -0
  71. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/__init__.py +0 -0
  72. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/jsonpath.py +0 -0
  73. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/action/navigation.py +0 -0
  74. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/data/sample.json +0 -0
  75. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/diff.py +0 -0
  76. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/editor.py +0 -0
  77. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/mode/__init__.py +0 -0
  78. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/mode/insert.py +0 -0
  79. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/py.typed +0 -0
  80. {jvim-0.5.0 → jvim-0.7.0}/src/jvim/styles/editor.tcss +0 -0
  81. {jvim-0.5.0 → jvim-0.7.0}/tests/__init__.py +0 -0
@@ -1,6 +1,7 @@
1
1
 
2
2
  키 바인딩이 추가되면 help.json 파일을 업데이트한다.
3
3
  푸시할 때 버전의 업그레이드 여부를 분석해서 보고한다.
4
+ 푸시할 때 변경 사항에 대해 위키를 업데이트한다.
4
5
  버전이 업그레이드되는 경우 README.md 파일에 추가할 만한 항목을 분석한다.
5
6
  README.md 파일이 수정되면 README.kr.md 파일도 동일한 내용으로 수정한다.
6
7
  jvim에 적용되는 기능 중에서 jvimdiff에도 적용 가능한 기능은 적용 여부를 확인한다.
@@ -31,7 +31,10 @@
31
31
  "Bash(.venv/bin/pytest:*)",
32
32
  "Bash(.venv/bin/ruff check:*)",
33
33
  "Bash(wc:*)",
34
- "Bash(ls:*)"
34
+ "Bash(ls:*)",
35
+ "Bash(uv run ruff check:*)",
36
+ "Bash(uv run ruff format:*)",
37
+ "Bash(uv run python3:*)"
35
38
  ]
36
39
  }
37
40
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jvim
3
- Version: 0.5.0
3
+ Version: 0.7.0
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
@@ -50,6 +50,7 @@ JSON editor with vim-style keybindings, built with [Textual](https://github.com/
50
50
  - **JSONL support** - Edit JSON Lines files with smart formatting
51
51
  - **Embedded JSON editing** - Edit JSON strings within JSON with nested level support
52
52
  - **Visual mode** - Character-wise (`v`) and line-wise (`V`) selection with `d`/`y`/`c` operators
53
+ - **Count prefix** - Vim-style numeric prefixes (`5j`, `3dd`, `d3d`) with fold-aware line operations
53
54
  - **Folding** - Collapse/expand JSON blocks and long string values
54
55
  - **Bracket matching** - Jump to matching brackets with `%`
55
56
  - **Undo/Redo** - Full undo history
@@ -330,6 +331,9 @@ Also available as `jvd` shortcut.
330
331
  - **Auto-fold** - On load, all structure is folded, then only diff sections are unfolded
331
332
  - **Hunk navigation** - `]c` next hunk, `[c` previous hunk (wraps around)
332
333
  - **Panel switch** - `Tab` to toggle focus between left and right panels
334
+ - **Active panel indicator** - Title bar highlights the focused panel
335
+ - **Cursor sync** - Cursor position syncs between panels
336
+ - **Line numbers** - Logical line number (left) and JSONL record number (both panels)
333
337
  - **Embedded JSON diff** - `ej` to diff embedded JSON strings within each panel
334
338
 
335
339
  ## Keybindings
@@ -19,6 +19,7 @@
19
19
  - **JSONL 지원** - JSON Lines 파일을 스마트하게 포맷팅하여 편집
20
20
  - **내장 JSON 편집** - JSON 내 문자열로 저장된 JSON을 중첩 레벨까지 편집
21
21
  - **Visual 모드** - 문자 단위(`v`) 및 줄 단위(`V`) 선택 후 `d`/`y`/`c` 연산자 지원
22
+ - **숫자 접두사(Count prefix)** - Vim 스타일 숫자 접두사(`5j`, `3dd`, `d3d`) 및 fold-aware 줄 조작
22
23
  - **접기(Folding)** - JSON 블록과 긴 문자열 값을 접기/펼치기
23
24
  - **괄호 매칭** - `%`로 짝이 맞는 괄호로 이동
24
25
  - **Undo/Redo** - 전체 실행 취소 기록 지원
@@ -299,6 +300,9 @@ jvimdiff --jsonl file1.json file2.json
299
300
  - **자동 접기** - 로드 시 전체 구조를 접고, diff가 있는 부분만 자동으로 펼침
300
301
  - **Hunk 탐색** - `]c` 다음 hunk, `[c` 이전 hunk (순환)
301
302
  - **패널 전환** - `Tab`으로 좌우 패널 간 포커스 전환
303
+ - **활성 패널 표시** - 타이틀 바로 포커스된 패널을 강조 표시
304
+ - **커서 동기화** - 양쪽 패널의 커서 위치가 동기화
305
+ - **라인 번호** - 논리적 라인 번호(왼쪽)와 JSONL 레코드 번호(양쪽 패널)
302
306
  - **내장 JSON diff** - `ej`로 각 패널의 내장 JSON 문자열을 비교
303
307
 
304
308
  ## 키 바인딩
@@ -21,6 +21,7 @@ JSON editor with vim-style keybindings, built with [Textual](https://github.com/
21
21
  - **JSONL support** - Edit JSON Lines files with smart formatting
22
22
  - **Embedded JSON editing** - Edit JSON strings within JSON with nested level support
23
23
  - **Visual mode** - Character-wise (`v`) and line-wise (`V`) selection with `d`/`y`/`c` operators
24
+ - **Count prefix** - Vim-style numeric prefixes (`5j`, `3dd`, `d3d`) with fold-aware line operations
24
25
  - **Folding** - Collapse/expand JSON blocks and long string values
25
26
  - **Bracket matching** - Jump to matching brackets with `%`
26
27
  - **Undo/Redo** - Full undo history
@@ -301,6 +302,9 @@ Also available as `jvd` shortcut.
301
302
  - **Auto-fold** - On load, all structure is folded, then only diff sections are unfolded
302
303
  - **Hunk navigation** - `]c` next hunk, `[c` previous hunk (wraps around)
303
304
  - **Panel switch** - `Tab` to toggle focus between left and right panels
305
+ - **Active panel indicator** - Title bar highlights the focused panel
306
+ - **Cursor sync** - Cursor position syncs between panels
307
+ - **Line numbers** - Logical line number (left) and JSONL record number (both panels)
304
308
  - **Embedded JSON diff** - `ej` to diff embedded JSON strings within each panel
305
309
 
306
310
  ## Keybindings
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jvim"
7
- version = "0.5.0"
7
+ version = "0.7.0"
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.5.0"
6
+ __version__ = "0.7.0"
@@ -45,11 +45,16 @@ class ClipboardMixin:
45
45
  self.cursor_row += inserted
46
46
  self.cursor_col = len(parts[-1]) - 1
47
47
  return
48
+ # fold-aware: fold 헤더이면 fold end 뒤에 삽입
49
+ insert_after = self.cursor_row
50
+ fold_end = self._folds.get(self.cursor_row)
51
+ if fold_end is not None:
52
+ insert_after = fold_end
48
53
  inserted = len(self.yank_buffer)
49
54
  for i, line in enumerate(self.yank_buffer):
50
- self.lines.insert(self.cursor_row + 1 + i, line)
51
- self._adjust_line_indices(self.cursor_row + 1, inserted)
52
- self.cursor_row += 1
55
+ self.lines.insert(insert_after + 1 + i, line)
56
+ self._adjust_line_indices(insert_after + 1, inserted)
57
+ self.cursor_row = insert_after + 1
53
58
  self.cursor_col = 0
54
59
 
55
60
  def _paste_before(self) -> None:
@@ -88,11 +93,33 @@ class ClipboardMixin:
88
93
  def _join_lines(self) -> None:
89
94
  if self.cursor_row >= len(self.lines) - 1:
90
95
  return
91
- self._save_undo()
92
- cur = self.lines[self.cursor_row].rstrip()
93
- nxt = self.lines[self.cursor_row + 1].lstrip()
94
- self.cursor_col = len(cur)
95
- self.lines[self.cursor_row] = cur + " " + nxt
96
- deleted_at = self.cursor_row + 1
97
- self.lines.pop(deleted_at)
98
- self._adjust_line_indices(deleted_at, -1)
96
+ # fold-aware: fold 헤더이면 fold 전체를 제거한 뒤 다음 보이는 줄과 join
97
+ fold_end = self._folds.get(self.cursor_row)
98
+ if fold_end is not None:
99
+ next_row = fold_end + 1
100
+ if next_row >= len(self.lines):
101
+ return
102
+ self._save_undo()
103
+ cur = self.lines[self.cursor_row].rstrip()
104
+ # fold 내부 행 제거 (cursor_row+1 ~ fold_end)
105
+ del self.lines[self.cursor_row + 1 : next_row]
106
+ removed = fold_end - self.cursor_row
107
+ del self._folds[self.cursor_row]
108
+ self._folded_lines_dirty = True
109
+ self._adjust_line_indices(self.cursor_row + 1, -removed)
110
+ # 이제 cursor_row+1 = 원래 next_row (fold 뒤 첫 줄)
111
+ nxt = self.lines[self.cursor_row + 1].lstrip()
112
+ self.cursor_col = len(cur)
113
+ self.lines[self.cursor_row] = cur + " " + nxt
114
+ deleted_at = self.cursor_row + 1
115
+ self.lines.pop(deleted_at)
116
+ self._adjust_line_indices(deleted_at, -1)
117
+ else:
118
+ self._save_undo()
119
+ cur = self.lines[self.cursor_row].rstrip()
120
+ nxt = self.lines[self.cursor_row + 1].lstrip()
121
+ self.cursor_col = len(cur)
122
+ self.lines[self.cursor_row] = cur + " " + nxt
123
+ deleted_at = self.cursor_row + 1
124
+ self.lines.pop(deleted_at)
125
+ self._adjust_line_indices(deleted_at, -1)
@@ -17,6 +17,7 @@ class FoldMixin:
17
17
  ne = e + delta if e >= from_line else e
18
18
  new_folds[ns] = ne
19
19
  self._folds = new_folds
20
+ self._folded_lines_dirty = True
20
21
  self._collapsed_strings = {
21
22
  (i + delta if i >= from_line else i) for i in self._collapsed_strings
22
23
  }
@@ -37,6 +38,7 @@ class FoldMixin:
37
38
  else:
38
39
  new_folds[s] = e - abs_d
39
40
  self._folds = new_folds
41
+ self._folded_lines_dirty = True
40
42
  self._collapsed_strings = {
41
43
  (i - abs_d if i >= del_end else i)
42
44
  for i in self._collapsed_strings
@@ -92,12 +94,22 @@ class FoldMixin:
92
94
  return rng
93
95
  return None
94
96
 
95
- def _is_line_folded(self, line_idx: int) -> bool:
96
- """fold 안에 숨겨진 라인인지 확인."""
97
+ def _rebuild_folded_set(self) -> None:
98
+ """_folds로부터 숨겨진 라인 세트 재구성."""
99
+ s = set()
97
100
  for start, end in self._folds.items():
98
- if start < line_idx <= end:
99
- return True
100
- return False
101
+ for i in range(start + 1, end + 1):
102
+ s.add(i)
103
+ self._folded_lines = s
104
+ self._folded_lines_dirty = False
105
+ self._folded_lines_folds_len = len(self._folds)
106
+
107
+ def _is_line_folded(self, line_idx: int) -> bool:
108
+ """fold 안에 숨겨진 라인인지 확인. O(1)."""
109
+ # dirty 플래그 또는 _folds 직접 변경 감지 (길이 불일치)
110
+ if self._folded_lines_dirty or len(self._folds) != self._folded_lines_folds_len:
111
+ self._rebuild_folded_set()
112
+ return line_idx in self._folded_lines
101
113
 
102
114
  def _next_visible_line(self, line_idx: int, direction: int = 1) -> int:
103
115
  """다음/이전 보이는 라인 인덱스 반환."""
@@ -123,6 +135,8 @@ class FoldMixin:
123
135
  to_remove = [s for s, e in self._folds.items() if s < line_idx <= e]
124
136
  for s in to_remove:
125
137
  del self._folds[s]
138
+ if to_remove:
139
+ self._folded_lines_dirty = True
126
140
 
127
141
  def _find_long_string_at(self, line_idx: int) -> tuple[int, int, int] | None:
128
142
  """라인에서 긴 string value를 찾는다.
@@ -152,14 +166,17 @@ class FoldMixin:
152
166
  """za: fold 토글."""
153
167
  if line_idx in self._folds:
154
168
  del self._folds[line_idx]
169
+ self._folded_lines_dirty = True
155
170
  return
156
171
  rng = self._find_foldable_at(line_idx)
157
172
  if rng:
158
173
  self._folds[rng[0]] = rng[1]
174
+ self._folded_lines_dirty = True
159
175
  return
160
176
  for start, end in list(self._folds.items()):
161
177
  if start < line_idx <= end:
162
178
  del self._folds[start]
179
+ self._folded_lines_dirty = True
163
180
  return
164
181
  if line_idx in self._collapsed_strings:
165
182
  self._collapsed_strings.discard(line_idx)
@@ -171,6 +188,7 @@ class FoldMixin:
171
188
  """zo: fold 열기."""
172
189
  if line_idx in self._folds:
173
190
  del self._folds[line_idx]
191
+ self._folded_lines_dirty = True
174
192
  self._collapsed_strings.discard(line_idx)
175
193
 
176
194
  def _close_fold(self, line_idx: int) -> None:
@@ -178,6 +196,7 @@ class FoldMixin:
178
196
  rng = self._find_foldable_at(line_idx)
179
197
  if rng:
180
198
  self._folds[rng[0]] = rng[1]
199
+ self._folded_lines_dirty = True
181
200
  return
182
201
  if self._find_long_string_at(line_idx):
183
202
  self._collapsed_strings.add(line_idx)
@@ -185,6 +204,7 @@ class FoldMixin:
185
204
  enclosing = self._find_enclosing_foldable(line_idx)
186
205
  if enclosing:
187
206
  self._folds[enclosing[0]] = enclosing[1]
207
+ self._folded_lines_dirty = True
188
208
  self.cursor_row = enclosing[0]
189
209
 
190
210
  def _fold_all(self) -> None:
@@ -201,10 +221,14 @@ class FoldMixin:
201
221
  if self._find_long_string_at(i):
202
222
  self._collapsed_strings.add(i)
203
223
  i += 1
224
+ self._folded_lines_dirty = True
204
225
 
205
226
  def _unfold_all(self) -> None:
206
227
  """zR: 모든 fold 해제."""
207
228
  self._folds.clear()
229
+ self._folded_lines.clear()
230
+ self._folded_lines_dirty = False
231
+ self._folded_lines_folds_len = 0
208
232
  self._collapsed_strings.clear()
209
233
 
210
234
  def _fold_all_nested(self) -> None:
@@ -219,6 +243,7 @@ class FoldMixin:
219
243
  self._collapsed_strings.add(i)
220
244
  if 0 in self._folds:
221
245
  del self._folds[0]
246
+ self._folded_lines_dirty = True
222
247
 
223
248
  def _fold_at_depth(self, depth: int) -> None:
224
249
  """지정된 depth의 foldable 블록과 긴 string을 접기."""
@@ -236,3 +261,4 @@ class FoldMixin:
236
261
  self._folds[rng[0]] = rng[1]
237
262
  elif self._find_long_string_at(i):
238
263
  self._collapsed_strings.add(i)
264
+ self._folded_lines_dirty = True
@@ -118,6 +118,9 @@ class SubstituteMixin:
118
118
  if delta:
119
119
  self._adjust_line_indices(end + 1, delta)
120
120
  self._folds.clear()
121
+ self._folded_lines.clear()
122
+ self._folded_lines_dirty = False
123
+ self._folded_lines_folds_len = 0
121
124
  self._collapsed_strings.clear()
122
125
  self.status_msg = f"{total_count} substitution(s)"
123
126
  self._invalidate_caches()
@@ -53,6 +53,9 @@ class VisualMixin:
53
53
  else:
54
54
  self.lines[sr : er + 1] = [""]
55
55
  self._folds.clear()
56
+ self._folded_lines.clear()
57
+ self._folded_lines_dirty = False
58
+ self._folded_lines_folds_len = 0
56
59
  self._collapsed_strings.clear()
57
60
  self.cursor_row = min(sr, len(self.lines) - 1)
58
61
  self.cursor_col = 0
@@ -1,4 +1,14 @@
1
1
  {
2
+ "Count Prefix": {
3
+ "{N}motion": "repeat motion N times (e.g., 5j)",
4
+ "{N}G": "go to line N",
5
+ "{N}dd": "delete N lines (fold-aware)",
6
+ "{N}yy": "yank N lines (fold-aware)",
7
+ "d{N}d": "delete N lines (e.g., d3d)",
8
+ "{N}x": "delete N chars",
9
+ "{N}p {N}P": "paste N times",
10
+ "{N}J": "join N times"
11
+ },
2
12
  "Movement": {
3
13
  "h j k l": "left/down/up/right",
4
14
  "w b": "word forward/backward",
@@ -81,5 +91,15 @@
81
91
  ":x": "save and quit (alias for :wq)",
82
92
  ":help": "toggle this help",
83
93
  "Tab": "path completion for :e, :w"
94
+ },
95
+ "Diff Commands": {
96
+ "]c": "next hunk",
97
+ "[c": "previous hunk",
98
+ ":ig <path>": "ignore JSONPath in diff highlight",
99
+ ":ig": "show ignored paths",
100
+ ":ig!": "clear all ignored paths",
101
+ ":uig <path>": "unignore JSONPath",
102
+ ":uig + Tab": "tab-complete from ignored list",
103
+ "Tab": "switch left/right panel"
84
104
  }
85
105
  }