linux: Primary clipboard (#10534)

apricotbucket28 created

Implements copying from and pasting to the primary selection.

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs                       | 23 ++++++++++++
crates/editor/src/element.rs                      | 31 +++++++++++++++++
crates/gpui/src/app.rs                            | 12 ++++++
crates/gpui/src/platform.rs                       |  2 +
crates/gpui/src/platform/linux/headless/client.rs |  6 +++
crates/gpui/src/platform/linux/platform.rs        | 11 +++++
crates/gpui/src/platform/linux/wayland/client.rs  | 16 ++++++++
crates/gpui/src/platform/linux/x11/client.rs      | 16 ++++++++
crates/gpui/src/platform/mac/platform.rs          |  6 +++
crates/gpui/src/platform/test/platform.rs         | 10 +++++
crates/gpui/src/platform/windows/platform.rs      |  6 +++
crates/terminal/src/terminal.rs                   | 22 +++++++++++
crates/terminal_view/src/terminal_element.rs      | 28 +++++++-------
13 files changed, 173 insertions(+), 16 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1828,6 +1828,29 @@ impl Editor {
         old_cursor_position: &Anchor,
         cx: &mut ViewContext<Self>,
     ) {
+        // Copy selections to primary selection buffer
+        #[cfg(target_os = "linux")]
+        if local {
+            let selections = self.selections.all::<usize>(cx);
+            let buffer_handle = self.buffer.read(cx).read(cx);
+
+            let mut text = String::new();
+            for (index, selection) in selections.iter().enumerate() {
+                let text_for_selection = buffer_handle
+                    .text_for_range(selection.start..selection.end)
+                    .collect::<String>();
+
+                text.push_str(&text_for_selection);
+                if index != selections.len() - 1 {
+                    text.push('\n');
+                }
+            }
+
+            if !text.is_empty() {
+                cx.write_to_primary(ClipboardItem::new(text));
+            }
+        }
+
         if self.focus_handle.is_focused(cx) && self.leader_peer_id.is_none() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.set_active_selections(

crates/editor/src/element.rs 🔗

@@ -502,6 +502,34 @@ impl EditorElement {
         cx.stop_propagation();
     }
 
+    fn mouse_middle_down(
+        editor: &mut Editor,
+        event: &MouseDownEvent,
+        position_map: &PositionMap,
+        text_hitbox: &Hitbox,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
+            return;
+        }
+
+        if let Some(item) = cx.read_from_primary() {
+            let point_for_position =
+                position_map.point_for_position(text_hitbox.bounds, event.position);
+            let position = point_for_position.previous_valid;
+
+            editor.select(
+                SelectPhase::Begin {
+                    position,
+                    add: false,
+                    click_count: 1,
+                },
+                cx,
+            );
+            editor.insert(item.text(), cx);
+        }
+    }
+
     fn mouse_up(
         editor: &mut Editor,
         event: &MouseUpEvent,
@@ -2903,6 +2931,9 @@ impl EditorElement {
                         MouseButton::Right => editor.update(cx, |editor, cx| {
                             Self::mouse_right_down(editor, event, &position_map, &text_hitbox, cx);
                         }),
+                        MouseButton::Middle => editor.update(cx, |editor, cx| {
+                            Self::mouse_middle_down(editor, event, &position_map, &text_hitbox, cx);
+                        }),
                         _ => {}
                     };
                 }

crates/gpui/src/app.rs 🔗

@@ -547,11 +547,23 @@ impl AppContext {
         self.platform.window_appearance()
     }
 
+    /// Writes data to the primary selection buffer.
+    /// Only available on Linux.
+    pub fn write_to_primary(&self, item: ClipboardItem) {
+        self.platform.write_to_primary(item)
+    }
+
     /// Writes data to the platform clipboard.
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform.write_to_clipboard(item)
     }
 
+    /// Reads data from the primary selection buffer.
+    /// Only available on Linux.
+    pub fn read_from_primary(&self) -> Option<ClipboardItem> {
+        self.platform.read_from_primary()
+    }
+
     /// Reads data from the platform clipboard.
     pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         self.platform.read_from_clipboard()

crates/gpui/src/platform.rs 🔗

@@ -151,7 +151,9 @@ pub(crate) trait Platform: 'static {
     fn set_cursor_style(&self, style: CursorStyle);
     fn should_auto_hide_scrollbars(&self) -> bool;
 
+    fn write_to_primary(&self, item: ClipboardItem);
     fn write_to_clipboard(&self, item: ClipboardItem);
+    fn read_from_primary(&self) -> Option<ClipboardItem>;
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;
 
     fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;

crates/gpui/src/platform/linux/headless/client.rs 🔗

@@ -79,8 +79,14 @@ impl LinuxClient for HeadlessClient {
     //todo(linux)
     fn set_cursor_style(&self, _style: CursorStyle) {}
 
+    fn write_to_primary(&self, item: crate::ClipboardItem) {}
+
     fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
 
+    fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
+        None
+    }
+
     fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
         None
     }

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -56,7 +56,9 @@ pub trait LinuxClient {
         options: WindowParams,
     ) -> Box<dyn PlatformWindow>;
     fn set_cursor_style(&self, style: CursorStyle);
+    fn write_to_primary(&self, item: ClipboardItem);
     fn write_to_clipboard(&self, item: ClipboardItem);
+    fn read_from_primary(&self) -> Option<ClipboardItem>;
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;
     fn run(&self);
 }
@@ -406,7 +408,6 @@ impl<P: LinuxClient + 'static> Platform for P {
         })
     }
 
-    //todo(linux): add trait methods for accessing the primary selection
     fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
         let url = url.to_string();
         self.background_executor().spawn(async move {
@@ -461,10 +462,18 @@ impl<P: LinuxClient + 'static> Platform for P {
         Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
     }
 
+    fn write_to_primary(&self, item: ClipboardItem) {
+        self.write_to_primary(item)
+    }
+
     fn write_to_clipboard(&self, item: ClipboardItem) {
         self.write_to_clipboard(item)
     }
 
+    fn read_from_primary(&self) -> Option<ClipboardItem> {
+        self.read_from_primary()
+    }
+
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         self.read_from_clipboard()
     }

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -377,10 +377,26 @@ impl LinuxClient for WaylandClient {
             .log_err();
     }
 
+    fn write_to_primary(&self, item: crate::ClipboardItem) {
+        self.0.borrow_mut().primary.set_contents(item.text);
+    }
+
     fn write_to_clipboard(&self, item: crate::ClipboardItem) {
         self.0.borrow_mut().clipboard.set_contents(item.text);
     }
 
+    fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
+        self.0
+            .borrow_mut()
+            .primary
+            .get_contents()
+            .ok()
+            .map(|s| crate::ClipboardItem {
+                text: s,
+                metadata: None,
+            })
+    }
+
     fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
         self.0
             .borrow_mut()

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -603,10 +603,26 @@ impl LinuxClient for X11Client {
     //todo(linux)
     fn set_cursor_style(&self, _style: CursorStyle) {}
 
+    fn write_to_primary(&self, item: crate::ClipboardItem) {
+        self.0.borrow_mut().primary.set_contents(item.text);
+    }
+
     fn write_to_clipboard(&self, item: crate::ClipboardItem) {
         self.0.borrow_mut().clipboard.set_contents(item.text);
     }
 
+    fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
+        self.0
+            .borrow_mut()
+            .primary
+            .get_contents()
+            .ok()
+            .map(|text| crate::ClipboardItem {
+                text,
+                metadata: None,
+            })
+    }
+
     fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
         self.0
             .borrow_mut()

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -849,6 +849,8 @@ impl Platform for MacPlatform {
         }
     }
 
+    fn write_to_primary(&self, _item: ClipboardItem) {}
+
     fn write_to_clipboard(&self, item: ClipboardItem) {
         let state = self.0.lock();
         unsafe {
@@ -886,6 +888,10 @@ impl Platform for MacPlatform {
         }
     }
 
+    fn read_from_primary(&self) -> Option<ClipboardItem> {
+        None
+    }
+
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         let state = self.0.lock();
         unsafe {

crates/gpui/src/platform/test/platform.rs 🔗

@@ -23,6 +23,7 @@ pub(crate) struct TestPlatform {
     active_display: Rc<dyn PlatformDisplay>,
     active_cursor: Mutex<CursorStyle>,
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
+    current_primary_item: Mutex<Option<ClipboardItem>>,
     pub(crate) prompts: RefCell<TestPrompts>,
     pub opened_url: RefCell<Option<String>>,
     weak: Weak<Self>,
@@ -44,6 +45,7 @@ impl TestPlatform {
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
             current_clipboard_item: Mutex::new(None),
+            current_primary_item: Mutex::new(None),
             weak: weak.clone(),
             opened_url: Default::default(),
         })
@@ -282,10 +284,18 @@ impl Platform for TestPlatform {
         false
     }
 
+    fn write_to_primary(&self, item: ClipboardItem) {
+        *self.current_primary_item.lock() = Some(item);
+    }
+
     fn write_to_clipboard(&self, item: ClipboardItem) {
         *self.current_clipboard_item.lock() = Some(item);
     }
 
+    fn read_from_primary(&self) -> Option<ClipboardItem> {
+        self.current_primary_item.lock().clone()
+    }
+
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         self.current_clipboard_item.lock().clone()
     }

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -692,6 +692,8 @@ impl Platform for WindowsPlatform {
         false
     }
 
+    fn write_to_primary(&self, _item: ClipboardItem) {}
+
     fn write_to_clipboard(&self, item: ClipboardItem) {
         if item.text.len() > 0 {
             let mut ctx = ClipboardContext::new().unwrap();
@@ -699,6 +701,10 @@ impl Platform for WindowsPlatform {
         }
     }
 
+    fn read_from_primary(&self) -> Option<ClipboardItem> {
+        None
+    }
+
     fn read_from_clipboard(&self) -> Option<ClipboardItem> {
         let mut ctx = ClipboardContext::new().unwrap();
         let content = ctx.get_contents().unwrap();

crates/terminal/src/terminal.rs 🔗

@@ -750,6 +750,11 @@ impl Terminal {
             InternalEvent::SetSelection(selection) => {
                 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
 
+                #[cfg(target_os = "linux")]
+                if let Some(selection_text) = term.selection_to_string() {
+                    cx.write_to_primary(ClipboardItem::new(selection_text));
+                }
+
                 if let Some((_, head)) = selection {
                     self.selection_head = Some(*head);
                 }
@@ -766,6 +771,11 @@ impl Terminal {
                     selection.update(point, side);
                     term.selection = Some(selection);
 
+                    #[cfg(target_os = "linux")]
+                    if let Some(selection_text) = term.selection_to_string() {
+                        cx.write_to_primary(ClipboardItem::new(selection_text));
+                    }
+
                     self.selection_head = Some(point);
                     cx.emit(Event::SelectionsChanged)
                 }
@@ -1192,7 +1202,12 @@ impl Terminal {
         Some(scroll_delta)
     }
 
-    pub fn mouse_down(&mut self, e: &MouseDownEvent, origin: Point<Pixels>) {
+    pub fn mouse_down(
+        &mut self,
+        e: &MouseDownEvent,
+        origin: Point<Pixels>,
+        cx: &mut ModelContext<Self>,
+    ) {
         let position = e.position - origin;
         let point = grid_point(
             position,
@@ -1229,6 +1244,11 @@ impl Terminal {
                 self.events
                     .push_back(InternalEvent::SetSelection(Some((sel, point))));
             }
+        } else if e.button == MouseButton::Middle {
+            if let Some(item) = cx.read_from_primary() {
+                let text = item.text().to_string();
+                self.input(text);
+            }
         }
     }
 

crates/terminal_view/src/terminal_element.rs 🔗

@@ -429,7 +429,7 @@ impl TerminalElement {
             move |e, cx| {
                 cx.focus(&focus);
                 terminal.update(cx, |terminal, cx| {
-                    terminal.mouse_down(&e, origin);
+                    terminal.mouse_down(&e, origin, cx);
                     cx.notify();
                 })
             }
@@ -479,6 +479,17 @@ impl TerminalElement {
                 },
             ),
         );
+        self.interactivity.on_mouse_down(
+            MouseButton::Middle,
+            TerminalElement::generic_button_handler(
+                terminal.clone(),
+                origin,
+                focus.clone(),
+                move |terminal, origin, e, cx| {
+                    terminal.mouse_down(&e, origin, cx);
+                },
+            ),
+        );
         self.interactivity.on_scroll_wheel({
             let terminal = terminal.clone();
             move |e, cx| {
@@ -498,19 +509,8 @@ impl TerminalElement {
                     terminal.clone(),
                     origin,
                     focus.clone(),
-                    move |terminal, origin, e, _cx| {
-                        terminal.mouse_down(&e, origin);
-                    },
-                ),
-            );
-            self.interactivity.on_mouse_down(
-                MouseButton::Middle,
-                TerminalElement::generic_button_handler(
-                    terminal.clone(),
-                    origin,
-                    focus.clone(),
-                    move |terminal, origin, e, _cx| {
-                        terminal.mouse_down(&e, origin);
+                    move |terminal, origin, e, cx| {
+                        terminal.mouse_down(&e, origin, cx);
                     },
                 ),
             );