Merge pull request 'feat: adding word actions, not done, and lots of failing tests' (#4) from feature/word-motions into master
All checks were successful
Run Test Suite / test (push) Successful in 15s

Reviewed-on: #4
This commit is contained in:
Hayden Hargreaves 2026-04-04 11:43:46 -07:00
commit a9dd5c008f
9 changed files with 1147 additions and 49 deletions

View File

@ -1,25 +0,0 @@
name: Qodo AI PR Reviewer
on:
pull_request:
types: [opened, synchronize]
jobs:
qodo_review:
runs-on: ubuntu-latest
steps:
- name: Run Qodo Merge
uses: https://github.com/Codium-ai/pr-agent@main
env:
# Platform Settings
CONFIG.GIT_PROVIDER: "gitea"
GITEA.URL: "https://git.gophernest.net"
GITEA.PERSONAL_ACCESS_TOKEN: ${{ secrets.GIT_TOKEN }}
# AI Settings
CONFIG.MODEL_PROVIDER: "google"
GOOGLE.API_KEY: ${{ secrets.GEMINI_API_KEY }}
CONFIG.MODEL: "google/gemini-3-flash" # The 2026 standard for fast reviews
# Automation
GITEA.PR_OPENED_COMMANDS: '["/review", "/describe"]'

View File

@ -14,8 +14,8 @@
- [x] `b` - Backward to start of word - [x] `b` - Backward to start of word
- [x] `W` - Forward to start of WORD (whitespace-delimited) - [x] `W` - Forward to start of WORD (whitespace-delimited)
- [x] `E` - Forward to end of WORD - [x] `E` - Forward to end of WORD
- [ ] `B` - Backward to start of WORD - [x] `B` - Backward to start of WORD
- [ ] `ge` - Backward to end of word - [x] `ge` - Backward to end of word
### Line Movement ### Line Movement
- [x] `0` - Move to start of line - [x] `0` - Move to start of line

View File

@ -24,6 +24,8 @@
glibc_multi glibc_multi
]; ];
name = "Gim";
# Define the shell that will be executed. # Define the shell that will be executed.
# Here, we explicitly use zsh. # Here, we explicitly use zsh.
# Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs` # Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs`

View File

@ -1685,3 +1685,816 @@ func TestMoveForwardWORDEndInVisualMode(t *testing.T) {
} }
}) })
} }
// --- B Motion Tests ---
func TestMoveBackwardWORD(t *testing.T) {
t.Run("test 'B' moves backward one WORD", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// Should move to start of "world" (index 6)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'BB' moves backward two WORDs", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "B", "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test '2B' moves backward two WORDs with count", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "2", "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' on punctuation-heavy text", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// "hello.world" is one WORD, should move to "next" (index 12)
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' vs 'b' on punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
// Test 'b'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "b")
m1 := getFinalModel(t, tm1)
// 'b' moves to start of "next" (index 12)
if m1.ActiveWindow().Cursor.Col != 12 {
t.Errorf("'b': CursorX() = %d, want 12", m1.ActiveWindow().Cursor.Col)
}
// Test 'B'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "B")
m2 := getFinalModel(t, tm2)
// 'B' treats "hello.world" as one WORD, moves to index 12
if m2.ActiveWindow().Cursor.Col != 12 {
t.Errorf("'B': CursorX() = %d, want 12", m2.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' crosses lines backward", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' at beginning of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// Should stay at 0,0
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'B' with multiple spaces", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// Should skip spaces and move to "hello"
if m.ActiveWindow().Cursor.Col != 9 {
t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' on empty lines", func(t *testing.T) {
lines := []string{"hello", "", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 2})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'B' complex code-like text", func(t *testing.T) {
lines := []string{"foo.bar(baz) next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "B")
m := getFinalModel(t, tm)
// "foo.bar(baz)" is one WORD, should move to "next" (index 13)
if m.ActiveWindow().Cursor.Col != 13 {
t.Errorf("CursorX() = %d, want 13", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'BB' complex code-like text", func(t *testing.T) {
lines := []string{"foo.bar(baz) next.thing"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 22, Line: 0})
sendKeys(tm, "B", "B")
m := getFinalModel(t, tm)
// Should move to start of first WORD "foo.bar(baz)" (index 0)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
}
func TestMoveBackwardWORDWithOperator(t *testing.T) {
t.Run("test 'dB' deletes backward WORD including punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "d", "B")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'dB' vs 'db' on dotted text", func(t *testing.T) {
lines := []string{"hello.world next"}
// First test 'db'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "d", "b")
m1 := getFinalModel(t, tm1)
if m1.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("'db': Line(0) = %q, want 'hello.world t'", m1.ActiveBuffer().Lines[0].String())
}
// Now test 'dB'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "d", "B")
m2 := getFinalModel(t, tm2)
if m2.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("'dB': Line(0) = %q, want 'hello.world t'", m2.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'd2B' deletes two WORDs backward", func(t *testing.T) {
lines := []string{"one.a two.b three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "d", "2", "B")
m := getFinalModel(t, tm)
// Should delete "two.b three" leaving "one.a e"
if m.ActiveBuffer().Lines[0].String() != "one.a e" {
t.Errorf("Line(0) = %q, want 'one.a e'", m.ActiveBuffer().Lines[0].String())
}
})
// BUG: This is a failing tests, cursor is not moving at start of yank
t.Run("test 'yB' yanks WORD including punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "y", "B")
m := getFinalModel(t, tm)
// Text should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "hello.world next" {
t.Errorf("Line(0) = %q, want 'hello.world next'", m.ActiveBuffer().Lines[0].String())
}
// Cursor should be at start of yanked region (index 12)
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'cB' changes WORD backward", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "c", "B")
m := getFinalModel(t, tm)
// Should delete " next" and enter insert mode
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestMoveBackwardWORDVisualMode(t *testing.T) {
t.Run("test 'vB' selects backward WORD", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "B")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at start of "next" (index 12)
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'vBd' deletes selected WORD", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "B", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.world " {
t.Errorf("Line(0) = %q, want 'hello.world '", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'v2B' selects backward two WORDs", func(t *testing.T) {
lines := []string{"foo.bar baz.qux rest"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 19, Line: 0})
sendKeys(tm, "v", "2", "B")
m := getFinalModel(t, tm)
// Cursor at start of "baz.qux" (index 8)
if m.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'VB' in visual line mode", func(t *testing.T) {
lines := []string{"hello.world", "next line"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 1})
sendKeys(tm, "V", "B")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Should select both lines
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
}
// --- ge Motion Tests ---
func TestMoveBackwardWordEnd(t *testing.T) {
t.Run("test 'ge' moves backward to previous word end", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gege' moves backward two word ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "g", "e", "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test '2ge' moves backward two word ends with count", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "2", "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' on punctuation-heavy text", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' crosses lines backward", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "hello" on previous line (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' at beginning of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should stay at 0,0
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'ge' with multiple spaces", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should skip spaces and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' on empty lines", func(t *testing.T) {
lines := []string{"hello", "", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should skip empty line and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' respects word classes", func(t *testing.T) {
lines := []string{"foo-bar baz"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of "bar" (index 6)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'ge' on method chain", func(t *testing.T) {
lines := []string{"obj.method().chain() next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 24, Line: 0})
sendKeys(tm, "g", "e")
m := getFinalModel(t, tm)
// Should move to end of ")" (index 19)
if m.ActiveWindow().Cursor.Col != 19 {
t.Errorf("CursorX() = %d, want 19", m.ActiveWindow().Cursor.Col)
}
})
}
func TestMoveBackwardWordEndWithOperator(t *testing.T) {
t.Run("test 'dge' deletes backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "d", "g", "e")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'd2ge' deletes backward two word ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "d", "2", "g", "e")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "on" {
t.Errorf("Line(0) = %q, want 'on'", m.ActiveBuffer().Lines[0].String())
}
})
// BUG: This is a failing tests, cursor is not moving at start of yank
t.Run("test 'yge' yanks backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "y", "g", "e")
m := getFinalModel(t, tm)
// Text should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "hello world next" {
t.Errorf("Line(0) = %q, want 'hello world next'", m.ActiveBuffer().Lines[0].String())
}
// Cursor should be at end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'cge' changes backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "c", "g", "e")
m := getFinalModel(t, tm)
// Should delete and enter insert mode
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestMoveBackwardWordEndVisualMode(t *testing.T) {
t.Run("test 'vge' selects backward to word end", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "e")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'vged' deletes selection", func(t *testing.T) {
lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "e", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'v2ge' selects backward two word ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "v", "2", "g", "e")
m := getFinalModel(t, tm)
// Cursor at end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'Vge' in visual line mode", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "V", "g", "e")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Should select both lines
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
}
// --- gE Motion Tests ---
func TestMoveBackwardWORDEnd(t *testing.T) {
t.Run("test 'gE' moves backward to previous WORD end", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gEgE' moves backward two WORD ends", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "g", "E", "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test '2gE' moves backward two WORD ends with count", func(t *testing.T) {
lines := []string{"one two three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
sendKeys(tm, "2", "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "one" (index 2)
if m.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' on punctuation-heavy text", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// "hello.world" is one WORD ending at index 10
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' vs 'ge' on punctuation", func(t *testing.T) {
lines := []string{"hello.world next"}
// Test 'ge'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "g", "e")
m1 := getFinalModel(t, tm1)
// 'ge' treats punctuation as separate word
if m1.ActiveWindow().Cursor.Col != 10 {
t.Errorf("'ge': CursorX() = %d, want 10", m1.ActiveWindow().Cursor.Col)
}
// Test 'gE'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "g", "E")
m2 := getFinalModel(t, tm2)
// 'gE' treats "hello.world" as one WORD
if m2.ActiveWindow().Cursor.Col != 10 {
t.Errorf("'gE': CursorX() = %d, want 10", m2.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' crosses lines backward", func(t *testing.T) {
lines := []string{"hello", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should move to end of "hello" on previous line (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' at beginning of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should stay at 0,0
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'gE' with multiple spaces", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should skip spaces and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' on empty lines", func(t *testing.T) {
lines := []string{"hello", "", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Should skip empty line and move to end of "hello" (index 4)
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' complex code-like text", func(t *testing.T) {
lines := []string{"foo.bar(baz) next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// "foo.bar(baz)" is one WORD ending at index 11
if m.ActiveWindow().Cursor.Col != 11 {
t.Errorf("CursorX() = %d, want 11", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'gE' on method chain", func(t *testing.T) {
lines := []string{"obj.method().chain() next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 24, Line: 0})
sendKeys(tm, "g", "E")
m := getFinalModel(t, tm)
// Entire chain is one WORD, ends at index 19
if m.ActiveWindow().Cursor.Col != 19 {
t.Errorf("CursorX() = %d, want 19", m.ActiveWindow().Cursor.Col)
}
})
}
func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
t.Run("test 'dgE' deletes backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "d", "g", "E")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'dgE' vs 'dge' on dotted text", func(t *testing.T) {
lines := []string{"hello.world next"}
// First test 'dge'
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm1, "d", "g", "e")
m1 := getFinalModel(t, tm1)
// 'dge' should delete to end of "world"
if m1.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("'dge': Line(0) = %q, want 'hello.worl'", m1.ActiveBuffer().Lines[0].String())
}
// Now test 'dgE'
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm2, "d", "g", "E")
m2 := getFinalModel(t, tm2)
if m2.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("'dgE': Line(0) = %q, want 'hello.worl'", m2.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'd2gE' deletes backward two WORD ends", func(t *testing.T) {
lines := []string{"one.a two.b three"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
sendKeys(tm, "d", "2", "g", "E")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "one." {
t.Errorf("Line(0) = %q, want 'one.'", m.ActiveBuffer().Lines[0].String())
}
})
// BUG: This is a failing tests, cursor is not moving at start of yank
t.Run("test 'ygE' yanks backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "y", "g", "E")
m := getFinalModel(t, tm)
// Text should remain unchanged
if m.ActiveBuffer().Lines[0].String() != "hello.world next" {
t.Errorf("Line(0) = %q, want 'hello.world next'", m.ActiveBuffer().Lines[0].String())
}
// Cursor should be at end of "hello.world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'cgE' changes backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "c", "g", "E")
m := getFinalModel(t, tm)
// Should delete and enter insert mode
if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
}
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
}
})
}
func TestMoveBackwardWORDEndVisualMode(t *testing.T) {
t.Run("test 'vgE' selects backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "E")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at end of "hello.world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'vgEd' deletes selection", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
sendKeys(tm, "v", "g", "E", "d")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
}
})
t.Run("test 'v2gE' selects backward two WORD ends", func(t *testing.T) {
lines := []string{"foo.bar baz.qux rest"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 19, Line: 0})
sendKeys(tm, "v", "2", "g", "E")
m := getFinalModel(t, tm)
// Cursor at end of "foo.bar" (index 6)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'VgE' in visual line mode", func(t *testing.T) {
lines := []string{"hello.world", "next line"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "V", "g", "E")
m := getFinalModel(t, tm)
if m.Mode() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Should select both lines
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
}

View File

@ -38,6 +38,9 @@ func NewNormalKeymap() *Keymap {
"e": motion.MoveForwardWordEnd{Count: 1}, "e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1}, "E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1}, "b": motion.MoveBackwardWord{Count: 1},
"B": motion.MoveBackwardWORD{Count: 1},
"ge": motion.MoveBackwardWordEnd{Count: 1},
"gE": motion.MoveBackwardWORDEnd{Count: 1},
"ctrl+u": motion.ScrollUpHalfPage{}, "ctrl+u": motion.ScrollUpHalfPage{},
"ctrl+d": motion.ScrollDownHalfPage{}, "ctrl+d": motion.ScrollDownHalfPage{},
";": action.RepeatFind{Count: 1, Reverse: false}, ";": action.RepeatFind{Count: 1, Reverse: false},
@ -124,6 +127,9 @@ func NewVisualKeymap() *Keymap {
"e": motion.MoveForwardWordEnd{Count: 1}, "e": motion.MoveForwardWordEnd{Count: 1},
"E": motion.MoveForwardWORDEnd{Count: 1}, "E": motion.MoveForwardWORDEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1}, "b": motion.MoveBackwardWord{Count: 1},
"B": motion.MoveBackwardWORD{Count: 1},
"ge": motion.MoveBackwardWordEnd{Count: 1},
"gE": motion.MoveBackwardWORDEnd{Count: 1},
// TODO: O and o. These are fun ones! Should be simple too // TODO: O and o. These are fun ones! Should be simple too
}, },
operators: map[string]action.Operator{ operators: map[string]action.Operator{

View File

@ -412,3 +412,290 @@ func (a MoveBackwardWord) Type() core.MotionType { return core.CharwiseExclusive
func (a MoveBackwardWord) WithCount(n int) action.Action { func (a MoveBackwardWord) WithCount(n int) action.Action {
return MoveBackwardWord{Count: n} return MoveBackwardWord{Count: n}
} }
// prevWORDStart: Finds the start of the previous WORD from position (x,y),
// treating all non-whitespace as a single class.
func prevWORDStart(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
// Back one to avoid being stuck on the current start
x--
if x < 0 {
if y == 0 {
return 0, 0 // beginning of file, stay put
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y // landed on an empty line
}
}
// Skip whitespace backward, crossing lines if needed
for {
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
x--
}
if x >= 0 {
break // landed on a non-whitespace char
}
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
}
}
// Skip to the start of the WORD (all non-whitespace is one class)
for x-1 >= 0 && line[x-1] != ' ' && line[x-1] != '\t' {
x--
}
return x, y
}
// prevWordEnd: Finds the end of the previous word from position (x,y),
// respecting word character classes.
func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
origY := y
// Back one to avoid being stuck on the current end
x--
if x < 0 {
if y == 0 {
return 0, 0 // beginning of file, stay put
}
y--
line = buf.Line(y)
x = len(line) - 1
// Don't return early for empty line - we'll handle it in whitespace skip
}
// Skip backward through current word class if we're on one
// BUT: if we crossed lines in the "back one" step, we're already at the end of a word
if y == origY && x >= 0 && line[x] != ' ' && line[x] != '\t' {
if isWordChar(line[x]) {
// Skip word characters
for x >= 0 && isWordChar(line[x]) {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
} else {
// Skip punctuation
for x >= 0 && isWordPunctuation(line[x]) {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
}
}
// Skip whitespace backward, crossing lines if needed
for {
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
x--
}
if x >= 0 {
break // landed on a non-whitespace char, this is our word end!
}
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
}
}
// Now x,y is at the start of the target word. Move forward to its end.
if x >= 0 {
if isWordChar(line[x]) {
for x+1 < len(line) && isWordChar(line[x+1]) {
x++
}
} else if isWordPunctuation(line[x]) {
for x+1 < len(line) && isWordPunctuation(line[x+1]) {
x++
}
}
}
return x, y
}
// prevWORDEnd: Finds the end of the previous WORD from position (x,y),
// treating all non-whitespace as a single class.
func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y)
origY := y
// Back one to avoid being stuck on the current end
x--
if x < 0 {
if y == 0 {
return 0, 0 // beginning of file, stay put
}
y--
line = buf.Line(y)
x = len(line) - 1
// Don't return early for empty line - we'll handle it in whitespace skip
}
// Skip backward through current WORD if we're on one
// BUT: if we crossed lines in the "back one" step, we're already at the end of a WORD
if y == origY && x >= 0 && line[x] != ' ' && line[x] != '\t' {
for x >= 0 && line[x] != ' ' && line[x] != '\t' {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
}
// Skip whitespace backward, crossing lines if needed
for {
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
x--
}
if x >= 0 {
break // landed on a non-whitespace char, this is our WORD end!
}
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if len(line) == 0 {
return 0, y // empty line acts as a word boundary
}
}
// Now x,y is at the start of the target WORD. Move forward to its end.
if x >= 0 {
for x+1 < len(line) && line[x+1] != ' ' && line[x+1] != '\t' {
x++
}
}
return x, y
}
// MoveBackwardWORD implements Motion (B) - charwise
type MoveBackwardWORD struct {
Count int
}
// MoveBackwardWORD.Execute: Moves the cursor backward by Count WORDs (B motion).
func (a MoveBackwardWORD) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x := win.Cursor.Col
y := win.Cursor.Line
for i := 0; i < a.Count; i++ {
x, y = prevWORDStart(buf, x, y)
}
win.SetCursorCol(x)
win.SetCursorLine(y)
return nil
}
// MoveBackwardWORD.Type: Returns CharwiseExclusive for backward WORD motion.
func (a MoveBackwardWORD) Type() core.MotionType { return core.CharwiseExclusive }
// MoveBackwardWORD.WithCount: Returns a new MoveBackwardWORD with the given count.
func (a MoveBackwardWORD) WithCount(n int) action.Action {
return MoveBackwardWORD{Count: n}
}
// MoveBackwardWordEnd implements Motion (ge) - charwise
type MoveBackwardWordEnd struct {
Count int
}
// MoveBackwardWordEnd.Execute: Moves the cursor to the end of the previous word (ge motion).
func (a MoveBackwardWordEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x := win.Cursor.Col
y := win.Cursor.Line
for i := 0; i < a.Count; i++ {
x, y = prevWordEnd(buf, x, y)
}
win.SetCursorCol(x)
win.SetCursorLine(y)
return nil
}
// MoveBackwardWordEnd.Type: Returns CharwiseInclusive for backward word-end motion.
func (a MoveBackwardWordEnd) Type() core.MotionType { return core.CharwiseInclusive }
// MoveBackwardWordEnd.WithCount: Returns a new MoveBackwardWordEnd with the given count.
func (a MoveBackwardWordEnd) WithCount(n int) action.Action {
return MoveBackwardWordEnd{Count: n}
}
// MoveBackwardWORDEnd implements Motion (gE) - charwise
type MoveBackwardWORDEnd struct {
Count int
}
// MoveBackwardWORDEnd.Execute: Moves the cursor to the end of the previous WORD (gE motion).
func (a MoveBackwardWORDEnd) Execute(m action.Model) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
x := win.Cursor.Col
y := win.Cursor.Line
for i := 0; i < a.Count; i++ {
x, y = prevWORDEnd(buf, x, y)
}
win.SetCursorCol(x)
win.SetCursorLine(y)
return nil
}
// MoveBackwardWORDEnd.Type: Returns CharwiseInclusive for backward WORD-end motion.
func (a MoveBackwardWORDEnd) Type() core.MotionType { return core.CharwiseInclusive }
// MoveBackwardWORDEnd.WithCount: Returns a new MoveBackwardWORDEnd with the given count.
func (a MoveBackwardWORDEnd) WithCount(n int) action.Action {
return MoveBackwardWORDEnd{Count: n}
}

View File

@ -30,8 +30,14 @@ func (o YankOperator) Operate(m action.Model, start, end core.Position, mtype co
}) })
} }
win.SetCursorCol(start.Col) // Normalize so cursor is set to the earlier position (important for backward motions)
win.SetCursorLine(start.Line) cursorPos := start
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
cursorPos = end
}
win.SetCursorCol(cursorPos.Col)
win.SetCursorLine(cursorPos.Line)
return nil return nil
} }
@ -66,16 +72,6 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
switch { switch {
case mtype.IsCharwise(): case mtype.IsCharwise():
// This shouldn't happen
// if start.Line != end.Line {
// m.SetCommandOutput(&core.CommandOutput{
// Lines: []string{"Start line and end line must match for charwise yank operations."},
// Inline: true,
// IsError: true,
// })
// return
// }
line := buf.Line(start.Line) line := buf.Line(start.Line)
startX := min(start.Col, end.Col) startX := min(start.Col, end.Col)
@ -91,17 +87,11 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
cnt := line[startX:endX] cnt := line[startX:endX]
m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt}) m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt})
case mtype == core.Linewise: win := m.ActiveWindow()
// This shouldn't happen win.SetCursorCol(startX)
// if start.Col != end.Col { win.SetCursorLine(start.Line)
// m.SetCommandOutput(&core.CommandOutput{
// Lines: []string{"Start column and end column must match for linewise yank operations."},
// Inline: true,
// IsError: true,
// })
// return
// }
case mtype == core.Linewise:
// These don't need to be validated, they are validated before being passed into the function // These don't need to be validated, they are validated before being passed into the function
startY := min(start.Line, end.Line) startY := min(start.Line, end.Line)
endY := max(start.Line, end.Line) endY := max(start.Line, end.Line)

25
qodo.md Normal file
View File

@ -0,0 +1,25 @@
### The Core Commands
* `/review`
Asks Gemini to read the entire PR diff and provide a structured summary, score the PR, identify potential bugs, and suggest high-level fixes.
* `/describe`
Automatically rewrites the PR's title and description based on the actual code changes. (Great for when you just want to push code and not write documentation).
* `/improve`
Scans the code and provides actionable, copy-pasteable snippets to improve the code, focusing on performance, security, and best practices.
* `/ask "<your question>"`
Turns the PR comment section into a chat window. It uses the PR diff as context. Example: `/ask "Did I properly handle the null pointer edge cases in the new database function?"`
### Specialized Tools
* `/test`
Asks the AI to generate unit tests specifically tailored for the new or modified code in the PR.
* `/update_changelog`
Automatically drafts an update for your `CHANGELOG.md` file based on the PR's contents.
* `/generate_labels`
Analyzes the code changes and recommends appropriate labels for the PR (e.g., `bug`, `enhancement`, `refactor`).
* `/help`
Forces the bot to reply with a quick cheat sheet of all available commands and usage instructions in case you forget them.
### Pro-Tip: Steering the AI
You can actually pass arguments directly to the commands to give Gemini specific instructions for that specific run.
For example, if you want a review but want it to be hyper-paranoid about security, you can type:
`/review --pr_reviewer.extra_instructions="Focus heavily on potential security vulnerabilities and SQL injection risks."`