@@ -29,12 +29,11 @@ use helix_core::{
2929 tree_sitter::Node,
3030 unicode::width::UnicodeWidthChar,
3131 visual_offset_from_block, Deletion, LineEnding, Position, Range, Rope, RopeGraphemes,
32− RopeSlice, Selection, SmallVec, Tendril, Transaction,
32+ RopeReader, RopeSlice, Selection, SmallVec, Tendril, Transaction,
3333};
3434use helix_view::{
35− clipboard::ClipboardType,
3635 document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
37− editor::{Action, CompleteAction, Motion},
36+ editor::{Action, CompleteAction},
3837 info::Info,
3938 input::KeyEvent,
4039 keyboard::KeyCode,
@@ -44,7 +43,6 @@ use helix_view::{
4443};
4544
4645use anyhow::{anyhow, bail, ensure, Context as _};
47−use fuzzy_matcher::FuzzyMatcher;
4846use insert::*;
4947use movement::Movement;
5048
@@ -55,15 +53,20 @@ use crate::{
5553 job::Callback,
5654 keymap::ReverseKeymap,
5755 ui::{
58− self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
59− FilePicker, Picker, Popup, Prompt, PromptEvent,
56+ self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
57+ Popup, Prompt, PromptEvent,
6058 },
6159};
6260
6361use crate::job::{self, Jobs};
64−use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
65−use std::{collections::HashMap, fmt, future::Future};
66−use std::{collections::HashSet, num::NonZeroUsize};
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+};
6770
6871use std::{
6972 borrow::Cow,
@@ -72,11 +75,11 @@ use std::{
7275
7376use once_cell::sync::Lazy;
7477use serde::de::{self, Deserialize, Deserializer};
78+use url::Url;
7579
7680use grep_regex::RegexMatcherBuilder;
7781use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
7882use ignore::{DirEntry, WalkBuilder, WalkState};
79−use tokio_stream::wrappers::UnboundedReceiverStream;
8083
8184pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
8285
@@ -247,6 +250,9 @@ impl MappableCommand {
247250 move_next_long_word_start, "Move to start of next long word",
248251 move_prev_long_word_start, "Move to start of previous long word",
249252 move_next_long_word_end, "Move to end of next 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",
250256 extend_next_word_start, "Extend to start of next word",
251257 extend_prev_word_start, "Extend to start of previous word",
252258 extend_next_word_end, "Extend to end of next word",
@@ -254,6 +260,9 @@ impl MappableCommand {
254260 extend_next_long_word_start, "Extend to start of next long word",
255261 extend_prev_long_word_start, "Extend to start of previous long word",
256262 extend_next_long_word_end, "Extend to end of next 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",
257266 find_till_char, "Move till next occurrence of char",
258267 find_next_char, "Move to next occurrence of char",
259268 extend_till_char, "Extend till next occurrence of char",
@@ -328,7 +337,7 @@ impl MappableCommand {
328337 goto_implementation, "Goto implementation",
329338 goto_file_start, "Goto line number <n> else file start",
330339 goto_file_end, "Goto file end",
331− goto_file, "Goto files in selection",
340+ goto_file, "Goto files/URLs in selection",
332341 goto_file_hsplit, "Goto files in selection (hsplit)",
333342 goto_file_vsplit, "Goto files in selection (vsplit)",
334343 goto_reference, "Goto references",
@@ -360,6 +369,7 @@ impl MappableCommand {
360369 extend_to_line_end, "Extend to line end",
361370 extend_to_line_end_newline, "Extend to line end",
362371 signature_help, "Show signature help",
372+ smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.",
363373 insert_tab, "Insert tab char",
364374 insert_newline, "Insert newline char",
365375 delete_char_backward, "Delete previous char",
@@ -374,6 +384,9 @@ impl MappableCommand {
374384 later, "Move forward in history",
375385 commit_undo_checkpoint, "Commit changes to new checkpoint",
376386 yank, "Yank selection",
387+ yank_to_clipboard, "Yank selections to clipboard",
388+ yank_to_primary_clipboard, "Yank selections to primary clipboard",
389+ yank_joined, "Join and yank selections",
377390 yank_joined_to_clipboard, "Join and yank selections to clipboard",
378391 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
379392 yank_joined_to_primary_clipboard, "Join and yank selections to primary clipboard",
@@ -404,6 +417,7 @@ impl MappableCommand {
404417 rotate_selections_backward, "Rotate selections backward",
405418 rotate_selection_contents_forward, "Rotate selection contents forward",
406419 rotate_selection_contents_backward, "Rotate selections contents backward",
420+ reverse_selection_contents, "Reverse selections contents",
407421 expand_selection, "Expand selection to parent syntax node",
408422 shrink_selection, "Shrink selection to previously expanded syntax node",
409423 select_next_sibling, "Select next sibling in syntax tree",
@@ -976,6 +990,7 @@ fn align_selections(cx: &mut Context) {
976990
977991 let transaction = Transaction::change(doc.text(), changes.into_iter());
978992 doc.apply(&transaction, view.id);
993+ exit_select_mode(cx);
979994}
980995
981996fn goto_window(cx: &mut Context, align: Align) {
@@ -1067,6 +1082,10 @@ fn move_prev_long_word_start(cx: &mut Context) {
10671082 move_word_impl(cx, movement::move_prev_long_word_start)
10681083}
10691084
1085+fn move_prev_long_word_end(cx: &mut Context) {
1086+ move_word_impl(cx, movement::move_prev_long_word_end)
1087+}
1088+
10701089fn move_next_long_word_end(cx: &mut Context) {
10711090 move_word_impl(cx, movement::move_next_long_word_end)
10721091}
@@ -1091,8 +1110,7 @@ where
10911110 .transform(|range| move_fn(text, range, count, behavior));
10921111 doc.set_selection(view.id, selection);
10931112 };
1094− motion(cx.editor);
1095− cx.editor.last_motion = Some(Motion(Box::new(motion)));
1113+ cx.editor.apply_motion(motion)
10961114}
10971115
10981116fn goto_prev_paragraph(cx: &mut Context) {
@@ -1147,6 +1165,10 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11471165 let (view, doc) = current_ref!(cx.editor);
11481166 let text = doc.text();
11491167 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();
11501172 let mut paths: Vec<_> = selections
11511173 .iter()
11521174 .map(|r| text.slice(r.from()..r.to()).to_string())
@@ -1174,16 +1196,80 @@ fn goto_file_impl(cx: &mut Context, action: Action) {
11741196 .to_string(),
11751197 );
11761198 }
1199+
11771200 for sel in paths {
11781201 let p = sel.trim();
1179− if !p.is_empty() {
1180− if let Err(e) = cx.editor.open(&PathBuf::from(p), action) {
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) {
11811250 cx.editor.set_error(format!("Open file failed: {:?}", e));
11821251 }
11831252 }
11841253 }
11851254}
11861255
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+
11871273fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
11881274where
11891275 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1224,14 +1310,74 @@ fn extend_prev_long_word_start(cx: &mut Context) {
12241310 extend_word_impl(cx, movement::move_prev_long_word_start)
12251311}
12261312
1313+fn extend_prev_long_word_end(cx: &mut Context) {
1314+ extend_word_impl(cx, movement::move_prev_long_word_end)
1315+}
1316+
12271317fn extend_next_long_word_end(cx: &mut Context) {
12281318 extend_word_impl(cx, movement::move_next_long_word_end)
12291319}
12301320
1231−fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
1232−where
1233− F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
1234−{
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) {
12351381 // TODO: count is reset to 1 before next key so we move it into the closure here.
12361382 // Would be nice to carry over.
12371383 let count = cx.count();
@@ -1244,13 +1390,9 @@ where
12441390 KeyEvent {
12451391 code: KeyCode::Enter,
12461392 ..
1247− } =>
1248− // TODO: this isn't quite correct when CRLF is involved.
1249− // This hack will work in most cases, since documents don't
1250− // usually mix line endings. But we should fix it eventually
1251− // anyway.
1252− {
1253− doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
1393+ } => {
1394+ find_char_line_ending(cx, count, direction, inclusive, extend);
1395+ return;
12541396 }
12551397
12561398 KeyEvent {
@@ -1263,11 +1405,18 @@ where
12631405 } => ch,
12641406 _ => return,
12651407 };
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+ };
12661418
1267− find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
1268− cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
1269− find_char_impl(editor, &search_fn, inclusive, extend, ch, 1);
1270− })));
1419+ cx.editor.apply_motion(motion);
12711420 })
12721421}
12731422
@@ -1346,46 +1495,39 @@ fn find_prev_char_impl(
13461495}
13471496
13481497fn find_till_char(cx: &mut Context) {
1349− will_find_char(cx, find_next_char_impl, false, false)
1498+ find_char(cx, Direction::Forward, false, false);
13501499}
13511500
13521501fn find_next_char(cx: &mut Context) {
1353− will_find_char(cx, find_next_char_impl, true, false)
1502+ find_char(cx, Direction::Forward, true, false)
13541503}
13551504
13561505fn extend_till_char(cx: &mut Context) {
1357− will_find_char(cx, find_next_char_impl, false, true)
1506+ find_char(cx, Direction::Forward, false, true)
13581507}
13591508
13601509fn extend_next_char(cx: &mut Context) {
1361− will_find_char(cx, find_next_char_impl, true, true)
1510+ find_char(cx, Direction::Forward, true, true)
13621511}
13631512
13641513fn till_prev_char(cx: &mut Context) {
1365− will_find_char(cx, find_prev_char_impl, false, false)
1514+ find_char(cx, Direction::Backward, false, false)
13661515}
13671516
13681517fn find_prev_char(cx: &mut Context) {
1369− will_find_char(cx, find_prev_char_impl, true, false)
1518+ find_char(cx, Direction::Backward, true, false)
13701519}
13711520
13721521fn extend_till_prev_char(cx: &mut Context) {
1373− will_find_char(cx, find_prev_char_impl, false, true)
1522+ find_char(cx, Direction::Backward, false, true)
13741523}
13751524
13761525fn extend_prev_char(cx: &mut Context) {
1377− will_find_char(cx, find_prev_char_impl, true, true)
1526+ find_char(cx, Direction::Backward, true, true)
13781527}
13791528
13801529fn repeat_last_motion(cx: &mut Context) {
1381− let count = cx.count();
1382− let last_motion = cx.editor.last_motion.take();
1383− if let Some(m) = &last_motion {
1384− for _ in 0..count {
1385− m.run(cx.editor);
1386− }
1387− cx.editor.last_motion = last_motion;
1388− }
1530+ cx.editor.repeat_last_motion(cx.count())
13891531}
13901532
13911533fn replace(cx: &mut Context) {
@@ -1452,6 +1594,7 @@ where
14521594 });
14531595
14541596 doc.apply(&transaction, view.id);
1597+ exit_select_mode(cx);
14551598}
14561599
14571600fn switch_case(cx: &mut Context) {
@@ -1693,8 +1836,8 @@ fn select_regex(cx: &mut Context) {
16931836 "select:".into(),
16941837 Some(reg),
16951838 ui::completers::none,
1696− move |editor, regex, event| {
1697− let (view, doc) = current!(editor);
1839+ move |cx, regex, event| {
1840+ let (view, doc) = current!(cx.editor);
16981841 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
16991842 return;
17001843 }
@@ -1715,8 +1858,8 @@ fn split_selection(cx: &mut Context) {
17151858 "split:".into(),
17161859 Some(reg),
17171860 ui::completers::none,
1718− move |editor, regex, event| {
1719− let (view, doc) = current!(editor);
1861+ move |cx, regex, event| {
1862+ let (view, doc) = current!(cx.editor);
17201863 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
17211864 return;
17221865 }
@@ -1839,11 +1982,11 @@ fn search_impl(
18391982
18401983fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
18411984 let mut items = reg
1842− .and_then(|reg| cx.editor.registers.get(reg))
1843− .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
1985+ .and_then(|reg| cx.editor.registers.read(reg, cx.editor))
1986+ .map_or(Vec::new(), |reg| reg.take(200).collect());
18441987 items.sort_unstable();
18451988 items.dedup();
1846− items.into_iter().cloned().collect()
1989+ items.into_iter().map(|value| value.to_string()).collect()
18471990}
18481991
18491992fn search(cx: &mut Context) {
@@ -1880,12 +2023,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
18802023 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
18812024 .collect()
18822025 },
1883− move |editor, regex, event| {
1884− if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
2026+ move |cx, regex, event| {
2027+ if event == PromptEvent::Validate {
2028+ cx.editor.registers.last_search_register = reg;
2029+ } else if event != PromptEvent::Update {
18852030 return;
18862031 }
18872032 search_impl(
1888− editor,
2033+ cx.editor,
18892034 &contents,
18902035 ®ex,
18912036 Movement::Move,
@@ -1900,11 +2045,13 @@ fn searcher(cx: &mut Context, direction: Direction) {
19002045
19012046fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
19022047 let count = cx.count();
2048+ let register = cx
2049+ .register
2050+ .unwrap_or(cx.editor.registers.last_search_register);
19032051 let config = cx.editor.config();
19042052 let scrolloff = config.scrolloff;
1905− let (_, doc) = current!(cx.editor);
1906− let registers = &cx.editor.registers;
1907− 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);
19082055 let contents = doc.text().slice(..).to_string();
19092056 let search_config = &config.search;
19102057 let case_insensitive = if search_config.smart_case {
@@ -1913,7 +2060,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
19132060 false
19142061 };
19152062 let wrap_around = search_config.wrap_around;
1916− if let Ok(regex) = RegexBuilder::new(query)
2063+ if let Ok(regex) = RegexBuilder::new(&query)
19172064 .case_insensitive(case_insensitive)
19182065 .multi_line(true)
19192066 .build()
@@ -1953,6 +2100,7 @@ fn extend_search_prev(cx: &mut Context) {
19532100}
19542101
19552102fn search_selection(cx: &mut Context) {
2103+ let register = cx.register.unwrap_or('/');
19562104 let (view, doc) = current!(cx.editor);
19572105 let contents = doc.text().slice(..);
19582106
@@ -1965,13 +2113,24 @@ fn search_selection(cx: &mut Context) {
19652113 .collect::<Vec<_>>()
19662114 .join("|");
19672115
1968− let msg = format!("register '{}' set to '{}'", '/', ®ex);
1969− cx.editor.registers.push('/', regex);
1970− cx.editor.set_status(msg);
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+ }
19712124}
19722125
19732126fn make_search_word_bounded(cx: &mut Context) {
1974− let regex = match cx.editor.registers.last('/') {
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) {
19752134 Some(regex) => regex,
19762135 None => return,
19772136 };
@@ -1989,14 +2148,19 @@ fn make_search_word_bounded(cx: &mut Context) {
19892148 if !start_anchored {
19902149 new_regex.push_str("\\b");
19912150 }
1992− new_regex.push_str(regex);
2151+ new_regex.push_str(®ex);
19932152 if !end_anchored {
19942153 new_regex.push_str("\\b");
19952154 }
19962155
1997− let msg = format!("register '{}' set to '{}'", '/', &new_regex);
1998− cx.editor.registers.push('/', new_regex);
1999− cx.editor.set_status(msg);
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+ }
20002164}
20012165
20022166fn global_search(cx: &mut Context) {
@@ -2035,13 +2199,11 @@ fn global_search(cx: &mut Context) {
20352199 }
20362200 }
20372201
2038− let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
20392202 let config = cx.editor.config();
20402203 let smart_case = config.search.smart_case;
20412204 let file_picker_config = config.file_picker.clone();
20422205
20432206 let reg = cx.register.unwrap_or('/');
2044−
20452207 let completions = search_completions(cx, Some(reg));
20462208 ui::regex_prompt(
20472209 cx,
@@ -2054,43 +2216,67 @@ fn global_search(cx: &mut Context) {
20542216 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
20552217 .collect()
20562218 },
2057− move |_editor, regex, event| {
2219+ move |cx, regex, event| {
20582220 if event != PromptEvent::Validate {
20592221 return;
20602222 }
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();
20612231
20622232 if let Ok(matcher) = RegexMatcherBuilder::new()
20632233 .case_smart(smart_case)
20642234 .build(regex.as_str())
20652235 {
2066− let searcher = SearcherBuilder::new()
2067− .binary_detection(BinaryDetection::quit(b'\x00'))
2068− .build();
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);
20692244
2070− let search_root = std::env::current_dir()
2071− .expect("Global search error: Failed to get current dir");
20722245 let dedup_symlinks = file_picker_config.deduplicate_links;
20732246 let absolute_root = search_root
20742247 .canonicalize()
20752248 .unwrap_or_else(|_| search_root.clone());
2076−
2077− WalkBuilder::new(search_root)
2078− .hidden(file_picker_config.hidden)
2079− .parents(file_picker_config.parents)
2080− .ignore(file_picker_config.ignore)
2081− .follow_links(file_picker_config.follow_symlinks)
2082− .git_ignore(file_picker_config.git_ignore)
2083− .git_global(file_picker_config.git_global)
2084− .git_exclude(file_picker_config.git_exclude)
2085− .max_depth(file_picker_config.max_depth)
2086− .filter_entry(move |entry| {
2087− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2088− })
2089− .build_parallel()
2090− .run(|| {
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(|| {
20912276 let mut searcher = searcher.clone();
20922277 let matcher = matcher.clone();
2093− let all_matches_sx = all_matches_sx.clone();
2278+ let injector = injector_.clone();
2279+ let documents = &documents;
20942280 Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
20952281 let entry = match entry {
20962282 Ok(entry) => entry,
@@ -2103,17 +2289,39 @@ fn global_search(cx: &mut Context) {
21032289 _ => return WalkState::Continue,
21042290 };
21052291
2106− let result = searcher.search_path(
2107− &matcher,
2108− entry.path(),
2109− sinks::UTF8(|line_num, _| {
2110− all_matches_sx
2111− .send(FileResult::new(entry.path(), line_num as usize - 1))
2112− .unwrap();
2113−
2114− Ok(true)
2115− }),
2116− );
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+ };
21172325
21182326 if let Err(err) = result {
21192327 log::error!(
@@ -2122,67 +2330,66 @@ fn global_search(cx: &mut Context) {
21222330 err
21232331 );
21242332 }
2125− WalkState::Continue
2333+ if stop {
2334+ WalkState::Quit
2335+ } else {
2336+ WalkState::Continue
2337+ }
21262338 })
21272339 });
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+ })
21282387 } else {
21292388 // Otherwise do nothing
21302389 // log::warn!("Global Search Invalid Pattern")
21312390 }
21322391 },
21332392 );
2134−
2135− let current_path = doc_mut!(cx.editor).path().cloned();
2136−
2137− let show_picker = async move {
2138− let all_matches: Vec<FileResult> =
2139− UnboundedReceiverStream::new(all_matches_rx).collect().await;
2140− let call: job::Callback = Callback::EditorCompositor(Box::new(
2141− move |editor: &mut Editor, compositor: &mut Compositor| {
2142− if all_matches.is_empty() {
2143− editor.set_status("No matches found");
2144− return;
2145− }
2146−
2147− let picker = FilePicker::new(
2148− all_matches,
2149− current_path,
2150− move |cx, FileResult { path, line_num }, action| {
2151− match cx.editor.open(path, action) {
2152− Ok(_) => {}
2153− Err(e) => {
2154− cx.editor.set_error(format!(
2155− "Failed to open file '{}': {}",
2156− path.display(),
2157− e
2158− ));
2159− return;
2160− }
2161− }
2162−
2163− let line_num = *line_num;
2164− let (view, doc) = current!(cx.editor);
2165− let text = doc.text();
2166− if line_num >= text.len_lines() {
2167− cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed.");
2168− return;
2169− }
2170− let start = text.line_to_char(line_num);
2171− let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2172−
2173− doc.set_selection(view.id, Selection::single(start, end));
2174− align_view(doc, view, Align::Center);
2175− },
2176− |_editor, FileResult { path, line_num }| {
2177− Some((path.clone().into(), Some((*line_num, *line_num))))
2178− },
2179− );
2180− compositor.push(Box::new(overlaid(picker)));
2181− },
2182− ));
2183− Ok(call)
2184− };
2185− cx.jobs.callback(show_picker);
21862393}
21872394
21882395enum Extend {
@@ -2305,17 +2512,35 @@ enum Operation {
23052512 Change,
23062513}
23072514
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+
23082529fn delete_selection_impl(cx: &mut Context, op: Operation) {
23092530 let (view, doc) = current!(cx.editor);
23102531
23112532 let selection = doc.selection(view.id);
2533+ let only_whole_lines = selection_is_linewise(selection, doc.text());
23122534
23132535 if cx.register != Some('_') {
23142536 // first yank the selection
23152537 let text = doc.text().slice(..);
23162538 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
23172539 let reg_name = cx.register.unwrap_or('"');
2318− 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());
2542+ return;
2543+ }
23192544 };
23202545
23212546 // then delete
@@ -2329,7 +2554,11 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
23292554 exit_select_mode(cx);
23302555 }
23312556 Operation::Change => {
2332− enter_insert_mode(cx);
2557+ if only_whole_lines {
2558+ open_above(cx);
2559+ } else {
2560+ enter_insert_mode(cx);
2561+ }
23332562 }
23342563 }
23352564}
@@ -2451,6 +2680,10 @@ fn insert_mode(cx: &mut Context) {
24512680 .transform(|range| Range::new(range.to(), range.from()));
24522681
24532682 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();
24542687}
24552688
24562689// inserts at the end of each selection
@@ -2487,6 +2720,10 @@ fn append_mode(cx: &mut Context) {
24872720
24882721fn file_picker(cx: &mut Context) {
24892722 let root = find_workspace().0;
2723+ if !root.exists() {
2724+ cx.editor.set_error("Workspace directory does not exist");
2725+ return;
2726+ }
24902727 let picker = ui::file_picker(root, &cx.editor.config());
24912728 cx.push_layer(Box::new(overlaid(picker)));
24922729}
@@ -2507,8 +2744,14 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) {
25072744 let picker = ui::file_picker(path, &cx.editor.config());
25082745 cx.push_layer(Box::new(overlaid(picker)));
25092746}
2747+
25102748fn file_picker_in_current_directory(cx: &mut Context) {
2511− let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./"));
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+ }
25122755 let picker = ui::file_picker(cwd, &cx.editor.config());
25132756 cx.push_layer(Box::new(overlaid(picker)));
25142757}
@@ -2567,22 +2810,18 @@ fn buffer_picker(cx: &mut Context) {
25672810 // mru
25682811 items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
25692812
2570− let picker = FilePicker::new(
2571− items,
2572− (),
2573− |cx, meta, action| {
2574− cx.editor.switch(meta.id, action);
2575− },
2576− |editor, meta| {
2577− let doc = &editor.documents.get(&meta.id)?;
2578− let &view_id = doc.selections().keys().next()?;
2579− let line = doc
2580− .selection(view_id)
2581− .primary()
2582− .cursor_line(doc.text().slice(..));
2583− Some((meta.id.into(), Some((line, line))))
2584− },
2585− );
2813+ let picker = Picker::new(items, (), |cx, meta, action| {
2814+ cx.editor.switch(meta.id, action);
2815+ })
2816+ .with_preview(|editor, meta| {
2817+ let doc = &editor.documents.get(&meta.id)?;
2818+ let &view_id = doc.selections().keys().next()?;
2819+ let line = doc
2820+ .selection(view_id)
2821+ .primary()
2822+ .cursor_line(doc.text().slice(..));
2823+ Some((meta.id.into(), Some((line, line))))
2824+ });
25862825 cx.push_layer(Box::new(overlaid(picker)));
25872826}
25882827
@@ -2648,7 +2887,7 @@ fn jumplist_picker(cx: &mut Context) {
26482887 }
26492888 };
26502889
2651− let picker = FilePicker::new(
2890+ let picker = Picker::new(
26522891 cx.editor
26532892 .tree
26542893 .views()
@@ -2662,16 +2901,18 @@ fn jumplist_picker(cx: &mut Context) {
26622901 |cx, meta, action| {
26632902 cx.editor.switch(meta.id, action);
26642903 let config = cx.editor.config();
2665− let (view, doc) = current!(cx.editor);
2904+ let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
26662905 doc.set_selection(view.id, meta.selection.clone());
2667− view.ensure_cursor_in_view_center(doc, config.scrolloff);
2668− },
2669− |editor, meta| {
2670− let doc = &editor.documents.get(&meta.id)?;
2671− let line = meta.selection.primary().cursor_line(doc.text().slice(..));
2672− Some((meta.path.clone()?.into(), Some((line, line))))
2906+ if action.align_view(view, doc.id()) {
2907+ view.ensure_cursor_in_view_center(doc, config.scrolloff);
2908+ }
26732909 },
2674− );
2910+ )
2911+ .with_preview(|editor, meta| {
2912+ let doc = &editor.documents.get(&meta.id)?;
2913+ let line = meta.selection.primary().cursor_line(doc.text().slice(..));
2914+ Some((meta.id.into(), Some((line, line))))
2915+ });
26752916 cx.push_layer(Box::new(overlaid(picker)));
26762917}
26772918
@@ -2705,6 +2946,9 @@ impl ui::menu::Item for MappableCommand {
27052946}
27062947
27072948pub fn command_palette(cx: &mut Context) {
2949+ let register = cx.register;
2950+ let count = cx.count;
2951+
27082952 cx.callback = Some(Box::new(
27092953 move |compositor: &mut Compositor, cx: &mut compositor::Context| {
27102954 let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
@@ -2722,8 +2966,8 @@ pub fn command_palette(cx: &mut Context) {
27222966
27232967 let picker = Picker::new(commands, keymap, move |cx, command, _action| {
27242968 let mut ctx = Context {
2725− register: None,
2726− count: std::num::NonZeroUsize::new(1),
2969+ register,
2970+ count,
27272971 editor: cx.editor,
27282972 callback: None,
27292973 on_next_key_callback: None,
@@ -2762,24 +3006,88 @@ fn last_picker(cx: &mut Context) {
27623006 }));
27633007}
27643008
2765−// I inserts at the first nonwhitespace character of each line with a selection
3009+/// Fallback position to use for [`insert_with_indent`].
3010+enum IndentFallbackPos {
3011+ LineStart,
3012+ LineEnd,
3013+}
3014+
3015+// `I` inserts at the first nonwhitespace character of each line with a selection.
3016+// If the line is empty, automatically indent.
27663017fn insert_at_line_start(cx: &mut Context) {
2767− goto_first_nonwhitespace(cx);
2768− enter_insert_mode(cx);
3018+ insert_with_indent(cx, IndentFallbackPos::LineStart);
27693019}
27703020
2771−// A inserts at the end of each line with a selection
3021+// `A` inserts at the end of each line with a selection.
3022+// If the line is empty, automatically indent.
27723023fn insert_at_line_end(cx: &mut Context) {
3024+ insert_with_indent(cx, IndentFallbackPos::LineEnd);
3025+}
3026+
3027+// Enter insert mode and auto-indent the current line if it is empty.
3028+// If the line is not empty, move the cursor to the specified fallback position.
3029+fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
27733030 enter_insert_mode(cx);
3031+
27743032 let (view, doc) = current!(cx.editor);
27753033
2776− let selection = doc.selection(view.id).clone().transform(|range| {
2777− let text = doc.text().slice(..);
2778− let line = range.cursor_line(text);
2779− let pos = line_end_char_index(&text, line);
2780− Range::new(pos, pos)
3034+ let text = doc.text().slice(..);
3035+ let contents = doc.text();
3036+ let selection = doc.selection(view.id);
3037+
3038+ let language_config = doc.language_config();
3039+ let syntax = doc.syntax();
3040+ let tab_width = doc.tab_width();
3041+
3042+ let mut ranges = SmallVec::with_capacity(selection.len());
3043+ let mut offs = 0;
3044+
3045+ let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
3046+ let cursor_line = range.cursor_line(text);
3047+ let cursor_line_start = text.line_to_char(cursor_line);
3048+
3049+ if line_end_char_index(&text, cursor_line) == cursor_line_start {
3050+ // line is empty => auto indent
3051+ let line_end_index = cursor_line_start;
3052+
3053+ let indent = indent::indent_for_newline(
3054+ language_config,
3055+ syntax,
3056+ &doc.config.load().indent_heuristic,
3057+ &doc.indent_style,
3058+ tab_width,
3059+ text,
3060+ cursor_line,
3061+ line_end_index,
3062+ cursor_line,
3063+ );
3064+
3065+ // calculate new selection ranges
3066+ let pos = offs + cursor_line_start;
3067+ let indent_width = indent.chars().count();
3068+ ranges.push(Range::point(pos + indent_width));
3069+ offs += indent_width;
3070+
3071+ (line_end_index, line_end_index, Some(indent.into()))
3072+ } else {
3073+ // move cursor to the fallback position
3074+ let pos = match cursor_fallback {
3075+ IndentFallbackPos::LineStart => {
3076+ find_first_non_whitespace_char(text.line(cursor_line))
3077+ .map(|ws_offset| ws_offset + cursor_line_start)
3078+ .unwrap_or(cursor_line_start)
3079+ }
3080+ IndentFallbackPos::LineEnd => line_end_char_index(&text, cursor_line),
3081+ };
3082+
3083+ ranges.push(range.put_cursor(text, pos + offs, cx.editor.mode == Mode::Select));
3084+
3085+ (cursor_line_start, cursor_line_start, None)
3086+ }
27813087 });
2782− doc.set_selection(view.id, selection);
3088+
3089+ transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
3090+ doc.apply(&transaction, view.id);
27833091}
27843092
27853093// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
@@ -2850,6 +3158,7 @@ fn open(cx: &mut Context, open: Open) {
28503158 Open::Below => graphemes::prev_grapheme_boundary(text, range.to()),
28513159 Open::Above => range.from(),
28523160 });
3161+
28533162 let new_line = match open {
28543163 // adjust position to the end of the line (next line - 1)
28553164 Open::Below => cursor_line + 1,
@@ -2857,13 +3166,15 @@ fn open(cx: &mut Context, open: Open) {
28573166 Open::Above => cursor_line,
28583167 };
28593168
3169+ let line_num = new_line.saturating_sub(1);
3170+
28603171 // Index to insert newlines after, as well as the char width
28613172 // to use to compensate for those inserted newlines.
28623173 let (line_end_index, line_end_offset_width) = if new_line == 0 {
28633174 (0, 0)
28643175 } else {
28653176 (
2866− line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
3177+ line_end_char_index(&text, line_num),
28673178 doc.line_ending.len_chars(),
28683179 )
28693180 };
@@ -2871,13 +3182,15 @@ fn open(cx: &mut Context, open: Open) {
28713182 let indent = indent::indent_for_newline(
28723183 doc.language_config(),
28733184 doc.syntax(),
3185+ &doc.config.load().indent_heuristic,
28743186 &doc.indent_style,
28753187 doc.tab_width(),
28763188 text,
2877− new_line.saturating_sub(1),
3189+ line_num,
28783190 line_end_index,
28793191 cursor_line,
28803192 );
3193+
28813194 let indent_len = indent.len();
28823195 let mut text = String::with_capacity(1 + indent_len);
28833196 text.push_str(doc.line_ending.as_str());
@@ -3176,8 +3489,7 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) {
31763489
31773490 doc.set_selection(view.id, selection)
31783491 };
3179− motion(cx.editor);
3180− cx.editor.last_motion = Some(Motion(Box::new(motion)));
3492+ cx.editor.apply_motion(motion);
31813493}
31823494
31833495/// Returns the [Range] for a [Hunk] in the given text.
@@ -3304,6 +3616,7 @@ pub mod insert {
33043616 }
33053617
33063618 use helix_core::auto_pairs;
3619+ use helix_view::editor::SmartTabConfig;
33073620
33083621 pub fn insert_char(cx: &mut Context, c: char) {
33093622 let (view, doc) = current_ref!(cx.editor);
@@ -3329,6 +3642,31 @@ pub mod insert {
33293642 }
33303643 }
33313644
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+
33323670 pub fn insert_tab(cx: &mut Context) {
33333671 let (view, doc) = current!(cx.editor);
33343672 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
@@ -3384,6 +3722,7 @@ pub mod insert {
33843722 let indent = indent::indent_for_newline(
33853723 doc.language_config(),
33863724 doc.syntax(),
3725+ &doc.config.load().indent_heuristic,
33873726 &doc.indent_style,
33883727 doc.tab_width(),
33893728 text,
@@ -3624,34 +3963,21 @@ fn commit_undo_checkpoint(cx: &mut Context) {
36243963// Yank / Paste
36253964
36263965fn yank(cx: &mut Context) {
3627− let (view, doc) = current!(cx.editor);
3628− let text = doc.text().slice(..);
3629−
3630− let values: Vec<String> = doc
3631− .selection(view.id)
3632− .fragments(text)
3633− .map(Cow::into_owned)
3634− .collect();
3635−
3636− let msg = format!(
3637− "yanked {} selection(s) to register {}",
3638− values.len(),
3639− cx.register.unwrap_or('"')
3640− );
3966+ yank_impl(cx.editor, cx.register.unwrap_or('"'));
3967+ exit_select_mode(cx);
3968+}
36413969
3642− cx.editor
3643− .registers
3644− .write(cx.register.unwrap_or('"'), values);
3970+fn yank_to_clipboard(cx: &mut Context) {
3971+ yank_impl(cx.editor, '+');
3972+ exit_select_mode(cx);
3973+}
36453974
3646− cx.editor.set_status(msg);
3975+fn yank_to_primary_clipboard(cx: &mut Context) {
3976+ yank_impl(cx.editor, '*');
36473977 exit_select_mode(cx);
36483978}
36493979
3650−fn yank_joined_to_clipboard_impl(
3651− editor: &mut Editor,
3652− separator: &str,
3653− clipboard_type: ClipboardType,
3654−) -> anyhow::Result<()> {
3980+fn yank_impl(editor: &mut Editor, register: char) {
36553981 let (view, doc) = current!(editor);
36563982 let text = doc.text().slice(..);
36573983
@@ -3660,74 +3986,79 @@ fn yank_joined_to_clipboard_impl(
36603986 .fragments(text)
36613987 .map(Cow::into_owned)
36623988 .collect();
3989+ let selections = values.len();
36633990
3664− let clipboard_text = match clipboard_type {
3665− ClipboardType::Clipboard => "system clipboard",
3666− ClipboardType::Selection => "primary clipboard",
3667− };
3668−
3669− let msg = format!(
3670− "joined and yanked {} selection(s) to {}",
3671− values.len(),
3672− clipboard_text,
3673− );
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+ }
3998+}
36743999
3675− let joined = values.join(separator);
4000+fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) {
4001+ let (view, doc) = current!(editor);
4002+ let text = doc.text().slice(..);
36764003
3677− editor
3678− .clipboard_provider
3679− .set_contents(joined, clipboard_type)
3680− .context("Couldn't set system clipboard content")?;
4004+ let selection = doc.selection(view.id);
4005+ let selections = selection.len();
4006+ let joined = selection
4007+ .fragments(text)
4008+ .fold(String::new(), |mut acc, fragment| {
4009+ if !acc.is_empty() {
4010+ acc.push_str(separator);
4011+ }
4012+ acc.push_str(&fragment);
4013+ acc
4014+ });
36814015
3682− editor.set_status(msg);
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+ }
4023+}
36834024
3684− Ok(())
4025+fn 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('"'));
4028+ exit_select_mode(cx);
36854029}
36864030
36874031fn yank_joined_to_clipboard(cx: &mut Context) {
36884032 let line_ending = doc!(cx.editor).line_ending;
3689− let _ =
3690− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
4033+ yank_joined_impl(cx.editor, line_ending.as_str(), '+');
36914034 exit_select_mode(cx);
36924035}
36934036
3694−fn yank_main_selection_to_clipboard_impl(
3695− editor: &mut Editor,
3696− clipboard_type: ClipboardType,
3697−) -> anyhow::Result<()> {
4037+fn yank_joined_to_primary_clipboard(cx: &mut Context) {
4038+ let line_ending = doc!(cx.editor).line_ending;
4039+ yank_joined_impl(cx.editor, line_ending.as_str(), '*');
4040+ exit_select_mode(cx);
4041+}
4042+
4043+fn yank_primary_selection_impl(editor: &mut Editor, register: char) {
36984044 let (view, doc) = current!(editor);
36994045 let text = doc.text().slice(..);
37004046
3701− let message_text = match clipboard_type {
3702− ClipboardType::Clipboard => "yanked main selection to system clipboard",
3703− ClipboardType::Selection => "yanked main selection to primary clipboard",
3704− };
3705−
3706− let value = doc.selection(view.id).primary().fragment(text);
4047+ let selection = doc.selection(view.id).primary().fragment(text).to_string();
37074048
3708− if let Err(e) = editor
3709− .clipboard_provider
3710− .set_contents(value.into_owned(), clipboard_type)
3711− {
3712− bail!("Couldn't set system clipboard content: {}", e);
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()),
37134052 }
3714−
3715− editor.set_status(message_text);
3716− Ok(())
37174053}
37184054
37194055fn yank_main_selection_to_clipboard(cx: &mut Context) {
3720− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
3721−}
3722−
3723−fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3724− let line_ending = doc!(cx.editor).line_ending;
3725− let _ =
3726− yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
4056+ yank_primary_selection_impl(cx.editor, '+');
4057+ exit_select_mode(cx);
37274058}
37284059
37294060fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
3730− let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
4061+ yank_primary_selection_impl(cx.editor, '*');
37314062 exit_select_mode(cx);
37324063}
37334064
@@ -3825,146 +4156,99 @@ pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
38254156 };
38264157 let (view, doc) = current!(cx.editor);
38274158 paste_impl(&[contents], doc, view, paste, count, cx.editor.mode);
3828−}
3829−
3830−fn paste_clipboard_impl(
3831− editor: &mut Editor,
3832− action: Paste,
3833− clipboard_type: ClipboardType,
3834− count: usize,
3835−) -> anyhow::Result<()> {
3836− let (view, doc) = current!(editor);
3837− match editor.clipboard_provider.get_contents(clipboard_type) {
3838− Ok(contents) => {
3839− paste_impl(&[contents], doc, view, action, count, editor.mode);
3840− Ok(())
3841− }
3842− Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3843− }
4159+ exit_select_mode(cx);
38444160}
38454161
38464162fn paste_clipboard_after(cx: &mut Context) {
3847− let _ = paste_clipboard_impl(
3848− cx.editor,
3849− Paste::After,
3850− ClipboardType::Clipboard,
3851− cx.count(),
3852− );
4163+ paste(cx.editor, '+', Paste::After, cx.count());
4164+ exit_select_mode(cx);
38534165}
38544166
38554167fn paste_clipboard_before(cx: &mut Context) {
3856− let _ = paste_clipboard_impl(
3857− cx.editor,
3858− Paste::Before,
3859− ClipboardType::Clipboard,
3860− cx.count(),
3861− );
4168+ paste(cx.editor, '+', Paste::Before, cx.count());
4169+ exit_select_mode(cx);
38624170}
38634171
38644172fn paste_primary_clipboard_after(cx: &mut Context) {
3865− let _ = paste_clipboard_impl(
3866− cx.editor,
3867− Paste::After,
3868− ClipboardType::Selection,
3869− cx.count(),
3870− );
4173+ paste(cx.editor, '*', Paste::After, cx.count());
4174+ exit_select_mode(cx);
38714175}
38724176
38734177fn paste_primary_clipboard_before(cx: &mut Context) {
3874− let _ = paste_clipboard_impl(
3875− cx.editor,
3876− Paste::Before,
3877− ClipboardType::Selection,
3878− cx.count(),
3879− );
4178+ paste(cx.editor, '*', Paste::Before, cx.count());
4179+ exit_select_mode(cx);
38804180}
38814181
38824182fn replace_with_yanked(cx: &mut Context) {
3883− let count = cx.count();
3884− let reg_name = cx.register.unwrap_or('"');
3885− let (view, doc) = current!(cx.editor);
3886− let registers = &mut cx.editor.registers;
3887−
3888− if let Some(values) = registers.read(reg_name) {
3889− if !values.is_empty() {
3890− let repeat = std::iter::repeat(
3891− values
3892− .last()
3893− .map(|value| Tendril::from(&value.repeat(count)))
3894− .unwrap(),
3895− );
3896− let mut values = values
3897− .iter()
3898− .map(|value| Tendril::from(&value.repeat(count)))
3899− .chain(repeat);
3900− let selection = doc.selection(view.id);
3901− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3902− if !range.is_empty() {
3903− (range.from(), range.to(), Some(values.next().unwrap()))
3904− } else {
3905− (range.from(), range.to(), None)
3906− }
3907− });
3908−
3909− doc.apply(&transaction, view.id);
3910− exit_select_mode(cx);
3911− }
3912− }
4183+ replace_with_yanked_impl(cx.editor, cx.register.unwrap_or('"'), cx.count());
4184+ exit_select_mode(cx);
39134185}
39144186
3915−fn replace_selections_with_clipboard_impl(
3916− cx: &mut Context,
3917− clipboard_type: ClipboardType,
3918−) -> anyhow::Result<()> {
3919− let count = cx.count();
3920− let (view, doc) = current!(cx.editor);
3921−
3922− match cx.editor.clipboard_provider.get_contents(clipboard_type) {
3923− Ok(contents) => {
3924− let selection = doc.selection(view.id);
3925− let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3926− (
3927− range.from(),
3928− range.to(),
3929− Some(contents.repeat(count).as_str().into()),
3930− )
3931− });
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();
39324192
3933− doc.apply(&transaction, view.id);
3934− doc.append_changes_to_history(view);
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)
39354210 }
3936− Err(e) => return Err(e.context("Couldn't get system clipboard contents")),
3937− }
4211+ });
39384212
3939− exit_select_mode(cx);
3940− Ok(())
4213+ doc.apply(&transaction, view.id);
39414214}
39424215
39434216fn replace_selections_with_clipboard(cx: &mut Context) {
3944− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard);
4217+ replace_with_yanked_impl(cx.editor, '+', cx.count());
4218+ exit_select_mode(cx);
39454219}
39464220
39474221fn replace_selections_with_primary_clipboard(cx: &mut Context) {
3948− let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection);
4222+ replace_with_yanked_impl(cx.editor, '*', cx.count());
4223+ exit_select_mode(cx);
39494224}
39504225
3951−fn paste(cx: &mut Context, pos: Paste) {
3952− let count = cx.count();
3953− let reg_name = cx.register.unwrap_or('"');
3954− let (view, doc) = current!(cx.editor);
3955− let registers = &mut cx.editor.registers;
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();
39564229
3957− if let Some(values) = registers.read(reg_name) {
3958− paste_impl(values, doc, view, pos, count, cx.editor.mode);
3959− }
4230+ let (view, doc) = current!(editor);
4231+ paste_impl(&values, doc, view, pos, count, editor.mode);
39604232}
39614233
39624234fn paste_after(cx: &mut Context) {
3963− paste(cx, Paste::After)
4235+ paste(
4236+ cx.editor,
4237+ cx.register.unwrap_or('"'),
4238+ Paste::After,
4239+ cx.count(),
4240+ );
4241+ exit_select_mode(cx);
39644242}
39654243
39664244fn paste_before(cx: &mut Context) {
3967− paste(cx, Paste::Before)
4245+ paste(
4246+ cx.editor,
4247+ cx.register.unwrap_or('"'),
4248+ Paste::Before,
4249+ cx.count(),
4250+ );
4251+ exit_select_mode(cx);
39684252}
39694253
39704254fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
@@ -4003,6 +4287,7 @@ fn indent(cx: &mut Context) {
40034287 }),
40044288 );
40054289 doc.apply(&transaction, view.id);
4290+ exit_select_mode(cx);
40064291}
40074292
40084293fn unindent(cx: &mut Context) {
@@ -4042,6 +4327,7 @@ fn unindent(cx: &mut Context) {
40424327 let transaction = Transaction::change(doc.text(), changes.into_iter());
40434328
40444329 doc.apply(&transaction, view.id);
4330+ exit_select_mode(cx);
40454331}
40464332
40474333fn format_selections(cx: &mut Context) {
@@ -4071,7 +4357,7 @@ fn format_selections(cx: &mut Context) {
40714357 })
40724358 else {
40734359 cx.editor
4074− .set_error("No configured language server does not support range formatting");
4360+ .set_error("No configured language server supports range formatting");
40754361 return;
40764362 };
40774363
@@ -4108,10 +4394,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
41084394 use movement::skip_while;
41094395 let (view, doc) = current!(cx.editor);
41104396 let text = doc.text();
4111− let slice = doc.text().slice(..);
4397+ let slice = text.slice(..);
41124398
41134399 let mut changes = Vec::new();
4114− let fragment = Tendril::from(" ");
41154400
41164401 for selection in doc.selection(view.id) {
41174402 let (start, mut end) = selection.line_range(slice);
@@ -4127,9 +4412,13 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
41274412 let mut end = text.line_to_char(line + 1);
41284413 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
41294414
4130− // need to skip from start, not end
4131− let change = (start, end, Some(fragment.clone()));
4132− changes.push(change);
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));
41334422 }
41344423 }
41354424
@@ -4141,9 +4430,6 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
41414430 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
41424431 changes.dedup();
41434432
4144− // TODO: joining multiple empty lines should be replaced by a single space.
4145− // need to merge change ranges that touch
4146−
41474433 // select inserted spaces
41484434 let transaction = if select_space {
41494435 let ranges: SmallVec<_> = changes
@@ -4155,9 +4441,9 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) {
41554441 })
41564442 .collect();
41574443 let selection = Selection::new(ranges, 0);
4158− Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
4444+ Transaction::change(text, changes.into_iter()).with_selection(selection)
41594445 } else {
4160− Transaction::change(doc.text(), changes.into_iter())
4446+ Transaction::change(text, changes.into_iter())
41614447 };
41624448
41634449 doc.apply(&transaction, view.id);
@@ -4171,8 +4457,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
41714457 if remove { "remove:" } else { "keep:" }.into(),
41724458 Some(reg),
41734459 ui::completers::none,
4174− move |editor, regex, event| {
4175− let (view, doc) = current!(editor);
4460+ move |cx, regex, event| {
4461+ let (view, doc) = current!(cx.editor);
41764462 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
41774463 return;
41784464 }
@@ -4406,7 +4692,13 @@ fn rotate_selections_backward(cx: &mut Context) {
44064692 rotate_selections(cx, Direction::Backward)
44074693}
44084694
4409−fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
4695+enum ReorderStrategy {
4696+ RotateForward,
4697+ RotateBackward,
4698+ Reverse,
4699+}
4700+
4701+fn reorder_selection_contents(cx: &mut Context, strategy: ReorderStrategy) {
44104702 let count = cx.count;
44114703 let (view, doc) = current!(cx.editor);
44124704 let text = doc.text().slice(..);
@@ -4424,9 +4716,10 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
44244716
44254717 for chunk in fragments.chunks_mut(group) {
44264718 // TODO: also modify main index
4427− match direction {
4428− Direction::Forward => chunk.rotate_right(1),
4429− Direction::Backward => chunk.rotate_left(1),
4719+ match strategy {
4720+ ReorderStrategy::RotateForward => chunk.rotate_right(1),
4721+ ReorderStrategy::RotateBackward => chunk.rotate_left(1),
4722+ ReorderStrategy::Reverse => chunk.reverse(),
44304723 };
44314724 }
44324725
@@ -4443,10 +4736,13 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
44434736}
44444737
44454738fn rotate_selection_contents_forward(cx: &mut Context) {
4446− rotate_selection_contents(cx, Direction::Forward)
4739+ reorder_selection_contents(cx, ReorderStrategy::RotateForward)
44474740}
44484741fn rotate_selection_contents_backward(cx: &mut Context) {
4449− rotate_selection_contents(cx, Direction::Backward)
4742+ reorder_selection_contents(cx, ReorderStrategy::RotateBackward)
4743+}
4744+fn reverse_selection_contents(cx: &mut Context) {
4745+ reorder_selection_contents(cx, ReorderStrategy::Reverse)
44504746}
44514747
44524748// tree sitter node selection
@@ -4470,8 +4766,7 @@ fn expand_selection(cx: &mut Context) {
44704766 }
44714767 }
44724768 };
4473− motion(cx.editor);
4474− cx.editor.last_motion = Some(Motion(Box::new(motion)));
4769+ cx.editor.apply_motion(motion);
44754770}
44764771
44774772fn shrink_selection(cx: &mut Context) {
@@ -4495,8 +4790,7 @@ fn shrink_selection(cx: &mut Context) {
44954790 doc.set_selection(view.id, selection);
44964791 }
44974792 };
4498− motion(cx.editor);
4499− cx.editor.last_motion = Some(Motion(Box::new(motion)));
4793+ cx.editor.apply_motion(motion);
45004794}
45014795
45024796fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
@@ -4514,8 +4808,7 @@ where
45144808 doc.set_selection(view.id, selection);
45154809 }
45164810 };
4517− motion(cx.editor);
4518− cx.editor.last_motion = Some(Motion(Box::new(motion)));
4811+ cx.editor.apply_motion(motion);
45194812}
45204813
45214814fn select_next_sibling(cx: &mut Context) {
@@ -4526,6 +4819,49 @@ fn select_prev_sibling(cx: &mut Context) {
45264819 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
45274820}
45284821
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+
45294865fn match_brackets(cx: &mut Context) {
45304866 let (view, doc) = current!(cx.editor);
45314867 let is_select = cx.editor.mode == Mode::Select;
@@ -4535,8 +4871,8 @@ fn match_brackets(cx: &mut Context) {
45354871 let selection = doc.selection(view.id).clone().transform(|range| {
45364872 let pos = range.cursor(text_slice);
45374873 if let Some(matched_pos) = doc.syntax().map_or_else(
4538− || match_brackets::find_matching_bracket_current_line_plaintext(text, pos),
4539− |syntax| match_brackets::find_matching_bracket_fuzzy(syntax, text, pos),
4874+ || match_brackets::find_matching_bracket_plaintext(text.slice(..), pos),
4875+ |syntax| match_brackets::find_matching_bracket_fuzzy(syntax, text.slice(..), pos),
45404876 ) {
45414877 range.put_cursor(text_slice, matched_pos, is_select)
45424878 } else {
@@ -4639,17 +4975,19 @@ fn transpose_view(cx: &mut Context) {
46394975 cx.editor.transpose_view()
46404976}
46414977
4642−// split helper, clear it later
4643−fn split(cx: &mut Context, action: Action) {
4644− let (view, doc) = current!(cx.editor);
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);
46454983 let id = doc.id();
46464984 let selection = doc.selection(view.id).clone();
46474985 let offset = view.offset;
46484986
4649− cx.editor.switch(id, action);
4987+ editor.switch(id, action);
46504988
46514989 // match the selection in the previous view
4652− let (view, doc) = current!(cx.editor);
4990+ let (view, doc) = current!(editor);
46534991 doc.set_selection(view.id, selection);
46544992 // match the view scroll offset (switch doesn't handle this fully
46554993 // since the selection is only matched after the split)
@@ -4657,7 +4995,7 @@ fn split(cx: &mut Context, action: Action) {
46574995}
46584996
46594997fn hsplit(cx: &mut Context) {
4660− split(cx, Action::HorizontalSplit);
4998+ split(cx.editor, Action::HorizontalSplit);
46614999}
46625000
46635001fn hsplit_new(cx: &mut Context) {
@@ -4665,7 +5003,7 @@ fn hsplit_new(cx: &mut Context) {
46655003}
46665004
46675005fn vsplit(cx: &mut Context) {
4668− split(cx, Action::VerticalSplit);
5006+ split(cx.editor, Action::VerticalSplit);
46695007}
46705008
46715009fn vsplit_new(cx: &mut Context) {
@@ -4714,7 +5052,12 @@ fn insert_register(cx: &mut Context) {
47145052 if let Some(ch) = event.char() {
47155053 cx.editor.autoinfo = None;
47165054 cx.register = Some(ch);
4717− paste(cx, Paste::Cursor);
5055+ paste(
5056+ cx.editor,
5057+ cx.register.unwrap_or('"'),
5058+ Paste::Cursor,
5059+ cx.count(),
5060+ );
47185061 }
47195062 })
47205063}
@@ -4798,8 +5141,7 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
47985141 editor.set_status("Syntax-tree is not available in current buffer");
47995142 }
48005143 };
4801− motion(cx.editor);
4802− cx.editor.last_motion = Some(Motion(Box::new(motion)));
5144+ cx.editor.apply_motion(motion);
48035145}
48045146
48055147fn goto_next_function(cx: &mut Context) {
@@ -4920,8 +5262,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
49205262 });
49215263 doc.set_selection(view.id, selection);
49225264 };
4923− textobject(cx.editor);
4924− cx.editor.last_motion = Some(Motion(Box::new(textobject)));
5265+ cx.editor.apply_motion(textobject);
49255266 }
49265267 });
49275268
@@ -5011,11 +5352,18 @@ fn surround_replace(cx: &mut Context) {
50115352 }
50125353 };
50135354
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+
50145362 cx.on_next_key(move |cx, event| {
50155363 let (view, doc) = current!(cx.editor);
50165364 let to = match event.char() {
50175365 Some(to) => to,
5018− None => return,
5366+ None => return doc.set_selection(view.id, selection),
50195367 };
50205368 let (open, close) = surround::get_pair(to);
50215369 let transaction = Transaction::change(
@@ -5026,6 +5374,7 @@ fn surround_replace(cx: &mut Context) {
50265374 (pos, pos + 1, Some(t))
50275375 }),
50285376 );
5377+ doc.set_selection(view.id, selection);
50295378 doc.apply(&transaction, view.id);
50305379 exit_select_mode(cx);
50315380 });
@@ -5260,12 +5609,18 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
52605609 };
52615610
52625611 // These `usize`s cannot underflow because selection ranges cannot overlap.
5263− // Once the MSRV is 1.66.0 (mixed_integer_ops is stabilized), we can use checked
5264− // arithmetic to assert this.
5265− let anchor = (to as isize + offset - deleted_len as isize) as usize;
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");
52665617 let new_range = Range::new(anchor, anchor + output_len).with_direction(range.direction());
52675618 ranges.push(new_range);
5268− 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");
52695624
52705625 changes.push((from, to, Some(output)));
52715626 }
@@ -5287,7 +5642,7 @@ fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBeha
52875642 cx,
52885643 prompt,
52895644 Some('|'),
5290− ui::completers::none,
5645+ ui::completers::filename,
52915646 move |cx, input: &str, event: PromptEvent| {
52925647 if event != PromptEvent::Validate {
52935648 return;
@@ -5404,6 +5759,7 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
54045759 let transaction = Transaction::change(doc.text(), changes.into_iter());
54055760 let transaction = transaction.with_selection(new_selection);
54065761 doc.apply(&transaction, view.id);
5762+ exit_select_mode(cx);
54075763 }
54085764}
54095765
@@ -5422,9 +5778,12 @@ fn record_macro(cx: &mut Context) {
54225778 }
54235779 })
54245780 .collect::<String>();
5425− cx.editor.registers.write(reg, vec![s]);
5426− cx.editor
5427− .set_status(format!("Recorded to register [{}]", reg));
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+ }
54285787 } else {
54295788 let reg = cx.register.take().unwrap_or('@');
54305789 cx.editor.macro_recording = Some((reg, Vec::new()));
@@ -5444,8 +5803,14 @@ fn replay_macro(cx: &mut Context) {
54445803 return;
54455804 }
54465805
5447− let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
5448− match helix_view::input::parse_macro(keys_str) {
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) {
54495814 Ok(keys) => keys,
54505815 Err(err) => {
54515816 cx.editor.set_error(format!("Invalid macro: {}", err));