@@ -1008,6 +1008,7 @@ impl ProjectPanel {
10081008 && ( cfg ! ( target_os = "windows" )
10091009 || ( settings. hide_root && visible_worktrees_count == 1 ) ) ;
10101010 let should_show_compare = !is_dir && self . file_abs_paths_to_diff ( cx) . is_some ( ) ;
1011+ let has_clipboard = self . has_clipboard_content ( cx) ;
10111012
10121013 let context_menu = ContextMenu :: build ( window, cx, |menu, _, _| {
10131014 menu. context ( self . focus_handle . clone ( ) ) . map ( |menu| {
@@ -1047,9 +1048,8 @@ impl ProjectPanel {
10471048 . action ( "Cut" , Box :: new ( Cut ) )
10481049 . action ( "Copy" , Box :: new ( Copy ) )
10491050 . action ( "Duplicate" , Box :: new ( Duplicate ) )
1050- // TODO: Paste should always be visible, cbut disabled when clipboard is empty
10511051 . action_disabled_when (
1052- self . clipboard . as_ref ( ) . is_none ( ) ,
1052+ !has_clipboard ,
10531053 "Paste" ,
10541054 Box :: new ( Paste ) ,
10551055 )
@@ -2590,19 +2590,68 @@ impl ProjectPanel {
25902590 fn cut ( & mut self , _: & Cut , _: & mut Window , cx : & mut Context < Self > ) {
25912591 let entries = self . disjoint_entries ( cx) ;
25922592 if !entries. is_empty ( ) {
2593- self . clipboard = Some ( ClipboardEntry :: Cut ( entries) ) ;
2593+ self . clipboard = Some ( ClipboardEntry :: Cut ( entries. clone ( ) ) ) ;
2594+ self . write_entries_to_system_clipboard ( & entries, true , cx) ;
25942595 cx. notify ( ) ;
25952596 }
25962597 }
25972598
25982599 fn copy ( & mut self , _: & Copy , _: & mut Window , cx : & mut Context < Self > ) {
25992600 let entries = self . disjoint_entries ( cx) ;
26002601 if !entries. is_empty ( ) {
2601- self . clipboard = Some ( ClipboardEntry :: Copied ( entries) ) ;
2602+ self . clipboard = Some ( ClipboardEntry :: Copied ( entries. clone ( ) ) ) ;
2603+ self . write_entries_to_system_clipboard ( & entries, false , cx) ;
26022604 cx. notify ( ) ;
26032605 }
26042606 }
26052607
2608+ fn write_entries_to_system_clipboard (
2609+ & self ,
2610+ entries : & BTreeSet < SelectedEntry > ,
2611+ is_cut : bool ,
2612+ cx : & Context < Self > ,
2613+ ) {
2614+ let project = self . project . read ( cx) ;
2615+ let file_paths = entries
2616+ . iter ( )
2617+ . filter_map ( |entry| {
2618+ let entry_path = project. path_for_entry ( entry. entry_id , cx) ?. path ;
2619+ let worktree = project. worktree_for_id ( entry. worktree_id , cx) ?;
2620+ Some ( worktree. read ( cx) . absolutize ( & entry_path) )
2621+ } )
2622+ . map ( |path| path. to_string_lossy ( ) . to_string ( ) )
2623+ . collect :: < Vec < _ > > ( ) ;
2624+
2625+ if !file_paths. is_empty ( ) {
2626+ let operation = if is_cut { "CUT" } else { "COPY" } ;
2627+ let clipboard_text = format ! ( "ZED_CLIPBOARD:{}\n {}" , operation, file_paths. join( "\n " ) ) ;
2628+ cx. write_to_clipboard ( ClipboardItem :: new_string ( clipboard_text) ) ;
2629+ }
2630+ }
2631+
2632+ fn has_clipboard_content ( & self , cx : & Context < Self > ) -> bool {
2633+ // Check local clipboard first (for performance)
2634+ if self . clipboard . is_some ( ) {
2635+ return true ;
2636+ }
2637+
2638+ // Check system clipboard for Zed clipboard data or file paths
2639+ if let Some ( clipboard_item) = cx. read_from_clipboard ( ) {
2640+ if let Some ( text) = clipboard_item. text ( ) {
2641+ // Check if it's Zed clipboard data
2642+ if text. starts_with ( "ZED_CLIPBOARD:" ) {
2643+ return true ;
2644+ }
2645+ // Check if it contains file paths (newline-separated paths)
2646+ if text. lines ( ) . any ( |line| Path :: new ( line) . exists ( ) ) {
2647+ return true ;
2648+ }
2649+ }
2650+ }
2651+
2652+ false
2653+ }
2654+
26062655 fn create_paste_path (
26072656 & self ,
26082657 source : & SelectedEntry ,
@@ -2659,14 +2708,56 @@ impl ProjectPanel {
26592708 }
26602709
26612710 fn paste ( & mut self , _: & Paste , window : & mut Window , cx : & mut Context < Self > ) {
2711+ // Try local clipboard first, then system clipboard
2712+ let clipboard_clone = self . clipboard . clone ( ) ;
2713+
2714+ if let Some ( clipboard_entries) = clipboard_clone. as_ref ( ) . filter ( |clipboard| !clipboard. items ( ) . is_empty ( ) ) {
2715+ self . paste_from_local_clipboard ( clipboard_entries, window, cx) ;
2716+ } else if let Some ( ( file_paths, _is_cut) ) = self . parse_system_clipboard ( cx) {
2717+ if let Some ( entry_id) = self . selected_entry_handle ( cx) . map ( |( _, e) | e. id ) {
2718+ self . drop_external_files ( & file_paths. iter ( ) . map ( PathBuf :: from) . collect :: < Vec < _ > > ( ) , entry_id, window, cx) ;
2719+ }
2720+ }
2721+ }
2722+
2723+ fn parse_system_clipboard ( & self , cx : & Context < Self > ) -> Option < ( Vec < String > , bool ) > {
2724+ let clipboard_item = cx. read_from_clipboard ( ) ?;
2725+ let text = clipboard_item. text ( ) ?;
2726+
2727+ // Check if it's Zed clipboard data
2728+ if let Some ( content) = text. strip_prefix ( "ZED_CLIPBOARD:" ) {
2729+ let mut lines = content. lines ( ) ;
2730+ let operation = lines. next ( ) ?;
2731+ let is_cut = operation == "CUT" ;
2732+ let file_paths: Vec < String > = lines. map ( |s| s. to_string ( ) ) . collect ( ) ;
2733+ if !file_paths. is_empty ( ) {
2734+ return Some ( ( file_paths, is_cut) ) ;
2735+ }
2736+ } else {
2737+ // Try to parse as plain file paths
2738+ let file_paths: Vec < String > = text
2739+ . lines ( )
2740+ . filter ( |line| !line. trim ( ) . is_empty ( ) && Path :: new ( line) . exists ( ) )
2741+ . map ( |s| s. to_string ( ) )
2742+ . collect ( ) ;
2743+ if !file_paths. is_empty ( ) {
2744+ return Some ( ( file_paths, false ) ) ;
2745+ }
2746+ }
2747+
2748+ None
2749+ }
2750+
2751+ fn paste_from_local_clipboard (
2752+ & mut self ,
2753+ clipboard_entries : & ClipboardEntry ,
2754+ window : & mut Window ,
2755+ cx : & mut Context < Self > ,
2756+ ) {
26622757 maybe ! ( {
26632758 let ( worktree, entry) = self . selected_entry_handle( cx) ?;
26642759 let entry = entry. clone( ) ;
26652760 let worktree_id = worktree. read( cx) . id( ) ;
2666- let clipboard_entries = self
2667- . clipboard
2668- . as_ref( )
2669- . filter( |clipboard| !clipboard. items( ) . is_empty( ) ) ?;
26702761
26712762 enum PasteTask {
26722763 Rename ( Task <Result <CreatedEntry >>) ,
0 commit comments