From 0753e908fb3b30b531e8ddd5c8a52e26de90cc1f Mon Sep 17 00:00:00 2001 From: Muhammad-Owais-Warsi Date: Fri, 25 Sep 2026 22:03:52 +0530 Subject: [PATCH] input: Paste from the context menu on the web backend On wasm32 the synchronous `read_from_clipboard()` is always `None` by design; the real read is `read_from_clipboard_async()`. The Input context menu gated Paste on the synchronous read, so it was permanently disabled there, and the `Paste` action bailed out on the same read, so un-greying it alone would have inserted nothing. - Offer Paste whenever the input is editable, without peeking at the clipboard, matching the touch-selection edit menu. - Let the `Paste` action fall back to the asynchronous clipboard read when the synchronous one is empty. The read starts inside the action so it stays under the user activation the browser requires. Fixes #3187 Co-authored-by: Cursor --- crates/base/src/input/base/state.rs | 34 +++++++++++++++++++++++++++-- crates/component/src/input/input.rs | 13 +++++++---- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/base/src/input/base/state.rs b/crates/base/src/input/base/state.rs index 722a78c0cd..771e365ffd 100644 --- a/crates/base/src/input/base/state.rs +++ b/crates/base/src/input/base/state.rs @@ -2627,9 +2627,39 @@ impl InputBaseState { if !self.is_editable() { return; } - let Some(clipboard) = cx.read_from_clipboard() else { + if let Some(clipboard) = cx.read_from_clipboard() { + self.insert_clipboard(clipboard, window, cx); return; - }; + } + // The synchronous read is empty on platforms whose clipboard is + // asynchronous and permission-gated (the web), so fall back to the + // real read. It has to start here, still inside the user activation + // that dispatched `Paste`, or the browser refuses the read. + let read = cx.read_from_clipboard_async(); + cx.spawn_in(window, async move |this, cx| match read.await { + Ok(Some(clipboard)) => { + this.update_in(cx, |this, window, cx| { + if this.is_editable() { + this.insert_clipboard(clipboard, window, cx); + } + }) + .ok(); + } + Ok(None) => {} + Err(error) => tracing::warn!("failed to read the clipboard for paste: {error}"), + }) + .detach(); + } + + /// Inserts the clipboard text the way `Paste` does: one atomic edit, + /// newlines dropped on a single-line input, one line per selection when + /// the counts match on a multi-line one. + fn insert_clipboard( + &mut self, + clipboard: ClipboardItem, + window: &mut Window, + cx: &mut Context, + ) { let mut new_text = clipboard.text().unwrap_or_default(); // A paste is one atomic edit, never part of a typing run. self.undo_manager.set_pending_intent(EditIntent::Atomic); diff --git a/crates/component/src/input/input.rs b/crates/component/src/input/input.rs index 69af2aa802..1419b3f5a9 100644 --- a/crates/component/src/input/input.rs +++ b/crates/component/src/input/input.rs @@ -374,9 +374,11 @@ impl Input { /// belong in app-owned state beside the input (e.g. `Attachment`s), never /// inside it. /// - /// Known limit: on web `read_from_clipboard()` is `None` (text arrives - /// through the platform input handler); image paste there needs - /// `read_from_clipboard_async` and permission, out of scope here. + /// Known limit: on web `read_from_clipboard()` is `None`, so the handler + /// is skipped there and the input inserts the plain text itself (a + /// keyboard paste arrives through the platform input handler, a menu + /// paste through the asynchronous clipboard read); image paste there + /// needs `read_from_clipboard_async` and permission, out of scope here. pub fn on_paste( mut self, handler: impl Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool + 'static, @@ -619,9 +621,12 @@ impl RenderOnce for Input { !capabilities.is_copyable(), Box::new(gpui_base::input::Copy), ) + // Offered whenever the text can change, without peeking + // at the clipboard: the synchronous read is always empty + // on the web, and an empty clipboard pastes nothing. .menu_with_disabled( t!("Input.Paste"), - !(editable && cx.read_from_clipboard().is_some()), + !editable, Box::new(gpui_base::input::Paste), ) .separator()