@@ -29,11 +29,12 @@ use helix_core::{
2929 tree_sitter::Node,
3030 unicode::width::UnicodeWidthChar,
3131 visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
32− RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
32+ RopeSlice, Selection, SmallVec, Tendril, Transaction,
3333};
3434use helix_view::{
35+ clipboard::ClipboardType,
3536 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
36− editor::{Action, CompleteAction},
37+ editor::{Action, CompleteAction, Motion},
3738 info::Info,
3839 input::KeyEvent,
3940 keyboard::KeyCode,
@@ -43,6 +44,7 @@ use helix_view::{
4344};
4445
4546use anyhow::{anyhow, bail, ensure, Context as _};
47+use fuzzy_matcher::FuzzyMatcher;
4648use insert::*;
4749use movement::Movement;
4850
@@ -59,14 +61,9 @@ use crate::{
5961};
6062
6163use crate::job::{self, Jobs};
62−use futures_util::{stream::FuturesUnordered, TryStreamExt};
63−use std::{
64− collections::{HashMap, HashSet},
65− fmt,
66− future::Future,
67− io::Read,
68− num::NonZeroUsize,
69−};
64+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
65+use std::{collections::HashMap, fmt, future::Future};
66+use std::{collections::HashSet, num::NonZeroUsize};
7067
7168use std::{
7269 borrow::Cow,
@@ -75,11 +72,11 @@ use std::{
7572
7673use once_cell::sync::Lazy;
7774use serde::de::{self, Deserialize, Deserializer};
78−use url::Url;
7975
8076use grep_regex::RegexMatcherBuilder;
8177use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
8278use ignore::{DirEntry, WalkBuilder, WalkState};
79+use tokio_stream::wrappers::UnboundedReceiverStream;
8380
8481pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
8582
@@ -251,8 +248,6 @@ impl MappableCommand {
251248 move_prev_long_word_start, "Move to start of previous long word",
252249 move_next_long_word_end, "Move to end of next long word",
253250 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",
256251 extend_next_word_start, "Extend to start of next word",
257252 extend_prev_word_start, "Extend to start of previous word",
258253 extend_next_word_end, "Extend to end of next word",
@@ -261,8 +256,6 @@ impl MappableCommand {
261256 extend_prev_long_word_start, "Extend to start of previous long word",
262257 extend_next_long_word_end, "Extend to end of next long word",
263258 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",
266259 find_till_char, "Move till next occurrence of char",
267260 find_next_char, "Move to next occurrence of char",
268261 extend_till_char, "Extend till next occurrence of char",
@@ -337,7 +330,7 @@ impl MappableCommand {
337330 goto_implementation, "Goto implementation",
338331 goto_file_start, "Goto line number <n> else file start",
339332 goto_file_end, "Goto file end",
340− goto_file, "Goto files/URLs in selection",
333+ goto_file, "Goto files in selection",
341334 goto_file_hsplit, "Goto files in selection (hsplit)",
342335 goto_file_vsplit, "Goto files in selection (vsplit)",
343336 goto_reference, "Goto references",
@@ -369,7 +362,6 @@ impl MappableCommand {
369362 extend_to_line_end, "Extend to line end",
370363 extend_to_line_end_newline, "Extend to line end",
371364 signature_help, "Show signature help",
372− smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
373365 insert_tab, "Insert tab char",
374366 insert_newline, "Insert newline char",
375367 delete_char_backward, "Delete previous char",
@@ -384,8 +376,6 @@ impl MappableCommand {
384376 later, "Move forward in history",
385377 commit_undo_checkpoint, "Commit changes to new checkpoint",
386378 yank, "Yank selection",
387− yank_to_clipboard, "Yank selections to clipboard",
388− yank_to_primary_clipboard, "Yank selections to primary clipboard",
389379 yank_joined, "Join and yank selections",
390380 yank_joined_to_clipboard, "Join and yank selections to clipboard",
391381 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -990,7 +980,6 @@ fn align_selections(cx: &mut Context) {
990980
991981 let transaction = Transaction::change(doc.text(), changes.into_iter());
992982 doc.apply(&transaction, view.id);
993− exit_select_mode(cx);
994983}
995984
996985fn goto_window(cx: &mut Context, align: Align) {
@@ -1110,7 +1099,8 @@ where
11101099 .transform(|range| move_fn(text, range, count, behavior));
11111100 doc.set_selection(view.id, selection);
11121101 };
1113− cx.editor.apply_motion(motion)
1102+ motion(cx.editor);
1103+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
11141104}
11151105
11161106fn goto_prev_paragraph(cx: &mut Context) {
@@ -1165,10 +1155,6 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11651155 let (view, doc) = current_ref!(cx.editor);
11661156 let text = doc.text();
11671157 let selections = doc.selection(view.id);
1168− let rel_path = doc
1169− .relative_path()
1170− .map(|path| path.parent().unwrap().to_path_buf())
1171− .unwrap_or_default();
11721158 let mut paths: Vec<_> = selections
11731159 .iter()
11741160 .map(|r| text.slice(r.from()..r.to()).to_string())
@@ -1196,80 +1182,16 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11961182 .to_string(),
11971183 );
11981184 }
1199−
12001185 for sel in paths {
12011186 let p = sel.trim();
1202− if p.is_empty() {
1203− continue;
1204− }
1205−
1206− if let Ok(url) = Url::parse(p) {
1207− return open_url(cx, url, action);
1208− }
1209−
1210− let path = &rel_path.join(p);
1211− if path.is_dir() {
1212− let picker = ui::file_picker(path.into(), &cx.editor.config());
1213− cx.push_layer(Box::new(overlaid(picker)));
1214− } else if let Err(e) = cx.editor.open(path, action) {
1215− cx.editor.set_error(format!("Open file failed: {:?}", e));
1216− }
1217− }
1218−}
1219−
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);
1224− let rel_path = doc
1225− .relative_path()
1226− .map(|path| path.parent().unwrap().to_path_buf())
1227− .unwrap_or_default();
1228−
1229− if url.scheme() != "file" {
1230− return open_external_url(cx, url);
1231− }
1232−
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]))
1238− });
1239−
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());
1246− if path.is_dir() {
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) {
1187+ if !p.is_empty() {
1188+ if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
12501189 cx.editor.set_error(format!("Open file failed: {:?}", e));
12511190 }
12521191 }
12531192 }
12541193}
12551194
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(|_| {})));
1265− }
1266− }
1267− Ok(job::Callback::Editor(Box::new(move |editor| {
1268− editor.set_error("Opening URL in external program failed")
1269− })))
1270− });
1271−}
1272−
12731195fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
12741196where
12751197 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1318,66 +1240,10 @@ fn extend_next_long_word_end(cx: &mut Context) {
13181240 extend_word_impl(cx, movement::move_next_long_word_end)
13191241}
13201242
1321−/// Separate branch to find_char designed only for `<ret>` char.
1322−//
1323−// This is necessary because the one document can have different line endings inside. And we
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(
1327− cx: &mut Context,
1328− count: usize,
1329− direction: Direction,
1330− inclusive: bool,
1331− extend: bool,
1332−) {
1333− let (view, doc) = current!(cx.editor);
1334− let text = doc.text().slice(..);
1335−
1336− let selection = doc.selection(view.id).clone().transform(|range| {
1337− let cursor = range.cursor(text);
1338− let cursor_line = range.cursor_line(text);
1339−
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 {
1348− return range;
1349− } else {
1350− line
1351− }
1352− }
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);
1356− if line <= 0 {
1357− return range;
1358− } else {
1359− line as usize
1360− }
1361− }
1362− };
1363−
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),
1369− };
1370−
1371− if extend {
1372− range.put_cursor(text, pos, true)
1373− } else {
1374− Range::point(range.cursor(text)).put_cursor(text, pos, true)
1375− }
1376− });
1377− doc.set_selection(view.id, selection);
1378−}
1379−
1380−fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) {
1243+fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
1244+where
1245+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
1246+{
13811247 // TODO: count is reset to 1 before next key so we move it into the closure here.
13821248 // Would be nice to carry over.
13831249 let count = cx.count();
@@ -1390,9 +1256,13 @@ fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bo
13901256 KeyEvent {
13911257 code: KeyCode::Enter,
13921258 ..
1393− } => {
1394− find_char_line_ending(cx, count, direction, inclusive, extend);
1395− return;
1259+ } =>
1260+ // TODO: this isn't quite correct when CRLF is involved.
1261+ // This hack will work in most cases, since documents don't
1262+ // usually mix line endings. But we should fix it eventually
1263+ // anyway.
1264+ {
1265+ doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
13961266 }
13971267
13981268 KeyEvent {
@@ -1405,18 +1275,11 @@ fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bo
14051275 } => ch,
14061276 _ => return,
14071277 };
1408− let motion = move |editor: &mut Editor| {
1409− match direction {
1410− Direction::Forward => {
1411− find_char_impl(editor, &find_next_char_impl, inclusive, extend, ch, count)
1412− }
1413− Direction::Backward => {
1414− find_char_impl(editor, &find_prev_char_impl, inclusive, extend, ch, count)
1415− }
1416− };
1417− };
14181278
1419− cx.editor.apply_motion(motion);
1279+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
1280+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
1281+ find_char_impl(editor, &search_fn, inclusive, extend, ch, 1);
1282+ })));
14201283 })
14211284}
14221285
@@ -1495,39 +1358,46 @@ fn find_prev_char_impl(
14951358}
14961359
14971360fn find_till_char(cx: &mut Context) {
1498− find_char(cx, Direction::Forward, false, false);
1361+ will_find_char(cx, find_next_char_impl, false, false)
14991362}
15001363
15011364fn find_next_char(cx: &mut Context) {
1502− find_char(cx, Direction::Forward, true, false)
1365+ will_find_char(cx, find_next_char_impl, true, false)
15031366}
15041367
15051368fn extend_till_char(cx: &mut Context) {
1506− find_char(cx, Direction::Forward, false, true)
1369+ will_find_char(cx, find_next_char_impl, false, true)
15071370}
15081371
15091372fn extend_next_char(cx: &mut Context) {
1510− find_char(cx, Direction::Forward, true, true)
1373+ will_find_char(cx, find_next_char_impl, true, true)
15111374}
15121375
15131376fn till_prev_char(cx: &mut Context) {
1514− find_char(cx, Direction::Backward, false, false)
1377+ will_find_char(cx, find_prev_char_impl, false, false)
15151378}
15161379
15171380fn find_prev_char(cx: &mut Context) {
1518− find_char(cx, Direction::Backward, true, false)
1381+ will_find_char(cx, find_prev_char_impl, true, false)
15191382}
15201383
15211384fn extend_till_prev_char(cx: &mut Context) {
1522− find_char(cx, Direction::Backward, false, true)
1385+ will_find_char(cx, find_prev_char_impl, false, true)
15231386}
15241387
15251388fn extend_prev_char(cx: &mut Context) {
1526− find_char(cx, Direction::Backward, true, true)
1389+ will_find_char(cx, find_prev_char_impl, true, true)
15271390}
15281391
15291392fn repeat_last_motion(cx: &mut Context) {
1530− cx.editor.repeat_last_motion(cx.count())
1393+ let count = cx.count();
1394+ let last_motion = cx.editor.last_motion.take();
1395+ if let Some(m) = &last_motion {
1396+ for _ in 0..count {
1397+ m.run(cx.editor);
1398+ }
1399+ cx.editor.last_motion = last_motion;
1400+ }
15311401}
15321402
15331403fn replace(cx: &mut Context) {
@@ -1594,7 +1464,6 @@ where
15941464 });
15951465
15961466 doc.apply(&transaction, view.id);
1597− exit_select_mode(cx);
15981467}
15991468
16001469fn switch_case(cx: &mut Context) {
@@ -1836,8 +1705,8 @@ fn select_regex(cx: &mut Context) {
18361705 "select:".into(),
18371706 Some(reg),
18381707 ui::completers::none,
1839− move |cx, regex, event| {
1840− let (view, doc) = current!(cx.editor);
1708+ move |editor, regex, event| {
1709+ let (view, doc) = current!(editor);
18411710 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
18421711 return;
18431712 }
@@ -1858,8 +1727,8 @@ fn split_selection(cx: &mut Context) {
18581727 "split:".into(),
18591728 Some(reg),
18601729 ui::completers::none,
1861− move |cx, regex, event| {
1862− let (view, doc) = current!(cx.editor);
1730+ move |editor, regex, event| {
1731+ let (view, doc) = current!(editor);
18631732 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
18641733 return;
18651734 }
@@ -1982,11 +1851,11 @@ fn search_impl(
19821851
19831852fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
19841853 let mut items = reg
1985− .and_then(|reg| cx.editor.registers.read(reg, cx.editor))
1986− .map_or(Vec::new(), |reg| reg.take(200).collect());
1854+ .and_then(|reg| cx.editor.registers.get(reg))
1855+ .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
19871856 items.sort_unstable();
19881857 items.dedup();
1989− items.into_iter().map(|value| value.to_string()).collect()
1858+ items.into_iter().cloned().collect()
19901859}
19911860
19921861fn search(cx: &mut Context) {
@@ -2023,14 +1892,12 @@ fn searcher(cx: &mut Context, direction: Direction) {
20231892 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
20241893 .collect()
20251894 },
2026− move |cx, regex, event| {
2027− if event == PromptEvent::Validate {
2028− cx.editor.registers.last_search_register = reg;
2029− } else if event != PromptEvent::Update {
1895+ move |editor, regex, event| {
1896+ if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
20301897 return;
20311898 }
20321899 search_impl(
2033− cx.editor,
1900+ editor,
20341901 &contents,
20351902 ®ex,
20361903 Movement::Move,
@@ -2045,13 +1912,11 @@ fn searcher(cx: &mut Context, direction: Direction) {
20451912
20461913fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
20471914 let count = cx.count();
2048− let register = cx
2049− .register
2050− .unwrap_or(cx.editor.registers.last_search_register);
20511915 let config = cx.editor.config();
20521916 let scrolloff = config.scrolloff;
2053− if let Some(query) = cx.editor.registers.first(register, cx.editor) {
2054− let doc = doc!(cx.editor);
1917+ let (_, doc) = current!(cx.editor);
1918+ let registers = &cx.editor.registers;
1919+ if let Some(query) = registers.read('/').and_then(|query| query.last()) {
20551920 let contents = doc.text().slice(..).to_string();
20561921 let search_config = &config.search;
20571922 let case_insensitive = if search_config.smart_case {
@@ -2060,7 +1925,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
20601925 false
20611926 };
20621927 let wrap_around = search_config.wrap_around;
2063− if let Ok(regex) = RegexBuilder::new(&query)
1928+ if let Ok(regex) = RegexBuilder::new(query)
20641929 .case_insensitive(case_insensitive)
20651930 .multi_line(true)
20661931 .build()
@@ -2100,7 +1965,6 @@ fn extend_search_prev(cx: &mut Context) {
21001965}
21011966
21021967fn search_selection(cx: &mut Context) {
2103− let register = cx.register.unwrap_or('/');
21041968 let (view, doc) = current!(cx.editor);
21051969 let contents = doc.text().slice(..);
21061970
@@ -2113,24 +1977,13 @@ fn search_selection(cx: &mut Context) {
21131977 .collect::<Vec<_>>()
21141978 .join("|");
21151979
2116− let msg = format!("register '{}' set to '{}'", register, ®ex);
2117− match cx.editor.registers.push(register, regex) {
2118− Ok(_) => {
2119− cx.editor.registers.last_search_register = register;
2120− cx.editor.set_status(msg)
2121− }
2122− Err(err) => cx.editor.set_error(err.to_string()),
2123− }
1980+ let msg = format!("register '{}' set to '{}'", '/', ®ex);
1981+ cx.editor.registers.push('/', regex);
1982+ cx.editor.set_status(msg);
21241983}
21251984
21261985fn 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
2129− // twice.
2130− let register = cx
2131− .register
2132− .unwrap_or(cx.editor.registers.last_search_register);
2133− let regex = match cx.editor.registers.first(register, cx.editor) {
1986+ let regex = match cx.editor.registers.last('/') {
21341987 Some(regex) => regex,
21351988 None => return,
21361989 };
@@ -2148,19 +2001,14 @@ fn make_search_word_bounded(cx: &mut Context) {
21482001 if !start_anchored {
21492002 new_regex.push_str("\\b");
21502003 }
2151− new_regex.push_str(®ex);
2004+ new_regex.push_str(regex);
21522005 if !end_anchored {
21532006 new_regex.push_str("\\b");
21542007 }
21552008
2156− let msg = format!("register '{}' set to '{}'", register, &new_regex);
2157− match cx.editor.registers.push(register, new_regex) {
2158− Ok(_) => {
2159− cx.editor.registers.last_search_register = register;
2160− cx.editor.set_status(msg)
2161− }
2162− Err(err) => cx.editor.set_error(err.to_string()),
2163− }
2009+ let msg = format!("register '{}' set to '{}'", '/', &new_regex);
2010+ cx.editor.registers.push('/', new_regex);
2011+ cx.editor.set_status(msg);
21642012}
21652013
21662014fn global_search(cx: &mut Context) {
@@ -2199,11 +2047,13 @@ fn global_search(cx: &mut Context) {
21992047 }
22002048 }
22012049
2050+ let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
22022051 let config = cx.editor.config();
22032052 let smart_case = config.search.smart_case;
22042053 let file_picker_config = config.file_picker.clone();
22052054
22062055 let reg = cx.register.unwrap_or('/');
2056+
22072057 let completions = search_completions(cx, Some(reg));
22082058 ui::regex_prompt(
22092059 cx,
@@ -2216,67 +2066,43 @@ fn global_search(cx: &mut Context) {
22162066 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
22172067 .collect()
22182068 },
2219− move |cx, regex, event| {
2069+ move |_editor, regex, event| {
22202070 if event != PromptEvent::Validate {
22212071 return;
22222072 }
2223− cx.editor.registers.last_search_register = reg;
2224−
2225− let current_path = doc_mut!(cx.editor).path().cloned();
2226− let documents: Vec<_> = cx
2227− .editor
2228− .documents()
2229− .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
2230− .collect();
22312073
22322074 if let Ok(matcher) = RegexMatcherBuilder::new()
22332075 .case_smart(smart_case)
22342076 .build(regex.as_str())
22352077 {
2236− let search_root = helix_loader::current_working_dir();
2237− if !search_root.exists() {
2238− cx.editor
2239− .set_error("Current working directory does not exist");
2240− return;
2241− }
2242−
2243− let (picker, injector) = Picker::stream(current_path);
2078+ let searcher = SearcherBuilder::new()
2079+ .binary_detection(BinaryDetection::quit(b'\x00'))
2080+ .build();
22442081
2082+ let search_root = std::env::current_dir()
2083+ .expect("Global search error: Failed to get current dir");
22452084 let dedup_symlinks = file_picker_config.deduplicate_links;
22462085 let absolute_root = search_root
22472086 .canonicalize()
22482087 .unwrap_or_else(|_| search_root.clone());
2249− let injector_ = injector.clone();
2250−
2251− std::thread::spawn(move || {
2252− let searcher = SearcherBuilder::new()
2253− .binary_detection(BinaryDetection::quit(b'\x00'))
2254− .build();
2255−
2256− let mut walk_builder = WalkBuilder::new(search_root);
2257−
2258− walk_builder
2259− .hidden(file_picker_config.hidden)
2260− .parents(file_picker_config.parents)
2261− .ignore(file_picker_config.ignore)
2262− .follow_links(file_picker_config.follow_symlinks)
2263− .git_ignore(file_picker_config.git_ignore)
2264− .git_global(file_picker_config.git_global)
2265− .git_exclude(file_picker_config.git_exclude)
2266− .max_depth(file_picker_config.max_depth)
2267− .filter_entry(move |entry| {
2268− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2269− });
2270−
2271− walk_builder
2272− .add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
2273− walk_builder.add_custom_ignore_filename(".helix/ignore");
2274−
2275− walk_builder.build_parallel().run(|| {
2088+
2089+ WalkBuilder::new(search_root)
2090+ .hidden(file_picker_config.hidden)
2091+ .parents(file_picker_config.parents)
2092+ .ignore(file_picker_config.ignore)
2093+ .follow_links(file_picker_config.follow_symlinks)
2094+ .git_ignore(file_picker_config.git_ignore)
2095+ .git_global(file_picker_config.git_global)
2096+ .git_exclude(file_picker_config.git_exclude)
2097+ .max_depth(file_picker_config.max_depth)
2098+ .filter_entry(move |entry| {
2099+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2100+ })
2101+ .build_parallel()
2102+ .run(|| {
22762103 let mut searcher = searcher.clone();
22772104 let matcher = matcher.clone();
2278− let injector = injector_.clone();
2279− let documents = &documents;
2105+ let all_matches_sx = all_matches_sx.clone();
22802106 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
22812107 let entry = match entry {
22822108 Ok(entry) => entry,
@@ -2289,39 +2115,17 @@ fn global_search(cx: &mut Context) {
22892115 _ => return WalkState::Continue,
22902116 };
22912117
2292− let mut stop = false;
2293− let sink = sinks::UTF8(|line_num, _| {
2294− stop = injector
2295− .push(FileResult::new(entry.path(), line_num as usize - 1))
2296− .is_err();
2297−
2298− Ok(!stop)
2299− });
2300− let doc = documents.iter().find(|&(doc_path, _)| {
2301− doc_path
2302− .as_ref()
2303− .map_or(false, |doc_path| doc_path == entry.path())
2304− });
2305−
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)
2315− } else {
2316− searcher.search_reader(
2317− &matcher,
2318− RopeReader::new(doc.slice(..)),
2319− sink,
2320− )
2321− }
2322− } else {
2323− searcher.search_path(&matcher, entry.path(), sink)
2324− };
2118+ let result = searcher.search_path(
2119+ &matcher,
2120+ entry.path(),
2121+ sinks::UTF8(|line_num, _| {
2122+ all_matches_sx
2123+ .send(FileResult::new(entry.path(), line_num as usize - 1))
2124+ .unwrap();
2125+
2126+ Ok(true)
2127+ }),
2128+ );
23252129
23262130 if let Err(err) = result {
23272131 log::error!(
@@ -2330,66 +2134,65 @@ fn global_search(cx: &mut Context) {
23302134 err
23312135 );
23322136 }
2333− if stop {
2334− WalkState::Quit
2335− } else {
2336− WalkState::Continue
2337− }
2137+ WalkState::Continue
23382138 })
23392139 });
2340− });
2341−
2342− cx.jobs.callback(async move {
2343− let call = move |_: &mut Editor, compositor: &mut Compositor| {
2344− let picker = Picker::with_stream(
2345− picker,
2346− injector,
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),
2350− Err(e) => {
2351− cx.editor.set_error(format!(
2352− "Failed to open file '{}': {}",
2353− path.display(),
2354− e
2355− ));
2356− return;
2357− }
2358− };
2359−
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.",
2366− );
2367− return;
2368− }
2369− let start = text.line_to_char(line_num);
2370− let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2371−
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);
2375− }
2376− },
2377− )
2378− .with_preview(
2379− |_editor, FileResult { path, line_num }| {
2380− Some((path.clone().into(), Some((*line_num, *line_num))))
2381− },
2382− );
2383− compositor.push(Box::new(overlaid(picker)))
2384− };
2385− Ok(Callback::EditorCompositor(Box::new(call)))
2386− })
23872140 } else {
23882141 // Otherwise do nothing
23892142 // log::warn!("Global Search Invalid Pattern")
23902143 }
23912144 },
23922145 );
2146+
2147+ let current_path = doc_mut!(cx.editor).path().cloned();
2148+
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");
2156+ return;
2157+ }
2158+
2159+ let picker = Picker::new(
2160+ all_matches,
2161+ current_path,
2162+ move |cx, FileResult { path, line_num }, action| {
2163+ match cx.editor.open(path, action) {
2164+ Ok(_) => {}
2165+ Err(e) => {
2166+ cx.editor.set_error(format!(
2167+ "Failed to open file '{}': {}",
2168+ path.display(),
2169+ e
2170+ ));
2171+ return;
2172+ }
2173+ }
2174+
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.");
2180+ return;
2181+ }
2182+ let start = text.line_to_char(line_num);
2183+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2184+
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))))
2189+ });
2190+ compositor.push(Box::new(overlaid(picker)));
2191+ },
2192+ ));
2193+ Ok(call)
2194+ };
2195+ cx.jobs.callback(show_picker);
23932196}
23942197
23952198enum Extend {
@@ -2512,35 +2315,17 @@ enum Operation {
25122315 Change,
25132316}
25142317
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 {
2519− return false;
2520− }
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()
2526− })
2527−}
2528−
25292318fn delete_selection_impl(cx: &mut Context, op: Operation) {
25302319 let (view, doc) = current!(cx.editor);
25312320
25322321 let selection = doc.selection(view.id);
2533− let only_whole_lines = selection_is_linewise(selection, doc.text());
25342322
25352323 if cx.register != Some('_') {
25362324 // first yank the selection
25372325 let text = doc.text().slice(..);
25382326 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
25392327 let reg_name = cx.register.unwrap_or('"');
2540− if let Err(err) = cx.editor.registers.write(reg_name, values) {
2541− cx.editor.set_error(err.to_string());
2542− return;
2543− }
2328+ cx.editor.registers.write(reg_name, values);
25442329 };
25452330
25462331 // then delete
@@ -2554,11 +2339,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
25542339 exit_select_mode(cx);
25552340 }
25562341 Operation::Change => {
2557− if only_whole_lines {
2558− open_above(cx);
2559− } else {
2560− enter_insert_mode(cx);
2561− }
2342+ enter_insert_mode(cx);
25622343 }
25632344 }
25642345}
@@ -2680,10 +2461,6 @@ fn insert_mode(cx: &mut Context) {
26802461 .transform(|range| Range::new(range.to(), range.from()));
26812462
26822463 doc.set_selection(view.id, selection);
2683−
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();
26872464}
26882465
26892466// inserts at the end of each selection
@@ -2720,10 +2497,6 @@ fn append_mode(cx: &mut Context) {
27202497
27212498fn file_picker(cx: &mut Context) {
27222499 let root = find_workspace().0;
2723− if !root.exists() {
2724− cx.editor.set_error("Workspace directory does not exist");
2725− return;
2726− }
27272500 let picker = ui::file_picker(root, &cx.editor.config());
27282501 cx.push_layer(Box::new(overlaid(picker)));
27292502}
@@ -2744,14 +2517,8 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
27442517 let picker = ui::file_picker(path, &cx.editor.config());
27452518 cx.push_layer(Box::new(overlaid(picker)));
27462519}
2747−
27482520fn file_picker_in_current_directory(cx: &mut Context) {
2749− let cwd = helix_loader::current_working_dir();
2750− if !cwd.exists() {
2751− cx.editor
2752− .set_error("Current working directory does not exist");
2753− return;
2754− }
2521+ let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
27552522 let picker = ui::file_picker(cwd, &cx.editor.config());
27562523 cx.push_layer(Box::new(overlaid(picker)));
27572524}
@@ -2901,11 +2668,9 @@ fn jumplist_picker(cx: &mut Context) {
29012668 |cx, meta, action| {
29022669 cx.editor.switch(meta.id, action);
29032670 let config = cx.editor.config();
2904− let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
2671+ let (view, doc) = current!(cx.editor);
29052672 doc.set_selection(view.id, meta.selection.clone());
2906− if action.align_view(view, doc.id()) {
2907− view.ensure_cursor_in_view_center(doc, config.scrolloff);
2908− }
2673+ view.ensure_cursor_in_view_center(doc, config.scrolloff);
29092674 },
29102675 )
29112676 .with_preview(|editor, meta| {
@@ -3053,7 +2818,6 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
30532818 let indent = indent::indent_for_newline(
30542819 language_config,
30552820 syntax,
3056− &doc.config.load().indent_heuristic,
30572821 &doc.indent_style,
30582822 tab_width,
30592823 text,
@@ -3158,7 +2922,6 @@ fn open(cx: &mut Context, open: Open) {
31582922 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
31592923 Open::Above => range.from(),
31602924 });
3161−
31622925 let new_line = match open {
31632926 // adjust position to the end of the line (next line - 1)
31642927 Open::Below => cursor_line + 1,
@@ -3166,15 +2929,13 @@ fn open(cx: &mut Context, open: Open) {
31662929 Open::Above => cursor_line,
31672930 };
31682931
3169− let line_num = new_line.saturating_sub(1);
3170−
31712932 // Index to insert newlines after, as well as the char width
31722933 // to use to compensate for those inserted newlines.
31732934 let (line_end_index, line_end_offset_width) = if new_line == 0 {
31742935 (0, 0)
31752936 } else {
31762937 (
3177− line_end_char_index(&text, line_num),
2938+ line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
31782939 doc.line_ending.len_chars(),
31792940 )
31802941 };
@@ -3182,15 +2943,13 @@ fn open(cx: &mut Context, open: Open) {
31822943 let indent = indent::indent_for_newline(
31832944 doc.language_config(),
31842945 doc.syntax(),
3185− &doc.config.load().indent_heuristic,
31862946 &doc.indent_style,
31872947 doc.tab_width(),
31882948 text,
3189− line_num,
2949+ new_line.saturating_sub(1),
31902950 line_end_index,
31912951 cursor_line,
31922952 );
3193−
31942953 let indent_len = indent.len();
31952954 let mut text = String::with_capacity(1 + indent_len);
31962955 text.push_str(doc.line_ending.as_str());
@@ -3489,7 +3248,8 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) {
34893248
34903249 doc.set_selection(view.id, selection)
34913250 };
3492− cx.editor.apply_motion(motion);
3251+ motion(cx.editor);
3252+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
34933253}
34943254
34953255/// Returns the [Range] for a [Hunk] in the given text.
@@ -3616,7 +3376,6 @@ pub mod insert {
36163376 }
36173377
36183378 use helix_core::auto_pairs;
3619− use helix_view::editor::SmartTabConfig;
36203379
36213380 pub fn insert_char(cx: &mut Context, c: char) {
36223381 let (view, doc) = current_ref!(cx.editor);
@@ -3642,31 +3401,6 @@ pub mod insert {
36423401 }
36433402 }
36443403
3645− pub fn smart_tab(cx: &mut Context) {
3646− let (view, doc) = current_ref!(cx.editor);
3647− let view_id = view.id;
3648−
3649− if matches!(
3650− cx.editor.config().smart_tab,
3651− Some(SmartTabConfig { enable: true, .. })
3652− ) {
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())
3659− });
3660−
3661− if !cursors_after_whitespace {
3662− move_parent_node_end(cx);
3663− return;
3664− }
3665− }
3666−
3667− insert_tab(cx);
3668− }
3669−
36703404 pub fn insert_tab(cx: &mut Context) {
36713405 let (view, doc) = current!(cx.editor);
36723406 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
@@ -3722,7 +3456,6 @@ pub mod insert {
37223456 let indent = indent::indent_for_newline(
37233457 doc.language_config(),
37243458 doc.syntax(),
3725− &doc.config.load().indent_heuristic,
37263459 &doc.indent_style,
37273460 doc.tab_width(),
37283461 text,
@@ -3963,22 +3696,7 @@ fn commit_undo_checkpoint(cx: &mut Context) {
39633696// Yank / Paste
39643697
39653698fn yank(cx: &mut Context) {
3966− yank_impl(cx.editor, cx.register.unwrap_or('"'));
3967− exit_select_mode(cx);
3968−}
3969−
3970−fn yank_to_clipboard(cx: &mut Context) {
3971− yank_impl(cx.editor, '+');
3972− exit_select_mode(cx);
3973−}
3974−
3975−fn yank_to_primary_clipboard(cx: &mut Context) {
3976− yank_impl(cx.editor, '*');
3977− exit_select_mode(cx);
3978−}
3979−
3980−fn yank_impl(editor: &mut Editor, register: char) {
3981− let (view, doc) = current!(editor);
3699+ let (view, doc) = current!(cx.editor);
39823700 let text = doc.text().slice(..);
39833701
39843702 let values: Vec<String> = doc
@@ -3986,15 +3704,19 @@ fn yank_impl(editor: &mut Editor, register: char) {
39863704 .fragments(text)
39873705 .map(Cow::into_owned)
39883706 .collect();
3989− let selections = values.len();
39903707
3991− match editor.registers.write(register, values) {
3992− Ok(_) => editor.set_status(format!(
3993− "yanked {selections} selection{} to register {register}",
3994− if selections == 1 { "" } else { "s" }
3995− )),
3996− Err(err) => editor.set_error(err.to_string()),
3997− }
3708+ let msg = format!(
3709+ "yanked {} selection(s) to register {}",
3710+ values.len(),
3711+ cx.register.unwrap_or('"')
3712+ );
3713+
3714+ cx.editor
3715+ .registers
3716+ .write(cx.register.unwrap_or('"'), values);
3717+
3718+ cx.editor.set_status(msg);
3719+ exit_select_mode(cx);
39983720}
39993721
40003722fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
@@ -4002,7 +3724,6 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
40023724 let text = doc.text().slice(..);
40033725
40043726 let selection = doc.selection(view.id);
4005− let selections = selection.len();
40063727 let joined = selection
40073728 .fragments(text)
40083729 .fold(String::new(), |mut acc, fragment| {
@@ -4013,52 +3734,104 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
40133734 acc
40143735 });
40153736
4016− match editor.registers.write(register, vec![joined]) {
4017− Ok(_) => editor.set_status(format!(
4018− "joined and yanked {selections} selection{} to register {register}",
4019− if selections == 1 { "" } else { "s" }
4020− )),
4021− Err(err) => editor.set_error(err.to_string()),
4022− }
3737+ let msg = format!(
3738+ "joined and yanked {} selection(s) to register {}",
3739+ selection.len(),
3740+ register,
3741+ );
3742+
3743+ editor.registers.write(register, vec![joined]);
3744+ editor.set_status(msg);
40233745}
40243746
40253747fn yank_joined(cx: &mut Context) {
4026− let separator = doc!(cx.editor).line_ending.as_str();
4027− yank_joined_impl(cx.editor, separator, cx.register.unwrap_or('"'));
3748+ let line_ending = doc!(cx.editor).line_ending;
3749+ let register = cx.register.unwrap_or('"');
3750+ yank_joined_impl(cx.editor, line_ending.as_str(), register);
40283751 exit_select_mode(cx);
40293752}
40303753
4031−fn yank_joined_to_clipboard(cx: &mut Context) {
4032− let line_ending = doc!(cx.editor).line_ending;
4033− yank_joined_impl(cx.editor, line_ending.as_str(), '+');
4034− exit_select_mode(cx);
3754+fn yank_joined_to_clipboard_impl(
3755+ editor: &mut Editor,
3756+ separator: &str,
3757+ clipboard_type: ClipboardType,
3758+) -> anyhow::Result<()> {
3759+ let (view, doc) = current!(editor);
3760+ let text = doc.text().slice(..);
3761+
3762+ let values: Vec<String> = doc
3763+ .selection(view.id)
3764+ .fragments(text)
3765+ .map(Cow::into_owned)
3766+ .collect();
3767+
3768+ let clipboard_text = match clipboard_type {
3769+ ClipboardType::Clipboard => "system clipboard",
3770+ ClipboardType::Selection => "primary clipboard",
3771+ };
3772+
3773+ let msg = format!(
3774+ "joined and yanked {} selection(s) to {}",
3775+ values.len(),
3776+ clipboard_text,
3777+ );
3778+
3779+ let joined = values.join(separator);
3780+
3781+ editor
3782+ .clipboard_provider
3783+ .set_contents(joined, clipboard_type)
3784+ .context("Couldn't set system clipboard content")?;
3785+
3786+ editor.set_status(msg);
3787+
3788+ Ok(())
40353789}
40363790
4037−fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3791+fn yank_joined_to_clipboard(cx: &mut Context) {
40383792 let line_ending = doc!(cx.editor).line_ending;
4039− yank_joined_impl(cx.editor, line_ending.as_str(), '*');
3793+ let _ =
3794+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
40403795 exit_select_mode(cx);
40413796}
40423797
4043−fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
3798+fn yank_main_selection_to_clipboard_impl(
3799+ editor: &mut Editor,
3800+ clipboard_type: ClipboardType,
3801+) -> anyhow::Result<()> {
40443802 let (view, doc) = current!(editor);
40453803 let text = doc.text().slice(..);
40463804
4047− let selection = doc.selection(view.id).primary().fragment(text).to_string();
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",
3808+ };
3809+
3810+ let value = doc.selection(view.id).primary().fragment(text);
40483811
4049− match editor.registers.write(register, vec![selection]) {
4050− Ok(_) => editor.set_status(format!("yanked primary selection to register {register}",)),
4051− Err(err) => editor.set_error(err.to_string()),
3812+ if let Err(e) = editor
3813+ .clipboard_provider
3814+ .set_contents(value.into_owned(), clipboard_type)
3815+ {
3816+ bail!("Couldn't set system clipboard content: {}", e);
40523817 }
3818+
3819+ editor.set_status(message_text);
3820+ Ok(())
40533821}
40543822
40553823fn yank_main_selection_to_clipboard(cx: &mut Context) {
4056− yank_primary_selection_impl(cx.editor, '+');
4057− exit_select_mode(cx);
3824+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
3825+}
3826+
3827+fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3828+ let line_ending = doc!(cx.editor).line_ending;
3829+ let _ =
3830+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
40583831}
40593832
40603833fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
4061− yank_primary_selection_impl(cx.editor, '*');
3834+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
40623835 exit_select_mode(cx);
40633836}
40643837
@@ -4156,99 +3929,146 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
41563929 };
41573930 let (view, doc) = current!(cx.editor);
41583931 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
4159− exit_select_mode(cx);
3932+}
3933+
3934+fn paste_clipboard_impl(
3935+ editor: &mut Editor,
3936+ action: Paste,
3937+ clipboard_type: ClipboardType,
3938+ count: usize,
3939+) -> anyhow::Result<()> {
3940+ let (view, doc) = current!(editor);
3941+ match editor.clipboard_provider.get_contents(clipboard_type) {
3942+ Ok(contents) => {
3943+ paste_impl(&[contents], doc, view, action, count, editor.mode);
3944+ Ok(())
3945+ }
3946+ Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3947+ }
41603948}
41613949
41623950fn paste_clipboard_after(cx: &mut Context) {
4163− paste(cx.editor, '+', Paste::After, cx.count());
4164− exit_select_mode(cx);
3951+ let _ = paste_clipboard_impl(
3952+ cx.editor,
3953+ Paste::After,
3954+ ClipboardType::Clipboard,
3955+ cx.count(),
3956+ );
41653957}
41663958
41673959fn paste_clipboard_before(cx: &mut Context) {
4168− paste(cx.editor, '+', Paste::Before, cx.count());
4169− exit_select_mode(cx);
3960+ let _ = paste_clipboard_impl(
3961+ cx.editor,
3962+ Paste::Before,
3963+ ClipboardType::Clipboard,
3964+ cx.count(),
3965+ );
41703966}
41713967
41723968fn paste_primary_clipboard_after(cx: &mut Context) {
4173− paste(cx.editor, '*', Paste::After, cx.count());
4174− exit_select_mode(cx);
3969+ let _ = paste_clipboard_impl(
3970+ cx.editor,
3971+ Paste::After,
3972+ ClipboardType::Selection,
3973+ cx.count(),
3974+ );
41753975}
41763976
41773977fn paste_primary_clipboard_before(cx: &mut Context) {
4178− paste(cx.editor, '*', Paste::Before, cx.count());
4179− exit_select_mode(cx);
3978+ let _ = paste_clipboard_impl(
3979+ cx.editor,
3980+ Paste::Before,
3981+ ClipboardType::Selection,
3982+ cx.count(),
3983+ );
41803984}
41813985
41823986fn replace_with_yanked(cx: &mut Context) {
4183− replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
4184− exit_select_mode(cx);
3987+ let count = cx.count();
3988+ let reg_name = cx.register.unwrap_or('"');
3989+ let (view, doc) = current!(cx.editor);
3990+ let registers = &mut cx.editor.registers;
3991+
3992+ if let Some(values) = registers.read(reg_name) {
3993+ if !values.is_empty() {
3994+ let repeat = std::iter::repeat(
3995+ values
3996+ .last()
3997+ .map(|value| Tendril::from(&value.repeat(count)))
3998+ .unwrap(),
3999+ );
4000+ let mut values = values
4001+ .iter()
4002+ .map(|value| Tendril::from(&value.repeat(count)))
4003+ .chain(repeat);
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()))
4008+ } else {
4009+ (range.from(), range.to(), None)
4010+ }
4011+ });
4012+
4013+ doc.apply(&transaction, view.id);
4014+ exit_select_mode(cx);
4015+ }
4016+ }
41854017}
41864018
4187−fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) {
4188− let Some(values) = editor.registers
4189− .read(register, editor)
4190− .filter(|values| values.len() > 0) else { return };
4191− let values: Vec<_> = values.map(|value| value.to_string()).collect();
4019+fn replace_selections_with_clipboard_impl(
4020+ cx: &mut Context,
4021+ clipboard_type: ClipboardType,
4022+) -> anyhow::Result<()> {
4023+ let count = cx.count();
4024+ let (view, doc) = current!(cx.editor);
41924025
4193− let (view, doc) = current!(editor);
4194− let repeat = std::iter::repeat(
4195− values
4196− .last()
4197− .map(|value| Tendril::from(&value.repeat(count)))
4198− .unwrap(),
4199− );
4200− let mut values = values
4201− .iter()
4202− .map(|value| Tendril::from(&value.repeat(count)))
4203− .chain(repeat);
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()))
4208− } else {
4209− (range.from(), range.to(), None)
4026+ match cx.editor.clipboard_provider.get_contents(clipboard_type) {
4027+ Ok(contents) => {
4028+ let selection = doc.selection(view.id);
4029+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4030+ (
4031+ range.from(),
4032+ range.to(),
4033+ Some(contents.repeat(count).as_str().into()),
4034+ )
4035+ });
4036+
4037+ doc.apply(&transaction, view.id);
4038+ doc.append_changes_to_history(view);
42104039 }
4211− });
4040+ Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
4041+ }
42124042
4213− doc.apply(&transaction, view.id);
4043+ exit_select_mode(cx);
4044+ Ok(())
42144045}
42154046
42164047fn replace_selections_with_clipboard(cx: &mut Context) {
4217− replace_with_yanked_impl(cx.editor, '+', cx.count());
4218− exit_select_mode(cx);
4048+ let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard);
42194049}
42204050
42214051fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4222− replace_with_yanked_impl(cx.editor, '*', cx.count());
4223− exit_select_mode(cx);
4052+ let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection);
42244053}
42254054
4226−fn paste(editor: &mut Editor, register: char, pos: Paste, count: usize) {
4227− let Some(values) = editor.registers.read(register, editor) else { return };
4228− let values: Vec<_> = values.map(|value| value.to_string()).collect();
4055+fn paste(cx: &mut Context, pos: Paste) {
4056+ let count = cx.count();
4057+ let reg_name = cx.register.unwrap_or('"');
4058+ let (view, doc) = current!(cx.editor);
4059+ let registers = &mut cx.editor.registers;
42294060
4230− let (view, doc) = current!(editor);
4231− paste_impl(&values, doc, view, pos, count, editor.mode);
4061+ if let Some(values) = registers.read(reg_name) {
4062+ paste_impl(values, doc, view, pos, count, cx.editor.mode);
4063+ }
42324064}
42334065
42344066fn paste_after(cx: &mut Context) {
4235− paste(
4236− cx.editor,
4237− cx.register.unwrap_or('"'),
4238− Paste::After,
4239− cx.count(),
4240− );
4241− exit_select_mode(cx);
4067+ paste(cx, Paste::After)
42424068}
42434069
42444070fn paste_before(cx: &mut Context) {
4245− paste(
4246− cx.editor,
4247− cx.register.unwrap_or('"'),
4248− Paste::Before,
4249− cx.count(),
4250− );
4251− exit_select_mode(cx);
4071+ paste(cx, Paste::Before)
42524072}
42534073
42544074fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
@@ -4287,7 +4107,6 @@ fn indent(cx: &mut Context) {
42874107 }),
42884108 );
42894109 doc.apply(&transaction, view.id);
4290− exit_select_mode(cx);
42914110}
42924111
42934112fn unindent(cx: &mut Context) {
@@ -4327,7 +4146,6 @@ fn unindent(cx: &mut Context) {
43274146 let transaction = Transaction::change(doc.text(), changes.into_iter());
43284147
43294148 doc.apply(&transaction, view.id);
4330− exit_select_mode(cx);
43314149}
43324150
43334151fn format_selections(cx: &mut Context) {
@@ -4357,7 +4175,7 @@ fn format_selections(cx: &mut Context) {
43574175 })
43584176 else {
43594177 cx.editor
4360− .set_error("No configured language server supports range formatting");
4178+ .set_error("No configured language server does not support range formatting");
43614179 return;
43624180 };
43634181
@@ -4394,9 +4212,10 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
43944212 use movement::skip_while;
43954213 let (view, doc) = current!(cx.editor);
43964214 let text = doc.text();
4397− let slice = text.slice(..);
4215+ let slice = doc.text().slice(..);
43984216
43994217 let mut changes = Vec::new();
4218+ let fragment = Tendril::from(" ");
44004219
44014220 for selection in doc.selection(view.id) {
44024221 let (start, mut end) = selection.line_range(slice);
@@ -4412,13 +4231,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
44124231 let mut end = text.line_to_char(line + 1);
44134232 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
44144233
4415− let separator = if end == line_end_char_index(&slice, line + 1) {
4416− // the joining line contains only space-characters => don't include a whitespace when joining
4417− None
4418− } else {
4419− Some(Tendril::from(" "))
4420− };
4421− changes.push((start, end, separator));
4234+ // need to skip from start, not end
4235+ let change = (start, end, Some(fragment.clone()));
4236+ changes.push(change);
44224237 }
44234238 }
44244239
@@ -4430,6 +4245,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
44304245 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
44314246 changes.dedup();
44324247
4248+ // TODO: joining multiple empty lines should be replaced by a single space.
4249+ // need to merge change ranges that touch
4250+
44334251 // select inserted spaces
44344252 let transaction = if select_space {
44354253 let ranges: SmallVec<_> = changes
@@ -4441,9 +4259,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
44414259 })
44424260 .collect();
44434261 let selection = Selection::new(ranges, 0);
4444− Transaction::change(text, changes.into_iter()).with_selection(selection)
4262+ Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
44454263 } else {
4446− Transaction::change(text, changes.into_iter())
4264+ Transaction::change(doc.text(), changes.into_iter())
44474265 };
44484266
44494267 doc.apply(&transaction, view.id);
@@ -4457,8 +4275,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
44574275 if remove { "remove:" } else { "keep:" }.into(),
44584276 Some(reg),
44594277 ui::completers::none,
4460− move |cx, regex, event| {
4461− let (view, doc) = current!(cx.editor);
4278+ move |editor, regex, event| {
4279+ let (view, doc) = current!(editor);
44624280 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
44634281 return;
44644282 }
@@ -4766,7 +4584,8 @@ fn expand_selection(cx: &mut Context) {
47664584 }
47674585 }
47684586 };
4769− cx.editor.apply_motion(motion);
4587+ motion(cx.editor);
4588+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
47704589}
47714590
47724591fn shrink_selection(cx: &mut Context) {
@@ -4790,7 +4609,8 @@ fn shrink_selection(cx: &mut Context) {
47904609 doc.set_selection(view.id, selection);
47914610 }
47924611 };
4793− cx.editor.apply_motion(motion);
4612+ motion(cx.editor);
4613+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
47944614}
47954615
47964616fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
@@ -4808,7 +4628,8 @@ where
48084628 doc.set_selection(view.id, selection);
48094629 }
48104630 };
4811− cx.editor.apply_motion(motion);
4631+ motion(cx.editor);
4632+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
48124633}
48134634
48144635fn select_next_sibling(cx: &mut Context) {
@@ -4819,49 +4640,6 @@ fn select_prev_sibling(cx: &mut Context) {
48194640 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
48204641}
48214642
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);
4825−
4826− if let Some(syntax) = doc.syntax() {
4827− let text = doc.text().slice(..);
4828− let current_selection = doc.selection(view.id);
4829−
4830− let selection = movement::move_parent_node_end(
4831− syntax,
4832− text,
4833− current_selection.clone(),
4834− dir,
4835− movement,
4836− );
4837−
4838− doc.set_selection(view.id, selection);
4839−
4840− // [TODO] temporary workaround until we're not using the idle timer to
4841− // trigger auto completions any more
4842− editor.clear_idle_timer();
4843− }
4844− };
4845−
4846− cx.editor.apply_motion(motion);
4847−}
4848−
4849−pub fn move_parent_node_end(cx: &mut Context) {
4850− move_node_bound_impl(cx, Direction::Forward, Movement::Move)
4851−}
4852−
4853−pub fn move_parent_node_start(cx: &mut Context) {
4854− move_node_bound_impl(cx, Direction::Backward, Movement::Move)
4855−}
4856−
4857−pub fn extend_parent_node_end(cx: &mut Context) {
4858− move_node_bound_impl(cx, Direction::Forward, Movement::Extend)
4859−}
4860−
4861−pub fn extend_parent_node_start(cx: &mut Context) {
4862− move_node_bound_impl(cx, Direction::Backward, Movement::Extend)
4863−}
4864−
48654643fn match_brackets(cx: &mut Context) {
48664644 let (view, doc) = current!(cx.editor);
48674645 let is_select = cx.editor.mode == Mode::Select;
@@ -4975,19 +4753,17 @@ fn transpose_view(cx: &mut Context) {
49754753 cx.editor.transpose_view()
49764754}
49774755
4978−/// Open a new split in the given direction specified by the action.
4979−///
4980−/// Maintain the current view (both the cursor's position and view in document).
4981−fn split(editor: &mut Editor, action: Action) {
4982− let (view, doc) = current!(editor);
4756+// split helper, clear it later
4757+fn split(cx: &mut Context, action: Action) {
4758+ let (view, doc) = current!(cx.editor);
49834759 let id = doc.id();
49844760 let selection = doc.selection(view.id).clone();
49854761 let offset = view.offset;
49864762
4987− editor.switch(id, action);
4763+ cx.editor.switch(id, action);
49884764
49894765 // match the selection in the previous view
4990− let (view, doc) = current!(editor);
4766+ let (view, doc) = current!(cx.editor);
49914767 doc.set_selection(view.id, selection);
49924768 // match the view scroll offset (switch doesn't handle this fully
49934769 // since the selection is only matched after the split)
@@ -4995,7 +4771,7 @@ fn split(editor: &mut Editor, action: Action) {
49954771}
49964772
49974773fn hsplit(cx: &mut Context) {
4998− split(cx.editor, Action::HorizontalSplit);
4774+ split(cx, Action::HorizontalSplit);
49994775}
50004776
50014777fn hsplit_new(cx: &mut Context) {
@@ -5003,7 +4779,7 @@ fn hsplit_new(cx: &mut Context) {
50034779}
50044780
50054781fn vsplit(cx: &mut Context) {
5006− split(cx.editor, Action::VerticalSplit);
4782+ split(cx, Action::VerticalSplit);
50074783}
50084784
50094785fn vsplit_new(cx: &mut Context) {
@@ -5052,12 +4828,7 @@ fn insert_register(cx: &mut Context) {
50524828 if let Some(ch) = event.char() {
50534829 cx.editor.autoinfo = None;
50544830 cx.register = Some(ch);
5055− paste(
5056− cx.editor,
5057− cx.register.unwrap_or('"'),
5058− Paste::Cursor,
5059− cx.count(),
5060− );
4831+ paste(cx, Paste::Cursor);
50614832 }
50624833 })
50634834}
@@ -5141,7 +4912,8 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
51414912 editor.set_status("Syntax-tree is not available in current buffer");
51424913 }
51434914 };
5144− cx.editor.apply_motion(motion);
4915+ motion(cx.editor);
4916+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
51454917}
51464918
51474919fn goto_next_function(cx: &mut Context) {
@@ -5262,7 +5034,8 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
52625034 });
52635035 doc.set_selection(view.id, selection);
52645036 };
5265− cx.editor.apply_motion(textobject);
5037+ textobject(cx.editor);
5038+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
52665039 }
52675040 });
52685041
@@ -5352,18 +5125,11 @@ fn surround_replace(cx: &mut Context) {
53525125 }
53535126 };
53545127
5355− let selection = selection.clone();
5356− let ranges: SmallVec<[Range; 1]> = change_pos.iter().map(|&p| Range::point(p)).collect();
5357− doc.set_selection(
5358− view.id,
5359− Selection::new(ranges, selection.primary_index() * 2),
5360− );
5361−
53625128 cx.on_next_key(move |cx, event| {
53635129 let (view, doc) = current!(cx.editor);
53645130 let to = match event.char() {
53655131 Some(to) => to,
5366− None => return doc.set_selection(view.id, selection),
5132+ None => return,
53675133 };
53685134 let (open, close) = surround::get_pair(to);
53695135 let transaction = Transaction::change(
@@ -5374,7 +5140,6 @@ fn surround_replace(cx: &mut Context) {
53745140 (pos, pos + 1, Some(t))
53755141 }),
53765142 );
5377− doc.set_selection(view.id, selection);
53785143 doc.apply(&transaction, view.id);
53795144 exit_select_mode(cx);
53805145 });
@@ -5609,18 +5374,12 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
56095374 };
56105375
56115376 // These `usize`s cannot underflow because selection ranges cannot overlap.
5612− let anchor = to
5613− .checked_add_signed(offset)
5614− .expect("Selection ranges cannot overlap")
5615− .checked_sub(deleted_len)
5616− .expect("Selection ranges cannot overlap");
5377+ // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
5378+ // arithmetic to assert this.
5379+ let anchor = (to as isize + offset - deleted_len as isize) as usize;
56175380 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
56185381 ranges.push(new_range);
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");
5382+ offset = offset + output_len as isize - deleted_len as isize;
56245383
56255384 changes.push((from, to, Some(output)));
56265385 }
@@ -5759,7 +5518,6 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
57595518 let transaction = Transaction::change(doc.text(), changes.into_iter());
57605519 let transaction = transaction.with_selection(new_selection);
57615520 doc.apply(&transaction, view.id);
5762− exit_select_mode(cx);
57635521 }
57645522}
57655523
@@ -5778,12 +5536,9 @@ fn record_macro(cx: &mut Context) {
57785536 }
57795537 })
57805538 .collect::<String>();
5781− match cx.editor.registers.write(reg, vec![s]) {
5782− Ok(_) => cx
5783− .editor
5784− .set_status(format!("Recorded to register [{}]", reg)),
5785− Err(err) => cx.editor.set_error(err.to_string()),
5786− }
5539+ cx.editor.registers.write(reg, vec![s]);
5540+ cx.editor
5541+ .set_status(format!("Recorded to register [{}]", reg));
57875542 } else {
57885543 let reg = cx.register.take().unwrap_or('@');
57895544 cx.editor.macro_recording = Some((reg, Vec::new()));
@@ -5803,14 +5558,8 @@ fn replay_macro(cx: &mut Context) {
58035558 return;
58045559 }
58055560
5806− let keys: Vec<KeyEvent> = if let Some(keys) = cx
5807− .editor
5808− .registers
5809− .read(reg, cx.editor)
5810− .filter(|values| values.len() == 1)
5811− .map(|mut values| values.next().unwrap())
5812− {
5813− match helix_view::input::parse_macro(&keys) {
5561+ let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
5562+ match helix_view::input::parse_macro(keys_str) {
58145563 Ok(keys) => keys,
58155564 Err(err) => {
58165565 cx.editor.set_error(format!("Invalid macro: {}", err));