feat: adding word actions, not done, and lots of failing tests #4
25
.github/workflows/qodo_review.yml
vendored
25
.github/workflows/qodo_review.yml
vendored
@ -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"]'
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
@ -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}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
25
qodo.md
Normal 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."`
|
||||||
Loading…
x
Reference in New Issue
Block a user