Skip to content

Commit 058800f

Browse files
author
SqlRush
committed
Support visual vim indentation
1 parent 0cfb7f7 commit 058800f

5 files changed

Lines changed: 77 additions & 10 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
732732

733733
本轮补充:interaction script loader 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 里恢复 script JSON,减少模型/SDK 录制脚本响应的手工拆包。
734734

735-
本轮补充:TUI Vim prompt editing 增加基础 visual/visual-line 模式,支持 `v`/`V` 进入选择、motion 扩展 selection、visual `o` 切换 active end、`y`/`d`/`c` 以及常用 visual `x`/`s` aliases 作用于选择范围、Esc 回到 normal,并让 interaction script 可用 `visual`/`visualLine` 断言当前 mode。
735+
本轮补充:TUI Vim prompt editing 增加基础 visual/visual-line 模式,支持 `v`/`V` 进入选择、motion 扩展 selection、visual `o` 切换 active end、visual `<`/`>` 行缩进/反缩进、`y`/`d`/`c` 以及常用 visual `x`/`s` aliases 作用于选择范围、Esc 回到 normal,并让 interaction script 可用 `visual`/`visualLine` 断言当前 mode。
736736

737737
本轮补充:prompt history 写入现在保留 image pasted-content 的 media type、filename、dimensions 和 image-cache source path 元数据,同时继续不把 inline base64 image bytes 或 text-paste hash 写进图片历史记录。
738738

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ test/parity/ # golden tests against TS/official behavior
232232
- 本轮补充:contract message、session entry 和 transcript loader 现在会把 message-type aliases 归一化为 canonical 类型,包括 `assistant_message``userMessage``system-event``attachmentMessage``progress_update``tombstone_event`;full loader、line index 和 indexed resume 统一使用 canonical user/assistant/system/attachment,并保留 progress bridge 语义。
233233
- 本轮补充:tail、byte-tail、window 和 metadata-only transcript loader 也改用 canonical message type 处理 progress bridge 与 compact-boundary,`progress_update``system-event` 等别名不再只在 full loader 路径生效。
234234
- 本轮补充:tail、byte-tail、window 和 streaming transcript search 现在复用 full/index loader 的 wrapped record 展开逻辑,可从 JSON:API/resource、GraphQL edge/node 和 collection/list wrapper 中恢复 transcript 批次,并保留 progress bridge。
235-
- 本轮补充:TUI Vim prompt editing 增加基础 visual/visual-line 模式,支持 `v`/`V` 进入选择、motion 扩展 selection、visual `o` 切换 active end、`y`/`d`/`c` 以及常用 visual `x`/`s` aliases 作用于选择范围、Esc 回到 normal,并让 interaction script 可用 `visual`/`visualLine` 断言当前 mode。
235+
- 本轮补充:TUI Vim prompt editing 增加基础 visual/visual-line 模式,支持 `v`/`V` 进入选择、motion 扩展 selection、visual `o` 切换 active end、visual `<`/`>` 行缩进/反缩进、`y`/`d`/`c` 以及常用 visual `x`/`s` aliases 作用于选择范围、Esc 回到 normal,并让 interaction script 可用 `visual`/`visualLine` 断言当前 mode。
236236
- 本轮补充:prompt history 写入现在保留 image pasted-content 的 media type、filename、dimensions 和 image-cache source path 元数据,同时继续不把 inline base64 image bytes 或 text-paste hash 写进图片历史记录。
237237
- 本轮补充:prompt history 读取旧 image pasted-content 记录时,如果缺少 source path 但对应 session 的 image-cache 文件仍存在,会自动补回 source path 并刷新内存 image path cache。
238238
- 本轮补充:interaction script key 字段现在接受 DOM-style key event object,可从 `key`/`code`(包括 `Numpad*`、扩展数字区括号/hash/backspace 和标点 key code)、旧式 `keyIdentifier`、数字 `keyCode`/`which`/`charCode`(包括标点和数字区运算符)、`keypress.which` 字符码、`ctrlKey`/`altKey`/`metaKey`/`shiftKey``modifiers` 数组还原现有 key 名,wrapper payload 中的 key event 也可驱动脚本回放。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ M6 progress now includes:
229229
M7 progress now includes:
230230

231231
- `internal/tui`: lightweight terminal frame renderer using ANSI clear/home/cursor control, message/status/prompt/dialog components, prompt input editing including shared kill-ring state, shift-enter multiline prompt input, line-local multiline prompt ctrl-a/ctrl-e/ctrl-u/ctrl-k editing plus multiline prompt wrap/render/cursor placement, ctrl-b/ctrl-f/ctrl-u/ctrl-k/ctrl-w line editing, ctrl-p/ctrl-n prompt and reverse-search navigation, alt-b/alt-f/alt-d/alt-backspace word editing, ctrl-left/ctrl-right/alt-left/alt-right word motion, ctrl-y yank, and alt-y yank-pop initial support, reverse-search cursor/word editing/kill/yank/yank-pop initial support, ctrl-c interrupt/double-press exit events, ctrl-d delete-forward and empty-input double-press exit events, ctrl-l redraw events, ctrl-o/ctrl-t global toggle events, ctrl-g/ctrl-s/ctrl-x chord chat events, prompt history navigation, reverse-search state/rendering/selection/script assertions including empty/cancel paths and cursor position, paste/image hint input handling with OSC ST terminators and base64 filename decoding, text/image pasted-content references with expanded submit text, metadata script assertions, and history entry restoration, SGR mouse parsing, alternate terminal navigation key sequences including modified Home/End/Delete/PageUp/PageDown, wheel scrolling with modifiers, primary clicks with modifiers, primary drag viewport selection, configurable viewport half-page/top/bottom scrolling, viewport line selection and dialog action clicks, focus/blur events, resize scroll preservation, default and configurable keybinding resolver with chord pending/null-unbind behavior, key/action camelCase aliases, keybinding JSON config loading, and focus/mouse/paste/image key names, vim insert/normal word, WORD, ge/gE, operator ge/gE, line-local `^`/`$`/`0`/`|`/`I`/`A`/`D`, quote, bracket, and j/k linewise motions/text objects, persistent yank/register/paste paths, last-change dot-repeat, G/gg line navigation, toggle-case, join, open-line, indent, substitute, delete/count/replace/undo/find/till/repeat/matching-pair % actions, normal-mode arrow/backspace/delete mappings, and operator character ranges, permission/task dialog builders with kind/id routing/runtime resolution/status line, stale dialog race guards, active dialog cancellation, permission id/bulk cancellation, queued permission promotion, active task dialog refresh, task lifecycle state transitions and bulk cancellation, idempotent alternate screen lifecycle/reset/reassert-interactive sequences, mouse/focus/bracketed-paste terminal mode lifecycle/reconciliation and reassertion sequences, ANSI snapshot capture/stripping, snapshot corpus write/compare/script-file compare/missing-baseline/diff/batch/strict unexpected-baseline status, scripted interaction runner with JSON/JSONL/wrapper loading and file-runner entrypoints, JSON/runtime/task camel field aliases, keybinding mutations, permission-cancel runtime mutations, and assertions including vim mode/register/task state/dialog result/runtime mutation/task bulk-cancel/status negative/snapshot negative/screen size/event-sequence/event-count/no-event/dialog-result-count/no-dialog-result plus multi-key/text/paste/image/pasted-content metadata and named-key input, REPL screen model, viewport scrolling, and selection focus model.
232-
- `internal/tui`: Vim prompt editing now includes basic visual and visual-line mode support with `v`/`V`, motion-extended selections, active-end toggle with visual `o`, `y`/`d`/`c` range operators plus common visual `x`/`s` aliases, Escape return-to-normal behavior, and script expectation aliases for `visual`/`visualLine` modes.
232+
- `internal/tui`: Vim prompt editing now includes basic visual and visual-line mode support with `v`/`V`, motion-extended selections, active-end toggle with visual `o`, visual `<`/`>` line indent/outdent, `y`/`d`/`c` range operators plus common visual `x`/`s` aliases, Escape return-to-normal behavior, and script expectation aliases for `visual`/`visualLine` modes.
233233
- `internal/session`: prompt history now persists image pasted-content metadata such as media type, filename, dimensions, and image-cache source path while still omitting inline base64 image bytes and text-paste hashes for images.
234234
- `internal/session`: prompt history loading now backfills missing image source paths from existing per-session image-cache files and refreshes the in-memory image path cache when those files exist.
235235
- `internal/tui`: interaction script key fields now accept DOM-style key event objects with `key`/`code` including `Numpad*`, extended numpad paren/hash/backspace codes, and punctuation key codes, legacy `keyIdentifier`, numeric `keyCode`/`which`/`charCode` including punctuation and numpad operators, `keypress.which` char-code replay, modifier booleans such as `ctrlKey`/`altKey`/`metaKey`/`shiftKey`, and modifier arrays, including wrapper payloads and modifier-only event filtering.

internal/tui/tui_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3784,6 +3784,17 @@ func TestREPLScreenVimVisualCharOperators(t *testing.T) {
37843784
if screen.VimMode != VimNormal || screen.VimRegister != "alpha beta" || screen.VimRegisterLinewise || screen.Prompt.Text != "alpha beta gamma" {
37853785
t.Fatalf("after visual o yank screen = %#v", screen)
37863786
}
3787+
3788+
screen = NewREPLScreen(40, 8, nil)
3789+
screen.SetVimEnabled(true)
3790+
typePromptText(&screen, "one\ntwo\nthree")
3791+
screen.ApplyKey(ParseKey("\x1b"))
3792+
for _, seq := range []string{"g", "g", "v", "j", ">"} {
3793+
screen.ApplyKey(ParseKey(seq))
3794+
}
3795+
if screen.VimMode != VimNormal || screen.Prompt.Text != " one\n two\nthree" || screen.Prompt.Cursor != len([]rune(" ")) {
3796+
t.Fatalf("after visual indent screen = %#v", screen)
3797+
}
37873798
}
37883799

37893800
func TestREPLScreenVimVisualLineOperators(t *testing.T) {
@@ -3852,6 +3863,17 @@ func TestREPLScreenVimVisualLineOperators(t *testing.T) {
38523863
if screen.VimMode != VimNormal || screen.VimRegister != "one\ntwo\nthree\n" || !screen.VimRegisterLinewise || screen.Prompt.Text != "one\ntwo\nthree\nfour" {
38533864
t.Fatalf("after visual line o yank screen = %#v", screen)
38543865
}
3866+
3867+
screen = NewREPLScreen(40, 8, nil)
3868+
screen.SetVimEnabled(true)
3869+
typePromptText(&screen, "one\n two\n three\nfour")
3870+
screen.ApplyKey(ParseKey("\x1b"))
3871+
for _, seq := range []string{"g", "g", "j", "V", "j", "<"} {
3872+
screen.ApplyKey(ParseKey(seq))
3873+
}
3874+
if screen.VimMode != VimNormal || screen.Prompt.Text != "one\ntwo\nthree\nfour" || screen.Prompt.Cursor != len([]rune("one\n")) {
3875+
t.Fatalf("after visual line outdent screen = %#v", screen)
3876+
}
38553877
}
38563878

38573879
func TestREPLScreenVimGLineNavigationAndOperators(t *testing.T) {

internal/tui/vim.go

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ func (s *REPLScreen) applyVimVisualRune(r rune) ScreenEvent {
305305
s.VimPendingCount = count
306306
case 'o':
307307
s.toggleVimVisualActiveEnd()
308+
case '>', '<':
309+
s.applyVimVisualIndent(r)
308310
case 'y', 'd', 'c':
309311
s.applyVimVisualOperator(r)
310312
case 'x':
@@ -392,6 +394,20 @@ func (s *REPLScreen) applyVimVisualOperator(operator rune) {
392394
s.exitVimVisual()
393395
}
394396

397+
func (s *REPLScreen) applyVimVisualIndent(dir rune) {
398+
start, end, _, ok := s.vimVisualSelectionRange()
399+
if !ok {
400+
s.exitVimVisual()
401+
return
402+
}
403+
s.recordVimUndo()
404+
affected := s.Prompt.indentRange(dir, start, end)
405+
if affected > 0 {
406+
s.recordVimChange(vimRecordedChange{Kind: "indent", Dir: dir, Count: affected})
407+
}
408+
s.exitVimVisual()
409+
}
410+
395411
func (s *REPLScreen) applyVimOperator(r rune) ScreenEvent {
396412
if isVimCountRune(r) && (r != '0' || s.VimCount > 0) {
397413
s.VimCount = s.VimCount*10 + int(r-'0')
@@ -1269,14 +1285,42 @@ func (p *PromptState) indentLines(dir rune, count int) {
12691285
if count <= 0 {
12701286
count = 1
12711287
}
1272-
lines := strings.Split(p.Text, "\n")
12731288
current := p.currentLogicalLine()
1274-
linesToAffect := count
1275-
if linesToAffect > len(lines)-current {
1276-
linesToAffect = len(lines) - current
1289+
p.indentLineRange(dir, current, current+count-1)
1290+
}
1291+
1292+
func (p *PromptState) indentRange(dir rune, start int, end int) int {
1293+
lines := strings.Split(p.Text, "\n")
1294+
if len(lines) == 0 {
1295+
return 0
1296+
}
1297+
startLine := p.lineIndexAtCursor(start)
1298+
inclusiveEnd := end
1299+
if inclusiveEnd > start {
1300+
inclusiveEnd--
1301+
}
1302+
endLine := p.lineIndexAtCursor(inclusiveEnd)
1303+
return p.indentLineRange(dir, startLine, endLine)
1304+
}
1305+
1306+
func (p *PromptState) indentLineRange(dir rune, startLine int, endLine int) int {
1307+
lines := strings.Split(p.Text, "\n")
1308+
if len(lines) == 0 {
1309+
return 0
1310+
}
1311+
if startLine < 0 {
1312+
startLine = 0
1313+
}
1314+
if endLine < startLine {
1315+
startLine, endLine = endLine, startLine
1316+
}
1317+
if startLine >= len(lines) {
1318+
startLine = len(lines) - 1
1319+
}
1320+
if endLine >= len(lines) {
1321+
endLine = len(lines) - 1
12771322
}
1278-
for i := 0; i < linesToAffect; i++ {
1279-
idx := current + i
1323+
for idx := startLine; idx <= endLine; idx++ {
12801324
line := lines[idx]
12811325
if dir == '>' {
12821326
lines[idx] = " " + line
@@ -1299,8 +1343,9 @@ func (p *PromptState) indentLines(dir rune, count int) {
12991343
}
13001344
}
13011345
p.Text = strings.Join(lines, "\n")
1302-
p.Cursor = lineStartOffset(lines, current) + firstNonBlankOffset(lines[current])
1346+
p.Cursor = lineStartOffset(lines, startLine) + firstNonBlankOffset(lines[startLine])
13031347
p.resetHistoryCursor()
1348+
return endLine - startLine + 1
13041349
}
13051350

13061351
func (p *PromptState) openLine(below bool) {

0 commit comments

Comments
 (0)