From 7d51805e94a461834ce34e0829da5859d1f9db32 Mon Sep 17 00:00:00 2001
From: Dmitry Sharshakov <d3dx12.xx@gmail.com>
Date: Thu, 12 Aug 2021 05:53:48 +0300
Subject: [PATCH] Support primary clipboard (#548)

* clipboard-none: add in-memory fallback buffer

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* view: add Wayland primary clipboard

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Format

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: copy to primary selection after mouse move stops

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: don't update primary selection if it is a single character

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: discard result of setting primary selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: add commands for interaction with primary clipboard

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* editor: implement primary selection copy/paste using commands

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: support xsel for primary selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: support xclip for primary selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: multiple cursor support for middle click paste

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* rename primary selection to primary clipboard in scope of PR

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: make middle click paste optional

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Format

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Update helix-term/src/ui/editor.rs

* fix formatting

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* config: correct defaults if terminal prop is not set

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* refactor: merge clipboard and primary selection implementations

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Tidy up code

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* view: remove names for different clipboard/selection providers

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Update helix-view/src/clipboard.rs

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>

* helix-view: tidy macros

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: refactor paste-replace commands

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: use new config for middle-click-paste

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: remove memory fallback for command and windows providers

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard-win: fix build

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: return empty string when primary clipboard is missing

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: fix errors in Windows build

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
---
 helix-term/src/commands.rs  | 182 +++++++++++++++++++++++++++++++-----
 helix-term/src/ui/editor.rs |  55 +++++++++++
 helix-view/src/clipboard.rs | 146 ++++++++++++++++++++++++-----
 helix-view/src/editor.rs    |   3 +
 4 files changed, 341 insertions(+), 45 deletions(-)

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<str>;
-    fn get_contents(&self) -> Result<String>;
-    fn set_contents(&self, contents: String) -> Result<()>;
+    fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>;
+    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<dyn ClipboardProvider> {
         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<dyn ClipboardProvider> {
         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<dyn ClipboardProvider> {
         }
     } 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<str> {
             Cow::Borrowed("none")
         }
 
-        fn get_contents(&self) -> Result<String> {
-            Ok(String::new())
+        fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
+            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<String> {
-            let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
-            Ok(contents)
+        fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
+            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<CommandConfig>,
+        pub set_primary_cmd: Option<CommandConfig>,
     }
 
     impl ClipboardProvider for CommandProvider {
@@ -203,16 +285,34 @@ mod provider {
             }
         }
 
-        fn get_contents(&self) -> Result<String> {
-            let output = self
-                .get_cmd
-                .execute(None, true)?
-                .context("output is missing")?;
-            Ok(output)
+        fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
+            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,
         }
     }
 }