Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/warp_tui/src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ fn set_native_text(text: &str) -> anyhow::Result<()> {
anyhow::bail!("native OS clipboard is not supported on this platform")
}

/// Reads the OS clipboard's plain text for a local paste (`cmd-v`).
///
/// This reuses the shared native clipboard backend (`arboard` via
/// `warpui::platform::create_system_clipboard`). Unlike [`copy_to_clipboard`],
/// there is no OSC 52 fallback: terminals do not reliably answer OSC 52 read
/// requests, so a remote/SSH `cmd-v` still relies on the terminal's own
/// bracketed-paste path (`TuiEditorAction::PasteText`). Returns an error only
/// when the native backend is unavailable.
pub(crate) fn read_from_clipboard() -> anyhow::Result<String> {
let mut clipboard = warpui::platform::create_system_clipboard()
.map_err(|error| error.context("native OS clipboard is unavailable"))?;
Ok(clipboard.read().plain_text)
}

fn write_osc52_sequences(text: &str, in_tmux: bool, writer: &mut impl Write) -> io::Result<()> {
let sequence = osc52_sequences(text, in_tmux);
writer.write_all(sequence.as_bytes())?;
Expand Down
91 changes: 82 additions & 9 deletions crates/warp_tui/src/editor_interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use warp_editor::selection::{TextDirection, TextUnit};
use warpui_core::text::word_boundaries::WordBoundariesPolicy;
use warpui_core::{AppContext, ModelHandle};

use crate::clipboard::copy_to_clipboard;
use crate::clipboard::{copy_to_clipboard, read_from_clipboard};
use crate::editor_element::TuiEditorAction;

/// Editing commands shared by TUI text fields.
Expand All @@ -33,9 +33,12 @@ pub enum TuiEditorCommand {
SelectDown,
SelectWordLeft,
SelectWordRight,
SelectToLineStart,
SelectToLineEnd,
SelectAll,
Copy,
Cut,
Paste,
KillToLineEnd,
KillToLineStart,
Yank,
Expand Down Expand Up @@ -120,6 +123,8 @@ pub(crate) enum TuiEditorInteractionOutcome {
FollowCursor,
PreserveViewport,
Clipboard(TuiEditorClipboardAction),
/// Insert the OS clipboard's text at the cursor (`cmd-v` / paste).
Paste,
}

/// Clipboard operation requested by a shared TUI editor command.
Expand Down Expand Up @@ -236,14 +241,14 @@ const SHARED_EDITOR_BINDINGS: &[EditorBindingSpec] = &[
input_name: Some("tui:input:move_to_line_start"),
editor_name: Some("tui:editor:move_to_line_start"),
description: "Move cursor to start of line",
keys: &["home", "ctrl-a"],
keys: &["home", "ctrl-a", "cmd-left"],
},
EditorBindingSpec {
command: TuiEditorCommand::MoveToLineEnd,
input_name: Some("tui:input:move_to_line_end"),
editor_name: Some("tui:editor:move_to_line_end"),
description: "Move cursor to end of line",
keys: &["end", "ctrl-e"],
keys: &["end", "ctrl-e", "cmd-right"],
},
EditorBindingSpec {
command: TuiEditorCommand::SelectLeft,
Expand Down Expand Up @@ -287,26 +292,47 @@ const SHARED_EDITOR_BINDINGS: &[EditorBindingSpec] = &[
description: "Extend selection one word right",
keys: &["ctrl-shift-right", "alt-shift-right"],
},
EditorBindingSpec {
command: TuiEditorCommand::SelectToLineStart,
input_name: Some("tui:input:select_to_line_start"),
editor_name: Some("tui:editor:select_to_line_start"),
description: "Extend selection to start of line",
keys: &["cmd-shift-left"],
},
EditorBindingSpec {
command: TuiEditorCommand::SelectToLineEnd,
input_name: Some("tui:input:select_to_line_end"),
editor_name: Some("tui:editor:select_to_line_end"),
description: "Extend selection to end of line",
keys: &["cmd-shift-right"],
},
EditorBindingSpec {
command: TuiEditorCommand::SelectAll,
input_name: Some("tui:input:select_all"),
editor_name: Some("tui:editor:select_all"),
description: "Select all text",
keys: &["ctrl-shift-A"],
keys: &["ctrl-shift-A", "cmd-a"],
},
EditorBindingSpec {
command: TuiEditorCommand::Copy,
input_name: Some("tui:input:copy"),
editor_name: Some("tui:editor:copy"),
description: "Copy selected text",
keys: &["ctrl-shift-C", "alt-w"],
keys: &["ctrl-shift-C", "alt-w", "cmd-c"],
},
EditorBindingSpec {
command: TuiEditorCommand::Cut,
input_name: Some("tui:input:cut"),
editor_name: Some("tui:editor:cut"),
description: "Cut selected text",
keys: &["ctrl-x"],
keys: &["ctrl-x", "cmd-x"],
},
EditorBindingSpec {
command: TuiEditorCommand::Paste,
input_name: Some("tui:input:paste"),
editor_name: Some("tui:editor:paste"),
description: "Paste text from the clipboard",
keys: &["cmd-v"],
},
EditorBindingSpec {
command: TuiEditorCommand::KillToLineEnd,
Expand All @@ -320,7 +346,7 @@ const SHARED_EDITOR_BINDINGS: &[EditorBindingSpec] = &[
input_name: Some("tui:input:kill_to_line_start"),
editor_name: Some("tui:editor:kill_to_line_start"),
description: "Delete to start of line",
keys: &["ctrl-u"],
keys: &["ctrl-u", "cmd-backspace"],
},
EditorBindingSpec {
command: TuiEditorCommand::Yank,
Expand All @@ -334,14 +360,14 @@ const SHARED_EDITOR_BINDINGS: &[EditorBindingSpec] = &[
input_name: Some("tui:input:undo"),
editor_name: Some("tui:editor:undo"),
description: "Undo",
keys: &["ctrl-z"],
keys: &["ctrl-z", "cmd-z"],
},
EditorBindingSpec {
command: TuiEditorCommand::Redo,
input_name: Some("tui:input:redo"),
editor_name: Some("tui:editor:redo"),
description: "Redo",
keys: &["ctrl-shift-Z"],
keys: &["ctrl-shift-Z", "cmd-shift-Z"],
},
];

Expand Down Expand Up @@ -478,6 +504,12 @@ impl TuiEditorState {
);
});
}
TuiEditorCommand::SelectToLineStart => {
model.update(ctx, |model, ctx| model.select_to_line_start(ctx));
}
TuiEditorCommand::SelectToLineEnd => {
model.update(ctx, |model, ctx| model.select_to_line_end(ctx));
}
TuiEditorCommand::SelectAll => {
model.update(ctx, |model, ctx| model.select_all(ctx));
}
Expand All @@ -487,6 +519,9 @@ impl TuiEditorState {
TuiEditorCommand::Cut => {
return TuiEditorInteractionOutcome::Clipboard(TuiEditorClipboardAction::Cut);
}
TuiEditorCommand::Paste => {
return TuiEditorInteractionOutcome::Paste;
}
TuiEditorCommand::KillToLineEnd => {
if let Some(killed) = model.update(ctx, |model, ctx| {
model.kill_to_char_cell_visual_row_end(ctx)
Expand Down Expand Up @@ -559,6 +594,44 @@ pub(crate) fn apply_editor_clipboard_action_for_test(
) -> anyhow::Result<bool> {
apply_editor_clipboard_action_with(model, action, copy, ctx)
}

/// Reads the OS clipboard and inserts its text at the cursor, applying the
/// editor's line policy. Returns `false` when the clipboard has no text.
pub(crate) fn apply_editor_paste(
model: &ModelHandle<CodeEditorModel>,
behavior: TuiEditorBehavior,
ctx: &mut AppContext,
) -> anyhow::Result<bool> {
apply_editor_paste_with(model, read_from_clipboard, behavior, ctx)
}

fn apply_editor_paste_with(
model: &ModelHandle<CodeEditorModel>,
read: impl FnOnce() -> anyhow::Result<String>,
behavior: TuiEditorBehavior,
ctx: &mut AppContext,
) -> anyhow::Result<bool> {
let text = read()?;
if text.is_empty() {
return Ok(false);
}
let text = behavior.normalize_text(&text).to_owned();
if text.is_empty() {
return Ok(false);
}
model.update(ctx, |model, ctx| model.user_insert(&text, ctx));
Ok(true)
}

#[cfg(test)]
pub(crate) fn apply_editor_paste_for_test(
model: &ModelHandle<CodeEditorModel>,
read: impl FnOnce() -> anyhow::Result<String>,
behavior: TuiEditorBehavior,
ctx: &mut AppContext,
) -> anyhow::Result<bool> {
apply_editor_paste_with(model, read, behavior, ctx)
}
/// Applies an element-originated action and reports the required viewport work.
pub(crate) fn apply_editor_action(
model: &ModelHandle<CodeEditorModel>,
Expand Down
8 changes: 7 additions & 1 deletion crates/warp_tui/src/editor_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use warpui_core::{
use crate::editor_element::{TuiEditorAction, TuiEditorElement};
use crate::editor_interaction::{
TuiEditorBehavior, TuiEditorCommand, TuiEditorInteractionOutcome, TuiEditorState,
apply_editor_action, apply_editor_clipboard_action, follow_editor_cursor,
apply_editor_action, apply_editor_clipboard_action, apply_editor_paste, follow_editor_cursor,
};

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -238,6 +238,12 @@ impl TypedActionView for TuiEditorView {
}
TuiEditorInteractionOutcome::FollowCursor
}
TuiEditorInteractionOutcome::Paste => {
if let Err(error) = apply_editor_paste(&self.model, self.editor_behavior, ctx) {
log::error!("Failed to paste into TUI editor: {error}");
}
TuiEditorInteractionOutcome::FollowCursor
}
outcome => outcome,
};
if outcome == TuiEditorInteractionOutcome::FollowCursor {
Expand Down
Loading
Loading