@@ -3,21 +3,25 @@ pub(crate) mod dap;
33pub use dap::*;
44
55use helix_core::{
6− comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
6+ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
7+ history::UndoKind,
8+ indent,
79 indent::IndentStyle,
810 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
911 match_brackets,
1012 movement::{self, Direction},
13+ numbers::NumberIncrementor,
1114 object, pos_at_coords,
1215 regex::{self, Regex, RegexBuilder},
1316 register::Register,
14− search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
15− RopeSlice, Selection, SmallVec, Tendril, Transaction,
17+ search, selection, surround, textobject,
18+ unicode::width::UnicodeWidthChar,
19+ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
20+ Transaction,
1621};
17−
1822use helix_view::{
1923 clipboard::ClipboardType,
20− document::Mode,
24+ document::{Mode, SCRATCH_BUFFER_NAME},
2125 editor::{Action, Motion},
2226 input::KeyEvent,
2327 keyboard::KeyCode,
@@ -27,7 +31,7 @@ use helix_view::{
2731
2832use anyhow::{anyhow, bail, Context as _};
2933use helix_lsp::{
30− lsp,
34+ block_on, lsp,
3135 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
3236 OffsetEncoding,
3337};
@@ -189,6 +193,7 @@ impl Command {
189193 copy_selection_on_prev_line, "Copy selection on previous line",
190194 move_next_word_start, "Move to beginning of next word",
191195 move_prev_word_start, "Move to beginning of previous word",
196+ move_prev_word_end, "Move to end of previous word",
192197 move_next_word_end, "Move to end of next word",
193198 move_next_long_word_start, "Move to beginning of next long word",
194199 move_prev_long_word_start, "Move to beginning of previous long word",
@@ -241,6 +246,7 @@ impl Command {
241246 code_action, "Perform code action",
242247 buffer_picker, "Open buffer picker",
243248 symbol_picker, "Open symbol picker",
249+ workspace_symbol_picker, "Open workspace symbol picker",
244250 last_picker, "Open last picker",
245251 prepend_to_line, "Insert at start of line",
246252 append_to_line, "Insert at end of line",
@@ -261,6 +267,7 @@ impl Command {
261267 goto_window_middle, "Goto window middle",
262268 goto_window_bottom, "Goto window bottom",
263269 goto_last_accessed_file, "Goto last accessed file",
270+ goto_last_modification, "Goto last modification",
264271 goto_line, "Goto line",
265272 goto_last_line, "Goto last line",
266273 goto_first_diag, "Goto first diagnostic",
@@ -274,6 +281,7 @@ impl Command {
274281 // TODO: different description ?
275282 goto_line_end_newline, "Goto line end",
276283 goto_first_nonwhitespace, "Goto first non-blank in line",
284+ trim_selections, "Trim whitespace from selections",
277285 extend_to_line_start, "Extend to line start",
278286 extend_to_line_end, "Extend to line end",
279287 extend_to_line_end_newline, "Extend to line end",
@@ -283,8 +291,13 @@ impl Command {
283291 delete_char_backward, "Delete previous char",
284292 delete_char_forward, "Delete next char",
285293 delete_word_backward, "Delete previous word",
294+ delete_word_forward, "Delete next word",
295+ kill_to_line_start, "Delete content till the start of the line",
296+ kill_to_line_end, "Delete content till the end of the line",
286297 undo, "Undo change",
287298 redo, "Redo change",
299+ earlier, "Move backward in history",
300+ later, "Move forward in history",
288301 yank, "Yank selection",
289302 yank_joined_to_clipboard, "Join and yank selections to clipboard",
290303 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -304,6 +317,7 @@ impl Command {
304317 format_selections, "Format selection",
305318 join_selections, "Join lines inside selection",
306319 keep_selections, "Keep selections matching regex",
320+ remove_selections, "Remove selections matching regex",
307321 keep_primary_selection, "Keep primary selection",
308322 remove_primary_selection, "Remove primary selection",
309323 completion, "Invoke completion popup",
@@ -324,7 +338,9 @@ impl Command {
324338 hsplit, "Horizontal bottom split",
325339 vsplit, "Vertical right split",
326340 wclose, "Close window",
341+ wonly, "Current window only",
327342 select_register, "Select register",
343+ insert_register, "Insert register",
328344 align_view_middle, "Align view middle",
329345 align_view_top, "Align view top",
330346 align_view_center, "Align view center",
@@ -358,6 +374,9 @@ impl Command {
358374 shell_append_output, "Append output of shell command after each selection",
359375 shell_keep_pipe, "Filter selections with shell predicate",
360376 suspend, "Suspend",
377+ rename_symbol, "Rename symbol",
378+ increment, "Increment",
379+ decrement, "Decrement",
361380 );
362381}
363382
@@ -581,6 +600,29 @@ fn extend_to_line_start(cx: &mut Context) {
581600 goto_line_start_impl(view, doc, Movement::Extend)
582601}
583602
603+fn kill_to_line_start(cx: &mut Context) {
604+ let (view, doc) = current!(cx.editor);
605+ let text = doc.text().slice(..);
606+
607+ let selection = doc.selection(view.id).clone().transform(|range| {
608+ let line = range.cursor_line(text);
609+ range.put_cursor(text, text.line_to_char(line), true)
610+ });
611+ delete_selection_insert_mode(doc, view, &selection);
612+}
613+
614+fn kill_to_line_end(cx: &mut Context) {
615+ let (view, doc) = current!(cx.editor);
616+ let text = doc.text().slice(..);
617+
618+ let selection = doc.selection(view.id).clone().transform(|range| {
619+ let line = range.cursor_line(text);
620+ let pos = line_end_char_index(&text, line);
621+ range.put_cursor(text, pos, true)
622+ });
623+ delete_selection_insert_mode(doc, view, &selection);
624+}
625+
584626fn goto_first_nonwhitespace(cx: &mut Context) {
585627 let (view, doc) = current!(cx.editor);
586628 let text = doc.text().slice(..);
@@ -598,6 +640,42 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
598640 doc.set_selection(view.id, selection);
599641}
600642
643+fn trim_selections(cx: &mut Context) {
644+ let (view, doc) = current!(cx.editor);
645+ let text = doc.text().slice(..);
646+
647+ let ranges: SmallVec<[Range; 1]> = doc
648+ .selection(view.id)
649+ .iter()
650+ .filter_map(|range| {
651+ if range.is_empty() || range.fragment(text).chars().all(|ch| ch.is_whitespace()) {
652+ return None;
653+ }
654+ let mut start = range.from();
655+ let mut end = range.to();
656+ start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start);
657+ end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end);
658+ if range.anchor < range.head {
659+ Some(Range::new(start, end))
660+ } else {
661+ Some(Range::new(end, start))
662+ }
663+ })
664+ .collect();
665+
666+ if !ranges.is_empty() {
667+ let primary = doc.selection(view.id).primary();
668+ let idx = ranges
669+ .iter()
670+ .position(|range| range.overlaps(&primary))
671+ .unwrap_or(ranges.len() - 1);
672+ doc.set_selection(view.id, Selection::new(ranges, idx));
673+ } else {
674+ collapse_selection(cx);
675+ keep_primary_selection(cx);
676+ };
677+}
678+
601679fn goto_window(cx: &mut Context, align: Align) {
602680 let (view, doc) = current!(cx.editor);
603681
@@ -657,6 +735,10 @@ fn move_prev_word_start(cx: &mut Context) {
657735 move_word_impl(cx, movement::move_prev_word_start)
658736}
659737
738+fn move_prev_word_end(cx: &mut Context) {
739+ move_word_impl(cx, movement::move_prev_word_end)
740+}
741+
660742fn move_next_word_end(cx: &mut Context) {
661743 move_word_impl(cx, movement::move_next_word_end)
662744}
@@ -1193,6 +1275,7 @@ fn search_impl(
11931275 regex: &Regex,
11941276 movement: Movement,
11951277 direction: Direction,
1278+ scrolloff: usize,
11961279) {
11971280 let text = doc.text().slice(..);
11981281 let selection = doc.selection(view.id);
@@ -1251,7 +1334,11 @@ fn search_impl(
12511334 };
12521335
12531336 doc.set_selection(view.id, selection);
1254− align_view(doc, view, Align::Center);
1337+ if view.is_cursor_in_view(doc, 0) {
1338+ view.ensure_cursor_in_view(doc, scrolloff);
1339+ } else {
1340+ align_view(doc, view, Align::Center)
1341+ }
12551342 };
12561343}
12571344
@@ -1275,6 +1362,8 @@ fn rsearch(cx: &mut Context) {
12751362// TODO: use one function for search vs extend
12761363fn searcher(cx: &mut Context, direction: Direction) {
12771364 let reg = cx.register.unwrap_or('/');
1365+ let scrolloff = cx.editor.config.scrolloff;
1366+
12781367 let (_, doc) = current!(cx.editor);
12791368
12801369 // TODO: could probably share with select_on_matches?
@@ -1299,7 +1388,15 @@ fn searcher(cx: &mut Context, direction: Direction) {
12991388 if event != PromptEvent::Update {
13001389 return;
13011390 }
1302− search_impl(doc, view, &contents, ®ex, Movement::Move, direction);
1391+ search_impl(
1392+ doc,
1393+ view,
1394+ &contents,
1395+ ®ex,
1396+ Movement::Move,
1397+ direction,
1398+ scrolloff,
1399+ );
13031400 },
13041401 );
13051402
@@ -1307,6 +1404,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
13071404}
13081405
13091406fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1407+ let scrolloff = cx.editor.config.scrolloff;
13101408 let (view, doc) = current!(cx.editor);
13111409 let registers = &cx.editor.registers;
13121410 if let Some(query) = registers.read('/') {
@@ -1321,7 +1419,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
13211419 .case_insensitive(case_insensitive)
13221420 .build()
13231421 {
1324− search_impl(doc, view, &contents, ®ex, movement, direction);
1422+ search_impl(doc, view, &contents, ®ex, movement, direction, scrolloff);
13251423 } else {
13261424 // get around warning `mutable_borrow_reservation_conflict`
13271425 // which will be a hard error in the future
@@ -1365,7 +1463,7 @@ fn global_search(cx: &mut Context) {
13651463 let completions = search_completions(cx, None);
13661464 let prompt = ui::regex_prompt(
13671465 cx,
1368− "global search:".into(),
1466+ "global-search:".into(),
13691467 None,
13701468 move |input: &str| {
13711469 completions
@@ -1543,6 +1641,17 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
15431641 doc.apply(&transaction, view_id);
15441642}
15451643
1644+#[inline]
1645+fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) {
1646+ let view_id = view.id;
1647+
1648+ // then delete
1649+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1650+ (range.from(), range.to(), None)
1651+ });
1652+ doc.apply(&transaction, view_id);
1653+}
1654+
15461655fn delete_selection(cx: &mut Context) {
15471656 let reg_name = cx.register.unwrap_or('"');
15481657 let (view, doc) = current!(cx.editor);
@@ -1657,8 +1766,7 @@ mod cmd {
16571766 buffers_remaining_impl(cx.editor)?
16581767 }
16591768
1660− cx.editor
1661− .close(view!(cx.editor).id, /* close_buffer */ false);
1769+ cx.editor.close(view!(cx.editor).id);
16621770
16631771 Ok(())
16641772 }
@@ -1668,8 +1776,7 @@ mod cmd {
16681776 _args: &[&str],
16691777 _event: PromptEvent,
16701778 ) -> anyhow::Result<()> {
1671− cx.editor
1672− .close(view!(cx.editor).id, /* close_buffer */ false);
1779+ cx.editor.close(view!(cx.editor).id);
16731780
16741781 Ok(())
16751782 }
@@ -1679,11 +1786,30 @@ mod cmd {
16791786 args: &[&str],
16801787 _event: PromptEvent,
16811788 ) -> anyhow::Result<()> {
1682− use helix_core::path::expand_tilde;
16831789 let path = args.get(0).context("wrong argument count")?;
1684− let _ = cx
1685− .editor
1686− .open(expand_tilde(Path::new(path)), Action::Replace)?;
1790+ let _ = cx.editor.open(path.into(), Action::Replace)?;
1791+ Ok(())
1792+ }
1793+
1794+ fn buffer_close(
1795+ cx: &mut compositor::Context,
1796+ _args: &[&str],
1797+ _event: PromptEvent,
1798+ ) -> anyhow::Result<()> {
1799+ let view = view!(cx.editor);
1800+ let doc_id = view.doc;
1801+ cx.editor.close_document(doc_id, false)?;
1802+ Ok(())
1803+ }
1804+
1805+ fn force_buffer_close(
1806+ cx: &mut compositor::Context,
1807+ _args: &[&str],
1808+ _event: PromptEvent,
1809+ ) -> anyhow::Result<()> {
1810+ let view = view!(cx.editor);
1811+ let doc_id = view.doc;
1812+ cx.editor.close_document(doc_id, true)?;
16871813 Ok(())
16881814 }
16891815
@@ -1838,13 +1964,13 @@ mod cmd {
18381964 args: &[&str],
18391965 _event: PromptEvent,
18401966 ) -> anyhow::Result<()> {
1841− let uk = args
1842− .join(" ")
1843− .parse::<helix_core::history::UndoKind>()
1844− .map_err(|s| anyhow!(s))?;
1967+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
18451968
18461969 let (view, doc) = current!(cx.editor);
1847− doc.earlier(view.id, uk);
1970+ let success = doc.earlier(view.id, uk);
1971+ if !success {
1972+ cx.editor.set_status("Already at oldest change".to_owned());
1973+ }
18481974
18491975 Ok(())
18501976 }
@@ -1854,12 +1980,12 @@ mod cmd {
18541980 args: &[&str],
18551981 _event: PromptEvent,
18561982 ) -> anyhow::Result<()> {
1857− let uk = args
1858− .join(" ")
1859− .parse::<helix_core::history::UndoKind>()
1860− .map_err(|s| anyhow!(s))?;
1983+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
18611984 let (view, doc) = current!(cx.editor);
1862− doc.later(view.id, uk);
1985+ let success = doc.later(view.id, uk);
1986+ if !success {
1987+ cx.editor.set_status("Already at newest change".to_owned());
1988+ }
18631989
18641990 Ok(())
18651991 }
@@ -1891,7 +2017,7 @@ mod cmd {
18912017 .map(|doc| {
18922018 doc.relative_path()
18932019 .map(|path| path.to_string_lossy().to_string())
1894− .unwrap_or_else(|| "[scratch]".into())
2020+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
18952021 })
18962022 .collect();
18972023 if !modified.is_empty() {
@@ -1933,7 +2059,7 @@ mod cmd {
19332059 // close all views
19342060 let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
19352061 for view_id in views {
1936− cx.editor.close(view_id, false);
2062+ cx.editor.close(view_id);
19372063 }
19382064 }
19392065
@@ -1977,7 +2103,7 @@ mod cmd {
19772103 // close all views
19782104 let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
19792105 for view_id in views {
1980− editor.close(view_id, false);
2106+ editor.close(view_id);
19812107 }
19822108
19832109 Ok(())
@@ -1999,6 +2125,25 @@ mod cmd {
19992125 quit_all_impl(&mut cx.editor, args, event, true)
20002126 }
20012127
2128+ fn cquit(
2129+ cx: &mut compositor::Context,
2130+ args: &[&str],
2131+ _event: PromptEvent,
2132+ ) -> anyhow::Result<()> {
2133+ let exit_code = args
2134+ .first()
2135+ .and_then(|code| code.parse::<i32>().ok())
2136+ .unwrap_or(1);
2137+ cx.editor.exit_code = exit_code;
2138+
2139+ let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
2140+ for view_id in views {
2141+ cx.editor.close(view_id);
2142+ }
2143+
2144+ Ok(())
2145+ }
2146+
20022147 fn theme(
20032148 cx: &mut compositor::Context,
20042149 args: &[&str],
@@ -2241,7 +2386,6 @@ mod cmd {
22412386 args: &[&str],
22422387 _event: PromptEvent,
22432388 ) -> anyhow::Result<()> {
2244− use helix_lsp::block_on;
22452389 if let Some(debugger) = cx.editor.debugger.as_mut() {
22462390 let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
22472391 (Some(frame), Some(thread_id)) => (frame, thread_id),
@@ -2344,6 +2488,20 @@ mod cmd {
23442488 fun: open,
23452489 completer: Some(completers::filename),
23462490 },
2491+ TypableCommand {
2492+ name: "buffer-close",
2493+ aliases: &["bc", "bclose"],
2494+ doc: "Close the current buffer.",
2495+ fun: buffer_close,
2496+ completer: None, // FIXME: buffer completer
2497+ },
2498+ TypableCommand {
2499+ name: "buffer-close!",
2500+ aliases: &["bc!", "bclose!"],
2501+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
2502+ fun: force_buffer_close,
2503+ completer: None, // FIXME: buffer completer
2504+ },
23472505 TypableCommand {
23482506 name: "write",
23492507 aliases: &["w"],
@@ -2442,6 +2600,13 @@ mod cmd {
24422600 fun: force_quit_all,
24432601 completer: None,
24442602 },
2603+ TypableCommand {
2604+ name: "cquit",
2605+ aliases: &["cq"],
2606+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2607+ fun: cquit,
2608+ completer: None,
2609+ },
24452610 TypableCommand {
24462611 name: "theme",
24472612 aliases: &[],
@@ -2698,36 +2863,66 @@ fn file_picker(cx: &mut Context) {
26982863fn buffer_picker(cx: &mut Context) {
26992864 let current = view!(cx.editor).doc;
27002865
2866+ struct BufferMeta {
2867+ id: DocumentId,
2868+ path: Option<PathBuf>,
2869+ is_modified: bool,
2870+ is_current: bool,
2871+ }
2872+
2873+ impl BufferMeta {
2874+ fn format(&self) -> Cow<str> {
2875+ let path = self
2876+ .path
2877+ .as_deref()
2878+ .map(helix_core::path::get_relative_path);
2879+ let path = match path.as_deref().and_then(Path::to_str) {
2880+ Some(path) => path,
2881+ None => return Cow::Borrowed(SCRATCH_BUFFER_NAME),
2882+ };
2883+
2884+ let mut flags = Vec::new();
2885+ if self.is_modified {
2886+ flags.push("+");
2887+ }
2888+ if self.is_current {
2889+ flags.push("*");
2890+ }
2891+
2892+ let flag = if flags.is_empty() {
2893+ "".into()
2894+ } else {
2895+ format!(" ({})", flags.join(""))
2896+ };
2897+ Cow::Owned(format!("{}{}", path, flag))
2898+ }
2899+ }
2900+
2901+ let new_meta = |doc: &Document| BufferMeta {
2902+ id: doc.id(),
2903+ path: doc.path().cloned(),
2904+ is_modified: doc.is_modified(),
2905+ is_current: doc.id() == current,
2906+ };
2907+
27012908 let picker = FilePicker::new(
27022909 cx.editor
27032910 .documents
27042911 .iter()
2705− .map(|(id, doc)| (*id, doc.path().cloned()))
2912+ .map(|(_, doc)| new_meta(doc))
27062913 .collect(),
2707− move |(id, path): &(DocumentId, Option<PathBuf>)| {
2708− let path = path.as_deref().map(helix_core::path::get_relative_path);
2709− match path.as_ref().and_then(|path| path.to_str()) {
2710− Some(path) => {
2711− if *id == current {
2712− format!("{} (*)", &path).into()
2713− } else {
2714− path.to_owned().into()
2715− }
2716− }
2717− None => "[scratch buffer]".into(),
2718− }
2719− },
2720− |cx, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
2721− cx.editor.switch(*id, Action::Replace);
2914+ BufferMeta::format,
2915+ |cx, meta, _action| {
2916+ cx.editor.switch(meta.id, Action::Replace);
27222917 },
2723− |editor, (id, path)| {
2724− let doc = &editor.documents.get(id)?;
2918+ |editor, meta| {
2919+ let doc = &editor.documents.get(&meta.id)?;
27252920 let &view_id = doc.selections().keys().next()?;
27262921 let line = doc
27272922 .selection(view_id)
27282923 .primary()
27292924 .cursor_line(doc.text().slice(..));
2730− Some((path.clone()?, Some((line, line))))
2925+ Some((meta.path.clone()?, Some((line, line))))
27312926 },
27322927 );
27332928 cx.push_layer(Box::new(picker));
@@ -2782,7 +2977,7 @@ fn symbol_picker(cx: &mut Context) {
27822977 }
27832978 };
27842979
2785− let picker = FilePicker::new(
2980+ let mut picker = FilePicker::new(
27862981 symbols,
27872982 |symbol| (&symbol.name).into(),
27882983 move |cx, symbol, _action| {
@@ -2807,6 +3002,69 @@ fn symbol_picker(cx: &mut Context) {
28073002 Some((path, line))
28083003 },
28093004 );
3005+ picker.truncate_start = false;
3006+ compositor.push(Box::new(picker))
3007+ }
3008+ },
3009+ )
3010+}
3011+
3012+fn workspace_symbol_picker(cx: &mut Context) {
3013+ let (_, doc) = current!(cx.editor);
3014+
3015+ let language_server = match doc.language_server() {
3016+ Some(language_server) => language_server,
3017+ None => return,
3018+ };
3019+ let offset_encoding = language_server.offset_encoding();
3020+
3021+ let future = language_server.workspace_symbols("".to_string());
3022+
3023+ let current_path = doc_mut!(cx.editor).path().cloned();
3024+ cx.callback(
3025+ future,
3026+ move |_editor: &mut Editor,
3027+ compositor: &mut Compositor,
3028+ response: Option<Vec<lsp::SymbolInformation>>| {
3029+ if let Some(symbols) = response {
3030+ let mut picker = FilePicker::new(
3031+ symbols,
3032+ move |symbol| {
3033+ let path = symbol.location.uri.to_file_path().unwrap();
3034+ if current_path.as_ref().map(|p| p == &path).unwrap_or(false) {
3035+ (&symbol.name).into()
3036+ } else {
3037+ let relative_path = helix_core::path::get_relative_path(path.as_path())
3038+ .to_str()
3039+ .unwrap()
3040+ .to_owned();
3041+ format!("{} ({})", &symbol.name, relative_path).into()
3042+ }
3043+ },
3044+ move |cx, symbol, action| {
3045+ let path = symbol.location.uri.to_file_path().unwrap();
3046+ cx.editor.open(path, action).expect("editor.open failed");
3047+ let (view, doc) = current!(cx.editor);
3048+
3049+ if let Some(range) =
3050+ lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
3051+ {
3052+ // we flip the range so that the cursor sits on the start of the symbol
3053+ // (for example start of the function).
3054+ doc.set_selection(view.id, Selection::single(range.head, range.anchor));
3055+ align_view(doc, view, Align::Center);
3056+ }
3057+ },
3058+ move |_editor, symbol| {
3059+ let path = symbol.location.uri.to_file_path().unwrap();
3060+ let line = Some((
3061+ symbol.location.range.start.line as usize,
3062+ symbol.location.range.end.line as usize,
3063+ ));
3064+ Some((path, line))
3065+ },
3066+ );
3067+ picker.truncate_start = false;
28103068 compositor.push(Box::new(picker))
28113069 }
28123070 },
@@ -2864,14 +3122,104 @@ pub fn code_action(cx: &mut Context) {
28643122 )
28653123}
28663124
3125+pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
3126+ use lsp::ResourceOp;
3127+ use std::fs;
3128+ match op {
3129+ ResourceOp::Create(op) => {
3130+ let path = op.uri.to_file_path().unwrap();
3131+ let ignore_if_exists = if let Some(options) = &op.options {
3132+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
3133+ } else {
3134+ false
3135+ };
3136+ if ignore_if_exists && path.exists() {
3137+ Ok(())
3138+ } else {
3139+ fs::write(&path, [])
3140+ }
3141+ }
3142+ ResourceOp::Delete(op) => {
3143+ let path = op.uri.to_file_path().unwrap();
3144+ if path.is_dir() {
3145+ let recursive = if let Some(options) = &op.options {
3146+ options.recursive.unwrap_or(false)
3147+ } else {
3148+ false
3149+ };
3150+ if recursive {
3151+ fs::remove_dir_all(&path)
3152+ } else {
3153+ fs::remove_dir(&path)
3154+ }
3155+ } else if path.is_file() {
3156+ fs::remove_file(&path)
3157+ } else {
3158+ Ok(())
3159+ }
3160+ }
3161+ ResourceOp::Rename(op) => {
3162+ let from = op.old_uri.to_file_path().unwrap();
3163+ let to = op.new_uri.to_file_path().unwrap();
3164+ let ignore_if_exists = if let Some(options) = &op.options {
3165+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
3166+ } else {
3167+ false
3168+ };
3169+ if ignore_if_exists && to.exists() {
3170+ Ok(())
3171+ } else {
3172+ fs::rename(&from, &to)
3173+ }
3174+ }
3175+ }
3176+}
3177+
28673178fn apply_workspace_edit(
28683179 editor: &mut Editor,
28693180 offset_encoding: OffsetEncoding,
28703181 workspace_edit: &lsp::WorkspaceEdit,
28713182) {
3183+ let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
3184+ let path = uri
3185+ .to_file_path()
3186+ .expect("unable to convert URI to filepath");
3187+
3188+ let current_view_id = view!(editor).id;
3189+ let doc_id = editor.open(path, Action::Load).unwrap();
3190+ let doc = editor
3191+ .document_mut(doc_id)
3192+ .expect("Document for document_changes not found");
3193+
3194+ // Need to determine a view for apply/append_changes_to_history
3195+ let selections = doc.selections();
3196+ let view_id = if selections.contains_key(¤t_view_id) {
3197+ // use current if possible
3198+ current_view_id
3199+ } else {
3200+ // Hack: we take the first available view_id
3201+ selections
3202+ .keys()
3203+ .next()
3204+ .copied()
3205+ .expect("No view_id available")
3206+ };
3207+
3208+ let transaction = helix_lsp::util::generate_transaction_from_edits(
3209+ doc.text(),
3210+ text_edits,
3211+ offset_encoding,
3212+ );
3213+ doc.apply(&transaction, view_id);
3214+ doc.append_changes_to_history(view_id);
3215+ };
3216+
28723217 if let Some(ref changes) = workspace_edit.changes {
28733218 log::debug!("workspace changes: {:?}", changes);
2874− editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
3219+ for (uri, text_edits) in changes {
3220+ let text_edits = text_edits.to_vec();
3221+ apply_edits(uri, text_edits);
3222+ }
28753223 return;
28763224 // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
28773225 // TODO: find some example that uses workspace changes, and test it
@@ -2889,30 +3237,6 @@ fn apply_workspace_edit(
28893237 match document_changes {
28903238 lsp::DocumentChanges::Edits(document_edits) => {
28913239 for document_edit in document_edits {
2892− let path = document_edit
2893− .text_document
2894− .uri
2895− .to_file_path()
2896− .expect("unable to convert URI to filepath");
2897− let current_view_id = view!(editor).id;
2898− let doc = editor
2899− .document_by_path_mut(path)
2900− .expect("Document for document_changes not found");
2901−
2902− // Need to determine a view for apply/append_changes_to_history
2903− let selections = doc.selections();
2904− let view_id = if selections.contains_key(¤t_view_id) {
2905− // use current if possible
2906− current_view_id
2907− } else {
2908− // Hack: we take the first available view_id
2909− selections
2910− .keys()
2911− .next()
2912− .copied()
2913− .expect("No view_id available")
2914− };
2915−
29163240 let edits = document_edit
29173241 .edits
29183242 .iter()
@@ -2924,19 +3248,33 @@ fn apply_workspace_edit(
29243248 })
29253249 .cloned()
29263250 .collect();
2927−
2928− let transaction = helix_lsp::util::generate_transaction_from_edits(
2929− doc.text(),
2930− edits,
2931− offset_encoding,
2932− );
2933− doc.apply(&transaction, view_id);
2934− doc.append_changes_to_history(view_id);
3251+ apply_edits(&document_edit.text_document.uri, edits);
29353252 }
29363253 }
29373254 lsp::DocumentChanges::Operations(operations) => {
29383255 log::debug!("document changes - operations: {:?}", operations);
2939− editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
3256+ for operateion in operations {
3257+ match operateion {
3258+ lsp::DocumentChangeOperation::Op(op) => {
3259+ apply_document_resource_op(op).unwrap();
3260+ }
3261+
3262+ lsp::DocumentChangeOperation::Edit(document_edit) => {
3263+ let edits = document_edit
3264+ .edits
3265+ .iter()
3266+ .map(|edit| match edit {
3267+ lsp::OneOf::Left(text_edit) => text_edit,
3268+ lsp::OneOf::Right(annotated_text_edit) => {
3269+ &annotated_text_edit.text_edit
3270+ }
3271+ })
3272+ .cloned()
3273+ .collect();
3274+ apply_edits(&document_edit.text_document.uri, edits);
3275+ }
3276+ }
3277+ }
29403278 }
29413279 }
29423280 }
@@ -3175,6 +3513,19 @@ fn goto_last_accessed_file(cx: &mut Context) {
31753513 }
31763514}
31773515
3516+fn goto_last_modification(cx: &mut Context) {
3517+ let (view, doc) = current!(cx.editor);
3518+ let pos = doc.history.get_mut().last_edit_pos();
3519+ let text = doc.text().slice(..);
3520+ if let Some(pos) = pos {
3521+ let selection = doc
3522+ .selection(view.id)
3523+ .clone()
3524+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3525+ doc.set_selection(view.id, selection);
3526+ }
3527+}
3528+
31783529fn select_mode(cx: &mut Context) {
31793530 let (view, doc) = current!(cx.editor);
31803531 let text = doc.text().slice(..);
@@ -3773,19 +4124,70 @@ pub mod insert {
37734124 doc.apply(&transaction, view.id);
37744125 }
37754126
3776− // TODO: handle indent-aware delete
37774127 pub fn delete_char_backward(cx: &mut Context) {
37784128 let count = cx.count();
37794129 let (view, doc) = current!(cx.editor);
37804130 let text = doc.text().slice(..);
4131+ let indent_unit = doc.indent_unit();
4132+ let tab_size = doc.tab_width();
4133+
37814134 let transaction =
37824135 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
37834136 let pos = range.cursor(text);
3784− (
3785− graphemes::nth_prev_grapheme_boundary(text, pos, count),
3786− pos,
3787− None,
3788− )
4137+ let line_start_pos = text.line_to_char(range.cursor_line(text));
4138+ // considier to delete by indent level if all characters before `pos` are indent units.
4139+ let fragment = Cow::from(text.slice(line_start_pos..pos));
4140+ if !fragment.is_empty() && fragment.chars().all(|ch| ch.is_whitespace()) {
4141+ if text.get_char(pos.saturating_sub(1)) == Some('\t') {
4142+ // fast path, delete one char
4143+ (
4144+ graphemes::nth_prev_grapheme_boundary(text, pos, 1),
4145+ pos,
4146+ None,
4147+ )
4148+ } else {
4149+ let unit_len = indent_unit.chars().count();
4150+ // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition.
4151+ let unit_size = if indent_unit.starts_with('\t') {
4152+ tab_size * unit_len
4153+ } else {
4154+ unit_len
4155+ };
4156+ let width: usize = fragment
4157+ .chars()
4158+ .map(|ch| {
4159+ if ch == '\t' {
4160+ tab_size
4161+ } else {
4162+ // it can be none if it still meet control characters other than '\t'
4163+ // here just set the width to 1 (or some value better?).
4164+ ch.width().unwrap_or(1)
4165+ }
4166+ })
4167+ .sum();
4168+ let mut drop = width % unit_size; // round down to nearest unit
4169+ if drop == 0 {
4170+ drop = unit_size
4171+ }; // if it's already at a unit, consume a whole unit
4172+ let mut chars = fragment.chars().rev();
4173+ let mut start = pos;
4174+ for _ in 0..drop {
4175+ // delete up to `drop` spaces
4176+ match chars.next() {
4177+ Some(' ') => start -= 1,
4178+ _ => break,
4179+ }
4180+ }
4181+ (start, pos, None) // delete!
4182+ }
4183+ } else {
4184+ // delete char
4185+ (
4186+ graphemes::nth_prev_grapheme_boundary(text, pos, count),
4187+ pos,
4188+ None,
4189+ )
4190+ }
37894191 });
37904192 doc.apply(&transaction, view.id);
37914193 }
@@ -3815,8 +4217,19 @@ pub mod insert {
38154217 .selection(view.id)
38164218 .clone()
38174219 .transform(|range| movement::move_prev_word_start(text, range, count));
3818− doc.set_selection(view.id, selection);
3819− delete_selection(cx)
4220+ delete_selection_insert_mode(doc, view, &selection);
4221+ }
4222+
4223+ pub fn delete_word_forward(cx: &mut Context) {
4224+ let count = cx.count();
4225+ let (view, doc) = current!(cx.editor);
4226+ let text = doc.text().slice(..);
4227+
4228+ let selection = doc
4229+ .selection(view.id)
4230+ .clone()
4231+ .transform(|range| movement::move_next_word_start(text, range, count));
4232+ delete_selection_insert_mode(doc, view, &selection);
38204233 }
38214234}
38224235
@@ -3826,20 +4239,48 @@ pub mod insert {
38264239// storing it?
38274240
38284241fn undo(cx: &mut Context) {
4242+ let count = cx.count();
38294243 let (view, doc) = current!(cx.editor);
3830− let view_id = view.id;
3831− let success = doc.undo(view_id);
3832− if !success {
3833− cx.editor.set_status("Already at oldest change".to_owned());
4244+ for _ in 0..count {
4245+ if !doc.undo(view.id) {
4246+ cx.editor.set_status("Already at oldest change".to_owned());
4247+ break;
4248+ }
38344249 }
38354250}
38364251
38374252fn redo(cx: &mut Context) {
4253+ let count = cx.count();
38384254 let (view, doc) = current!(cx.editor);
3839− let view_id = view.id;
3840− let success = doc.redo(view_id);
3841− if !success {
3842− cx.editor.set_status("Already at newest change".to_owned());
4255+ for _ in 0..count {
4256+ if !doc.redo(view.id) {
4257+ cx.editor.set_status("Already at newest change".to_owned());
4258+ break;
4259+ }
4260+ }
4261+}
4262+
4263+fn earlier(cx: &mut Context) {
4264+ let count = cx.count();
4265+ let (view, doc) = current!(cx.editor);
4266+ for _ in 0..count {
4267+ // rather than doing in batch we do this so get error halfway
4268+ if !doc.earlier(view.id, UndoKind::Steps(1)) {
4269+ cx.editor.set_status("Already at oldest change".to_owned());
4270+ break;
4271+ }
4272+ }
4273+}
4274+
4275+fn later(cx: &mut Context) {
4276+ let count = cx.count();
4277+ let (view, doc) = current!(cx.editor);
4278+ for _ in 0..count {
4279+ // rather than doing in batch we do this so get error halfway
4280+ if !doc.later(view.id, UndoKind::Steps(1)) {
4281+ cx.editor.set_status("Already at newest change".to_owned());
4282+ break;
4283+ }
38434284 }
38444285}
38454286
@@ -4297,12 +4738,12 @@ fn join_selections(cx: &mut Context) {
42974738 doc.append_changes_to_history(view.id);
42984739}
42994740
4300−fn keep_selections(cx: &mut Context) {
4301− // keep selections matching regex
4741+fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
4742+ // keep or remove selections matching regex
43024743 let reg = cx.register.unwrap_or('/');
43034744 let prompt = ui::regex_prompt(
43044745 cx,
4305− "keep:".into(),
4746+ if !remove { "keep:" } else { "remove:" }.into(),
43064747 Some(reg),
43074748 |_input: &str| Vec::new(),
43084749 move |view, doc, regex, event| {
@@ -4311,7 +4752,9 @@ fn keep_selections(cx: &mut Context) {
43114752 }
43124753 let text = doc.text().slice(..);
43134754
4314− if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) {
4755+ if let Some(selection) =
4756+ selection::keep_or_remove_matches(text, doc.selection(view.id), ®ex, remove)
4757+ {
43154758 doc.set_selection(view.id, selection);
43164759 }
43174760 },
@@ -4320,6 +4763,14 @@ fn keep_selections(cx: &mut Context) {
43204763 cx.push_layer(Box::new(prompt));
43214764}
43224765
4766+fn keep_selections(cx: &mut Context) {
4767+ keep_or_remove_selections_impl(cx, false)
4768+}
4769+
4770+fn remove_selections(cx: &mut Context) {
4771+ keep_or_remove_selections_impl(cx, true)
4772+}
4773+
43234774fn keep_primary_selection(cx: &mut Context) {
43244775 let (view, doc) = current!(cx.editor);
43254776 // TODO: handle count
@@ -4445,19 +4896,15 @@ pub fn completion(cx: &mut Context) {
44454896 return;
44464897 }
44474898 let size = compositor.size();
4448− let ui = compositor
4449− .find(std::any::type_name::<ui::EditorView>())
4450− .unwrap();
4451− if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
4452− ui.set_completion(
4453− editor,
4454− items,
4455− offset_encoding,
4456− start_offset,
4457− trigger_offset,
4458− size,
4459− );
4460− };
4899+ let ui = compositor.find::<ui::EditorView>().unwrap();
4900+ ui.set_completion(
4901+ editor,
4902+ items,
4903+ offset_encoding,
4904+ start_offset,
4905+ trigger_offset,
4906+ size,
4907+ );
44614908 },
44624909 );
44634910}
@@ -4487,18 +4934,27 @@ fn hover(cx: &mut Context) {
44874934 move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
44884935 if let Some(hover) = response {
44894936 // hover.contents / .range <- used for visualizing
4490− let contents = match hover.contents {
4491− lsp::HoverContents::Scalar(contents) => {
4492− // markedstring(string/languagestring to be highlighted)
4493− // TODO
4494− log::error!("hover contents {:?}", contents);
4495− return;
4496− }
4497− lsp::HoverContents::Array(contents) => {
4498− log::error!("hover contents {:?}", contents);
4499− return;
4937+
4938+ fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
4939+ match contents {
4940+ lsp::MarkedString::String(contents) => contents,
4941+ lsp::MarkedString::LanguageString(string) => {
4942+ if string.language == "markdown" {
4943+ string.value
4944+ } else {
4945+ format!("```{}\n{}\n```", string.language, string.value)
4946+ }
4947+ }
45004948 }
4501− // TODO: render markdown
4949+ }
4950+
4951+ let contents = match hover.contents {
4952+ lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
4953+ lsp::HoverContents::Array(contents) => contents
4954+ .into_iter()
4955+ .map(marked_string_to_markdown)
4956+ .collect::<Vec<_>>()
4957+ .join("\n\n"),
45024958 lsp::HoverContents::Markup(contents) => contents.value,
45034959 };
45044960
@@ -4706,7 +5162,21 @@ fn wclose(cx: &mut Context) {
47065162 }
47075163 let view_id = view!(cx.editor).id;
47085164 // close current split
4709− cx.editor.close(view_id, /* close_buffer */ false);
5165+ cx.editor.close(view_id);
5166+}
5167+
5168+fn wonly(cx: &mut Context) {
5169+ let views = cx
5170+ .editor
5171+ .tree
5172+ .views()
5173+ .map(|(v, focus)| (v.id, focus))
5174+ .collect::<Vec<_>>();
5175+ for (view_id, focus) in views {
5176+ if !focus {
5177+ cx.editor.close(view_id);
5178+ }
5179+ }
47105180}
47115181
47125182fn select_register(cx: &mut Context) {
@@ -4717,6 +5187,15 @@ fn select_register(cx: &mut Context) {
47175187 })
47185188}
47195189
5190+fn insert_register(cx: &mut Context) {
5191+ cx.on_next_key(move |cx, event| {
5192+ if let Some(ch) = event.char() {
5193+ cx.editor.selected_register = Some(ch);
5194+ paste_before(cx);
5195+ }
5196+ })
5197+}
5198+
47205199fn align_view_top(cx: &mut Context) {
47215200 let (view, doc) = current!(cx.editor);
47225201 align_view(doc, view, Align::Top);
@@ -4785,10 +5264,19 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
47855264
47865265 let selection = doc.selection(view.id).clone().transform(|range| {
47875266 match ch {
4788− 'w' => textobject::textobject_word(text, range, objtype, count),
5267+ 'w' => textobject::textobject_word(text, range, objtype, count, false),
5268+ 'W' => textobject::textobject_word(text, range, objtype, count, true),
47895269 'c' => textobject_treesitter("class", range),
47905270 'f' => textobject_treesitter("function", range),
47915271 'p' => textobject_treesitter("parameter", range),
5272+ 'm' => {
5273+ let ch = text.char(range.cursor(text));
5274+ if !ch.is_ascii_alphanumeric() {
5275+ textobject::textobject_surround(text, range, objtype, ch, count)
5276+ } else {
5277+ range
5278+ }
5279+ }
47925280 // TODO: cancel new ranges if inconsistent surround matches across lines
47935281 ch if !ch.is_ascii_alphanumeric() => {
47945282 textobject::textobject_surround(text, range, objtype, ch, count)
@@ -5051,6 +5539,10 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
50515539 doc.apply(&transaction, view.id);
50525540 doc.append_changes_to_history(view.id);
50535541 }
5542+
5543+ // after replace cursor may be out of bounds, do this to
5544+ // make sure cursor is in view and update scroll as well
5545+ view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
50545546 },
50555547 None,
50565548 );
@@ -5096,3 +5588,76 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
50965588 doc.apply(&transaction, view.id);
50975589 doc.append_changes_to_history(view.id);
50985590}
5591+
5592+fn rename_symbol(cx: &mut Context) {
5593+ let prompt = Prompt::new(
5594+ "rename-to:".into(),
5595+ None,
5596+ |_input: &str| Vec::new(),
5597+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
5598+ if event != PromptEvent::Validate {
5599+ return;
5600+ }
5601+
5602+ log::debug!("renaming to: {:?}", input);
5603+
5604+ let (view, doc) = current!(cx.editor);
5605+ let language_server = match doc.language_server() {
5606+ Some(language_server) => language_server,
5607+ None => return,
5608+ };
5609+
5610+ let offset_encoding = language_server.offset_encoding();
5611+
5612+ let pos = pos_to_lsp_pos(
5613+ doc.text(),
5614+ doc.selection(view.id)
5615+ .primary()
5616+ .cursor(doc.text().slice(..)),
5617+ offset_encoding,
5618+ );
5619+
5620+ let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
5621+ let edits = block_on(task).unwrap_or_default();
5622+ log::debug!("Edits from LSP: {:?}", edits);
5623+ apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
5624+ },
5625+ None,
5626+ );
5627+ cx.push_layer(Box::new(prompt));
5628+}
5629+
5630+/// Increment object under cursor by count.
5631+fn increment(cx: &mut Context) {
5632+ increment_impl(cx, cx.count() as i64);
5633+}
5634+
5635+/// Decrement object under cursor by count.
5636+fn decrement(cx: &mut Context) {
5637+ increment_impl(cx, -(cx.count() as i64));
5638+}
5639+
5640+/// Decrement object under cursor by `amount`.
5641+fn increment_impl(cx: &mut Context, amount: i64) {
5642+ let (view, doc) = current!(cx.editor);
5643+ let selection = doc.selection(view.id);
5644+ let text = doc.text();
5645+
5646+ let changes = selection.ranges().iter().filter_map(|range| {
5647+ let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
5648+ let new_text = incrementor.incremented_text(amount);
5649+ Some((
5650+ incrementor.range.from(),
5651+ incrementor.range.to(),
5652+ Some(new_text),
5653+ ))
5654+ });
5655+
5656+ if changes.clone().count() > 0 {
5657+ let transaction = Transaction::change(doc.text(), changes);
5658+ let transaction = transaction.with_selection(selection.clone());
5659+
5660+ doc.apply(&transaction, view.id);
5661+ doc.append_changes_to_history(view.id);
5662+ }
5663+}