Skip to content

Commit b6c651f

Browse files
author
SqlRush
committed
Support vim last nonblank g motion
1 parent d32787e commit b6c651f

3 files changed

Lines changed: 92 additions & 0 deletions

File tree

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ M7 progress now includes:
242242
- `internal/tui`: Vim prompt editing now includes `gu`/`gU`/`g~` case-conversion operators across motion, linewise, find/till, text-object, and dot-repeat paths without updating the yank register.
243243
- `internal/tui`: Vim prompt editing now includes normal-mode `gJ` raw line joins that do not insert or normalize whitespace and can be dot-repeated.
244244
- `internal/tui`: Vim prompt editing now supports normal, operator, and visual `+`/`-`/`_` first-nonblank line motions, including linewise operator ranges and dot-repeat replay.
245+
- `internal/tui`: Vim prompt editing now supports normal, operator, and visual `g_` last-nonblank line motions, preserving charwise operator ranges for commands such as `dg_`.
245246
- `internal/tui`: Vim prompt editing now includes visual and visual-line `J`/`gJ` joins across selected line ranges, sharing the normal whitespace-normalized and raw join semantics plus undo, `gv`, and dot-repeat recording.
246247
- `internal/tui`: Vim prompt editing now includes visual and visual-line `p`/`P` paste-over-selection behavior for characterwise and linewise registers, updating the unnamed register with the replaced text and avoiding extra trailing blank lines for end-of-buffer line replacements.
247248
- `internal/tui`: Vim prompt editing now includes visual and visual-line `r{char}` selection replacement, replacing non-newline characters while preserving line structure, supporting undo, and remembering the previous visual range for `gv`.

internal/tui/tui_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3573,6 +3573,47 @@ func TestREPLScreenVimFirstNonBlankLineMotions(t *testing.T) {
35733573
}
35743574
}
35753575

3576+
func TestREPLScreenVimLastNonBlankGMotion(t *testing.T) {
3577+
screen := NewREPLScreen(40, 8, nil)
3578+
screen.SetVimEnabled(true)
3579+
typePromptText(&screen, "one \n two \nthree")
3580+
screen.ApplyKey(ParseKey("\x1b"))
3581+
for _, seq := range []string{"g", "g", "g", "_"} {
3582+
screen.ApplyKey(ParseKey(seq))
3583+
}
3584+
if screen.Prompt.Cursor != len([]rune("one"))-1 {
3585+
t.Fatalf("cursor after g_ = %d", screen.Prompt.Cursor)
3586+
}
3587+
for _, seq := range []string{"g", "g", "2", "g", "_"} {
3588+
screen.ApplyKey(ParseKey(seq))
3589+
}
3590+
if screen.Prompt.Cursor != len([]rune("one \n two"))-1 {
3591+
t.Fatalf("cursor after 2g_ = %d", screen.Prompt.Cursor)
3592+
}
3593+
3594+
screen = NewREPLScreen(40, 8, nil)
3595+
screen.SetVimEnabled(true)
3596+
typePromptText(&screen, "one \n two")
3597+
screen.ApplyKey(ParseKey("\x1b"))
3598+
for _, seq := range []string{"g", "g", "v", "g", "_", "y"} {
3599+
screen.ApplyKey(ParseKey(seq))
3600+
}
3601+
if screen.VimMode != VimNormal || screen.VimRegister != "one" || screen.VimRegisterLinewise || screen.Prompt.Text != "one \n two" {
3602+
t.Fatalf("after visual g_ yank screen = %#v", screen)
3603+
}
3604+
3605+
screen = NewREPLScreen(40, 8, nil)
3606+
screen.SetVimEnabled(true)
3607+
typePromptText(&screen, "one \n two")
3608+
screen.ApplyKey(ParseKey("\x1b"))
3609+
for _, seq := range []string{"g", "g", "d", "g", "_"} {
3610+
screen.ApplyKey(ParseKey(seq))
3611+
}
3612+
if screen.Prompt.Text != " \n two" || screen.Prompt.Cursor != 0 || screen.VimRegister != "one" || screen.VimRegisterLinewise {
3613+
t.Fatalf("after dg_ screen = %#v", screen)
3614+
}
3615+
}
3616+
35763617
func TestREPLScreenVimBackwardEndMotions(t *testing.T) {
35773618
screen := NewREPLScreen(40, 8, nil)
35783619
screen.SetVimEnabled(true)

internal/tui/vim.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ func (s *REPLScreen) applyVimVisualG(r rune) ScreenEvent {
434434
applyN(count, func() { s.Prompt.moveWORDBackwardEnd() })
435435
case 'J':
436436
s.applyVimVisualJoin(true)
437+
case '_':
438+
s.Prompt.moveVimLastNonBlankMotion(count)
437439
}
438440
return ScreenEvent{}
439441
}
@@ -862,6 +864,12 @@ func (s *REPLScreen) applyVimG(r rune) ScreenEvent {
862864
} else {
863865
s.applyVimBackwardEndMotionOperator(operator, r, count)
864866
}
867+
case '_':
868+
if operator == 0 {
869+
s.Prompt.moveVimLastNonBlankMotion(count)
870+
} else {
871+
s.applyVimMotionOperator(operator, r, count)
872+
}
865873
}
866874
return ScreenEvent{}
867875
}
@@ -1676,13 +1684,55 @@ func (p *PromptState) moveVimFirstNonBlankMotion(motion rune, count int) {
16761684
p.moveFirstNonBlank()
16771685
}
16781686

1687+
func (p *PromptState) vimLastNonBlankMotionTargetLine(count int) int {
1688+
if count <= 0 {
1689+
count = 1
1690+
}
1691+
return p.currentLogicalLine() + count
1692+
}
1693+
1694+
func (p *PromptState) moveVimLastNonBlankMotion(count int) {
1695+
lines := strings.Split(p.Text, "\n")
1696+
line := p.vimLastNonBlankMotionTargetLine(count)
1697+
if line < 1 {
1698+
line = 1
1699+
}
1700+
if line > len(lines) {
1701+
line = len(lines)
1702+
}
1703+
lineIndex := line - 1
1704+
p.Cursor = lineStartOffset(lines, lineIndex) + lastNonBlankOffset(lines[lineIndex])
1705+
}
1706+
1707+
func lastNonBlankOffset(line string) int {
1708+
runes := []rune(line)
1709+
for i := len(runes) - 1; i >= 0; i-- {
1710+
if !unicode.IsSpace(runes[i]) {
1711+
return i
1712+
}
1713+
}
1714+
return 0
1715+
}
1716+
16791717
func (p *PromptState) operatorMotionRange(operator rune, motion rune, count int) (int, int, bool, bool) {
16801718
if count <= 0 {
16811719
count = 1
16821720
}
16831721
runes := []rune(p.Text)
16841722
start := p.clampCursor(p.Cursor)
16851723
cursor := *p
1724+
if motion == '_' {
1725+
cursor.moveVimLastNonBlankMotion(count)
1726+
end := cursor.Cursor
1727+
if end >= start {
1728+
if end < len(runes) {
1729+
end++
1730+
}
1731+
} else if start < len(runes) {
1732+
start++
1733+
}
1734+
return orderedRange(start, end, false)
1735+
}
16861736
switch motion {
16871737
case 'j':
16881738
start, end, ok := p.lineMotionRange(p.currentLogicalLine() + count + 1)

0 commit comments

Comments
 (0)