Skip to content

Commit 3511586

Browse files
author
SqlRush
committed
Support visual vim toggle case
1 parent b813149 commit 3511586

5 files changed

Lines changed: 73 additions & 12 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、visual `<`/`>` 行缩进/反缩进、`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 `<`/`>` 行缩进/反缩进、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、visual `<`/`>` 行缩进/反缩进、`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 `<`/`>` 行缩进/反缩进、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`, 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.
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, visual `~` case toggling, `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
@@ -3774,6 +3774,17 @@ func TestREPLScreenVimVisualCharOperators(t *testing.T) {
37743774
t.Fatalf("after visual s screen = %#v", screen)
37753775
}
37763776

3777+
screen = NewREPLScreen(40, 8, nil)
3778+
screen.SetVimEnabled(true)
3779+
typePromptText(&screen, "alpha beta gamma")
3780+
screen.ApplyKey(ParseKey("\x1b"))
3781+
for _, seq := range []string{"0", "w", "v", "e", "~"} {
3782+
screen.ApplyKey(ParseKey(seq))
3783+
}
3784+
if screen.VimMode != VimNormal || screen.Prompt.Text != "alpha BETA gamma" || screen.Prompt.Cursor != len([]rune("alpha ")) {
3785+
t.Fatalf("after visual toggle case screen = %#v", screen)
3786+
}
3787+
37773788
screen = NewREPLScreen(40, 8, nil)
37783789
screen.SetVimEnabled(true)
37793790
typePromptText(&screen, "alpha beta gamma")
@@ -3853,6 +3864,17 @@ func TestREPLScreenVimVisualLineOperators(t *testing.T) {
38533864
t.Fatalf("after visual line s screen = %#v", screen)
38543865
}
38553866

3867+
screen = NewREPLScreen(40, 8, nil)
3868+
screen.SetVimEnabled(true)
3869+
typePromptText(&screen, "one\ntwo\nthree")
3870+
screen.ApplyKey(ParseKey("\x1b"))
3871+
for _, seq := range []string{"g", "g", "V", "j", "~"} {
3872+
screen.ApplyKey(ParseKey(seq))
3873+
}
3874+
if screen.VimMode != VimNormal || screen.Prompt.Text != "ONE\nTWO\nthree" || screen.Prompt.Cursor != 0 {
3875+
t.Fatalf("after visual line toggle case screen = %#v", screen)
3876+
}
3877+
38563878
screen = NewREPLScreen(40, 8, nil)
38573879
screen.SetVimEnabled(true)
38583880
typePromptText(&screen, "one\ntwo\nthree\nfour")

internal/tui/vim.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ func (s *REPLScreen) applyVimVisualRune(r rune) ScreenEvent {
307307
s.toggleVimVisualActiveEnd()
308308
case '>', '<':
309309
s.applyVimVisualIndent(r)
310+
case '~':
311+
s.applyVimVisualToggleCase()
310312
case 'y', 'd', 'c':
311313
s.applyVimVisualOperator(r)
312314
case 'x':
@@ -408,6 +410,20 @@ func (s *REPLScreen) applyVimVisualIndent(dir rune) {
408410
s.exitVimVisual()
409411
}
410412

413+
func (s *REPLScreen) applyVimVisualToggleCase() {
414+
start, end, _, ok := s.vimVisualSelectionRange()
415+
if !ok {
416+
s.exitVimVisual()
417+
return
418+
}
419+
s.recordVimUndo()
420+
count := s.Prompt.toggleCaseRange(start, end)
421+
if count > 0 {
422+
s.recordVimChange(vimRecordedChange{Kind: "toggleCase", Count: count})
423+
}
424+
s.exitVimVisual()
425+
}
426+
411427
func (s *REPLScreen) applyVimOperator(r rune) ScreenEvent {
412428
if isVimCountRune(r) && (r != '0' || s.VimCount > 0) {
413429
s.VimCount = s.VimCount*10 + int(r-'0')
@@ -1228,15 +1244,7 @@ func (p *PromptState) toggleCase(count int) {
12281244
cursor := p.clampCursor(p.Cursor)
12291245
for i := 0; i < count && cursor+i < len(runes); i++ {
12301246
idx := cursor + i
1231-
r := runes[idx]
1232-
switch {
1233-
case unicode.IsUpper(r):
1234-
runes[idx] = unicode.ToLower(r)
1235-
case unicode.IsLower(r):
1236-
runes[idx] = unicode.ToUpper(r)
1237-
default:
1238-
runes[idx] = unicode.ToUpper(r)
1239-
}
1247+
runes[idx] = toggleRuneCase(runes[idx])
12401248
}
12411249
p.Text = string(runes)
12421250
if cursor+count <= len(runes) {
@@ -1247,6 +1255,37 @@ func (p *PromptState) toggleCase(count int) {
12471255
p.resetHistoryCursor()
12481256
}
12491257

1258+
func (p *PromptState) toggleCaseRange(start int, end int) int {
1259+
runes := []rune(p.Text)
1260+
if start < 0 {
1261+
start = 0
1262+
}
1263+
if end > len(runes) {
1264+
end = len(runes)
1265+
}
1266+
if end < start {
1267+
start, end = end, start
1268+
}
1269+
for i := start; i < end; i++ {
1270+
runes[i] = toggleRuneCase(runes[i])
1271+
}
1272+
p.Text = string(runes)
1273+
p.Cursor = start
1274+
p.resetHistoryCursor()
1275+
return end - start
1276+
}
1277+
1278+
func toggleRuneCase(r rune) rune {
1279+
switch {
1280+
case unicode.IsUpper(r):
1281+
return unicode.ToLower(r)
1282+
case unicode.IsLower(r):
1283+
return unicode.ToUpper(r)
1284+
default:
1285+
return unicode.ToUpper(r)
1286+
}
1287+
}
1288+
12501289
func (p *PromptState) joinLines(count int) {
12511290
if count <= 0 {
12521291
count = 1

0 commit comments

Comments
 (0)