@@ -18,7 +18,7 @@ use text::{Bias, SelectionGoal};
1818use workspace:: searchable;
1919use workspace:: searchable:: FilteredSearchRange ;
2020
21- use crate :: motion;
21+ use crate :: motion:: { self , MotionKind } ;
2222use crate :: state:: SearchState ;
2323use crate :: {
2424 Vim ,
@@ -48,6 +48,10 @@ actions!(
4848 HelixDuplicateBelow ,
4949 /// Copies all selections above.
5050 HelixDuplicateAbove ,
51+ /// Delete the selection and enter edit mode.
52+ HelixSubstitute ,
53+ /// Delete the selection and enter edit mode, without yanking the selection.
54+ HelixSubstituteNoYank ,
5155 ]
5256) ;
5357
@@ -68,6 +72,8 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
6872 let times = Vim :: take_count ( cx) ;
6973 vim. helix_duplicate_selections_above ( times, window, cx) ;
7074 } ) ;
75+ Vim :: action ( editor, cx, Vim :: helix_substitute) ;
76+ Vim :: action ( editor, cx, Vim :: helix_substitute_no_yank) ;
7177}
7278
7379impl Vim {
@@ -604,6 +610,54 @@ impl Vim {
604610 editor. change_selections ( Default :: default ( ) , window, cx, |s| s. select ( vec ! [ newest] ) ) ;
605611 } ) ;
606612 }
613+
614+ fn do_helix_substitute ( & mut self , yank : bool , window : & mut Window , cx : & mut Context < Self > ) {
615+ self . update_editor ( cx, |vim, editor, cx| {
616+ editor. set_clip_at_line_ends ( false , cx) ;
617+ editor. transact ( window, cx, |editor, window, cx| {
618+ editor. change_selections ( SelectionEffects :: no_scroll ( ) , window, cx, |s| {
619+ s. move_with ( |map, selection| {
620+ if selection. start == selection. end {
621+ selection. end = movement:: right ( map, selection. end ) ;
622+ }
623+
624+ // If the selection starts and ends on a newline, we exclude the last one.
625+ if !selection. is_empty ( )
626+ && selection. start . column ( ) == 0
627+ && selection. end . column ( ) == 0
628+ {
629+ selection. end = movement:: left ( map, selection. end ) ;
630+ }
631+ } )
632+ } ) ;
633+ if yank {
634+ vim. copy_selections_content ( editor, MotionKind :: Exclusive , window, cx) ;
635+ }
636+ let selections = editor. selections . all :: < Point > ( cx) . into_iter ( ) ;
637+ let edits = selections. map ( |selection| ( selection. start ..selection. end , "" ) ) ;
638+ editor. edit ( edits, cx) ;
639+ } ) ;
640+ } ) ;
641+ self . switch_mode ( Mode :: Insert , true , window, cx) ;
642+ }
643+
644+ fn helix_substitute (
645+ & mut self ,
646+ _: & HelixSubstitute ,
647+ window : & mut Window ,
648+ cx : & mut Context < Self > ,
649+ ) {
650+ self . do_helix_substitute ( true , window, cx) ;
651+ }
652+
653+ fn helix_substitute_no_yank (
654+ & mut self ,
655+ _: & HelixSubstituteNoYank ,
656+ window : & mut Window ,
657+ cx : & mut Context < Self > ,
658+ ) {
659+ self . do_helix_substitute ( false , window, cx) ;
660+ }
607661}
608662
609663#[ cfg( test) ]
@@ -1241,4 +1295,67 @@ mod test {
12411295 cx. simulate_keystrokes ( "s o n e enter" ) ;
12421296 cx. assert_state ( "ˇone two one" , Mode :: HelixNormal ) ;
12431297 }
1298+
1299+ #[ gpui:: test]
1300+ async fn test_helix_substitute ( cx : & mut gpui:: TestAppContext ) {
1301+ let mut cx = VimTestContext :: new ( cx, true ) . await ;
1302+
1303+ cx. set_state ( "ˇone two" , Mode :: HelixNormal ) ;
1304+ cx. simulate_keystrokes ( "c" ) ;
1305+ cx. assert_state ( "ˇne two" , Mode :: Insert ) ;
1306+
1307+ cx. set_state ( "«oneˇ» two" , Mode :: HelixNormal ) ;
1308+ cx. simulate_keystrokes ( "c" ) ;
1309+ cx. assert_state ( "ˇ two" , Mode :: Insert ) ;
1310+
1311+ cx. set_state (
1312+ indoc ! { "
1313+ oneˇ two
1314+ three
1315+ " } ,
1316+ Mode :: HelixNormal ,
1317+ ) ;
1318+ cx. simulate_keystrokes ( "x c" ) ;
1319+ cx. assert_state (
1320+ indoc ! { "
1321+ ˇ
1322+ three
1323+ " } ,
1324+ Mode :: Insert ,
1325+ ) ;
1326+
1327+ cx. set_state (
1328+ indoc ! { "
1329+ one twoˇ
1330+ three
1331+ " } ,
1332+ Mode :: HelixNormal ,
1333+ ) ;
1334+ cx. simulate_keystrokes ( "c" ) ;
1335+ cx. assert_state (
1336+ indoc ! { "
1337+ one twoˇthree
1338+ " } ,
1339+ Mode :: Insert ,
1340+ ) ;
1341+
1342+ // Helix doesn't set the cursor to the first non-blank one when
1343+ // replacing lines: it uses language-dependent indent queries instead.
1344+ cx. set_state (
1345+ indoc ! { "
1346+ one two
1347+ « indented
1348+ three not indentedˇ»
1349+ " } ,
1350+ Mode :: HelixNormal ,
1351+ ) ;
1352+ cx. simulate_keystrokes ( "c" ) ;
1353+ cx. set_state (
1354+ indoc ! { "
1355+ one two
1356+ ˇ
1357+ " } ,
1358+ Mode :: Insert ,
1359+ ) ;
1360+ }
12441361}
0 commit comments