@@ -1,16 +1,16 @@
11use helix_core::{
22 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
33 history::UndoKind,
4+ increment::date_time::DateTimeIncrementor,
5+ increment::{number::NumberIncrementor, Increment},
46 indent,
57 indent::IndentStyle,
68 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
79 match_brackets,
810 movement::{self, Direction},
9− numbers::NumberIncrementor,
1011 object, pos_at_coords,
1112 regex::{self, Regex, RegexBuilder},
12− register::Register,
13− search, selection, surround, textobject,
13+ search, selection, shellwords, surround, textobject,
1414 unicode::width::UnicodeWidthChar,
1515 LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
1616 Transaction,
@@ -25,7 +25,7 @@ use helix_view::{
2525 Document, DocumentId, Editor, ViewId,
2626};
2727
28−use anyhow::{anyhow, bail, Context as _};
28+use anyhow::{anyhow, bail, ensure, Context as _};
2929use helix_lsp::{
3030 block_on, lsp,
3131 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -41,7 +41,7 @@ use crate::{
4141
4242use crate::job::{self, Job, Jobs};
4343use futures_util::{FutureExt, StreamExt};
44−use std::num::NonZeroUsize;
44+use std::{collections::HashSet, num::NonZeroUsize};
4545use std::{fmt, future::Future};
4646
4747use std::{
@@ -70,7 +70,7 @@ pub struct Context<'a> {
7070impl<'a> Context<'a> {
7171 /// Push a new component onto the compositor.
7272 pub fn push_layer(&mut self, component: Box<dyn Component>) {
73− self.callback = Some(Box::new(|compositor: &mut Compositor| {
73+ self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
7474 compositor.push(component)
7575 }));
7676 }
@@ -135,47 +135,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
135135 view.offset.row = line.saturating_sub(relative);
136136}
137137
138−/// A command is composed of a static name, and a function that takes the current state plus a count,
139−/// and does a side-effect on the state (usually by creating and applying a transaction).
140−#[derive(Copy, Clone)]
141−pub struct Command {
142− name: &'static str,
143− fun: fn(cx: &mut Context),
144− doc: &'static str,
145−}
146−
147−macro_rules! commands {
138+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
139+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
140+/// Both of these types of commands can be mapped with keybindings in the config.toml.
141+#[derive(Clone)]
142+pub enum MappableCommand {
143+ Typable {
144+ name: String,
145+ args: Vec<String>,
146+ doc: String,
147+ },
148+ Static {
149+ name: &'static str,
150+ fun: fn(cx: &mut Context),
151+ doc: &'static str,
152+ },
153+}
154+
155+macro_rules! static_commands {
148156 ( $($name:ident, $doc:literal,)* ) => {
149157 $(
150158 #[allow(non_upper_case_globals)]
151− pub const $name: Self = Self {
159+ pub const $name: Self = Self::Static {
152160 name: stringify!($name),
153161 fun: $name,
154162 doc: $doc
155163 };
156164 )*
157165
158− pub const COMMAND_LIST: &'static [Self] = &[
166+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
159167 $( Self::$name, )*
160168 ];
161169 }
162170}
163171
164−impl Command {
172+impl MappableCommand {
165173 pub fn execute(&self, cx: &mut Context) {
166− (self.fun)(cx);
174+ match &self {
175+ MappableCommand::Typable { name, args, doc: _ } => {
176+ let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
177+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
178+ let mut cx = compositor::Context {
179+ editor: cx.editor,
180+ jobs: cx.jobs,
181+ scroll: None,
182+ };
183+ if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
184+ cx.editor.set_error(format!("{}", e));
185+ }
186+ }
187+ }
188+ MappableCommand::Static { fun, .. } => (fun)(cx),
189+ }
167190 }
168191
169− pub fn name(&self) -> &'static str {
170− self.name
192+ pub fn name(&self) -> &str {
193+ match &self {
194+ MappableCommand::Typable { name, .. } => name,
195+ MappableCommand::Static { name, .. } => name,
196+ }
171197 }
172198
173− pub fn doc(&self) -> &'static str {
174− self.doc
199+ pub fn doc(&self) -> &str {
200+ match &self {
201+ MappableCommand::Typable { doc, .. } => doc,
202+ MappableCommand::Static { doc, .. } => doc,
203+ }
175204 }
176205
177206 #[rustfmt::skip]
178− commands!(
207+ static_commands!(
179208 no_op, "Do nothing",
180209 move_char_left, "Move left",
181210 move_char_right, "Move right",
@@ -232,7 +261,9 @@ impl Command {
232261 extend_line, "Select current line, if already selected, extend to next line",
233262 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
234263 delete_selection, "Delete selection",
264+ delete_selection_noyank, "Delete selection, without yanking",
235265 change_selection, "Change selection (delete and enter insert mode)",
266+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
236267 collapse_selection, "Collapse selection onto a single cursor",
237268 flip_selections, "Flip selection cursor and anchor",
238269 insert_mode, "Insert before selection",
@@ -258,11 +289,15 @@ impl Command {
258289 goto_implementation, "Goto implementation",
259290 goto_file_start, "Goto file start/line",
260291 goto_file_end, "Goto file end",
292+ goto_file, "Goto files in selection",
293+ goto_file_hsplit, "Goto files in selection (hsplit)",
294+ goto_file_vsplit, "Goto files in selection (vsplit)",
261295 goto_reference, "Goto references",
262296 goto_window_top, "Goto window top",
263− goto_window_middle, "Goto window middle",
297+ goto_window_center, "Goto window center",
264298 goto_window_bottom, "Goto window bottom",
265299 goto_last_accessed_file, "Goto last accessed file",
300+ goto_last_modified_file, "Goto last modified file",
266301 goto_last_modification, "Goto last modification",
267302 goto_line, "Goto line",
268303 goto_last_line, "Goto last line",
@@ -327,6 +362,7 @@ impl Command {
327362 expand_selection, "Expand selection to parent syntax node",
328363 jump_forward, "Jump forward on jumplist",
329364 jump_backward, "Jump backward on jumplist",
365+ save_selection, "Save the current selection to the jumplist",
330366 jump_view_right, "Jump to the split to the right",
331367 jump_view_left, "Jump to the split to the left",
332368 jump_view_up, "Jump to the split above",
@@ -359,36 +395,56 @@ impl Command {
359395 rename_symbol, "Rename symbol",
360396 increment, "Increment",
361397 decrement, "Decrement",
398+ record_macro, "Record macro",
399+ play_macro, "Play macro",
362400 );
363401}
364402
365−impl fmt::Debug for Command {
403+impl fmt::Debug for MappableCommand {
366404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367− let Command { name, .. } = self;
368− f.debug_tuple("Command").field(name).finish()
405+ f.debug_tuple("MappableCommand")
406+ .field(&self.name())
407+ .finish()
369408 }
370409}
371410
372−impl fmt::Display for Command {
411+impl fmt::Display for MappableCommand {
373412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374− let Command { name, .. } = self;
375− f.write_str(name)
413+ f.write_str(self.name())
376414 }
377415}
378416
379−impl std::str::FromStr for Command {
417+impl std::str::FromStr for MappableCommand {
380418 type Err = anyhow::Error;
381419
382420 fn from_str(s: &str) -> Result<Self, Self::Err> {
383− Command::COMMAND_LIST
384− .iter()
385− .copied()
386− .find(|cmd| cmd.name == s)
387− .ok_or_else(|| anyhow!("No command named '{}'", s))
421+ if let Some(suffix) = s.strip_prefix(':') {
422+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
423+ let name = typable_command
424+ .next()
425+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
426+ let args = typable_command
427+ .map(|s| s.to_owned())
428+ .collect::<Vec<String>>();
429+ cmd::TYPABLE_COMMAND_MAP
430+ .get(name)
431+ .map(|cmd| MappableCommand::Typable {
432+ name: cmd.name.to_owned(),
433+ doc: format!(":{} {:?}", cmd.name, args),
434+ args,
435+ })
436+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
437+ } else {
438+ MappableCommand::STATIC_COMMAND_LIST
439+ .iter()
440+ .cloned()
441+ .find(|cmd| cmd.name() == s)
442+ .ok_or_else(|| anyhow!("No command named '{}'", s))
443+ }
388444 }
389445}
390446
391−impl<'de> Deserialize<'de> for Command {
447+impl<'de> Deserialize<'de> for MappableCommand {
392448 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
393449 where
394450 D: Deserializer<'de>,
@@ -398,9 +454,27 @@ impl<'de> Deserialize<'de> for Command {
398454 }
399455}
400456
401−impl PartialEq for Command {
457+impl PartialEq for MappableCommand {
402458 fn eq(&self, other: &Self) -> bool {
403− self.name() == other.name()
459+ match (self, other) {
460+ (
461+ MappableCommand::Typable {
462+ name: first_name, ..
463+ },
464+ MappableCommand::Typable {
465+ name: second_name, ..
466+ },
467+ ) => first_name == second_name,
468+ (
469+ MappableCommand::Static {
470+ name: first_name, ..
471+ },
472+ MappableCommand::Static {
473+ name: second_name, ..
474+ },
475+ ) => first_name == second_name,
476+ _ => false,
477+ }
404478 }
405479}
406480
@@ -599,8 +673,15 @@ fn kill_to_line_end(cx: &mut Context) {
599673
600674 let selection = doc.selection(view.id).clone().transform(|range| {
601675 let line = range.cursor_line(text);
602− let pos = line_end_char_index(&text, line);
603− range.put_cursor(text, pos, true)
676+ let line_end_pos = line_end_char_index(&text, line);
677+ let pos = range.cursor(text);
678+
679+ let mut new_range = range.put_cursor(text, line_end_pos, true);
680+ // don't want to remove the line separator itself if the cursor doesn't reach the end of line.
681+ if pos != line_end_pos {
682+ new_range.head = line_end_pos;
683+ }
684+ new_range
604685 });
605686 delete_selection_insert_mode(doc, view, &selection);
606687}
@@ -729,10 +810,12 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) ->
729810}
730811
731812fn goto_window(cx: &mut Context, align: Align) {
813+ let count = cx.count() - 1;
732814 let (view, doc) = current!(cx.editor);
733815
734816 let height = view.inner_area().height as usize;
735817
818+ // respect user given count if any
736819 // - 1 so we have at least one gap in the middle.
737820 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
738821 // as we type
@@ -741,10 +824,11 @@ fn goto_window(cx: &mut Context, align: Align) {
741824 let last_line = view.last_line(doc);
742825
743826 let line = match align {
744− Align::Top => (view.offset.row + scrolloff),
745− Align::Center => (view.offset.row + (height / 2)),
746− Align::Bottom => last_line.saturating_sub(scrolloff),
827+ Align::Top => (view.offset.row + scrolloff + count),
828+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
829+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
747830 }
831+ .max(view.offset.row + scrolloff)
748832 .min(last_line.saturating_sub(scrolloff));
749833
750834 let pos = doc.text().line_to_char(line);
@@ -756,7 +840,7 @@ fn goto_window_top(cx: &mut Context) {
756840 goto_window(cx, Align::Top)
757841}
758842
759−fn goto_window_middle(cx: &mut Context) {
843+fn goto_window_center(cx: &mut Context) {
760844 goto_window(cx, Align::Center)
761845}
762846
@@ -834,6 +918,49 @@ fn goto_file_end(cx: &mut Context) {
834918 doc.set_selection(view.id, selection);
835919}
836920
921+fn goto_file(cx: &mut Context) {
922+ goto_file_impl(cx, Action::Replace);
923+}
924+
925+fn goto_file_hsplit(cx: &mut Context) {
926+ goto_file_impl(cx, Action::HorizontalSplit);
927+}
928+
929+fn goto_file_vsplit(cx: &mut Context) {
930+ goto_file_impl(cx, Action::VerticalSplit);
931+}
932+
933+fn goto_file_impl(cx: &mut Context, action: Action) {
934+ let (view, doc) = current_ref!(cx.editor);
935+ let text = doc.text();
936+ let selections = doc.selection(view.id);
937+ let mut paths: Vec<_> = selections
938+ .iter()
939+ .map(|r| text.slice(r.from()..r.to()).to_string())
940+ .collect();
941+ let primary = selections.primary();
942+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
943+ let current_word = movement::move_next_long_word_start(
944+ text.slice(..),
945+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
946+ 1,
947+ );
948+ paths.clear();
949+ paths.push(
950+ text.slice(current_word.from()..current_word.to())
951+ .to_string(),
952+ );
953+ }
954+ for sel in paths {
955+ let p = sel.trim();
956+ if !p.is_empty() {
957+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
958+ cx.editor.set_error(format!("Open file failed: {:?}", e));
959+ }
960+ }
961+ }
962+}
963+
837964fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
838965where
839966 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1693,19 +1820,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
16931820 );
16941821}
16951822
1696−fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1823+enum Operation {
1824+ Delete,
1825+ Change,
1826+}
1827+
1828+fn delete_selection_impl(cx: &mut Context, op: Operation) {
1829+ let (view, doc) = current!(cx.editor);
1830+
16971831 let text = doc.text().slice(..);
1698− let selection = doc.selection(view_id);
1832+ let selection = doc.selection(view.id);
16991833
1700− // first yank the selection
1701− let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1702− reg.write(values);
1834+ if cx.register != Some('_') {
1835+ // first yank the selection
1836+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1837+ let reg_name = cx.register.unwrap_or('"');
1838+ let registers = &mut cx.editor.registers;
1839+ let reg = registers.get_mut(reg_name);
1840+ reg.write(values);
1841+ };
17031842
17041843 // then delete
17051844 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
17061845 (range.from(), range.to(), None)
17071846 });
1708− doc.apply(&transaction, view_id);
1847+ doc.apply(&transaction, view.id);
1848+
1849+ match op {
1850+ Operation::Delete => {
1851+ doc.append_changes_to_history(view.id);
1852+ // exit select mode, if currently in select mode
1853+ exit_select_mode(cx);
1854+ }
1855+ Operation::Change => {
1856+ enter_insert_mode(doc);
1857+ }
1858+ }
17091859}
17101860
17111861#[inline]
@@ -1720,25 +1870,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
17201870}
17211871
17221872fn delete_selection(cx: &mut Context) {
1723− let reg_name = cx.register.unwrap_or('"');
1724− let (view, doc) = current!(cx.editor);
1725− let registers = &mut cx.editor.registers;
1726− let reg = registers.get_mut(reg_name);
1727− delete_selection_impl(reg, doc, view.id);
1728−
1729− doc.append_changes_to_history(view.id);
1873+ delete_selection_impl(cx, Operation::Delete);
1874+}
17301875
1731− // exit select mode, if currently in select mode
1732− exit_select_mode(cx);
1876+fn delete_selection_noyank(cx: &mut Context) {
1877+ cx.register = Some('_');
1878+ delete_selection_impl(cx, Operation::Delete);
17331879}
17341880
17351881fn change_selection(cx: &mut Context) {
1736− let reg_name = cx.register.unwrap_or('"');
1737− let (view, doc) = current!(cx.editor);
1738− let registers = &mut cx.editor.registers;
1739− let reg = registers.get_mut(reg_name);
1740− delete_selection_impl(reg, doc, view.id);
1741− enter_insert_mode(doc);
1882+ delete_selection_impl(cx, Operation::Change);
1883+}
1884+
1885+fn change_selection_noyank(cx: &mut Context) {
1886+ cx.register = Some('_');
1887+ delete_selection_impl(cx, Operation::Change);
17421888}
17431889
17441890fn collapse_selection(cx: &mut Context) {
@@ -1806,7 +1952,7 @@ fn append_mode(cx: &mut Context) {
18061952 doc.set_selection(view.id, selection);
18071953}
18081954
1809−mod cmd {
1955+pub mod cmd {
18101956 use super::*;
18111957 use std::collections::HashMap;
18121958
@@ -1819,13 +1965,13 @@ mod cmd {
18191965 pub aliases: &'static [&'static str],
18201966 pub doc: &'static str,
18211967 // params, flags, helper, completer
1822− pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
1968+ pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
18231969 pub completer: Option<Completer>,
18241970 }
18251971
18261972 fn quit(
18271973 cx: &mut compositor::Context,
1828− _args: &[&str],
1974+ _args: &[Cow<str>],
18291975 _event: PromptEvent,
18301976 ) -> anyhow::Result<()> {
18311977 // last view and we have unsaved changes
@@ -1840,7 +1986,7 @@ mod cmd {
18401986
18411987 fn force_quit(
18421988 cx: &mut compositor::Context,
1843− _args: &[&str],
1989+ _args: &[Cow<str>],
18441990 _event: PromptEvent,
18451991 ) -> anyhow::Result<()> {
18461992 cx.editor.close(view!(cx.editor).id);
@@ -1850,17 +1996,19 @@ mod cmd {
18501996
18511997 fn open(
18521998 cx: &mut compositor::Context,
1853− args: &[&str],
1999+ args: &[Cow<str>],
18542000 _event: PromptEvent,
18552001 ) -> anyhow::Result<()> {
1856− let path = args.get(0).context("wrong argument count")?;
1857− let _ = cx.editor.open(path.into(), Action::Replace)?;
2002+ ensure!(!args.is_empty(), "wrong argument count");
2003+ for arg in args {
2004+ let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?;
2005+ }
18582006 Ok(())
18592007 }
18602008
18612009 fn buffer_close(
18622010 cx: &mut compositor::Context,
1863− _args: &[&str],
2011+ _args: &[Cow<str>],
18642012 _event: PromptEvent,
18652013 ) -> anyhow::Result<()> {
18662014 let view = view!(cx.editor);
@@ -1871,7 +2019,7 @@ mod cmd {
18712019
18722020 fn force_buffer_close(
18732021 cx: &mut compositor::Context,
1874− _args: &[&str],
2022+ _args: &[Cow<str>],
18752023 _event: PromptEvent,
18762024 ) -> anyhow::Result<()> {
18772025 let view = view!(cx.editor);
@@ -1880,15 +2028,12 @@ mod cmd {
18802028 Ok(())
18812029 }
18822030
1883− fn write_impl<P: AsRef<Path>>(
1884− cx: &mut compositor::Context,
1885− path: Option<P>,
1886− ) -> anyhow::Result<()> {
2031+ fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
18872032 let jobs = &mut cx.jobs;
18882033 let (_, doc) = current!(cx.editor);
18892034
1890− if let Some(path) = path {
1891− doc.set_path(Some(path.as_ref()))
2035+ if let Some(ref path) = path {
2036+ doc.set_path(Some(path.as_ref().as_ref()))
18922037 .context("invalid filepath")?;
18932038 }
18942039 if doc.path().is_none() {
@@ -1907,12 +2052,17 @@ mod cmd {
19072052 });
19082053 let future = doc.format_and_save(fmt);
19092054 cx.jobs.add(Job::new(future).wait_before_exiting());
2055+
2056+ if path.is_some() {
2057+ let id = doc.id();
2058+ let _ = cx.editor.refresh_language_server(id);
2059+ }
19102060 Ok(())
19112061 }
19122062
19132063 fn write(
19142064 cx: &mut compositor::Context,
1915− args: &[&str],
2065+ args: &[Cow<str>],
19162066 _event: PromptEvent,
19172067 ) -> anyhow::Result<()> {
19182068 write_impl(cx, args.first())
@@ -1920,7 +2070,7 @@ mod cmd {
19202070
19212071 fn new_file(
19222072 cx: &mut compositor::Context,
1923− _args: &[&str],
2073+ _args: &[Cow<str>],
19242074 _event: PromptEvent,
19252075 ) -> anyhow::Result<()> {
19262076 cx.editor.new_file(Action::Replace);
@@ -1930,7 +2080,7 @@ mod cmd {
19302080
19312081 fn format(
19322082 cx: &mut compositor::Context,
1933− _args: &[&str],
2083+ _args: &[Cow<str>],
19342084 _event: PromptEvent,
19352085 ) -> anyhow::Result<()> {
19362086 let (_, doc) = current!(cx.editor);
@@ -1945,7 +2095,7 @@ mod cmd {
19452095 }
19462096 fn set_indent_style(
19472097 cx: &mut compositor::Context,
1948− args: &[&str],
2098+ args: &[Cow<str>],
19492099 _event: PromptEvent,
19502100 ) -> anyhow::Result<()> {
19512101 use IndentStyle::*;
@@ -1965,7 +2115,7 @@ mod cmd {
19652115 // Attempt to parse argument as an indent style.
19662116 let style = match args.get(0) {
19672117 Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
1968− Some(&"0") => Some(Tabs),
2118+ Some(Cow::Borrowed("0")) => Some(Tabs),
19692119 Some(arg) => arg
19702120 .parse::<u8>()
19712121 .ok()
@@ -1984,7 +2134,7 @@ mod cmd {
19842134 /// Sets or reports the current document's line ending setting.
19852135 fn set_line_ending(
19862136 cx: &mut compositor::Context,
1987− args: &[&str],
2137+ args: &[Cow<str>],
19882138 _event: PromptEvent,
19892139 ) -> anyhow::Result<()> {
19902140 use LineEnding::*;
@@ -2028,7 +2178,7 @@ mod cmd {
20282178
20292179 fn earlier(
20302180 cx: &mut compositor::Context,
2031− args: &[&str],
2181+ args: &[Cow<str>],
20322182 _event: PromptEvent,
20332183 ) -> anyhow::Result<()> {
20342184 let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2044,7 +2194,7 @@ mod cmd {
20442194
20452195 fn later(
20462196 cx: &mut compositor::Context,
2047− args: &[&str],
2197+ args: &[Cow<str>],
20482198 _event: PromptEvent,
20492199 ) -> anyhow::Result<()> {
20502200 let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2059,7 +2209,7 @@ mod cmd {
20592209
20602210 fn write_quit(
20612211 cx: &mut compositor::Context,
2062− args: &[&str],
2212+ args: &[Cow<str>],
20632213 event: PromptEvent,
20642214 ) -> anyhow::Result<()> {
20652215 write_impl(cx, args.first())?;
@@ -2068,7 +2218,7 @@ mod cmd {
20682218
20692219 fn force_write_quit(
20702220 cx: &mut compositor::Context,
2071− args: &[&str],
2221+ args: &[Cow<str>],
20722222 event: PromptEvent,
20732223 ) -> anyhow::Result<()> {
20742224 write_impl(cx, args.first())?;
@@ -2099,7 +2249,7 @@ mod cmd {
20992249
21002250 fn write_all_impl(
21012251 cx: &mut compositor::Context,
2102− _args: &[&str],
2252+ _args: &[Cow<str>],
21032253 _event: PromptEvent,
21042254 quit: bool,
21052255 force: bool,
@@ -2135,7 +2285,7 @@ mod cmd {
21352285
21362286 fn write_all(
21372287 cx: &mut compositor::Context,
2138− args: &[&str],
2288+ args: &[Cow<str>],
21392289 event: PromptEvent,
21402290 ) -> anyhow::Result<()> {
21412291 write_all_impl(cx, args, event, false, false)
@@ -2143,7 +2293,7 @@ mod cmd {
21432293
21442294 fn write_all_quit(
21452295 cx: &mut compositor::Context,
2146− args: &[&str],
2296+ args: &[Cow<str>],
21472297 event: PromptEvent,
21482298 ) -> anyhow::Result<()> {
21492299 write_all_impl(cx, args, event, true, false)
@@ -2151,7 +2301,7 @@ mod cmd {
21512301
21522302 fn force_write_all_quit(
21532303 cx: &mut compositor::Context,
2154− args: &[&str],
2304+ args: &[Cow<str>],
21552305 event: PromptEvent,
21562306 ) -> anyhow::Result<()> {
21572307 write_all_impl(cx, args, event, true, true)
@@ -2159,7 +2309,7 @@ mod cmd {
21592309
21602310 fn quit_all_impl(
21612311 editor: &mut Editor,
2162− _args: &[&str],
2312+ _args: &[Cow<str>],
21632313 _event: PromptEvent,
21642314 force: bool,
21652315 ) -> anyhow::Result<()> {
@@ -2178,23 +2328,23 @@ mod cmd {
21782328
21792329 fn quit_all(
21802330 cx: &mut compositor::Context,
2181− args: &[&str],
2331+ args: &[Cow<str>],
21822332 event: PromptEvent,
21832333 ) -> anyhow::Result<()> {
2184− quit_all_impl(&mut cx.editor, args, event, false)
2334+ quit_all_impl(cx.editor, args, event, false)
21852335 }
21862336
21872337 fn force_quit_all(
21882338 cx: &mut compositor::Context,
2189− args: &[&str],
2339+ args: &[Cow<str>],
21902340 event: PromptEvent,
21912341 ) -> anyhow::Result<()> {
2192− quit_all_impl(&mut cx.editor, args, event, true)
2342+ quit_all_impl(cx.editor, args, event, true)
21932343 }
21942344
21952345 fn cquit(
21962346 cx: &mut compositor::Context,
2197− args: &[&str],
2347+ args: &[Cow<str>],
21982348 _event: PromptEvent,
21992349 ) -> anyhow::Result<()> {
22002350 let exit_code = args
@@ -2213,85 +2363,91 @@ mod cmd {
22132363
22142364 fn theme(
22152365 cx: &mut compositor::Context,
2216− args: &[&str],
2366+ args: &[Cow<str>],
22172367 _event: PromptEvent,
22182368 ) -> anyhow::Result<()> {
2219− let theme = args.first().context("theme not provided")?;
2220− cx.editor.set_theme_from_name(theme)
2369+ let theme = args.first().context("Theme not provided")?;
2370+ let theme = cx
2371+ .editor
2372+ .theme_loader
2373+ .load(theme)
2374+ .with_context(|| format!("Failed setting theme {}", theme))?;
2375+ let true_color = cx.editor.config.true_color || crate::true_color();
2376+ if !(true_color || theme.is_16_color()) {
2377+ bail!("Unsupported theme: theme requires true color support");
2378+ }
2379+ cx.editor.set_theme(theme);
2380+ Ok(())
22212381 }
22222382
22232383 fn yank_main_selection_to_clipboard(
22242384 cx: &mut compositor::Context,
2225− _args: &[&str],
2385+ _args: &[Cow<str>],
22262386 _event: PromptEvent,
22272387 ) -> anyhow::Result<()> {
2228− yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
2388+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
22292389 }
22302390
22312391 fn yank_joined_to_clipboard(
22322392 cx: &mut compositor::Context,
2233− args: &[&str],
2393+ args: &[Cow<str>],
22342394 _event: PromptEvent,
22352395 ) -> anyhow::Result<()> {
22362396 let (_, doc) = current!(cx.editor);
2237− let separator = args
2238− .first()
2239− .copied()
2240− .unwrap_or_else(|| doc.line_ending.as_str());
2241− yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
2397+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
2398+ let separator = args.first().unwrap_or(&default_sep);
2399+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
22422400 }
22432401
22442402 fn yank_main_selection_to_primary_clipboard(
22452403 cx: &mut compositor::Context,
2246− _args: &[&str],
2404+ _args: &[Cow<str>],
22472405 _event: PromptEvent,
22482406 ) -> anyhow::Result<()> {
2249− yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
2407+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
22502408 }
22512409
22522410 fn yank_joined_to_primary_clipboard(
22532411 cx: &mut compositor::Context,
2254− args: &[&str],
2412+ args: &[Cow<str>],
22552413 _event: PromptEvent,
22562414 ) -> anyhow::Result<()> {
22572415 let (_, doc) = current!(cx.editor);
2258− let separator = args
2259− .first()
2260− .copied()
2261− .unwrap_or_else(|| doc.line_ending.as_str());
2262− yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
2416+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
2417+ let separator = args.first().unwrap_or(&default_sep);
2418+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
22632419 }
22642420
22652421 fn paste_clipboard_after(
22662422 cx: &mut compositor::Context,
2267− _args: &[&str],
2423+ _args: &[Cow<str>],
22682424 _event: PromptEvent,
22692425 ) -> anyhow::Result<()> {
2270− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
2426+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
22712427 }
22722428
22732429 fn paste_clipboard_before(
22742430 cx: &mut compositor::Context,
2275− _args: &[&str],
2431+ _args: &[Cow<str>],
22762432 _event: PromptEvent,
22772433 ) -> anyhow::Result<()> {
2278− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
2434+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
22792435 }
22802436
22812437 fn paste_primary_clipboard_after(
22822438 cx: &mut compositor::Context,
2283− _args: &[&str],
2439+ _args: &[Cow<str>],
22842440 _event: PromptEvent,
22852441 ) -> anyhow::Result<()> {
2286− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
2442+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
22872443 }
22882444
22892445 fn paste_primary_clipboard_before(
22902446 cx: &mut compositor::Context,
2291− _args: &[&str],
2447+ _args: &[Cow<str>],
22922448 _event: PromptEvent,
22932449 ) -> anyhow::Result<()> {
2294− paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
2450+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
22952451 }
22962452
22972453 fn replace_selections_with_clipboard_impl(
@@ -2318,7 +2474,7 @@ mod cmd {
23182474
23192475 fn replace_selections_with_clipboard(
23202476 cx: &mut compositor::Context,
2321− _args: &[&str],
2477+ _args: &[Cow<str>],
23222478 _event: PromptEvent,
23232479 ) -> anyhow::Result<()> {
23242480 replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@@ -2326,7 +2482,7 @@ mod cmd {
23262482
23272483 fn replace_selections_with_primary_clipboard(
23282484 cx: &mut compositor::Context,
2329− _args: &[&str],
2485+ _args: &[Cow<str>],
23302486 _event: PromptEvent,
23312487 ) -> anyhow::Result<()> {
23322488 replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@@ -2334,7 +2490,7 @@ mod cmd {
23342490
23352491 fn show_clipboard_provider(
23362492 cx: &mut compositor::Context,
2337− _args: &[&str],
2493+ _args: &[Cow<str>],
23382494 _event: PromptEvent,
23392495 ) -> anyhow::Result<()> {
23402496 cx.editor
@@ -2344,12 +2500,13 @@ mod cmd {
23442500
23452501 fn change_current_directory(
23462502 cx: &mut compositor::Context,
2347− args: &[&str],
2503+ args: &[Cow<str>],
23482504 _event: PromptEvent,
23492505 ) -> anyhow::Result<()> {
23502506 let dir = helix_core::path::expand_tilde(
23512507 args.first()
23522508 .context("target directory not provided")?
2509+ .as_ref()
23532510 .as_ref(),
23542511 );
23552512
@@ -2367,7 +2524,7 @@ mod cmd {
23672524
23682525 fn show_current_directory(
23692526 cx: &mut compositor::Context,
2370− _args: &[&str],
2527+ _args: &[Cow<str>],
23712528 _event: PromptEvent,
23722529 ) -> anyhow::Result<()> {
23732530 let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@@ -2379,7 +2536,7 @@ mod cmd {
23792536 /// Sets the [`Document`]'s encoding..
23802537 fn set_encoding(
23812538 cx: &mut compositor::Context,
2382− args: &[&str],
2539+ args: &[Cow<str>],
23832540 _event: PromptEvent,
23842541 ) -> anyhow::Result<()> {
23852542 let (_, doc) = current!(cx.editor);
@@ -2395,7 +2552,7 @@ mod cmd {
23952552 /// Reload the [`Document`] from its source file.
23962553 fn reload(
23972554 cx: &mut compositor::Context,
2398− _args: &[&str],
2555+ _args: &[Cow<str>],
23992556 _event: PromptEvent,
24002557 ) -> anyhow::Result<()> {
24012558 let (view, doc) = current!(cx.editor);
@@ -2404,7 +2561,7 @@ mod cmd {
24042561
24052562 fn tree_sitter_scopes(
24062563 cx: &mut compositor::Context,
2407− _args: &[&str],
2564+ _args: &[Cow<str>],
24082565 _event: PromptEvent,
24092566 ) -> anyhow::Result<()> {
24102567 let (view, doc) = current!(cx.editor);
@@ -2418,15 +2575,18 @@ mod cmd {
24182575
24192576 fn vsplit(
24202577 cx: &mut compositor::Context,
2421− args: &[&str],
2578+ args: &[Cow<str>],
24222579 _event: PromptEvent,
24232580 ) -> anyhow::Result<()> {
24242581 let id = view!(cx.editor).doc;
24252582
2426− if let Some(path) = args.get(0) {
2427− cx.editor.open(path.into(), Action::VerticalSplit)?;
2428− } else {
2583+ if args.is_empty() {
24292584 cx.editor.switch(id, Action::VerticalSplit);
2585+ } else {
2586+ for arg in args {
2587+ cx.editor
2588+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
2589+ }
24302590 }
24312591
24322592 Ok(())
@@ -2434,15 +2594,18 @@ mod cmd {
24342594
24352595 fn hsplit(
24362596 cx: &mut compositor::Context,
2437− args: &[&str],
2597+ args: &[Cow<str>],
24382598 _event: PromptEvent,
24392599 ) -> anyhow::Result<()> {
24402600 let id = view!(cx.editor).doc;
24412601
2442− if let Some(path) = args.get(0) {
2443− cx.editor.open(path.into(), Action::HorizontalSplit)?;
2444− } else {
2602+ if args.is_empty() {
24452603 cx.editor.switch(id, Action::HorizontalSplit);
2604+ } else {
2605+ for arg in args {
2606+ cx.editor
2607+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
2608+ }
24462609 }
24472610
24482611 Ok(())
@@ -2450,7 +2613,7 @@ mod cmd {
24502613
24512614 fn tutor(
24522615 cx: &mut compositor::Context,
2453− _args: &[&str],
2616+ _args: &[Cow<str>],
24542617 _event: PromptEvent,
24552618 ) -> anyhow::Result<()> {
24562619 let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2460,6 +2623,24 @@ mod cmd {
24602623 Ok(())
24612624 }
24622625
2626+ pub(super) fn goto_line_number(
2627+ cx: &mut compositor::Context,
2628+ args: &[Cow<str>],
2629+ _event: PromptEvent,
2630+ ) -> anyhow::Result<()> {
2631+ ensure!(!args.is_empty(), "Line number required");
2632+
2633+ let line = args[0].parse::<usize>()?;
2634+
2635+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
2636+
2637+ let (view, doc) = current!(cx.editor);
2638+
2639+ view.ensure_cursor_in_view(doc, line);
2640+
2641+ Ok(())
2642+ }
2643+
24632644 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
24642645 TypableCommand {
24652646 name: "quit",
@@ -2513,7 +2694,7 @@ mod cmd {
25132694 TypableCommand {
25142695 name: "format",
25152696 aliases: &["fmt"],
2516− doc: "Format the file using a formatter.",
2697+ doc: "Format the file using the LSP formatter.",
25172698 fun: format,
25182699 completer: None,
25192700 },
@@ -2604,7 +2785,7 @@ mod cmd {
26042785 TypableCommand {
26052786 name: "theme",
26062787 aliases: &[],
2607− doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
2788+ doc: "Change the editor theme.",
26082789 fun: theme,
26092790 completer: Some(completers::theme),
26102791 },
@@ -2688,7 +2869,7 @@ mod cmd {
26882869 TypableCommand {
26892870 name: "change-current-directory",
26902871 aliases: &["cd"],
2691− doc: "Change the current working directory (:cd <dir>).",
2872+ doc: "Change the current working directory.",
26922873 fun: change_current_directory,
26932874 completer: Some(completers::directory),
26942875 },
@@ -2741,17 +2922,25 @@ mod cmd {
27412922 fun: tutor,
27422923 completer: None,
27432924 },
2925+ TypableCommand {
2926+ name: "goto",
2927+ aliases: &["g"],
2928+ doc: "Go to line number.",
2929+ fun: goto_line_number,
2930+ completer: None,
2931+ }
27442932 ];
27452933
2746− pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2747− TYPABLE_COMMAND_LIST
2748− .iter()
2749− .flat_map(|cmd| {
2750− std::iter::once((cmd.name, cmd))
2751− .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2752− })
2753− .collect()
2754− });
2934+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
2935+ Lazy::new(|| {
2936+ TYPABLE_COMMAND_LIST
2937+ .iter()
2938+ .flat_map(|cmd| {
2939+ std::iter::once((cmd.name, cmd))
2940+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2941+ })
2942+ .collect()
2943+ });
27552944}
27562945
27572946fn command_mode(cx: &mut Context) {
@@ -2777,7 +2966,7 @@ fn command_mode(cx: &mut Context) {
27772966 if let Some(cmd::TypableCommand {
27782967 completer: Some(completer),
27792968 ..
2780− }) = cmd::COMMANDS.get(parts[0])
2969+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
27812970 {
27822971 completer(part)
27832972 .into_iter()
@@ -2803,8 +2992,18 @@ fn command_mode(cx: &mut Context) {
28032992 return;
28042993 }
28052994
2806− if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2807− if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2995+ // If command is numeric, interpret as line number and go there.
2996+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
2997+ if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
2998+ cx.editor.set_error(format!("{}", e));
2999+ }
3000+ return;
3001+ }
3002+
3003+ // Handle typable commands
3004+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
3005+ let args = shellwords::shellwords(input);
3006+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
28083007 cx.editor.set_error(format!("{}", e));
28093008 }
28103009 } else {
@@ -2816,7 +3015,7 @@ fn command_mode(cx: &mut Context) {
28163015 prompt.doc_fn = Box::new(|input: &str| {
28173016 let part = input.split(' ').next().unwrap_or_default();
28183017
2819− if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
3018+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
28203019 return Some(doc);
28213020 }
28223021
@@ -3254,7 +3453,7 @@ fn apply_workspace_edit(
32543453
32553454fn last_picker(cx: &mut Context) {
32563455 // TODO: last picker does not seem to work well with buffer_picker
3257− cx.callback = Some(Box::new(|compositor: &mut Compositor| {
3456+ cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
32583457 if let Some(picker) = compositor.last_picker.take() {
32593458 compositor.push(picker);
32603459 }
@@ -3436,10 +3635,14 @@ fn push_jump(editor: &mut Editor) {
34363635}
34373636
34383637fn goto_line(cx: &mut Context) {
3439− if let Some(count) = cx.count {
3440− push_jump(cx.editor);
3638+ goto_line_impl(cx.editor, cx.count)
3639+}
34413640
3442− let (view, doc) = current!(cx.editor);
3641+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
3642+ if let Some(count) = count {
3643+ push_jump(editor);
3644+
3645+ let (view, doc) = current!(editor);
34433646 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
34443647 // If the last line is blank, don't jump to it.
34453648 doc.text().len_lines().saturating_sub(2)
@@ -3498,6 +3701,20 @@ fn goto_last_modification(cx: &mut Context) {
34983701 }
34993702}
35003703
3704+fn goto_last_modified_file(cx: &mut Context) {
3705+ let view = view!(cx.editor);
3706+ let alternate_file = view
3707+ .last_modified_docs
3708+ .into_iter()
3709+ .flatten()
3710+ .find(|&id| id != view.doc);
3711+ if let Some(alt) = alternate_file {
3712+ cx.editor.switch(alt, Action::Replace);
3713+ } else {
3714+ cx.editor.set_error("no last modified buffer".to_owned())
3715+ }
3716+}
3717+
35013718fn select_mode(cx: &mut Context) {
35023719 let (view, doc) = current!(cx.editor);
35033720 let text = doc.text().slice(..);
@@ -3982,8 +4199,9 @@ pub mod insert {
39824199 // The default insert hook: simply insert the character
39834200 #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
39844201 fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
4202+ let cursors = selection.clone().cursors(doc.slice(..));
39854203 let t = Tendril::from_char(ch);
3986− let transaction = Transaction::insert(doc, selection, t);
4204+ let transaction = Transaction::insert(doc, &cursors, t);
39874205 Some(transaction)
39884206 }
39894207
@@ -3998,11 +4216,11 @@ pub mod insert {
39984216 };
39994217
40004218 let text = doc.text();
4001− let selection = doc.selection(view.id).clone().cursors(text.slice(..));
4219+ let selection = doc.selection(view.id);
40024220
40034221 // run through insert hooks, stopping on the first one that returns Some(t)
40044222 for hook in hooks {
4005− if let Some(transaction) = hook(text, &selection, c) {
4223+ if let Some(transaction) = hook(text, selection, c) {
40064224 doc.apply(&transaction, view.id);
40074225 break;
40084226 }
@@ -4317,11 +4535,8 @@ fn yank_joined_to_clipboard_impl(
43174535
43184536fn yank_joined_to_clipboard(cx: &mut Context) {
43194537 let line_ending = doc!(cx.editor).line_ending;
4320− let _ = yank_joined_to_clipboard_impl(
4321− &mut cx.editor,
4322− line_ending.as_str(),
4323− ClipboardType::Clipboard,
4324− );
4538+ let _ =
4539+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
43254540 exit_select_mode(cx);
43264541}
43274542
@@ -4346,20 +4561,17 @@ fn yank_main_selection_to_clipboard_impl(
43464561}
43474562
43484563fn yank_main_selection_to_clipboard(cx: &mut Context) {
4349− let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
4564+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
43504565}
43514566
43524567fn yank_joined_to_primary_clipboard(cx: &mut Context) {
43534568 let line_ending = doc!(cx.editor).line_ending;
4354− let _ = yank_joined_to_clipboard_impl(
4355− &mut cx.editor,
4356− line_ending.as_str(),
4357− ClipboardType::Selection,
4358− );
4569+ let _ =
4570+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
43594571}
43604572
43614573fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
4362− let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
4574+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
43634575 exit_select_mode(cx);
43644576}
43654577
@@ -4374,11 +4586,12 @@ fn paste_impl(
43744586 doc: &mut Document,
43754587 view: &View,
43764588 action: Paste,
4589+ count: usize,
43774590) -> Option<Transaction> {
43784591 let repeat = std::iter::repeat(
43794592 values
43804593 .last()
4381− .map(|value| Tendril::from_slice(value))
4594+ .map(|value| Tendril::from(value.repeat(count)))
43824595 .unwrap(),
43834596 );
43844597
@@ -4393,7 +4606,7 @@ fn paste_impl(
43934606 let mut values = values
43944607 .iter()
43954608 .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
4396− .map(|value| Tendril::from(value.as_ref()))
4609+ .map(|value| Tendril::from(value.as_ref().repeat(count)))
43974610 .chain(repeat);
43984611
43994612 let text = doc.text();
@@ -4413,7 +4626,7 @@ fn paste_impl(
44134626 // paste append
44144627 (Paste::After, false) => range.to(),
44154628 };
4416− (pos, pos, Some(values.next().unwrap()))
4629+ (pos, pos, values.next())
44174630 });
44184631
44194632 Some(transaction)
@@ -4423,13 +4636,14 @@ fn paste_clipboard_impl(
44234636 editor: &mut Editor,
44244637 action: Paste,
44254638 clipboard_type: ClipboardType,
4639+ count: usize,
44264640) -> anyhow::Result<()> {
44274641 let (view, doc) = current!(editor);
44284642
44294643 match editor
44304644 .clipboard_provider
44314645 .get_contents(clipboard_type)
4432− .map(|contents| paste_impl(&[contents], doc, view, action))
4646+ .map(|contents| paste_impl(&[contents], doc, view, action, count))
44334647 {
44344648 Ok(Some(transaction)) => {
44354649 doc.apply(&transaction, view.id);
@@ -4442,22 +4656,43 @@ fn paste_clipboard_impl(
44424656}
44434657
44444658fn paste_clipboard_after(cx: &mut Context) {
4445− let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
4659+ let _ = paste_clipboard_impl(
4660+ cx.editor,
4661+ Paste::After,
4662+ ClipboardType::Clipboard,
4663+ cx.count(),
4664+ );
44464665}
44474666
44484667fn paste_clipboard_before(cx: &mut Context) {
4449− let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
4668+ let _ = paste_clipboard_impl(
4669+ cx.editor,
4670+ Paste::Before,
4671+ ClipboardType::Clipboard,
4672+ cx.count(),
4673+ );
44504674}
44514675
44524676fn paste_primary_clipboard_after(cx: &mut Context) {
4453− let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
4677+ let _ = paste_clipboard_impl(
4678+ cx.editor,
4679+ Paste::After,
4680+ ClipboardType::Selection,
4681+ cx.count(),
4682+ );
44544683}
44554684
44564685fn paste_primary_clipboard_before(cx: &mut Context) {
4457− let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
4686+ let _ = paste_clipboard_impl(
4687+ cx.editor,
4688+ Paste::Before,
4689+ ClipboardType::Selection,
4690+ cx.count(),
4691+ );
44584692}
44594693
44604694fn replace_with_yanked(cx: &mut Context) {
4695+ let count = cx.count();
44614696 let reg_name = cx.register.unwrap_or('"');
44624697 let (view, doc) = current!(cx.editor);
44634698 let registers = &mut cx.editor.registers;
@@ -4467,12 +4702,12 @@ fn replace_with_yanked(cx: &mut Context) {
44674702 let repeat = std::iter::repeat(
44684703 values
44694704 .last()
4470− .map(|value| Tendril::from_slice(value))
4705+ .map(|value| Tendril::from_slice(&value.repeat(count)))
44714706 .unwrap(),
44724707 );
44734708 let mut values = values
44744709 .iter()
4475− .map(|value| Tendril::from_slice(value))
4710+ .map(|value| Tendril::from_slice(&value.repeat(count)))
44764711 .chain(repeat);
44774712 let selection = doc.selection(view.id);
44784713 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4492,6 +4727,7 @@ fn replace_with_yanked(cx: &mut Context) {
44924727fn replace_selections_with_clipboard_impl(
44934728 editor: &mut Editor,
44944729 clipboard_type: ClipboardType,
4730+ count: usize,
44954731) -> anyhow::Result<()> {
44964732 let (view, doc) = current!(editor);
44974733
@@ -4499,7 +4735,11 @@ fn replace_selections_with_clipboard_impl(
44994735 Ok(contents) => {
45004736 let selection = doc.selection(view.id);
45014737 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4502− (range.from(), range.to(), Some(contents.as_str().into()))
4738+ (
4739+ range.from(),
4740+ range.to(),
4741+ Some(contents.repeat(count).as_str().into()),
4742+ )
45034743 });
45044744
45054745 doc.apply(&transaction, view.id);
@@ -4511,21 +4751,22 @@ fn replace_selections_with_clipboard_impl(
45114751}
45124752
45134753fn replace_selections_with_clipboard(cx: &mut Context) {
4514− let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
4754+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
45154755}
45164756
45174757fn replace_selections_with_primary_clipboard(cx: &mut Context) {
4518− let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
4758+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
45194759}
45204760
45214761fn paste_after(cx: &mut Context) {
4762+ let count = cx.count();
45224763 let reg_name = cx.register.unwrap_or('"');
45234764 let (view, doc) = current!(cx.editor);
45244765 let registers = &mut cx.editor.registers;
45254766
45264767 if let Some(transaction) = registers
45274768 .read(reg_name)
4528− .and_then(|values| paste_impl(values, doc, view, Paste::After))
4769+ .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
45294770 {
45304771 doc.apply(&transaction, view.id);
45314772 doc.append_changes_to_history(view.id);
@@ -4533,13 +4774,14 @@ fn paste_after(cx: &mut Context) {
45334774}
45344775
45354776fn paste_before(cx: &mut Context) {
4777+ let count = cx.count();
45364778 let reg_name = cx.register.unwrap_or('"');
45374779 let (view, doc) = current!(cx.editor);
45384780 let registers = &mut cx.editor.registers;
45394781
45404782 if let Some(transaction) = registers
45414783 .read(reg_name)
4542− .and_then(|values| paste_impl(values, doc, view, Paste::Before))
4784+ .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
45434785 {
45444786 doc.apply(&transaction, view.id);
45454787 doc.append_changes_to_history(view.id);
@@ -4935,8 +5177,12 @@ fn hover(cx: &mut Context) {
49355177 // skip if contents empty
49365178
49375179 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
4938− let popup = Popup::new(contents);
4939− compositor.push(Box::new(popup));
5180+ let popup = Popup::new("documentation", contents);
5181+ if let Some(doc_popup) = compositor.find_id("documentation") {
5182+ *doc_popup = popup;
5183+ } else {
5184+ compositor.push(Box::new(popup));
5185+ }
49405186 }
49415187 },
49425188 );
@@ -5030,7 +5276,7 @@ fn expand_selection(cx: &mut Context) {
50305276 doc.set_selection(view.id, selection);
50315277 }
50325278 };
5033− motion(&mut cx.editor);
5279+ motion(cx.editor);
50345280 cx.editor.last_motion = Some(Motion(Box::new(motion)));
50355281}
50365282
@@ -5086,6 +5332,12 @@ fn jump_backward(cx: &mut Context) {
50865332 };
50875333}
50885334
5335+fn save_selection(cx: &mut Context) {
5336+ push_jump(cx.editor);
5337+ cx.editor
5338+ .set_status("Selection saved to jumplist".to_owned());
5339+}
5340+
50895341fn rotate_view(cx: &mut Context) {
50905342 cx.editor.focus_next()
50915343}
@@ -5262,7 +5514,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
52625514 });
52635515 doc.set_selection(view.id, selection);
52645516 };
5265− textobject(&mut cx.editor);
5517+ textobject(cx.editor);
52665518 cx.editor.last_motion = Some(Motion(Box::new(textobject)));
52675519 }
52685520 })
@@ -5428,9 +5680,7 @@ fn shell_impl(
54285680) -> anyhow::Result<(Tendril, bool)> {
54295681 use std::io::Write;
54305682 use std::process::{Command, Stdio};
5431− if shell.is_empty() {
5432− bail!("No shell set");
5433− }
5683+ ensure!(!shell.is_empty(), "No shell set");
54345684
54355685 let mut process = match Command::new(&shell[0])
54365686 .args(&shell[1..])
@@ -5594,7 +5844,7 @@ fn rename_symbol(cx: &mut Context) {
55945844 let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
55955845 let edits = block_on(task).unwrap_or_default();
55965846 log::debug!("Edits from LSP: {:?}", edits);
5597− apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
5847+ apply_workspace_edit(cx.editor, offset_encoding, &edits);
55985848 },
55995849 );
56005850 cx.push_layer(Box::new(prompt));
@@ -5614,16 +5864,45 @@ fn decrement(cx: &mut Context) {
56145864fn increment_impl(cx: &mut Context, amount: i64) {
56155865 let (view, doc) = current!(cx.editor);
56165866 let selection = doc.selection(view.id);
5617− let text = doc.text();
5867+ let text = doc.text().slice(..);
5868+
5869+ let changes: Vec<_> = selection
5870+ .ranges()
5871+ .iter()
5872+ .filter_map(|range| {
5873+ let incrementor: Box<dyn Increment> =
5874+ if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) {
5875+ Box::new(incrementor)
5876+ } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) {
5877+ Box::new(incrementor)
5878+ } else {
5879+ return None;
5880+ };
5881+
5882+ let (range, new_text) = incrementor.increment(amount);
5883+
5884+ Some((range.from(), range.to(), Some(new_text)))
5885+ })
5886+ .collect();
56185887
5619− let changes = selection.ranges().iter().filter_map(|range| {
5620− let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
5621− let new_text = incrementor.incremented_text(amount);
5622− Some((
5623− incrementor.range.from(),
5624− incrementor.range.to(),
5625− Some(new_text),
5626− ))
5888+ // Overlapping changes in a transaction will panic, so we need to find and remove them.
5889+ // For example, if there are cursors on each of the year, month, and day of `2021-11-29`,
5890+ // incrementing will give overlapping changes, with each change incrementing a different part of
5891+ // the date. Since these conflict with each other we remove these changes from the transaction
5892+ // so nothing happens.
5893+ let mut overlapping_indexes = HashSet::new();
5894+ for (i, changes) in changes.windows(2).enumerate() {
5895+ if changes[0].1 > changes[1].0 {
5896+ overlapping_indexes.insert(i);
5897+ overlapping_indexes.insert(i + 1);
5898+ }
5899+ }
5900+ let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
5901+ if overlapping_indexes.contains(&i) {
5902+ None
5903+ } else {
5904+ Some(change)
5905+ }
56275906 });
56285907
56295908 if changes.clone().count() > 0 {
@@ -5634,3 +5913,56 @@ fn increment_impl(cx: &mut Context, amount: i64) {
56345913 doc.append_changes_to_history(view.id);
56355914 }
56365915}
5916+
5917+fn record_macro(cx: &mut Context) {
5918+ if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
5919+ // Remove the keypress which ends the recording
5920+ keys.pop();
5921+ let s = keys
5922+ .into_iter()
5923+ .map(|key| format!("{}", key))
5924+ .collect::<Vec<_>>()
5925+ .join(" ");
5926+ cx.editor.registers.get_mut(reg).write(vec![s]);
5927+ cx.editor
5928+ .set_status(format!("Recorded to register {}", reg));
5929+ } else {
5930+ let reg = cx.register.take().unwrap_or('@');
5931+ cx.editor.macro_recording = Some((reg, Vec::new()));
5932+ cx.editor
5933+ .set_status(format!("Recording to register {}", reg));
5934+ }
5935+}
5936+
5937+fn play_macro(cx: &mut Context) {
5938+ let reg = cx.register.unwrap_or('@');
5939+ let keys = match cx
5940+ .editor
5941+ .registers
5942+ .get(reg)
5943+ .and_then(|reg| reg.read().get(0))
5944+ .context("Register empty")
5945+ .and_then(|s| {
5946+ s.split_whitespace()
5947+ .map(str::parse::<KeyEvent>)
5948+ .collect::<Result<Vec<_>, _>>()
5949+ .context("Failed to parse macro")
5950+ }) {
5951+ Ok(keys) => keys,
5952+ Err(e) => {
5953+ cx.editor.set_error(format!("{}", e));
5954+ return;
5955+ }
5956+ };
5957+ let count = cx.count();
5958+
5959+ cx.callback = Some(Box::new(
5960+ move |compositor: &mut Compositor, cx: &mut compositor::Context| {
5961+ for _ in 0..count {
5962+ for &key in keys.iter() {
5963+ compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
5964+ }
5965+ }
5966+ },
5967+ ));
5968+}