package editor import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/core" ) // ============================================================================ // Word Text Object Tests (iw/aw) // ============================================================================ func TestTextObjectInnerWord(t *testing.T) { t.Run("test 'viw' selects inner word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "v", "i", "w") m := getFinalModel(t, tm) if !m.Mode().IsVisualMode() { t.Errorf("Expected visual mode") } // Should select "hello" (cols 0-4) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 4 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=4", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'diw' deletes inner word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "d", "i", "w") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != " world" { t.Errorf("lines[0] = %q, want ' world'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'ciw' changes inner word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "c", "i", "w") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != " world" { t.Errorf("lines[0] = %q, want ' world'", m.ActiveBuffer().Lines[0]) } if m.Mode() != core.InsertMode { t.Errorf("Expected insert mode after ciw") } }) t.Run("test 'yiw' yanks inner word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "y", "i", "w") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatalf("Default register not found") } if len(reg.Content) != 1 || reg.Content[0] != "hello" { t.Errorf("register content = %v, want ['hello']", reg.Content) } }) t.Run("test 'viw' at start of word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "v", "i", "w") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 4 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=4", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'viw' at end of word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0}) sendKeys(tm, "v", "i", "w") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 4 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=4", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'viw' on underscore word", func(t *testing.T) { lines := []string{"hello_world test"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0}) sendKeys(tm, "v", "i", "w") m := getFinalModel(t, tm) // Should select "hello_world" (cols 0-10) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 10 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=10", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'viw' on punctuation", func(t *testing.T) { lines := []string{"foo-bar baz"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "w") m := getFinalModel(t, tm) // Should select just "-" (col 3) if m.ActiveWindow().Anchor.Col != 3 || m.ActiveWindow().Cursor.Col != 3 { t.Errorf("anchor=%d, cursor=%d, want anchor=3, cursor=3", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) } func TestTextObjectAroundWord(t *testing.T) { t.Run("test 'vaw' selects around word with trailing space", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "v", "a", "w") m := getFinalModel(t, tm) // Should select "hello " (cols 0-5, includes trailing space) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'daw' deletes around word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "d", "a", "w") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "world" { t.Errorf("lines[0] = %q, want 'world'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'daw' on last word (no trailing space)", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0}) sendKeys(tm, "d", "a", "w") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "hello " { t.Errorf("lines[0] = %q, want 'hello '", m.ActiveBuffer().Lines[0]) } }) } // ============================================================================ // WORD Text Object Tests (iW/aW) // ============================================================================ func TestTextObjectInnerWORD(t *testing.T) { t.Run("test 'viW' selects inner WORD", func(t *testing.T) { lines := []string{"foo-bar baz"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "v", "i", "W") m := getFinalModel(t, tm) // Should select "foo-bar" (cols 0-6) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 6 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=6", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'diW' deletes inner WORD", func(t *testing.T) { lines := []string{"foo-bar.baz test"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0}) sendKeys(tm, "d", "i", "W") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != " test" { t.Errorf("lines[0] = %q, want ' test'", m.ActiveBuffer().Lines[0]) } }) } func TestTextObjectAroundWORD(t *testing.T) { t.Run("test 'vaW' selects around WORD with trailing space", func(t *testing.T) { lines := []string{"foo-bar baz"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "v", "a", "W") m := getFinalModel(t, tm) // Should select "foo-bar " (cols 0-7, includes trailing space) if m.ActiveWindow().Anchor.Col != 0 || m.ActiveWindow().Cursor.Col != 7 { t.Errorf("anchor=%d, cursor=%d, want anchor=0, cursor=7", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'daW' deletes around WORD", func(t *testing.T) { lines := []string{"foo-bar baz"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "d", "a", "W") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "baz" { t.Errorf("lines[0] = %q, want 'baz'", m.ActiveBuffer().Lines[0]) } }) } // ============================================================================ // Delimiter Text Object Tests (i"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "<") m := getFinalModel(t, tm) // Should select "hello" (cols 1-5) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'vi>' works same as 'vi<'", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", ">") m := getFinalModel(t, tm) // Should select "hello" (cols 1-5) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di<' deletes inner angle brackets", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "i", "<") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "<>" { t.Errorf("lines[0] = %q, want '<>'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da<' deletes around angle brackets", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "a", "<") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'vi<' on empty brackets does nothing", func(t *testing.T) { lines := []string{"<>"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", "<") m := getFinalModel(t, tm) // Should remain unchanged if m.ActiveBuffer().Lines[0] != "<>" { t.Errorf("lines[0] = %q, want '<>'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'vi<' in nested brackets", func(t *testing.T) { lines := []string{">"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0}) sendKeys(tm, "v", "i", "<") m := getFinalModel(t, tm) // Should select "bar" (cols 5-7, the innermost pair) if m.ActiveWindow().Anchor.Col != 5 || m.ActiveWindow().Cursor.Col != 7 { t.Errorf("anchor=%d, cursor=%d, want anchor=5, cursor=7", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) } func TestTextObjectParentheses(t *testing.T) { t.Run("test 'vi(' selects inner parentheses", func(t *testing.T) { lines := []string{"(hello)"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "(") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'vi)' works same as 'vi('", func(t *testing.T) { lines := []string{"(hello)"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", ")") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di(' deletes inner parentheses", func(t *testing.T) { lines := []string{"func(hello)"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "func()" { t.Errorf("lines[0] = %q, want 'func()'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da(' deletes around parentheses", func(t *testing.T) { lines := []string{"func(hello)"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0}) sendKeys(tm, "d", "a", "(") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "func" { t.Errorf("lines[0] = %q, want 'func'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'vi(' on empty parens does nothing", func(t *testing.T) { lines := []string{"()"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "()" { t.Errorf("lines[0] = %q, want '()'", m.ActiveBuffer().Lines[0]) } }) } func TestTextObjectBraces(t *testing.T) { t.Run("test 'vi{' selects inner braces", func(t *testing.T) { lines := []string{"{hello}"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "{") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di{' deletes inner braces", func(t *testing.T) { lines := []string{"{hello}"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "{}" { t.Errorf("lines[0] = %q, want '{}'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da{' deletes around braces", func(t *testing.T) { lines := []string{"{hello}"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "a", "{") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) } func TestTextObjectBrackets(t *testing.T) { t.Run("test 'vi[' selects inner brackets", func(t *testing.T) { lines := []string{"[hello]"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "[") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di[' deletes inner brackets", func(t *testing.T) { lines := []string{"[hello]"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "i", "[") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "[]" { t.Errorf("lines[0] = %q, want '[]'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da[' deletes around brackets", func(t *testing.T) { lines := []string{"[hello]"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "a", "[") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) } func TestTextObjectDoubleQuotes(t *testing.T) { t.Run("test 'vi\"' selects inner double quotes", func(t *testing.T) { lines := []string{`"hello"`} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", `"`) m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di\"' deletes inner double quotes", func(t *testing.T) { lines := []string{`"hello"`} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "i", `"`) m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != `""` { t.Errorf("lines[0] = %q, want '\"\"'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da\"' deletes around double quotes", func(t *testing.T) { lines := []string{`"hello"`} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "a", `"`) m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'vi\"' on empty quotes does nothing", func(t *testing.T) { lines := []string{`""`} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", `"`) m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != `""` { t.Errorf("lines[0] = %q, want '\"\"'", m.ActiveBuffer().Lines[0]) } }) } func TestTextObjectSingleQuotes(t *testing.T) { t.Run("test 'vi'' selects inner single quotes", func(t *testing.T) { lines := []string{"'hello'"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "'") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di'' deletes inner single quotes", func(t *testing.T) { lines := []string{"'hello'"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "i", "'") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "''" { t.Errorf("lines[0] = %q, want \"''\"", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da'' deletes around single quotes", func(t *testing.T) { lines := []string{"'hello'"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "a", "'") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) } func TestTextObjectBackticks(t *testing.T) { t.Run("test 'vi`' selects inner backticks", func(t *testing.T) { lines := []string{"`hello`"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "v", "i", "`") m := getFinalModel(t, tm) if m.ActiveWindow().Anchor.Col != 1 || m.ActiveWindow().Cursor.Col != 5 { t.Errorf("anchor=%d, cursor=%d, want anchor=1, cursor=5", m.ActiveWindow().Anchor.Col, m.ActiveWindow().Cursor.Col) } }) t.Run("test 'di`' deletes inner backticks", func(t *testing.T) { lines := []string{"`hello`"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "i", "`") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "``" { t.Errorf("lines[0] = %q, want '``'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'da`' deletes around backticks", func(t *testing.T) { lines := []string{"`hello`"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "d", "a", "`") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) } // ============================================================================ // Edge Cases and Complex Scenarios // ============================================================================ func TestTextObjectEdgeCases(t *testing.T) { t.Run("test 'diw' on single character word", func(t *testing.T) { lines := []string{"a b c"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", "w") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != " b c" { t.Errorf("lines[0] = %q, want ' b c'", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'ci<' then type replacement", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "c", "i", "<") sendKeyString(tm, "world") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "" { t.Errorf("lines[0] = %q, want ''", m.ActiveBuffer().Lines[0]) } }) t.Run("test 'yi(' then paste", func(t *testing.T) { lines := []string{"func(arg)", "test"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0}) sendKeys(tm, "y", "i", "(") sendKeys(tm, "j", "p") m := getFinalModel(t, tm) // 'p' pastes after cursor, so "arg" is pasted after 't' -> "testarg" if m.ActiveBuffer().Lines[1] != "testarg" { t.Errorf("lines[1] = %q, want 'testarg'", m.ActiveBuffer().Lines[1]) } }) t.Run("test text object cursor after delimiters does nothing", func(t *testing.T) { lines := []string{"before (hello) after"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) // Should remain unchanged since cursor is not inside parens if m.ActiveBuffer().Lines[0] != "before (hello) after" { t.Errorf("lines[0] = %q, want 'before (hello) after'", m.ActiveBuffer().Lines[0]) } }) t.Run("test text object cursor before delimiters selects inside", func(t *testing.T) { lines := []string{"before (hello) after"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "before () after" { t.Errorf("lines[0] = %q, want 'before () after'", m.ActiveBuffer().Lines[0]) } }) t.Run("test text object cursor before delimiters with 'a' modifier", func(t *testing.T) { lines := []string{"before (hello) after"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "a", "(") m := getFinalModel(t, tm) // 'a' should delete including the delimiters if m.ActiveBuffer().Lines[0] != "before after" { t.Errorf("lines[0] = %q, want 'before after'", m.ActiveBuffer().Lines[0]) } }) t.Run("test text object cursor on opening delimiter", func(t *testing.T) { lines := []string{"text (hello) more"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) // Cursor on '(' at position 5, should still select inside if m.ActiveBuffer().Lines[0] != "text () more" { t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0]) } }) t.Run("test text object cursor on closing delimiter", func(t *testing.T) { lines := []string{"text (hello) more"} // "text (hello) more" // 01234567891011 <- ')' is at position 11 tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 11, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) // Cursor on ')', should still select inside if m.ActiveBuffer().Lines[0] != "text () more" { t.Errorf("lines[0] = %q, want 'text () more'", m.ActiveBuffer().Lines[0]) } }) t.Run("test multiple delimiter pairs - cursor before first", func(t *testing.T) { lines := []string{"(foo) bar (baz)"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) // Should select the first pair it finds if m.ActiveBuffer().Lines[0] != "() bar (baz)" { t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0]) } }) t.Run("test multiple delimiter pairs - cursor between pairs", func(t *testing.T) { lines := []string{"(foo) bar (baz)"} // Cursor on 'b' in "bar" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) // Should search forward and find the second pair if m.ActiveBuffer().Lines[0] != "(foo) bar ()" { t.Errorf("lines[0] = %q, want '(foo) bar ()'", m.ActiveBuffer().Lines[0]) } }) t.Run("test multiple delimiter pairs - cursor inside first", func(t *testing.T) { lines := []string{"(foo) bar (baz)"} // Cursor on 'o' in "foo" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) // Should select the first pair since cursor is inside it if m.ActiveBuffer().Lines[0] != "() bar (baz)" { t.Errorf("lines[0] = %q, want '() bar (baz)'", m.ActiveBuffer().Lines[0]) } }) t.Run("test multiple quoted strings - cursor before first", func(t *testing.T) { lines := []string{`foo "bar" baz "qux"`} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", `"`) m := getFinalModel(t, tm) // Should find and select first quoted string if m.ActiveBuffer().Lines[0] != `foo "" baz "qux"` { t.Errorf("lines[0] = %q, want 'foo \"\" baz \"qux\"'", m.ActiveBuffer().Lines[0]) } }) t.Run("test multiple quoted strings - cursor between pairs", func(t *testing.T) { lines := []string{`"foo" bar "baz"`} // Cursor on 'b' in "bar" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0}) sendKeys(tm, "d", "i", `"`) m := getFinalModel(t, tm) // Should search forward and find second string if m.ActiveBuffer().Lines[0] != `"foo" bar ""` { t.Errorf("lines[0] = %q, want '\"foo\" bar \"\"'", m.ActiveBuffer().Lines[0]) } }) } // ============================================================================ // Multi-line Delimiter Tests // ============================================================================ func TestTextObjectMultiLineDelimiters(t *testing.T) { t.Run("test 'di{' on multi-line braces", func(t *testing.T) { lines := []string{ "func test() {", " body", "}", } // Cursor on "body" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 1}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "func test() {", "}", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test 'da{' on multi-line braces", func(t *testing.T) { lines := []string{ "func test() {", " body", "}", } // Cursor on "body" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 1}) sendKeys(tm, "d", "a", "{") m := getFinalModel(t, tm) expected := []string{ "func test() ", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test 'vi(' on multi-line parentheses", func(t *testing.T) { lines := []string{ "function(", " arg1,", " arg2", ")", } // Cursor on "arg1" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 1}) sendKeys(tm, "v", "i", "(") m := getFinalModel(t, tm) // Should select from after '(' to before ')' // Line 0, col 9 (after '(') to line 3, col -1 (before ')') // But since we're in visual mode, check the anchor and cursor if m.ActiveWindow().Anchor.Line != 0 || m.ActiveWindow().Cursor.Line != 2 { t.Errorf("anchor.Line=%d, cursor.Line=%d, want anchor.Line=0, cursor.Line=2", m.ActiveWindow().Anchor.Line, m.ActiveWindow().Cursor.Line) } // Anchor should be at col 9 (after '('), cursor at end of line 2 if m.ActiveWindow().Anchor.Col != 9 { t.Errorf("anchor.Col=%d, want 9", m.ActiveWindow().Anchor.Col) } }) t.Run("test 'di(' on multi-line parentheses", func(t *testing.T) { lines := []string{ "function(", " arg1,", " arg2", ")", } // Cursor on "arg1" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 1}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) expected := []string{ "function(", ")", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test nested multi-line braces - cursor in outer", func(t *testing.T) { lines := []string{ "outer {", " inner {", " content", " }", " more", "}", } // Cursor on "more" (inside outer, outside inner) tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 4}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "outer {", "}", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test nested multi-line braces - cursor in inner", func(t *testing.T) { lines := []string{ "outer {", " inner {", " content", " }", " more", "}", } // Cursor on "content" (inside inner block) tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 2}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "outer {", " inner {", " }", " more", "}", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test nested multi-line braces with multiple nesting levels", func(t *testing.T) { lines := []string{ "level1 {", " level2 {", " level3 {", " target", " }", " }", "}", } // Cursor on "target" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 3}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "level1 {", " level2 {", " level3 {", " }", " }", "}", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test multi-line delimiters - cursor on opening line", func(t *testing.T) { lines := []string{ "function(arg) {", " body", "}", } // Cursor on opening line, after '{' tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 14, Line: 0}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "function(arg) {", "}", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test multi-line delimiters - cursor on closing line", func(t *testing.T) { lines := []string{ "function(arg) {", " body", "}", } // Cursor on closing line, before '}' tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "function(arg) {", "}", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test multi-line delimiters - cursor before delimiters searches forward", func(t *testing.T) { lines := []string{ "before", "function(arg) {", " body", "}", "after", } // Cursor on "before" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 0}) sendKeys(tm, "d", "i", "{") m := getFinalModel(t, tm) expected := []string{ "before", "function(arg) {", "}", "after", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) t.Run("test nested parentheses across lines", func(t *testing.T) { lines := []string{ "outer(", " inner(", " content", " ),", " more", ")", } // Cursor on "content" tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 2}) sendKeys(tm, "d", "i", "(") m := getFinalModel(t, tm) expected := []string{ "outer(", " inner(", " ),", " more", ")", } if !slicesEqual(m.ActiveBuffer().Lines, expected) { t.Errorf("lines = %v, want %v", m.ActiveBuffer().Lines, expected) } }) } // Helper function to compare slices func slicesEqual(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }