@@ -189,6 +189,7 @@ pub struct VimSet {
189189#[ derive( Clone , PartialEq , Action ) ]
190190#[ action( namespace = vim, no_json, no_register) ]
191191struct VimSave {
192+ pub range : Option < CommandRange > ,
192193 pub save_intent : Option < SaveIntent > ,
193194 pub filename : String ,
194195}
@@ -324,6 +325,134 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
324325 } ) ;
325326
326327 Vim :: action ( editor, cx, |vim, action : & VimSave , window, cx| {
328+ if let Some ( range) = & action. range {
329+ vim. update_editor ( cx, |vim, editor, cx| {
330+ let Some ( range) = range. buffer_range ( vim, editor, window, cx) . ok ( ) else {
331+ return ;
332+ } ;
333+ let Some ( ( line_ending, text, whole_buffer) ) = editor. buffer ( ) . update ( cx, |multi, cx| {
334+ Some ( multi. as_singleton ( ) ?. update ( cx, |buffer, _| {
335+ (
336+ buffer. line_ending ( ) ,
337+ buffer. as_rope ( ) . slice_rows ( range. start . 0 ..range. end . 0 + 1 ) ,
338+ range. start . 0 == 0 && range. end . 0 + 1 >= buffer. row_count ( ) ,
339+ )
340+ } ) )
341+ } ) else {
342+ return ;
343+ } ;
344+
345+ let filename = action. filename . clone ( ) ;
346+ let filename = if filename. is_empty ( ) {
347+ let Some ( file) = editor
348+ . buffer ( )
349+ . read ( cx)
350+ . as_singleton ( )
351+ . and_then ( |buffer| buffer. read ( cx) . file ( ) )
352+ else {
353+ let _ = window. prompt (
354+ gpui:: PromptLevel :: Warning ,
355+ "No file name" ,
356+ Some ( "Partial buffer write requires file name." ) ,
357+ & [ "Cancel" ] ,
358+ cx,
359+ ) ;
360+ return ;
361+ } ;
362+ file. path ( ) . display ( file. path_style ( cx) ) . to_string ( )
363+ } else {
364+ filename
365+ } ;
366+
367+ if action. filename . is_empty ( ) {
368+ if whole_buffer {
369+ if let Some ( workspace) = vim. workspace ( window) {
370+ workspace. update ( cx, |workspace, cx| {
371+ workspace
372+ . save_active_item (
373+ action. save_intent . unwrap_or ( SaveIntent :: Save ) ,
374+ window,
375+ cx,
376+ )
377+ . detach_and_prompt_err ( "Failed to save" , window, cx, |_, _, _| None ) ;
378+ } ) ;
379+ }
380+ return ;
381+ }
382+ if Some ( SaveIntent :: Overwrite ) != action. save_intent {
383+ let _ = window. prompt (
384+ gpui:: PromptLevel :: Warning ,
385+ "Use ! to write partial buffer" ,
386+ Some ( "Overwriting the current file with selected buffer content requires '!'." ) ,
387+ & [ "Cancel" ] ,
388+ cx,
389+ ) ;
390+ return ;
391+ }
392+ editor. buffer ( ) . update ( cx, |multi, cx| {
393+ if let Some ( buffer) = multi. as_singleton ( ) {
394+ buffer. update ( cx, |buffer, _| buffer. set_conflict ( ) ) ;
395+ }
396+ } ) ;
397+ } ;
398+
399+ editor. project ( ) . unwrap ( ) . update ( cx, |project, cx| {
400+ let worktree = project. visible_worktrees ( cx) . next ( ) . unwrap ( ) ;
401+
402+ worktree. update ( cx, |worktree, cx| {
403+ let path_style = worktree. path_style ( ) ;
404+ let Some ( path) = RelPath :: new ( Path :: new ( & filename) , path_style) . ok ( ) else {
405+ return ;
406+ } ;
407+
408+ let rx = ( worktree. entry_for_path ( & path) . is_some ( ) && Some ( SaveIntent :: Overwrite ) != action. save_intent ) . then ( || {
409+ window. prompt (
410+ gpui:: PromptLevel :: Warning ,
411+ & format ! ( "{path:?} already exists. Do you want to replace it?" ) ,
412+ Some (
413+ "A file or folder with the same name already exists. Replacing it will overwrite its current contents." ,
414+ ) ,
415+ & [ "Replace" , "Cancel" ] ,
416+ cx
417+ )
418+ } ) ;
419+ let filename = filename. clone ( ) ;
420+ cx. spawn_in ( window, async move |this, cx| {
421+ if let Some ( rx) = rx
422+ && Ok ( 0 ) != rx. await
423+ {
424+ return ;
425+ }
426+
427+ let _ = this. update_in ( cx, |worktree, window, cx| {
428+ let Some ( path) = RelPath :: new ( Path :: new ( & filename) , path_style) . ok ( ) else {
429+ return ;
430+ } ;
431+ worktree
432+ . write_file ( path. into_arc ( ) , text. clone ( ) , line_ending, cx)
433+ . detach_and_prompt_err ( "Failed to write lines" , window, cx, |_, _, _| None ) ;
434+ } ) ;
435+ } )
436+ . detach ( ) ;
437+ } ) ;
438+ } ) ;
439+ } ) ;
440+ return ;
441+ }
442+ if action. filename . is_empty ( ) {
443+ if let Some ( workspace) = vim. workspace ( window) {
444+ workspace. update ( cx, |workspace, cx| {
445+ workspace
446+ . save_active_item (
447+ action. save_intent . unwrap_or ( SaveIntent :: Save ) ,
448+ window,
449+ cx,
450+ )
451+ . detach_and_prompt_err ( "Failed to save" , window, cx, |_, _, _| None ) ;
452+ } ) ;
453+ }
454+ return ;
455+ }
327456 vim. update_editor ( cx, |_, editor, cx| {
328457 let Some ( project) = editor. project ( ) . cloned ( ) else {
329458 return ;
@@ -1175,24 +1304,34 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
11751304 vec ! [
11761305 VimCommand :: new(
11771306 ( "w" , "rite" ) ,
1178- workspace :: Save {
1307+ VimSave {
11791308 save_intent: Some ( SaveIntent :: Save ) ,
1309+ filename: "" . into( ) ,
1310+ range: None ,
11801311 } ,
11811312 )
1182- . bang( workspace :: Save {
1313+ . bang( VimSave {
11831314 save_intent: Some ( SaveIntent :: Overwrite ) ,
1315+ filename: "" . into( ) ,
1316+ range: None ,
11841317 } )
11851318 . filename( |action, filename| {
11861319 Some (
11871320 VimSave {
11881321 save_intent: action
11891322 . as_any( )
1190- . downcast_ref:: <workspace :: Save >( )
1323+ . downcast_ref:: <VimSave >( )
11911324 . and_then( |action| action. save_intent) ,
11921325 filename,
1326+ range: None ,
11931327 }
11941328 . boxed_clone( ) ,
11951329 )
1330+ } )
1331+ . range( |action, range| {
1332+ let mut action: VimSave = action. as_any( ) . downcast_ref:: <VimSave >( ) . unwrap( ) . clone( ) ;
1333+ action. range. replace( range. clone( ) ) ;
1334+ Some ( Box :: new( action) )
11961335 } ) ,
11971336 VimCommand :: new( ( "e" , "dit" ) , editor:: actions:: ReloadFile )
11981337 . bang( editor:: actions:: ReloadFile )
@@ -1692,12 +1831,12 @@ pub fn command_interceptor(
16921831 let mut positions: Vec < _ > = positions. iter ( ) . map ( |& pos| pos + offset) . collect ( ) ;
16931832 positions. splice ( 0 ..0 , no_args_positions. clone ( ) ) ;
16941833 let string = format ! ( "{display_string} {string}" ) ;
1695- let action = match cx
1696- . update ( |cx| commands ( cx ) . get ( cmd_idx ) ? . parse ( & string [ 1 .. ] , & range , cx ) )
1697- {
1698- Ok ( Some ( action) ) => action,
1699- _ => continue ,
1700- } ;
1834+ let ( range , query ) = VimCommand :: parse_range ( & string [ 1 .. ] ) ;
1835+ let action =
1836+ match cx . update ( |cx| commands ( cx ) . get ( cmd_idx ) ? . parse ( & query , & range , cx ) ) {
1837+ Ok ( Some ( action) ) => action,
1838+ _ => continue ,
1839+ } ;
17011840 results. push ( CommandInterceptItem {
17021841 action,
17031842 string,
@@ -2302,7 +2441,7 @@ impl ShellExec {
23022441
23032442#[ cfg( test) ]
23042443mod test {
2305- use std:: path:: Path ;
2444+ use std:: path:: { Path , PathBuf } ;
23062445
23072446 use crate :: {
23082447 VimAddon ,
@@ -2314,7 +2453,7 @@ mod test {
23142453 use indoc:: indoc;
23152454 use settings:: Settings ;
23162455 use util:: path;
2317- use workspace:: Workspace ;
2456+ use workspace:: { OpenOptions , Workspace } ;
23182457
23192458 #[ gpui:: test]
23202459 async fn test_command_basics ( cx : & mut TestAppContext ) {
@@ -2619,6 +2758,48 @@ mod test {
26192758 } ) ;
26202759 }
26212760
2761+ #[ gpui:: test]
2762+ async fn test_command_write_range ( cx : & mut TestAppContext ) {
2763+ let mut cx = VimTestContext :: new ( cx, true ) . await ;
2764+
2765+ cx. workspace ( |workspace, _, cx| {
2766+ assert_active_item ( workspace, path ! ( "/root/dir/file.rs" ) , "" , cx) ;
2767+ } ) ;
2768+
2769+ cx. set_state (
2770+ indoc ! { "
2771+ The quick
2772+ brown« fox
2773+ jumpsˇ» over
2774+ the lazy dog
2775+ " } ,
2776+ Mode :: Visual ,
2777+ ) ;
2778+
2779+ cx. simulate_keystrokes ( ": w space dir/other.rs" ) ;
2780+ cx. simulate_keystrokes ( "enter" ) ;
2781+
2782+ let other = path ! ( "/root/dir/other.rs" ) ;
2783+
2784+ let _ = cx
2785+ . workspace ( |workspace, window, cx| {
2786+ workspace. open_abs_path ( PathBuf :: from ( other) , OpenOptions :: default ( ) , window, cx)
2787+ } )
2788+ . await ;
2789+
2790+ cx. workspace ( |workspace, _, cx| {
2791+ assert_active_item (
2792+ workspace,
2793+ other,
2794+ indoc ! { "
2795+ brown fox
2796+ jumps over
2797+ " } ,
2798+ cx,
2799+ ) ;
2800+ } ) ;
2801+ }
2802+
26222803 #[ gpui:: test]
26232804 async fn test_command_matching_lines ( cx : & mut TestAppContext ) {
26242805 let mut cx = NeovimBackedTestContext :: new ( cx) . await ;
0 commit comments