From e6670a5413c86fc816ab0b56d90b86b06b6bfdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Anh=20Khoa?= Date: Fri, 21 Jan 2022 04:04:50 +0700 Subject: [PATCH] Remote copy/paste (#1003) * add clipboard-rs * set g:clipboard when starting remote session * add rpcrequest handler * add paste to clipboard rpcrequest * seperate clipboard to bridge/clipboard * update remote clipboard logic - when paste: use line ending matching file format - when copy: use line ending of remote system * update remote clipboard setup - use `neovide_no_custom_clipboard` to disable custom clipboard - disable cache to not use old clipboard contents when error returned from `get_remote_clipboard` * code cleanup and format --- Cargo.lock | 62 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/bridge/clipboard.rs | 56 +++++++++++++++++++++++++++++++++++++ src/bridge/handler.rs | 31 +++++++++++++++++++++ src/bridge/mod.rs | 7 ++++- src/bridge/setup.rs | 46 +++++++++++++++++++++++++++++- 6 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 src/bridge/clipboard.rs diff --git a/Cargo.lock b/Cargo.lock index 61a2e08..5d78c79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,28 @@ dependencies = [ "vec_map", ] +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -1317,6 +1339,7 @@ dependencies = [ "async-trait", "cfg-if 0.1.10", "clap", + "clipboard", "derive-new", "dirs 2.0.2", "euclid", @@ -1503,6 +1526,26 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -2638,6 +2681,15 @@ dependencies = [ "toml", ] +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + [[package]] name = "x11-dl" version = "2.19.1" @@ -2658,6 +2710,16 @@ dependencies = [ "libc", ] +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + [[package]] name = "xcursor" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index 44d5662..b9b7943 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ gl = "0.14.0" swash = "0.1.4" clap="2.33.3" xdg="2.4.0" +clipboard="0.5.0" [dev-dependencies] mockall = "0.7.0" diff --git a/src/bridge/clipboard.rs b/src/bridge/clipboard.rs new file mode 100644 index 0000000..e84f438 --- /dev/null +++ b/src/bridge/clipboard.rs @@ -0,0 +1,56 @@ +use std::error::Error; + +use rmpv::Value; + +use clipboard::ClipboardContext; +use clipboard::ClipboardProvider; + +pub fn get_remote_clipboard(format: Option<&str>) -> Result> { + let mut ctx: ClipboardContext = ClipboardProvider::new()?; + let clipboard_raw = ctx.get_contents()?.replace("\r", ""); + + let lines = if let Some("dos") = format { + // add \r to lines of current file format is dos + clipboard_raw.replace("\n", "\r\n") + } else { + // else, \r is stripped, leaving only \n + clipboard_raw + } + .split("\n") + .map(|line| Value::from(line)) + .collect::>(); + + let lines = Value::from(lines); + // v paste is normal paste (everything in lines is pasted) + // V paste is paste with extra endline (line paste) + // If you want V paste, copy text with extra endline + let paste_mode = Value::from("v"); + + // returns [content: [String], paste_mode: v or V] + Ok(Value::from(vec![lines, paste_mode])) +} + +pub fn set_remote_clipboard(arguments: Vec) -> Result<(), Box> { + if arguments.len() != 3 { + return Err("expected exactly 3 arguments to set_remote_clipboard".into()); + } + + #[cfg(not(windows))] + let endline = "\n"; + #[cfg(windows)] + let endline = "\r\n"; + + let lines = arguments[0] + .as_array() + .map(|arr| { + arr.iter() + .filter_map(|x| x.as_str().map(String::from)) + .map(|s| s.replace("\r", "")) // strip \r + .collect::>() + .join(endline) + }) + .ok_or("can't build string from provided text")?; + + let mut ctx: ClipboardContext = ClipboardProvider::new()?; + ctx.set_contents(lines) +} diff --git a/src/bridge/handler.rs b/src/bridge/handler.rs index 7f8abac..fd9e64e 100644 --- a/src/bridge/handler.rs +++ b/src/bridge/handler.rs @@ -3,6 +3,7 @@ use log::trace; use nvim_rs::{Handler, Neovim}; use rmpv::Value; +use crate::bridge::clipboard::{get_remote_clipboard, set_remote_clipboard}; #[cfg(windows)] use crate::bridge::ui_commands::{ParallelCommand, UiCommand}; use crate::{ @@ -27,6 +28,33 @@ impl NeovimHandler { impl Handler for NeovimHandler { type Writer = TxWrapper; + async fn handle_request( + &self, + event_name: String, + _arguments: Vec, + neovim: Neovim, + ) -> Result { + trace!("Neovim request: {:?}", &event_name); + + match event_name.as_ref() { + "neovide.get_clipboard" => { + let endline_type = neovim + .command_output("set ff") + .await + .ok() + .and_then(|format| { + let mut s = format.split('='); + s.next(); + s.next().map(String::from) + }); + + get_remote_clipboard(endline_type.as_deref()) + .or(Err(Value::from("cannot get remote clipboard content"))) + } + _ => Ok(Value::from("rpcrequest not handled")), + } + } + async fn handle_notify( &self, event_name: String, @@ -63,6 +91,9 @@ impl Handler for NeovimHandler { "neovide.unregister_right_click" => { EVENT_AGGREGATOR.send(UiCommand::Parallel(ParallelCommand::UnregisterRightClick)); } + "neovide.set_clipboard" => { + set_remote_clipboard(arguments).ok(); + } _ => {} } } diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index 67ae89d..2724aeb 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -1,3 +1,4 @@ +mod clipboard; pub mod create; mod events; mod handler; @@ -59,7 +60,11 @@ async fn start_neovim_runtime() { } } - setup_neovide_specific_state(&nvim).await; + if let ConnectionMode::RemoteTcp(_) = connection_mode() { + setup_neovide_specific_state(&nvim, true).await; + } else { + setup_neovide_specific_state(&nvim, false).await; + } let settings = SETTINGS.get::(); let geometry = settings.geometry; diff --git a/src/bridge/setup.rs b/src/bridge/setup.rs index 2fa83cf..61e3831 100644 --- a/src/bridge/setup.rs +++ b/src/bridge/setup.rs @@ -7,7 +7,47 @@ use crate::{ error_handling::ResultPanicExplanation, }; -pub async fn setup_neovide_specific_state(nvim: &Neovim) { +pub async fn setup_neovide_remote_clipboard(nvim: &Neovim, neovide_channel: u64) { + // users can opt-out with + // vim: `let g:neovide_no_custom_clipboard = v:true` + // lua: `vim.g.neovide_no_custom_clipboard = true` + let no_custom_clipboard = nvim + .get_var("neovide_no_custom_clipboard") + .await + .ok() + .and_then(|v| v.as_bool()); + if Some(true) == no_custom_clipboard { + info!("Neovide working remotely but custom clipboard is disabled"); + return; + } + + // don't know how to setup lambdas with Value, so use string as command + let custom_clipboard = r#" + let g:clipboard = { + 'name': 'custom', + 'copy': { + '+': { + lines, + regtype -> rpcnotify(neovide_channel, 'neovide.set_clipboard', lines, regtype, '+') + }, + '*': { + lines, + regtype -> rpcnotify(neovide_channel, 'neovide.set_clipboard', lines, regtype, '*') + }, + }, + 'paste': { + '+': {-> rpcrequest(neovide_channel, 'neovide.get_clipboard', '+')}, + '*': {-> rpcrequest(neovide_channel, 'neovide.get_clipboard', '*')}, + }, + 'cache_enabled': 0 + } + "# + .replace("\n", "") // make one-liner, because multiline is not accepted (?) + .replace("neovide_channel", &neovide_channel.to_string()); + nvim.command(&custom_clipboard).await.ok(); +} + +pub async fn setup_neovide_specific_state(nvim: &Neovim, is_remote: bool) { // Set variable indicating to user config that neovide is being used nvim.set_var("neovide", Value::Boolean(true)) .await @@ -94,6 +134,10 @@ pub async fn setup_neovide_specific_state(nvim: &Neovim) { nvim.command("autocmd VimLeave * call rpcnotify(1, 'neovide.quit', v:exiting)") .await .ok(); + + if is_remote { + setup_neovide_remote_clipboard(nvim, neovide_channel).await; + } } #[cfg(windows)]