diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0acb57e2..2b1b859b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -12,8 +12,8 @@ use helix_core::{ }; use helix_view::{ - document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode, view::View, Document, - DocumentId, Editor, ViewId, + clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode, + view::View, Document, DocumentId, Editor, ViewId, }; use anyhow::{anyhow, bail, Context as _}; @@ -258,12 +258,17 @@ impl Command { yank, "Yank selection", yank_joined_to_clipboard, "Join and yank selections to clipboard", yank_main_selection_to_clipboard, "Yank main selection to clipboard", + yank_joined_to_primary_clipboard, "Join and yank selections to primary clipboard", + yank_main_selection_to_primary_clipboard, "Yank main selection to primary clipboard", replace_with_yanked, "Replace with yanked text", replace_selections_with_clipboard, "Replace selections by clipboard content", + replace_selections_with_primary_clipboard, "Replace selections by primary clipboard content", paste_after, "Paste after selection", paste_before, "Paste before selection", paste_clipboard_after, "Paste clipboard after selections", paste_clipboard_before, "Paste clipboard before selections", + paste_primary_clipboard_after, "Paste primary clipboard after selections", + paste_primary_clipboard_before, "Paste primary clipboard before selections", indent, "Indent selection", unindent, "Unindent selection", format_selections, "Format selection", @@ -1705,7 +1710,7 @@ mod cmd { _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - yank_main_selection_to_clipboard_impl(&mut cx.editor) + yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard) } fn yank_joined_to_clipboard( @@ -1718,7 +1723,28 @@ mod cmd { .first() .copied() .unwrap_or_else(|| doc.line_ending.as_str()); - yank_joined_to_clipboard_impl(&mut cx.editor, separator) + yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard) + } + + fn yank_main_selection_to_primary_clipboard( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection) + } + + fn yank_joined_to_primary_clipboard( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let (_, doc) = current!(cx.editor); + let separator = args + .first() + .copied() + .unwrap_or_else(|| doc.line_ending.as_str()); + yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection) } fn paste_clipboard_after( @@ -1726,7 +1752,7 @@ mod cmd { _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(&mut cx.editor, Paste::After) + paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard) } fn paste_clipboard_before( @@ -1734,17 +1760,32 @@ mod cmd { _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(&mut cx.editor, Paste::After) + paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard) } - fn replace_selections_with_clipboard( + fn paste_primary_clipboard_after( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, + ) -> anyhow::Result<()> { + paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection) + } + + fn paste_primary_clipboard_before( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection) + } + + fn replace_selections_with_clipboard_impl( + cx: &mut compositor::Context, + clipboard_type: ClipboardType, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); - match cx.editor.clipboard_provider.get_contents() { + match cx.editor.clipboard_provider.get_contents(clipboard_type) { Ok(contents) => { let selection = doc.selection(view.id); let transaction = @@ -1760,13 +1801,29 @@ mod cmd { } } + fn replace_selections_with_clipboard( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard) + } + + fn replace_selections_with_primary_clipboard( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + replace_selections_with_clipboard_impl(cx, ClipboardType::Selection) + } + fn show_clipboard_provider( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor - .set_status(cx.editor.clipboard_provider.name().into()); + .set_status(cx.editor.clipboard_provider.name().to_string()); Ok(()) } @@ -1967,6 +2024,20 @@ mod cmd { fun: yank_joined_to_clipboard, completer: None, }, + TypableCommand { + name: "primary-clipboard-yank", + alias: None, + doc: "Yank main selection into system primary clipboard.", + fun: yank_main_selection_to_primary_clipboard, + completer: None, + }, + TypableCommand { + name: "primary-clipboard-yank-join", + alias: None, + doc: "Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc. + fun: yank_joined_to_primary_clipboard, + completer: None, + }, TypableCommand { name: "clipboard-paste-after", alias: None, @@ -1988,6 +2059,27 @@ mod cmd { fun: replace_selections_with_clipboard, completer: None, }, + TypableCommand { + name: "primary-clipboard-paste-after", + alias: None, + doc: "Paste primary clipboard after selections.", + fun: paste_primary_clipboard_after, + completer: None, + }, + TypableCommand { + name: "primary-clipboard-paste-before", + alias: None, + doc: "Paste primary clipboard before selections.", + fun: paste_primary_clipboard_before, + completer: None, + }, + TypableCommand { + name: "primary-clipboard-paste-replace", + alias: None, + doc: "Replace selections with content of system primary clipboard.", + fun: replace_selections_with_primary_clipboard, + completer: None, + }, TypableCommand { name: "show-clipboard-provider", alias: None, @@ -3209,7 +3301,11 @@ fn yank(cx: &mut Context) { exit_select_mode(cx); } -fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) -> anyhow::Result<()> { +fn yank_joined_to_clipboard_impl( + editor: &mut Editor, + separator: &str, + clipboard_type: ClipboardType, +) -> anyhow::Result<()> { let (view, doc) = current!(editor); let text = doc.text().slice(..); @@ -3228,7 +3324,7 @@ fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) -> anyhow editor .clipboard_provider - .set_contents(joined) + .set_contents(joined, clipboard_type) .context("Couldn't set system clipboard content")?; editor.set_status(msg); @@ -3238,17 +3334,27 @@ fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) -> anyhow fn yank_joined_to_clipboard(cx: &mut Context) { let line_ending = current!(cx.editor).1.line_ending; - let _ = yank_joined_to_clipboard_impl(&mut cx.editor, line_ending.as_str()); + let _ = yank_joined_to_clipboard_impl( + &mut cx.editor, + line_ending.as_str(), + ClipboardType::Clipboard, + ); exit_select_mode(cx); } -fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) -> anyhow::Result<()> { +fn yank_main_selection_to_clipboard_impl( + editor: &mut Editor, + clipboard_type: ClipboardType, +) -> anyhow::Result<()> { let (view, doc) = current!(editor); let text = doc.text().slice(..); let value = doc.selection(view.id).primary().fragment(text); - if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) { + if let Err(e) = editor + .clipboard_provider + .set_contents(value.into_owned(), clipboard_type) + { bail!("Couldn't set system clipboard content: {:?}", e); } @@ -3257,7 +3363,20 @@ fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) -> anyhow::Result< } fn yank_main_selection_to_clipboard(cx: &mut Context) { - let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor); + let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard); +} + +fn yank_joined_to_primary_clipboard(cx: &mut Context) { + let line_ending = current!(cx.editor).1.line_ending; + let _ = yank_joined_to_clipboard_impl( + &mut cx.editor, + line_ending.as_str(), + ClipboardType::Selection, + ); +} + +fn yank_main_selection_to_primary_clipboard(cx: &mut Context) { + let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection); exit_select_mode(cx); } @@ -3310,12 +3429,16 @@ fn paste_impl( Some(transaction) } -fn paste_clipboard_impl(editor: &mut Editor, action: Paste) -> anyhow::Result<()> { +fn paste_clipboard_impl( + editor: &mut Editor, + action: Paste, + clipboard_type: ClipboardType, +) -> anyhow::Result<()> { let (view, doc) = current!(editor); match editor .clipboard_provider - .get_contents() + .get_contents(clipboard_type) .map(|contents| paste_impl(&[contents], doc, view, action)) { Ok(Some(transaction)) => { @@ -3329,11 +3452,19 @@ fn paste_clipboard_impl(editor: &mut Editor, action: Paste) -> anyhow::Result<() } fn paste_clipboard_after(cx: &mut Context) { - let _ = paste_clipboard_impl(&mut cx.editor, Paste::After); + let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard); } fn paste_clipboard_before(cx: &mut Context) { - let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before); + let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard); +} + +fn paste_primary_clipboard_after(cx: &mut Context) { + let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection); +} + +fn paste_primary_clipboard_before(cx: &mut Context) { + let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection); } fn replace_with_yanked(cx: &mut Context) { @@ -3358,10 +3489,13 @@ fn replace_with_yanked(cx: &mut Context) { } } -fn replace_selections_with_clipboard_impl(editor: &mut Editor) -> anyhow::Result<()> { +fn replace_selections_with_clipboard_impl( + editor: &mut Editor, + clipboard_type: ClipboardType, +) -> anyhow::Result<()> { let (view, doc) = current!(editor); - match editor.clipboard_provider.get_contents() { + match editor.clipboard_provider.get_contents(clipboard_type) { Ok(contents) => { let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { @@ -3377,7 +3511,11 @@ fn replace_selections_with_clipboard_impl(editor: &mut Editor) -> anyhow::Result } fn replace_selections_with_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(&mut cx.editor); + let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard); +} + +fn replace_selections_with_primary_clipboard(cx: &mut Context) { + let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection); } fn paste_after(cx: &mut Context) { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index aa21a389..96a4afe8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -785,6 +785,61 @@ impl EditorView { EventResult::Consumed(None) } + + MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + .. + } => { + if !cxt.editor.config.middle_click_paste { + return EventResult::Ignored; + } + + let (view, doc) = current!(cxt.editor); + let range = doc.selection(view.id).primary(); + + if range.to() - range.from() <= 1 { + return EventResult::Ignored; + } + + commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt); + + EventResult::Consumed(None) + } + + MouseEvent { + kind: MouseEventKind::Up(MouseButton::Middle), + row, + column, + modifiers, + .. + } => { + let editor = &mut cxt.editor; + if !editor.config.middle_click_paste { + return EventResult::Ignored; + } + + if modifiers == crossterm::event::KeyModifiers::ALT { + commands::Command::replace_selections_with_primary_clipboard.execute(cxt); + + return EventResult::Consumed(None); + } + + let result = editor.tree.views().find_map(|(view, _focus)| { + view.pos_at_screen_coords(&editor.documents[view.doc], row, column) + .map(|pos| (pos, view.id)) + }); + + if let Some((pos, view_id)) = result { + let doc = &mut editor.documents[editor.tree.get(view_id).doc]; + doc.set_selection(view_id, Selection::point(pos)); + editor.tree.focus = view_id; + commands::Command::paste_primary_clipboard_before.execute(cxt); + return EventResult::Consumed(None); + } + + EventResult::Ignored + } + _ => EventResult::Ignored, } } diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index 401c0459..3778c8d4 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -3,10 +3,15 @@ use anyhow::Result; use std::borrow::Cow; +pub enum ClipboardType { + Clipboard, + Selection, +} + pub trait ClipboardProvider: std::fmt::Debug { fn name(&self) -> Cow; - fn get_contents(&self) -> Result; - fn set_contents(&self, contents: String) -> Result<()>; + fn get_contents(&self, clipboard_type: ClipboardType) -> Result; + fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>; } macro_rules! command_provider { @@ -20,6 +25,33 @@ macro_rules! command_provider { prg: $set_prg, args: &[ $( $set_arg ),* ], }, + get_primary_cmd: None, + set_primary_cmd: None, + }) + }}; + + (paste => $get_prg:literal $( , $get_arg:literal )* ; + copy => $set_prg:literal $( , $set_arg:literal )* ; + primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ; + primary_copy => $pr_set_prg:literal $( , $pr_set_arg:literal )* ; + ) => {{ + Box::new(provider::CommandProvider { + get_cmd: provider::CommandConfig { + prg: $get_prg, + args: &[ $( $get_arg ),* ], + }, + set_cmd: provider::CommandConfig { + prg: $set_prg, + args: &[ $( $set_arg ),* ], + }, + get_primary_cmd: Some(provider::CommandConfig { + prg: $pr_get_prg, + args: &[ $( $pr_get_arg ),* ], + }), + set_primary_cmd: Some(provider::CommandConfig { + prg: $pr_set_prg, + args: &[ $( $pr_set_arg ),* ], + }), }) }}; } @@ -37,11 +69,15 @@ pub fn get_clipboard_provider() -> Box { command_provider! { paste => "wl-paste", "--no-newline"; copy => "wl-copy", "--type", "text/plain"; + primary_paste => "wl-paste", "-p", "--no-newline"; + primary_copy => "wl-copy", "-p", "--type", "text/plain"; } } else if env_var_is_set("DISPLAY") && exists("xclip") { command_provider! { paste => "xclip", "-o", "-selection", "clipboard"; copy => "xclip", "-i", "-selection", "clipboard"; + primary_paste => "xclip", "-o"; + primary_copy => "xclip", "-i"; } } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"]) { @@ -49,6 +85,8 @@ pub fn get_clipboard_provider() -> Box { command_provider! { paste => "xsel", "-o", "-b"; copy => "xsel", "--nodetach", "-i", "-b"; + primary_paste => "xsel", "-o"; + primary_copy => "xsel", "-i"; } } else if exists("lemonade") { command_provider! { @@ -78,10 +116,10 @@ pub fn get_clipboard_provider() -> Box { } } else { #[cfg(target_os = "windows")] - return Box::new(provider::WindowsProvider); + return Box::new(provider::WindowsProvider::new()); #[cfg(not(target_os = "windows"))] - return Box::new(provider::NopProvider); + return Box::new(provider::NopProvider::new()); } } @@ -103,30 +141,62 @@ fn is_exit_success(program: &str, args: &[&str]) -> bool { } mod provider { - use super::ClipboardProvider; + use super::{ClipboardProvider, ClipboardType}; use anyhow::{bail, Context as _, Result}; use std::borrow::Cow; #[derive(Debug)] - pub struct NopProvider; + pub struct NopProvider { + buf: String, + primary_buf: String, + } + + impl NopProvider { + pub fn new() -> Self { + Self { + buf: String::new(), + primary_buf: String::new(), + } + } + } impl ClipboardProvider for NopProvider { fn name(&self) -> Cow { Cow::Borrowed("none") } - fn get_contents(&self) -> Result { - Ok(String::new()) + fn get_contents(&self, clipboard_type: ClipboardType) -> Result { + let value = match clipboard_type { + ClipboardType::Clipboard => self.buf.clone(), + ClipboardType::Selection => self.primary_buf.clone(), + }; + + Ok(value) } - fn set_contents(&self, _: String) -> Result<()> { + fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> { + match clipboard_type { + ClipboardType::Clipboard => self.buf = content, + ClipboardType::Selection => self.primary_buf = content, + } Ok(()) } } #[cfg(target_os = "windows")] #[derive(Debug)] - pub struct WindowsProvider; + pub struct WindowsProvider { + selection_buf: String, + } + + #[cfg(target_os = "windows")] + impl WindowsProvider { + pub fn new() -> Self { + Self { + selection_buf: String::new(), + } + } + } #[cfg(target_os = "windows")] impl ClipboardProvider for WindowsProvider { @@ -134,13 +204,23 @@ mod provider { Cow::Borrowed("clipboard-win") } - fn get_contents(&self) -> Result { - let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; - Ok(contents) + fn get_contents(&self, clipboard_type: ClipboardType) -> Result { + match clipboard_type { + ClipboardType::Clipboard => { + let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; + Ok(contents) + } + ClipboardType::Selection => Ok(String::new()), + } } - fn set_contents(&self, contents: String) -> Result<()> { - clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?; + fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> { + match clipboard_type { + ClipboardType::Clipboard => { + clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents); + } + ClipboardType::Selection => {} + }; Ok(()) } } @@ -192,6 +272,8 @@ mod provider { pub struct CommandProvider { pub get_cmd: CommandConfig, pub set_cmd: CommandConfig, + pub get_primary_cmd: Option, + pub set_primary_cmd: Option, } impl ClipboardProvider for CommandProvider { @@ -203,16 +285,34 @@ mod provider { } } - fn get_contents(&self) -> Result { - let output = self - .get_cmd - .execute(None, true)? - .context("output is missing")?; - Ok(output) + fn get_contents(&self, clipboard_type: ClipboardType) -> Result { + match clipboard_type { + ClipboardType::Clipboard => Ok(self + .get_cmd + .execute(None, true)? + .context("output is missing")?), + ClipboardType::Selection => { + if let Some(cmd) = &self.get_primary_cmd { + return cmd.execute(None, true)?.context("output is missing"); + } + + Ok(String::new()) + } + } } - fn set_contents(&self, value: String) -> Result<()> { - self.set_cmd.execute(Some(&value), false).map(|_| ()) + fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> { + let cmd = match clipboard_type { + ClipboardType::Clipboard => &self.set_cmd, + ClipboardType::Selection => { + if let Some(cmd) = &self.set_primary_cmd { + cmd + } else { + return Ok(()); + } + } + }; + cmd.execute(Some(&value), false).map(|_| ()) } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ec3cedd6..9b7f8429 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -29,6 +29,8 @@ pub struct Config { pub scroll_lines: isize, /// Mouse support. Defaults to true. pub mouse: bool, + /// Middle click paste support. Defaults to true + pub middle_click_paste: bool, } impl Default for Config { @@ -37,6 +39,7 @@ impl Default for Config { scrolloff: 5, scroll_lines: 3, mouse: true, + middle_click_paste: true, } } }