@@ -166,7 +166,7 @@ function createHandler(
166166 const [ mode , setMode ] = createSignal < "normal" | "insert" | "replace" | "visual" | "visual-line" | "copy" > (
167167 options ?. mode ?? "normal" ,
168168 )
169- const [ pending , setPending ] = createSignal < "" | "c" | "d" | "g" | "z" | "f" | "F" | "t" | "T" | "y" > ( "" )
169+ const [ pending , setPending ] = createSignal < "" | "c" | "d" | "g" | "z" | "f" | "F" | "t" | "T" | "y" | "r" | "vr" > ( "" )
170170 const [ lastFind , setLastFind ] = createSignal < { char : string ; forward : boolean ; till : boolean } | null > ( null )
171171 const [ register , setRegister ] = createSignal < { text : string ; linewise : boolean } | null > ( null )
172172 const [ anchor , setAnchor ] = createSignal < number | null > ( null )
@@ -1565,6 +1565,72 @@ describe("vim motion handler", () => {
15651565 expect ( ctx . textarea . cursorOffset ) . toBe ( 0 )
15661566 } )
15671567
1568+ test ( "r replaces one character and stays normal" , ( ) => {
1569+ const ctx = createHandler ( "abcd" )
1570+ ctx . textarea . cursorOffset = 1
1571+
1572+ const start = createEvent ( "r" )
1573+ expect ( ctx . handler . handleKey ( start . event ) ) . toBe ( true )
1574+ expect ( start . prevented ( ) ) . toBe ( true )
1575+ expect ( ctx . state . pending ( ) ) . toBe ( "r" )
1576+
1577+ const replacement = createEvent ( "X" )
1578+ expect ( ctx . handler . handleKey ( replacement . event ) ) . toBe ( true )
1579+ expect ( replacement . prevented ( ) ) . toBe ( true )
1580+ expect ( ctx . textarea . plainText ) . toBe ( "aXcd" )
1581+ expect ( ctx . textarea . cursorOffset ) . toBe ( 1 )
1582+ expect ( ctx . state . mode ( ) ) . toBe ( "normal" )
1583+ expect ( ctx . state . pending ( ) ) . toBe ( "" )
1584+ } )
1585+
1586+ test ( "r replaces with space" , ( ) => {
1587+ const ctx = createHandler ( "abcd" )
1588+ ctx . textarea . cursorOffset = 2
1589+
1590+ ctx . handler . handleKey ( createEvent ( "r" ) . event )
1591+ ctx . handler . handleKey ( createEvent ( "space" ) . event )
1592+
1593+ expect ( ctx . textarea . plainText ) . toBe ( "ab d" )
1594+ expect ( ctx . textarea . cursorOffset ) . toBe ( 2 )
1595+ expect ( ctx . state . mode ( ) ) . toBe ( "normal" )
1596+ } )
1597+
1598+ test ( "r return replaces character with newline and moves to next line" , ( ) => {
1599+ const ctx = createHandler ( "abcd" )
1600+ ctx . textarea . cursorOffset = 1
1601+
1602+ ctx . handler . handleKey ( createEvent ( "r" ) . event )
1603+ ctx . handler . handleKey ( createEvent ( "return" ) . event )
1604+
1605+ expect ( ctx . textarea . plainText ) . toBe ( "a\ncd" )
1606+ expect ( ctx . textarea . cursorOffset ) . toBe ( 2 )
1607+ expect ( ctx . state . mode ( ) ) . toBe ( "normal" )
1608+ } )
1609+
1610+ test ( "r does not insert on empty line" , ( ) => {
1611+ const ctx = createHandler ( "ab\n\ncd" )
1612+ ctx . textarea . cursorOffset = 3
1613+
1614+ ctx . handler . handleKey ( createEvent ( "r" ) . event )
1615+ ctx . handler . handleKey ( createEvent ( "X" ) . event )
1616+
1617+ expect ( ctx . textarea . plainText ) . toBe ( "ab\n\ncd" )
1618+ expect ( ctx . textarea . cursorOffset ) . toBe ( 3 )
1619+ expect ( ctx . state . pending ( ) ) . toBe ( "" )
1620+ } )
1621+
1622+ test ( "r replaces with uppercase characters before jump handling" , ( ) => {
1623+ const ctx = createHandler ( "abcd" )
1624+ ctx . textarea . cursorOffset = 1
1625+
1626+ ctx . handler . handleKey ( createEvent ( "r" ) . event )
1627+ ctx . handler . handleKey ( createEvent ( "G" ) . event )
1628+
1629+ expect ( ctx . textarea . plainText ) . toBe ( "aGcd" )
1630+ expect ( ctx . textarea . cursorOffset ) . toBe ( 1 )
1631+ expect ( ctx . jumpCalls ) . toEqual ( [ ] )
1632+ } )
1633+
15681634 test ( "R enters replace mode and escape exits" , ( ) => {
15691635 const ctx = createHandler ( "abc" )
15701636
@@ -3451,6 +3517,35 @@ describe("vim motion handler", () => {
34513517 expect ( ( ctx . textarea as any ) . editorView . getSelection ( ) ) . toBe ( null )
34523518 } )
34533519
3520+ test ( "visual r replaces selection before jump handling" , ( ) => {
3521+ const ctx = createHandler ( "abcd\nefgh" )
3522+ ctx . textarea . cursorOffset = 1
3523+
3524+ ctx . handler . handleKey ( createEvent ( "v" ) . event )
3525+ ctx . handler . handleKey ( createEvent ( "l" ) . event )
3526+ ctx . handler . handleKey ( createEvent ( "r" ) . event )
3527+ ctx . handler . handleKey ( createEvent ( "G" ) . event )
3528+
3529+ expect ( ctx . textarea . plainText ) . toBe ( "aGGd\nefgh" )
3530+ expect ( ctx . textarea . cursorOffset ) . toBe ( 1 )
3531+ expect ( ctx . jumpCalls ) . toEqual ( [ ] )
3532+ expect ( ctx . state . mode ( ) ) . toBe ( "normal" )
3533+ } )
3534+
3535+ test ( "visual r return inserts carriage returns without splitting lines" , ( ) => {
3536+ const ctx = createHandler ( "abcd" )
3537+ ctx . textarea . cursorOffset = 1
3538+
3539+ ctx . handler . handleKey ( createEvent ( "v" ) . event )
3540+ ctx . handler . handleKey ( createEvent ( "l" ) . event )
3541+ ctx . handler . handleKey ( createEvent ( "r" ) . event )
3542+ ctx . handler . handleKey ( createEvent ( "return" ) . event )
3543+
3544+ expect ( ctx . textarea . plainText ) . toBe ( "a\r\rd" )
3545+ expect ( ctx . textarea . cursorOffset ) . toBe ( 1 )
3546+ expect ( ctx . state . mode ( ) ) . toBe ( "normal" )
3547+ } )
3548+
34543549 test ( "visual mode with backward motion" , ( ) => {
34553550 const ctx = createHandler ( "hello world" )
34563551 ctx . textarea . cursorOffset = 5
@@ -5140,7 +5235,7 @@ describe("copy mode cursor state", () => {
51405235 const textarea = createTextarea ( "" )
51415236 const [ enabled ] = createSignal ( true )
51425237 const [ mode , setMode ] = createSignal < "normal" | "insert" | "replace" | "visual" | "visual-line" | "copy" > ( "copy" )
5143- const [ pending , setPending ] = createSignal < "" | "c" | "d" | "g" | "z" | "f" | "F" | "t" | "T" | "y" > ( "" )
5238+ const [ pending , setPending ] = createSignal < "" | "c" | "d" | "g" | "z" | "f" | "F" | "t" | "T" | "y" | "r" | "vr" > ( "" )
51445239 const [ lastFind , setLastFind ] = createSignal < { char : string ; forward : boolean ; till : boolean } | null > ( null )
51455240 const [ register , setRegister ] = createSignal < { text : string ; linewise : boolean } | null > ( null )
51465241 const [ anchor , setAnchor ] = createSignal < number | null > ( null )
0 commit comments