diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs
index 14fd3f7f..e82051dd 100644
--- a/helix-view/src/register.rs
+++ b/helix-view/src/register.rs
@@ -1,8 +1,13 @@
 use std::{borrow::Cow, collections::HashMap, iter};
 
 use anyhow::Result;
+use helix_core::NATIVE_LINE_ENDING;
 
-use crate::{document::SCRATCH_BUFFER_NAME, Editor};
+use crate::{
+    clipboard::{get_clipboard_provider, ClipboardProvider, ClipboardType},
+    document::SCRATCH_BUFFER_NAME,
+    Editor,
+};
 
 /// A key-value store for saving sets of values.
 ///
@@ -14,9 +19,21 @@ use crate::{document::SCRATCH_BUFFER_NAME, Editor};
 /// * Selection indices (`#`): index number of each selection starting at 1
 /// * Selection contents (`.`)
 /// * Document path (`%`): filename of the current buffer
-#[derive(Debug, Default)]
+/// * System clipboard (`*`)
+/// * Primary clipboard (`+`)
+#[derive(Debug)]
 pub struct Registers {
     inner: HashMap<char, Vec<String>>,
+    clipboard_provider: Box<dyn ClipboardProvider>,
+}
+
+impl Default for Registers {
+    fn default() -> Self {
+        Self {
+            inner: Default::default(),
+            clipboard_provider: get_clipboard_provider(),
+        }
+    }
 }
 
 impl Registers {
@@ -48,6 +65,15 @@ impl Registers {
 
                 Some(RegisterValues::new(iter::once(path)))
             }
+            '*' | '+' => Some(read_from_clipboard(
+                self.clipboard_provider.as_ref(),
+                self.inner.get(&name),
+                match name {
+                    '*' => ClipboardType::Clipboard,
+                    '+' => ClipboardType::Selection,
+                    _ => unreachable!(),
+                },
+            )),
             _ => self
                 .inner
                 .get(&name)
@@ -59,6 +85,18 @@ impl Registers {
         match name {
             '_' => Ok(()),
             '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")),
+            '*' | '+' => {
+                self.clipboard_provider.set_contents(
+                    values.join(NATIVE_LINE_ENDING.as_str()),
+                    match name {
+                        '*' => ClipboardType::Clipboard,
+                        '+' => ClipboardType::Selection,
+                        _ => unreachable!(),
+                    },
+                )?;
+                self.inner.insert(name, values);
+                Ok(())
+            }
             _ => {
                 self.inner.insert(name, values);
                 Ok(())
@@ -70,6 +108,27 @@ impl Registers {
         match name {
             '_' => Ok(()),
             '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")),
+            '*' | '+' => {
+                let clipboard_type = match name {
+                    '*' => ClipboardType::Clipboard,
+                    '+' => ClipboardType::Selection,
+                    _ => unreachable!(),
+                };
+                let contents = self.clipboard_provider.get_contents(clipboard_type)?;
+                let saved_values = self.inner.entry(name).or_insert_with(Vec::new);
+
+                if !contents_are_saved(saved_values, &contents) {
+                    anyhow::bail!("Failed to push to register {name}: clipboard does not match register contents");
+                }
+
+                saved_values.push(value);
+                self.clipboard_provider.set_contents(
+                    saved_values.join(NATIVE_LINE_ENDING.as_str()),
+                    clipboard_type,
+                )?;
+
+                Ok(())
+            }
             _ => {
                 self.inner.entry(name).or_insert_with(Vec::new).push(value);
                 Ok(())
@@ -88,6 +147,7 @@ impl Registers {
     pub fn iter_preview(&self) -> impl Iterator<Item = (char, &str)> {
         self.inner
             .iter()
+            .filter(|(name, _)| !matches!(name, '*' | '+'))
             .map(|(name, values)| {
                 let preview = values
                     .first()
@@ -102,6 +162,8 @@ impl Registers {
                     ('#', "<selection indices>"),
                     ('.', "<selection contents>"),
                     ('%', "<document path>"),
+                    ('*', "<system clipboard>"),
+                    ('+', "<primary clipboard>"),
                 ]
                 .iter()
                 .copied(),
@@ -109,15 +171,97 @@ impl Registers {
     }
 
     pub fn clear(&mut self) {
+        self.clear_clipboard(ClipboardType::Clipboard);
+        self.clear_clipboard(ClipboardType::Selection);
         self.inner.clear()
     }
 
     pub fn remove(&mut self, name: char) -> bool {
         match name {
+            '*' | '+' => {
+                self.clear_clipboard(match name {
+                    '*' => ClipboardType::Clipboard,
+                    '+' => ClipboardType::Selection,
+                    _ => unreachable!(),
+                });
+                self.inner.remove(&name);
+
+                true
+            }
             '_' | '#' | '.' | '%' => false,
             _ => self.inner.remove(&name).is_some(),
         }
     }
+
+    fn clear_clipboard(&mut self, clipboard_type: ClipboardType) {
+        if let Err(err) = self
+            .clipboard_provider
+            .set_contents("".into(), clipboard_type)
+        {
+            log::error!(
+                "Failed to clear {} clipboard: {err}",
+                match clipboard_type {
+                    ClipboardType::Clipboard => "system",
+                    ClipboardType::Selection => "primary",
+                }
+            )
+        }
+    }
+}
+
+fn read_from_clipboard<'a>(
+    provider: &dyn ClipboardProvider,
+    saved_values: Option<&'a Vec<String>>,
+    clipboard_type: ClipboardType,
+) -> RegisterValues<'a> {
+    match provider.get_contents(clipboard_type) {
+        Ok(contents) => {
+            // If we're pasting the same values that we just yanked, re-use
+            // the saved values. This allows pasting multiple selections
+            // even when yanked to a clipboard.
+            let Some(values) = saved_values else { return RegisterValues::new(iter::once(contents.into())) };
+
+            if contents_are_saved(values, &contents) {
+                RegisterValues::new(values.iter().map(Cow::from))
+            } else {
+                RegisterValues::new(iter::once(contents.into()))
+            }
+        }
+        Err(err) => {
+            log::error!(
+                "Failed to read {} clipboard: {err}",
+                match clipboard_type {
+                    ClipboardType::Clipboard => "system",
+                    ClipboardType::Selection => "primary",
+                }
+            );
+
+            RegisterValues::new(iter::empty())
+        }
+    }
+}
+
+fn contents_are_saved(saved_values: &[String], mut contents: &str) -> bool {
+    let line_ending = NATIVE_LINE_ENDING.as_str();
+    let mut values = saved_values.iter();
+
+    match values.next() {
+        Some(first) if contents.starts_with(first) => {
+            contents = &contents[first.len()..];
+        }
+        None if contents.is_empty() => return true,
+        _ => return false,
+    }
+
+    for value in values {
+        if contents.starts_with(line_ending) && contents[line_ending.len()..].starts_with(value) {
+            contents = &contents[line_ending.len() + value.len()..];
+        } else {
+            return false;
+        }
+    }
+
+    true
 }
 
 // This is a wrapper of an iterator that is both double ended and exact size,