@@ -29,12 +29,11 @@ use helix_core::{
29 tree_sitter::Node,
29 tree_sitter::Node,
30 unicode::width::UnicodeWidthChar,
30 unicode::width::UnicodeWidthChar,
31 visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
31 visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
32− RopeSlice, Selection, SmallVec, Tendril, Transaction,
32+ RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
34use helix_view::{
34use helix_view::{
35− clipboard::ClipboardType,
36 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
35 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
37− editor::{Action, CompleteAction, Motion},
36+ editor::{Action, CompleteAction},
38 info::Info,
37 info::Info,
39 input::KeyEvent,
38 input::KeyEvent,
40 keyboard::KeyCode,
39 keyboard::KeyCode,
@@ -44,7 +43,6 @@ use helix_view::{
46use anyhow::{anyhow, bail, ensure, Context as _};
45use anyhow::{anyhow, bail, ensure, Context as _};
47−use fuzzy_matcher::FuzzyMatcher;
48use insert::*;
46use insert::*;
49use movement::Movement;
47use movement::Movement;
@@ -61,9 +59,14 @@ use crate::{
63use crate::job::{self, Jobs};
61use crate::job::{self, Jobs};
64−use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
62+use futures_util::{stream::FuturesUnordered, TryStreamExt};
65−use std::{collections::HashMap, fmt, future::Future};
64+ collections::{HashMap, HashSet},
66−use std::{collections::HashSet, num::NonZeroUsize};
68+ num::NonZeroUsize,
69 borrow::Cow,
72 borrow::Cow,
@@ -72,11 +75,11 @@ use std::{
73use once_cell::sync::Lazy;
76use once_cell::sync::Lazy;
74use serde::de::{self, Deserialize, Deserializer};
77use serde::de::{self, Deserialize, Deserializer};
76use grep_regex::RegexMatcherBuilder;
80use grep_regex::RegexMatcherBuilder;
77use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
81use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
78use ignore::{DirEntry, WalkBuilder, WalkState};
82use ignore::{DirEntry, WalkBuilder, WalkState};
79−use tokio_stream::wrappers::UnboundedReceiverStream;
81pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
84pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
@@ -248,6 +251,8 @@ impl MappableCommand {
248 move_prev_long_word_start, "Move to start of previous long word",
251 move_prev_long_word_start, "Move to start of previous long word",
249 move_next_long_word_end, "Move to end of next long word",
252 move_next_long_word_end, "Move to end of next long word",
250 move_prev_long_word_end, "Move to end of previous long word",
253 move_prev_long_word_end, "Move to end of previous long word",
254+ move_parent_node_end, "Move to end of the parent node",
255+ move_parent_node_start, "Move to beginning of the parent node",
251 extend_next_word_start, "Extend to start of next word",
256 extend_next_word_start, "Extend to start of next word",
252 extend_prev_word_start, "Extend to start of previous word",
257 extend_prev_word_start, "Extend to start of previous word",
253 extend_next_word_end, "Extend to end of next word",
258 extend_next_word_end, "Extend to end of next word",
@@ -256,6 +261,8 @@ impl MappableCommand {
256 extend_prev_long_word_start, "Extend to start of previous long word",
261 extend_prev_long_word_start, "Extend to start of previous long word",
257 extend_next_long_word_end, "Extend to end of next long word",
262 extend_next_long_word_end, "Extend to end of next long word",
258 extend_prev_long_word_end, "Extend to end of prev long word",
263 extend_prev_long_word_end, "Extend to end of prev long word",
264+ extend_parent_node_end, "Extend to end of the parent node",
265+ extend_parent_node_start, "Extend to beginning of the parent node",
259 find_till_char, "Move till next occurrence of char",
266 find_till_char, "Move till next occurrence of char",
260 find_next_char, "Move to next occurrence of char",
267 find_next_char, "Move to next occurrence of char",
261 extend_till_char, "Extend till next occurrence of char",
268 extend_till_char, "Extend till next occurrence of char",
@@ -330,7 +337,7 @@ impl MappableCommand {
330 goto_implementation, "Goto implementation",
337 goto_implementation, "Goto implementation",
331 goto_file_start, "Goto line number <n> else file start",
338 goto_file_start, "Goto line number <n> else file start",
332 goto_file_end, "Goto file end",
339 goto_file_end, "Goto file end",
333− goto_file, "Goto files in selection",
340+ goto_file, "Goto files/URLs in selection",
334 goto_file_hsplit, "Goto files in selection (hsplit)",
341 goto_file_hsplit, "Goto files in selection (hsplit)",
335 goto_file_vsplit, "Goto files in selection (vsplit)",
342 goto_file_vsplit, "Goto files in selection (vsplit)",
336 goto_reference, "Goto references",
343 goto_reference, "Goto references",
@@ -362,6 +369,7 @@ impl MappableCommand {
362 extend_to_line_end, "Extend to line end",
369 extend_to_line_end, "Extend to line end",
363 extend_to_line_end_newline, "Extend to line end",
370 extend_to_line_end_newline, "Extend to line end",
364 signature_help, "Show signature help",
371 signature_help, "Show signature help",
372+ smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
365 insert_tab, "Insert tab char",
373 insert_tab, "Insert tab char",
366 insert_newline, "Insert newline char",
374 insert_newline, "Insert newline char",
367 delete_char_backward, "Delete previous char",
375 delete_char_backward, "Delete previous char",
@@ -376,6 +384,8 @@ impl MappableCommand {
376 later, "Move forward in history",
384 later, "Move forward in history",
377 commit_undo_checkpoint, "Commit changes to new checkpoint",
385 commit_undo_checkpoint, "Commit changes to new checkpoint",
378 yank, "Yank selection",
386 yank, "Yank selection",
387+ yank_to_clipboard, "Yank selections to clipboard",
388+ yank_to_primary_clipboard, "Yank selections to primary clipboard",
379 yank_joined, "Join and yank selections",
389 yank_joined, "Join and yank selections",
380 yank_joined_to_clipboard, "Join and yank selections to clipboard",
390 yank_joined_to_clipboard, "Join and yank selections to clipboard",
381 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
391 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -980,6 +990,7 @@ fn align_selections(cx: &mut Context) {
981 let transaction = Transaction::change(doc.text(), changes.into_iter());
991 let transaction = Transaction::change(doc.text(), changes.into_iter());
982 doc.apply(&transaction, view.id);
992 doc.apply(&transaction, view.id);
993+ exit_select_mode(cx);
985fn goto_window(cx: &mut Context, align: Align) {
996fn goto_window(cx: &mut Context, align: Align) {
@@ -1099,8 +1110,7 @@ where
1099 .transform(|range| move_fn(text, range, count, behavior));
1110 .transform(|range| move_fn(text, range, count, behavior));
1100 doc.set_selection(view.id, selection);
1111 doc.set_selection(view.id, selection);
1103− cx.editor.last_motion = Some(Motion(Box::new(motion)));
1113+ cx.editor.apply_motion(motion)
1106fn goto_prev_paragraph(cx: &mut Context) {
1116fn goto_prev_paragraph(cx: &mut Context) {
@@ -1155,6 +1165,10 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
1155 let (view, doc) = current_ref!(cx.editor);
1165 let (view, doc) = current_ref!(cx.editor);
1156 let text = doc.text();
1166 let text = doc.text();
1157 let selections = doc.selection(view.id);
1167 let selections = doc.selection(view.id);
1170+ .map(|path| path.parent().unwrap().to_path_buf())
1171+ .unwrap_or_default();
1158 let mut paths: Vec<_> = selections
1172 let mut paths: Vec<_> = selections
1160 .map(|r| text.slice(r.from()..r.to()).to_string())
1174 .map(|r| text.slice(r.from()..r.to()).to_string())
@@ -1182,16 +1196,80 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
1182 .to_string(),
1196 .to_string(),
1185 for sel in paths {
1200 for sel in paths {
1186 let p = sel.trim();
1201 let p = sel.trim();
1187− if !p.is_empty() {
1202+ if p.is_empty() {
1206+ if let Ok(url) = Url::parse(p) {
1207+ return open_url(cx, url, action);
1210+ let path = &rel_path.join(p);
1212+ let picker = ui::file_picker(path.into(), &cx.editor.config());
1213+ cx.push_layer(Box::new(overlaid(picker)));
1188− if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
1214+ } else if let Err(e) = cx.editor.open(path, action) {
1215+ cx.editor.set_error(format!("Open file failed: {:?}", e));
1220+/// Opens the given url. If the URL points to a valid textual file it is open in helix.
1221+// Otherwise, the file is open using external program.
1222+fn open_url(cx: &mut Context, url: Url, action: Action) {
1223+ let doc = doc!(cx.editor);
1226+ .map(|path| path.parent().unwrap().to_path_buf())
1227+ .unwrap_or_default();
1229+ if url.scheme() != "file" {
1230+ return open_external_url(cx, url);
1233+ let content_type = std::fs::File::open(url.path()).and_then(|file| {
1234+ // Read up to 1kb to detect the content type
1235+ let mut read_buffer = Vec::new();
1236+ let n = file.take(1024).read_to_end(&mut read_buffer)?;
1237+ Ok(content_inspector::inspect(&read_buffer[..n]))
1240+ // we attempt to open binary files - files that can't be open in helix - using external
1241+ // program as well, e.g. pdf files or images
1242+ match content_type {
1243+ Ok(content_inspector::ContentType::BINARY) => open_external_url(cx, url),
1244+ Ok(_) | Err(_) => {
1245+ let path = &rel_path.join(url.path());
1247+ let picker = ui::file_picker(path.into(), &cx.editor.config());
1248+ cx.push_layer(Box::new(overlaid(picker)));
1249+ } else if let Err(e) = cx.editor.open(path, action) {
1189 cx.editor.set_error(format!("Open file failed: {:?}", e));
1250 cx.editor.set_error(format!("Open file failed: {:?}", e));
1256+/// Opens URL in external program.
1257+fn open_external_url(cx: &mut Context, url: Url) {
1258+ let commands = open::commands(url.as_str());
1259+ cx.jobs.callback(async {
1260+ for cmd in commands {
1261+ let mut command = tokio::process::Command::new(cmd.get_program());
1262+ command.args(cmd.get_args());
1263+ if command.output().await.is_ok() {
1264+ return Ok(job::Callback::Editor(Box::new(|_| {})));
1267+ Ok(job::Callback::Editor(Box::new(move |editor| {
1268+ editor.set_error("Opening URL in external program failed")
1195fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
1273fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
1197 F: Fn(RopeSlice, Range, usize) -> Range,
1275 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1240,10 +1318,66 @@ fn extend_next_long_word_end(cx: &mut Context) {
1240 extend_word_impl(cx, movement::move_next_long_word_end)
1318 extend_word_impl(cx, movement::move_next_long_word_end)
1243−fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
1321+/// Separate branch to find_char designed only for `<ret>` char.
1245− F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
1323+// This is necessary because the one document can have different line endings inside. And we
1246−{
1324+// cannot predict what character to find when <ret> is pressed. On the current line it can be `lf`
1325+// but on the next line it can be `crlf`. That's why [`find_char_impl`] cannot be applied here.
1326+fn find_char_line_ending(
1329+ direction: Direction,
1333+ let (view, doc) = current!(cx.editor);
1334+ let text = doc.text().slice(..);
1336+ let selection = doc.selection(view.id).clone().transform(|range| {
1337+ let cursor = range.cursor(text);
1338+ let cursor_line = range.cursor_line(text);
1340+ // Finding the line where we're going to find <ret>. Depends mostly on
1341+ // `count`, but also takes into account edge cases where we're already at the end
1342+ // of a line or the beginning of a line
1343+ let find_on_line = match direction {
1344+ Direction::Forward => {
1345+ let on_edge = line_end_char_index(&text, cursor_line) == cursor;
1346+ let line = cursor_line + count - 1 + (on_edge as usize);
1347+ if line >= text.len_lines() - 1 {
1353+ Direction::Backward => {
1354+ let on_edge = text.line_to_char(cursor_line) == cursor && !inclusive;
1355+ let line = cursor_line as isize - (count as isize - 1 + on_edge as isize);
1364+ let pos = match (direction, inclusive) {
1365+ (Direction::Forward, true) => line_end_char_index(&text, find_on_line),
1366+ (Direction::Forward, false) => line_end_char_index(&text, find_on_line) - 1,
1367+ (Direction::Backward, true) => line_end_char_index(&text, find_on_line - 1),
1368+ (Direction::Backward, false) => text.line_to_char(find_on_line),
1372+ range.put_cursor(text, pos, true)
1374+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
1377+ doc.set_selection(view.id, selection);
1380+fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) {
1247 // TODO: count is reset to 1 before next key so we move it into the closure here.
1381 // TODO: count is reset to 1 before next key so we move it into the closure here.
1248 // Would be nice to carry over.
1382 // Would be nice to carry over.
1249 let count = cx.count();
1383 let count = cx.count();
@@ -1256,13 +1390,9 @@ where
1256 KeyEvent {
1390 KeyEvent {
1257 code: KeyCode::Enter,
1391 code: KeyCode::Enter,
1260− // TODO: this isn't quite correct when CRLF is involved.
1394+ find_char_line_ending(cx, count, direction, inclusive, extend);
1261− // This hack will work in most cases, since documents don't
1395+ return;
1262− // usually mix line endings. But we should fix it eventually
1265− doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
1268 KeyEvent {
1398 KeyEvent {
@@ -1275,11 +1405,18 @@ where
1275 } => ch,
1405 } => ch,
1276 _ => return,
1406 _ => return,
1408+ let motion = move |editor: &mut Editor| {
1410+ Direction::Forward => {
1411+ find_char_impl(editor, &find_next_char_impl, inclusive, extend, ch, count)
1413+ Direction::Backward => {
1414+ find_char_impl(editor, &find_prev_char_impl, inclusive, extend, ch, count)
1279− find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
1419+ cx.editor.apply_motion(motion);
1280− cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
1281− find_char_impl(editor, &search_fn, inclusive, extend, ch, 1);
@@ -1358,46 +1495,39 @@ fn find_prev_char_impl(
1360fn find_till_char(cx: &mut Context) {
1497fn find_till_char(cx: &mut Context) {
1361− will_find_char(cx, find_next_char_impl, false, false)
1498+ find_char(cx, Direction::Forward, false, false);
1364fn find_next_char(cx: &mut Context) {
1501fn find_next_char(cx: &mut Context) {
1365− will_find_char(cx, find_next_char_impl, true, false)
1502+ find_char(cx, Direction::Forward, true, false)
1368fn extend_till_char(cx: &mut Context) {
1505fn extend_till_char(cx: &mut Context) {
1369− will_find_char(cx, find_next_char_impl, false, true)
1506+ find_char(cx, Direction::Forward, false, true)
1372fn extend_next_char(cx: &mut Context) {
1509fn extend_next_char(cx: &mut Context) {
1373− will_find_char(cx, find_next_char_impl, true, true)
1510+ find_char(cx, Direction::Forward, true, true)
1376fn till_prev_char(cx: &mut Context) {
1513fn till_prev_char(cx: &mut Context) {
1377− will_find_char(cx, find_prev_char_impl, false, false)
1514+ find_char(cx, Direction::Backward, false, false)
1380fn find_prev_char(cx: &mut Context) {
1517fn find_prev_char(cx: &mut Context) {
1381− will_find_char(cx, find_prev_char_impl, true, false)
1518+ find_char(cx, Direction::Backward, true, false)
1384fn extend_till_prev_char(cx: &mut Context) {
1521fn extend_till_prev_char(cx: &mut Context) {
1385− will_find_char(cx, find_prev_char_impl, false, true)
1522+ find_char(cx, Direction::Backward, false, true)
1388fn extend_prev_char(cx: &mut Context) {
1525fn extend_prev_char(cx: &mut Context) {
1389− will_find_char(cx, find_prev_char_impl, true, true)
1526+ find_char(cx, Direction::Backward, true, true)
1392fn repeat_last_motion(cx: &mut Context) {
1529fn repeat_last_motion(cx: &mut Context) {
1393− let count = cx.count();
1530+ cx.editor.repeat_last_motion(cx.count())
1394− let last_motion = cx.editor.last_motion.take();
1395− if let Some(m) = &last_motion {
1396− for _ in 0..count {
1399− cx.editor.last_motion = last_motion;
1403fn replace(cx: &mut Context) {
1533fn replace(cx: &mut Context) {
@@ -1464,6 +1594,7 @@ where
1466 doc.apply(&transaction, view.id);
1596 doc.apply(&transaction, view.id);
1597+ exit_select_mode(cx);
1469fn switch_case(cx: &mut Context) {
1600fn switch_case(cx: &mut Context) {
@@ -1705,8 +1836,8 @@ fn select_regex(cx: &mut Context) {
1705 "select:".into(),
1836 "select:".into(),
1706 Some(reg),
1837 Some(reg),
1707 ui::completers::none,
1838 ui::completers::none,
1708− move |editor, regex, event| {
1839+ move |cx, regex, event| {
1709− let (view, doc) = current!(editor);
1840+ let (view, doc) = current!(cx.editor);
1710 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1841 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
@@ -1727,8 +1858,8 @@ fn split_selection(cx: &mut Context) {
1727 "split:".into(),
1858 "split:".into(),
1728 Some(reg),
1859 Some(reg),
1729 ui::completers::none,
1860 ui::completers::none,
1730− move |editor, regex, event| {
1861+ move |cx, regex, event| {
1731− let (view, doc) = current!(editor);
1862+ let (view, doc) = current!(cx.editor);
1732 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1863 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
@@ -1851,11 +1982,11 @@ fn search_impl(
1852fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
1983fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
1853 let mut items = reg
1984 let mut items = reg
1854− .and_then(|reg| cx.editor.registers.get(reg))
1985+ .and_then(|reg| cx.editor.registers.read(reg, cx.editor))
1855− .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
1986+ .map_or(Vec::new(), |reg| reg.take(200).collect());
1856 items.sort_unstable();
1987 items.sort_unstable();
1857 items.dedup();
1988 items.dedup();
1858− items.into_iter().cloned().collect()
1989+ items.into_iter().map(|value| value.to_string()).collect()
1861fn search(cx: &mut Context) {
1992fn search(cx: &mut Context) {
@@ -1892,12 +2023,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
1892 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2023 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1893 .collect()
2024 .collect()
1895− move |editor, regex, event| {
2026+ move |cx, regex, event| {
2027+ if event == PromptEvent::Validate {
2028+ cx.editor.registers.last_search_register = reg;
1896− if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
2029+ } else if event != PromptEvent::Update {
1899 search_impl(
2032 search_impl(
1900− editor,
2033+ cx.editor,
1901 &contents,
2034 &contents,
1903 Movement::Move,
2036 Movement::Move,
@@ -1912,11 +2045,13 @@ fn searcher(cx: &mut Context, direction: Direction) {
1913fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
2046fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1914 let count = cx.count();
2047 let count = cx.count();
2050+ .unwrap_or(cx.editor.registers.last_search_register);
1915 let config = cx.editor.config();
2051 let config = cx.editor.config();
1916 let scrolloff = config.scrolloff;
2052 let scrolloff = config.scrolloff;
1917− let (_, doc) = current!(cx.editor);
1918− let registers = &cx.editor.registers;
1919− if let Some(query) = registers.read('/').and_then(|query| query.last()) {
2053+ if let Some(query) = cx.editor.registers.first(register, cx.editor) {
2054+ let doc = doc!(cx.editor);
1920 let contents = doc.text().slice(..).to_string();
2055 let contents = doc.text().slice(..).to_string();
1921 let search_config = &config.search;
2056 let search_config = &config.search;
1922 let case_insensitive = if search_config.smart_case {
2057 let case_insensitive = if search_config.smart_case {
@@ -1925,7 +2060,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
1927 let wrap_around = search_config.wrap_around;
2062 let wrap_around = search_config.wrap_around;
1928− if let Ok(regex) = RegexBuilder::new(query)
2063+ if let Ok(regex) = RegexBuilder::new(&query)
1929 .case_insensitive(case_insensitive)
2064 .case_insensitive(case_insensitive)
1930 .multi_line(true)
2065 .multi_line(true)
1931 .build()
2066 .build()
@@ -1965,6 +2100,7 @@ fn extend_search_prev(cx: &mut Context) {
1967fn search_selection(cx: &mut Context) {
2102fn search_selection(cx: &mut Context) {
2103+ let register = cx.register.unwrap_or('/');
1968 let (view, doc) = current!(cx.editor);
2104 let (view, doc) = current!(cx.editor);
1969 let contents = doc.text().slice(..);
2105 let contents = doc.text().slice(..);
@@ -1977,13 +2113,24 @@ fn search_selection(cx: &mut Context) {
1977 .collect::<Vec<_>>()
2113 .collect::<Vec<_>>()
1978 .join("|");
2114 .join("|");
1980− let msg = format!("register '{}' set to '{}'", '/', ®ex);
2116+ let msg = format!("register '{}' set to '{}'", register, ®ex);
1981− cx.editor.registers.push('/', regex);
2117+ match cx.editor.registers.push(register, regex) {
2119+ cx.editor.registers.last_search_register = register;
1982− cx.editor.set_status(msg);
2120+ cx.editor.set_status(msg)
2122+ Err(err) => cx.editor.set_error(err.to_string()),
1985fn make_search_word_bounded(cx: &mut Context) {
2126fn make_search_word_bounded(cx: &mut Context) {
2127+ // Defaults to the active search register instead `/` to be more ergonomic assuming most people
2128+ // would use this command following `search_selection`. This avoids selecting the register
2132+ .unwrap_or(cx.editor.registers.last_search_register);
1986− let regex = match cx.editor.registers.last('/') {
2133+ let regex = match cx.editor.registers.first(register, cx.editor) {
1987 Some(regex) => regex,
2134 Some(regex) => regex,
1988 None => return,
2135 None => return,
@@ -2001,14 +2148,19 @@ fn make_search_word_bounded(cx: &mut Context) {
2001 if !start_anchored {
2148 if !start_anchored {
2002 new_regex.push_str("\\b");
2149 new_regex.push_str("\\b");
2004− new_regex.push_str(regex);
2151+ new_regex.push_str(®ex);
2005 if !end_anchored {
2152 if !end_anchored {
2006 new_regex.push_str("\\b");
2153 new_regex.push_str("\\b");
2009− let msg = format!("register '{}' set to '{}'", '/', &new_regex);
2156+ let msg = format!("register '{}' set to '{}'", register, &new_regex);
2010− cx.editor.registers.push('/', new_regex);
2157+ match cx.editor.registers.push(register, new_regex) {
2159+ cx.editor.registers.last_search_register = register;
2011− cx.editor.set_status(msg);
2160+ cx.editor.set_status(msg)
2162+ Err(err) => cx.editor.set_error(err.to_string()),
2014fn global_search(cx: &mut Context) {
2166fn global_search(cx: &mut Context) {
@@ -2047,13 +2199,11 @@ fn global_search(cx: &mut Context) {
2050− let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
2051 let config = cx.editor.config();
2202 let config = cx.editor.config();
2052 let smart_case = config.search.smart_case;
2203 let smart_case = config.search.smart_case;
2053 let file_picker_config = config.file_picker.clone();
2204 let file_picker_config = config.file_picker.clone();
2055 let reg = cx.register.unwrap_or('/');
2206 let reg = cx.register.unwrap_or('/');
2057 let completions = search_completions(cx, Some(reg));
2207 let completions = search_completions(cx, Some(reg));
2058 ui::regex_prompt(
2208 ui::regex_prompt(
@@ -2066,43 +2216,67 @@ fn global_search(cx: &mut Context) {
2066 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2216 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2067 .collect()
2217 .collect()
2069− move |_editor, regex, event| {
2219+ move |cx, regex, event| {
2070 if event != PromptEvent::Validate {
2220 if event != PromptEvent::Validate {
2223+ cx.editor.registers.last_search_register = reg;
2225+ let current_path = doc_mut!(cx.editor).path().cloned();
2226+ let documents: Vec<_> = cx
2229+ .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
2074 if let Ok(matcher) = RegexMatcherBuilder::new()
2232 if let Ok(matcher) = RegexMatcherBuilder::new()
2075 .case_smart(smart_case)
2233 .case_smart(smart_case)
2076 .build(regex.as_str())
2234 .build(regex.as_str())
2078− let searcher = SearcherBuilder::new()
2236+ let search_root = helix_loader::current_working_dir();
2079− .binary_detection(BinaryDetection::quit(b'\x00'))
2237+ if !search_root.exists() {
2080− .build();
2238+ cx.editor
2239+ .set_error("Current working directory does not exist");
2243+ let (picker, injector) = Picker::stream(current_path);
2082− let search_root = std::env::current_dir()
2083− .expect("Global search error: Failed to get current dir");
2084 let dedup_symlinks = file_picker_config.deduplicate_links;
2245 let dedup_symlinks = file_picker_config.deduplicate_links;
2085 let absolute_root = search_root
2246 let absolute_root = search_root
2086 .canonicalize()
2247 .canonicalize()
2087 .unwrap_or_else(|_| search_root.clone());
2248 .unwrap_or_else(|_| search_root.clone());
2088−
2249+ let injector_ = injector.clone();
2251+ std::thread::spawn(move || {
2252+ let searcher = SearcherBuilder::new()
2253+ .binary_detection(BinaryDetection::quit(b'\x00'))
2089− WalkBuilder::new(search_root)
2256+ let mut walk_builder = WalkBuilder::new(search_root);
2090− .hidden(file_picker_config.hidden)
2259+ .hidden(file_picker_config.hidden)
2091− .parents(file_picker_config.parents)
2260+ .parents(file_picker_config.parents)
2092− .ignore(file_picker_config.ignore)
2261+ .ignore(file_picker_config.ignore)
2093− .follow_links(file_picker_config.follow_symlinks)
2262+ .follow_links(file_picker_config.follow_symlinks)
2094− .git_ignore(file_picker_config.git_ignore)
2263+ .git_ignore(file_picker_config.git_ignore)
2095− .git_global(file_picker_config.git_global)
2264+ .git_global(file_picker_config.git_global)
2096− .git_exclude(file_picker_config.git_exclude)
2265+ .git_exclude(file_picker_config.git_exclude)
2097− .max_depth(file_picker_config.max_depth)
2266+ .max_depth(file_picker_config.max_depth)
2098− .filter_entry(move |entry| {
2267+ .filter_entry(move |entry| {
2099− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2268+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2272+ .add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
2273+ walk_builder.add_custom_ignore_filename(".helix/ignore");
2101− .build_parallel()
2275+ walk_builder.build_parallel().run(|| {
2103 let mut searcher = searcher.clone();
2276 let mut searcher = searcher.clone();
2104 let matcher = matcher.clone();
2277 let matcher = matcher.clone();
2105− let all_matches_sx = all_matches_sx.clone();
2278+ let injector = injector_.clone();
2279+ let documents = &documents;
2106 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2280 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2107 let entry = match entry {
2281 let entry = match entry {
2108 Ok(entry) => entry,
2282 Ok(entry) => entry,
@@ -2115,17 +2289,39 @@ fn global_search(cx: &mut Context) {
2115 _ => return WalkState::Continue,
2289 _ => return WalkState::Continue,
2118− let result = searcher.search_path(
2292+ let mut stop = false;
2121− sinks::UTF8(|line_num, _| {
2293+ let sink = sinks::UTF8(|line_num, _| {
2122− all_matches_sx
2294+ stop = injector
2123− .send(FileResult::new(entry.path(), line_num as usize - 1))
2295+ .push(FileResult::new(entry.path(), line_num as usize - 1))
2124− .unwrap();
2296+ .is_err();
2126− Ok(true)
2298+ Ok(!stop)
2300+ let doc = documents.iter().find(|&(doc_path, _)| {
2303+ .map_or(false, |doc_path| doc_path == entry.path())
2306+ let result = if let Some((_, doc)) = doc {
2307+ // there is already a buffer for this file
2308+ // search the buffer instead of the file because it's faster
2309+ // and captures new edits without requiring a save
2310+ if searcher.multi_line_with_matcher(&matcher) {
2311+ // in this case a continous buffer is required
2312+ // convert the rope to a string
2313+ let text = doc.to_string();
2314+ searcher.search_slice(&matcher, text.as_bytes(), sink)
2316+ searcher.search_reader(
2318+ RopeReader::new(doc.slice(..)),
2323+ searcher.search_path(&matcher, entry.path(), sink)
2130 if let Err(err) = result {
2326 if let Err(err) = result {
2131 log::error!(
2327 log::error!(
@@ -2134,65 +2330,66 @@ fn global_search(cx: &mut Context) {
2137− WalkState::Continue
2336+ WalkState::Continue
2342+ cx.jobs.callback(async move {
2343+ let call = move |_: &mut Editor, compositor: &mut Compositor| {
2344+ let picker = Picker::with_stream(
2347+ move |cx, FileResult { path, line_num }, action| {
2348+ let doc = match cx.editor.open(path, action) {
2349+ Ok(id) => doc_mut!(cx.editor, &id),
2351+ cx.editor.set_error(format!(
2352+ "Failed to open file '{}': {}",
2360+ let line_num = *line_num;
2361+ let view = view_mut!(cx.editor);
2362+ let text = doc.text();
2363+ if line_num >= text.len_lines() {
2364+ cx.editor.set_error(
2365+ "The line you jumped to does not exist anymore because the file has changed.",
2369+ let start = text.line_to_char(line_num);
2370+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2372+ doc.set_selection(view.id, Selection::single(start, end));
2373+ if action.align_view(view, doc.id()) {
2374+ align_view(doc, view, Align::Center);
2379+ |_editor, FileResult { path, line_num }| {
2380+ Some((path.clone().into(), Some((*line_num, *line_num))))
2383+ compositor.push(Box::new(overlaid(picker)))
2385+ Ok(Callback::EditorCompositor(Box::new(call)))
2140 } else {
2387 } else {
2141 // Otherwise do nothing
2388 // Otherwise do nothing
2142 // log::warn!("Global Search Invalid Pattern")
2389 // log::warn!("Global Search Invalid Pattern")
2147− let current_path = doc_mut!(cx.editor).path().cloned();
2149− let show_picker = async move {
2150− let all_matches: Vec<FileResult> =
2151− UnboundedReceiverStream::new(all_matches_rx).collect().await;
2152− let call: job::Callback = Callback::EditorCompositor(Box::new(
2153− move |editor: &mut Editor, compositor: &mut Compositor| {
2154− if all_matches.is_empty() {
2155− editor.set_status("No matches found");
2159− let picker = Picker::new(
2162− move |cx, FileResult { path, line_num }, action| {
2163− match cx.editor.open(path, action) {
2166− cx.editor.set_error(format!(
2167− "Failed to open file '{}': {}",
2175− let line_num = *line_num;
2176− let (view, doc) = current!(cx.editor);
2177− let text = doc.text();
2178− if line_num >= text.len_lines() {
2179− cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed.");
2182− let start = text.line_to_char(line_num);
2183− let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2185− doc.set_selection(view.id, Selection::single(start, end));
2186− align_view(doc, view, Align::Center);
2187− }).with_preview(|_editor, FileResult { path, line_num }| {
2188− Some((path.clone().into(), Some((*line_num, *line_num))))
2190− compositor.push(Box::new(overlaid(picker)));
2195− cx.jobs.callback(show_picker);
2198enum Extend {
2395enum Extend {
@@ -2315,17 +2512,35 @@ enum Operation {
2515+fn selection_is_linewise(selection: &Selection, text: &Rope) -> bool {
2516+ selection.ranges().iter().all(|range| {
2517+ let text = text.slice(..);
2518+ if range.slice(text).len_lines() < 2 {
2521+ // If the start of the selection is at the start of a line and the end at the end of a line.
2522+ let (start_line, end_line) = range.line_range(text);
2523+ let start = text.line_to_char(start_line);
2524+ let end = text.line_to_char((end_line + 1).min(text.len_lines()));
2525+ start == range.from() && end == range.to()
2318fn delete_selection_impl(cx: &mut Context, op: Operation) {
2529fn delete_selection_impl(cx: &mut Context, op: Operation) {
2319 let (view, doc) = current!(cx.editor);
2530 let (view, doc) = current!(cx.editor);
2321 let selection = doc.selection(view.id);
2532 let selection = doc.selection(view.id);
2533+ let only_whole_lines = selection_is_linewise(selection, doc.text());
2323 if cx.register != Some('_') {
2535 if cx.register != Some('_') {
2324 // first yank the selection
2536 // first yank the selection
2325 let text = doc.text().slice(..);
2537 let text = doc.text().slice(..);
2326 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
2538 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
2327 let reg_name = cx.register.unwrap_or('"');
2539 let reg_name = cx.register.unwrap_or('"');
2328− cx.editor.registers.write(reg_name, values);
2540+ if let Err(err) = cx.editor.registers.write(reg_name, values) {
2541+ cx.editor.set_error(err.to_string());
2331 // then delete
2546 // then delete
@@ -2339,7 +2554,11 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
2339 exit_select_mode(cx);
2554 exit_select_mode(cx);
2341 Operation::Change => {
2556 Operation::Change => {
2557+ if only_whole_lines {
2342− enter_insert_mode(cx);
2560+ enter_insert_mode(cx);
@@ -2461,6 +2680,10 @@ fn insert_mode(cx: &mut Context) {
2461 .transform(|range| Range::new(range.to(), range.from()));
2680 .transform(|range| Range::new(range.to(), range.from()));
2463 doc.set_selection(view.id, selection);
2682 doc.set_selection(view.id, selection);
2684+ // [TODO] temporary workaround until we're not using the idle timer to
2685+ // trigger auto completions any more
2686+ cx.editor.clear_idle_timer();
2466// inserts at the end of each selection
2689// inserts at the end of each selection
@@ -2497,6 +2720,10 @@ fn append_mode(cx: &mut Context) {
2498fn file_picker(cx: &mut Context) {
2721fn file_picker(cx: &mut Context) {
2499 let root = find_workspace().0;
2722 let root = find_workspace().0;
2723+ if !root.exists() {
2724+ cx.editor.set_error("Workspace directory does not exist");
2500 let picker = ui::file_picker(root, &cx.editor.config());
2727 let picker = ui::file_picker(root, &cx.editor.config());
2501 cx.push_layer(Box::new(overlaid(picker)));
2728 cx.push_layer(Box::new(overlaid(picker)));
@@ -2517,8 +2744,14 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
2517 let picker = ui::file_picker(path, &cx.editor.config());
2744 let picker = ui::file_picker(path, &cx.editor.config());
2518 cx.push_layer(Box::new(overlaid(picker)));
2745 cx.push_layer(Box::new(overlaid(picker)));
2520fn file_picker_in_current_directory(cx: &mut Context) {
2748fn file_picker_in_current_directory(cx: &mut Context) {
2521− let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
2749+ let cwd = helix_loader::current_working_dir();
2752+ .set_error("Current working directory does not exist");
2522 let picker = ui::file_picker(cwd, &cx.editor.config());
2755 let picker = ui::file_picker(cwd, &cx.editor.config());
2523 cx.push_layer(Box::new(overlaid(picker)));
2756 cx.push_layer(Box::new(overlaid(picker)));
@@ -2668,9 +2901,11 @@ fn jumplist_picker(cx: &mut Context) {
2668 |cx, meta, action| {
2901 |cx, meta, action| {
2669 cx.editor.switch(meta.id, action);
2902 cx.editor.switch(meta.id, action);
2670 let config = cx.editor.config();
2903 let config = cx.editor.config();
2671− let (view, doc) = current!(cx.editor);
2904+ let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
2672 doc.set_selection(view.id, meta.selection.clone());
2905 doc.set_selection(view.id, meta.selection.clone());
2906+ if action.align_view(view, doc.id()) {
2673− view.ensure_cursor_in_view_center(doc, config.scrolloff);
2907+ view.ensure_cursor_in_view_center(doc, config.scrolloff);
2676 .with_preview(|editor, meta| {
2911 .with_preview(|editor, meta| {
@@ -2818,6 +3053,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
2818 let indent = indent::indent_for_newline(
3053 let indent = indent::indent_for_newline(
2819 language_config,
3054 language_config,
3056+ &doc.config.load().indent_heuristic,
2821 &doc.indent_style,
3057 &doc.indent_style,
2822 tab_width,
3058 tab_width,
@@ -2922,6 +3158,7 @@ fn open(cx: &mut Context, open: Open) {
2922 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
3158 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
2923 Open::Above => range.from(),
3159 Open::Above => range.from(),
2925 let new_line = match open {
3162 let new_line = match open {
2926 // adjust position to the end of the line (next line - 1)
3163 // adjust position to the end of the line (next line - 1)
2927 Open::Below => cursor_line + 1,
3164 Open::Below => cursor_line + 1,
@@ -2929,13 +3166,15 @@ fn open(cx: &mut Context, open: Open) {
2929 Open::Above => cursor_line,
3166 Open::Above => cursor_line,
3169+ let line_num = new_line.saturating_sub(1);
2932 // Index to insert newlines after, as well as the char width
3171 // Index to insert newlines after, as well as the char width
2933 // to use to compensate for those inserted newlines.
3172 // to use to compensate for those inserted newlines.
2934 let (line_end_index, line_end_offset_width) = if new_line == 0 {
3173 let (line_end_index, line_end_offset_width) = if new_line == 0 {
2936 } else {
3175 } else {
2938− line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
3177+ line_end_char_index(&text, line_num),
2939 doc.line_ending.len_chars(),
3178 doc.line_ending.len_chars(),
@@ -2943,13 +3182,15 @@ fn open(cx: &mut Context, open: Open) {
2943 let indent = indent::indent_for_newline(
3182 let indent = indent::indent_for_newline(
2944 doc.language_config(),
3183 doc.language_config(),
2945 doc.syntax(),
3184 doc.syntax(),
3185+ &doc.config.load().indent_heuristic,
2946 &doc.indent_style,
3186 &doc.indent_style,
2947 doc.tab_width(),
3187 doc.tab_width(),
2949− new_line.saturating_sub(1),
3189+ line_num,
2950 line_end_index,
3190 line_end_index,
2951 cursor_line,
3191 cursor_line,
2953 let indent_len = indent.len();
3194 let indent_len = indent.len();
2954 let mut text = String::with_capacity(1 + indent_len);
3195 let mut text = String::with_capacity(1 + indent_len);
2955 text.push_str(doc.line_ending.as_str());
3196 text.push_str(doc.line_ending.as_str());
@@ -3248,8 +3489,7 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) {
3249 doc.set_selection(view.id, selection)
3490 doc.set_selection(view.id, selection)
3252− cx.editor.last_motion = Some(Motion(Box::new(motion)));
3492+ cx.editor.apply_motion(motion);
3255/// Returns the [Range] for a [Hunk] in the given text.
3495/// Returns the [Range] for a [Hunk] in the given text.
@@ -3376,6 +3616,7 @@ pub mod insert {
3378 use helix_core::auto_pairs;
3618 use helix_core::auto_pairs;
3619+ use helix_view::editor::SmartTabConfig;
3380 pub fn insert_char(cx: &mut Context, c: char) {
3621 pub fn insert_char(cx: &mut Context, c: char) {
3381 let (view, doc) = current_ref!(cx.editor);
3622 let (view, doc) = current_ref!(cx.editor);
@@ -3401,6 +3642,31 @@ pub mod insert {
3645+ pub fn smart_tab(cx: &mut Context) {
3646+ let (view, doc) = current_ref!(cx.editor);
3647+ let view_id = view.id;
3650+ cx.editor.config().smart_tab,
3651+ Some(SmartTabConfig { enable: true, .. })
3653+ let cursors_after_whitespace = doc.selection(view_id).ranges().iter().all(|range| {
3654+ let cursor = range.cursor(doc.text().slice(..));
3655+ let current_line_num = doc.text().char_to_line(cursor);
3656+ let current_line_start = doc.text().line_to_char(current_line_num);
3657+ let left = doc.text().slice(current_line_start..cursor);
3658+ left.chars().all(|c| c.is_whitespace())
3661+ if !cursors_after_whitespace {
3662+ move_parent_node_end(cx);
3404 pub fn insert_tab(cx: &mut Context) {
3670 pub fn insert_tab(cx: &mut Context) {
3405 let (view, doc) = current!(cx.editor);
3671 let (view, doc) = current!(cx.editor);
3406 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
3672 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
@@ -3456,6 +3722,7 @@ pub mod insert {
3456 let indent = indent::indent_for_newline(
3722 let indent = indent::indent_for_newline(
3457 doc.language_config(),
3723 doc.language_config(),
3458 doc.syntax(),
3724 doc.syntax(),
3725+ &doc.config.load().indent_heuristic,
3459 &doc.indent_style,
3726 &doc.indent_style,
3460 doc.tab_width(),
3727 doc.tab_width(),
@@ -3696,7 +3963,22 @@ fn commit_undo_checkpoint(cx: &mut Context) {
3696// Yank / Paste
3963// Yank / Paste
3698fn yank(cx: &mut Context) {
3965fn yank(cx: &mut Context) {
3966+ yank_impl(cx.editor, cx.register.unwrap_or('"'));
3967+ exit_select_mode(cx);
3970+fn yank_to_clipboard(cx: &mut Context) {
3971+ yank_impl(cx.editor, '+');
3972+ exit_select_mode(cx);
3975+fn yank_to_primary_clipboard(cx: &mut Context) {
3976+ yank_impl(cx.editor, '*');
3977+ exit_select_mode(cx);
3980+fn yank_impl(editor: &mut Editor, register: char) {
3699− let (view, doc) = current!(cx.editor);
3981+ let (view, doc) = current!(editor);
3700 let text = doc.text().slice(..);
3982 let text = doc.text().slice(..);
3702 let values: Vec<String> = doc
3984 let values: Vec<String> = doc
@@ -3704,19 +3986,15 @@ fn yank(cx: &mut Context) {
3704 .fragments(text)
3986 .fragments(text)
3705 .map(Cow::into_owned)
3987 .map(Cow::into_owned)
3706 .collect();
3988 .collect();
3989+ let selections = values.len();
3708− let msg = format!(
3991+ match editor.registers.write(register, values) {
3992+ Ok(_) => editor.set_status(format!(
3709− "yanked {} selection(s) to register {}",
3993+ "yanked {selections} selection{} to register {register}",
3710− values.len(),
3994+ if selections == 1 { "" } else { "s" }
3711− cx.register.unwrap_or('"')
3995+ )),
3712− );
3996+ Err(err) => editor.set_error(err.to_string()),
3716− .write(cx.register.unwrap_or('"'), values);
3718− cx.editor.set_status(msg);
3719− exit_select_mode(cx);
3722fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
4000fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
@@ -3724,6 +4002,7 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
3724 let text = doc.text().slice(..);
4002 let text = doc.text().slice(..);
3726 let selection = doc.selection(view.id);
4004 let selection = doc.selection(view.id);
4005+ let selections = selection.len();
3727 let joined = selection
4006 let joined = selection
3728 .fragments(text)
4007 .fragments(text)
3729 .fold(String::new(), |mut acc, fragment| {
4008 .fold(String::new(), |mut acc, fragment| {
@@ -3734,104 +4013,52 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
3738− "joined and yanked {} selection(s) to register {}",
3743− editor.registers.write(register, vec![joined]);
4016+ match editor.registers.write(register, vec![joined]) {
3744− editor.set_status(msg);
4017+ Ok(_) => editor.set_status(format!(
4018+ "joined and yanked {selections} selection{} to register {register}",
4019+ if selections == 1 { "" } else { "s" }
4021+ Err(err) => editor.set_error(err.to_string()),
3747fn yank_joined(cx: &mut Context) {
4025fn yank_joined(cx: &mut Context) {
3748− let line_ending = doc!(cx.editor).line_ending;
4026+ let separator = doc!(cx.editor).line_ending.as_str();
3749− let register = cx.register.unwrap_or('"');
4027+ yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"'));
3750− yank_joined_impl(cx.editor, line_ending.as_str(), register);
3751 exit_select_mode(cx);
4028 exit_select_mode(cx);
3754−fn yank_joined_to_clipboard_impl(
4031+fn yank_joined_to_clipboard(cx: &mut Context) {
3755− editor: &mut Editor,
4032+ let line_ending = doc!(cx.editor).line_ending;
3756− separator: &str,
4033+ yank_joined_impl(cx.editor, line_ending.as_str(), '+');
3757− clipboard_type: ClipboardType,
4034+ exit_select_mode(cx);
3758−) -> anyhow::Result<()> {
3759− let (view, doc) = current!(editor);
3760− let text = doc.text().slice(..);
3762− let values: Vec<String> = doc
3763− .selection(view.id)
3765− .map(Cow::into_owned)
3768− let clipboard_text = match clipboard_type {
3769− ClipboardType::Clipboard => "system clipboard",
3770− ClipboardType::Selection => "primary clipboard",
3774− "joined and yanked {} selection(s) to {}",
3779− let joined = values.join(separator);
3782− .clipboard_provider
3783− .set_contents(joined, clipboard_type)
3784− .context("Couldn't set system clipboard content")?;
3786− editor.set_status(msg);
3791−fn yank_joined_to_clipboard(cx: &mut Context) {
4037+fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3792 let line_ending = doc!(cx.editor).line_ending;
4038 let line_ending = doc!(cx.editor).line_ending;
3794− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
4039+ yank_joined_impl(cx.editor, line_ending.as_str(), '*');
3795 exit_select_mode(cx);
4040 exit_select_mode(cx);
3798−fn yank_main_selection_to_clipboard_impl(
3799− editor: &mut Editor,
4043+fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
3800− clipboard_type: ClipboardType,
3801−) -> anyhow::Result<()> {
3802 let (view, doc) = current!(editor);
4044 let (view, doc) = current!(editor);
3803 let text = doc.text().slice(..);
4045 let text = doc.text().slice(..);
3805− let message_text = match clipboard_type {
3806− ClipboardType::Clipboard => "yanked main selection to system clipboard",
3807− ClipboardType::Selection => "yanked main selection to primary clipboard",
3810− let value = doc.selection(view.id).primary().fragment(text);
4047+ let selection = doc.selection(view.id).primary().fragment(text).to_string();
3812− if let Err(e) = editor
4049+ match editor.registers.write(register, vec![selection]) {
3813− .clipboard_provider
4050+ Ok(_) => editor.set_status(format!("yanked primary selection to register {register}",)),
3814− .set_contents(value.into_owned(), clipboard_type)
4051+ Err(err) => editor.set_error(err.to_string()),
3816− bail!("Couldn't set system clipboard content: {}", e);
3819− editor.set_status(message_text);
3823fn yank_main_selection_to_clipboard(cx: &mut Context) {
4055fn yank_main_selection_to_clipboard(cx: &mut Context) {
3824− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
4056+ yank_primary_selection_impl(cx.editor, '+');
3825−}
4057+ exit_select_mode(cx);
3827−fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3828− let line_ending = doc!(cx.editor).line_ending;
3830− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
3833fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
4060fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
3834− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
4061+ yank_primary_selection_impl(cx.editor, '*');
3835 exit_select_mode(cx);
4062 exit_select_mode(cx);
@@ -3929,146 +4156,99 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
3930 let (view, doc) = current!(cx.editor);
4157 let (view, doc) = current!(cx.editor);
3931 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
4158 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
3932−}
4159+ exit_select_mode(cx);
3934−fn paste_clipboard_impl(
3935− editor: &mut Editor,
3937− clipboard_type: ClipboardType,
3939−) -> anyhow::Result<()> {
3940− let (view, doc) = current!(editor);
3941− match editor.clipboard_provider.get_contents(clipboard_type) {
3943− paste_impl(&[contents], doc, view, action, count, editor.mode);
3946− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3950fn paste_clipboard_after(cx: &mut Context) {
4162fn paste_clipboard_after(cx: &mut Context) {
3951− let _ = paste_clipboard_impl(
3953− Paste::After,
4163+ paste(cx.editor, '+', Paste::After, cx.count());
3954− ClipboardType::Clipboard,
4164+ exit_select_mode(cx);
3959fn paste_clipboard_before(cx: &mut Context) {
4167fn paste_clipboard_before(cx: &mut Context) {
3960− let _ = paste_clipboard_impl(
3962− Paste::Before,
4168+ paste(cx.editor, '+', Paste::Before, cx.count());
3963− ClipboardType::Clipboard,
4169+ exit_select_mode(cx);
3968fn paste_primary_clipboard_after(cx: &mut Context) {
4172fn paste_primary_clipboard_after(cx: &mut Context) {
3969− let _ = paste_clipboard_impl(
3971− Paste::After,
4173+ paste(cx.editor, '*', Paste::After, cx.count());
3972− ClipboardType::Selection,
4174+ exit_select_mode(cx);
3977fn paste_primary_clipboard_before(cx: &mut Context) {
4177fn paste_primary_clipboard_before(cx: &mut Context) {
3978− let _ = paste_clipboard_impl(
3980− Paste::Before,
4178+ paste(cx.editor, '*', Paste::Before, cx.count());
3981− ClipboardType::Selection,
4179+ exit_select_mode(cx);
3986fn replace_with_yanked(cx: &mut Context) {
4182fn replace_with_yanked(cx: &mut Context) {
3987− let count = cx.count();
3988− let reg_name = cx.register.unwrap_or('"');
4183+ replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
3989− let (view, doc) = current!(cx.editor);
3990− let registers = &mut cx.editor.registers;
3992− if let Some(values) = registers.read(reg_name) {
3993− if !values.is_empty() {
3994− let repeat = std::iter::repeat(
3997− .map(|value| Tendril::from(&value.repeat(count)))
4000− let mut values = values
4002− .map(|value| Tendril::from(&value.repeat(count)))
4004− let selection = doc.selection(view.id);
4005− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4006− if !range.is_empty() {
4007− (range.from(), range.to(), Some(values.next().unwrap()))
4009− (range.from(), range.to(), None)
4013− doc.apply(&transaction, view.id);
4014− exit_select_mode(cx);
4184+ exit_select_mode(cx);
4019−fn replace_selections_with_clipboard_impl(
4187+fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) {
4020− cx: &mut Context,
4188+ let Some(values) = editor.registers
4021− clipboard_type: ClipboardType,
4189+ .read(register, editor)
4022−) -> anyhow::Result<()> {
4190+ .filter(|values| values.len() > 0) else { return };
4023− let count = cx.count();
4191+ let values: Vec<_> = values.map(|value| value.to_string()).collect();
4024− let (view, doc) = current!(cx.editor);
4026− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
4028− let selection = doc.selection(view.id);
4029− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4033− Some(contents.repeat(count).as_str().into()),
4037− doc.apply(&transaction, view.id);
4193+ let (view, doc) = current!(editor);
4038− doc.append_changes_to_history(view);
4194+ let repeat = std::iter::repeat(
4197+ .map(|value| Tendril::from(&value.repeat(count)))
4200+ let mut values = values
4202+ .map(|value| Tendril::from(&value.repeat(count)))
4204+ let selection = doc.selection(view.id);
4205+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4206+ if !range.is_empty() {
4207+ (range.from(), range.to(), Some(values.next().unwrap()))
4209+ (range.from(), range.to(), None)
4040− Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
4043− exit_select_mode(cx);
4213+ doc.apply(&transaction, view.id);
4047fn replace_selections_with_clipboard(cx: &mut Context) {
4216fn replace_selections_with_clipboard(cx: &mut Context) {
4048− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard);
4217+ replace_with_yanked_impl(cx.editor, '+', cx.count());
4218+ exit_select_mode(cx);
4051fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4221fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4052− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection);
4222+ replace_with_yanked_impl(cx.editor, '*', cx.count());
4223+ exit_select_mode(cx);
4055−fn paste(cx: &mut Context, pos: Paste) {
4226+fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) {
4056− let count = cx.count();
4227+ let Some(values) = editor.registers.read(register, editor) else { return };
4057− let reg_name = cx.register.unwrap_or('"');
4228+ let values: Vec<_> = values.map(|value| value.to_string()).collect();
4058− let (view, doc) = current!(cx.editor);
4059− let registers = &mut cx.editor.registers;
4061− if let Some(values) = registers.read(reg_name) {
4230+ let (view, doc) = current!(editor);
4062− paste_impl(values, doc, view, pos, count, cx.editor.mode);
4231+ paste_impl(&values, doc, view, pos, count, editor.mode);
4066fn paste_after(cx: &mut Context) {
4234fn paste_after(cx: &mut Context) {
4237+ cx.register.unwrap_or('"'),
4067− paste(cx, Paste::After)
4238+ Paste::After,
4241+ exit_select_mode(cx);
4070fn paste_before(cx: &mut Context) {
4244fn paste_before(cx: &mut Context) {
4247+ cx.register.unwrap_or('"'),
4071− paste(cx, Paste::Before)
4248+ Paste::Before,
4251+ exit_select_mode(cx);
4074fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
4254fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
@@ -4107,6 +4287,7 @@ fn indent(cx: &mut Context) {
4109 doc.apply(&transaction, view.id);
4289 doc.apply(&transaction, view.id);
4290+ exit_select_mode(cx);
4112fn unindent(cx: &mut Context) {
4293fn unindent(cx: &mut Context) {
@@ -4146,6 +4327,7 @@ fn unindent(cx: &mut Context) {
4146 let transaction = Transaction::change(doc.text(), changes.into_iter());
4327 let transaction = Transaction::change(doc.text(), changes.into_iter());
4148 doc.apply(&transaction, view.id);
4329 doc.apply(&transaction, view.id);
4330+ exit_select_mode(cx);
4151fn format_selections(cx: &mut Context) {
4333fn format_selections(cx: &mut Context) {
@@ -4175,7 +4357,7 @@ fn format_selections(cx: &mut Context) {
4177 cx.editor
4359 cx.editor
4178− .set_error("No configured language server does not support range formatting");
4360+ .set_error("No configured language server supports range formatting");
@@ -4212,10 +4394,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4212 use movement::skip_while;
4394 use movement::skip_while;
4213 let (view, doc) = current!(cx.editor);
4395 let (view, doc) = current!(cx.editor);
4214 let text = doc.text();
4396 let text = doc.text();
4215− let slice = doc.text().slice(..);
4397+ let slice = text.slice(..);
4217 let mut changes = Vec::new();
4399 let mut changes = Vec::new();
4218− let fragment = Tendril::from(" ");
4220 for selection in doc.selection(view.id) {
4401 for selection in doc.selection(view.id) {
4221 let (start, mut end) = selection.line_range(slice);
4402 let (start, mut end) = selection.line_range(slice);
@@ -4231,9 +4412,13 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4231 let mut end = text.line_to_char(line + 1);
4412 let mut end = text.line_to_char(line + 1);
4232 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
4413 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
4234− // need to skip from start, not end
4415+ let separator = if end == line_end_char_index(&slice, line + 1) {
4235− let change = (start, end, Some(fragment.clone()));
4416+ // the joining line contains only space-characters => don't include a whitespace when joining
4419+ Some(Tendril::from(" "))
4236− changes.push(change);
4421+ changes.push((start, end, separator));
@@ -4245,9 +4430,6 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4245 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
4430 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
4246 changes.dedup();
4431 changes.dedup();
4248− // TODO: joining multiple empty lines should be replaced by a single space.
4249− // need to merge change ranges that touch
4251 // select inserted spaces
4433 // select inserted spaces
4252 let transaction = if select_space {
4434 let transaction = if select_space {
4253 let ranges: SmallVec<_> = changes
4435 let ranges: SmallVec<_> = changes
@@ -4259,9 +4441,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
4260 .collect();
4442 .collect();
4261 let selection = Selection::new(ranges, 0);
4443 let selection = Selection::new(ranges, 0);
4262− Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
4444+ Transaction::change(text, changes.into_iter()).with_selection(selection)
4263 } else {
4445 } else {
4264− Transaction::change(doc.text(), changes.into_iter())
4446+ Transaction::change(text, changes.into_iter())
4267 doc.apply(&transaction, view.id);
4449 doc.apply(&transaction, view.id);
@@ -4275,8 +4457,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
4275 if remove { "remove:" } else { "keep:" }.into(),
4457 if remove { "remove:" } else { "keep:" }.into(),
4276 Some(reg),
4458 Some(reg),
4277 ui::completers::none,
4459 ui::completers::none,
4278− move |editor, regex, event| {
4460+ move |cx, regex, event| {
4279− let (view, doc) = current!(editor);
4461+ let (view, doc) = current!(cx.editor);
4280 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
4462 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
@@ -4584,8 +4766,7 @@ fn expand_selection(cx: &mut Context) {
4588− cx.editor.last_motion = Some(Motion(Box::new(motion)));
4769+ cx.editor.apply_motion(motion);
4591fn shrink_selection(cx: &mut Context) {
4772fn shrink_selection(cx: &mut Context) {
@@ -4609,8 +4790,7 @@ fn shrink_selection(cx: &mut Context) {
4609 doc.set_selection(view.id, selection);
4790 doc.set_selection(view.id, selection);
4613− cx.editor.last_motion = Some(Motion(Box::new(motion)));
4793+ cx.editor.apply_motion(motion);
4616fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
4796fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
@@ -4628,8 +4808,7 @@ where
4628 doc.set_selection(view.id, selection);
4808 doc.set_selection(view.id, selection);
4632− cx.editor.last_motion = Some(Motion(Box::new(motion)));
4811+ cx.editor.apply_motion(motion);
4635fn select_next_sibling(cx: &mut Context) {
4814fn select_next_sibling(cx: &mut Context) {
@@ -4640,6 +4819,49 @@ fn select_prev_sibling(cx: &mut Context) {
4640 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
4819 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
4822+fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
4823+ let motion = move |editor: &mut Editor| {
4824+ let (view, doc) = current!(editor);
4826+ if let Some(syntax) = doc.syntax() {
4827+ let text = doc.text().slice(..);
4828+ let current_selection = doc.selection(view.id);
4830+ let selection = movement::move_parent_node_end(
4833+ current_selection.clone(),
4838+ doc.set_selection(view.id, selection);
4840+ // [TODO] temporary workaround until we're not using the idle timer to
4841+ // trigger auto completions any more
4842+ editor.clear_idle_timer();
4846+ cx.editor.apply_motion(motion);
4849+pub fn move_parent_node_end(cx: &mut Context) {
4850+ move_node_bound_impl(cx, Direction::Forward, Movement::Move)
4853+pub fn move_parent_node_start(cx: &mut Context) {
4854+ move_node_bound_impl(cx, Direction::Backward, Movement::Move)
4857+pub fn extend_parent_node_end(cx: &mut Context) {
4858+ move_node_bound_impl(cx, Direction::Forward, Movement::Extend)
4861+pub fn extend_parent_node_start(cx: &mut Context) {
4862+ move_node_bound_impl(cx, Direction::Backward, Movement::Extend)
4643fn match_brackets(cx: &mut Context) {
4865fn match_brackets(cx: &mut Context) {
4644 let (view, doc) = current!(cx.editor);
4866 let (view, doc) = current!(cx.editor);
4645 let is_select = cx.editor.mode == Mode::Select;
4867 let is_select = cx.editor.mode == Mode::Select;
@@ -4753,17 +4975,19 @@ fn transpose_view(cx: &mut Context) {
4753 cx.editor.transpose_view()
4975 cx.editor.transpose_view()
4756−// split helper, clear it later
4978+/// Open a new split in the given direction specified by the action.
4980+/// Maintain the current view (both the cursor's position and view in document).
4757−fn split(cx: &mut Context, action: Action) {
4981+fn split(editor: &mut Editor, action: Action) {
4758− let (view, doc) = current!(cx.editor);
4982+ let (view, doc) = current!(editor);
4759 let id = doc.id();
4983 let id = doc.id();
4760 let selection = doc.selection(view.id).clone();
4984 let selection = doc.selection(view.id).clone();
4761 let offset = view.offset;
4985 let offset = view.offset;
4763− cx.editor.switch(id, action);
4987+ editor.switch(id, action);
4765 // match the selection in the previous view
4989 // match the selection in the previous view
4766− let (view, doc) = current!(cx.editor);
4990+ let (view, doc) = current!(editor);
4767 doc.set_selection(view.id, selection);
4991 doc.set_selection(view.id, selection);
4768 // match the view scroll offset (switch doesn't handle this fully
4992 // match the view scroll offset (switch doesn't handle this fully
4769 // since the selection is only matched after the split)
4993 // since the selection is only matched after the split)
@@ -4771,7 +4995,7 @@ fn split(cx: &mut Context, action: Action) {
4773fn hsplit(cx: &mut Context) {
4997fn hsplit(cx: &mut Context) {
4774− split(cx, Action::HorizontalSplit);
4998+ split(cx.editor, Action::HorizontalSplit);
4777fn hsplit_new(cx: &mut Context) {
5001fn hsplit_new(cx: &mut Context) {
@@ -4779,7 +5003,7 @@ fn hsplit_new(cx: &mut Context) {
4781fn vsplit(cx: &mut Context) {
5005fn vsplit(cx: &mut Context) {
4782− split(cx, Action::VerticalSplit);
5006+ split(cx.editor, Action::VerticalSplit);
4785fn vsplit_new(cx: &mut Context) {
5009fn vsplit_new(cx: &mut Context) {
@@ -4828,7 +5052,12 @@ fn insert_register(cx: &mut Context) {
4828 if let Some(ch) = event.char() {
5052 if let Some(ch) = event.char() {
4829 cx.editor.autoinfo = None;
5053 cx.editor.autoinfo = None;
4830 cx.register = Some(ch);
5054 cx.register = Some(ch);
5057+ cx.register.unwrap_or('"'),
4831− paste(cx, Paste::Cursor);
5058+ Paste::Cursor,
@@ -4912,8 +5141,7 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
4912 editor.set_status("Syntax-tree is not available in current buffer");
5141 editor.set_status("Syntax-tree is not available in current buffer");
4916− cx.editor.last_motion = Some(Motion(Box::new(motion)));
5144+ cx.editor.apply_motion(motion);
4919fn goto_next_function(cx: &mut Context) {
5147fn goto_next_function(cx: &mut Context) {
@@ -5034,8 +5262,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
5035 doc.set_selection(view.id, selection);
5263 doc.set_selection(view.id, selection);
5037− textobject(cx.editor);
5038− cx.editor.last_motion = Some(Motion(Box::new(textobject)));
5265+ cx.editor.apply_motion(textobject);
@@ -5125,11 +5352,18 @@ fn surround_replace(cx: &mut Context) {
5355+ let selection = selection.clone();
5356+ let ranges: SmallVec<[Range; 1]> = change_pos.iter().map(|&p| Range::point(p)).collect();
5359+ Selection::new(ranges, selection.primary_index() * 2),
5128 cx.on_next_key(move |cx, event| {
5362 cx.on_next_key(move |cx, event| {
5129 let (view, doc) = current!(cx.editor);
5363 let (view, doc) = current!(cx.editor);
5130 let to = match event.char() {
5364 let to = match event.char() {
5131 Some(to) => to,
5365 Some(to) => to,
5132− None => return,
5366+ None => return doc.set_selection(view.id, selection),
5134 let (open, close) = surround::get_pair(to);
5368 let (open, close) = surround::get_pair(to);
5135 let transaction = Transaction::change(
5369 let transaction = Transaction::change(
@@ -5140,6 +5374,7 @@ fn surround_replace(cx: &mut Context) {
5140 (pos, pos + 1, Some(t))
5374 (pos, pos + 1, Some(t))
5377+ doc.set_selection(view.id, selection);
5143 doc.apply(&transaction, view.id);
5378 doc.apply(&transaction, view.id);
5144 exit_select_mode(cx);
5379 exit_select_mode(cx);
@@ -5374,12 +5609,18 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
5376 // These `usize`s cannot underflow because selection ranges cannot overlap.
5611 // These `usize`s cannot underflow because selection ranges cannot overlap.
5377− // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
5612+ let anchor = to
5378− // arithmetic to assert this.
5613+ .checked_add_signed(offset)
5379− let anchor = (to as isize + offset - deleted_len as isize) as usize;
5614+ .expect("Selection ranges cannot overlap")
5615+ .checked_sub(deleted_len)
5616+ .expect("Selection ranges cannot overlap");
5380 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
5617 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
5381 ranges.push(new_range);
5618 ranges.push(new_range);
5382− offset = offset + output_len as isize - deleted_len as isize;
5619+ offset = offset
5620+ .checked_add_unsigned(output_len)
5621+ .expect("Selection ranges cannot overlap")
5622+ .checked_sub_unsigned(deleted_len)
5623+ .expect("Selection ranges cannot overlap");
5384 changes.push((from, to, Some(output)));
5625 changes.push((from, to, Some(output)));
@@ -5518,6 +5759,7 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
5518 let transaction = Transaction::change(doc.text(), changes.into_iter());
5759 let transaction = Transaction::change(doc.text(), changes.into_iter());
5519 let transaction = transaction.with_selection(new_selection);
5760 let transaction = transaction.with_selection(new_selection);
5520 doc.apply(&transaction, view.id);
5761 doc.apply(&transaction, view.id);
5762+ exit_select_mode(cx);
@@ -5536,9 +5778,12 @@ fn record_macro(cx: &mut Context) {
5538 .collect::<String>();
5780 .collect::<String>();
5539− cx.editor.registers.write(reg, vec![s]);
5781+ match cx.editor.registers.write(reg, vec![s]) {
5540− cx.editor
5783+ .editor
5541− .set_status(format!("Recorded to register [{}]", reg));
5784+ .set_status(format!("Recorded to register [{}]", reg)),
5785+ Err(err) => cx.editor.set_error(err.to_string()),
5542 } else {
5787 } else {
5543 let reg = cx.register.take().unwrap_or('@');
5788 let reg = cx.register.take().unwrap_or('@');
5544 cx.editor.macro_recording = Some((reg, Vec::new()));
5789 cx.editor.macro_recording = Some((reg, Vec::new()));
@@ -5558,8 +5803,14 @@ fn replay_macro(cx: &mut Context) {
5561− let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
5806+ let keys: Vec<KeyEvent> = if let Some(keys) = cx
5809+ .read(reg, cx.editor)
5810+ .filter(|values| values.len() == 1)
5811+ .map(|mut values| values.next().unwrap())
5562− match helix_view::input::parse_macro(keys_str) {
5813+ match helix_view::input::parse_macro(&keys) {
5563 Ok(keys) => keys,
5814 Ok(keys) => keys,
5564 Err(err) => {
5815 Err(err) => {
5565 cx.editor.set_error(format!("Invalid macro: {}", err));
5816 cx.editor.set_error(format!("Invalid macro: {}", err));