@@ -16,8 +16,13 @@ use helix_core::{
18use helix_view::{
18use helix_view::{
19− clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
19+ clipboard::ClipboardType,
21+ editor::{Action, Motion},
20− view::View, Document, DocumentId, Editor, ViewId,
25+ Document, DocumentId, Editor, ViewId,
23use anyhow::{anyhow, bail, Context as _};
28use anyhow::{anyhow, bail, Context as _};
@@ -202,6 +207,7 @@ impl Command {
202 find_prev_char, "Move to previous occurance of char",
207 find_prev_char, "Move to previous occurance of char",
203 extend_till_prev_char, "Extend till previous occurance of char",
208 extend_till_prev_char, "Extend till previous occurance of char",
204 extend_prev_char, "Extend to previous occurance of char",
209 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...)",
205 replace, "Replace with new char",
211 replace, "Replace with new char",
206 switch_case, "Switch (toggle) case",
212 switch_case, "Switch (toggle) case",
207 switch_to_uppercase, "Switch to uppercase",
213 switch_to_uppercase, "Switch to uppercase",
@@ -215,8 +221,11 @@ impl Command {
215 split_selection, "Split selection into subselections on regex matches",
221 split_selection, "Split selection into subselections on regex matches",
216 split_selection_on_newline, "Split selection on newlines",
222 split_selection_on_newline, "Split selection on newlines",
217 search, "Search for regex pattern",
223 search, "Search for regex pattern",
224+ rsearch, "Reverse search for regex pattern",
218 search_next, "Select next search match",
225 search_next, "Select next search match",
226+ search_prev, "Select previous search match",
219 extend_search_next, "Add next search match to selection",
227 extend_search_next, "Add next search match to selection",
228+ extend_search_prev, "Add previous search match to selection",
220 search_selection, "Use current selection as search pattern",
229 search_selection, "Use current selection as search pattern",
221 global_search, "Global Search in workspace folder",
230 global_search, "Global Search in workspace folder",
222 extend_line, "Select current line, if already selected, extend to next line",
231 extend_line, "Select current line, if already selected, extend to next line",
@@ -260,6 +269,8 @@ impl Command {
260 goto_prev_diag, "Goto previous diagnostic",
269 goto_prev_diag, "Goto previous diagnostic",
261 goto_line_start, "Goto line start",
270 goto_line_start, "Goto line start",
262 goto_line_end, "Goto line end",
271 goto_line_end, "Goto line end",
272+ goto_next_buffer, "Goto next buffer",
273+ goto_previous_buffer, "Goto previous buffer",
263 // TODO: different description ?
274 // TODO: different description ?
264 goto_line_end_newline, "Goto line end",
275 goto_line_end_newline, "Goto line end",
265 goto_first_nonwhitespace, "Goto first non-blank in line",
276 goto_first_nonwhitespace, "Goto first non-blank in line",
@@ -305,6 +316,10 @@ impl Command {
305 expand_selection, "Expand selection to parent syntax node",
316 expand_selection, "Expand selection to parent syntax node",
306 jump_forward, "Jump forward on jumplist",
317 jump_forward, "Jump forward on jumplist",
307 jump_backward, "Jump backward on jumplist",
318 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",
308 rotate_view, "Goto next window",
323 rotate_view, "Goto next window",
309 hsplit, "Horizontal bottom split",
324 hsplit, "Horizontal bottom split",
310 vsplit, "Vertical right split",
325 vsplit, "Vertical right split",
@@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) {
546+fn goto_next_buffer(cx: &mut Context) {
547+ goto_buffer(cx, Direction::Forward);
550+fn goto_previous_buffer(cx: &mut Context) {
551+ goto_buffer(cx, Direction::Backward);
554+fn goto_buffer(cx: &mut Context, direction: Direction) {
555+ let current = view!(cx.editor).doc;
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())
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
569+ .or_else(|| cx.editor.documents.keys().rev().next())
576+ cx.editor.switch(id, Action::Replace);
531fn extend_to_line_start(cx: &mut Context) {
579fn extend_to_line_start(cx: &mut Context) {
532 let (view, doc) = current!(cx.editor);
580 let (view, doc) = current!(cx.editor);
533 goto_line_start_impl(view, doc, Movement::Extend)
581 goto_line_start_impl(view, doc, Movement::Extend)
@@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) {
632 push_jump(cx.editor);
680 push_jump(cx.editor);
633 let (view, doc) = current!(cx.editor);
681 let (view, doc) = current!(cx.editor);
634− doc.set_selection(view.id, Selection::point(0));
682+ let text = doc.text().slice(..);
686+ .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select));
687+ doc.set_selection(view.id, selection);
638fn goto_file_end(cx: &mut Context) {
691fn goto_file_end(cx: &mut Context) {
639 push_jump(cx.editor);
692 push_jump(cx.editor);
640 let (view, doc) = current!(cx.editor);
693 let (view, doc) = current!(cx.editor);
694+ let text = doc.text().slice(..);
641− doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
695+ let pos = doc.text().len_chars();
699+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
700+ doc.set_selection(view.id, selection);
644fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
703fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
@@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) {
681 extend_word_impl(cx, movement::move_next_long_word_end)
740 extend_word_impl(cx, movement::move_next_long_word_end)
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)
687 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
745 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
@@ -704,13 +762,7 @@ where
704 // usually mix line endings. But we should fix it eventually
762 // usually mix line endings. But we should fix it eventually
705 // anyway.
763 // anyway.
709− .line_ending
765+ doc!(cx.editor).line_ending.as_str().chars().next().unwrap()
716 KeyEvent {
768 KeyEvent {
@@ -720,29 +772,48 @@ where
720 _ => return,
772 _ => return,
723− let (view, doc) = current!(cx.editor);
775+ find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
724− let text = doc.text().slice(..);
776+ cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
777+ find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
726− let selection = doc.selection(view.id).clone().transform(|range| {
782+//
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 {
785+fn find_char_impl<F>(
786+ editor: &mut Editor,
793+ F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
795+ let (view, doc) = current!(editor);
796+ let text = doc.text().slice(..);
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 {
736− search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
808+ search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
737− if extend {
809+ if extend {
738− range.put_cursor(text, pos, true)
810+ range.put_cursor(text, pos, true)
739− } else {
811+ } else {
740− Range::point(range.cursor(text)).put_cursor(text, pos, true)
812+ Range::point(range.cursor(text)).put_cursor(text, pos, true)
744− doc.set_selection(view.id, selection);
816+ doc.set_selection(view.id, selection);
748fn find_next_char_impl(
819fn find_next_char_impl(
@@ -756,6 +827,10 @@ fn find_next_char_impl(
756 if inclusive {
827 if inclusive {
757 search::find_nth_next(text, ch, pos, n)
828 search::find_nth_next(text, ch, pos, n)
830+ let n = match text.get_char(pos) {
831+ Some(next_ch) if next_ch == ch => n + 1,
759 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
834 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
@@ -770,80 +845,52 @@ fn find_prev_char_impl(
770 if inclusive {
845 if inclusive {
771 search::find_nth_prev(text, ch, pos, n)
846 search::find_nth_prev(text, ch, pos, n)
848+ let n = match text.get_char(pos.saturating_sub(1)) {
849+ Some(next_ch) if next_ch == ch => n + 1,
773 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
852 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
777fn find_till_char(cx: &mut Context) {
856fn find_till_char(cx: &mut Context) {
780− find_next_char_impl,
857+ will_find_char(cx, find_next_char_impl, false, false)
781− false, /* inclusive */
786fn find_next_char(cx: &mut Context) {
860fn find_next_char(cx: &mut Context) {
789− find_next_char_impl,
861+ will_find_char(cx, find_next_char_impl, true, false)
790− true, /* inclusive */
795fn extend_till_char(cx: &mut Context) {
864fn extend_till_char(cx: &mut Context) {
798− find_next_char_impl,
865+ will_find_char(cx, find_next_char_impl, false, true)
799− false, /* inclusive */
804fn extend_next_char(cx: &mut Context) {
868fn extend_next_char(cx: &mut Context) {
807− find_next_char_impl,
869+ will_find_char(cx, find_next_char_impl, true, true)
808− true, /* inclusive */
813fn till_prev_char(cx: &mut Context) {
872fn till_prev_char(cx: &mut Context) {
816− find_prev_char_impl,
873+ will_find_char(cx, find_prev_char_impl, false, false)
817− false, /* inclusive */
822fn find_prev_char(cx: &mut Context) {
876fn find_prev_char(cx: &mut Context) {
825− find_prev_char_impl,
877+ will_find_char(cx, find_prev_char_impl, true, false)
826− true, /* inclusive */
831fn extend_till_prev_char(cx: &mut Context) {
880fn extend_till_prev_char(cx: &mut Context) {
834− find_prev_char_impl,
881+ will_find_char(cx, find_prev_char_impl, false, true)
835− false, /* inclusive */
840fn extend_prev_char(cx: &mut Context) {
884fn extend_prev_char(cx: &mut Context) {
843− find_prev_char_impl,
885+ will_find_char(cx, find_prev_char_impl, true, true)
844− true, /* inclusive */
886+}
845− true, /* extend */
887+
846− )
888+fn repeat_last_motion(cx: &mut Context) {
889+ let last_motion = cx.editor.last_motion.take();
890+ if let Some(m) = &last_motion {
892+ cx.editor.last_motion = last_motion;
849fn replace(cx: &mut Context) {
896fn replace(cx: &mut Context) {
@@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) {
1092 "select:".into(),
1139 "select:".into(),
1093 Some(reg),
1140 Some(reg),
1141+ |_input: &str| Vec::new(),
1094 move |view, doc, regex, event| {
1142 move |view, doc, regex, event| {
1095 if event != PromptEvent::Update {
1143 if event != PromptEvent::Update {
@@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) {
1114 "split:".into(),
1162 "split:".into(),
1115 Some(reg),
1163 Some(reg),
1164+ |_input: &str| Vec::new(),
1116 move |view, doc, regex, event| {
1165 move |view, doc, regex, event| {
1117 if event != PromptEvent::Update {
1166 if event != PromptEvent::Update {
@@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) {
1137 doc.set_selection(view.id, selection);
1186 doc.set_selection(view.id, selection);
1140−fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
1189+fn search_impl(
1190+ doc: &mut Document,
1194+ movement: Movement,
1195+ direction: Direction,
1141 let text = doc.text().slice(..);
1197 let text = doc.text().slice(..);
1142 let selection = doc.selection(view.id);
1198 let selection = doc.selection(view.id);
1144− // Get the right side of the primary block cursor.
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 {
1145− let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1203+ Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary(
1205+ selection.primary().to(),
1207+ Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary(
1147− selection.primary().cursor(text),
1209+ selection.primary().from(),
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;
1150 // use find_at to find the next match after the cursor, loop around the end
1219 // use find_at to find the next match after the cursor, loop around the end
1151 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1220 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1152− let mat = regex
1221+ let mat = match direction {
1222+ Direction::Forward => regex
1153− .find_at(contents, start)
1223+ .find_at(contents, start)
1154− .or_else(|| regex.find(contents));
1224+ .or_else(|| regex.find(contents)),
1225+ Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
1227+ regex.find_iter(&contents[start..]).last()
1155 // TODO: message on wraparound
1230 // TODO: message on wraparound
1156 if let Some(mat) = mat {
1231 if let Some(mat) = mat {
1157− let start = text.byte_to_char(mat.start());
1232+ let start = text.byte_to_char(mat.start() + offset);
1158− let end = text.byte_to_char(mat.end());
1233+ let end = text.byte_to_char(mat.end() + offset);
1160 if end == 0 {
1235 if end == 0 {
1161 // skip empty matches that don't make sense
1236 // skip empty matches that don't make sense
1240+ // Determine range direction based on the primary range
1165− let selection = if extend {
1241+ let primary = selection.primary();
1242+ let range = if primary.head < primary.anchor {
1166− selection.clone().push(Range::new(start, end))
1243+ Range::new(end, start)
1167 } else {
1244 } else {
1168− Selection::single(start, end)
1245+ Range::new(start, end)
1248+ let selection = match movement {
1249+ Movement::Extend => selection.clone().push(range),
1250+ Movement::Move => selection.clone().replace(selection.primary_index(), range),
1171 doc.set_selection(view.id, selection);
1253 doc.set_selection(view.id, selection);
@@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
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();
1264+ items.into_iter().cloned().collect()
1176// TODO: use one function for search vs extend
1267// TODO: use one function for search vs extend
1177fn search(cx: &mut Context) {
1268fn search(cx: &mut Context) {
1269+ searcher(cx, Direction::Forward)
1272+fn rsearch(cx: &mut Context) {
1273+ searcher(cx, Direction::Backward)
1275+// TODO: use one function for search vs extend
1276+fn searcher(cx: &mut Context, direction: Direction) {
1178 let reg = cx.register.unwrap_or('/');
1277 let reg = cx.register.unwrap_or('/');
1179 let (_, doc) = current!(cx.editor);
1278 let (_, doc) = current!(cx.editor);
@@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) {
1183 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
1282 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
1184 // feed chunks into the regex yet
1283 // feed chunks into the regex yet
1185 let contents = doc.text().slice(..).to_string();
1284 let contents = doc.text().slice(..).to_string();
1285+ let completions = search_completions(cx, Some(reg));
1187 let prompt = ui::regex_prompt(
1287 let prompt = ui::regex_prompt(
1189 "search:".into(),
1289 "search:".into(),
1190 Some(reg),
1290 Some(reg),
1291+ move |input: &str| {
1294+ .filter(|comp| comp.starts_with(input))
1295+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1191 move |view, doc, regex, event| {
1298 move |view, doc, regex, event| {
1192 if event != PromptEvent::Update {
1299 if event != PromptEvent::Update {
1195− search_impl(doc, view, &contents, ®ex, false);
1302+ search_impl(doc, view, &contents, ®ex, Movement::Move, direction);
1199 cx.push_layer(Box::new(prompt));
1306 cx.push_layer(Box::new(prompt));
1202−fn search_next_impl(cx: &mut Context, extend: bool) {
1309+fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1203 let (view, doc) = current!(cx.editor);
1310 let (view, doc) = current!(cx.editor);
1204 let registers = &cx.editor.registers;
1311 let registers = &cx.editor.registers;
1205 if let Some(query) = registers.read('/') {
1312 if let Some(query) = registers.read('/') {
@@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
1214 .case_insensitive(case_insensitive)
1321 .case_insensitive(case_insensitive)
1215 .build()
1322 .build()
1217− search_impl(doc, view, &contents, ®ex, extend);
1324+ search_impl(doc, view, &contents, ®ex, movement, direction);
1218 } else {
1325 } else {
1219 // get around warning `mutable_borrow_reservation_conflict`
1326 // get around warning `mutable_borrow_reservation_conflict`
1220 // which will be a hard error in the future
1327 // which will be a hard error in the future
@@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) {
1228fn search_next(cx: &mut Context) {
1335fn search_next(cx: &mut Context) {
1229− search_next_impl(cx, false);
1336+ search_next_or_prev_impl(cx, Movement::Move, Direction::Forward);
1339+fn search_prev(cx: &mut Context) {
1340+ search_next_or_prev_impl(cx, Movement::Move, Direction::Backward);
1232fn extend_search_next(cx: &mut Context) {
1342fn extend_search_next(cx: &mut Context) {
1233− search_next_impl(cx, true);
1343+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward);
1346+fn extend_search_prev(cx: &mut Context) {
1347+ search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward);
1236fn search_selection(cx: &mut Context) {
1350fn search_selection(cx: &mut Context) {
@@ -1247,10 +1361,19 @@ fn global_search(cx: &mut Context) {
1247 let (all_matches_sx, all_matches_rx) =
1361 let (all_matches_sx, all_matches_rx) =
1248 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1362 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1249 let smart_case = cx.editor.config.smart_case;
1363 let smart_case = cx.editor.config.smart_case;
1365+ let completions = search_completions(cx, None);
1250 let prompt = ui::regex_prompt(
1366 let prompt = ui::regex_prompt(
1252 "global search:".into(),
1368 "global search:".into(),
1370+ move |input: &str| {
1373+ .filter(|comp| comp.starts_with(input))
1374+ .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1254 move |_view, _doc, regex, event| {
1377 move |_view, _doc, regex, event| {
1255 if event != PromptEvent::Validate {
1378 if event != PromptEvent::Validate {
@@ -1572,7 +1695,8 @@ mod cmd {
1572 let (_, doc) = current!(cx.editor);
1695 let (_, doc) = current!(cx.editor);
1574 if let Some(path) = path {
1697 if let Some(path) = path {
1698+ doc.set_path(Some(path.as_ref()))
1575− doc.set_path(path.as_ref()).context("invalid filepath")?;
1699+ .context("invalid filepath")?;
1577 if doc.path().is_none() {
1701 if doc.path().is_none() {
1578 bail!("cannot write a buffer without a filename");
1702 bail!("cannot write a buffer without a filename");
@@ -1635,7 +1759,7 @@ mod cmd {
1636 // If no argument, report current indent style.
1760 // If no argument, report current indent style.
1637 if args.is_empty() {
1761 if args.is_empty() {
1638− let style = current!(cx.editor).1.indent_style;
1762+ let style = doc!(cx.editor).indent_style;
1639 cx.editor.set_status(match style {
1763 cx.editor.set_status(match style {
1640 Tabs => "tabs".into(),
1764 Tabs => "tabs".into(),
1641 Spaces(1) => "1 space".into(),
1765 Spaces(1) => "1 space".into(),
@@ -1674,7 +1798,7 @@ mod cmd {
1675 // If no argument, report current line ending setting.
1799 // If no argument, report current line ending setting.
1676 if args.is_empty() {
1800 if args.is_empty() {
1677− let line_ending = current!(cx.editor).1.line_ending;
1801+ let line_ending = doc!(cx.editor).line_ending;
1678 cx.editor.set_status(match line_ending {
1802 cx.editor.set_status(match line_ending {
1679 Crlf => "crlf".into(),
1803 Crlf => "crlf".into(),
1680 LF => "line feed".into(),
1804 LF => "line feed".into(),
@@ -1790,7 +1914,7 @@ mod cmd {
1790 let mut errors = String::new();
1914 let mut errors = String::new();
1792 // save all documents
1916 // save all documents
1793− for (_, doc) in &mut cx.editor.documents {
1917+ for doc in &mut cx.editor.documents.values_mut() {
1794 if doc.path().is_none() {
1918 if doc.path().is_none() {
1795 errors.push_str("cannot write a buffer without a filename\n");
1919 errors.push_str("cannot write a buffer without a filename\n");
1796 continue;
1920 continue;
@@ -2085,8 +2209,7 @@ mod cmd {
2085 args: &[&str],
2209 args: &[&str],
2086 _event: PromptEvent,
2210 _event: PromptEvent,
2087 ) -> anyhow::Result<()> {
2211 ) -> anyhow::Result<()> {
2088− let (_, doc) = current!(cx.editor);
2212+ let id = view!(cx.editor).doc;
2091 if let Some(path) = args.get(0) {
2214 if let Some(path) = args.get(0) {
2092 cx.editor.open(path.into(), Action::VerticalSplit)?;
2215 cx.editor.open(path.into(), Action::VerticalSplit)?;
@@ -2102,8 +2225,7 @@ mod cmd {
2102 args: &[&str],
2225 args: &[&str],
2103 _event: PromptEvent,
2226 _event: PromptEvent,
2104 ) -> anyhow::Result<()> {
2227 ) -> anyhow::Result<()> {
2105− let (_, doc) = current!(cx.editor);
2228+ let id = view!(cx.editor).doc;
2108 if let Some(path) = args.get(0) {
2230 if let Some(path) = args.get(0) {
2109 cx.editor.open(path.into(), Action::HorizontalSplit)?;
2231 cx.editor.open(path.into(), Action::HorizontalSplit)?;
@@ -2188,6 +2310,18 @@ mod cmd {
2314+ cx: &mut compositor::Context,
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)?;
2191 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2325 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2192 TypableCommand {
2326 TypableCommand {
2193 name: "quit",
2327 name: "quit",
@@ -2199,7 +2333,7 @@ mod cmd {
2199 TypableCommand {
2333 TypableCommand {
2200 name: "quit!",
2334 name: "quit!",
2201 aliases: &["q!"],
2335 aliases: &["q!"],
2202− doc: "Close the current view.",
2336+ doc: "Close the current view forcefully (ignoring unsaved changes).",
2203 fun: force_quit,
2337 fun: force_quit,
2204 completer: None,
2338 completer: None,
@@ -2262,35 +2396,35 @@ mod cmd {
2262 TypableCommand {
2396 TypableCommand {
2263 name: "write-quit",
2397 name: "write-quit",
2264 aliases: &["wq", "x"],
2398 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)",
2266 fun: write_quit,
2400 fun: write_quit,
2267 completer: Some(completers::filename),
2401 completer: Some(completers::filename),
2269 TypableCommand {
2403 TypableCommand {
2270 name: "write-quit!",
2404 name: "write-quit!",
2271 aliases: &["wq!", "x!"],
2405 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)",
2273 fun: force_write_quit,
2407 fun: force_write_quit,
2274 completer: Some(completers::filename),
2408 completer: Some(completers::filename),
2276 TypableCommand {
2410 TypableCommand {
2277 name: "write-all",
2411 name: "write-all",
2278 aliases: &["wa"],
2412 aliases: &["wa"],
2279− doc: "Writes changes from all views to disk.",
2413+ doc: "Write changes from all views to disk.",
2280 fun: write_all,
2414 fun: write_all,
2281 completer: None,
2415 completer: None,
2283 TypableCommand {
2417 TypableCommand {
2284 name: "write-quit-all",
2418 name: "write-quit-all",
2285 aliases: &["wqa", "xa"],
2419 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.",
2287 fun: write_all_quit,
2421 fun: write_all_quit,
2288 completer: None,
2422 completer: None,
2290 TypableCommand {
2424 TypableCommand {
2291 name: "write-quit-all!",
2425 name: "write-quit-all!",
2292 aliases: &["wqa!", "xa!"],
2426 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).",
2294 fun: force_write_all_quit,
2428 fun: force_write_all_quit,
2295 completer: None,
2429 completer: None,
@@ -2461,7 +2595,14 @@ mod cmd {
2461 doc: "Open the file in a horizontal split.",
2595 doc: "Open the file in a horizontal split.",
2462 fun: hsplit,
2596 fun: hsplit,
2463 completer: Some(completers::filename),
2597 completer: Some(completers::filename),
2602+ doc: "Open the tutorial.",
2467 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2608 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) {
2561 cx.editor
2702 cx.editor
2562 .documents
2703 .documents
2564− .map(|(id, doc)| (id, doc.path().cloned()))
2705+ .map(|(id, doc)| (*id, doc.path().cloned()))
2565 .collect(),
2706 .collect(),
2566 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2707 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2567 let path = path.as_deref().map(helix_core::path::get_relative_path);
2708 let path = path.as_deref().map(helix_core::path::get_relative_path);
@@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) {
2580 editor.switch(*id, Action::Replace);
2721 editor.switch(*id, Action::Replace);
2582 |editor, (id, path)| {
2723 |editor, (id, path)| {
2583− let doc = &editor.documents.get(*id)?;
2724+ let doc = &editor.documents.get(id)?;
2584 let &view_id = doc.selections().keys().next()?;
2725 let &view_id = doc.selections().keys().next()?;
2585 let line = doc
2726 let line = doc
2586 .selection(view_id)
2727 .selection(view_id)
@@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) {
2996 doc.text().len_lines() - 1
3137 doc.text().len_lines() - 1
2998 let line_idx = std::cmp::min(count.get() - 1, max_line);
3139 let line_idx = std::cmp::min(count.get() - 1, max_line);
3140+ let text = doc.text().slice(..);
2999 let pos = doc.text().line_to_char(line_idx);
3141 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)
3145+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3146+ doc.set_selection(view.id, selection);
@@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) {
3011 } else {
3157 } else {
3012 doc.text().len_lines() - 1
3158 doc.text().len_lines() - 1
3160+ let text = doc.text().slice(..);
3014 let pos = doc.text().line_to_char(line_idx);
3161 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)
3165+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3166+ doc.set_selection(view.id, selection);
3018fn goto_last_accessed_file(cx: &mut Context) {
3169fn goto_last_accessed_file(cx: &mut Context) {
@@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) {
3306 let editor = &mut cx.editor;
3457 let editor = &mut cx.editor;
3307 let (_, doc) = current!(editor);
3458 let (_, doc) = current!(editor);
3309− let diag = if let Some(diag) = doc.diagnostics().first() {
3460+ let pos = match doc.diagnostics().first() {
3310− diag.range.start
3461+ Some(diag) => diag.range.start,
3312− return;
3462+ None => return,
3315− goto_pos(editor, diag);
3465+ goto_pos(editor, pos);
3318fn goto_last_diag(cx: &mut Context) {
3468fn goto_last_diag(cx: &mut Context) {
3319 let editor = &mut cx.editor;
3469 let editor = &mut cx.editor;
3320 let (_, doc) = current!(editor);
3470 let (_, doc) = current!(editor);
3322− let diag = if let Some(diag) = doc.diagnostics().last() {
3472+ let pos = match doc.diagnostics().last() {
3323− diag.range.start
3473+ Some(diag) => diag.range.start,
3325− return;
3474+ None => return,
3328− goto_pos(editor, diag);
3477+ goto_pos(editor, pos);
3331fn goto_next_diag(cx: &mut Context) {
3480fn goto_next_diag(cx: &mut Context) {
@@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) {
3336 .selection(view.id)
3485 .selection(view.id)
3337 .primary()
3486 .primary()
3338 .cursor(doc.text().slice(..));
3487 .cursor(doc.text().slice(..));
3339− let diag = if let Some(diag) = doc
3489+ let diag = doc
3340 .diagnostics()
3490 .diagnostics()
3342− .map(|diag| diag.range.start)
3343− .find(|&pos| pos > cursor_pos)
3492+ .find(|diag| diag.range.start > cursor_pos)
3346− } else if let Some(diag) = doc.diagnostics().first() {
3493+ .or_else(|| doc.diagnostics().first());
3495+ let pos = match diag {
3347− diag.range.start
3496+ Some(diag) => diag.range.start,
3349− return;
3497+ None => return,
3352− goto_pos(editor, diag);
3500+ goto_pos(editor, pos);
3355fn goto_prev_diag(cx: &mut Context) {
3503fn goto_prev_diag(cx: &mut Context) {
@@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) {
3360 .selection(view.id)
3508 .selection(view.id)
3361 .primary()
3509 .primary()
3362 .cursor(doc.text().slice(..));
3510 .cursor(doc.text().slice(..));
3363− let diag = if let Some(diag) = doc
3512+ let diag = doc
3364 .diagnostics()
3513 .diagnostics()
3367− .map(|diag| diag.range.start)
3368− .find(|&pos| pos < cursor_pos)
3516+ .find(|diag| diag.range.start < cursor_pos)
3371− } else if let Some(diag) = doc.diagnostics().last() {
3517+ .or_else(|| doc.diagnostics().last());
3519+ let pos = match diag {
3372− diag.range.start
3520+ Some(diag) => diag.range.start,
3374− return;
3521+ None => return,
3377− goto_pos(editor, diag);
3524+ goto_pos(editor, pos);
3380fn signature_help(cx: &mut Context) {
3527fn signature_help(cx: &mut Context) {
@@ -3423,7 +3570,26 @@ pub mod insert {
3423 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3570 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3424 pub type PostHook = fn(&mut Context, char);
3571 pub type PostHook = fn(&mut Context, 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);
3580+ use helix_core::chars::char_is_word;
3581+ let mut iter = text.chars_at(cursor);
3583+ for _ in 0..cx.editor.config.completion_trigger_len {
3584+ match iter.next() {
3585+ Some(c) if char_is_word(c) => {}
3426− fn completion(cx: &mut Context, ch: char) {
3589+ super::completion(cx);
3592+ fn language_server_completion(cx: &mut Context, ch: char) {
3427 // if ch matches completion char, trigger completion
3593 // if ch matches completion char, trigger completion
3428 let doc = doc_mut!(cx.editor);
3594 let doc = doc_mut!(cx.editor);
3429 let language_server = match doc.language_server() {
3595 let language_server = match doc.language_server() {
@@ -3433,19 +3599,14 @@ pub mod insert {
3434 let capabilities = language_server.capabilities();
3600 let capabilities = language_server.capabilities();
3436− if let lsp::ServerCapabilities {
3602+ if let Some(lsp::CompletionOptions {
3437− completion_provider:
3438− Some(lsp::CompletionOptions {
3439− trigger_characters: Some(triggers),
3603+ trigger_characters: Some(triggers),
3443− } = capabilities
3605+ }) = &capabilities.completion_provider
3445 // TODO: what if trigger is multiple chars long
3607 // TODO: what if trigger is multiple chars long
3446− let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3608+ if triggers.iter().any(|trigger| trigger.contains(ch)) {
3447−
3609+ cx.editor.clear_idle_timer();
3449 super::completion(cx);
3610 super::completion(cx);
@@ -3527,7 +3688,8 @@ pub mod insert {
3527 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3688 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3528 // this could also generically look at Transaction, but it's a bit annoying to look at
3689 // this could also generically look at Transaction, but it's a bit annoying to look at
3529 // Operation instead of Change.
3690 // 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] {
3531 hook(cx, c);
3693 hook(cx, c);
@@ -3668,13 +3830,19 @@ pub mod insert {
3668fn undo(cx: &mut Context) {
3830fn undo(cx: &mut Context) {
3669 let (view, doc) = current!(cx.editor);
3831 let (view, doc) = current!(cx.editor);
3670 let view_id = view.id;
3832 let view_id = view.id;
3671− doc.undo(view_id);
3833+ let success = doc.undo(view_id);
3835+ cx.editor.set_status("Already at oldest change".to_owned());
3674fn redo(cx: &mut Context) {
3839fn redo(cx: &mut Context) {
3675 let (view, doc) = current!(cx.editor);
3840 let (view, doc) = current!(cx.editor);
3676 let view_id = view.id;
3841 let view_id = view.id;
3677− doc.redo(view_id);
3842+ let success = doc.redo(view_id);
3844+ cx.editor.set_status("Already at newest change".to_owned());
3680// Yank / Paste
3848// Yank / Paste
@@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl(
3737fn yank_joined_to_clipboard(cx: &mut Context) {
3905fn 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;
3739 let _ = yank_joined_to_clipboard_impl(
3907 let _ = yank_joined_to_clipboard_impl(
3740 &mut cx.editor,
3908 &mut cx.editor,
3741 line_ending.as_str(),
3909 line_ending.as_str(),
@@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) {
3771fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3939fn 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;
3773 let _ = yank_joined_to_clipboard_impl(
3941 let _ = yank_joined_to_clipboard_impl(
3774 &mut cx.editor,
3942 &mut cx.editor,
3775 line_ending.as_str(),
3943 line_ending.as_str(),
@@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) {
3882 let registers = &mut cx.editor.registers;
4050 let registers = &mut cx.editor.registers;
3884 if let Some(values) = registers.read(reg_name) {
4052 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(
4057+ .map(|value| Tendril::from_slice(value))
4060+ let mut values = values
4062+ .map(|value| Tendril::from_slice(value))
3886 let selection = doc.selection(view.id);
4064 let selection = doc.selection(view.id);
3887 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
4065 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3888 if !range.is_empty() {
4066 if !range.is_empty() {
3889− (range.from(), range.to(), Some(yank.as_str().into()))
4067+ (range.from(), range.to(), Some(values.next().unwrap()))
3890 } else {
4068 } else {
3891 (range.from(), range.to(), None)
4069 (range.from(), range.to(), None)
@@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) {
4129 "keep:".into(),
4307 "keep:".into(),
4130 Some(reg),
4308 Some(reg),
4309+ |_input: &str| Vec::new(),
4131 move |view, doc, regex, event| {
4310 move |view, doc, regex, event| {
4132 if event != PromptEvent::Update {
4311 if event != PromptEvent::Update {
@@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) {
4228 iter.reverse();
4407 iter.reverse();
4229 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4408 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4230 let start_offset = cursor.saturating_sub(offset);
4409 let start_offset = cursor.saturating_sub(offset);
4410+ let prefix = text.slice(start_offset..cursor).to_string();
4232 cx.callback(
4412 cx.callback(
@@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) {
4243− let items = match response {
4423+ let mut items = match response {
4244 Some(lsp::CompletionResponse::Array(items)) => items,
4424 Some(lsp::CompletionResponse::Array(items)) => items,
4245 // TODO: do something with is_incomplete
4425 // TODO: do something with is_incomplete
4246 Some(lsp::CompletionResponse::List(lsp::CompletionList {
4426 Some(lsp::CompletionResponse::List(lsp::CompletionList {
@@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) {
4250 None => Vec::new(),
4430 None => Vec::new(),
4433+ if !prefix.is_empty() {
4439+ .unwrap_or(&item.label)
4440+ .starts_with(&prefix)
4253 if items.is_empty() {
4445 if items.is_empty() {
4254 // editor.set_error("No completion available".to_string());
4446 // editor.set_error("No completion available".to_string());
@@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) {
4401// tree sitter node selection
4593// tree sitter node selection
4403fn expand_selection(cx: &mut Context) {
4595fn expand_selection(cx: &mut Context) {
4596+ let motion = |editor: &mut Editor| {
4404− let (view, doc) = current!(cx.editor);
4597+ let (view, doc) = current!(editor);
4406− if let Some(syntax) = doc.syntax() {
4599+ if let Some(syntax) = doc.syntax() {
4407− let text = doc.text().slice(..);
4600+ let text = doc.text().slice(..);
4408− let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4601+ let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4409− doc.set_selection(view.id, selection);
4602+ doc.set_selection(view.id, selection);
4605+ motion(&mut cx.editor);
4606+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
4413fn match_brackets(cx: &mut Context) {
4609fn match_brackets(cx: &mut Context) {
4414 let (view, doc) = current!(cx.editor);
4610 let (view, doc) = current!(cx.editor);
4416 if let Some(syntax) = doc.syntax() {
4612 if let Some(syntax) = doc.syntax() {
4418− .selection(view.id)
4420− .cursor(doc.text().slice(..));
4613+ let text = doc.text().slice(..);
4614+ let selection = doc.selection(view.id).clone().transform(|range| {
4421− if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
4615+ if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
4422− let selection = Selection::point(pos);
4616+ range.put_cursor(text, pos, doc.mode == Mode::Select)
4423− doc.set_selection(view.id, selection);
4621+ doc.set_selection(view.id, selection);
@@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) {
4430fn jump_forward(cx: &mut Context) {
4627fn jump_forward(cx: &mut Context) {
4431 let count = cx.count();
4628 let count = cx.count();
4432− let (view, _doc) = current!(cx.editor);
4629+ let view = view_mut!(cx.editor);
4434 if let Some((id, selection)) = view.jumps.forward(count) {
4631 if let Some((id, selection)) = view.jumps.forward(count) {
4435 view.doc = *id;
4632 view.doc = *id;
@@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) {
4463 cx.editor.focus_next()
4660 cx.editor.focus_next()
4663+fn jump_view_right(cx: &mut Context) {
4664+ cx.editor.focus_right()
4667+fn jump_view_left(cx: &mut Context) {
4668+ cx.editor.focus_left()
4671+fn jump_view_up(cx: &mut Context) {
4672+ cx.editor.focus_up()
4675+fn jump_view_down(cx: &mut Context) {
4676+ cx.editor.focus_down()
4466// split helper, clear it later
4679// split helper, clear it later
4467fn split(cx: &mut Context, action: Action) {
4680fn split(cx: &mut Context, action: Action) {
4468 let (view, doc) = current!(cx.editor);
4681 let (view, doc) = current!(cx.editor);
@@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
4552 let count = cx.count();
4765 let count = cx.count();
4553 cx.on_next_key(move |cx, event| {
4766 cx.on_next_key(move |cx, event| {
4554 if let Some(ch) = event.char() {
4767 if let Some(ch) = event.char() {
4768+ let textobject = move |editor: &mut Editor| {
4555− let (view, doc) = current!(cx.editor);
4769+ let (view, doc) = current!(editor);
4556− let text = doc.text().slice(..);
4770+ let text = doc.text().slice(..);
4772+ let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4773+ let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4775+ None => return range,
4777+ textobject::textobject_treesitter(
4782+ syntax.tree().root_node(),
4558− let selection = doc.selection(view.id).clone().transform(|range| {
4788+ let selection = doc.selection(view.id).clone().transform(|range| {
4559− match ch {
4789+ match ch {
4560− 'w' => textobject::textobject_word(text, range, objtype, count),
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),
4561− // TODO: cancel new ranges if inconsistent surround matches across lines
4794+ // TODO: cancel new ranges if inconsistent surround matches across lines
4562− ch if !ch.is_ascii_alphanumeric() => {
4795+ ch if !ch.is_ascii_alphanumeric() => {
4563− textobject::textobject_surround(text, range, objtype, ch, count)
4796+ textobject::textobject_surround(text, range, objtype, ch, count)
4568− doc.set_selection(view.id, selection);
4801+ doc.set_selection(view.id, selection);
4803+ textobject(&mut cx.editor);
4804+ cx.editor.last_motion = Some(Motion(Box::new(textobject)));
@@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) {
4577 let selection = doc.selection(view.id);
4813 let selection = doc.selection(view.id);
4578 let (open, close) = surround::get_pair(ch);
4814 let (open, close) = surround::get_pair(ch);
4580− let mut changes = Vec::new();
4816+ let mut changes = Vec::with_capacity(selection.len() * 2);
4581 for range in selection.iter() {
4817 for range in selection.iter() {
4582 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
4818 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
4583 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
4819 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));