@@ -13,7 +13,6 @@ use helix_core::{
13 numbers::NumberIncrementor,
13 numbers::NumberIncrementor,
14 object, pos_at_coords,
14 object, pos_at_coords,
15 regex::{self, Regex, RegexBuilder},
15 regex::{self, Regex, RegexBuilder},
17 search, selection, surround, textobject,
16 search, selection, surround, textobject,
18 unicode::width::UnicodeWidthChar,
17 unicode::width::UnicodeWidthChar,
19 LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
18 LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
@@ -236,7 +235,9 @@ impl Command {
236 extend_line, "Select current line, if already selected, extend to next line",
235 extend_line, "Select current line, if already selected, extend to next line",
237 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
236 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
238 delete_selection, "Delete selection",
237 delete_selection, "Delete selection",
238+ delete_selection_noyank, "Delete selection, without yanking",
239 change_selection, "Change selection (delete and enter insert mode)",
239 change_selection, "Change selection (delete and enter insert mode)",
240+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
240 collapse_selection, "Collapse selection onto a single cursor",
241 collapse_selection, "Collapse selection onto a single cursor",
241 flip_selections, "Flip selection cursor and anchor",
242 flip_selections, "Flip selection cursor and anchor",
242 insert_mode, "Insert before selection",
243 insert_mode, "Insert before selection",
@@ -262,6 +263,9 @@ impl Command {
262 goto_implementation, "Goto implementation",
263 goto_implementation, "Goto implementation",
263 goto_file_start, "Goto file start/line",
264 goto_file_start, "Goto file start/line",
264 goto_file_end, "Goto file end",
265 goto_file_end, "Goto file end",
266+ goto_file, "Goto files in the selection",
267+ goto_file_hsplit, "Goto files in the selection in horizontal splits",
268+ goto_file_vsplit, "Goto files in the selection in vertical splits",
265 goto_reference, "Goto references",
269 goto_reference, "Goto references",
266 goto_window_top, "Goto window top",
270 goto_window_top, "Goto window top",
267 goto_window_middle, "Goto window middle",
271 goto_window_middle, "Goto window middle",
@@ -318,6 +322,7 @@ impl Command {
318 join_selections, "Join lines inside selection",
322 join_selections, "Join lines inside selection",
319 keep_selections, "Keep selections matching regex",
323 keep_selections, "Keep selections matching regex",
320 remove_selections, "Remove selections matching regex",
324 remove_selections, "Remove selections matching regex",
325+ align_selections, "Align selections in column",
321 keep_primary_selection, "Keep primary selection",
326 keep_primary_selection, "Keep primary selection",
322 remove_primary_selection, "Remove primary selection",
327 remove_primary_selection, "Remove primary selection",
323 completion, "Invoke completion popup",
328 completion, "Invoke completion popup",
@@ -676,11 +681,83 @@ fn trim_selections(cx: &mut Context) {
684+// align text in selection
685+fn align_selections(cx: &mut Context) {
686+ let align_style = cx.count();
687+ if align_style > 3 {
688+ cx.editor.set_error(
689+ "align only accept 1,2,3 as count to set left/center/right align".to_string(),
694+ let (view, doc) = current!(cx.editor);
695+ let text = doc.text().slice(..);
696+ let selection = doc.selection(view.id);
697+ let mut column_widths = vec![];
698+ let mut last_line = text.len_lines();
700+ // first of all, we need compute all column's width, let use max width of the selections in a column
701+ for sel in selection {
702+ let (l1, l2) = sel.line_range(text);
705+ .set_error("align cannot work with multi line selections".to_string());
708+ // if the selection is not in the same line with last selection, we set the column to 0
709+ column = if l1 != last_line { 0 } else { column + 1 };
712+ if column < column_widths.len() {
713+ if sel.to() - sel.from() > column_widths[column] {
714+ column_widths[column] = sel.to() - sel.from();
717+ // a new column, current selection width is the temp width of the column
718+ column_widths.push(sel.to() - sel.from());
721+ last_line = text.len_lines();
722+ // once we get the with of each column, we transform each selection with to it's column width based on the align style
723+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
724+ let l = range.cursor_line(text);
725+ column = if l != last_line { 0 } else { column + 1 };
732+ align_fragment_to_width(&range.fragment(text), column_widths[column], align_style)
738+ doc.apply(&transaction, view.id);
739+ doc.append_changes_to_history(view.id);
742+fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
743+ let trimed = fragment.trim_matches(|c| c == ' ');
744+ let mut s = " ".repeat(width - trimed.chars().count());
746+ 1 => s.insert_str(0, trimed), // left align
747+ 2 => s.insert_str(s.len() / 2, trimed), // center align
748+ 3 => s.push_str(trimed), // right align
749+ n => unimplemented!("{}", n),
679fn goto_window(cx: &mut Context, align: Align) {
754fn goto_window(cx: &mut Context, align: Align) {
755+ let count = cx.count() - 1;
680 let (view, doc) = current!(cx.editor);
756 let (view, doc) = current!(cx.editor);
682 let height = view.inner_area().height as usize;
758 let height = view.inner_area().height as usize;
760+ // respect user given count if any
684 // - 1 so we have at least one gap in the middle.
761 // - 1 so we have at least one gap in the middle.
685 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
762 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
686 // as we type
763 // as we type
@@ -689,11 +766,12 @@ fn goto_window(cx: &mut Context, align: Align) {
689 let last_line = view.last_line(doc);
766 let last_line = view.last_line(doc);
691 let line = match align {
768 let line = match align {
692− Align::Top => (view.offset.row + scrolloff),
769+ Align::Top => (view.offset.row + scrolloff + count),
693− Align::Center => (view.offset.row + (height / 2)),
770+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
694− Align::Bottom => last_line.saturating_sub(scrolloff),
771+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
696− .min(last_line.saturating_sub(scrolloff));
773+ .min(last_line.saturating_sub(scrolloff))
774+ .max(view.offset.row + scrolloff);
698 let pos = doc.text().line_to_char(line);
776 let pos = doc.text().line_to_char(line);
@@ -782,6 +860,49 @@ fn goto_file_end(cx: &mut Context) {
782 doc.set_selection(view.id, selection);
860 doc.set_selection(view.id, selection);
863+fn goto_file(cx: &mut Context) {
864+ goto_file_impl(cx, Action::Replace);
867+fn goto_file_hsplit(cx: &mut Context) {
868+ goto_file_impl(cx, Action::HorizontalSplit);
871+fn goto_file_vsplit(cx: &mut Context) {
872+ goto_file_impl(cx, Action::VerticalSplit);
875+fn goto_file_impl(cx: &mut Context, action: Action) {
876+ let (view, doc) = current_ref!(cx.editor);
877+ let text = doc.text();
878+ let selections = doc.selection(view.id);
879+ let mut paths: Vec<_> = selections
881+ .map(|r| text.slice(r.from()..r.to()).to_string())
883+ let primary = selections.primary();
884+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
885+ let current_word = movement::move_next_long_word_start(
887+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
892+ text.slice(current_word.from()..current_word.to())
899+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
900+ cx.editor.set_error(format!("Open file failed: {:?}", e));
785fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
906fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
787 F: Fn(RopeSlice, Range, usize) -> Range,
908 F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1459,6 +1580,7 @@ fn global_search(cx: &mut Context) {
1459 let (all_matches_sx, all_matches_rx) =
1580 let (all_matches_sx, all_matches_rx) =
1460 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1581 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1461 let smart_case = cx.editor.config.smart_case;
1582 let smart_case = cx.editor.config.smart_case;
1583+ let file_picker_config = cx.editor.config.file_picker.clone();
1463 let completions = search_completions(cx, None);
1585 let completions = search_completions(cx, None);
1464 let prompt = ui::regex_prompt(
1586 let prompt = ui::regex_prompt(
@@ -1487,41 +1609,55 @@ fn global_search(cx: &mut Context) {
1488 let search_root = std::env::current_dir()
1610 let search_root = std::env::current_dir()
1489 .expect("Global search error: Failed to get current dir");
1611 .expect("Global search error: Failed to get current dir");
1490− WalkBuilder::new(search_root).build_parallel().run(|| {
1612+ WalkBuilder::new(search_root)
1613+ .hidden(file_picker_config.hidden)
1614+ .parents(file_picker_config.parents)
1615+ .ignore(file_picker_config.ignore)
1616+ .git_ignore(file_picker_config.git_ignore)
1617+ .git_global(file_picker_config.git_global)
1618+ .git_exclude(file_picker_config.git_exclude)
1619+ .max_depth(file_picker_config.max_depth)
1491− let mut searcher_cl = searcher.clone();
1622+ let mut searcher_cl = searcher.clone();
1492− let matcher_cl = matcher.clone();
1623+ let matcher_cl = matcher.clone();
1493− let all_matches_sx_cl = all_matches_sx.clone();
1624+ let all_matches_sx_cl = all_matches_sx.clone();
1494− Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1625+ Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1495− let dent = match dent {
1626+ let dent = match dent {
1496− Ok(dent) => dent,
1627+ Ok(dent) => dent,
1497− Err(_) => return WalkState::Continue,
1628+ Err(_) => return WalkState::Continue,
1500− match dent.file_type() {
1631+ match dent.file_type() {
1501− Some(fi) => {
1632+ Some(fi) => {
1502− if !fi.is_file() {
1633+ if !fi.is_file() {
1503− return WalkState::Continue;
1634+ return WalkState::Continue;
1637+ None => return WalkState::Continue,
1506− None => return WalkState::Continue,
1509− let result_sink = sinks::UTF8(|line_num, _| {
1640+ let result_sink = sinks::UTF8(|line_num, _| {
1510− match all_matches_sx_cl
1641+ match all_matches_sx_cl
1511− .send((line_num as usize - 1, dent.path().to_path_buf()))
1642+ .send((line_num as usize - 1, dent.path().to_path_buf()))
1513− Ok(_) => Ok(true),
1644+ Ok(_) => Ok(true),
1514− Err(_) => Ok(false),
1645+ Err(_) => Ok(false),
1649+ searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1651+ if let Err(err) = result {
1653+ "Global search error: {}, {}",
1654+ dent.path().display(),
1517− let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1519− if let Err(err) = result {
1520− log::error!("Global search error: {}, {}", dent.path().display(), err);
1522− WalkState::Continue
1658+ WalkState::Continue
1525 } else {
1661 } else {
1526 // Otherwise do nothing
1662 // Otherwise do nothing
1527 // log::warn!("Global Search Invalid Pattern")
1663 // log::warn!("Global Search Invalid Pattern")
@@ -1626,19 +1762,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
1629−fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1770+fn delete_selection_impl(cx: &mut Context, op: Operation) {
1771+ let (view, doc) = current!(cx.editor);
1630 let text = doc.text().slice(..);
1773 let text = doc.text().slice(..);
1631− let selection = doc.selection(view_id);
1774+ let selection = doc.selection(view.id);
1776+ if cx.register != Some('_') {
1633− // first yank the selection
1777+ // first yank the selection
1634− let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1778+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1779+ let reg_name = cx.register.unwrap_or('"');
1780+ let registers = &mut cx.editor.registers;
1781+ let reg = registers.get_mut(reg_name);
1635− reg.write(values);
1782+ reg.write(values);
1637 // then delete
1785 // then delete
1638 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1786 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1639 (range.from(), range.to(), None)
1787 (range.from(), range.to(), None)
1641− doc.apply(&transaction, view_id);
1789+ doc.apply(&transaction, view.id);
1792+ Operation::Delete => {
1793+ doc.append_changes_to_history(view.id);
1794+ // exit select mode, if currently in select mode
1795+ exit_select_mode(cx);
1797+ Operation::Change => {
1798+ enter_insert_mode(doc);
1644#[inline]
1803#[inline]
@@ -1653,25 +1812,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
1655fn delete_selection(cx: &mut Context) {
1814fn delete_selection(cx: &mut Context) {
1656− let reg_name = cx.register.unwrap_or('"');
1657− let (view, doc) = current!(cx.editor);
1658− let registers = &mut cx.editor.registers;
1659− let reg = registers.get_mut(reg_name);
1660− delete_selection_impl(reg, doc, view.id);
1815+ delete_selection_impl(cx, Operation::Delete);
1662− doc.append_changes_to_history(view.id);
1664− // exit select mode, if currently in select mode
1818+fn delete_selection_noyank(cx: &mut Context) {
1665− exit_select_mode(cx);
1819+ cx.register = Some('_');
1820+ delete_selection_impl(cx, Operation::Delete);
1668fn change_selection(cx: &mut Context) {
1823fn change_selection(cx: &mut Context) {
1669− let reg_name = cx.register.unwrap_or('"');
1824+ delete_selection_impl(cx, Operation::Change);
1670− let (view, doc) = current!(cx.editor);
1825+}
1671− let registers = &mut cx.editor.registers;
1826+
1672− let reg = registers.get_mut(reg_name);
1827+fn change_selection_noyank(cx: &mut Context) {
1828+ cx.register = Some('_');
1673− delete_selection_impl(reg, doc, view.id);
1829+ delete_selection_impl(cx, Operation::Change);
1674− enter_insert_mode(doc);
1677fn collapse_selection(cx: &mut Context) {
1832fn collapse_selection(cx: &mut Context) {
@@ -1820,7 +1975,7 @@ mod cmd {
1820 let jobs = &mut cx.jobs;
1975 let jobs = &mut cx.jobs;
1821 let (_, doc) = current!(cx.editor);
1976 let (_, doc) = current!(cx.editor);
1823− if let Some(path) = path {
1978+ if let Some(ref path) = path {
1824 doc.set_path(Some(path.as_ref()))
1979 doc.set_path(Some(path.as_ref()))
1825 .context("invalid filepath")?;
1980 .context("invalid filepath")?;
@@ -1840,6 +1995,11 @@ mod cmd {
1841 let future = doc.format_and_save(fmt);
1996 let future = doc.format_and_save(fmt);
1842 cx.jobs.add(Job::new(future).wait_before_exiting());
1997 cx.jobs.add(Job::new(future).wait_before_exiting());
1999+ if path.is_some() {
2001+ let _ = cx.editor.refresh_language_server(id);
@@ -2466,6 +2626,26 @@ mod cmd {
2629+ pub(super) fn goto_line_number(
2630+ cx: &mut compositor::Context,
2632+ _event: PromptEvent,
2633+ ) -> anyhow::Result<()> {
2634+ if args.is_empty() {
2635+ bail!("Line number required");
2638+ let line = args[0].parse::<usize>()?;
2640+ goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
2642+ let (view, doc) = current!(cx.editor);
2644+ view.ensure_cursor_in_view(doc, line);
2469 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2649 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2470 TypableCommand {
2650 TypableCommand {
2471 name: "quit",
2651 name: "quit",
@@ -2768,6 +2948,13 @@ mod cmd {
2768 fun: tutor,
2948 fun: tutor,
2769 completer: None,
2949 completer: None,
2954+ doc: "Go to line number.",
2955+ fun: goto_line_number,
2773 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2960 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2830,6 +3017,15 @@ fn command_mode(cx: &mut Context) {
3020+ // If command is numeric, interpret as line number and go there.
3021+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
3022+ if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) {
3023+ cx.editor.set_error(format!("{}", e));
3028+ // Handle typable commands
2833 if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
3029 if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2834 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
3030 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2835 cx.editor.set_error(format!("{}", e));
3031 cx.editor.set_error(format!("{}", e));
@@ -2855,7 +3051,7 @@ fn command_mode(cx: &mut Context) {
2856fn file_picker(cx: &mut Context) {
3052fn file_picker(cx: &mut Context) {
2857 let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
3053 let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
2858− let picker = ui::file_picker(root);
3054+ let picker = ui::file_picker(root, &cx.editor.config);
2859 cx.push_layer(Box::new(picker));
3055 cx.push_layer(Box::new(picker));
@@ -3463,10 +3659,14 @@ fn push_jump(editor: &mut Editor) {
3465fn goto_line(cx: &mut Context) {
3661fn goto_line(cx: &mut Context) {
3466− if let Some(count) = cx.count {
3662+ goto_line_impl(&mut cx.editor, cx.count)
3467− push_jump(cx.editor);
3663+}
3665+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
3666+ if let Some(count) = count {
3469− let (view, doc) = current!(cx.editor);
3669+ let (view, doc) = current!(editor);
3470 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
3670 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
3471 // If the last line is blank, don't jump to it.
3671 // If the last line is blank, don't jump to it.
3472 doc.text().len_lines().saturating_sub(2)
3672 doc.text().len_lines().saturating_sub(2)
@@ -5065,7 +5265,9 @@ fn match_brackets(cx: &mut Context) {
5065 if let Some(syntax) = doc.syntax() {
5265 if let Some(syntax) = doc.syntax() {
5066 let text = doc.text().slice(..);
5266 let text = doc.text().slice(..);
5067 let selection = doc.selection(view.id).clone().transform(|range| {
5267 let selection = doc.selection(view.id).clone().transform(|range| {
5068− if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
5269+ match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
5069 range.put_cursor(text, pos, doc.mode == Mode::Select)
5271 range.put_cursor(text, pos, doc.mode == Mode::Select)
5070 } else {
5272 } else {