From eb914682b39c154dca8d6352184afbf89b00f782 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 25 Jun 2024 14:54:52 -0600 Subject: [PATCH] Fix multi-cursor copy/paste on linux (#13523) The clipboard library we use for X11 doesn't yet support multiple formats on the clipboard, so for now we just store this in memory for the current zed process, as we do for Wayland. Fixes: #11971 Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- .../gpui/src/platform/linux/wayland/client.rs | 22 +++--------- .../src/platform/linux/wayland/clipboard.rs | 34 +++++++++---------- crates/gpui/src/platform/linux/x11/client.rs | 25 +++++++++++--- crates/gpui/src/platform/linux/x11/window.rs | 1 - 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 6b7cfd62d8341dfd7a6066bfb16f07b2ebdeeaa9..973e4a12886bf8aee1fab2a31806938d7198c55f 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -671,7 +671,7 @@ impl LinuxClient for WaylandClient { return; }; if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { - state.clipboard.set_primary(item.text); + state.clipboard.set_primary(item); let serial = state.serial_tracker.get(SerialKind::KeyPress); let data_source = primary_selection_manager.create_source(&state.globals.qh, ()); data_source.offer(state.clipboard.self_mime()); @@ -689,7 +689,7 @@ impl LinuxClient for WaylandClient { return; }; if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() { - state.clipboard.set(item.text); + state.clipboard.set(item); let serial = state.serial_tracker.get(SerialKind::KeyPress); let data_source = data_device_manager.create_data_source(&state.globals.qh, ()); data_source.offer(state.clipboard.self_mime()); @@ -699,25 +699,11 @@ impl LinuxClient for WaylandClient { } fn read_from_primary(&self) -> Option { - self.0 - .borrow_mut() - .clipboard - .read_primary() - .map(|s| crate::ClipboardItem { - text: s, - metadata: None, - }) + self.0.borrow_mut().clipboard.read_primary() } fn read_from_clipboard(&self) -> Option { - self.0 - .borrow_mut() - .clipboard - .read() - .map(|s| crate::ClipboardItem { - text: s, - metadata: None, - }) + self.0.borrow_mut().clipboard.read() } fn active_window(&self) -> Option { diff --git a/crates/gpui/src/platform/linux/wayland/clipboard.rs b/crates/gpui/src/platform/linux/wayland/clipboard.rs index ec1ced07fe1f557fe8deb36a041a51a6fb629908..3df06123afad8d2da33009faac2a81a123277f94 100644 --- a/crates/gpui/src/platform/linux/wayland/clipboard.rs +++ b/crates/gpui/src/platform/linux/wayland/clipboard.rs @@ -9,7 +9,7 @@ use filedescriptor::Pipe; use wayland_client::{protocol::wl_data_offer::WlDataOffer, Connection}; use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1; -use crate::{platform::linux::platform::read_fd, WaylandClientStatePtr}; +use crate::{platform::linux::platform::read_fd, ClipboardItem, WaylandClientStatePtr}; pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8"; pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; @@ -23,13 +23,13 @@ pub(crate) struct Clipboard { self_mime: String, // Internal clipboard - contents: Option, - primary_contents: Option, + contents: Option, + primary_contents: Option, // External clipboard - cached_read: Option, + cached_read: Option, current_offer: Option>, - cached_primary_read: Option, + cached_primary_read: Option, current_primary_offer: Option>, } @@ -89,12 +89,12 @@ impl Clipboard { } } - pub fn set(&mut self, text: String) { - self.contents = Some(text); + pub fn set(&mut self, item: ClipboardItem) { + self.contents = Some(item); } - pub fn set_primary(&mut self, text: String) { - self.primary_contents = Some(text); + pub fn set_primary(&mut self, item: ClipboardItem) { + self.primary_contents = Some(item); } pub fn set_offer(&mut self, data_offer: Option>) { @@ -113,17 +113,17 @@ impl Clipboard { pub fn send(&self, _mime_type: String, fd: OwnedFd) { if let Some(contents) = &self.contents { - self.send_internal(fd, contents.as_bytes().to_owned()); + self.send_internal(fd, contents.text.as_bytes().to_owned()); } } pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) { if let Some(primary_contents) = &self.primary_contents { - self.send_internal(fd, primary_contents.as_bytes().to_owned()); + self.send_internal(fd, primary_contents.text.as_bytes().to_owned()); } } - pub fn read(&mut self) -> Option { + pub fn read(&mut self) -> Option { let offer = self.current_offer.clone()?; if let Some(cached) = self.cached_read.clone() { return Some(cached); @@ -145,8 +145,8 @@ impl Clipboard { match unsafe { read_fd(fd) } { Ok(v) => { - self.cached_read = Some(v.clone()); - Some(v) + self.cached_read = Some(ClipboardItem::new(v)); + self.cached_read.clone() } Err(err) => { log::error!("error reading clipboard pipe: {err:?}"); @@ -155,7 +155,7 @@ impl Clipboard { } } - pub fn read_primary(&mut self) -> Option { + pub fn read_primary(&mut self) -> Option { let offer = self.current_primary_offer.clone()?; if let Some(cached) = self.cached_primary_read.clone() { return Some(cached); @@ -177,8 +177,8 @@ impl Clipboard { match unsafe { read_fd(fd) } { Ok(v) => { - self.cached_primary_read = Some(v.clone()); - Some(v) + self.cached_primary_read = Some(ClipboardItem::new(v.clone())); + self.cached_primary_read.clone() } Err(err) => { log::error!("error reading clipboard pipe: {err:?}"); diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index b630e5dfff37411fcc2e39477569270e66cb337f..347c597b714d27ad335a94bb9692c119e44b7589 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -29,9 +29,9 @@ use xkbcommon::xkb as xkbc; use crate::platform::linux::LinuxClient; use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ - modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, - Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point, - ScrollDelta, Size, TouchPhase, WindowParams, X11Window, + modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, + DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, + Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, }; use super::{ @@ -129,6 +129,7 @@ pub struct X11ClientState { pub(crate) common: LinuxCommon, pub(crate) clipboard: x11_clipboard::Clipboard, + pub(crate) clipboard_item: Option, } #[derive(Clone)] @@ -413,6 +414,7 @@ impl X11Client { scroll_y: None, clipboard, + clipboard_item: None, }))) } @@ -1097,7 +1099,7 @@ impl LinuxClient for X11Client { } fn write_to_clipboard(&self, item: crate::ClipboardItem) { - let state = self.0.borrow_mut(); + let mut state = self.0.borrow_mut(); state .clipboard .store( @@ -1106,6 +1108,7 @@ impl LinuxClient for X11Client { item.text().as_bytes(), ) .ok(); + state.clipboard_item.replace(item); } fn read_from_primary(&self) -> Option { @@ -1127,6 +1130,20 @@ impl LinuxClient for X11Client { fn read_from_clipboard(&self) -> Option { let state = self.0.borrow_mut(); + // if the last copy was from this app, return our cached item + // which has metadata attached. + if state + .clipboard + .setter + .connection + .get_selection_owner(state.clipboard.setter.atoms.clipboard) + .ok() + .and_then(|r| r.reply().ok()) + .map(|reply| reply.owner == state.clipboard.setter.window) + .unwrap_or(false) + { + return state.clipboard_item.clone(); + } state .clipboard .load( diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 4dea09393d523348081fe59bae4d88a5e5c55125..3497415613309ef0499476385c67ee73b8b2bf52 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -34,7 +34,6 @@ use std::{ }; use super::{X11Display, XINPUT_MASTER_DEVICE}; - x11rb::atom_manager! { pub XcbAtoms: AtomsCookie { UTF8_STRING,