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()