@@ -16,8 +16,13 @@ use helix_core::{
1616};
1717
1818use helix_view::{
19− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
20− view::View, Document, DocumentId, Editor, ViewId,
19+ clipboard::ClipboardType,
20+ document::Mode,
21+ editor::{Action, Motion},
22+ input::KeyEvent,
23+ keyboard::KeyCode,
24+ view::View,
25+ Document, DocumentId, Editor, ViewId,
2126};
2227
2328use anyhow::{anyhow, bail, Context as _};
@@ -202,6 +207,7 @@ impl Command {
202207 find_prev_char, "Move to previous occurance of char",
203208 extend_till_prev_char, "Extend till previous occurance of char",
204209 extend_prev_char, "Extend to previous occurance of char",
210+ repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)",
205211 replace, "Replace with new char",
206212 switch_case, "Switch (toggle) case",
207213 switch_to_uppercase, "Switch to uppercase",
@@ -215,8 +221,11 @@ impl Command {
215221 split_selection, "Split selection into subselections on regex matches",
216222 split_selection_on_newline, "Split selection on newlines",
217223 search, "Search for regex pattern",
224+ rsearch, "Reverse search for regex pattern",
218225 search_next, "Select next search match",
226+ search_prev, "Select previous search match",
219227 extend_search_next, "Add next search match to selection",
228+ extend_search_prev, "Add previous search match to selection",
220229 search_selection, "Use current selection as search pattern",
221230 global_search, "Global Search in workspace folder",
222231 extend_line, "Select current line, if already selected, extend to next line",
@@ -260,6 +269,8 @@ impl Command {
260269 goto_prev_diag, "Goto previous diagnostic",
261270 goto_line_start, "Goto line start",
262271 goto_line_end, "Goto line end",
272+ goto_next_buffer, "Goto next buffer",
273+ goto_previous_buffer, "Goto previous buffer",
263274 // TODO: different description ?
264275 goto_line_end_newline, "Goto line end",
265276 goto_first_nonwhitespace, "Goto first non-blank in line",
@@ -305,6 +316,10 @@ impl Command {
305316 expand_selection, "Expand selection to parent syntax node",
306317 jump_forward, "Jump forward on jumplist",
307318 jump_backward, "Jump backward on jumplist",
319+ jump_view_right, "Jump to the split to the right",
320+ jump_view_left, "Jump to the split to the left",
321+ jump_view_up, "Jump to the split above",
322+ jump_view_down, "Jump to the split below",
308323 rotate_view, "Goto next window",
309324 hsplit, "Horizontal bottom split",
310325 vsplit, "Vertical right split",
@@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) {
528543 )
529544}
530545
546+fn goto_next_buffer(cx: &mut Context) {
547+ goto_buffer(cx, Direction::Forward);
548+}
549+
550+fn goto_previous_buffer(cx: &mut Context) {
551+ goto_buffer(cx, Direction::Backward);
552+}
553+
554+fn goto_buffer(cx: &mut Context, direction: Direction) {
555+ let current = view!(cx.editor).doc;
556+
557+ let id = match direction {
558+ Direction::Forward => {
559+ let iter = cx.editor.documents.keys();
560+ let mut iter = iter.skip_while(|id| *id != ¤t);
561+ iter.next(); // skip current item
562+ iter.next().or_else(|| cx.editor.documents.keys().next())
563+ }
564+ Direction::Backward => {
565+ let iter = cx.editor.documents.keys();
566+ let mut iter = iter.rev().skip_while(|id| *id != ¤t);
567+ iter.next(); // skip current item
568+ iter.next()
569+ .or_else(|| cx.editor.documents.keys().rev().next())
570+ }
571+ }
572+ .unwrap();
573+
574+ let id = *id;
575+
576+ cx.editor.switch(id, Action::Replace);
577+}
578+
531579fn extend_to_line_start(cx: &mut Context) {
532580 let (view, doc) = current!(cx.editor);
533581 goto_line_start_impl(view, doc, Movement::Extend)
@@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) {
631679 } else {
632680 push_jump(cx.editor);
633681 let (view, doc) = current!(cx.editor);
634− doc.set_selection(view.id, Selection::point(0));
682+ let text = doc.text().slice(..);
683+ let selection = doc
684+ .selection(view.id)
685+ .clone()
686+ .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select));
687+ doc.set_selection(view.id, selection);
635688 }
636689}
637690
638691fn goto_file_end(cx: &mut Context) {
639692 push_jump(cx.editor);
640693 let (view, doc) = current!(cx.editor);
641− doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
694+ let text = doc.text().slice(..);
695+ let pos = doc.text().len_chars();
696+ let selection = doc
697+ .selection(view.id)
698+ .clone()
699+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
700+ doc.set_selection(view.id, selection);
642701}
643702
644703fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
@@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
681740 extend_word_impl(cx, movement::move_next_long_word_end)
682741}
683742
684−#[inline]
685−fn find_char_impl<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
743+fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
686744where
687745 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
688746{
@@ -704,13 +762,7 @@ where
704762 // usually mix line endings. But we should fix it eventually
705763 // anyway.
706764 {
707− current!(cx.editor)
708− .1
709− .line_ending
710− .as_str()
711− .chars()
712− .next()
713− .unwrap()
765+ doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
714766 }
715767
716768 KeyEvent {
@@ -720,29 +772,48 @@ where
720772 _ => return,
721773 };
722774
723− let (view, doc) = current!(cx.editor);
724− let text = doc.text().slice(..);
775+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
776+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
777+ find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
778+ })));
779+ })
780+}
725781
726− let selection = doc.selection(view.id).clone().transform(|range| {
727− // TODO: use `Range::cursor()` here instead. However, that works in terms of
728− // graphemes, whereas this function doesn't yet. So we're doing the same logic
729− // here, but just in terms of chars instead.
730− let search_start_pos = if range.anchor < range.head {
731− range.head - 1
732− } else {
733− range.head
734− };
782+//
735783
736− search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
737− if extend {
738− range.put_cursor(text, pos, true)
739− } else {
740− Range::point(range.cursor(text)).put_cursor(text, pos, true)
741− }
742− })
743− });
744− doc.set_selection(view.id, selection);
745− })
784+#[inline]
785+fn find_char_impl<F>(
786+ editor: &mut Editor,
787+ search_fn: &F,
788+ inclusive: bool,
789+ extend: bool,
790+ ch: char,
791+ count: usize,
792+) where
793+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
794+{
795+ let (view, doc) = current!(editor);
796+ let text = doc.text().slice(..);
797+
798+ let selection = doc.selection(view.id).clone().transform(|range| {
799+ // TODO: use `Range::cursor()` here instead. However, that works in terms of
800+ // graphemes, whereas this function doesn't yet. So we're doing the same logic
801+ // here, but just in terms of chars instead.
802+ let search_start_pos = if range.anchor < range.head {
803+ range.head - 1
804+ } else {
805+ range.head
806+ };
807+
808+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
809+ if extend {
810+ range.put_cursor(text, pos, true)
811+ } else {
812+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
813+ }
814+ })
815+ });
816+ doc.set_selection(view.id, selection);
746817}
747818
748819fn find_next_char_impl(
@@ -756,6 +827,10 @@ fn find_next_char_impl(
756827 if inclusive {
757828 search::find_nth_next(text, ch, pos, n)
758829 } else {
830+ let n = match text.get_char(pos) {
831+ Some(next_ch) if next_ch == ch => n + 1,
832+ _ => n,
833+ };
759834 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
760835 }
761836}
@@ -770,80 +845,52 @@ fn find_prev_char_impl(
770845 if inclusive {
771846 search::find_nth_prev(text, ch, pos, n)
772847 } else {
848+ let n = match text.get_char(pos.saturating_sub(1)) {
849+ Some(next_ch) if next_ch == ch => n + 1,
850+ _ => n,
851+ };
773852 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
774853 }
775854}
776855
777856fn find_till_char(cx: &mut Context) {
778− find_char_impl(
779− cx,
780− find_next_char_impl,
781− false, /* inclusive */
782− false, /* extend */
783− )
857+ will_find_char(cx, find_next_char_impl, false, false)
784858}
785859
786860fn find_next_char(cx: &mut Context) {
787− find_char_impl(
788− cx,
789− find_next_char_impl,
790− true, /* inclusive */
791− false, /* extend */
792− )
861+ will_find_char(cx, find_next_char_impl, true, false)
793862}
794863
795864fn extend_till_char(cx: &mut Context) {
796− find_char_impl(
797− cx,
798− find_next_char_impl,
799− false, /* inclusive */
800− true, /* extend */
801− )
865+ will_find_char(cx, find_next_char_impl, false, true)
802866}
803867
804868fn extend_next_char(cx: &mut Context) {
805− find_char_impl(
806− cx,
807− find_next_char_impl,
808− true, /* inclusive */
809− true, /* extend */
810− )
869+ will_find_char(cx, find_next_char_impl, true, true)
811870}
812871
813872fn till_prev_char(cx: &mut Context) {
814− find_char_impl(
815− cx,
816− find_prev_char_impl,
817− false, /* inclusive */
818− false, /* extend */
819− )
873+ will_find_char(cx, find_prev_char_impl, false, false)
820874}
821875
822876fn find_prev_char(cx: &mut Context) {
823− find_char_impl(
824− cx,
825− find_prev_char_impl,
826− true, /* inclusive */
827− false, /* extend */
828− )
877+ will_find_char(cx, find_prev_char_impl, true, false)
829878}
830879
831880fn extend_till_prev_char(cx: &mut Context) {
832− find_char_impl(
833− cx,
834− find_prev_char_impl,
835− false, /* inclusive */
836− true, /* extend */
837− )
881+ will_find_char(cx, find_prev_char_impl, false, true)
838882}
839883
840884fn extend_prev_char(cx: &mut Context) {
841− find_char_impl(
842− cx,
843− find_prev_char_impl,
844− true, /* inclusive */
845− true, /* extend */
846− )
885+ will_find_char(cx, find_prev_char_impl, true, true)
886+}
887+
888+fn repeat_last_motion(cx: &mut Context) {
889+ let last_motion = cx.editor.last_motion.take();
890+ if let Some(m) = &last_motion {
891+ m.run(cx.editor);
892+ cx.editor.last_motion = last_motion;
893+ }
847894}
848895
849896fn replace(cx: &mut Context) {
@@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) {
10911138 cx,
10921139 "select:".into(),
10931140 Some(reg),
1141+ |_input: &str| Vec::new(),
10941142 move |view, doc, regex, event| {
10951143 if event != PromptEvent::Update {
10961144 return;
@@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) {
11131161 cx,
11141162 "split:".into(),
11151163 Some(reg),
1164+ |_input: &str| Vec::new(),
11161165 move |view, doc, regex, event| {
11171166 if event != PromptEvent::Update {
11181167 return;
@@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) {
11371186 doc.set_selection(view.id, selection);
11381187}
11391188
1140−fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
1189+fn search_impl(
1190+ doc: &mut Document,
1191+ view: &mut View,
1192+ contents: &str,
1193+ regex: &Regex,
1194+ movement: Movement,
1195+ direction: Direction,
1196+) {
11411197 let text = doc.text().slice(..);
11421198 let selection = doc.selection(view.id);
11431199
1144− // Get the right side of the primary block cursor.
1145− let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1146− text,
1147− selection.primary().cursor(text),
1148− ));
1200+ // Get the right side of the primary block cursor for forward search, or the
1201+ //grapheme before the start of the selection for reverse search.
1202+ let start = match direction {
1203+ Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary(
1204+ text,
1205+ selection.primary().to(),
1206+ )),
1207+ Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary(
1208+ text,
1209+ selection.primary().from(),
1210+ )),
1211+ };
1212+
1213+ //A regex::Match returns byte-positions in the str. In the case where we
1214+ //do a reverse search and wraparound to the end, we don't need to search
1215+ //the text before the current cursor position for matches, but by slicing
1216+ //it out, we need to add it back to the position of the selection.
1217+ let mut offset = 0;
11491218
11501219 // use find_at to find the next match after the cursor, loop around the end
11511220 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1152− let mat = regex
1153− .find_at(contents, start)
1154− .or_else(|| regex.find(contents));
1221+ let mat = match direction {
1222+ Direction::Forward => regex
1223+ .find_at(contents, start)
1224+ .or_else(|| regex.find(contents)),
1225+ Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
1226+ offset = start;
1227+ regex.find_iter(&contents[start..]).last()
1228+ }),
1229+ };
11551230 // TODO: message on wraparound
11561231 if let Some(mat) = mat {
1157− let start = text.byte_to_char(mat.start());
1158− let end = text.byte_to_char(mat.end());
1232+ let start = text.byte_to_char(mat.start() + offset);
1233+ let end = text.byte_to_char(mat.end() + offset);
11591234
11601235 if end == 0 {
11611236 // skip empty matches that don't make sense
11621237 return;
11631238 }
11641239
1165− let selection = if extend {
1166− selection.clone().push(Range::new(start, end))
1240+ // Determine range direction based on the primary range
1241+ let primary = selection.primary();
1242+ let range = if primary.head < primary.anchor {
1243+ Range::new(end, start)
11671244 } else {
1168− Selection::single(start, end)
1245+ Range::new(start, end)
1246+ };
1247+
1248+ let selection = match movement {
1249+ Movement::Extend => selection.clone().push(range),
1250+ Movement::Move => selection.clone().replace(selection.primary_index(), range),
11691251 };
11701252
11711253 doc.set_selection(view.id, selection);
@@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
11731255 };
11741256}
11751257
1258+fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
1259+ let mut items = reg
1260+ .and_then(|reg| cx.editor.registers.get(reg))
1261+ .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect());
1262+ items.sort_unstable();
1263+ items.dedup();
1264+ items.into_iter().cloned().collect()
1265+}
1266+
11761267// TODO: use one function for search vs extend
11771268fn search(cx: &mut Context) {
1269+ searcher(cx, Direction::Forward)
1270+}
1271+
1272+fn rsearch(cx: &mut Context) {
1273+ searcher(cx, Direction::Backward)
1274+}
1275+// TODO: use one function for search vs extend
1276+fn searcher(cx: &mut Context, direction: Direction) {
11781277 let reg = cx.register.unwrap_or('/');
11791278 let (_, doc) = current!(cx.editor);
11801279
@@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) {
11831282 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
11841283 // feed chunks into the regex yet
11851284 let contents = doc.text().slice(..).to_string();
1285+ let completions = search_completions(cx, Some(reg));
11861286
11871287 let prompt = ui::regex_prompt(
11881288 cx,
11891289 "search:".into(),
11901290 Some(reg),
1291+ move |input: &str| {
1292+ completions
1293+ .iter()
1294+ .filter(|comp| comp.starts_with(input))
1295+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1296+ .collect()
1297+ },
11911298 move |view, doc, regex, event| {
11921299 if event != PromptEvent::Update {
11931300 return;
11941301 }
1195− search_impl(doc, view, &contents, ®ex, false);
1302+ search_impl(doc, view, &contents, ®ex, Movement::Move, direction);
11961303 },
11971304 );
11981305
11991306 cx.push_layer(Box::new(prompt));
12001307}
12011308
1202−fn search_next_impl(cx: &mut Context, extend: bool) {
1309+fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
12031310 let (view, doc) = current!(cx.editor);
12041311 let registers = &cx.editor.registers;
12051312 if let Some(query) = registers.read('/') {
@@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
12141321 .case_insensitive(case_insensitive)
12151322 .build()
12161323 {
1217− search_impl(doc, view, &contents, ®ex, extend);
1324+ search_impl(doc, view, &contents, ®ex, movement, direction);
12181325 } else {
12191326 // get around warning `mutable_borrow_reservation_conflict`
12201327 // which will be a hard error in the future
@@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
12261333}
12271334
12281335fn search_next(cx: &mut Context) {
1229− search_next_impl(cx, false);
1336+ search_next_or_prev_impl(cx, Movement::Move, Direction::Forward);
12301337}
12311338
1339+fn search_prev(cx: &mut Context) {
1340+ search_next_or_prev_impl(cx, Movement::Move, Direction::Backward);
1341+}
12321342fn extend_search_next(cx: &mut Context) {
1233− search_next_impl(cx, true);
1343+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward);
1344+}
1345+
1346+fn extend_search_prev(cx: &mut Context) {
1347+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward);
12341348}
12351349
12361350fn search_selection(cx: &mut Context) {
@@ -1247,10 +1361,19 @@ fn global_search(cx: &mut Context) {
12471361 let (all_matches_sx, all_matches_rx) =
12481362 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
12491363 let smart_case = cx.editor.config.smart_case;
1364+
1365+ let completions = search_completions(cx, None);
12501366 let prompt = ui::regex_prompt(
12511367 cx,
12521368 "global search:".into(),
12531369 None,
1370+ move |input: &str| {
1371+ completions
1372+ .iter()
1373+ .filter(|comp| comp.starts_with(input))
1374+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1375+ .collect()
1376+ },
12541377 move |_view, _doc, regex, event| {
12551378 if event != PromptEvent::Validate {
12561379 return;
@@ -1572,7 +1695,8 @@ mod cmd {
15721695 let (_, doc) = current!(cx.editor);
15731696
15741697 if let Some(path) = path {
1575− doc.set_path(path.as_ref()).context("invalid filepath")?;
1698+ doc.set_path(Some(path.as_ref()))
1699+ .context("invalid filepath")?;
15761700 }
15771701 if doc.path().is_none() {
15781702 bail!("cannot write a buffer without a filename");
@@ -1635,7 +1759,7 @@ mod cmd {
16351759
16361760 // If no argument, report current indent style.
16371761 if args.is_empty() {
1638− let style = current!(cx.editor).1.indent_style;
1762+ let style = doc!(cx.editor).indent_style;
16391763 cx.editor.set_status(match style {
16401764 Tabs => "tabs".into(),
16411765 Spaces(1) => "1 space".into(),
@@ -1674,7 +1798,7 @@ mod cmd {
16741798
16751799 // If no argument, report current line ending setting.
16761800 if args.is_empty() {
1677− let line_ending = current!(cx.editor).1.line_ending;
1801+ let line_ending = doc!(cx.editor).line_ending;
16781802 cx.editor.set_status(match line_ending {
16791803 Crlf => "crlf".into(),
16801804 LF => "line feed".into(),
@@ -1790,7 +1914,7 @@ mod cmd {
17901914 let mut errors = String::new();
17911915
17921916 // save all documents
1793− for (_, doc) in &mut cx.editor.documents {
1917+ for doc in &mut cx.editor.documents.values_mut() {
17941918 if doc.path().is_none() {
17951919 errors.push_str("cannot write a buffer without a filename\n");
17961920 continue;
@@ -2085,8 +2209,7 @@ mod cmd {
20852209 args: &[&str],
20862210 _event: PromptEvent,
20872211 ) -> anyhow::Result<()> {
2088− let (_, doc) = current!(cx.editor);
2089− let id = doc.id();
2212+ let id = view!(cx.editor).doc;
20902213
20912214 if let Some(path) = args.get(0) {
20922215 cx.editor.open(path.into(), Action::VerticalSplit)?;
@@ -2102,8 +2225,7 @@ mod cmd {
21022225 args: &[&str],
21032226 _event: PromptEvent,
21042227 ) -> anyhow::Result<()> {
2105− let (_, doc) = current!(cx.editor);
2106− let id = doc.id();
2228+ let id = view!(cx.editor).doc;
21072229
21082230 if let Some(path) = args.get(0) {
21092231 cx.editor.open(path.into(), Action::HorizontalSplit)?;
@@ -2188,6 +2310,18 @@ mod cmd {
21882310 Ok(())
21892311 }
21902312
2313+ fn tutor(
2314+ cx: &mut compositor::Context,
2315+ _args: &[&str],
2316+ _event: PromptEvent,
2317+ ) -> anyhow::Result<()> {
2318+ let path = helix_core::runtime_dir().join("tutor.txt");
2319+ cx.editor.open(path, Action::Replace)?;
2320+ // Unset path to prevent accidentally saving to the original tutor file.
2321+ doc_mut!(cx.editor).set_path(None)?;
2322+ Ok(())
2323+ }
2324+
21912325 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
21922326 TypableCommand {
21932327 name: "quit",
@@ -2199,7 +2333,7 @@ mod cmd {
21992333 TypableCommand {
22002334 name: "quit!",
22012335 aliases: &["q!"],
2202− doc: "Close the current view.",
2336+ doc: "Close the current view forcefully (ignoring unsaved changes).",
22032337 fun: force_quit,
22042338 completer: None,
22052339 },
@@ -2262,35 +2396,35 @@ mod cmd {
22622396 TypableCommand {
22632397 name: "write-quit",
22642398 aliases: &["wq", "x"],
2265− doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)",
2399+ doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
22662400 fun: write_quit,
22672401 completer: Some(completers::filename),
22682402 },
22692403 TypableCommand {
22702404 name: "write-quit!",
22712405 aliases: &["wq!", "x!"],
2272− doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2406+ doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
22732407 fun: force_write_quit,
22742408 completer: Some(completers::filename),
22752409 },
22762410 TypableCommand {
22772411 name: "write-all",
22782412 aliases: &["wa"],
2279− doc: "Writes changes from all views to disk.",
2413+ doc: "Write changes from all views to disk.",
22802414 fun: write_all,
22812415 completer: None,
22822416 },
22832417 TypableCommand {
22842418 name: "write-quit-all",
22852419 aliases: &["wqa", "xa"],
2286− doc: "Writes changes from all views to disk and close all views.",
2420+ doc: "Write changes from all views to disk and close all views.",
22872421 fun: write_all_quit,
22882422 completer: None,
22892423 },
22902424 TypableCommand {
22912425 name: "write-quit-all!",
22922426 aliases: &["wqa!", "xa!"],
2293− doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
2427+ doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
22942428 fun: force_write_all_quit,
22952429 completer: None,
22962430 },
@@ -2461,7 +2595,14 @@ mod cmd {
24612595 doc: "Open the file in a horizontal split.",
24622596 fun: hsplit,
24632597 completer: Some(completers::filename),
2464− }
2598+ },
2599+ TypableCommand {
2600+ name: "tutor",
2601+ aliases: &[],
2602+ doc: "Open the tutorial.",
2603+ fun: tutor,
2604+ completer: None,
2605+ },
24652606 ];
24662607
24672608 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) {
25612702 cx.editor
25622703 .documents
25632704 .iter()
2564− .map(|(id, doc)| (id, doc.path().cloned()))
2705+ .map(|(id, doc)| (*id, doc.path().cloned()))
25652706 .collect(),
25662707 move |(id, path): &(DocumentId, Option<PathBuf>)| {
25672708 let path = path.as_deref().map(helix_core::path::get_relative_path);
@@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) {
25802721 editor.switch(*id, Action::Replace);
25812722 },
25822723 |editor, (id, path)| {
2583− let doc = &editor.documents.get(*id)?;
2724+ let doc = &editor.documents.get(id)?;
25842725 let &view_id = doc.selections().keys().next()?;
25852726 let line = doc
25862727 .selection(view_id)
@@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) {
29963137 doc.text().len_lines() - 1
29973138 };
29983139 let line_idx = std::cmp::min(count.get() - 1, max_line);
3140+ let text = doc.text().slice(..);
29993141 let pos = doc.text().line_to_char(line_idx);
3000− doc.set_selection(view.id, Selection::point(pos));
3142+ let selection = doc
3143+ .selection(view.id)
3144+ .clone()
3145+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3146+ doc.set_selection(view.id, selection);
30013147 }
30023148}
30033149
@@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) {
30113157 } else {
30123158 doc.text().len_lines() - 1
30133159 };
3160+ let text = doc.text().slice(..);
30143161 let pos = doc.text().line_to_char(line_idx);
3015− doc.set_selection(view.id, Selection::point(pos));
3162+ let selection = doc
3163+ .selection(view.id)
3164+ .clone()
3165+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3166+ doc.set_selection(view.id, selection);
30163167}
30173168
30183169fn goto_last_accessed_file(cx: &mut Context) {
@@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) {
33063457 let editor = &mut cx.editor;
33073458 let (_, doc) = current!(editor);
33083459
3309− let diag = if let Some(diag) = doc.diagnostics().first() {
3310− diag.range.start
3311− } else {
3312− return;
3460+ let pos = match doc.diagnostics().first() {
3461+ Some(diag) => diag.range.start,
3462+ None => return,
33133463 };
33143464
3315− goto_pos(editor, diag);
3465+ goto_pos(editor, pos);
33163466}
33173467
33183468fn goto_last_diag(cx: &mut Context) {
33193469 let editor = &mut cx.editor;
33203470 let (_, doc) = current!(editor);
33213471
3322− let diag = if let Some(diag) = doc.diagnostics().last() {
3323− diag.range.start
3324− } else {
3325− return;
3472+ let pos = match doc.diagnostics().last() {
3473+ Some(diag) => diag.range.start,
3474+ None => return,
33263475 };
33273476
3328− goto_pos(editor, diag);
3477+ goto_pos(editor, pos);
33293478}
33303479
33313480fn goto_next_diag(cx: &mut Context) {
@@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) {
33363485 .selection(view.id)
33373486 .primary()
33383487 .cursor(doc.text().slice(..));
3339− let diag = if let Some(diag) = doc
3488+
3489+ let diag = doc
33403490 .diagnostics()
33413491 .iter()
3342− .map(|diag| diag.range.start)
3343− .find(|&pos| pos > cursor_pos)
3344− {
3345− diag
3346− } else if let Some(diag) = doc.diagnostics().first() {
3347− diag.range.start
3348− } else {
3349− return;
3492+ .find(|diag| diag.range.start > cursor_pos)
3493+ .or_else(|| doc.diagnostics().first());
3494+
3495+ let pos = match diag {
3496+ Some(diag) => diag.range.start,
3497+ None => return,
33503498 };
33513499
3352− goto_pos(editor, diag);
3500+ goto_pos(editor, pos);
33533501}
33543502
33553503fn goto_prev_diag(cx: &mut Context) {
@@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) {
33603508 .selection(view.id)
33613509 .primary()
33623510 .cursor(doc.text().slice(..));
3363− let diag = if let Some(diag) = doc
3511+
3512+ let diag = doc
33643513 .diagnostics()
33653514 .iter()
33663515 .rev()
3367− .map(|diag| diag.range.start)
3368− .find(|&pos| pos < cursor_pos)
3369− {
3370− diag
3371− } else if let Some(diag) = doc.diagnostics().last() {
3372− diag.range.start
3373− } else {
3374− return;
3516+ .find(|diag| diag.range.start < cursor_pos)
3517+ .or_else(|| doc.diagnostics().last());
3518+
3519+ let pos = match diag {
3520+ Some(diag) => diag.range.start,
3521+ None => return,
33753522 };
33763523
3377− goto_pos(editor, diag);
3524+ goto_pos(editor, pos);
33783525}
33793526
33803527fn signature_help(cx: &mut Context) {
@@ -3423,7 +3570,26 @@ pub mod insert {
34233570 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
34243571 pub type PostHook = fn(&mut Context, char);
34253572
3426− fn completion(cx: &mut Context, ch: char) {
3573+ // It trigger completion when idle timer reaches deadline
3574+ // Only trigger completion if the word under cursor is longer than n characters
3575+ pub fn idle_completion(cx: &mut Context) {
3576+ let (view, doc) = current!(cx.editor);
3577+ let text = doc.text().slice(..);
3578+ let cursor = doc.selection(view.id).primary().cursor(text);
3579+
3580+ use helix_core::chars::char_is_word;
3581+ let mut iter = text.chars_at(cursor);
3582+ iter.reverse();
3583+ for _ in 0..cx.editor.config.completion_trigger_len {
3584+ match iter.next() {
3585+ Some(c) if char_is_word(c) => {}
3586+ _ => return,
3587+ }
3588+ }
3589+ super::completion(cx);
3590+ }
3591+
3592+ fn language_server_completion(cx: &mut Context, ch: char) {
34273593 // if ch matches completion char, trigger completion
34283594 let doc = doc_mut!(cx.editor);
34293595 let language_server = match doc.language_server() {
@@ -3433,19 +3599,14 @@ pub mod insert {
34333599
34343600 let capabilities = language_server.capabilities();
34353601
3436− if let lsp::ServerCapabilities {
3437− completion_provider:
3438− Some(lsp::CompletionOptions {
3439− trigger_characters: Some(triggers),
3440− ..
3441− }),
3602+ if let Some(lsp::CompletionOptions {
3603+ trigger_characters: Some(triggers),
34423604 ..
3443− } = capabilities
3605+ }) = &capabilities.completion_provider
34443606 {
34453607 // TODO: what if trigger is multiple chars long
3446− let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3447−
3448− if is_trigger {
3608+ if triggers.iter().any(|trigger| trigger.contains(ch)) {
3609+ cx.editor.clear_idle_timer();
34493610 super::completion(cx);
34503611 }
34513612 }
@@ -3527,7 +3688,8 @@ pub mod insert {
35273688 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
35283689 // this could also generically look at Transaction, but it's a bit annoying to look at
35293690 // Operation instead of Change.
3530− for hook in &[completion, signature_help] {
3691+ for hook in &[language_server_completion, signature_help] {
3692+ // for hook in &[signature_help] {
35313693 hook(cx, c);
35323694 }
35333695 }
@@ -3668,13 +3830,19 @@ pub mod insert {
36683830fn undo(cx: &mut Context) {
36693831 let (view, doc) = current!(cx.editor);
36703832 let view_id = view.id;
3671− doc.undo(view_id);
3833+ let success = doc.undo(view_id);
3834+ if !success {
3835+ cx.editor.set_status("Already at oldest change".to_owned());
3836+ }
36723837}
36733838
36743839fn redo(cx: &mut Context) {
36753840 let (view, doc) = current!(cx.editor);
36763841 let view_id = view.id;
3677− doc.redo(view_id);
3842+ let success = doc.redo(view_id);
3843+ if !success {
3844+ cx.editor.set_status("Already at newest change".to_owned());
3845+ }
36783846}
36793847
36803848// Yank / Paste
@@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl(
37353903}
37363904
37373905fn yank_joined_to_clipboard(cx: &mut Context) {
3738− let line_ending = current!(cx.editor).1.line_ending;
3906+ let line_ending = doc!(cx.editor).line_ending;
37393907 let _ = yank_joined_to_clipboard_impl(
37403908 &mut cx.editor,
37413909 line_ending.as_str(),
@@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) {
37693937}
37703938
37713939fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3772− let line_ending = current!(cx.editor).1.line_ending;
3940+ let line_ending = doc!(cx.editor).line_ending;
37733941 let _ = yank_joined_to_clipboard_impl(
37743942 &mut cx.editor,
37753943 line_ending.as_str(),
@@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) {
38824050 let registers = &mut cx.editor.registers;
38834051
38844052 if let Some(values) = registers.read(reg_name) {
3885− if let Some(yank) = values.first() {
4053+ if !values.is_empty() {
4054+ let repeat = std::iter::repeat(
4055+ values
4056+ .last()
4057+ .map(|value| Tendril::from_slice(value))
4058+ .unwrap(),
4059+ );
4060+ let mut values = values
4061+ .iter()
4062+ .map(|value| Tendril::from_slice(value))
4063+ .chain(repeat);
38864064 let selection = doc.selection(view.id);
38874065 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
38884066 if !range.is_empty() {
3889− (range.from(), range.to(), Some(yank.as_str().into()))
4067+ (range.from(), range.to(), Some(values.next().unwrap()))
38904068 } else {
38914069 (range.from(), range.to(), None)
38924070 }
@@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) {
41284306 cx,
41294307 "keep:".into(),
41304308 Some(reg),
4309+ |_input: &str| Vec::new(),
41314310 move |view, doc, regex, event| {
41324311 if event != PromptEvent::Update {
41334312 return;
@@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) {
42284407 iter.reverse();
42294408 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
42304409 let start_offset = cursor.saturating_sub(offset);
4410+ let prefix = text.slice(start_offset..cursor).to_string();
42314411
42324412 cx.callback(
42334413 future,
@@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) {
42404420 return;
42414421 }
42424422
4243− let items = match response {
4423+ let mut items = match response {
42444424 Some(lsp::CompletionResponse::Array(items)) => items,
42454425 // TODO: do something with is_incomplete
42464426 Some(lsp::CompletionResponse::List(lsp::CompletionList {
@@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) {
42504430 None => Vec::new(),
42514431 };
42524432
4433+ if !prefix.is_empty() {
4434+ items = items
4435+ .into_iter()
4436+ .filter(|item| {
4437+ item.filter_text
4438+ .as_ref()
4439+ .unwrap_or(&item.label)
4440+ .starts_with(&prefix)
4441+ })
4442+ .collect();
4443+ }
4444+
42534445 if items.is_empty() {
42544446 // editor.set_error("No completion available".to_string());
42554447 return;
@@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
44014593// tree sitter node selection
44024594
44034595fn expand_selection(cx: &mut Context) {
4404− let (view, doc) = current!(cx.editor);
4596+ let motion = |editor: &mut Editor| {
4597+ let (view, doc) = current!(editor);
44054598
4406− if let Some(syntax) = doc.syntax() {
4407− let text = doc.text().slice(..);
4408− let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4409− doc.set_selection(view.id, selection);
4410− }
4599+ if let Some(syntax) = doc.syntax() {
4600+ let text = doc.text().slice(..);
4601+ let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4602+ doc.set_selection(view.id, selection);
4603+ }
4604+ };
4605+ motion(&mut cx.editor);
4606+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
44114607}
44124608
44134609fn match_brackets(cx: &mut Context) {
44144610 let (view, doc) = current!(cx.editor);
44154611
44164612 if let Some(syntax) = doc.syntax() {
4417− let pos = doc
4418− .selection(view.id)
4419− .primary()
4420− .cursor(doc.text().slice(..));
4421− if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
4422− let selection = Selection::point(pos);
4423− doc.set_selection(view.id, selection);
4424− };
4613+ let text = doc.text().slice(..);
4614+ let selection = doc.selection(view.id).clone().transform(|range| {
4615+ if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
4616+ range.put_cursor(text, pos, doc.mode == Mode::Select)
4617+ } else {
4618+ range
4619+ }
4620+ });
4621+ doc.set_selection(view.id, selection);
44254622 }
44264623}
44274624
@@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) {
44294626
44304627fn jump_forward(cx: &mut Context) {
44314628 let count = cx.count();
4432− let (view, _doc) = current!(cx.editor);
4629+ let view = view_mut!(cx.editor);
44334630
44344631 if let Some((id, selection)) = view.jumps.forward(count) {
44354632 view.doc = *id;
@@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) {
44634660 cx.editor.focus_next()
44644661}
44654662
4663+fn jump_view_right(cx: &mut Context) {
4664+ cx.editor.focus_right()
4665+}
4666+
4667+fn jump_view_left(cx: &mut Context) {
4668+ cx.editor.focus_left()
4669+}
4670+
4671+fn jump_view_up(cx: &mut Context) {
4672+ cx.editor.focus_up()
4673+}
4674+
4675+fn jump_view_down(cx: &mut Context) {
4676+ cx.editor.focus_down()
4677+}
4678+
44664679// split helper, clear it later
44674680fn split(cx: &mut Context, action: Action) {
44684681 let (view, doc) = current!(cx.editor);
@@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
45524765 let count = cx.count();
45534766 cx.on_next_key(move |cx, event| {
45544767 if let Some(ch) = event.char() {
4555− let (view, doc) = current!(cx.editor);
4556− let text = doc.text().slice(..);
4768+ let textobject = move |editor: &mut Editor| {
4769+ let (view, doc) = current!(editor);
4770+ let text = doc.text().slice(..);
4771+
4772+ let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4773+ let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4774+ Some(t) => t,
4775+ None => return range,
4776+ };
4777+ textobject::textobject_treesitter(
4778+ text,
4779+ range,
4780+ objtype,
4781+ obj_name,
4782+ syntax.tree().root_node(),
4783+ lang_config,
4784+ count,
4785+ )
4786+ };
45574787
4558− let selection = doc.selection(view.id).clone().transform(|range| {
4559− match ch {
4560− 'w' => textobject::textobject_word(text, range, objtype, count),
4561− // TODO: cancel new ranges if inconsistent surround matches across lines
4562− ch if !ch.is_ascii_alphanumeric() => {
4563− textobject::textobject_surround(text, range, objtype, ch, count)
4788+ let selection = doc.selection(view.id).clone().transform(|range| {
4789+ match ch {
4790+ 'w' => textobject::textobject_word(text, range, objtype, count),
4791+ 'c' => textobject_treesitter("class", range),
4792+ 'f' => textobject_treesitter("function", range),
4793+ 'p' => textobject_treesitter("parameter", range),
4794+ // TODO: cancel new ranges if inconsistent surround matches across lines
4795+ ch if !ch.is_ascii_alphanumeric() => {
4796+ textobject::textobject_surround(text, range, objtype, ch, count)
4797+ }
4798+ _ => range,
45644799 }
4565− _ => range,
4566− }
4567− });
4568− doc.set_selection(view.id, selection);
4800+ });
4801+ doc.set_selection(view.id, selection);
4802+ };
4803+ textobject(&mut cx.editor);
4804+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
45694805 }
45704806 })
45714807}
@@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) {
45774813 let selection = doc.selection(view.id);
45784814 let (open, close) = surround::get_pair(ch);
45794815
4580− let mut changes = Vec::new();
4816+ let mut changes = Vec::with_capacity(selection.len() * 2);
45814817 for range in selection.iter() {
45824818 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
45834819 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));