@@ -10,22 +10,14 @@ class QueryEditingOperatorsMixin:
1010 """Delete/yank/change operator actions for the query editor."""
1111
1212 def action_delete_line (self : QueryMixinHost ) -> None :
13- """Delete the current line in the query editor ."""
13+ """Delete the current line (dd), with count support for multi-line delete ."""
1414 self ._clear_leader_pending ()
15- result = edit_delete .delete_line (
16- self .query_input .text ,
17- * self .query_input .cursor_location ,
18- )
19- self ._apply_edit_result (result )
15+ self ._delete_with_motion ("_" ) # _ is the current line motion
2016
2117 def action_delete_word (self : QueryMixinHost ) -> None :
22- """Delete forward word starting at cursor ."""
18+ """Delete forward word (dw), with count support ."""
2319 self ._clear_leader_pending ()
24- result = edit_delete .delete_word (
25- self .query_input .text ,
26- * self .query_input .cursor_location ,
27- )
28- self ._apply_edit_result (result )
20+ self ._delete_with_motion ("w" )
2921
3022 def action_delete_word_back (self : QueryMixinHost ) -> None :
3123 """Delete word backwards from cursor."""
@@ -200,24 +192,57 @@ def handle_result(obj_char: str | None) -> None:
200192 self .push_screen (TextObjectMenuScreen (mode , operator = "delete" ), handle_result )
201193
202194 def _delete_with_motion (self : QueryMixinHost , motion_key : str , char : str | None = None ) -> None :
203- """Execute delete with a motion."""
204- from sqlit .domains .query .editing import MOTIONS , operator_delete
195+ """Execute delete with a motion, with optional count prefix support ."""
196+ from sqlit .domains .query .editing import MOTIONS , MotionType , Position , Range , operator_delete
205197
206198 motion_func = MOTIONS .get (motion_key )
207199 if not motion_func :
208200 return
209201
202+ # Get count prefix (if any)
203+ count = self ._get_and_clear_count () or 1
204+
210205 text = self .query_input .text
211206 row , col = self .query_input .cursor_location
207+ lines = text .split ("\n " )
212208
213209 result = motion_func (text , row , col , char )
214210 if not result .range :
215211 return
216212
213+ final_range = result .range
214+
215+ # Handle count for line motions (e.g., 3dd deletes 3 lines)
216+ if motion_key == "_" and count > 1 :
217+ # _ is the current line motion; expand to cover `count` lines
218+ start_row = row
219+ end_row = min (row + count - 1 , len (lines ) - 1 )
220+ end_col = len (lines [end_row ]) if end_row < len (lines ) else 0
221+ final_range = Range (
222+ Position (start_row , 0 ),
223+ Position (end_row , end_col ),
224+ MotionType .LINEWISE ,
225+ )
226+ elif count > 1 :
227+ # For other motions, iterate to expand range
228+ end_row , end_col = result .position .row , result .position .col
229+ for _ in range (count - 1 ):
230+ next_result = motion_func (text , end_row , end_col , char )
231+ if (next_result .position .row , next_result .position .col ) == (end_row , end_col ):
232+ break # Motion didn't move
233+ end_row , end_col = next_result .position .row , next_result .position .col
234+ # Rebuild range from original position to final position
235+ final_range = Range (
236+ result .range .start ,
237+ Position (end_row , end_col ),
238+ result .range .motion_type ,
239+ result .range .inclusive ,
240+ )
241+
217242 # Push undo state before delete
218243 self ._push_undo_state ()
219244
220- op_result = operator_delete (text , result . range )
245+ op_result = operator_delete (text , final_range )
221246 self .query_input .text = op_result .text
222247 self .query_input .cursor_location = (op_result .row , op_result .col )
223248
@@ -389,27 +414,58 @@ def handle_result(obj_char: str | None) -> None:
389414 self .push_screen (TextObjectMenuScreen (mode , operator = "yank" ), handle_result )
390415
391416 def _yank_with_motion (self : QueryMixinHost , motion_key : str , char : str | None = None ) -> None :
392- """Execute yank with a motion."""
393- from sqlit .domains .query .editing import MOTIONS , operator_yank
417+ """Execute yank with a motion, with optional count prefix support ."""
418+ from sqlit .domains .query .editing import MOTIONS , MotionType , Position , Range , operator_yank
394419
395420 motion_func = MOTIONS .get (motion_key )
396421 if not motion_func :
397422 return
398423
424+ # Get count prefix (if any)
425+ count = self ._get_and_clear_count () or 1
426+
399427 text = self .query_input .text
400428 row , col = self .query_input .cursor_location
429+ lines = text .split ("\n " )
401430
402431 result = motion_func (text , row , col , char )
403432 if not result .range :
404433 return
405434
406- op_result = operator_yank (text , result .range )
435+ final_range = result .range
436+
437+ # Handle count for line motions (e.g., 3yy yanks 3 lines)
438+ if motion_key == "_" and count > 1 :
439+ start_row = row
440+ end_row = min (row + count - 1 , len (lines ) - 1 )
441+ end_col = len (lines [end_row ]) if end_row < len (lines ) else 0
442+ final_range = Range (
443+ Position (start_row , 0 ),
444+ Position (end_row , end_col ),
445+ MotionType .LINEWISE ,
446+ )
447+ elif count > 1 :
448+ # For other motions, iterate to expand range
449+ end_row , end_col = result .position .row , result .position .col
450+ for _ in range (count - 1 ):
451+ next_result = motion_func (text , end_row , end_col , char )
452+ if (next_result .position .row , next_result .position .col ) == (end_row , end_col ):
453+ break
454+ end_row , end_col = next_result .position .row , next_result .position .col
455+ final_range = Range (
456+ result .range .start ,
457+ Position (end_row , end_col ),
458+ result .range .motion_type ,
459+ result .range .inclusive ,
460+ )
461+
462+ op_result = operator_yank (text , final_range )
407463
408464 # Copy yanked text to system clipboard
409465 if op_result .yanked :
410466 self ._copy_text (op_result .yanked )
411467 # Flash the yanked range
412- ordered = result . range .ordered ()
468+ ordered = final_range .ordered ()
413469 self ._flash_yank_range (
414470 ordered .start .row , ordered .start .col ,
415471 ordered .end .row , ordered .end .col ,
@@ -590,24 +646,55 @@ def handle_result(obj_char: str | None) -> None:
590646 self .push_screen (TextObjectMenuScreen (mode , operator = "change" ), handle_result )
591647
592648 def _change_with_motion (self : QueryMixinHost , motion_key : str , char : str | None = None ) -> None :
593- """Execute change with a motion (delete + enter insert mode) ."""
594- from sqlit .domains .query .editing import MOTIONS , operator_change
649+ """Execute change with a motion, with optional count prefix support ."""
650+ from sqlit .domains .query .editing import MOTIONS , MotionType , Position , Range , operator_change
595651
596652 motion_func = MOTIONS .get (motion_key )
597653 if not motion_func :
598654 return
599655
656+ # Get count prefix (if any)
657+ count = self ._get_and_clear_count () or 1
658+
600659 text = self .query_input .text
601660 row , col = self .query_input .cursor_location
661+ lines = text .split ("\n " )
602662
603663 result = motion_func (text , row , col , char )
604664 if not result .range :
605665 return
606666
667+ final_range = result .range
668+
669+ # Handle count for line motions (e.g., 3cc changes 3 lines)
670+ if motion_key == "_" and count > 1 :
671+ start_row = row
672+ end_row = min (row + count - 1 , len (lines ) - 1 )
673+ end_col = len (lines [end_row ]) if end_row < len (lines ) else 0
674+ final_range = Range (
675+ Position (start_row , 0 ),
676+ Position (end_row , end_col ),
677+ MotionType .LINEWISE ,
678+ )
679+ elif count > 1 :
680+ # For other motions, iterate to expand range
681+ end_row , end_col = result .position .row , result .position .col
682+ for _ in range (count - 1 ):
683+ next_result = motion_func (text , end_row , end_col , char )
684+ if (next_result .position .row , next_result .position .col ) == (end_row , end_col ):
685+ break
686+ end_row , end_col = next_result .position .row , next_result .position .col
687+ final_range = Range (
688+ result .range .start ,
689+ Position (end_row , end_col ),
690+ result .range .motion_type ,
691+ result .range .inclusive ,
692+ )
693+
607694 # Push undo state before change
608695 self ._push_undo_state ()
609696
610- op_result = operator_change (text , result . range )
697+ op_result = operator_change (text , final_range )
611698 self .query_input .text = op_result .text
612699 self .query_input .cursor_location = (op_result .row , op_result .col )
613700
0 commit comments