mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 01:16:18 +04:00
Add system clipboard yank and paste commands
This commit adds six new commands to interact with system clipboard: - clipboard-yank - clipboard-yank-join - clipboard-paste-after - clipboard-paste-before - clipboard-paste-replace - show-clipboard-provider System clipboard provider is detected by checking a few environment variables and executables. Currently only built-in detection is supported. `clipboard-yank` will only yank the "main" selection, which is currently the first one. This will need to be revisited later. Closes https://github.com/helix-editor/helix/issues/76
This commit is contained in:
parent
d59c9f3baf
commit
a2b8cfca34
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -140,6 +140,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -360,6 +366,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1063,6 +1070,16 @@ version = "0.10.2+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -1280,6 +1280,96 @@ fn theme(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
|||||||
editor.set_theme_from_name(theme);
|
editor.set_theme_from_name(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn yank_main_selection_to_clipboard(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
// TODO: currently the main selection is the first one. This needs to be revisited later.
|
||||||
|
let range = doc
|
||||||
|
.selection(view.id)
|
||||||
|
.ranges()
|
||||||
|
.first()
|
||||||
|
.expect("at least one selection");
|
||||||
|
|
||||||
|
let value = range.fragment(doc.text().slice(..));
|
||||||
|
|
||||||
|
if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
|
||||||
|
log::error!("Couldn't set system clipboard content: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.set_status("yanked main selection to system clipboard".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
let values: Vec<String> = doc
|
||||||
|
.selection(view.id)
|
||||||
|
.fragments(doc.text().slice(..))
|
||||||
|
.map(Cow::into_owned)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let msg = format!(
|
||||||
|
"joined and yanked {} selection(s) to system clipboard",
|
||||||
|
values.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let joined = values.join("\n");
|
||||||
|
|
||||||
|
if let Err(e) = editor.clipboard_provider.set_contents(joined) {
|
||||||
|
log::error!("Couldn't set system clipboard content: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.set_status(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_impl(editor: &mut Editor, action: Paste) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
match editor
|
||||||
|
.clipboard_provider
|
||||||
|
.get_contents()
|
||||||
|
.map(|contents| paste_impl(&[contents], doc, view, action))
|
||||||
|
{
|
||||||
|
Ok(Some(transaction)) => {
|
||||||
|
doc.apply(&transaction, view.id);
|
||||||
|
doc.append_changes_to_history(view.id);
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_after(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
paste_clipboard_impl(editor, Paste::After);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_clipboard_before(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||||
|
paste_clipboard_impl(editor, Paste::After);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_selections_with_clipboard(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
match editor.clipboard_provider.get_contents() {
|
||||||
|
Ok(contents) => {
|
||||||
|
let transaction =
|
||||||
|
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
|
||||||
|
let max_to = doc.text().len_chars().saturating_sub(1);
|
||||||
|
let to = std::cmp::min(max_to, range.to() + 1);
|
||||||
|
(range.from(), to, Some(contents.as_str().into()))
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.apply(&transaction, view.id);
|
||||||
|
doc.append_changes_to_history(view.id);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_clipboard_provider(editor: &mut Editor, _: &[&str], _: PromptEvent) {
|
||||||
|
editor.set_status(editor.clipboard_provider.name().into());
|
||||||
|
}
|
||||||
|
|
||||||
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "quit",
|
name: "quit",
|
||||||
@ -1400,7 +1490,48 @@ fn theme(editor: &mut Editor, args: &[&str], event: PromptEvent) {
|
|||||||
fun: theme,
|
fun: theme,
|
||||||
completer: Some(completers::theme),
|
completer: Some(completers::theme),
|
||||||
},
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-yank",
|
||||||
|
alias: None,
|
||||||
|
doc: "Yank main selection into system clipboard.",
|
||||||
|
fun: yank_main_selection_to_clipboard,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-yank-join",
|
||||||
|
alias: None,
|
||||||
|
doc: "Yank joined selections into system clipboard.",
|
||||||
|
fun: yank_joined_to_clipboard,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-paste-after",
|
||||||
|
alias: None,
|
||||||
|
doc: "Paste system clipboard after selections.",
|
||||||
|
fun: paste_clipboard_after,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-paste-before",
|
||||||
|
alias: None,
|
||||||
|
doc: "Paste system clipboard before selections.",
|
||||||
|
fun: paste_clipboard_before,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "clipboard-paste-replace",
|
||||||
|
alias: None,
|
||||||
|
doc: "Replace selections with content of system clipboard.",
|
||||||
|
fun: replace_selections_with_clipboard,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "show-clipboard-provider",
|
||||||
|
alias: None,
|
||||||
|
doc: "Show clipboard provider name in status bar.",
|
||||||
|
fun: show_clipboard_provider,
|
||||||
|
completer: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
|
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
|
||||||
|
@ -34,3 +34,6 @@ slotmap = "1"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
log = "~0.4"
|
log = "~0.4"
|
||||||
|
|
||||||
|
which = "4.1"
|
||||||
|
|
||||||
|
193
helix-view/src/clipboard.rs
Normal file
193
helix-view/src/clipboard.rs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub trait ClipboardProvider: std::fmt::Debug {
|
||||||
|
fn name(&self) -> Cow<str>;
|
||||||
|
fn get_contents(&self) -> Result<String>;
|
||||||
|
fn set_contents(&self, contents: String) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! command_provider {
|
||||||
|
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $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 ),* ],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||||
|
// TODO: support for user-defined provider, probably when we have plugin support by setting a
|
||||||
|
// variable?
|
||||||
|
|
||||||
|
if exists("pbcopy") && exists("pbpaste") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "pbpaste";
|
||||||
|
copy => "pbcopy";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "wl-paste", "--no-newline";
|
||||||
|
copy => "wl-copy", "--foreground", "--type", "text/plain";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("DISPLAY") && exists("xclip") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "xclip", "-o", "-selection", "clipboard";
|
||||||
|
copy => "xclip", "-i", "-selection", "clipboard";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
|
||||||
|
{
|
||||||
|
// FIXME: check performance of is_exit_success
|
||||||
|
command_provider! {
|
||||||
|
paste => "xsel", "-o", "-b";
|
||||||
|
copy => "xsel", "--nodetach", "-i", "-b";
|
||||||
|
}
|
||||||
|
} else if exists("lemonade") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "lemonade", "paste";
|
||||||
|
copy => "lemonade", "copy";
|
||||||
|
}
|
||||||
|
} else if exists("doitclient") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "doitclient", "wclip", "-r";
|
||||||
|
copy => "doitclient", "wclip";
|
||||||
|
}
|
||||||
|
} else if exists("win32yank.exe") {
|
||||||
|
// FIXME: does it work within WSL?
|
||||||
|
command_provider! {
|
||||||
|
paste => "win32yank.exe", "-o", "--lf";
|
||||||
|
copy => "win32yank.exe", "-i", "--crlf";
|
||||||
|
}
|
||||||
|
} else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "termux-clipboard-get";
|
||||||
|
copy => "termux-clipboard-set";
|
||||||
|
}
|
||||||
|
} else if env_var_is_set("TMUX") && exists("tmux") {
|
||||||
|
command_provider! {
|
||||||
|
paste => "tmux", "save-buffer", "-";
|
||||||
|
copy => "tmux", "load-buffer", "-";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box::new(provider::NopProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(executable_name: &str) -> bool {
|
||||||
|
which::which(executable_name).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_var_is_set(env_var_name: &str) -> bool {
|
||||||
|
std::env::var_os(env_var_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_exit_success(program: &str, args: &[&str]) -> bool {
|
||||||
|
std::process::Command::new(program)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
mod provider {
|
||||||
|
use super::ClipboardProvider;
|
||||||
|
use anyhow::{bail, Context as _, Result};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NopProvider;
|
||||||
|
|
||||||
|
impl ClipboardProvider for NopProvider {
|
||||||
|
fn name(&self) -> Cow<str> {
|
||||||
|
Cow::Borrowed("none")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_contents(&self) -> Result<String> {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_contents(&self, _: String) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandConfig {
|
||||||
|
pub prg: &'static str,
|
||||||
|
pub args: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandConfig {
|
||||||
|
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
|
||||||
|
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
|
||||||
|
|
||||||
|
let mut child = Command::new(self.prg)
|
||||||
|
.args(self.args)
|
||||||
|
.stdin(stdin)
|
||||||
|
.stdout(stdout)
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
if let Some(input) = input {
|
||||||
|
let mut stdin = child.stdin.take().context("stdin is missing")?;
|
||||||
|
stdin
|
||||||
|
.write_all(input.as_bytes())
|
||||||
|
.context("couldn't write in stdin")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add timer?
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!("clipboard provider {} failed", self.prg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if pipe_output {
|
||||||
|
Ok(Some(String::from_utf8(output.stdout)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandProvider {
|
||||||
|
pub get_cmd: CommandConfig,
|
||||||
|
pub set_cmd: CommandConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipboardProvider for CommandProvider {
|
||||||
|
fn name(&self) -> Cow<str> {
|
||||||
|
if self.get_cmd.prg != self.set_cmd.prg {
|
||||||
|
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(self.get_cmd.prg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_contents(&self) -> Result<String> {
|
||||||
|
let output = self
|
||||||
|
.get_cmd
|
||||||
|
.execute(None, true)?
|
||||||
|
.context("output is missing")?;
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_contents(&self, value: String) -> Result<()> {
|
||||||
|
self.set_cmd.execute(Some(&value), false).map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::clipboard::{get_clipboard_provider, ClipboardProvider};
|
||||||
use crate::{
|
use crate::{
|
||||||
theme::{self, Theme},
|
theme::{self, Theme},
|
||||||
tree::Tree,
|
tree::Tree,
|
||||||
@ -14,9 +15,10 @@
|
|||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
|
||||||
|
use helix_core::Position;
|
||||||
|
|
||||||
pub use helix_core::diagnostic::Severity;
|
pub use helix_core::diagnostic::Severity;
|
||||||
pub use helix_core::register::Registers;
|
pub use helix_core::register::Registers;
|
||||||
use helix_core::Position;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
@ -27,6 +29,7 @@ pub struct Editor {
|
|||||||
pub registers: Registers,
|
pub registers: Registers,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub language_servers: helix_lsp::Registry,
|
pub language_servers: helix_lsp::Registry,
|
||||||
|
pub clipboard_provider: Box<dyn ClipboardProvider>,
|
||||||
|
|
||||||
pub syn_loader: Arc<syntax::Loader>,
|
pub syn_loader: Arc<syntax::Loader>,
|
||||||
pub theme_loader: Arc<theme::Loader>,
|
pub theme_loader: Arc<theme::Loader>,
|
||||||
@ -62,6 +65,7 @@ pub fn new(
|
|||||||
syn_loader: config_loader,
|
syn_loader: config_loader,
|
||||||
theme_loader: themes,
|
theme_loader: themes,
|
||||||
registers: Registers::default(),
|
registers: Registers::default(),
|
||||||
|
clipboard_provider: get_clipboard_provider(),
|
||||||
status_msg: None,
|
status_msg: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
|
||||||
|
pub mod clipboard;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod register_selection;
|
pub mod register_selection;
|
||||||
|
Loading…
Reference in New Issue
Block a user