2 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
4 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
6 movement::{self, Direction},
10 search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
11 RopeSlice, Selection, SmallVec, Tendril, Transaction,
15 clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode,
16 view::View, Document, DocumentId, Editor, ViewId,
19use anyhow::{anyhow, bail, Context as _};
22 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
26use movement::Movement;
29 compositor::{self, Component, Compositor},
30 ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
32use tokio_stream::wrappers::UnboundedReceiverStream;
34use crate::job::{self, Job, Jobs};
35use futures_util::FutureExt;
36use std::num::NonZeroUsize;
37use std::{collections::HashMap, fmt, future::Future};
41 path::{Path, PathBuf},
44use once_cell::sync::Lazy;
45use serde::de::{self, Deserialize, Deserializer};
47pub struct Context<'a> {
48 pub selected_register: helix_view::RegisterSelection,
49 pub count: Option<NonZeroUsize>,
50 pub editor: &'a mut Editor,
52 pub callback: Option<crate::compositor::Callback>,
53 pub on_next_key_callback: Option<Box<dyn FnOnce(&mut Context, KeyEvent)>>,
54 pub jobs: &'a mut Jobs,
58 /// Push a new component onto the compositor.
59 pub fn push_layer(&mut self, component: Box<dyn Component>) {
60 self.callback = Some(Box::new(|compositor: &mut Compositor| {
61 compositor.push(component)
68 on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static,
70 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
74 pub fn callback<T, F>(
76 call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
79 T: for<'de> serde::Deserialize<'de> + Send + 'static,
80 F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
82 let callback = Box::pin(async move {
83 let json = call.await?;
84 let response = serde_json::from_value(json)?;
85 let call: job::Callback =
86 Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
87 callback(editor, compositor, response)
91 self.jobs.callback(callback);
94 /// Returns 1 if no explicit count was provided
96 pub fn count(&self) -> usize {
97 self.count.map_or(1, |v| v.get())
107pub fn align_view(doc: &Document, view: &mut View, align: Align) {
111 .cursor(doc.text().slice(..));
112 let line = doc.text().char_to_line(pos);
114 let height = view.inner_area().height as usize;
116 let relative = match align {
117 Align::Center => height / 2,
119 Align::Bottom => height,
122 view.offset.row = line.saturating_sub(relative);
125/// A command is composed of a static name, and a function that takes the current state plus a count,
126/// and does a side-effect on the state (usually by creating and applying a transaction).
127#[derive(Copy, Clone)]
130 fun: fn(cx: &mut Context),
134macro_rules! commands {
135 ( $($name:ident, $doc:literal),* ) => {
137 #[allow(non_upper_case_globals)]
138 pub const $name: Self = Self {
139 name: stringify!($name),
145 pub const COMMAND_LIST: &'static [Self] = &[
152 pub fn execute(&self, cx: &mut Context) {
156 pub fn name(&self) -> &'static str {
160 pub fn doc(&self) -> &'static str {
166 move_char_left, "Move left",
167 move_char_right, "Move right",
168 move_line_up, "Move up",
169 move_line_down, "Move down",
170 extend_char_left, "Extend left",
171 extend_char_right, "Extend right",
172 extend_line_up, "Extend up",
173 extend_line_down, "Extend down",
174 copy_selection_on_next_line, "Copy selection on next line",
175 copy_selection_on_prev_line, "Copy selection on previous line",
176 move_next_word_start, "Move to beginning of next word",
177 move_prev_word_start, "Move to beginning of previous word",
178 move_next_word_end, "Move to end of next word",
179 move_next_long_word_start, "Move to beginning of next long word",
180 move_prev_long_word_start, "Move to beginning of previous long word",
181 move_next_long_word_end, "Move to end of next long word",
182 extend_next_word_start, "Extend to beginning of next word",
183 extend_prev_word_start, "Extend to beginning of previous word",
184 extend_next_word_end, "Extend to end of next word",
185 find_till_char, "Move till next occurance of char",
186 find_next_char, "Move to next occurance of char",
187 extend_till_char, "Extend till next occurance of char",
188 extend_next_char, "Extend to next occurance of char",
189 till_prev_char, "Move till previous occurance of char",
190 find_prev_char, "Move to previous occurance of char",
191 extend_till_prev_char, "Extend till previous occurance of char",
192 extend_prev_char, "Extend to previous occurance of char",
193 replace, "Replace with new char",
194 switch_case, "Switch (toggle) case",
195 switch_to_uppercase, "Switch to uppercase",
196 switch_to_lowercase, "Switch to lowercase",
197 page_up, "Move page up",
198 page_down, "Move page down",
199 half_page_up, "Move half page up",
200 half_page_down, "Move half page down",
201 select_all, "Select whole document",
202 select_regex, "Select all regex matches inside selections",
203 split_selection, "Split selection into subselections on regex matches",
204 split_selection_on_newline, "Split selection on newlines",
205 search, "Search for regex pattern",
206 search_next, "Select next search match",
207 extend_search_next, "Add next search match to selection",
208 search_selection, "Use current selection as search pattern",
209 extend_line, "Select current line, if already selected, extend to next line",
210 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
211 delete_selection, "Delete selection",
212 change_selection, "Change selection (delete and enter insert mode)",
213 collapse_selection, "Collapse selection onto a single cursor",
214 flip_selections, "Flip selection cursor and anchor",
215 insert_mode, "Insert before selection",
216 append_mode, "Insert after selection (append)",
217 command_mode, "Enter command mode",
218 file_picker, "Open file picker",
219 code_action, "Perform code action",
220 buffer_picker, "Open buffer picker",
221 symbol_picker, "Open symbol picker",
222 last_picker, "Open last picker",
223 prepend_to_line, "Insert at start of line",
224 append_to_line, "Insert at end of line",
225 open_below, "Open new line below selection",
226 open_above, "Open new line above selection",
227 normal_mode, "Enter normal mode",
228 select_mode, "Enter selection extend mode",
229 exit_select_mode, "Exit selection mode",
230 goto_definition, "Goto definition",
231 goto_type_definition, "Goto type definition",
232 goto_implementation, "Goto implementation",
233 goto_file_start, "Goto file start/line",
234 goto_file_end, "Goto file end",
235 goto_reference, "Goto references",
236 goto_window_top, "Goto window top",
237 goto_window_middle, "Goto window middle",
238 goto_window_bottom, "Goto window bottom",
239 goto_last_accessed_file, "Goto last accessed file",
240 goto_line, "Goto line",
241 goto_last_line, "Goto last line",
242 goto_first_diag, "Goto first diagnostic",
243 goto_last_diag, "Goto last diagnostic",
244 goto_next_diag, "Goto next diagnostic",
245 goto_prev_diag, "Goto previous diagnostic",
246 goto_line_start, "Goto line start",
247 goto_line_end, "Goto line end",
248 // TODO: different description ?
249 goto_line_end_newline, "Goto line end",
250 goto_first_nonwhitespace, "Goto first non-blank in line",
251 signature_help, "Show signature help",
252 insert_tab, "Insert tab char",
253 insert_newline, "Insert newline char",
254 delete_char_backward, "Delete previous char",
255 delete_char_forward, "Delete next char",
256 delete_word_backward, "Delete previous word",
259 yank, "Yank selection",
260 yank_joined_to_clipboard, "Join and yank selections to clipboard",
261 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
262 yank_joined_to_primary_clipboard, "Join and yank selections to primary clipboard",
263 yank_main_selection_to_primary_clipboard, "Yank main selection to primary clipboard",
264 replace_with_yanked, "Replace with yanked text",
265 replace_selections_with_clipboard, "Replace selections by clipboard content",
266 replace_selections_with_primary_clipboard, "Replace selections by primary clipboard content",
267 paste_after, "Paste after selection",
268 paste_before, "Paste before selection",
269 paste_clipboard_after, "Paste clipboard after selections",
270 paste_clipboard_before, "Paste clipboard before selections",
271 paste_primary_clipboard_after, "Paste primary clipboard after selections",
272 paste_primary_clipboard_before, "Paste primary clipboard before selections",
273 indent, "Indent selection",
274 unindent, "Unindent selection",
275 format_selections, "Format selection",
276 join_selections, "Join lines inside selection",
277 keep_selections, "Keep selections matching regex",
278 keep_primary_selection, "Keep primary selection",
279 completion, "Invoke completion popup",
280 hover, "Show docs for item under cursor",
281 toggle_comments, "Comment/uncomment selections",
282 rotate_selections_forward, "Rotate selections forward",
283 rotate_selections_backward, "Rotate selections backward",
284 rotate_selection_contents_forward, "Rotate selection contents forward",
285 rotate_selection_contents_backward, "Rotate selections contents backward",
286 expand_selection, "Expand selection to parent syntax node",
287 jump_forward, "Jump forward on jumplist",
288 jump_backward, "Jump backward on jumplist",
289 rotate_view, "Goto next window",
290 hsplit, "Horizontal bottom split",
291 vsplit, "Vertical right split",
292 wclose, "Close window",
293 select_register, "Select register",
294 align_view_middle, "Align view middle",
295 align_view_top, "Align view top",
296 align_view_center, "Align view center",
297 align_view_bottom, "Align view bottom",
298 scroll_up, "Scroll view up",
299 scroll_down, "Scroll view down",
300 match_brackets, "Goto matching bracket",
301 surround_add, "Surround add",
302 surround_replace, "Surround replace",
303 surround_delete, "Surround delete",
304 select_textobject_around, "Select around object",
305 select_textobject_inner, "Select inside object",
306 dap_toggle_breakpoint, "Toggle breakpoint",
307 dap_start, "Start debug session",
308 dap_run, "Begin program execution",
309 dap_continue, "Continue program execution",
310 dap_pause, "Pause program execution",
313 dap_next, "Step to next",
314 dap_variables, "List variables",
315 dap_terminate, "End debug session",
320impl fmt::Debug for Command {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 let Command { name, .. } = self;
323 f.debug_tuple("Command").field(name).finish()
327impl fmt::Display for Command {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329 let Command { name, .. } = self;
334impl std::str::FromStr for Command {
335 type Err = anyhow::Error;
337 fn from_str(s: &str) -> Result<Self, Self::Err> {
338 Command::COMMAND_LIST
341 .find(|cmd| cmd.name == s)
342 .ok_or_else(|| anyhow!("No command named '{}'", s))
346impl<'de> Deserialize<'de> for Command {
347 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
349 D: Deserializer<'de>,
351 let s = String::deserialize(deserializer)?;
352 s.parse().map_err(de::Error::custom)
356impl PartialEq for Command {
357 fn eq(&self, other: &Self) -> bool {
358 self.name() == other.name()
362fn move_char_left(cx: &mut Context) {
363 let count = cx.count();
364 let (view, doc) = current!(cx.editor);
365 let text = doc.text().slice(..);
367 let selection = doc.selection(view.id).clone().transform(|range| {
368 movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
370 doc.set_selection(view.id, selection);
373fn move_char_right(cx: &mut Context) {
374 let count = cx.count();
375 let (view, doc) = current!(cx.editor);
376 let text = doc.text().slice(..);
378 let selection = doc.selection(view.id).clone().transform(|range| {
379 movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
381 doc.set_selection(view.id, selection);
384fn move_line_up(cx: &mut Context) {
385 let count = cx.count();
386 let (view, doc) = current!(cx.editor);
387 let text = doc.text().slice(..);
389 let selection = doc.selection(view.id).clone().transform(|range| {
390 movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
392 doc.set_selection(view.id, selection);
395fn move_line_down(cx: &mut Context) {
396 let count = cx.count();
397 let (view, doc) = current!(cx.editor);
398 let text = doc.text().slice(..);
400 let selection = doc.selection(view.id).clone().transform(|range| {
401 movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
403 doc.set_selection(view.id, selection);
406fn goto_line_end(cx: &mut Context) {
407 let (view, doc) = current!(cx.editor);
408 let text = doc.text().slice(..);
410 let selection = doc.selection(view.id).clone().transform(|range| {
411 let line = range.cursor_line(text);
412 let line_start = text.line_to_char(line);
414 let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
417 range.put_cursor(text, pos, doc.mode == Mode::Select)
419 doc.set_selection(view.id, selection);
422fn goto_line_end_newline(cx: &mut Context) {
423 let (view, doc) = current!(cx.editor);
424 let text = doc.text().slice(..);
426 let selection = doc.selection(view.id).clone().transform(|range| {
427 let line = range.cursor_line(text);
428 let pos = line_end_char_index(&text, line);
430 range.put_cursor(text, pos, doc.mode == Mode::Select)
432 doc.set_selection(view.id, selection);
435fn goto_line_start(cx: &mut Context) {
436 let (view, doc) = current!(cx.editor);
437 let text = doc.text().slice(..);
439 let selection = doc.selection(view.id).clone().transform(|range| {
440 let line = range.cursor_line(text);
442 // adjust to start of the line
443 let pos = text.line_to_char(line);
444 range.put_cursor(text, pos, doc.mode == Mode::Select)
446 doc.set_selection(view.id, selection);
449fn goto_first_nonwhitespace(cx: &mut Context) {
450 let (view, doc) = current!(cx.editor);
451 let text = doc.text().slice(..);
453 let selection = doc.selection(view.id).clone().transform(|range| {
454 let line = range.cursor_line(text);
456 if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
457 let pos = pos + text.line_to_char(line);
458 range.put_cursor(text, pos, doc.mode == Mode::Select)
463 doc.set_selection(view.id, selection);
466fn goto_window(cx: &mut Context, align: Align) {
467 let (view, doc) = current!(cx.editor);
469 let height = view.inner_area().height as usize;
471 // - 1 so we have at least one gap in the middle.
472 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
474 let scrolloff = cx.editor.config.scrolloff.min(height.saturating_sub(1) / 2);
476 let last_line = view.last_line(doc);
478 let line = match align {
479 Align::Top => (view.offset.row + scrolloff),
480 Align::Center => (view.offset.row + (height / 2)),
481 Align::Bottom => last_line.saturating_sub(scrolloff),
483 .min(last_line.saturating_sub(scrolloff));
485 let pos = doc.text().line_to_char(line);
487 doc.set_selection(view.id, Selection::point(pos));
490fn goto_window_top(cx: &mut Context) {
491 goto_window(cx, Align::Top)
494fn goto_window_middle(cx: &mut Context) {
495 goto_window(cx, Align::Center)
498fn goto_window_bottom(cx: &mut Context) {
499 goto_window(cx, Align::Bottom)
502fn move_next_word_start(cx: &mut Context) {
503 let count = cx.count();
504 let (view, doc) = current!(cx.editor);
505 let text = doc.text().slice(..);
510 .transform(|range| movement::move_next_word_start(text, range, count));
511 doc.set_selection(view.id, selection);
514fn move_prev_word_start(cx: &mut Context) {
515 let count = cx.count();
516 let (view, doc) = current!(cx.editor);
517 let text = doc.text().slice(..);
522 .transform(|range| movement::move_prev_word_start(text, range, count));
523 doc.set_selection(view.id, selection);
526fn move_next_word_end(cx: &mut Context) {
527 let count = cx.count();
528 let (view, doc) = current!(cx.editor);
529 let text = doc.text().slice(..);
534 .transform(|range| movement::move_next_word_end(text, range, count));
535 doc.set_selection(view.id, selection);
538fn move_next_long_word_start(cx: &mut Context) {
539 let count = cx.count();
540 let (view, doc) = current!(cx.editor);
541 let text = doc.text().slice(..);
546 .transform(|range| movement::move_next_long_word_start(text, range, count));
547 doc.set_selection(view.id, selection);
550fn move_prev_long_word_start(cx: &mut Context) {
551 let count = cx.count();
552 let (view, doc) = current!(cx.editor);
553 let text = doc.text().slice(..);
558 .transform(|range| movement::move_prev_long_word_start(text, range, count));
559 doc.set_selection(view.id, selection);
562fn move_next_long_word_end(cx: &mut Context) {
563 let count = cx.count();
564 let (view, doc) = current!(cx.editor);
565 let text = doc.text().slice(..);
570 .transform(|range| movement::move_next_long_word_end(text, range, count));
571 doc.set_selection(view.id, selection);
574fn goto_file_start(cx: &mut Context) {
575 if cx.count.is_some() {
578 push_jump(cx.editor);
579 let (view, doc) = current!(cx.editor);
580 doc.set_selection(view.id, Selection::point(0));
584fn goto_file_end(cx: &mut Context) {
585 push_jump(cx.editor);
586 let (view, doc) = current!(cx.editor);
587 doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
590fn extend_next_word_start(cx: &mut Context) {
591 let count = cx.count();
592 let (view, doc) = current!(cx.editor);
593 let text = doc.text().slice(..);
595 let selection = doc.selection(view.id).clone().transform(|range| {
596 let word = movement::move_next_word_start(text, range, count);
597 let pos = word.cursor(text);
598 range.put_cursor(text, pos, true)
600 doc.set_selection(view.id, selection);
603fn extend_prev_word_start(cx: &mut Context) {
604 let count = cx.count();
605 let (view, doc) = current!(cx.editor);
606 let text = doc.text().slice(..);
608 let selection = doc.selection(view.id).clone().transform(|range| {
609 let word = movement::move_prev_word_start(text, range, count);
610 let pos = word.cursor(text);
611 range.put_cursor(text, pos, true)
613 doc.set_selection(view.id, selection);
616fn extend_next_word_end(cx: &mut Context) {
617 let count = cx.count();
618 let (view, doc) = current!(cx.editor);
619 let text = doc.text().slice(..);
621 let selection = doc.selection(view.id).clone().transform(|range| {
622 let word = movement::move_next_word_end(text, range, count);
623 let pos = word.cursor(text);
624 range.put_cursor(text, pos, true)
626 doc.set_selection(view.id, selection);
630fn find_char_impl<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
632 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
634 // TODO: count is reset to 1 before next key so we move it into the closure here.
635 // Would be nice to carry over.
636 let count = cx.count();
638 // need to wait for next key
639 // TODO: should this be done by grapheme rather than char? For example,
640 // we can't properly handle the line-ending CRLF case here in terms of char.
641 cx.on_next_key(move |cx, event| {
642 let ch = match event {
644 code: KeyCode::Enter,
647 // TODO: this isn't quite correct when CRLF is involved.
648 // This hack will work in most cases, since documents don't
649 // usually mix line endings. But we should fix it eventually
662 code: KeyCode::Char(ch),
668 let (view, doc) = current!(cx.editor);
669 let text = doc.text().slice(..);
671 let selection = doc.selection(view.id).clone().transform(|range| {
672 // TODO: use `Range::cursor()` here instead. However, that works in terms of
673 // graphemes, whereas this function doesn't yet. So we're doing the same logic
674 // here, but just in terms of chars instead.
675 let search_start_pos = if range.anchor < range.head {
681 search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
683 range.put_cursor(text, pos, true)
685 Range::point(range.cursor(text)).put_cursor(text, pos, true)
689 doc.set_selection(view.id, selection);
693fn find_next_char_impl(
700 let pos = (pos + 1).min(text.len_chars());
702 search::find_nth_next(text, ch, pos, n)
704 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
708fn find_prev_char_impl(
716 search::find_nth_prev(text, ch, pos, n)
718 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
722fn find_till_char(cx: &mut Context) {
726 false, /* inclusive */
731fn find_next_char(cx: &mut Context) {
735 true, /* inclusive */
740fn extend_till_char(cx: &mut Context) {
744 false, /* inclusive */
749fn extend_next_char(cx: &mut Context) {
753 true, /* inclusive */
758fn till_prev_char(cx: &mut Context) {
762 false, /* inclusive */
767fn find_prev_char(cx: &mut Context) {
771 true, /* inclusive */
776fn extend_till_prev_char(cx: &mut Context) {
780 false, /* inclusive */
785fn extend_prev_char(cx: &mut Context) {
789 true, /* inclusive */
794fn replace(cx: &mut Context) {
795 let mut buf = [0u8; 4]; // To hold utf8 encoded char.
797 // need to wait for next key
798 cx.on_next_key(move |cx, event| {
799 let (view, doc) = current!(cx.editor);
800 let ch = match event {
802 code: KeyCode::Char(ch),
804 } => Some(&ch.encode_utf8(&mut buf[..])[..]),
806 code: KeyCode::Enter,
808 } => Some(doc.line_ending.as_str()),
812 let selection = doc.selection(view.id);
814 if let Some(ch) = ch {
815 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
816 if !range.is_empty() {
818 RopeGraphemes::new(doc.text().slice(range.from()..range.to()))
820 let cow: Cow<str> = g.into();
821 if str_is_line_ending(&cow) {
829 (range.from(), range.to(), Some(text.into()))
832 (range.from(), range.to(), None)
836 doc.apply(&transaction, view.id);
837 doc.append_changes_to_history(view.id);
842fn switch_case(cx: &mut Context) {
843 let (view, doc) = current!(cx.editor);
844 let selection = doc.selection(view.id);
845 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
846 let text: Tendril = range
847 .fragment(doc.text().slice(..))
850 if ch.is_lowercase() {
851 ch.to_uppercase().collect()
852 } else if ch.is_uppercase() {
853 ch.to_lowercase().collect()
860 (range.from(), range.to(), Some(text))
863 doc.apply(&transaction, view.id);
864 doc.append_changes_to_history(view.id);
867fn switch_to_uppercase(cx: &mut Context) {
868 let (view, doc) = current!(cx.editor);
869 let selection = doc.selection(view.id);
870 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
871 let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
873 (range.from(), range.to(), Some(text))
876 doc.apply(&transaction, view.id);
877 doc.append_changes_to_history(view.id);
880fn switch_to_lowercase(cx: &mut Context) {
881 let (view, doc) = current!(cx.editor);
882 let selection = doc.selection(view.id);
883 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
884 let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
886 (range.from(), range.to(), Some(text))
889 doc.apply(&transaction, view.id);
890 doc.append_changes_to_history(view.id);
893pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
895 let (view, doc) = current!(cx.editor);
897 let range = doc.selection(view.id).primary();
898 let text = doc.text().slice(..);
900 let cursor = coords_at_pos(text, range.cursor(text));
901 let doc_last_line = doc.text().len_lines().saturating_sub(1);
903 let last_line = view.last_line(doc);
905 if direction == Backward && view.offset.row == 0
906 || direction == Forward && last_line == doc_last_line
911 let height = view.inner_area().height;
913 let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2);
915 view.offset.row = match direction {
916 Forward => view.offset.row + offset,
917 Backward => view.offset.row.saturating_sub(offset),
921 // recalculate last line
922 let last_line = view.last_line(doc);
924 // clamp into viewport
927 .max(view.offset.row + scrolloff)
928 .min(last_line.saturating_sub(scrolloff));
930 let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
932 let anchor = if doc.mode == Mode::Select {
938 // TODO: only manipulate main selection
939 doc.set_selection(view.id, Selection::single(anchor, head));
942fn page_up(cx: &mut Context) {
943 let view = view!(cx.editor);
944 let offset = view.inner_area().height as usize;
945 scroll(cx, offset, Direction::Backward);
948fn page_down(cx: &mut Context) {
949 let view = view!(cx.editor);
950 let offset = view.inner_area().height as usize;
951 scroll(cx, offset, Direction::Forward);
954fn half_page_up(cx: &mut Context) {
955 let view = view!(cx.editor);
956 let offset = view.inner_area().height as usize / 2;
957 scroll(cx, offset, Direction::Backward);
960fn half_page_down(cx: &mut Context) {
961 let view = view!(cx.editor);
962 let offset = view.inner_area().height as usize / 2;
963 scroll(cx, offset, Direction::Forward);
966fn extend_char_left(cx: &mut Context) {
967 let count = cx.count();
968 let (view, doc) = current!(cx.editor);
969 let text = doc.text().slice(..);
971 let selection = doc.selection(view.id).clone().transform(|range| {
972 movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
974 doc.set_selection(view.id, selection);
977fn extend_char_right(cx: &mut Context) {
978 let count = cx.count();
979 let (view, doc) = current!(cx.editor);
980 let text = doc.text().slice(..);
982 let selection = doc.selection(view.id).clone().transform(|range| {
983 movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
985 doc.set_selection(view.id, selection);
988fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
989 let count = cx.count();
990 let (view, doc) = current!(cx.editor);
991 let text = doc.text().slice(..);
992 let selection = doc.selection(view.id);
993 let mut ranges = SmallVec::with_capacity(selection.ranges().len() * (count + 1));
994 ranges.extend_from_slice(selection.ranges());
995 let mut primary_index = 0;
996 for range in selection.iter() {
997 let is_primary = *range == selection.primary();
998 let head_pos = coords_at_pos(text, range.head);
999 let anchor_pos = coords_at_pos(text, range.anchor);
1000 let height = std::cmp::max(head_pos.row, anchor_pos.row)
1001 - std::cmp::min(head_pos.row, anchor_pos.row)
1005 primary_index = ranges.len();
1007 ranges.push(*range);
1011 while sels < count {
1012 let offset = (i + 1) * height;
1014 let anchor_row = match direction {
1015 Direction::Forward => anchor_pos.row + offset,
1016 Direction::Backward => anchor_pos.row.saturating_sub(offset),
1019 let head_row = match direction {
1020 Direction::Forward => head_pos.row + offset,
1021 Direction::Backward => head_pos.row.saturating_sub(offset),
1024 if anchor_row >= text.len_lines() || head_row >= text.len_lines() {
1028 let anchor = pos_at_coords(text, Position::new(anchor_row, anchor_pos.col), true);
1029 let head = pos_at_coords(text, Position::new(head_row, head_pos.col), true);
1031 // skip lines that are too short
1032 if coords_at_pos(text, anchor).col == anchor_pos.col
1033 && coords_at_pos(text, head).col == head_pos.col
1036 primary_index = ranges.len();
1038 ranges.push(Range::new(anchor, head));
1046 let selection = Selection::new(ranges, primary_index);
1047 doc.set_selection(view.id, selection);
1050fn copy_selection_on_prev_line(cx: &mut Context) {
1051 copy_selection_on_line(cx, Direction::Backward)
1054fn copy_selection_on_next_line(cx: &mut Context) {
1055 copy_selection_on_line(cx, Direction::Forward)
1058fn extend_line_up(cx: &mut Context) {
1059 let count = cx.count();
1060 let (view, doc) = current!(cx.editor);
1061 let text = doc.text().slice(..);
1063 let selection = doc.selection(view.id).clone().transform(|range| {
1064 movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
1066 doc.set_selection(view.id, selection);
1069fn extend_line_down(cx: &mut Context) {
1070 let count = cx.count();
1071 let (view, doc) = current!(cx.editor);
1072 let text = doc.text().slice(..);
1074 let selection = doc.selection(view.id).clone().transform(|range| {
1075 movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
1077 doc.set_selection(view.id, selection);
1080fn select_all(cx: &mut Context) {
1081 let (view, doc) = current!(cx.editor);
1083 let end = doc.text().len_chars();
1084 doc.set_selection(view.id, Selection::single(0, end))
1087fn select_regex(cx: &mut Context) {
1088 let prompt = ui::regex_prompt(cx, "select:".to_string(), move |view, doc, _, regex| {
1089 let text = doc.text().slice(..);
1090 if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex)
1092 doc.set_selection(view.id, selection);
1096 cx.push_layer(Box::new(prompt));
1099fn split_selection(cx: &mut Context) {
1100 let prompt = ui::regex_prompt(cx, "split:".to_string(), move |view, doc, _, regex| {
1101 let text = doc.text().slice(..);
1102 let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex);
1103 doc.set_selection(view.id, selection);
1106 cx.push_layer(Box::new(prompt));
1109fn split_selection_on_newline(cx: &mut Context) {
1110 let (view, doc) = current!(cx.editor);
1111 let text = doc.text().slice(..);
1112 // only compile the regex once
1113 #[allow(clippy::trivial_regex)]
1114 static REGEX: Lazy<Regex> =
1115 Lazy::new(|| Regex::new(r"\r\n|[\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}]").unwrap());
1116 let selection = selection::split_on_matches(text, doc.selection(view.id), ®EX);
1117 doc.set_selection(view.id, selection);
1120fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
1121 let text = doc.text().slice(..);
1122 let selection = doc.selection(view.id);
1124 // Get the right side of the primary block cursor.
1125 let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1127 selection.primary().cursor(text),
1130 // use find_at to find the next match after the cursor, loop around the end
1131 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1133 .find_at(contents, start)
1134 .or_else(|| regex.find(contents));
1135 // TODO: message on wraparound
1136 if let Some(mat) = mat {
1137 let start = text.byte_to_char(mat.start());
1138 let end = text.byte_to_char(mat.end());
1141 // skip empty matches that don't make sense
1145 let selection = if extend {
1146 selection.clone().push(Range::new(start, end))
1148 Selection::single(start, end)
1151 doc.set_selection(view.id, selection);
1152 align_view(doc, view, Align::Center);
1156// TODO: use one function for search vs extend
1157fn search(cx: &mut Context) {
1158 let (_, doc) = current!(cx.editor);
1160 // TODO: could probably share with select_on_matches?
1162 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
1163 // feed chunks into the regex yet
1164 let contents = doc.text().slice(..).to_string();
1166 let prompt = ui::regex_prompt(
1168 "search:".to_string(),
1169 move |view, doc, registers, regex| {
1170 search_impl(doc, view, &contents, ®ex, false);
1171 // TODO: only store on enter (accept), not update
1172 registers.write('/', vec![regex.as_str().to_string()]);
1176 cx.push_layer(Box::new(prompt));
1179fn search_next_impl(cx: &mut Context, extend: bool) {
1180 let (view, doc) = current!(cx.editor);
1181 let registers = &mut cx.editor.registers;
1182 if let Some(query) = registers.read('/') {
1183 let query = query.first().unwrap();
1184 let contents = doc.text().slice(..).to_string();
1185 let regex = Regex::new(query).unwrap();
1186 search_impl(doc, view, &contents, ®ex, extend);
1190fn search_next(cx: &mut Context) {
1191 search_next_impl(cx, false);
1194fn extend_search_next(cx: &mut Context) {
1195 search_next_impl(cx, true);
1198fn search_selection(cx: &mut Context) {
1199 let (view, doc) = current!(cx.editor);
1200 let contents = doc.text().slice(..);
1201 let query = doc.selection(view.id).primary().fragment(contents);
1202 let regex = regex::escape(&query);
1203 cx.editor.registers.write('/', vec![regex]);
1207fn extend_line(cx: &mut Context) {
1208 let count = cx.count();
1209 let (view, doc) = current!(cx.editor);
1211 let text = doc.text();
1212 let range = doc.selection(view.id).primary();
1214 let (start_line, end_line) = range.line_range(text.slice(..));
1215 let start = text.line_to_char(start_line);
1216 let mut end = text.line_to_char((end_line + count).min(text.len_lines()));
1218 if range.from() == start && range.to() == end {
1219 end = text.line_to_char((end_line + count + 1).min(text.len_lines()));
1222 doc.set_selection(view.id, Selection::single(start, end));
1225fn extend_to_line_bounds(cx: &mut Context) {
1226 let (view, doc) = current!(cx.editor);
1230 doc.selection(view.id).clone().transform(|range| {
1231 let text = doc.text();
1233 let (start_line, end_line) = range.line_range(text.slice(..));
1234 let start = text.line_to_char(start_line);
1235 let end = text.line_to_char((end_line + 1).min(text.len_lines()));
1237 if range.anchor <= range.head {
1238 Range::new(start, end)
1240 Range::new(end, start)
1246fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1247 let text = doc.text().slice(..);
1248 let selection = doc.selection(view_id);
1250 // first yank the selection
1251 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1255 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1256 (range.from(), range.to(), None)
1258 doc.apply(&transaction, view_id);
1261fn delete_selection(cx: &mut Context) {
1262 let reg_name = cx.selected_register.name();
1263 let (view, doc) = current!(cx.editor);
1264 let registers = &mut cx.editor.registers;
1265 let reg = registers.get_mut(reg_name);
1266 delete_selection_impl(reg, doc, view.id);
1268 doc.append_changes_to_history(view.id);
1270 // exit select mode, if currently in select mode
1271 exit_select_mode(cx);
1274fn change_selection(cx: &mut Context) {
1275 let reg_name = cx.selected_register.name();
1276 let (view, doc) = current!(cx.editor);
1277 let registers = &mut cx.editor.registers;
1278 let reg = registers.get_mut(reg_name);
1279 delete_selection_impl(reg, doc, view.id);
1280 enter_insert_mode(doc);
1283fn collapse_selection(cx: &mut Context) {
1284 let (view, doc) = current!(cx.editor);
1285 let text = doc.text().slice(..);
1287 let selection = doc.selection(view.id).clone().transform(|range| {
1288 let pos = range.cursor(text);
1289 Range::new(pos, pos)
1291 doc.set_selection(view.id, selection);
1294fn flip_selections(cx: &mut Context) {
1295 let (view, doc) = current!(cx.editor);
1300 .transform(|range| Range::new(range.head, range.anchor));
1301 doc.set_selection(view.id, selection);
1304fn enter_insert_mode(doc: &mut Document) {
1305 doc.mode = Mode::Insert;
1308// inserts at the start of each selection
1309fn insert_mode(cx: &mut Context) {
1310 let (view, doc) = current!(cx.editor);
1311 enter_insert_mode(doc);
1316 .transform(|range| Range::new(range.to(), range.from()));
1317 doc.set_selection(view.id, selection);
1320// inserts at the end of each selection
1321fn append_mode(cx: &mut Context) {
1322 let (view, doc) = current!(cx.editor);
1323 enter_insert_mode(doc);
1324 doc.restore_cursor = true;
1325 let text = doc.text().slice(..);
1327 // Make sure there's room at the end of the document if the last
1328 // selection butts up against it.
1329 let end = text.len_chars();
1330 let last_range = doc.selection(view.id).iter().last().unwrap();
1331 if !last_range.is_empty() && last_range.head == end {
1332 let transaction = Transaction::change(
1334 std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
1336 doc.apply(&transaction, view.id);
1339 let selection = doc.selection(view.id).clone().transform(|range| {
1342 graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()),
1345 doc.set_selection(view.id, selection);
1351 use helix_view::editor::Action;
1352 use ui::completers::{self, Completer};
1355 pub struct TypableCommand {
1356 pub name: &'static str,
1357 pub alias: Option<&'static str>,
1358 pub doc: &'static str,
1359 // params, flags, helper, completer
1360 pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
1361 pub completer: Option<Completer>,
1365 cx: &mut compositor::Context,
1367 _event: PromptEvent,
1368 ) -> anyhow::Result<()> {
1369 // last view and we have unsaved changes
1370 if cx.editor.tree.views().count() == 1 {
1371 buffers_remaining_impl(cx.editor)?
1375 .close(view!(cx.editor).id, /* close_buffer */ false);
1381 cx: &mut compositor::Context,
1383 _event: PromptEvent,
1384 ) -> anyhow::Result<()> {
1386 .close(view!(cx.editor).id, /* close_buffer */ false);
1392 cx: &mut compositor::Context,
1394 _event: PromptEvent,
1395 ) -> anyhow::Result<()> {
1396 let path = args.get(0).context("wrong argument count")?;
1397 let _ = cx.editor.open(path.into(), Action::Replace)?;
1401 fn write_impl<P: AsRef<Path>>(
1402 cx: &mut compositor::Context,
1404 ) -> anyhow::Result<()> {
1405 let jobs = &mut cx.jobs;
1406 let (_, doc) = current!(cx.editor);
1408 if let Some(path) = path {
1409 doc.set_path(path.as_ref()).context("invalid filepath")?;
1411 if doc.path().is_none() {
1412 bail!("cannot write a buffer without a filename");
1414 let fmt = doc.auto_format().map(|fmt| {
1415 let shared = fmt.shared();
1416 let callback = make_format_callback(
1419 Modified::SetUnmodified,
1422 jobs.callback(callback);
1425 let future = doc.format_and_save(fmt);
1426 cx.jobs.add(Job::new(future).wait_before_exiting());
1431 cx: &mut compositor::Context,
1433 _event: PromptEvent,
1434 ) -> anyhow::Result<()> {
1435 write_impl(cx, args.first())
1439 cx: &mut compositor::Context,
1441 _event: PromptEvent,
1442 ) -> anyhow::Result<()> {
1443 cx.editor.new_file(Action::Replace);
1449 cx: &mut compositor::Context,
1451 _event: PromptEvent,
1452 ) -> anyhow::Result<()> {
1453 let (_, doc) = current!(cx.editor);
1455 if let Some(format) = doc.format() {
1457 make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
1458 cx.jobs.callback(callback);
1463 fn set_indent_style(
1464 cx: &mut compositor::Context,
1466 _event: PromptEvent,
1467 ) -> anyhow::Result<()> {
1470 // If no argument, report current indent style.
1471 if args.is_empty() {
1472 let style = current!(cx.editor).1.indent_style;
1473 cx.editor.set_status(match style {
1474 Tabs => "tabs".into(),
1475 Spaces(1) => "1 space".into(),
1476 Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
1477 _ => "error".into(), // Shouldn't happen.
1482 // Attempt to parse argument as an indent style.
1483 let style = match args.get(0) {
1484 Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
1485 Some(&"0") => Some(Tabs),
1489 .filter(|n| (1..=8).contains(n))
1494 let style = style.context("invalid indent style")?;
1495 let doc = doc_mut!(cx.editor);
1496 doc.indent_style = style;
1501 /// Sets or reports the current document's line ending setting.
1503 cx: &mut compositor::Context,
1505 _event: PromptEvent,
1506 ) -> anyhow::Result<()> {
1509 // If no argument, report current line ending setting.
1510 if args.is_empty() {
1511 let line_ending = current!(cx.editor).1.line_ending;
1512 cx.editor.set_status(match line_ending {
1513 Crlf => "crlf".into(),
1514 LF => "line feed".into(),
1515 FF => "form feed".into(),
1516 CR => "carriage return".into(),
1517 Nel => "next line".into(),
1519 // These should never be a document's default line ending.
1520 VT | LS | PS => "error".into(),
1528 .context("argument missing")?
1529 .to_ascii_lowercase();
1531 // Attempt to parse argument as a line ending.
1532 let line_ending = match arg {
1533 // We check for CR first because it shares a common prefix with CRLF.
1534 arg if arg.starts_with("cr") => CR,
1535 arg if arg.starts_with("crlf") => Crlf,
1536 arg if arg.starts_with("lf") => LF,
1537 arg if arg.starts_with("ff") => FF,
1538 arg if arg.starts_with("nel") => Nel,
1539 _ => bail!("invalid line ending"),
1542 doc_mut!(cx.editor).line_ending = line_ending;
1547 cx: &mut compositor::Context,
1549 _event: PromptEvent,
1550 ) -> anyhow::Result<()> {
1553 .parse::<helix_core::history::UndoKind>()
1554 .map_err(|s| anyhow!(s))?;
1556 let (view, doc) = current!(cx.editor);
1557 doc.earlier(view.id, uk);
1563 cx: &mut compositor::Context,
1565 _event: PromptEvent,
1566 ) -> anyhow::Result<()> {
1569 .parse::<helix_core::history::UndoKind>()
1570 .map_err(|s| anyhow!(s))?;
1571 let (view, doc) = current!(cx.editor);
1572 doc.later(view.id, uk);
1578 cx: &mut compositor::Context,
1581 ) -> anyhow::Result<()> {
1582 write_impl(cx, args.first())?;
1583 quit(cx, &[], event)
1586 fn force_write_quit(
1587 cx: &mut compositor::Context,
1590 ) -> anyhow::Result<()> {
1591 write_impl(cx, args.first())?;
1592 force_quit(cx, &[], event)
1595 /// Results an error if there are modified buffers remaining and sets editor error,
1596 /// otherwise returns `Ok(())`
1597 fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
1598 let modified: Vec<_> = editor
1600 .filter(|doc| doc.is_modified())
1603 .map(|path| path.to_string_lossy().to_string())
1604 .unwrap_or_else(|| "[scratch]".into())
1607 if !modified.is_empty() {
1609 "{} unsaved buffer(s) remaining: {:?}",
1618 cx: &mut compositor::Context,
1620 _event: PromptEvent,
1623 ) -> anyhow::Result<()> {
1624 let mut errors = String::new();
1626 // save all documents
1627 for (_, doc) in &mut cx.editor.documents {
1628 if doc.path().is_none() {
1629 errors.push_str("cannot write a buffer without a filename\n");
1633 // TODO: handle error.
1634 let handle = doc.save();
1635 cx.jobs.add(Job::new(handle).wait_before_exiting());
1640 buffers_remaining_impl(cx.editor)?;
1644 let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
1645 for view_id in views {
1646 cx.editor.close(view_id, false);
1654 cx: &mut compositor::Context,
1657 ) -> anyhow::Result<()> {
1658 write_all_impl(cx, args, event, false, false)
1662 cx: &mut compositor::Context,
1665 ) -> anyhow::Result<()> {
1666 write_all_impl(cx, args, event, true, false)
1669 fn force_write_all_quit(
1670 cx: &mut compositor::Context,
1673 ) -> anyhow::Result<()> {
1674 write_all_impl(cx, args, event, true, true)
1678 editor: &mut Editor,
1680 _event: PromptEvent,
1682 ) -> anyhow::Result<()> {
1684 buffers_remaining_impl(editor)?;
1688 let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
1689 for view_id in views {
1690 editor.close(view_id, false);
1697 cx: &mut compositor::Context,
1700 ) -> anyhow::Result<()> {
1701 quit_all_impl(&mut cx.editor, args, event, false)
1705 cx: &mut compositor::Context,
1708 ) -> anyhow::Result<()> {
1709 quit_all_impl(&mut cx.editor, args, event, true)
1713 cx: &mut compositor::Context,
1715 _event: PromptEvent,
1716 ) -> anyhow::Result<()> {
1717 let theme = args.first().context("theme not provided")?;
1718 cx.editor.set_theme_from_name(theme)
1721 fn yank_main_selection_to_clipboard(
1722 cx: &mut compositor::Context,
1724 _event: PromptEvent,
1725 ) -> anyhow::Result<()> {
1726 yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
1729 fn yank_joined_to_clipboard(
1730 cx: &mut compositor::Context,
1732 _event: PromptEvent,
1733 ) -> anyhow::Result<()> {
1734 let (_, doc) = current!(cx.editor);
1735 let separator = args
1738 .unwrap_or_else(|| doc.line_ending.as_str());
1739 yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
1742 fn yank_main_selection_to_primary_clipboard(
1743 cx: &mut compositor::Context,
1745 _event: PromptEvent,
1746 ) -> anyhow::Result<()> {
1747 yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
1750 fn yank_joined_to_primary_clipboard(
1751 cx: &mut compositor::Context,
1753 _event: PromptEvent,
1754 ) -> anyhow::Result<()> {
1755 let (_, doc) = current!(cx.editor);
1756 let separator = args
1759 .unwrap_or_else(|| doc.line_ending.as_str());
1760 yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
1763 fn paste_clipboard_after(
1764 cx: &mut compositor::Context,
1766 _event: PromptEvent,
1767 ) -> anyhow::Result<()> {
1768 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
1771 fn paste_clipboard_before(
1772 cx: &mut compositor::Context,
1774 _event: PromptEvent,
1775 ) -> anyhow::Result<()> {
1776 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
1779 fn paste_primary_clipboard_after(
1780 cx: &mut compositor::Context,
1782 _event: PromptEvent,
1783 ) -> anyhow::Result<()> {
1784 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
1787 fn paste_primary_clipboard_before(
1788 cx: &mut compositor::Context,
1790 _event: PromptEvent,
1791 ) -> anyhow::Result<()> {
1792 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
1795 fn replace_selections_with_clipboard_impl(
1796 cx: &mut compositor::Context,
1797 clipboard_type: ClipboardType,
1798 ) -> anyhow::Result<()> {
1799 let (view, doc) = current!(cx.editor);
1801 match cx.editor.clipboard_provider.get_contents(clipboard_type) {
1803 let selection = doc.selection(view.id);
1805 Transaction::change_by_selection(doc.text(), selection, |range| {
1806 (range.from(), range.to(), Some(contents.as_str().into()))
1809 doc.apply(&transaction, view.id);
1810 doc.append_changes_to_history(view.id);
1813 Err(e) => Err(e.context("Couldn't get system clipboard contents")),
1817 fn replace_selections_with_clipboard(
1818 cx: &mut compositor::Context,
1820 _event: PromptEvent,
1821 ) -> anyhow::Result<()> {
1822 replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
1825 fn replace_selections_with_primary_clipboard(
1826 cx: &mut compositor::Context,
1828 _event: PromptEvent,
1829 ) -> anyhow::Result<()> {
1830 replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
1833 fn show_clipboard_provider(
1834 cx: &mut compositor::Context,
1836 _event: PromptEvent,
1837 ) -> anyhow::Result<()> {
1839 .set_status(cx.editor.clipboard_provider.name().to_string());
1843 fn change_current_directory(
1844 cx: &mut compositor::Context,
1846 _event: PromptEvent,
1847 ) -> anyhow::Result<()> {
1848 let dir = args.first().context("target directory not provided")?;
1850 if let Err(e) = std::env::set_current_dir(dir) {
1851 bail!("Couldn't change the current working directory: {}", e);
1854 let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
1855 cx.editor.set_status(format!(
1856 "Current working directory is now {}",
1862 fn show_current_directory(
1863 cx: &mut compositor::Context,
1865 _event: PromptEvent,
1866 ) -> anyhow::Result<()> {
1867 let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
1869 .set_status(format!("Current working directory is {}", cwd.display()));
1873 /// Sets the [`Document`]'s encoding..
1875 cx: &mut compositor::Context,
1877 _event: PromptEvent,
1878 ) -> anyhow::Result<()> {
1879 let (_, doc) = current!(cx.editor);
1880 if let Some(label) = args.first() {
1881 doc.set_encoding(label)
1883 let encoding = doc.encoding().name().to_string();
1884 cx.editor.set_status(encoding);
1889 /// Reload the [`Document`] from its source file.
1891 cx: &mut compositor::Context,
1893 _event: PromptEvent,
1894 ) -> anyhow::Result<()> {
1895 let (view, doc) = current!(cx.editor);
1899 fn tree_sitter_scopes(
1900 cx: &mut compositor::Context,
1902 _event: PromptEvent,
1903 ) -> anyhow::Result<()> {
1904 let (view, doc) = current!(cx.editor);
1905 let text = doc.text().slice(..);
1907 let pos = doc.selection(view.id).primary().cursor(text);
1908 let scopes = indent::get_scopes(doc.syntax(), text, pos);
1909 cx.editor.set_status(format!("scopes: {:?}", &scopes));
1914 cx: &mut compositor::Context,
1916 _event: PromptEvent,
1917 ) -> anyhow::Result<()> {
1918 use helix_lsp::block_on;
1919 if let Some(debugger) = cx.editor.debugger.as_mut() {
1920 let id = debugger.stack_pointer.clone().map(|x| x.id);
1921 let response = block_on(debugger.eval(args.join(" "), id))?;
1922 cx.editor.set_status(response.result);
1927 fn impl_edit_breakpoint(
1928 cx: &mut compositor::Context,
1929 condition: Option<String>,
1930 log_message: Option<String>,
1932 use helix_lsp::block_on;
1934 let (view, doc) = current!(cx.editor);
1935 let text = doc.text().slice(..);
1936 let pos = doc.selection(view.id).primary().cursor(text);
1937 let breakpoint = helix_dap::SourceBreakpoint {
1938 line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
1941 ..Default::default()
1943 let path = match doc.path() {
1944 Some(path) => path.to_path_buf(),
1947 .set_error("Can't edit breakpoint: document has no path".to_string());
1951 if let Some(debugger) = &mut cx.editor.debugger {
1952 if breakpoint.condition.is_some()
1957 .supports_conditional_breakpoints
1958 .unwrap_or_default()
1960 cx.editor.set_error(
1961 "Can't edit breakpoint: debugger does not support conditional breakpoints"
1966 if breakpoint.log_message.is_some()
1971 .supports_log_points
1972 .unwrap_or_default()
1974 cx.editor.set_error(
1975 "Can't edit breakpoint: debugger does not support logpoints".to_string(),
1980 let breakpoints = debugger.breakpoints.entry(path.clone()).or_default();
1981 if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) {
1982 breakpoints.remove(pos);
1983 breakpoints.push(breakpoint);
1985 let breakpoints = breakpoints.clone();
1987 let request = debugger.set_breakpoints(path, breakpoints);
1988 let _ = block_on(request).unwrap();
1993 fn debug_breakpoint_condition(
1994 cx: &mut compositor::Context,
1996 _event: PromptEvent,
1997 ) -> anyhow::Result<()> {
1998 let condition = args.join(" ");
1999 let condition = if condition.is_empty() {
2005 impl_edit_breakpoint(cx, condition, None);
2009 fn debug_set_logpoint(
2010 cx: &mut compositor::Context,
2012 _event: PromptEvent,
2013 ) -> anyhow::Result<()> {
2014 let log_message = args.join(" ");
2015 let log_message = if log_message.is_empty() {
2021 impl_edit_breakpoint(cx, None, log_message);
2025 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2029 doc: "Close the current view.",
2036 doc: "Close the current view.",
2043 doc: "Open a file from disk into the current view.",
2045 completer: Some(completers::filename),
2050 doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2052 completer: Some(completers::filename),
2057 doc: "Create a new scratch buffer.",
2059 completer: Some(completers::filename),
2064 doc: "Format the file using a formatter.",
2069 name: "indent-style",
2071 doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
2072 fun: set_indent_style,
2076 name: "line-ending",
2078 doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2079 fun: set_line_ending,
2085 doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2092 doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2099 doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)",
2101 completer: Some(completers::filename),
2104 name: "write-quit!",
2106 doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2107 fun: force_write_quit,
2108 completer: Some(completers::filename),
2113 doc: "Writes changes from all views to disk.",
2118 name: "write-quit-all",
2120 doc: "Writes changes from all views to disk and close all views.",
2121 fun: write_all_quit,
2125 name: "write-quit-all!",
2126 alias: Some("wqa!"),
2127 doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
2128 fun: force_write_all_quit,
2134 doc: "Close all views.",
2141 doc: "Close all views forcefully (ignoring unsaved changes).",
2142 fun: force_quit_all,
2148 doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
2150 completer: Some(completers::theme),
2153 name: "clipboard-yank",
2155 doc: "Yank main selection into system clipboard.",
2156 fun: yank_main_selection_to_clipboard,
2160 name: "clipboard-yank-join",
2162 doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
2163 fun: yank_joined_to_clipboard,
2167 name: "primary-clipboard-yank",
2169 doc: "Yank main selection into system primary clipboard.",
2170 fun: yank_main_selection_to_primary_clipboard,
2174 name: "primary-clipboard-yank-join",
2176 doc: "Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
2177 fun: yank_joined_to_primary_clipboard,
2181 name: "clipboard-paste-after",
2183 doc: "Paste system clipboard after selections.",
2184 fun: paste_clipboard_after,
2188 name: "clipboard-paste-before",
2190 doc: "Paste system clipboard before selections.",
2191 fun: paste_clipboard_before,
2195 name: "clipboard-paste-replace",
2197 doc: "Replace selections with content of system clipboard.",
2198 fun: replace_selections_with_clipboard,
2202 name: "primary-clipboard-paste-after",
2204 doc: "Paste primary clipboard after selections.",
2205 fun: paste_primary_clipboard_after,
2209 name: "primary-clipboard-paste-before",
2211 doc: "Paste primary clipboard before selections.",
2212 fun: paste_primary_clipboard_before,
2216 name: "primary-clipboard-paste-replace",
2218 doc: "Replace selections with content of system primary clipboard.",
2219 fun: replace_selections_with_primary_clipboard,
2223 name: "show-clipboard-provider",
2225 doc: "Show clipboard provider name in status bar.",
2226 fun: show_clipboard_provider,
2230 name: "change-current-directory",
2232 doc: "Change the current working directory (:cd <dir>).",
2233 fun: change_current_directory,
2234 completer: Some(completers::directory),
2237 name: "show-directory",
2239 doc: "Show the current working directory.",
2240 fun: show_current_directory,
2246 doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
2253 doc: "Discard changes and reload from the source file.",
2258 name: "tree-sitter-scopes",
2260 doc: "Display tree sitter scopes, primarily for theming and development.",
2261 fun: tree_sitter_scopes,
2267 doc: "Evaluate expression in current debug context.",
2272 name: "debug-breakpoint-condition",
2274 doc: "Set current breakpoint condition.",
2275 fun: debug_breakpoint_condition,
2279 name: "debug-set-logpoint",
2281 doc: "Make current breakpoint a log point.",
2282 fun: debug_set_logpoint,
2287 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2288 let mut map = HashMap::new();
2290 for cmd in TYPABLE_COMMAND_LIST {
2291 map.insert(cmd.name, cmd);
2292 if let Some(alias) = cmd.alias {
2293 map.insert(alias, cmd);
2301fn command_mode(cx: &mut Context) {
2302 let mut prompt = Prompt::new(
2306 // we use .this over split_whitespace() because we care about empty segments
2307 let parts = input.split(' ').collect::<Vec<&str>>();
2309 // simple heuristic: if there's no just one part, complete command name.
2310 // if there's a space, per command completion kicks in.
2311 if parts.len() <= 1 {
2313 cmd::TYPABLE_COMMAND_LIST
2315 .filter(|command| command.name.contains(input))
2316 .map(|command| (end.clone(), Cow::Borrowed(command.name)))
2319 let part = parts.last().unwrap();
2321 if let Some(cmd::TypableCommand {
2322 completer: Some(completer),
2324 }) = cmd::COMMANDS.get(parts[0])
2328 .map(|(range, file)| {
2329 // offset ranges to input
2330 let offset = input.len() - part.len();
2331 let range = (range.start + offset)..;
2340 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
2341 if event != PromptEvent::Validate {
2345 let parts = input.split_whitespace().collect::<Vec<&str>>();
2346 if parts.is_empty() {
2350 if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2351 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2352 cx.editor.set_error(format!("{}", e));
2356 .set_error(format!("no such command: '{}'", parts[0]));
2360 prompt.doc_fn = Box::new(|input: &str| {
2361 let part = input.split(' ').next().unwrap_or_default();
2363 if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
2370 cx.push_layer(Box::new(prompt));
2373fn file_picker(cx: &mut Context) {
2374 let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
2375 let picker = ui::file_picker(root);
2376 cx.push_layer(Box::new(picker));
2379fn buffer_picker(cx: &mut Context) {
2380 let current = view!(cx.editor).doc;
2382 let picker = FilePicker::new(
2386 .map(|(id, doc)| (id, doc.relative_path()))
2388 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2390 match path.as_ref().and_then(|path| path.to_str()) {
2393 format!("{} (*)", path).into()
2398 None => "[scratch buffer]".into(),
2401 |editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
2402 editor.switch(*id, Action::Replace);
2404 |editor, (id, path)| {
2405 let doc = &editor.documents.get(*id)?;
2406 let &view_id = doc.selections().keys().next()?;
2410 .cursor_line(doc.text().slice(..));
2411 Some((path.clone()?, Some(line)))
2414 cx.push_layer(Box::new(picker));
2417fn symbol_picker(cx: &mut Context) {
2419 list: &mut Vec<lsp::SymbolInformation>,
2420 file: &lsp::TextDocumentIdentifier,
2421 symbol: lsp::DocumentSymbol,
2423 #[allow(deprecated)]
2424 list.push(lsp::SymbolInformation {
2428 deprecated: symbol.deprecated,
2429 location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
2430 container_name: None,
2432 for child in symbol.children.into_iter().flatten() {
2433 nested_to_flat(list, file, child);
2436 let (_, doc) = current!(cx.editor);
2438 let language_server = match doc.language_server() {
2439 Some(language_server) => language_server,
2442 let offset_encoding = language_server.offset_encoding();
2444 let future = language_server.document_symbols(doc.identifier());
2448 move |editor: &mut Editor,
2449 compositor: &mut Compositor,
2450 response: Option<lsp::DocumentSymbolResponse>| {
2451 if let Some(symbols) = response {
2452 // lsp has two ways to represent symbols (flat/nested)
2453 // convert the nested variant to flat, so that we have a homogeneous list
2454 let symbols = match symbols {
2455 lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
2456 lsp::DocumentSymbolResponse::Nested(symbols) => {
2457 let (_view, doc) = current!(editor);
2458 let mut flat_symbols = Vec::new();
2459 for symbol in symbols {
2460 nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
2466 let picker = FilePicker::new(
2468 |symbol| (&symbol.name).into(),
2469 move |editor: &mut Editor, symbol, _action| {
2471 let (view, doc) = current!(editor);
2473 if let Some(range) =
2474 lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
2476 doc.set_selection(view.id, Selection::single(range.anchor, range.head));
2477 align_view(doc, view, Align::Center);
2480 move |_editor, symbol| {
2481 let path = symbol.location.uri.to_file_path().unwrap();
2482 let line = Some(symbol.location.range.start.line as usize);
2486 compositor.push(Box::new(picker))
2492pub fn code_action(cx: &mut Context) {
2493 let (view, doc) = current!(cx.editor);
2495 let language_server = match doc.language_server() {
2496 Some(language_server) => language_server,
2500 let range = range_to_lsp_range(
2502 doc.selection(view.id).primary(),
2503 language_server.offset_encoding(),
2506 let future = language_server.code_actions(doc.identifier(), range);
2507 let offset_encoding = language_server.offset_encoding();
2511 move |_editor: &mut Editor,
2512 compositor: &mut Compositor,
2513 response: Option<lsp::CodeActionResponse>| {
2514 if let Some(actions) = response {
2515 let picker = Picker::new(
2518 |action| match action {
2519 lsp::CodeActionOrCommand::CodeAction(action) => {
2520 action.title.as_str().into()
2522 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
2524 move |editor, code_action, _action| match code_action {
2525 lsp::CodeActionOrCommand::Command(command) => {
2526 log::debug!("code action command: {:?}", command);
2527 editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2529 lsp::CodeActionOrCommand::CodeAction(code_action) => {
2530 log::debug!("code action: {:?}", code_action);
2531 if let Some(ref workspace_edit) = code_action.edit {
2532 apply_workspace_edit(editor, offset_encoding, workspace_edit)
2537 compositor.push(Box::new(picker))
2543fn apply_workspace_edit(
2544 editor: &mut Editor,
2545 offset_encoding: OffsetEncoding,
2546 workspace_edit: &lsp::WorkspaceEdit,
2548 if let Some(ref changes) = workspace_edit.changes {
2549 log::debug!("workspace changes: {:?}", changes);
2550 editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2552 // 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
2553 // TODO: find some example that uses workspace changes, and test it
2554 // for (url, edits) in changes.iter() {
2555 // let file_path = url.origin().ascii_serialization();
2556 // let file_path = std::path::PathBuf::from(file_path);
2557 // let file = std::fs::File::open(file_path).unwrap();
2558 // let mut text = Rope::from_reader(file).unwrap();
2559 // let transaction = edits_to_changes(&text, edits);
2560 // transaction.apply(&mut text);
2564 if let Some(ref document_changes) = workspace_edit.document_changes {
2565 match document_changes {
2566 lsp::DocumentChanges::Edits(document_edits) => {
2567 for document_edit in document_edits {
2568 let (view, doc) = current!(editor);
2569 assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
2570 let edits = document_edit
2573 .map(|edit| match edit {
2574 lsp::OneOf::Left(text_edit) => text_edit,
2575 lsp::OneOf::Right(annotated_text_edit) => {
2576 &annotated_text_edit.text_edit
2582 let transaction = helix_lsp::util::generate_transaction_from_edits(
2587 doc.apply(&transaction, view.id);
2588 doc.append_changes_to_history(view.id);
2591 lsp::DocumentChanges::Operations(operations) => {
2592 log::debug!("document changes - operations: {:?}", operations);
2593 editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2599fn last_picker(cx: &mut Context) {
2600 // TODO: last picker does not seem to work well with buffer_picker
2601 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
2602 if let Some(picker) = compositor.last_picker.take() {
2603 compositor.push(picker);
2605 // XXX: figure out how to show error when no last picker lifetime
2606 // cx.editor.set_error("no last picker".to_owned())
2610// I inserts at the first nonwhitespace character of each line with a selection
2611fn prepend_to_line(cx: &mut Context) {
2612 goto_first_nonwhitespace(cx);
2613 let doc = doc_mut!(cx.editor);
2614 enter_insert_mode(doc);
2617// A inserts at the end of each line with a selection
2618fn append_to_line(cx: &mut Context) {
2619 let (view, doc) = current!(cx.editor);
2620 enter_insert_mode(doc);
2622 let selection = doc.selection(view.id).clone().transform(|range| {
2623 let text = doc.text().slice(..);
2624 let line = range.cursor_line(text);
2625 let pos = line_end_char_index(&text, line);
2626 Range::new(pos, pos)
2628 doc.set_selection(view.id, selection);
2631/// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for
2632/// example because we just applied the same changes while saving.
2638// Creates an LspCallback that waits for formatting changes to be computed. When they're done,
2639// it applies them, but only if the doc hasn't changed.
2641// TODO: provide some way to cancel this, probably as part of a more general job cancellation
2643async fn make_format_callback(
2647 format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static,
2648) -> anyhow::Result<job::Callback> {
2649 let format = format.await;
2650 let call: job::Callback = Box::new(move |editor: &mut Editor, _compositor: &mut Compositor| {
2651 let view_id = view!(editor).id;
2652 if let Some(doc) = editor.document_mut(doc_id) {
2653 if doc.version() == doc_version {
2654 doc.apply(&Transaction::from(format), view_id);
2655 doc.append_changes_to_history(view_id);
2656 if let Modified::SetUnmodified = modified {
2657 doc.reset_modified();
2660 log::info!("discarded formatting changes because the document changed");
2672fn open(cx: &mut Context, open: Open) {
2673 let count = cx.count();
2674 let (view, doc) = current!(cx.editor);
2675 enter_insert_mode(doc);
2677 let text = doc.text().slice(..);
2678 let contents = doc.text();
2679 let selection = doc.selection(view.id);
2681 let mut ranges = SmallVec::with_capacity(selection.len());
2684 let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
2685 let line = range.cursor_line(text);
2687 let line = match open {
2688 // adjust position to the end of the line (next line - 1)
2689 Open::Below => line + 1,
2690 // adjust position to the end of the previous line (current line - 1)
2691 Open::Above => line,
2694 // Index to insert newlines after, as well as the char width
2695 // to use to compensate for those inserted newlines.
2696 let (line_end_index, line_end_offset_width) = if line == 0 {
2700 line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)),
2701 doc.line_ending.len_chars(),
2705 // TODO: share logic with insert_newline for indentation
2706 let indent_level = indent::suggested_indent_for_pos(
2707 doc.language_config(),
2713 let indent = doc.indent_unit().repeat(indent_level);
2714 let indent_len = indent.len();
2715 let mut text = String::with_capacity(1 + indent_len);
2716 text.push_str(doc.line_ending.as_str());
2717 text.push_str(&indent);
2718 let text = text.repeat(count);
2720 // calculate new selection ranges
2721 let pos = offs + line_end_index + line_end_offset_width;
2723 // pos -> beginning of reference line,
2724 // + (i * (1+indent_len)) -> beginning of i'th line from pos
2725 // + indent_len -> -> indent for i'th line
2726 ranges.push(Range::point(pos + (i * (1 + indent_len)) + indent_len));
2729 offs += text.chars().count();
2731 (line_end_index, line_end_index, Some(text.into()))
2734 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
2736 doc.apply(&transaction, view.id);
2739// o inserts a new line after each line with a selection
2740fn open_below(cx: &mut Context) {
2741 open(cx, Open::Below)
2744// O inserts a new line before each line with a selection
2745fn open_above(cx: &mut Context) {
2746 open(cx, Open::Above)
2749fn normal_mode(cx: &mut Context) {
2750 let (view, doc) = current!(cx.editor);
2752 doc.mode = Mode::Normal;
2754 doc.append_changes_to_history(view.id);
2756 // if leaving append mode, move cursor back by 1
2757 if doc.restore_cursor {
2758 let text = doc.text().slice(..);
2759 let selection = doc.selection(view.id).clone().transform(|range| {
2762 graphemes::prev_grapheme_boundary(text, range.to()),
2765 doc.set_selection(view.id, selection);
2767 doc.restore_cursor = false;
2771// Store a jump on the jumplist.
2772fn push_jump(editor: &mut Editor) {
2773 let (view, doc) = current!(editor);
2774 let jump = (doc.id(), doc.selection(view.id).clone());
2775 view.jumps.push(jump);
2778fn goto_line(cx: &mut Context) {
2779 if let Some(count) = cx.count {
2780 push_jump(cx.editor);
2782 let (view, doc) = current!(cx.editor);
2783 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
2784 // If the last line is blank, don't jump to it.
2785 doc.text().len_lines().saturating_sub(2)
2787 doc.text().len_lines() - 1
2789 let line_idx = std::cmp::min(count.get() - 1, max_line);
2790 let pos = doc.text().line_to_char(line_idx);
2791 doc.set_selection(view.id, Selection::point(pos));
2795fn goto_last_line(cx: &mut Context) {
2796 push_jump(cx.editor);
2798 let (view, doc) = current!(cx.editor);
2799 let line_idx = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
2800 // If the last line is blank, don't jump to it.
2801 doc.text().len_lines().saturating_sub(2)
2803 doc.text().len_lines() - 1
2805 let pos = doc.text().line_to_char(line_idx);
2806 doc.set_selection(view.id, Selection::point(pos));
2809fn goto_last_accessed_file(cx: &mut Context) {
2810 let alternate_file = view!(cx.editor).last_accessed_doc;
2811 if let Some(alt) = alternate_file {
2812 cx.editor.switch(alt, Action::Replace);
2814 cx.editor.set_error("no last accessed buffer".to_owned())
2818fn select_mode(cx: &mut Context) {
2819 let (view, doc) = current!(cx.editor);
2820 let text = doc.text().slice(..);
2822 // Make sure end-of-document selections are also 1-width.
2823 // (With the exception of being in an empty document, of course.)
2824 let selection = doc.selection(view.id).clone().transform(|range| {
2825 if range.is_empty() && range.head == text.len_chars() {
2827 graphemes::prev_grapheme_boundary(text, range.anchor),
2834 doc.set_selection(view.id, selection);
2836 doc_mut!(cx.editor).mode = Mode::Select;
2839fn exit_select_mode(cx: &mut Context) {
2840 let doc = doc_mut!(cx.editor);
2841 if doc.mode == Mode::Select {
2842 doc.mode = Mode::Normal;
2847 editor: &mut Editor,
2848 compositor: &mut Compositor,
2849 locations: Vec<lsp::Location>,
2850 offset_encoding: OffsetEncoding,
2855 editor: &mut Editor,
2856 location: &lsp::Location,
2857 offset_encoding: OffsetEncoding,
2863 .expect("unable to convert URI to filepath");
2864 let _id = editor.open(path, action).expect("editor.open failed");
2865 let (view, doc) = current!(editor);
2866 let definition_pos = location.range.start;
2867 // TODO: convert inside server
2869 if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) {
2874 doc.set_selection(view.id, Selection::point(new_pos));
2875 align_view(doc, view, Align::Center);
2878 let cwdir = std::env::current_dir().expect("couldn't determine current directory");
2880 match locations.as_slice() {
2882 jump_to(editor, location, offset_encoding, Action::Replace);
2885 editor.set_error("No definition found.".to_string());
2888 let picker = FilePicker::new(
2891 let file: Cow<'_, str> = (location.uri.scheme() == "file")
2897 // strip root prefix
2898 path.strip_prefix(&cwdir)
2899 .map(|path| path.to_path_buf())
2903 .and_then(|path| path.to_str().map(|path| path.to_owned().into()))
2906 .unwrap_or_else(|| location.uri.as_str().into());
2907 let line = location.range.start.line;
2908 format!("{}:{}", file, line).into()
2910 move |editor: &mut Editor, location, action| {
2911 jump_to(editor, location, offset_encoding, action)
2913 |_editor, location| {
2914 let path = location.uri.to_file_path().unwrap();
2915 let line = Some(location.range.start.line as usize);
2919 compositor.push(Box::new(picker));
2924fn goto_definition(cx: &mut Context) {
2925 let (view, doc) = current!(cx.editor);
2926 let language_server = match doc.language_server() {
2927 Some(language_server) => language_server,
2931 let offset_encoding = language_server.offset_encoding();
2933 let pos = pos_to_lsp_pos(
2935 doc.selection(view.id)
2937 .cursor(doc.text().slice(..)),
2941 let future = language_server.goto_definition(doc.identifier(), pos, None);
2945 move |editor: &mut Editor,
2946 compositor: &mut Compositor,
2947 response: Option<lsp::GotoDefinitionResponse>| {
2948 let items = match response {
2949 Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
2950 Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
2951 Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
2953 .map(|location_link| lsp::Location {
2954 uri: location_link.target_uri,
2955 range: location_link.target_range,
2961 goto_impl(editor, compositor, items, offset_encoding);
2966fn goto_type_definition(cx: &mut Context) {
2967 let (view, doc) = current!(cx.editor);
2968 let language_server = match doc.language_server() {
2969 Some(language_server) => language_server,
2973 let offset_encoding = language_server.offset_encoding();
2975 let pos = pos_to_lsp_pos(
2977 doc.selection(view.id)
2979 .cursor(doc.text().slice(..)),
2983 let future = language_server.goto_type_definition(doc.identifier(), pos, None);
2987 move |editor: &mut Editor,
2988 compositor: &mut Compositor,
2989 response: Option<lsp::GotoDefinitionResponse>| {
2990 let items = match response {
2991 Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
2992 Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
2993 Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
2995 .map(|location_link| lsp::Location {
2996 uri: location_link.target_uri,
2997 range: location_link.target_range,
3003 goto_impl(editor, compositor, items, offset_encoding);
3008fn goto_implementation(cx: &mut Context) {
3009 let (view, doc) = current!(cx.editor);
3010 let language_server = match doc.language_server() {
3011 Some(language_server) => language_server,
3015 let offset_encoding = language_server.offset_encoding();
3017 let pos = pos_to_lsp_pos(
3019 doc.selection(view.id)
3021 .cursor(doc.text().slice(..)),
3025 let future = language_server.goto_implementation(doc.identifier(), pos, None);
3029 move |editor: &mut Editor,
3030 compositor: &mut Compositor,
3031 response: Option<lsp::GotoDefinitionResponse>| {
3032 let items = match response {
3033 Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
3034 Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
3035 Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
3037 .map(|location_link| lsp::Location {
3038 uri: location_link.target_uri,
3039 range: location_link.target_range,
3045 goto_impl(editor, compositor, items, offset_encoding);
3050fn goto_reference(cx: &mut Context) {
3051 let (view, doc) = current!(cx.editor);
3052 let language_server = match doc.language_server() {
3053 Some(language_server) => language_server,
3057 let offset_encoding = language_server.offset_encoding();
3059 let pos = pos_to_lsp_pos(
3061 doc.selection(view.id)
3063 .cursor(doc.text().slice(..)),
3067 let future = language_server.goto_reference(doc.identifier(), pos, None);
3071 move |editor: &mut Editor,
3072 compositor: &mut Compositor,
3073 items: Option<Vec<lsp::Location>>| {
3077 items.unwrap_or_default(),
3084fn goto_pos(editor: &mut Editor, pos: usize) {
3087 let (view, doc) = current!(editor);
3089 doc.set_selection(view.id, Selection::point(pos));
3090 align_view(doc, view, Align::Center);
3093fn goto_first_diag(cx: &mut Context) {
3094 let editor = &mut cx.editor;
3095 let (_, doc) = current!(editor);
3097 let diag = if let Some(diag) = doc.diagnostics().first() {
3103 goto_pos(editor, diag);
3106fn goto_last_diag(cx: &mut Context) {
3107 let editor = &mut cx.editor;
3108 let (_, doc) = current!(editor);
3110 let diag = if let Some(diag) = doc.diagnostics().last() {
3116 goto_pos(editor, diag);
3119fn goto_next_diag(cx: &mut Context) {
3120 let editor = &mut cx.editor;
3121 let (view, doc) = current!(editor);
3123 let cursor_pos = doc
3126 .cursor(doc.text().slice(..));
3127 let diag = if let Some(diag) = doc
3130 .map(|diag| diag.range.start)
3131 .find(|&pos| pos > cursor_pos)
3134 } else if let Some(diag) = doc.diagnostics().first() {
3140 goto_pos(editor, diag);
3143fn goto_prev_diag(cx: &mut Context) {
3144 let editor = &mut cx.editor;
3145 let (view, doc) = current!(editor);
3147 let cursor_pos = doc
3150 .cursor(doc.text().slice(..));
3151 let diag = if let Some(diag) = doc
3155 .map(|diag| diag.range.start)
3156 .find(|&pos| pos < cursor_pos)
3159 } else if let Some(diag) = doc.diagnostics().last() {
3165 goto_pos(editor, diag);
3168fn signature_help(cx: &mut Context) {
3169 let (view, doc) = current!(cx.editor);
3171 let language_server = match doc.language_server() {
3172 Some(language_server) => language_server,
3176 let pos = pos_to_lsp_pos(
3178 doc.selection(view.id)
3180 .cursor(doc.text().slice(..)),
3181 language_server.offset_encoding(),
3184 let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
3188 move |_editor: &mut Editor,
3189 _compositor: &mut Compositor,
3190 response: Option<lsp::SignatureHelp>| {
3191 if let Some(signature_help) = response {
3192 log::info!("{:?}", signature_help);
3202 // with active param highlighted
3208// NOTE: Transactions in this module get appended to history when we switch back to normal mode.
3211 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3212 pub type PostHook = fn(&mut Context, char);
3214 fn completion(cx: &mut Context, ch: char) {
3215 // if ch matches completion char, trigger completion
3216 let doc = doc_mut!(cx.editor);
3217 let language_server = match doc.language_server() {
3218 Some(language_server) => language_server,
3222 let capabilities = language_server.capabilities();
3224 if let lsp::ServerCapabilities {
3225 completion_provider:
3226 Some(lsp::CompletionOptions {
3227 trigger_characters: Some(triggers),
3233 // TODO: what if trigger is multiple chars long
3234 let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3237 super::completion(cx);
3242 fn signature_help(cx: &mut Context, ch: char) {
3243 // if ch matches signature_help char, trigger
3244 let doc = doc_mut!(cx.editor);
3245 let language_server = match doc.language_server() {
3246 Some(language_server) => language_server,
3250 let capabilities = language_server.capabilities();
3252 if let lsp::ServerCapabilities {
3253 signature_help_provider:
3254 Some(lsp::SignatureHelpOptions {
3255 trigger_characters: Some(triggers),
3256 // TODO: retrigger_characters
3262 // TODO: what if trigger is multiple chars long
3263 let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3266 super::signature_help(cx);
3272 // SignatureInformation {
3273 // label: "fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error>",
3274 // documentation: None,
3275 // parameters: Some(
3276 // [ParameterInformation { label: Simple("path: PathBuf"), documentation: None },
3277 // ParameterInformation { label: Simple("action: Action"), documentation: None }]
3279 // active_parameter: Some(0)
3282 // active_signature: None, active_parameter: Some(0)
3286 // The default insert hook: simply insert the character
3287 #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
3288 fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
3289 let t = Tendril::from_char(ch);
3290 let transaction = Transaction::insert(doc, selection, t);
3294 use helix_core::auto_pairs;
3295 const HOOKS: &[Hook] = &[auto_pairs::hook, insert];
3296 const POST_HOOKS: &[PostHook] = &[completion, signature_help];
3298 pub fn insert_char(cx: &mut Context, c: char) {
3299 let (view, doc) = current!(cx.editor);
3301 let text = doc.text();
3302 let selection = doc.selection(view.id).clone().cursors(text.slice(..));
3304 // run through insert hooks, stopping on the first one that returns Some(t)
3306 if let Some(transaction) = hook(text, &selection, c) {
3307 doc.apply(&transaction, view.id);
3312 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3313 // this could also generically look at Transaction, but it's a bit annoying to look at
3314 // Operation instead of Change.
3315 for hook in POST_HOOKS {
3320 pub fn insert_tab(cx: &mut Context) {
3321 let (view, doc) = current!(cx.editor);
3322 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
3323 // indent by one to reach 4 spaces).
3325 let indent = Tendril::from(doc.indent_unit());
3326 let transaction = Transaction::insert(
3328 &doc.selection(view.id).clone().cursors(doc.text().slice(..)),
3331 doc.apply(&transaction, view.id);
3334 pub fn insert_newline(cx: &mut Context) {
3335 let (view, doc) = current!(cx.editor);
3336 let text = doc.text().slice(..);
3338 let contents = doc.text();
3339 let selection = doc.selection(view.id).clone().cursors(text);
3340 let mut ranges = SmallVec::with_capacity(selection.len());
3342 // TODO: this is annoying, but we need to do it to properly calculate pos after edits
3345 let mut transaction = Transaction::change_by_selection(contents, &selection, |range| {
3346 let pos = range.head;
3348 let prev = if pos == 0 {
3351 contents.char(pos - 1)
3353 let curr = contents.get_char(pos).unwrap_or(' ');
3355 // TODO: offset range.head by 1? when calculating?
3356 let indent_level = indent::suggested_indent_for_pos(
3357 doc.language_config(),
3360 pos.saturating_sub(1),
3363 let indent = doc.indent_unit().repeat(indent_level);
3364 let mut text = String::with_capacity(1 + indent.len());
3365 text.push_str(doc.line_ending.as_str());
3366 text.push_str(&indent);
3368 let head = pos + offs + text.chars().count();
3370 // TODO: range replace or extend
3371 // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
3372 // can be used with cx.mode to do replace or extend on most changes
3373 ranges.push(Range::new(
3374 if range.is_empty() {
3382 // if between a bracket pair
3383 if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
3384 // another newline, indent the end bracket one level less
3385 let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
3386 text.push_str(doc.line_ending.as_str());
3387 text.push_str(&indent);
3390 offs += text.chars().count();
3392 (pos, pos, Some(text.into()))
3395 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
3398 doc.apply(&transaction, view.id);
3401 // TODO: handle indent-aware delete
3402 pub fn delete_char_backward(cx: &mut Context) {
3403 let count = cx.count();
3404 let (view, doc) = current!(cx.editor);
3405 let text = doc.text().slice(..);
3407 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3408 let pos = range.cursor(text);
3410 graphemes::nth_prev_grapheme_boundary(text, pos, count),
3415 doc.apply(&transaction, view.id);
3418 pub fn delete_char_forward(cx: &mut Context) {
3419 let count = cx.count();
3420 let (view, doc) = current!(cx.editor);
3421 let text = doc.text().slice(..);
3423 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3424 let pos = range.cursor(text);
3427 graphemes::nth_next_grapheme_boundary(text, pos, count),
3431 doc.apply(&transaction, view.id);
3434 pub fn delete_word_backward(cx: &mut Context) {
3435 let count = cx.count();
3436 let (view, doc) = current!(cx.editor);
3437 let text = doc.text().slice(..);
3442 .transform(|range| movement::move_prev_word_start(text, range, count));
3443 doc.set_selection(view.id, selection);
3444 delete_selection(cx)
3450// TODO: each command could simply return a Option<transaction>, then the higher level handles
3453fn undo(cx: &mut Context) {
3454 let (view, doc) = current!(cx.editor);
3455 let view_id = view.id;
3459fn redo(cx: &mut Context) {
3460 let (view, doc) = current!(cx.editor);
3461 let view_id = view.id;
3467fn yank(cx: &mut Context) {
3468 let (view, doc) = current!(cx.editor);
3469 let text = doc.text().slice(..);
3471 let values: Vec<String> = doc
3474 .map(Cow::into_owned)
3478 "yanked {} selection(s) to register {}",
3480 cx.selected_register.name()
3485 .write(cx.selected_register.name(), values);
3487 cx.editor.set_status(msg);
3488 exit_select_mode(cx);
3491fn yank_joined_to_clipboard_impl(
3492 editor: &mut Editor,
3494 clipboard_type: ClipboardType,
3495) -> anyhow::Result<()> {
3496 let (view, doc) = current!(editor);
3497 let text = doc.text().slice(..);
3499 let values: Vec<String> = doc
3502 .map(Cow::into_owned)
3506 "joined and yanked {} selection(s) to system clipboard",
3510 let joined = values.join(separator);
3514 .set_contents(joined, clipboard_type)
3515 .context("Couldn't set system clipboard content")?;
3517 editor.set_status(msg);
3522fn yank_joined_to_clipboard(cx: &mut Context) {
3523 let line_ending = current!(cx.editor).1.line_ending;
3524 let _ = yank_joined_to_clipboard_impl(
3526 line_ending.as_str(),
3527 ClipboardType::Clipboard,
3529 exit_select_mode(cx);
3532fn yank_main_selection_to_clipboard_impl(
3533 editor: &mut Editor,
3534 clipboard_type: ClipboardType,
3535) -> anyhow::Result<()> {
3536 let (view, doc) = current!(editor);
3537 let text = doc.text().slice(..);
3539 let value = doc.selection(view.id).primary().fragment(text);
3541 if let Err(e) = editor
3543 .set_contents(value.into_owned(), clipboard_type)
3545 bail!("Couldn't set system clipboard content: {}", e);
3548 editor.set_status("yanked main selection to system clipboard".to_owned());
3552fn yank_main_selection_to_clipboard(cx: &mut Context) {
3553 let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
3556fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3557 let line_ending = current!(cx.editor).1.line_ending;
3558 let _ = yank_joined_to_clipboard_impl(
3560 line_ending.as_str(),
3561 ClipboardType::Selection,
3565fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
3566 let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
3567 exit_select_mode(cx);
3570#[derive(Copy, Clone)]
3581) -> Option<Transaction> {
3582 let repeat = std::iter::repeat(
3585 .map(|value| Tendril::from_slice(value))
3589 // if any of values ends with a line ending, it's linewise paste
3590 let linewise = values
3592 .any(|value| get_line_ending_of_str(value).is_some());
3594 let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
3596 let text = doc.text();
3597 let selection = doc.selection(view.id);
3599 let transaction = Transaction::change_by_selection(text, selection, |range| {
3600 let pos = match (action, linewise) {
3601 // paste linewise before
3602 (Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())),
3603 // paste linewise after
3604 (Paste::After, true) => {
3605 let line = range.line_range(text.slice(..)).1;
3606 text.line_to_char((line + 1).min(text.len_lines()))
3609 (Paste::Before, false) => range.from(),
3611 (Paste::After, false) => range.to(),
3613 (pos, pos, Some(values.next().unwrap()))
3619fn paste_clipboard_impl(
3620 editor: &mut Editor,
3622 clipboard_type: ClipboardType,
3623) -> anyhow::Result<()> {
3624 let (view, doc) = current!(editor);
3628 .get_contents(clipboard_type)
3629 .map(|contents| paste_impl(&[contents], doc, view, action))
3631 Ok(Some(transaction)) => {
3632 doc.apply(&transaction, view.id);
3633 doc.append_changes_to_history(view.id);
3637 Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3641fn paste_clipboard_after(cx: &mut Context) {
3642 let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
3645fn paste_clipboard_before(cx: &mut Context) {
3646 let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
3649fn paste_primary_clipboard_after(cx: &mut Context) {
3650 let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
3653fn paste_primary_clipboard_before(cx: &mut Context) {
3654 let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
3657fn replace_with_yanked(cx: &mut Context) {
3658 let reg_name = cx.selected_register.name();
3659 let (view, doc) = current!(cx.editor);
3660 let registers = &mut cx.editor.registers;
3662 if let Some(values) = registers.read(reg_name) {
3663 if let Some(yank) = values.first() {
3664 let selection = doc.selection(view.id);
3665 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3666 if !range.is_empty() {
3667 (range.from(), range.to(), Some(yank.as_str().into()))
3669 (range.from(), range.to(), None)
3673 doc.apply(&transaction, view.id);
3674 doc.append_changes_to_history(view.id);
3679fn replace_selections_with_clipboard_impl(
3680 editor: &mut Editor,
3681 clipboard_type: ClipboardType,
3682) -> anyhow::Result<()> {
3683 let (view, doc) = current!(editor);
3685 match editor.clipboard_provider.get_contents(clipboard_type) {
3687 let selection = doc.selection(view.id);
3688 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3689 (range.from(), range.to(), Some(contents.as_str().into()))
3692 doc.apply(&transaction, view.id);
3693 doc.append_changes_to_history(view.id);
3696 Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3700fn replace_selections_with_clipboard(cx: &mut Context) {
3701 let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
3704fn replace_selections_with_primary_clipboard(cx: &mut Context) {
3705 let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
3708fn paste_after(cx: &mut Context) {
3709 let reg_name = cx.selected_register.name();
3710 let (view, doc) = current!(cx.editor);
3711 let registers = &mut cx.editor.registers;
3713 if let Some(transaction) = registers
3715 .and_then(|values| paste_impl(values, doc, view, Paste::After))
3717 doc.apply(&transaction, view.id);
3718 doc.append_changes_to_history(view.id);
3722fn paste_before(cx: &mut Context) {
3723 let reg_name = cx.selected_register.name();
3724 let (view, doc) = current!(cx.editor);
3725 let registers = &mut cx.editor.registers;
3727 if let Some(transaction) = registers
3729 .and_then(|values| paste_impl(values, doc, view, Paste::Before))
3731 doc.apply(&transaction, view.id);
3732 doc.append_changes_to_history(view.id);
3736fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
3737 let mut lines = Vec::new();
3739 // Get all line numbers
3740 for range in doc.selection(view_id) {
3741 let (start, end) = range.line_range(doc.text().slice(..));
3743 for line in start..=end {
3747 lines.sort_unstable(); // sorting by usize so _unstable is preferred
3752fn indent(cx: &mut Context) {
3753 let count = cx.count();
3754 let (view, doc) = current!(cx.editor);
3755 let lines = get_lines(doc, view.id);
3757 // Indent by one level
3758 let indent = Tendril::from(doc.indent_unit().repeat(count));
3760 let transaction = Transaction::change(
3762 lines.into_iter().map(|line| {
3763 let pos = doc.text().line_to_char(line);
3764 (pos, pos, Some(indent.clone()))
3767 doc.apply(&transaction, view.id);
3768 doc.append_changes_to_history(view.id);
3771fn unindent(cx: &mut Context) {
3772 let count = cx.count();
3773 let (view, doc) = current!(cx.editor);
3774 let lines = get_lines(doc, view.id);
3775 let mut changes = Vec::with_capacity(lines.len());
3776 let tab_width = doc.tab_width();
3777 let indent_width = count * tab_width;
3779 for line_idx in lines {
3780 let line = doc.text().line(line_idx);
3784 for ch in line.chars() {
3787 '\t' => width = (width / tab_width + 1) * tab_width,
3793 if width >= indent_width {
3798 // now delete from start to first non-blank
3800 let start = doc.text().line_to_char(line_idx);
3801 changes.push((start, start + pos, None))
3805 let transaction = Transaction::change(doc.text(), changes.into_iter());
3807 doc.apply(&transaction, view.id);
3808 doc.append_changes_to_history(view.id);
3811fn format_selections(cx: &mut Context) {
3812 let (view, doc) = current!(cx.editor);
3814 // via lsp if available
3815 // else via tree-sitter indentation calculations
3817 let language_server = match doc.language_server() {
3818 Some(language_server) => language_server,
3822 let ranges: Vec<lsp::Range> = doc
3825 .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
3828 // TODO: all of the TODO's and commented code inside the loop,
3829 // to make this actually work.
3830 for _range in ranges {
3831 let _language_server = match doc.language_server() {
3832 Some(language_server) => language_server,
3835 // TODO: handle fails
3836 // TODO: concurrent map
3838 // TODO: need to block to get the formatting
3840 // let edits = block_on(language_server.text_document_range_formatting(
3841 // doc.identifier(),
3843 // lsp::FormattingOptions::default(),
3845 // .unwrap_or_default();
3847 // let transaction = helix_lsp::util::generate_transaction_from_edits(
3850 // language_server.offset_encoding(),
3853 // doc.apply(&transaction, view.id);
3856 doc.append_changes_to_history(view.id);
3859fn join_selections(cx: &mut Context) {
3860 use movement::skip_while;
3861 let (view, doc) = current!(cx.editor);
3862 let text = doc.text();
3863 let slice = doc.text().slice(..);
3865 let mut changes = Vec::new();
3866 let fragment = Tendril::from(" ");
3868 for selection in doc.selection(view.id) {
3869 let (start, mut end) = selection.line_range(slice);
3871 end = (end + 1).min(text.len_lines() - 1);
3873 let lines = start..end;
3875 changes.reserve(lines.len());
3878 let start = line_end_char_index(&slice, line);
3879 let mut end = text.line_to_char(line + 1);
3880 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
3882 // need to skip from start, not end
3883 let change = (start, end, Some(fragment.clone()));
3884 changes.push(change);
3888 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
3891 // TODO: joining multiple empty lines should be replaced by a single space.
3892 // need to merge change ranges that touch
3894 let transaction = Transaction::change(doc.text(), changes.into_iter());
3895 // TODO: select inserted spaces
3896 // .with_selection(selection);
3898 doc.apply(&transaction, view.id);
3899 doc.append_changes_to_history(view.id);
3902fn keep_selections(cx: &mut Context) {
3903 // keep selections matching regex
3904 let prompt = ui::regex_prompt(cx, "keep:".to_string(), move |view, doc, _, regex| {
3905 let text = doc.text().slice(..);
3907 if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) {
3908 doc.set_selection(view.id, selection);
3912 cx.push_layer(Box::new(prompt));
3915fn keep_primary_selection(cx: &mut Context) {
3916 let (view, doc) = current!(cx.editor);
3918 let range = doc.selection(view.id).primary();
3919 doc.set_selection(view.id, Selection::single(range.anchor, range.head));
3922fn completion(cx: &mut Context) {
3923 // trigger on trigger char, or if user calls it
3924 // (or on word char typing??)
3925 // after it's triggered, if response marked is_incomplete, update on every subsequent keypress
3927 // lsp calls are done via a callback: it sends a request and doesn't block.
3928 // when we get the response similarly to notification, trigger a call to the completion popup
3930 // language_server.completion(params, |cx: &mut Context, _meta, response| {
3931 // // called at response time
3932 // // compositor, lookup completion layer
3933 // // downcast dyn Component to Completion component
3934 // // emit response to completion (completion.complete/handle(response))
3937 // typing after prompt opens: usually start offset is tracked and everything between
3938 // start_offset..cursor is replaced. For our purposes we could keep the start state (doc,
3939 // selection) and revert to them before applying. This needs to properly reset changes/history
3942 // company-mode does this by matching the prefix of the completion and removing it.
3944 // ignore isIncomplete for now
3945 // keep state while typing
3946 // the behavior should be, filter the menu based on input
3947 // if items returns empty at any point, remove the popup
3948 // if backspace past initial offset point, remove the popup
3950 // debounce requests!
3952 // need an idle timeout thing.
3953 // https://github.com/company-mode/company-mode/blob/master/company.el#L620-L622
3955 // "The idle delay in seconds until completion starts automatically.
3956 // The prefix still has to satisfy `company-minimum-prefix-length' before that
3957 // happens. The value of nil means no idle completion."
3959 let (view, doc) = current!(cx.editor);
3961 let language_server = match doc.language_server() {
3962 Some(language_server) => language_server,
3966 let offset_encoding = language_server.offset_encoding();
3970 .cursor(doc.text().slice(..));
3972 let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
3974 let future = language_server.completion(doc.identifier(), pos, None);
3976 let trigger_offset = cursor;
3980 move |editor: &mut Editor,
3981 compositor: &mut Compositor,
3982 response: Option<lsp::CompletionResponse>| {
3983 let (_, doc) = current!(editor);
3984 if doc.mode() != Mode::Insert {
3985 // we're not in insert mode anymore
3989 let items = match response {
3990 Some(lsp::CompletionResponse::Array(items)) => items,
3991 // TODO: do something with is_incomplete
3992 Some(lsp::CompletionResponse::List(lsp::CompletionList {
3993 is_incomplete: _is_incomplete,
3999 if items.is_empty() {
4000 editor.set_error("No completion available".to_string());
4003 let size = compositor.size();
4005 .find(std::any::type_name::<ui::EditorView>())
4007 if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
4008 ui.set_completion(items, offset_encoding, trigger_offset, size);
4014fn hover(cx: &mut Context) {
4015 let (view, doc) = current!(cx.editor);
4017 let language_server = match doc.language_server() {
4018 Some(language_server) => language_server,
4022 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
4024 let pos = pos_to_lsp_pos(
4026 doc.selection(view.id)
4028 .cursor(doc.text().slice(..)),
4029 language_server.offset_encoding(),
4032 let future = language_server.text_document_hover(doc.identifier(), pos, None);
4036 move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
4037 if let Some(hover) = response {
4038 // hover.contents / .range <- used for visualizing
4039 let contents = match hover.contents {
4040 lsp::HoverContents::Scalar(contents) => {
4041 // markedstring(string/languagestring to be highlighted)
4043 log::error!("hover contents {:?}", contents);
4046 lsp::HoverContents::Array(contents) => {
4047 log::error!("hover contents {:?}", contents);
4050 // TODO: render markdown
4051 lsp::HoverContents::Markup(contents) => contents.value,
4054 // skip if contents empty
4056 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
4057 let popup = Popup::new(contents);
4058 compositor.push(Box::new(popup));
4065fn toggle_comments(cx: &mut Context) {
4066 let (view, doc) = current!(cx.editor);
4069 .and_then(|lc| lc.comment_token.as_ref())
4070 .map(|tc| tc.as_ref());
4071 let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
4073 doc.apply(&transaction, view.id);
4074 doc.append_changes_to_history(view.id);
4075 exit_select_mode(cx);
4078fn rotate_selections(cx: &mut Context, direction: Direction) {
4079 let count = cx.count();
4080 let (view, doc) = current!(cx.editor);
4081 let mut selection = doc.selection(view.id).clone();
4082 let index = selection.primary_index();
4083 let len = selection.len();
4084 selection.set_primary_index(match direction {
4085 Direction::Forward => (index + count) % len,
4086 Direction::Backward => (index + (len.saturating_sub(count) % len)) % len,
4088 doc.set_selection(view.id, selection);
4090fn rotate_selections_forward(cx: &mut Context) {
4091 rotate_selections(cx, Direction::Forward)
4093fn rotate_selections_backward(cx: &mut Context) {
4094 rotate_selections(cx, Direction::Backward)
4097fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
4098 let count = cx.count;
4099 let (view, doc) = current!(cx.editor);
4100 let text = doc.text().slice(..);
4102 let selection = doc.selection(view.id);
4103 let mut fragments: Vec<_> = selection
4105 .map(|fragment| Tendril::from_slice(&fragment))
4109 .map(|count| count.get())
4110 .unwrap_or(fragments.len()) // default to rotating everything as one group
4111 .min(fragments.len());
4113 for chunk in fragments.chunks_mut(group) {
4114 // TODO: also modify main index
4116 Direction::Forward => chunk.rotate_right(1),
4117 Direction::Backward => chunk.rotate_left(1),
4121 let transaction = Transaction::change(
4127 .map(|(range, fragment)| (range.from(), range.to(), Some(fragment))),
4130 doc.apply(&transaction, view.id);
4131 doc.append_changes_to_history(view.id);
4133fn rotate_selection_contents_forward(cx: &mut Context) {
4134 rotate_selection_contents(cx, Direction::Forward)
4136fn rotate_selection_contents_backward(cx: &mut Context) {
4137 rotate_selection_contents(cx, Direction::Backward)
4140// tree sitter node selection
4142fn expand_selection(cx: &mut Context) {
4143 let (view, doc) = current!(cx.editor);
4145 if let Some(syntax) = doc.syntax() {
4146 let text = doc.text().slice(..);
4147 let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4148 doc.set_selection(view.id, selection);
4152fn match_brackets(cx: &mut Context) {
4153 let (view, doc) = current!(cx.editor);
4155 if let Some(syntax) = doc.syntax() {
4159 .cursor(doc.text().slice(..));
4160 if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
4161 let selection = Selection::point(pos);
4162 doc.set_selection(view.id, selection);
4169fn jump_forward(cx: &mut Context) {
4170 let count = cx.count();
4171 let (view, _doc) = current!(cx.editor);
4173 if let Some((id, selection)) = view.jumps.forward(count) {
4175 let selection = selection.clone();
4176 let (view, doc) = current!(cx.editor); // refetch doc
4177 doc.set_selection(view.id, selection);
4179 align_view(doc, view, Align::Center);
4183fn jump_backward(cx: &mut Context) {
4184 let count = cx.count();
4185 let (view, doc) = current!(cx.editor);
4187 if let Some((id, selection)) = view.jumps.backward(view.id, doc, count) {
4188 // manually set the alternate_file as we cannot use the Editor::switch function here.
4189 if view.doc != *id {
4190 view.last_accessed_doc = Some(view.doc)
4193 let selection = selection.clone();
4194 let (view, doc) = current!(cx.editor); // refetch doc
4195 doc.set_selection(view.id, selection);
4197 align_view(doc, view, Align::Center);
4201fn rotate_view(cx: &mut Context) {
4202 cx.editor.focus_next()
4205// split helper, clear it later
4206fn split(cx: &mut Context, action: Action) {
4207 let (view, doc) = current!(cx.editor);
4209 let selection = doc.selection(view.id).clone();
4210 let offset = view.offset;
4212 cx.editor.switch(id, action);
4214 // match the selection in the previous view
4215 let (view, doc) = current!(cx.editor);
4216 view.offset = offset;
4217 doc.set_selection(view.id, selection);
4220fn hsplit(cx: &mut Context) {
4221 split(cx, Action::HorizontalSplit);
4224fn vsplit(cx: &mut Context) {
4225 split(cx, Action::VerticalSplit);
4228fn wclose(cx: &mut Context) {
4229 let view_id = view!(cx.editor).id;
4230 // close current split
4231 cx.editor.close(view_id, /* close_buffer */ false);
4234fn select_register(cx: &mut Context) {
4235 cx.on_next_key(move |cx, event| {
4236 if let Some(ch) = event.char() {
4237 cx.editor.selected_register.select(ch);
4242fn align_view_top(cx: &mut Context) {
4243 let (view, doc) = current!(cx.editor);
4244 align_view(doc, view, Align::Top);
4247fn align_view_center(cx: &mut Context) {
4248 let (view, doc) = current!(cx.editor);
4249 align_view(doc, view, Align::Center);
4252fn align_view_bottom(cx: &mut Context) {
4253 let (view, doc) = current!(cx.editor);
4254 align_view(doc, view, Align::Bottom);
4257fn align_view_middle(cx: &mut Context) {
4258 let (view, doc) = current!(cx.editor);
4259 let text = doc.text().slice(..);
4260 let pos = doc.selection(view.id).primary().cursor(text);
4261 let pos = coords_at_pos(text, pos);
4263 view.offset.col = pos
4265 .saturating_sub((view.inner_area().width as usize) / 2);
4268fn scroll_up(cx: &mut Context) {
4269 scroll(cx, cx.count(), Direction::Backward);
4272fn scroll_down(cx: &mut Context) {
4273 scroll(cx, cx.count(), Direction::Forward);
4276fn select_textobject_around(cx: &mut Context) {
4277 select_textobject(cx, textobject::TextObject::Around);
4280fn select_textobject_inner(cx: &mut Context) {
4281 select_textobject(cx, textobject::TextObject::Inside);
4284fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
4285 let count = cx.count();
4286 cx.on_next_key(move |cx, event| {
4287 if let Some(ch) = event.char() {
4288 let (view, doc) = current!(cx.editor);
4289 let text = doc.text().slice(..);
4291 let selection = doc.selection(view.id).clone().transform(|range| {
4293 'w' => textobject::textobject_word(text, range, objtype, count),
4294 // TODO: cancel new ranges if inconsistent surround matches across lines
4295 ch if !ch.is_ascii_alphanumeric() => {
4296 textobject::textobject_surround(text, range, objtype, ch, count)
4301 doc.set_selection(view.id, selection);
4306fn surround_add(cx: &mut Context) {
4307 cx.on_next_key(move |cx, event| {
4308 if let Some(ch) = event.char() {
4309 let (view, doc) = current!(cx.editor);
4310 let selection = doc.selection(view.id);
4311 let (open, close) = surround::get_pair(ch);
4313 let mut changes = Vec::new();
4314 for range in selection.iter() {
4315 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
4316 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
4319 let transaction = Transaction::change(doc.text(), changes.into_iter());
4320 doc.apply(&transaction, view.id);
4321 doc.append_changes_to_history(view.id);
4326fn surround_replace(cx: &mut Context) {
4327 let count = cx.count();
4328 cx.on_next_key(move |cx, event| {
4329 if let Some(from) = event.char() {
4330 cx.on_next_key(move |cx, event| {
4331 if let Some(to) = event.char() {
4332 let (view, doc) = current!(cx.editor);
4333 let text = doc.text().slice(..);
4334 let selection = doc.selection(view.id);
4336 let change_pos = match surround::get_surround_pos(text, selection, from, count)
4342 let (open, close) = surround::get_pair(to);
4343 let transaction = Transaction::change(
4345 change_pos.iter().enumerate().map(|(i, &pos)| {
4349 Some(Tendril::from_char(if i % 2 == 0 { open } else { close })),
4353 doc.apply(&transaction, view.id);
4354 doc.append_changes_to_history(view.id);
4361fn surround_delete(cx: &mut Context) {
4362 let count = cx.count();
4363 cx.on_next_key(move |cx, event| {
4364 if let Some(ch) = event.char() {
4365 let (view, doc) = current!(cx.editor);
4366 let text = doc.text().slice(..);
4367 let selection = doc.selection(view.id);
4369 let change_pos = match surround::get_surround_pos(text, selection, ch, count) {
4375 Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
4376 doc.apply(&transaction, view.id);
4377 doc.append_changes_to_history(view.id);
4382fn suspend(_cx: &mut Context) {
4383 #[cfg(not(windows))]
4384 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
4388fn dap_start(cx: &mut Context) {
4389 use helix_dap::Client;
4390 use helix_lsp::block_on;
4391 use serde_json::to_value;
4393 let (_, doc) = current!(cx.editor);
4395 // TODO config picker
4396 let path = match doc.path() {
4397 Some(path) => path.to_path_buf(),
4400 .set_error("Can't start debug: document has no path".to_string());
4408 .language_config_for_file_name(&path)
4409 .and_then(|x| x.debug_adapter.clone());
4410 let config = match config {
4413 cx.editor.set_error(
4414 "Can't start debug: no debug adapter available for language".to_string(),
4419 let started = Client::process(config.clone(), 0);
4420 let (mut debugger, events) = block_on(started).unwrap();
4422 let request = debugger.initialize(config.name);
4423 let _ = block_on(request).unwrap();
4428 .language_config_for_file_name(&path)
4429 .and_then(|x| x.debug_configs.clone());
4431 let sessions = match sessions {
4434 cx.editor.set_error(
4435 "Can't start debug: no debug sessions available for language".to_string(),
4442 let args = sessions.get(0);
4444 let request = debugger.launch(to_value(args).unwrap());
4445 let _ = block_on(request).unwrap();
4447 // TODO: either await "initialized" or buffer commands until event is received
4448 cx.editor.debugger = Some(debugger);
4449 let stream = UnboundedReceiverStream::new(events);
4450 cx.editor.debugger_events.push(stream);
4453fn dap_toggle_breakpoint(cx: &mut Context) {
4454 use helix_lsp::block_on;
4456 let (view, doc) = current!(cx.editor);
4457 let text = doc.text().slice(..);
4458 let pos = doc.selection(view.id).primary().cursor(text);
4460 let breakpoint = helix_dap::SourceBreakpoint {
4461 line: text.char_to_line(pos) + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init)
4462 ..Default::default()
4465 let path = match doc.path() {
4466 Some(path) => path.to_path_buf(),
4469 .set_error("Can't set breakpoint: document has no path".to_string());
4474 // TODO: need to map breakpoints over edits and update them?
4475 // we shouldn't really allow editing while debug is running though
4477 if let Some(debugger) = &mut cx.editor.debugger {
4478 let breakpoints = debugger.breakpoints.entry(path.clone()).or_default();
4479 if let Some(pos) = breakpoints.iter().position(|b| b.line == breakpoint.line) {
4480 breakpoints.remove(pos);
4482 breakpoints.push(breakpoint);
4485 let breakpoints = breakpoints.clone();
4487 let request = debugger.set_breakpoints(path, breakpoints);
4488 let _ = block_on(request).unwrap();
4492fn dap_run(cx: &mut Context) {
4493 use helix_lsp::block_on;
4495 if let Some(debugger) = &mut cx.editor.debugger {
4496 if debugger.is_running {
4498 .set_status("Debuggee is already running".to_owned());
4501 let request = debugger.configuration_done();
4502 let _ = block_on(request).unwrap();
4503 debugger.is_running = true;
4507fn dap_continue(cx: &mut Context) {
4508 use helix_lsp::block_on;
4510 if let Some(debugger) = &mut cx.editor.debugger {
4511 if debugger.is_running {
4513 .set_status("Debuggee is already running".to_owned());
4517 let request = debugger.continue_thread(debugger.stopped_thread.unwrap());
4518 let _ = block_on(request).unwrap();
4519 debugger.is_running = true;
4520 debugger.stack_pointer = None;
4524fn dap_pause(cx: &mut Context) {
4525 use helix_lsp::block_on;
4527 if let Some(debugger) = &mut cx.editor.debugger {
4528 if !debugger.is_running {
4529 cx.editor.set_status("Debuggee is not running".to_owned());
4533 // FIXME: correct number here
4534 let request = debugger.pause(0);
4535 let _ = block_on(request).unwrap();
4539fn dap_in(cx: &mut Context) {
4540 use helix_lsp::block_on;
4542 if let Some(debugger) = &mut cx.editor.debugger {
4543 if debugger.is_running {
4545 .set_status("Debuggee is already running".to_owned());
4549 let request = debugger.step_in(debugger.stopped_thread.unwrap());
4550 let _ = block_on(request).unwrap();
4554fn dap_out(cx: &mut Context) {
4555 use helix_lsp::block_on;
4557 if let Some(debugger) = &mut cx.editor.debugger {
4558 if debugger.is_running {
4560 .set_status("Debuggee is already running".to_owned());
4564 let request = debugger.step_out(debugger.stopped_thread.unwrap());
4565 let _ = block_on(request).unwrap();
4569fn dap_next(cx: &mut Context) {
4570 use helix_lsp::block_on;
4572 if let Some(debugger) = &mut cx.editor.debugger {
4573 if debugger.is_running {
4575 .set_status("Debuggee is already running".to_owned());
4579 let request = debugger.next(debugger.stopped_thread.unwrap());
4580 let _ = block_on(request).unwrap();
4584fn dap_variables(cx: &mut Context) {
4585 use helix_lsp::block_on;
4587 if let Some(debugger) = &mut cx.editor.debugger {
4588 if debugger.is_running {
4590 .set_status("Cannot access variables while target is running".to_owned());
4593 if debugger.stack_pointer.is_none() {
4595 .set_status("Cannot find current stack pointer to access variables".to_owned());
4599 let frame_id = debugger.stack_pointer.clone().unwrap().id;
4600 let scopes = block_on(debugger.scopes(frame_id)).unwrap();
4601 let mut s = String::new();
4603 for scope in scopes.iter() {
4604 let response = block_on(debugger.variables(scope.variables_reference));
4606 if let Ok(vars) = response {
4608 let prefix = match var.data_type {
4609 Some(data_type) => format!("{} ", data_type),
4610 None => "".to_owned(),
4612 s.push_str(&format!("{}{} = {}; ", prefix, var.name, var.value));
4616 cx.editor.set_status(s);
4620fn dap_terminate(cx: &mut Context) {
4621 use helix_lsp::block_on;
4623 if let Some(debugger) = &mut cx.editor.debugger {
4624 let request = debugger.disconnect();
4625 let _ = block_on(request).unwrap();
4626 cx.editor.debugger = None;
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list