1 use helix_core::{
2 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
3 indent::IndentStyle,
4 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
5 match_brackets,
6 movement::{self, Direction},
7 object, pos_at_coords,
8 regex::{self, Regex, RegexBuilder},
9 register::Register,
10 search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
11 RopeSlice, Selection, SmallVec, Tendril, Transaction,
12 };
13
14 use helix_view::{
15 clipboard::ClipboardType,
16 document::Mode,
17 editor::{Action, Motion},
18 input::KeyEvent,
19 keyboard::KeyCode,
20 view::View,
21 Document, DocumentId, Editor, ViewId,
22 };
23
24 use anyhow::{anyhow, bail, Context as _};
25 use helix_lsp::{
26 lsp,
27 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
28 OffsetEncoding,
29 };
30 use insert::*;
31 use movement::Movement;
32
33 use crate::{
34 compositor::{self, Component, Compositor},
35 ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
36 };
37
38 use crate::job::{self, Job, Jobs};
39 use futures_util::{FutureExt, StreamExt};
40 use std::num::NonZeroUsize;
41 use std::{fmt, future::Future};
42
43 use std::{
44 borrow::Cow,
45 path::{Path, PathBuf},
46 };
47
48 use once_cell::sync::Lazy;
49 use serde::de::{self, Deserialize, Deserializer};
50
51 use grep_regex::RegexMatcherBuilder;
52 use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
53 use ignore::{DirEntry, WalkBuilder, WalkState};
54 use tokio_stream::wrappers::UnboundedReceiverStream;
55
56 pub struct Context<'a> {
57 pub register: Option<char>,
58 pub count: Option<NonZeroUsize>,
59 pub editor: &'a mut Editor,
60
61 pub callback: Option<crate::compositor::Callback>,
62 pub on_next_key_callback: Option<Box<dyn FnOnce(&mut Context, KeyEvent)>>,
63 pub jobs: &'a mut Jobs,
64 }
65
66 impl<'a> Context<'a> {
67 /// Push a new component onto the compositor.
push_layer(&mut self, component: Box<dyn Component>)68 pub fn push_layer(&mut self, component: Box<dyn Component>) {
69 self.callback = Some(Box::new(|compositor: &mut Compositor| {
70 compositor.push(component)
71 }));
72 }
73
74 #[inline]
on_next_key( &mut self, on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static, )75 pub fn on_next_key(
76 &mut self,
77 on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static,
78 ) {
79 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
80 }
81
82 #[inline]
callback<T, F>( &mut self, call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, callback: F, ) where T: for<'de> serde::Deserialize<'de> + Send + 'static, F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,83 pub fn callback<T, F>(
84 &mut self,
85 call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
86 callback: F,
87 ) where
88 T: for<'de> serde::Deserialize<'de> + Send + 'static,
89 F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static,
90 {
91 let callback = Box::pin(async move {
92 let json = call.await?;
93 let response = serde_json::from_value(json)?;
94 let call: job::Callback =
95 Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
96 callback(editor, compositor, response)
97 });
98 Ok(call)
99 });
100 self.jobs.callback(callback);
101 }
102
103 /// Returns 1 if no explicit count was provided
104 #[inline]
count(&self) -> usize105 pub fn count(&self) -> usize {
106 self.count.map_or(1, |v| v.get())
107 }
108 }
109
110 enum Align {
111 Top,
112 Center,
113 Bottom,
114 }
115
align_view(doc: &Document, view: &mut View, align: Align)116 fn align_view(doc: &Document, view: &mut View, align: Align) {
117 let pos = doc
118 .selection(view.id)
119 .primary()
120 .cursor(doc.text().slice(..));
121 let line = doc.text().char_to_line(pos);
122
123 let height = view.inner_area().height as usize;
124
125 let relative = match align {
126 Align::Center => height / 2,
127 Align::Top => 0,
128 Align::Bottom => height,
129 };
130
131 view.offset.row = line.saturating_sub(relative);
132 }
133
134 /// A command is composed of a static name, and a function that takes the current state plus a count,
135 /// and does a side-effect on the state (usually by creating and applying a transaction).
136 #[derive(Copy, Clone)]
137 pub struct Command {
138 name: &'static str,
139 fun: fn(cx: &mut Context),
140 doc: &'static str,
141 }
142
143 macro_rules! commands {
144 ( $($name:ident, $doc:literal,)* ) => {
145 $(
146 #[allow(non_upper_case_globals)]
147 pub const $name: Self = Self {
148 name: stringify!($name),
149 fun: $name,
150 doc: $doc
151 };
152 )*
153
154 pub const COMMAND_LIST: &'static [Self] = &[
155 $( Self::$name, )*
156 ];
157 }
158 }
159
160 impl Command {
execute(&self, cx: &mut Context)161 pub fn execute(&self, cx: &mut Context) {
162 (self.fun)(cx);
163 }
164
name(&self) -> &'static str165 pub fn name(&self) -> &'static str {
166 self.name
167 }
168
doc(&self) -> &'static str169 pub fn doc(&self) -> &'static str {
170 self.doc
171 }
172
173 #[rustfmt::skip]
174 commands!(
175 no_op, "Do nothing",
176 move_char_left, "Move left",
177 move_char_right, "Move right",
178 move_line_up, "Move up",
179 move_line_down, "Move down",
180 extend_char_left, "Extend left",
181 extend_char_right, "Extend right",
182 extend_line_up, "Extend up",
183 extend_line_down, "Extend down",
184 copy_selection_on_next_line, "Copy selection on next line",
185 copy_selection_on_prev_line, "Copy selection on previous line",
186 move_next_word_start, "Move to beginning of next word",
187 move_prev_word_start, "Move to beginning of previous word",
188 move_next_word_end, "Move to end of next word",
189 move_next_long_word_start, "Move to beginning of next long word",
190 move_prev_long_word_start, "Move to beginning of previous long word",
191 move_next_long_word_end, "Move to end of next long word",
192 extend_next_word_start, "Extend to beginning of next word",
193 extend_prev_word_start, "Extend to beginning of previous word",
194 extend_next_long_word_start, "Extend to beginning of next long word",
195 extend_prev_long_word_start, "Extend to beginning of previous long word",
196 extend_next_long_word_end, "Extend to end of next long word",
197 extend_next_word_end, "Extend to end of next word",
198 find_till_char, "Move till next occurance of char",
199 find_next_char, "Move to next occurance of char",
200 extend_till_char, "Extend till next occurance of char",
201 extend_next_char, "Extend to next occurance of char",
202 till_prev_char, "Move till previous occurance of char",
203 find_prev_char, "Move to previous occurance of char",
204 extend_till_prev_char, "Extend till previous occurance of char",
205 extend_prev_char, "Extend to previous occurance of char",
206 repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)",
207 replace, "Replace with new char",
208 switch_case, "Switch (toggle) case",
209 switch_to_uppercase, "Switch to uppercase",
210 switch_to_lowercase, "Switch to lowercase",
211 page_up, "Move page up",
212 page_down, "Move page down",
213 half_page_up, "Move half page up",
214 half_page_down, "Move half page down",
215 select_all, "Select whole document",
216 select_regex, "Select all regex matches inside selections",
217 split_selection, "Split selection into subselections on regex matches",
218 split_selection_on_newline, "Split selection on newlines",
219 search, "Search for regex pattern",
220 search_next, "Select next search match",
221 extend_search_next, "Add next search match to selection",
222 search_selection, "Use current selection as search pattern",
223 global_search, "Global Search in workspace folder",
224 extend_line, "Select current line, if already selected, extend to next line",
225 extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
226 delete_selection, "Delete selection",
227 change_selection, "Change selection (delete and enter insert mode)",
228 collapse_selection, "Collapse selection onto a single cursor",
229 flip_selections, "Flip selection cursor and anchor",
230 insert_mode, "Insert before selection",
231 append_mode, "Insert after selection (append)",
232 command_mode, "Enter command mode",
233 file_picker, "Open file picker",
234 code_action, "Perform code action",
235 buffer_picker, "Open buffer picker",
236 symbol_picker, "Open symbol picker",
237 last_picker, "Open last picker",
238 prepend_to_line, "Insert at start of line",
239 append_to_line, "Insert at end of line",
240 open_below, "Open new line below selection",
241 open_above, "Open new line above selection",
242 normal_mode, "Enter normal mode",
243 select_mode, "Enter selection extend mode",
244 exit_select_mode, "Exit selection mode",
245 goto_definition, "Goto definition",
246 add_newline_above, "Add newline above",
247 add_newline_below, "Add newline below",
248 goto_type_definition, "Goto type definition",
249 goto_implementation, "Goto implementation",
250 goto_file_start, "Goto file start/line",
251 goto_file_end, "Goto file end",
252 goto_reference, "Goto references",
253 goto_window_top, "Goto window top",
254 goto_window_middle, "Goto window middle",
255 goto_window_bottom, "Goto window bottom",
256 goto_last_accessed_file, "Goto last accessed file",
257 goto_line, "Goto line",
258 goto_last_line, "Goto last line",
259 goto_first_diag, "Goto first diagnostic",
260 goto_last_diag, "Goto last diagnostic",
261 goto_next_diag, "Goto next diagnostic",
262 goto_prev_diag, "Goto previous diagnostic",
263 goto_line_start, "Goto line start",
264 goto_line_end, "Goto line end",
265 // TODO: different description ?
266 goto_line_end_newline, "Goto line end",
267 goto_first_nonwhitespace, "Goto first non-blank in line",
268 extend_to_line_start, "Extend to line start",
269 extend_to_line_end, "Extend to line end",
270 extend_to_line_end_newline, "Extend to line end",
271 signature_help, "Show signature help",
272 insert_tab, "Insert tab char",
273 insert_newline, "Insert newline char",
274 delete_char_backward, "Delete previous char",
275 delete_char_forward, "Delete next char",
276 delete_word_backward, "Delete previous word",
277 undo, "Undo change",
278 redo, "Redo change",
279 yank, "Yank selection",
280 yank_joined_to_clipboard, "Join and yank selections to clipboard",
281 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
282 yank_joined_to_primary_clipboard, "Join and yank selections to primary clipboard",
283 yank_main_selection_to_primary_clipboard, "Yank main selection to primary clipboard",
284 replace_with_yanked, "Replace with yanked text",
285 replace_selections_with_clipboard, "Replace selections by clipboard content",
286 replace_selections_with_primary_clipboard, "Replace selections by primary clipboard content",
287 paste_after, "Paste after selection",
288 paste_before, "Paste before selection",
289 paste_clipboard_after, "Paste clipboard after selections",
290 paste_clipboard_before, "Paste clipboard before selections",
291 paste_primary_clipboard_after, "Paste primary clipboard after selections",
292 paste_primary_clipboard_before, "Paste primary clipboard before selections",
293 indent, "Indent selection",
294 unindent, "Unindent selection",
295 format_selections, "Format selection",
296 join_selections, "Join lines inside selection",
297 keep_selections, "Keep selections matching regex",
298 keep_primary_selection, "Keep primary selection",
299 remove_primary_selection, "Remove primary selection",
300 completion, "Invoke completion popup",
301 hover, "Show docs for item under cursor",
302 toggle_comments, "Comment/uncomment selections",
303 rotate_selections_forward, "Rotate selections forward",
304 rotate_selections_backward, "Rotate selections backward",
305 rotate_selection_contents_forward, "Rotate selection contents forward",
306 rotate_selection_contents_backward, "Rotate selections contents backward",
307 expand_selection, "Expand selection to parent syntax node",
308 jump_forward, "Jump forward on jumplist",
309 jump_backward, "Jump backward on jumplist",
310 jump_view_right, "Jump to the split to the right",
311 jump_view_left, "Jump to the split to the left",
312 jump_view_up, "Jump to the split above",
313 jump_view_down, "Jump to the split below",
314 rotate_view, "Goto next window",
315 hsplit, "Horizontal bottom split",
316 vsplit, "Vertical right split",
317 wclose, "Close window",
318 select_register, "Select register",
319 align_view_middle, "Align view middle",
320 align_view_top, "Align view top",
321 align_view_center, "Align view center",
322 align_view_bottom, "Align view bottom",
323 scroll_up, "Scroll view up",
324 scroll_down, "Scroll view down",
325 match_brackets, "Goto matching bracket",
326 surround_add, "Surround add",
327 surround_replace, "Surround replace",
328 surround_delete, "Surround delete",
329 select_textobject_around, "Select around object",
330 select_textobject_inner, "Select inside object",
331 shell_pipe, "Pipe selections through shell command",
332 shell_pipe_to, "Pipe selections into shell command, ignoring command output",
333 shell_insert_output, "Insert output of shell command before each selection",
334 shell_append_output, "Append output of shell command after each selection",
335 shell_keep_pipe, "Filter selections with shell predicate",
336 suspend, "Suspend",
337 );
338 }
339
340 impl fmt::Debug for Command {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 let Command { name, .. } = self;
343 f.debug_tuple("Command").field(name).finish()
344 }
345 }
346
347 impl fmt::Display for Command {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 let Command { name, .. } = self;
350 f.write_str(name)
351 }
352 }
353
354 impl std::str::FromStr for Command {
355 type Err = anyhow::Error;
356
from_str(s: &str) -> Result<Self, Self::Err>357 fn from_str(s: &str) -> Result<Self, Self::Err> {
358 Command::COMMAND_LIST
359 .iter()
360 .copied()
361 .find(|cmd| cmd.name == s)
362 .ok_or_else(|| anyhow!("No command named '{}'", s))
363 }
364 }
365
366 impl<'de> Deserialize<'de> for Command {
deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>,367 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
368 where
369 D: Deserializer<'de>,
370 {
371 let s = String::deserialize(deserializer)?;
372 s.parse().map_err(de::Error::custom)
373 }
374 }
375
376 impl PartialEq for Command {
eq(&self, other: &Self) -> bool377 fn eq(&self, other: &Self) -> bool {
378 self.name() == other.name()
379 }
380 }
381
no_op(_cx: &mut Context)382 fn no_op(_cx: &mut Context) {}
383
move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) where F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range,384 fn move_impl<F>(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement)
385 where
386 F: Fn(RopeSlice, Range, Direction, usize, Movement) -> Range,
387 {
388 let count = cx.count();
389 let (view, doc) = current!(cx.editor);
390 let text = doc.text().slice(..);
391
392 let selection = doc
393 .selection(view.id)
394 .clone()
395 .transform(|range| move_fn(text, range, dir, count, behaviour));
396 doc.set_selection(view.id, selection);
397 }
398
399 use helix_core::movement::{move_horizontally, move_vertically};
400
move_char_left(cx: &mut Context)401 fn move_char_left(cx: &mut Context) {
402 move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
403 }
404
move_char_right(cx: &mut Context)405 fn move_char_right(cx: &mut Context) {
406 move_impl(cx, move_horizontally, Direction::Forward, Movement::Move)
407 }
408
move_line_up(cx: &mut Context)409 fn move_line_up(cx: &mut Context) {
410 move_impl(cx, move_vertically, Direction::Backward, Movement::Move)
411 }
412
move_line_down(cx: &mut Context)413 fn move_line_down(cx: &mut Context) {
414 move_impl(cx, move_vertically, Direction::Forward, Movement::Move)
415 }
416
extend_char_left(cx: &mut Context)417 fn extend_char_left(cx: &mut Context) {
418 move_impl(cx, move_horizontally, Direction::Backward, Movement::Extend)
419 }
420
extend_char_right(cx: &mut Context)421 fn extend_char_right(cx: &mut Context) {
422 move_impl(cx, move_horizontally, Direction::Forward, Movement::Extend)
423 }
424
extend_line_up(cx: &mut Context)425 fn extend_line_up(cx: &mut Context) {
426 move_impl(cx, move_vertically, Direction::Backward, Movement::Extend)
427 }
428
extend_line_down(cx: &mut Context)429 fn extend_line_down(cx: &mut Context) {
430 move_impl(cx, move_vertically, Direction::Forward, Movement::Extend)
431 }
432
goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement)433 fn goto_line_end_impl(view: &mut View, doc: &mut Document, movement: Movement) {
434 let text = doc.text().slice(..);
435
436 let selection = doc.selection(view.id).clone().transform(|range| {
437 let line = range.cursor_line(text);
438 let line_start = text.line_to_char(line);
439
440 let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
441 .max(line_start);
442
443 range.put_cursor(text, pos, movement == Movement::Extend)
444 });
445 doc.set_selection(view.id, selection);
446 }
447
goto_line_end(cx: &mut Context)448 fn goto_line_end(cx: &mut Context) {
449 let (view, doc) = current!(cx.editor);
450 goto_line_end_impl(
451 view,
452 doc,
453 if doc.mode == Mode::Select {
454 Movement::Extend
455 } else {
456 Movement::Move
457 },
458 )
459 }
460
extend_to_line_end(cx: &mut Context)461 fn extend_to_line_end(cx: &mut Context) {
462 let (view, doc) = current!(cx.editor);
463 goto_line_end_impl(view, doc, Movement::Extend)
464 }
465
goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement)466 fn goto_line_end_newline_impl(view: &mut View, doc: &mut Document, movement: Movement) {
467 let text = doc.text().slice(..);
468
469 let selection = doc.selection(view.id).clone().transform(|range| {
470 let line = range.cursor_line(text);
471 let pos = line_end_char_index(&text, line);
472
473 range.put_cursor(text, pos, movement == Movement::Extend)
474 });
475 doc.set_selection(view.id, selection);
476 }
477
goto_line_end_newline(cx: &mut Context)478 fn goto_line_end_newline(cx: &mut Context) {
479 let (view, doc) = current!(cx.editor);
480 goto_line_end_newline_impl(
481 view,
482 doc,
483 if doc.mode == Mode::Select {
484 Movement::Extend
485 } else {
486 Movement::Move
487 },
488 )
489 }
490
extend_to_line_end_newline(cx: &mut Context)491 fn extend_to_line_end_newline(cx: &mut Context) {
492 let (view, doc) = current!(cx.editor);
493 goto_line_end_newline_impl(view, doc, Movement::Extend)
494 }
495
goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement)496 fn goto_line_start_impl(view: &mut View, doc: &mut Document, movement: Movement) {
497 let text = doc.text().slice(..);
498
499 let selection = doc.selection(view.id).clone().transform(|range| {
500 let line = range.cursor_line(text);
501
502 // adjust to start of the line
503 let pos = text.line_to_char(line);
504 range.put_cursor(text, pos, movement == Movement::Extend)
505 });
506 doc.set_selection(view.id, selection);
507 }
508
goto_line_start(cx: &mut Context)509 fn goto_line_start(cx: &mut Context) {
510 let (view, doc) = current!(cx.editor);
511 goto_line_start_impl(
512 view,
513 doc,
514 if doc.mode == Mode::Select {
515 Movement::Extend
516 } else {
517 Movement::Move
518 },
519 )
520 }
521
extend_to_line_start(cx: &mut Context)522 fn extend_to_line_start(cx: &mut Context) {
523 let (view, doc) = current!(cx.editor);
524 goto_line_start_impl(view, doc, Movement::Extend)
525 }
526
goto_first_nonwhitespace(cx: &mut Context)527 fn goto_first_nonwhitespace(cx: &mut Context) {
528 let (view, doc) = current!(cx.editor);
529 let text = doc.text().slice(..);
530
531 let selection = doc.selection(view.id).clone().transform(|range| {
532 let line = range.cursor_line(text);
533
534 if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
535 let pos = pos + text.line_to_char(line);
536 range.put_cursor(text, pos, doc.mode == Mode::Select)
537 } else {
538 range
539 }
540 });
541 doc.set_selection(view.id, selection);
542 }
543
goto_window(cx: &mut Context, align: Align)544 fn goto_window(cx: &mut Context, align: Align) {
545 let (view, doc) = current!(cx.editor);
546
547 let height = view.inner_area().height as usize;
548
549 // - 1 so we have at least one gap in the middle.
550 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
551 // as we type
552 let scrolloff = cx.editor.config.scrolloff.min(height.saturating_sub(1) / 2);
553
554 let last_line = view.last_line(doc);
555
556 let line = match align {
557 Align::Top => (view.offset.row + scrolloff),
558 Align::Center => (view.offset.row + (height / 2)),
559 Align::Bottom => last_line.saturating_sub(scrolloff),
560 }
561 .min(last_line.saturating_sub(scrolloff));
562
563 let pos = doc.text().line_to_char(line);
564
565 doc.set_selection(view.id, Selection::point(pos));
566 }
567
goto_window_top(cx: &mut Context)568 fn goto_window_top(cx: &mut Context) {
569 goto_window(cx, Align::Top)
570 }
571
goto_window_middle(cx: &mut Context)572 fn goto_window_middle(cx: &mut Context) {
573 goto_window(cx, Align::Center)
574 }
575
goto_window_bottom(cx: &mut Context)576 fn goto_window_bottom(cx: &mut Context) {
577 goto_window(cx, Align::Bottom)
578 }
579
move_word_impl<F>(cx: &mut Context, move_fn: F) where F: Fn(RopeSlice, Range, usize) -> Range,580 fn move_word_impl<F>(cx: &mut Context, move_fn: F)
581 where
582 F: Fn(RopeSlice, Range, usize) -> Range,
583 {
584 let count = cx.count();
585 let (view, doc) = current!(cx.editor);
586 let text = doc.text().slice(..);
587
588 let selection = doc
589 .selection(view.id)
590 .clone()
591 .transform(|range| move_fn(text, range, count));
592 doc.set_selection(view.id, selection);
593 }
594
move_next_word_start(cx: &mut Context)595 fn move_next_word_start(cx: &mut Context) {
596 move_word_impl(cx, movement::move_next_word_start)
597 }
598
move_prev_word_start(cx: &mut Context)599 fn move_prev_word_start(cx: &mut Context) {
600 move_word_impl(cx, movement::move_prev_word_start)
601 }
602
move_next_word_end(cx: &mut Context)603 fn move_next_word_end(cx: &mut Context) {
604 move_word_impl(cx, movement::move_next_word_end)
605 }
606
move_next_long_word_start(cx: &mut Context)607 fn move_next_long_word_start(cx: &mut Context) {
608 move_word_impl(cx, movement::move_next_long_word_start)
609 }
610
move_prev_long_word_start(cx: &mut Context)611 fn move_prev_long_word_start(cx: &mut Context) {
612 move_word_impl(cx, movement::move_prev_long_word_start)
613 }
614
move_next_long_word_end(cx: &mut Context)615 fn move_next_long_word_end(cx: &mut Context) {
616 move_word_impl(cx, movement::move_next_long_word_end)
617 }
618
goto_file_start(cx: &mut Context)619 fn goto_file_start(cx: &mut Context) {
620 if cx.count.is_some() {
621 goto_line(cx);
622 } else {
623 push_jump(cx.editor);
624 let (view, doc) = current!(cx.editor);
625 doc.set_selection(view.id, Selection::point(0));
626 }
627 }
628
goto_file_end(cx: &mut Context)629 fn goto_file_end(cx: &mut Context) {
630 push_jump(cx.editor);
631 let (view, doc) = current!(cx.editor);
632 doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
633 }
634
extend_word_impl<F>(cx: &mut Context, extend_fn: F) where F: Fn(RopeSlice, Range, usize) -> Range,635 fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
636 where
637 F: Fn(RopeSlice, Range, usize) -> Range,
638 {
639 let count = cx.count();
640 let (view, doc) = current!(cx.editor);
641 let text = doc.text().slice(..);
642
643 let selection = doc.selection(view.id).clone().transform(|range| {
644 let word = extend_fn(text, range, count);
645 let pos = word.cursor(text);
646 range.put_cursor(text, pos, true)
647 });
648 doc.set_selection(view.id, selection);
649 }
650
extend_next_word_start(cx: &mut Context)651 fn extend_next_word_start(cx: &mut Context) {
652 extend_word_impl(cx, movement::move_next_word_start)
653 }
654
extend_prev_word_start(cx: &mut Context)655 fn extend_prev_word_start(cx: &mut Context) {
656 extend_word_impl(cx, movement::move_prev_word_start)
657 }
658
extend_next_word_end(cx: &mut Context)659 fn extend_next_word_end(cx: &mut Context) {
660 extend_word_impl(cx, movement::move_next_word_end)
661 }
662
extend_next_long_word_start(cx: &mut Context)663 fn extend_next_long_word_start(cx: &mut Context) {
664 extend_word_impl(cx, movement::move_next_long_word_start)
665 }
666
extend_prev_long_word_start(cx: &mut Context)667 fn extend_prev_long_word_start(cx: &mut Context) {
668 extend_word_impl(cx, movement::move_prev_long_word_start)
669 }
670
extend_next_long_word_end(cx: &mut Context)671 fn extend_next_long_word_end(cx: &mut Context) {
672 extend_word_impl(cx, movement::move_next_long_word_end)
673 }
674
will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,675 fn will_find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
676 where
677 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
678 {
679 // TODO: count is reset to 1 before next key so we move it into the closure here.
680 // Would be nice to carry over.
681 let count = cx.count();
682
683 // need to wait for next key
684 // TODO: should this be done by grapheme rather than char? For example,
685 // we can't properly handle the line-ending CRLF case here in terms of char.
686 cx.on_next_key(move |cx, event| {
687 let ch = match event {
688 KeyEvent {
689 code: KeyCode::Enter,
690 ..
691 } =>
692 // TODO: this isn't quite correct when CRLF is involved.
693 // This hack will work in most cases, since documents don't
694 // usually mix line endings. But we should fix it eventually
695 // anyway.
696 {
697 current!(cx.editor)
698 .1
699 .line_ending
700 .as_str()
701 .chars()
702 .next()
703 .unwrap()
704 }
705
706 KeyEvent {
707 code: KeyCode::Char(ch),
708 ..
709 } => ch,
710 _ => return,
711 };
712
713 find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count);
714 cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| {
715 find_char_impl(editor, &search_fn, inclusive, true, ch, 1);
716 })));
717 })
718 }
719
720 //
721
722 #[inline]
find_char_impl<F>( editor: &mut Editor, search_fn: &F, inclusive: bool, extend: bool, ch: char, count: usize, ) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,723 fn find_char_impl<F>(
724 editor: &mut Editor,
725 search_fn: &F,
726 inclusive: bool,
727 extend: bool,
728 ch: char,
729 count: usize,
730 ) where
731 F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
732 {
733 let (view, doc) = current!(editor);
734 let text = doc.text().slice(..);
735
736 let selection = doc.selection(view.id).clone().transform(|range| {
737 // TODO: use `Range::cursor()` here instead. However, that works in terms of
738 // graphemes, whereas this function doesn't yet. So we're doing the same logic
739 // here, but just in terms of chars instead.
740 let search_start_pos = if range.anchor < range.head {
741 range.head - 1
742 } else {
743 range.head
744 };
745
746 search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| {
747 if extend {
748 range.put_cursor(text, pos, true)
749 } else {
750 Range::point(range.cursor(text)).put_cursor(text, pos, true)
751 }
752 })
753 });
754 doc.set_selection(view.id, selection);
755 }
756
find_next_char_impl( text: RopeSlice, ch: char, pos: usize, n: usize, inclusive: bool, ) -> Option<usize>757 fn find_next_char_impl(
758 text: RopeSlice,
759 ch: char,
760 pos: usize,
761 n: usize,
762 inclusive: bool,
763 ) -> Option<usize> {
764 let pos = (pos + 1).min(text.len_chars());
765 if inclusive {
766 search::find_nth_next(text, ch, pos, n)
767 } else {
768 let n = match text.get_char(pos) {
769 Some(next_ch) if next_ch == ch => n + 1,
770 _ => n,
771 };
772 search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
773 }
774 }
775
find_prev_char_impl( text: RopeSlice, ch: char, pos: usize, n: usize, inclusive: bool, ) -> Option<usize>776 fn find_prev_char_impl(
777 text: RopeSlice,
778 ch: char,
779 pos: usize,
780 n: usize,
781 inclusive: bool,
782 ) -> Option<usize> {
783 if inclusive {
784 search::find_nth_prev(text, ch, pos, n)
785 } else {
786 let n = match text.get_char(pos.saturating_sub(1)) {
787 Some(next_ch) if next_ch == ch => n + 1,
788 _ => n,
789 };
790 search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
791 }
792 }
793
find_till_char(cx: &mut Context)794 fn find_till_char(cx: &mut Context) {
795 will_find_char(cx, find_next_char_impl, false, false)
796 }
797
find_next_char(cx: &mut Context)798 fn find_next_char(cx: &mut Context) {
799 will_find_char(cx, find_next_char_impl, true, false)
800 }
801
extend_till_char(cx: &mut Context)802 fn extend_till_char(cx: &mut Context) {
803 will_find_char(cx, find_next_char_impl, false, true)
804 }
805
extend_next_char(cx: &mut Context)806 fn extend_next_char(cx: &mut Context) {
807 will_find_char(cx, find_next_char_impl, true, true)
808 }
809
till_prev_char(cx: &mut Context)810 fn till_prev_char(cx: &mut Context) {
811 will_find_char(cx, find_prev_char_impl, false, false)
812 }
813
find_prev_char(cx: &mut Context)814 fn find_prev_char(cx: &mut Context) {
815 will_find_char(cx, find_prev_char_impl, true, false)
816 }
817
extend_till_prev_char(cx: &mut Context)818 fn extend_till_prev_char(cx: &mut Context) {
819 will_find_char(cx, find_prev_char_impl, false, true)
820 }
821
extend_prev_char(cx: &mut Context)822 fn extend_prev_char(cx: &mut Context) {
823 will_find_char(cx, find_prev_char_impl, true, true)
824 }
825
repeat_last_motion(cx: &mut Context)826 fn repeat_last_motion(cx: &mut Context) {
827 let last_motion = cx.editor.last_motion.take();
828 if let Some(m) = &last_motion {
829 m.run(cx.editor);
830 cx.editor.last_motion = last_motion;
831 }
832 }
833
replace(cx: &mut Context)834 fn replace(cx: &mut Context) {
835 let mut buf = [0u8; 4]; // To hold utf8 encoded char.
836
837 // need to wait for next key
838 cx.on_next_key(move |cx, event| {
839 let (view, doc) = current!(cx.editor);
840 let ch = match event {
841 KeyEvent {
842 code: KeyCode::Char(ch),
843 ..
844 } => Some(&ch.encode_utf8(&mut buf[..])[..]),
845 KeyEvent {
846 code: KeyCode::Enter,
847 ..
848 } => Some(doc.line_ending.as_str()),
849 _ => None,
850 };
851
852 let selection = doc.selection(view.id);
853
854 if let Some(ch) = ch {
855 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
856 if !range.is_empty() {
857 let text: String =
858 RopeGraphemes::new(doc.text().slice(range.from()..range.to()))
859 .map(|g| {
860 let cow: Cow<str> = g.into();
861 if str_is_line_ending(&cow) {
862 cow
863 } else {
864 ch.into()
865 }
866 })
867 .collect();
868
869 (range.from(), range.to(), Some(text.into()))
870 } else {
871 // No change.
872 (range.from(), range.to(), None)
873 }
874 });
875
876 doc.apply(&transaction, view.id);
877 doc.append_changes_to_history(view.id);
878 }
879 })
880 }
881
switch_case_impl<F>(cx: &mut Context, change_fn: F) where F: Fn(Cow<str>) -> Tendril,882 fn switch_case_impl<F>(cx: &mut Context, change_fn: F)
883 where
884 F: Fn(Cow<str>) -> Tendril,
885 {
886 let (view, doc) = current!(cx.editor);
887 let selection = doc.selection(view.id);
888 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
889 let text: Tendril = change_fn(range.fragment(doc.text().slice(..)));
890
891 (range.from(), range.to(), Some(text))
892 });
893
894 doc.apply(&transaction, view.id);
895 doc.append_changes_to_history(view.id);
896 }
897
switch_case(cx: &mut Context)898 fn switch_case(cx: &mut Context) {
899 switch_case_impl(cx, |string| {
900 string
901 .chars()
902 .flat_map(|ch| {
903 if ch.is_lowercase() {
904 ch.to_uppercase().collect()
905 } else if ch.is_uppercase() {
906 ch.to_lowercase().collect()
907 } else {
908 vec![ch]
909 }
910 })
911 .collect()
912 });
913 }
914
switch_to_uppercase(cx: &mut Context)915 fn switch_to_uppercase(cx: &mut Context) {
916 switch_case_impl(cx, |string| string.to_uppercase().into());
917 }
918
switch_to_lowercase(cx: &mut Context)919 fn switch_to_lowercase(cx: &mut Context) {
920 switch_case_impl(cx, |string| string.to_lowercase().into());
921 }
922
scroll(cx: &mut Context, offset: usize, direction: Direction)923 pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
924 use Direction::*;
925 let (view, doc) = current!(cx.editor);
926
927 let range = doc.selection(view.id).primary();
928 let text = doc.text().slice(..);
929
930 let cursor = coords_at_pos(text, range.cursor(text));
931 let doc_last_line = doc.text().len_lines().saturating_sub(1);
932
933 let last_line = view.last_line(doc);
934
935 if direction == Backward && view.offset.row == 0
936 || direction == Forward && last_line == doc_last_line
937 {
938 return;
939 }
940
941 let height = view.inner_area().height;
942
943 let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2);
944
945 view.offset.row = match direction {
946 Forward => view.offset.row + offset,
947 Backward => view.offset.row.saturating_sub(offset),
948 }
949 .min(doc_last_line);
950
951 // recalculate last line
952 let last_line = view.last_line(doc);
953
954 // clamp into viewport
955 let line = cursor
956 .row
957 .max(view.offset.row + scrolloff)
958 .min(last_line.saturating_sub(scrolloff));
959
960 let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
961
962 let anchor = if doc.mode == Mode::Select {
963 range.anchor
964 } else {
965 head
966 };
967
968 // TODO: only manipulate main selection
969 doc.set_selection(view.id, Selection::single(anchor, head));
970 }
971
page_up(cx: &mut Context)972 fn page_up(cx: &mut Context) {
973 let view = view!(cx.editor);
974 let offset = view.inner_area().height as usize;
975 scroll(cx, offset, Direction::Backward);
976 }
977
page_down(cx: &mut Context)978 fn page_down(cx: &mut Context) {
979 let view = view!(cx.editor);
980 let offset = view.inner_area().height as usize;
981 scroll(cx, offset, Direction::Forward);
982 }
983
half_page_up(cx: &mut Context)984 fn half_page_up(cx: &mut Context) {
985 let view = view!(cx.editor);
986 let offset = view.inner_area().height as usize / 2;
987 scroll(cx, offset, Direction::Backward);
988 }
989
half_page_down(cx: &mut Context)990 fn half_page_down(cx: &mut Context) {
991 let view = view!(cx.editor);
992 let offset = view.inner_area().height as usize / 2;
993 scroll(cx, offset, Direction::Forward);
994 }
995
copy_selection_on_line(cx: &mut Context, direction: Direction)996 fn copy_selection_on_line(cx: &mut Context, direction: Direction) {
997 let count = cx.count();
998 let (view, doc) = current!(cx.editor);
999 let text = doc.text().slice(..);
1000 let selection = doc.selection(view.id);
1001 let mut ranges = SmallVec::with_capacity(selection.ranges().len() * (count + 1));
1002 ranges.extend_from_slice(selection.ranges());
1003 let mut primary_index = 0;
1004 for range in selection.iter() {
1005 let is_primary = *range == selection.primary();
1006 let head_pos = coords_at_pos(text, range.head);
1007 let anchor_pos = coords_at_pos(text, range.anchor);
1008 let height = std::cmp::max(head_pos.row, anchor_pos.row)
1009 - std::cmp::min(head_pos.row, anchor_pos.row)
1010 + 1;
1011
1012 if is_primary {
1013 primary_index = ranges.len();
1014 }
1015 ranges.push(*range);
1016
1017 let mut sels = 0;
1018 let mut i = 0;
1019 while sels < count {
1020 let offset = (i + 1) * height;
1021
1022 let anchor_row = match direction {
1023 Direction::Forward => anchor_pos.row + offset,
1024 Direction::Backward => anchor_pos.row.saturating_sub(offset),
1025 };
1026
1027 let head_row = match direction {
1028 Direction::Forward => head_pos.row + offset,
1029 Direction::Backward => head_pos.row.saturating_sub(offset),
1030 };
1031
1032 if anchor_row >= text.len_lines() || head_row >= text.len_lines() {
1033 break;
1034 }
1035
1036 let anchor = pos_at_coords(text, Position::new(anchor_row, anchor_pos.col), true);
1037 let head = pos_at_coords(text, Position::new(head_row, head_pos.col), true);
1038
1039 // skip lines that are too short
1040 if coords_at_pos(text, anchor).col == anchor_pos.col
1041 && coords_at_pos(text, head).col == head_pos.col
1042 {
1043 if is_primary {
1044 primary_index = ranges.len();
1045 }
1046 ranges.push(Range::new(anchor, head));
1047 sels += 1;
1048 }
1049
1050 i += 1;
1051 }
1052 }
1053
1054 let selection = Selection::new(ranges, primary_index);
1055 doc.set_selection(view.id, selection);
1056 }
1057
copy_selection_on_prev_line(cx: &mut Context)1058 fn copy_selection_on_prev_line(cx: &mut Context) {
1059 copy_selection_on_line(cx, Direction::Backward)
1060 }
1061
copy_selection_on_next_line(cx: &mut Context)1062 fn copy_selection_on_next_line(cx: &mut Context) {
1063 copy_selection_on_line(cx, Direction::Forward)
1064 }
1065
select_all(cx: &mut Context)1066 fn select_all(cx: &mut Context) {
1067 let (view, doc) = current!(cx.editor);
1068
1069 let end = doc.text().len_chars();
1070 doc.set_selection(view.id, Selection::single(0, end))
1071 }
1072
select_regex(cx: &mut Context)1073 fn select_regex(cx: &mut Context) {
1074 let reg = cx.register.unwrap_or('/');
1075 let prompt = ui::regex_prompt(
1076 cx,
1077 "select:".into(),
1078 Some(reg),
1079 move |view, doc, regex, event| {
1080 if event != PromptEvent::Update {
1081 return;
1082 }
1083 let text = doc.text().slice(..);
1084 if let Some(selection) =
1085 selection::select_on_matches(text, doc.selection(view.id), ®ex)
1086 {
1087 doc.set_selection(view.id, selection);
1088 }
1089 },
1090 );
1091
1092 cx.push_layer(Box::new(prompt));
1093 }
1094
split_selection(cx: &mut Context)1095 fn split_selection(cx: &mut Context) {
1096 let reg = cx.register.unwrap_or('/');
1097 let prompt = ui::regex_prompt(
1098 cx,
1099 "split:".into(),
1100 Some(reg),
1101 move |view, doc, regex, event| {
1102 if event != PromptEvent::Update {
1103 return;
1104 }
1105 let text = doc.text().slice(..);
1106 let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex);
1107 doc.set_selection(view.id, selection);
1108 },
1109 );
1110
1111 cx.push_layer(Box::new(prompt));
1112 }
1113
split_selection_on_newline(cx: &mut Context)1114 fn split_selection_on_newline(cx: &mut Context) {
1115 let (view, doc) = current!(cx.editor);
1116 let text = doc.text().slice(..);
1117 // only compile the regex once
1118 #[allow(clippy::trivial_regex)]
1119 static REGEX: Lazy<Regex> =
1120 Lazy::new(|| Regex::new(r"\r\n|[\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}]").unwrap());
1121 let selection = selection::split_on_matches(text, doc.selection(view.id), ®EX);
1122 doc.set_selection(view.id, selection);
1123 }
1124
search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool)1125 fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
1126 let text = doc.text().slice(..);
1127 let selection = doc.selection(view.id);
1128
1129 // Get the right side of the primary block cursor.
1130 let start = text.char_to_byte(graphemes::next_grapheme_boundary(
1131 text,
1132 selection.primary().cursor(text),
1133 ));
1134
1135 // use find_at to find the next match after the cursor, loop around the end
1136 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1137 let mat = regex
1138 .find_at(contents, start)
1139 .or_else(|| regex.find(contents));
1140 // TODO: message on wraparound
1141 if let Some(mat) = mat {
1142 let start = text.byte_to_char(mat.start());
1143 let end = text.byte_to_char(mat.end());
1144
1145 if end == 0 {
1146 // skip empty matches that don't make sense
1147 return;
1148 }
1149
1150 let selection = if extend {
1151 selection.clone().push(Range::new(start, end))
1152 } else {
1153 Selection::single(start, end)
1154 };
1155
1156 doc.set_selection(view.id, selection);
1157 align_view(doc, view, Align::Center);
1158 };
1159 }
1160
1161 // TODO: use one function for search vs extend
search(cx: &mut Context)1162 fn search(cx: &mut Context) {
1163 let reg = cx.register.unwrap_or('/');
1164 let (_, doc) = current!(cx.editor);
1165
1166 // TODO: could probably share with select_on_matches?
1167
1168 // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't
1169 // feed chunks into the regex yet
1170 let contents = doc.text().slice(..).to_string();
1171
1172 let prompt = ui::regex_prompt(
1173 cx,
1174 "search:".into(),
1175 Some(reg),
1176 move |view, doc, regex, event| {
1177 if event != PromptEvent::Update {
1178 return;
1179 }
1180 search_impl(doc, view, &contents, ®ex, false);
1181 },
1182 );
1183
1184 cx.push_layer(Box::new(prompt));
1185 }
1186
search_next_impl(cx: &mut Context, extend: bool)1187 fn search_next_impl(cx: &mut Context, extend: bool) {
1188 let (view, doc) = current!(cx.editor);
1189 let registers = &cx.editor.registers;
1190 if let Some(query) = registers.read('/') {
1191 let query = query.last().unwrap();
1192 let contents = doc.text().slice(..).to_string();
1193 let case_insensitive = if cx.editor.config.smart_case {
1194 !query.chars().any(char::is_uppercase)
1195 } else {
1196 false
1197 };
1198 if let Ok(regex) = RegexBuilder::new(query)
1199 .case_insensitive(case_insensitive)
1200 .build()
1201 {
1202 search_impl(doc, view, &contents, ®ex, extend);
1203 } else {
1204 // get around warning `mutable_borrow_reservation_conflict`
1205 // which will be a hard error in the future
1206 // see: https://github.com/rust-lang/rust/issues/59159
1207 let query = query.clone();
1208 cx.editor.set_error(format!("Invalid regex: {}", query));
1209 }
1210 }
1211 }
1212
search_next(cx: &mut Context)1213 fn search_next(cx: &mut Context) {
1214 search_next_impl(cx, false);
1215 }
1216
extend_search_next(cx: &mut Context)1217 fn extend_search_next(cx: &mut Context) {
1218 search_next_impl(cx, true);
1219 }
1220
search_selection(cx: &mut Context)1221 fn search_selection(cx: &mut Context) {
1222 let (view, doc) = current!(cx.editor);
1223 let contents = doc.text().slice(..);
1224 let query = doc.selection(view.id).primary().fragment(contents);
1225 let regex = regex::escape(&query);
1226 cx.editor.registers.get_mut('/').push(regex);
1227 let msg = format!("register '{}' set to '{}'", '\\', query);
1228 cx.editor.set_status(msg);
1229 }
1230
global_search(cx: &mut Context)1231 fn global_search(cx: &mut Context) {
1232 let (all_matches_sx, all_matches_rx) =
1233 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1234 let smart_case = cx.editor.config.smart_case;
1235 let prompt = ui::regex_prompt(
1236 cx,
1237 "global search:".into(),
1238 None,
1239 move |_view, _doc, regex, event| {
1240 if event != PromptEvent::Validate {
1241 return;
1242 }
1243
1244 if let Ok(matcher) = RegexMatcherBuilder::new()
1245 .case_smart(smart_case)
1246 .build(regex.as_str())
1247 {
1248 let searcher = SearcherBuilder::new()
1249 .binary_detection(BinaryDetection::quit(b'\x00'))
1250 .build();
1251
1252 let search_root = std::env::current_dir()
1253 .expect("Global search error: Failed to get current dir");
1254 WalkBuilder::new(search_root).build_parallel().run(|| {
1255 let mut searcher_cl = searcher.clone();
1256 let matcher_cl = matcher.clone();
1257 let all_matches_sx_cl = all_matches_sx.clone();
1258 Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
1259 let dent = match dent {
1260 Ok(dent) => dent,
1261 Err(_) => return WalkState::Continue,
1262 };
1263
1264 match dent.file_type() {
1265 Some(fi) => {
1266 if !fi.is_file() {
1267 return WalkState::Continue;
1268 }
1269 }
1270 None => return WalkState::Continue,
1271 }
1272
1273 let result_sink = sinks::UTF8(|line_num, _| {
1274 match all_matches_sx_cl
1275 .send((line_num as usize - 1, dent.path().to_path_buf()))
1276 {
1277 Ok(_) => Ok(true),
1278 Err(_) => Ok(false),
1279 }
1280 });
1281 let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
1282
1283 if let Err(err) = result {
1284 log::error!("Global search error: {}, {}", dent.path().display(), err);
1285 }
1286 WalkState::Continue
1287 })
1288 });
1289 } else {
1290 // Otherwise do nothing
1291 // log::warn!("Global Search Invalid Pattern")
1292 }
1293 },
1294 );
1295
1296 cx.push_layer(Box::new(prompt));
1297
1298 let current_path = doc_mut!(cx.editor).path().cloned();
1299
1300 let show_picker = async move {
1301 let all_matches: Vec<(usize, PathBuf)> =
1302 UnboundedReceiverStream::new(all_matches_rx).collect().await;
1303 let call: job::Callback =
1304 Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
1305 if all_matches.is_empty() {
1306 editor.set_status("No matches found".to_string());
1307 return;
1308 }
1309
1310 let picker = FilePicker::new(
1311 all_matches,
1312 move |(_line_num, path)| {
1313 let relative_path = helix_core::path::get_relative_path(path)
1314 .to_str()
1315 .unwrap()
1316 .to_owned();
1317 if current_path.as_ref().map(|p| p == path).unwrap_or(false) {
1318 format!("{} (*)", relative_path).into()
1319 } else {
1320 relative_path.into()
1321 }
1322 },
1323 move |editor: &mut Editor, (line_num, path), action| {
1324 match editor.open(path.into(), action) {
1325 Ok(_) => {}
1326 Err(e) => {
1327 editor.set_error(format!(
1328 "Failed to open file '{}': {}",
1329 path.display(),
1330 e
1331 ));
1332 return;
1333 }
1334 }
1335
1336 let line_num = *line_num;
1337 let (view, doc) = current!(editor);
1338 let text = doc.text();
1339 let start = text.line_to_char(line_num);
1340 let end = text.line_to_char((line_num + 1).min(text.len_lines()));
1341
1342 doc.set_selection(view.id, Selection::single(start, end));
1343 align_view(doc, view, Align::Center);
1344 },
1345 |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
1346 );
1347 compositor.push(Box::new(picker));
1348 });
1349 Ok(call)
1350 };
1351 cx.jobs.callback(show_picker);
1352 }
1353
extend_line(cx: &mut Context)1354 fn extend_line(cx: &mut Context) {
1355 let count = cx.count();
1356 let (view, doc) = current!(cx.editor);
1357
1358 let text = doc.text();
1359 let range = doc.selection(view.id).primary();
1360
1361 let (start_line, end_line) = range.line_range(text.slice(..));
1362 let start = text.line_to_char(start_line);
1363 let mut end = text.line_to_char((end_line + count).min(text.len_lines()));
1364
1365 if range.from() == start && range.to() == end {
1366 end = text.line_to_char((end_line + count + 1).min(text.len_lines()));
1367 }
1368
1369 doc.set_selection(view.id, Selection::single(start, end));
1370 }
1371
extend_to_line_bounds(cx: &mut Context)1372 fn extend_to_line_bounds(cx: &mut Context) {
1373 let (view, doc) = current!(cx.editor);
1374
1375 doc.set_selection(
1376 view.id,
1377 doc.selection(view.id).clone().transform(|range| {
1378 let text = doc.text();
1379
1380 let (start_line, end_line) = range.line_range(text.slice(..));
1381 let start = text.line_to_char(start_line);
1382 let end = text.line_to_char((end_line + 1).min(text.len_lines()));
1383
1384 if range.anchor <= range.head {
1385 Range::new(start, end)
1386 } else {
1387 Range::new(end, start)
1388 }
1389 }),
1390 );
1391 }
1392
delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId)1393 fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
1394 let text = doc.text().slice(..);
1395 let selection = doc.selection(view_id);
1396
1397 // first yank the selection
1398 let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
1399 reg.write(values);
1400
1401 // then delete
1402 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1403 (range.from(), range.to(), None)
1404 });
1405 doc.apply(&transaction, view_id);
1406 }
1407
delete_selection(cx: &mut Context)1408 fn delete_selection(cx: &mut Context) {
1409 let reg_name = cx.register.unwrap_or('"');
1410 let (view, doc) = current!(cx.editor);
1411 let registers = &mut cx.editor.registers;
1412 let reg = registers.get_mut(reg_name);
1413 delete_selection_impl(reg, doc, view.id);
1414
1415 doc.append_changes_to_history(view.id);
1416
1417 // exit select mode, if currently in select mode
1418 exit_select_mode(cx);
1419 }
1420
change_selection(cx: &mut Context)1421 fn change_selection(cx: &mut Context) {
1422 let reg_name = cx.register.unwrap_or('"');
1423 let (view, doc) = current!(cx.editor);
1424 let registers = &mut cx.editor.registers;
1425 let reg = registers.get_mut(reg_name);
1426 delete_selection_impl(reg, doc, view.id);
1427 enter_insert_mode(doc);
1428 }
1429
collapse_selection(cx: &mut Context)1430 fn collapse_selection(cx: &mut Context) {
1431 let (view, doc) = current!(cx.editor);
1432 let text = doc.text().slice(..);
1433
1434 let selection = doc.selection(view.id).clone().transform(|range| {
1435 let pos = range.cursor(text);
1436 Range::new(pos, pos)
1437 });
1438 doc.set_selection(view.id, selection);
1439 }
1440
flip_selections(cx: &mut Context)1441 fn flip_selections(cx: &mut Context) {
1442 let (view, doc) = current!(cx.editor);
1443
1444 let selection = doc
1445 .selection(view.id)
1446 .clone()
1447 .transform(|range| Range::new(range.head, range.anchor));
1448 doc.set_selection(view.id, selection);
1449 }
1450
enter_insert_mode(doc: &mut Document)1451 fn enter_insert_mode(doc: &mut Document) {
1452 doc.mode = Mode::Insert;
1453 }
1454
1455 // inserts at the start of each selection
insert_mode(cx: &mut Context)1456 fn insert_mode(cx: &mut Context) {
1457 let (view, doc) = current!(cx.editor);
1458 enter_insert_mode(doc);
1459
1460 let selection = doc
1461 .selection(view.id)
1462 .clone()
1463 .transform(|range| Range::new(range.to(), range.from()));
1464 doc.set_selection(view.id, selection);
1465 }
1466
1467 // inserts at the end of each selection
append_mode(cx: &mut Context)1468 fn append_mode(cx: &mut Context) {
1469 let (view, doc) = current!(cx.editor);
1470 enter_insert_mode(doc);
1471 doc.restore_cursor = true;
1472 let text = doc.text().slice(..);
1473
1474 // Make sure there's room at the end of the document if the last
1475 // selection butts up against it.
1476 let end = text.len_chars();
1477 let last_range = doc.selection(view.id).iter().last().unwrap();
1478 if !last_range.is_empty() && last_range.head == end {
1479 let transaction = Transaction::change(
1480 doc.text(),
1481 std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
1482 );
1483 doc.apply(&transaction, view.id);
1484 }
1485
1486 let selection = doc.selection(view.id).clone().transform(|range| {
1487 Range::new(
1488 range.from(),
1489 graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()),
1490 )
1491 });
1492 doc.set_selection(view.id, selection);
1493 }
1494
1495 mod cmd {
1496 use super::*;
1497 use std::collections::HashMap;
1498
1499 use helix_view::editor::Action;
1500 use ui::completers::{self, Completer};
1501
1502 #[derive(Clone)]
1503 pub struct TypableCommand {
1504 pub name: &'static str,
1505 pub aliases: &'static [&'static str],
1506 pub doc: &'static str,
1507 // params, flags, helper, completer
1508 pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
1509 pub completer: Option<Completer>,
1510 }
1511
quit( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1512 fn quit(
1513 cx: &mut compositor::Context,
1514 _args: &[&str],
1515 _event: PromptEvent,
1516 ) -> anyhow::Result<()> {
1517 // last view and we have unsaved changes
1518 if cx.editor.tree.views().count() == 1 {
1519 buffers_remaining_impl(cx.editor)?
1520 }
1521
1522 cx.editor
1523 .close(view!(cx.editor).id, /* close_buffer */ false);
1524
1525 Ok(())
1526 }
1527
force_quit( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1528 fn force_quit(
1529 cx: &mut compositor::Context,
1530 _args: &[&str],
1531 _event: PromptEvent,
1532 ) -> anyhow::Result<()> {
1533 cx.editor
1534 .close(view!(cx.editor).id, /* close_buffer */ false);
1535
1536 Ok(())
1537 }
1538
open( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1539 fn open(
1540 cx: &mut compositor::Context,
1541 args: &[&str],
1542 _event: PromptEvent,
1543 ) -> anyhow::Result<()> {
1544 use helix_core::path::expand_tilde;
1545 let path = args.get(0).context("wrong argument count")?;
1546 let _ = cx
1547 .editor
1548 .open(expand_tilde(Path::new(path)), Action::Replace)?;
1549 Ok(())
1550 }
1551
write_impl<P: AsRef<Path>>( cx: &mut compositor::Context, path: Option<P>, ) -> anyhow::Result<()>1552 fn write_impl<P: AsRef<Path>>(
1553 cx: &mut compositor::Context,
1554 path: Option<P>,
1555 ) -> anyhow::Result<()> {
1556 let jobs = &mut cx.jobs;
1557 let (_, doc) = current!(cx.editor);
1558
1559 if let Some(path) = path {
1560 doc.set_path(Some(path.as_ref()))
1561 .context("invalid filepath")?;
1562 }
1563 if doc.path().is_none() {
1564 bail!("cannot write a buffer without a filename");
1565 }
1566 let fmt = doc.auto_format().map(|fmt| {
1567 let shared = fmt.shared();
1568 let callback = make_format_callback(
1569 doc.id(),
1570 doc.version(),
1571 Modified::SetUnmodified,
1572 shared.clone(),
1573 );
1574 jobs.callback(callback);
1575 shared
1576 });
1577 let future = doc.format_and_save(fmt);
1578 cx.jobs.add(Job::new(future).wait_before_exiting());
1579 Ok(())
1580 }
1581
write( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1582 fn write(
1583 cx: &mut compositor::Context,
1584 args: &[&str],
1585 _event: PromptEvent,
1586 ) -> anyhow::Result<()> {
1587 write_impl(cx, args.first())
1588 }
1589
new_file( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1590 fn new_file(
1591 cx: &mut compositor::Context,
1592 _args: &[&str],
1593 _event: PromptEvent,
1594 ) -> anyhow::Result<()> {
1595 cx.editor.new_file(Action::Replace);
1596
1597 Ok(())
1598 }
1599
format( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1600 fn format(
1601 cx: &mut compositor::Context,
1602 _args: &[&str],
1603 _event: PromptEvent,
1604 ) -> anyhow::Result<()> {
1605 let (_, doc) = current!(cx.editor);
1606
1607 if let Some(format) = doc.format() {
1608 let callback =
1609 make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
1610 cx.jobs.callback(callback);
1611 }
1612
1613 Ok(())
1614 }
set_indent_style( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1615 fn set_indent_style(
1616 cx: &mut compositor::Context,
1617 args: &[&str],
1618 _event: PromptEvent,
1619 ) -> anyhow::Result<()> {
1620 use IndentStyle::*;
1621
1622 // If no argument, report current indent style.
1623 if args.is_empty() {
1624 let style = current!(cx.editor).1.indent_style;
1625 cx.editor.set_status(match style {
1626 Tabs => "tabs".into(),
1627 Spaces(1) => "1 space".into(),
1628 Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
1629 _ => "error".into(), // Shouldn't happen.
1630 });
1631 return Ok(());
1632 }
1633
1634 // Attempt to parse argument as an indent style.
1635 let style = match args.get(0) {
1636 Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
1637 Some(&"0") => Some(Tabs),
1638 Some(arg) => arg
1639 .parse::<u8>()
1640 .ok()
1641 .filter(|n| (1..=8).contains(n))
1642 .map(Spaces),
1643 _ => None,
1644 };
1645
1646 let style = style.context("invalid indent style")?;
1647 let doc = doc_mut!(cx.editor);
1648 doc.indent_style = style;
1649
1650 Ok(())
1651 }
1652
1653 /// Sets or reports the current document's line ending setting.
set_line_ending( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1654 fn set_line_ending(
1655 cx: &mut compositor::Context,
1656 args: &[&str],
1657 _event: PromptEvent,
1658 ) -> anyhow::Result<()> {
1659 use LineEnding::*;
1660
1661 // If no argument, report current line ending setting.
1662 if args.is_empty() {
1663 let line_ending = current!(cx.editor).1.line_ending;
1664 cx.editor.set_status(match line_ending {
1665 Crlf => "crlf".into(),
1666 LF => "line feed".into(),
1667 FF => "form feed".into(),
1668 CR => "carriage return".into(),
1669 Nel => "next line".into(),
1670
1671 // These should never be a document's default line ending.
1672 VT | LS | PS => "error".into(),
1673 });
1674
1675 return Ok(());
1676 }
1677
1678 let arg = args
1679 .get(0)
1680 .context("argument missing")?
1681 .to_ascii_lowercase();
1682
1683 // Attempt to parse argument as a line ending.
1684 let line_ending = match arg {
1685 // We check for CR first because it shares a common prefix with CRLF.
1686 arg if arg.starts_with("cr") => CR,
1687 arg if arg.starts_with("crlf") => Crlf,
1688 arg if arg.starts_with("lf") => LF,
1689 arg if arg.starts_with("ff") => FF,
1690 arg if arg.starts_with("nel") => Nel,
1691 _ => bail!("invalid line ending"),
1692 };
1693
1694 doc_mut!(cx.editor).line_ending = line_ending;
1695 Ok(())
1696 }
1697
earlier( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1698 fn earlier(
1699 cx: &mut compositor::Context,
1700 args: &[&str],
1701 _event: PromptEvent,
1702 ) -> anyhow::Result<()> {
1703 let uk = args
1704 .join(" ")
1705 .parse::<helix_core::history::UndoKind>()
1706 .map_err(|s| anyhow!(s))?;
1707
1708 let (view, doc) = current!(cx.editor);
1709 doc.earlier(view.id, uk);
1710
1711 Ok(())
1712 }
1713
later( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1714 fn later(
1715 cx: &mut compositor::Context,
1716 args: &[&str],
1717 _event: PromptEvent,
1718 ) -> anyhow::Result<()> {
1719 let uk = args
1720 .join(" ")
1721 .parse::<helix_core::history::UndoKind>()
1722 .map_err(|s| anyhow!(s))?;
1723 let (view, doc) = current!(cx.editor);
1724 doc.later(view.id, uk);
1725
1726 Ok(())
1727 }
1728
write_quit( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1729 fn write_quit(
1730 cx: &mut compositor::Context,
1731 args: &[&str],
1732 event: PromptEvent,
1733 ) -> anyhow::Result<()> {
1734 write_impl(cx, args.first())?;
1735 quit(cx, &[], event)
1736 }
1737
force_write_quit( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1738 fn force_write_quit(
1739 cx: &mut compositor::Context,
1740 args: &[&str],
1741 event: PromptEvent,
1742 ) -> anyhow::Result<()> {
1743 write_impl(cx, args.first())?;
1744 force_quit(cx, &[], event)
1745 }
1746
1747 /// Results an error if there are modified buffers remaining and sets editor error,
1748 /// otherwise returns `Ok(())`
buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()>1749 pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
1750 let modified: Vec<_> = editor
1751 .documents()
1752 .filter(|doc| doc.is_modified())
1753 .map(|doc| {
1754 doc.relative_path()
1755 .map(|path| path.to_string_lossy().to_string())
1756 .unwrap_or_else(|| "[scratch]".into())
1757 })
1758 .collect();
1759 if !modified.is_empty() {
1760 bail!(
1761 "{} unsaved buffer(s) remaining: {:?}",
1762 modified.len(),
1763 modified
1764 );
1765 }
1766 Ok(())
1767 }
1768
write_all_impl( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, quit: bool, force: bool, ) -> anyhow::Result<()>1769 fn write_all_impl(
1770 cx: &mut compositor::Context,
1771 _args: &[&str],
1772 _event: PromptEvent,
1773 quit: bool,
1774 force: bool,
1775 ) -> anyhow::Result<()> {
1776 let mut errors = String::new();
1777
1778 // save all documents
1779 for (_, doc) in &mut cx.editor.documents {
1780 if doc.path().is_none() {
1781 errors.push_str("cannot write a buffer without a filename\n");
1782 continue;
1783 }
1784
1785 // TODO: handle error.
1786 let handle = doc.save();
1787 cx.jobs.add(Job::new(handle).wait_before_exiting());
1788 }
1789
1790 if quit {
1791 if !force {
1792 buffers_remaining_impl(cx.editor)?;
1793 }
1794
1795 // close all views
1796 let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
1797 for view_id in views {
1798 cx.editor.close(view_id, false);
1799 }
1800 }
1801
1802 bail!(errors)
1803 }
1804
write_all( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1805 fn write_all(
1806 cx: &mut compositor::Context,
1807 args: &[&str],
1808 event: PromptEvent,
1809 ) -> anyhow::Result<()> {
1810 write_all_impl(cx, args, event, false, false)
1811 }
1812
write_all_quit( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1813 fn write_all_quit(
1814 cx: &mut compositor::Context,
1815 args: &[&str],
1816 event: PromptEvent,
1817 ) -> anyhow::Result<()> {
1818 write_all_impl(cx, args, event, true, false)
1819 }
1820
force_write_all_quit( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1821 fn force_write_all_quit(
1822 cx: &mut compositor::Context,
1823 args: &[&str],
1824 event: PromptEvent,
1825 ) -> anyhow::Result<()> {
1826 write_all_impl(cx, args, event, true, true)
1827 }
1828
quit_all_impl( editor: &mut Editor, _args: &[&str], _event: PromptEvent, force: bool, ) -> anyhow::Result<()>1829 fn quit_all_impl(
1830 editor: &mut Editor,
1831 _args: &[&str],
1832 _event: PromptEvent,
1833 force: bool,
1834 ) -> anyhow::Result<()> {
1835 if !force {
1836 buffers_remaining_impl(editor)?;
1837 }
1838
1839 // close all views
1840 let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
1841 for view_id in views {
1842 editor.close(view_id, false);
1843 }
1844
1845 Ok(())
1846 }
1847
quit_all( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1848 fn quit_all(
1849 cx: &mut compositor::Context,
1850 args: &[&str],
1851 event: PromptEvent,
1852 ) -> anyhow::Result<()> {
1853 quit_all_impl(&mut cx.editor, args, event, false)
1854 }
1855
force_quit_all( cx: &mut compositor::Context, args: &[&str], event: PromptEvent, ) -> anyhow::Result<()>1856 fn force_quit_all(
1857 cx: &mut compositor::Context,
1858 args: &[&str],
1859 event: PromptEvent,
1860 ) -> anyhow::Result<()> {
1861 quit_all_impl(&mut cx.editor, args, event, true)
1862 }
1863
theme( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1864 fn theme(
1865 cx: &mut compositor::Context,
1866 args: &[&str],
1867 _event: PromptEvent,
1868 ) -> anyhow::Result<()> {
1869 let theme = args.first().context("theme not provided")?;
1870 cx.editor.set_theme_from_name(theme)
1871 }
1872
yank_main_selection_to_clipboard( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1873 fn yank_main_selection_to_clipboard(
1874 cx: &mut compositor::Context,
1875 _args: &[&str],
1876 _event: PromptEvent,
1877 ) -> anyhow::Result<()> {
1878 yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
1879 }
1880
yank_joined_to_clipboard( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1881 fn yank_joined_to_clipboard(
1882 cx: &mut compositor::Context,
1883 args: &[&str],
1884 _event: PromptEvent,
1885 ) -> anyhow::Result<()> {
1886 let (_, doc) = current!(cx.editor);
1887 let separator = args
1888 .first()
1889 .copied()
1890 .unwrap_or_else(|| doc.line_ending.as_str());
1891 yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
1892 }
1893
yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1894 fn yank_main_selection_to_primary_clipboard(
1895 cx: &mut compositor::Context,
1896 _args: &[&str],
1897 _event: PromptEvent,
1898 ) -> anyhow::Result<()> {
1899 yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
1900 }
1901
yank_joined_to_primary_clipboard( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1902 fn yank_joined_to_primary_clipboard(
1903 cx: &mut compositor::Context,
1904 args: &[&str],
1905 _event: PromptEvent,
1906 ) -> anyhow::Result<()> {
1907 let (_, doc) = current!(cx.editor);
1908 let separator = args
1909 .first()
1910 .copied()
1911 .unwrap_or_else(|| doc.line_ending.as_str());
1912 yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
1913 }
1914
paste_clipboard_after( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1915 fn paste_clipboard_after(
1916 cx: &mut compositor::Context,
1917 _args: &[&str],
1918 _event: PromptEvent,
1919 ) -> anyhow::Result<()> {
1920 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
1921 }
1922
paste_clipboard_before( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1923 fn paste_clipboard_before(
1924 cx: &mut compositor::Context,
1925 _args: &[&str],
1926 _event: PromptEvent,
1927 ) -> anyhow::Result<()> {
1928 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
1929 }
1930
paste_primary_clipboard_after( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1931 fn paste_primary_clipboard_after(
1932 cx: &mut compositor::Context,
1933 _args: &[&str],
1934 _event: PromptEvent,
1935 ) -> anyhow::Result<()> {
1936 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
1937 }
1938
paste_primary_clipboard_before( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1939 fn paste_primary_clipboard_before(
1940 cx: &mut compositor::Context,
1941 _args: &[&str],
1942 _event: PromptEvent,
1943 ) -> anyhow::Result<()> {
1944 paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
1945 }
1946
replace_selections_with_clipboard_impl( cx: &mut compositor::Context, clipboard_type: ClipboardType, ) -> anyhow::Result<()>1947 fn replace_selections_with_clipboard_impl(
1948 cx: &mut compositor::Context,
1949 clipboard_type: ClipboardType,
1950 ) -> anyhow::Result<()> {
1951 let (view, doc) = current!(cx.editor);
1952
1953 match cx.editor.clipboard_provider.get_contents(clipboard_type) {
1954 Ok(contents) => {
1955 let selection = doc.selection(view.id);
1956 let transaction =
1957 Transaction::change_by_selection(doc.text(), selection, |range| {
1958 (range.from(), range.to(), Some(contents.as_str().into()))
1959 });
1960
1961 doc.apply(&transaction, view.id);
1962 doc.append_changes_to_history(view.id);
1963 Ok(())
1964 }
1965 Err(e) => Err(e.context("Couldn't get system clipboard contents")),
1966 }
1967 }
1968
replace_selections_with_clipboard( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1969 fn replace_selections_with_clipboard(
1970 cx: &mut compositor::Context,
1971 _args: &[&str],
1972 _event: PromptEvent,
1973 ) -> anyhow::Result<()> {
1974 replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
1975 }
1976
replace_selections_with_primary_clipboard( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1977 fn replace_selections_with_primary_clipboard(
1978 cx: &mut compositor::Context,
1979 _args: &[&str],
1980 _event: PromptEvent,
1981 ) -> anyhow::Result<()> {
1982 replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
1983 }
1984
show_clipboard_provider( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1985 fn show_clipboard_provider(
1986 cx: &mut compositor::Context,
1987 _args: &[&str],
1988 _event: PromptEvent,
1989 ) -> anyhow::Result<()> {
1990 cx.editor
1991 .set_status(cx.editor.clipboard_provider.name().to_string());
1992 Ok(())
1993 }
1994
change_current_directory( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>1995 fn change_current_directory(
1996 cx: &mut compositor::Context,
1997 args: &[&str],
1998 _event: PromptEvent,
1999 ) -> anyhow::Result<()> {
2000 let dir = helix_core::path::expand_tilde(
2001 args.first()
2002 .context("target directory not provided")?
2003 .as_ref(),
2004 );
2005
2006 if let Err(e) = std::env::set_current_dir(dir) {
2007 bail!("Couldn't change the current working directory: {}", e);
2008 }
2009
2010 let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
2011 cx.editor.set_status(format!(
2012 "Current working directory is now {}",
2013 cwd.display()
2014 ));
2015 Ok(())
2016 }
2017
show_current_directory( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2018 fn show_current_directory(
2019 cx: &mut compositor::Context,
2020 _args: &[&str],
2021 _event: PromptEvent,
2022 ) -> anyhow::Result<()> {
2023 let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
2024 cx.editor
2025 .set_status(format!("Current working directory is {}", cwd.display()));
2026 Ok(())
2027 }
2028
2029 /// Sets the [`Document`]'s encoding..
set_encoding( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2030 fn set_encoding(
2031 cx: &mut compositor::Context,
2032 args: &[&str],
2033 _event: PromptEvent,
2034 ) -> anyhow::Result<()> {
2035 let (_, doc) = current!(cx.editor);
2036 if let Some(label) = args.first() {
2037 doc.set_encoding(label)
2038 } else {
2039 let encoding = doc.encoding().name().to_string();
2040 cx.editor.set_status(encoding);
2041 Ok(())
2042 }
2043 }
2044
2045 /// Reload the [`Document`] from its source file.
reload( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2046 fn reload(
2047 cx: &mut compositor::Context,
2048 _args: &[&str],
2049 _event: PromptEvent,
2050 ) -> anyhow::Result<()> {
2051 let (view, doc) = current!(cx.editor);
2052 doc.reload(view.id)
2053 }
2054
tree_sitter_scopes( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2055 fn tree_sitter_scopes(
2056 cx: &mut compositor::Context,
2057 _args: &[&str],
2058 _event: PromptEvent,
2059 ) -> anyhow::Result<()> {
2060 let (view, doc) = current!(cx.editor);
2061 let text = doc.text().slice(..);
2062
2063 let pos = doc.selection(view.id).primary().cursor(text);
2064 let scopes = indent::get_scopes(doc.syntax(), text, pos);
2065 cx.editor.set_status(format!("scopes: {:?}", &scopes));
2066 Ok(())
2067 }
2068
vsplit( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2069 fn vsplit(
2070 cx: &mut compositor::Context,
2071 args: &[&str],
2072 _event: PromptEvent,
2073 ) -> anyhow::Result<()> {
2074 let (_, doc) = current!(cx.editor);
2075 let id = doc.id();
2076
2077 if let Some(path) = args.get(0) {
2078 cx.editor.open(path.into(), Action::VerticalSplit)?;
2079 } else {
2080 cx.editor.switch(id, Action::VerticalSplit);
2081 }
2082
2083 Ok(())
2084 }
2085
hsplit( cx: &mut compositor::Context, args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2086 fn hsplit(
2087 cx: &mut compositor::Context,
2088 args: &[&str],
2089 _event: PromptEvent,
2090 ) -> anyhow::Result<()> {
2091 let (_, doc) = current!(cx.editor);
2092 let id = doc.id();
2093
2094 if let Some(path) = args.get(0) {
2095 cx.editor.open(path.into(), Action::HorizontalSplit)?;
2096 } else {
2097 cx.editor.switch(id, Action::HorizontalSplit);
2098 }
2099
2100 Ok(())
2101 }
2102
tutor( cx: &mut compositor::Context, _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()>2103 fn tutor(
2104 cx: &mut compositor::Context,
2105 _args: &[&str],
2106 _event: PromptEvent,
2107 ) -> anyhow::Result<()> {
2108 let path = helix_core::runtime_dir().join("tutor.txt");
2109 cx.editor.open(path, Action::Replace)?;
2110 // Unset path to prevent accidentally saving to the original tutor file.
2111 doc_mut!(cx.editor).set_path(None)?;
2112 Ok(())
2113 }
2114
2115 pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2116 TypableCommand {
2117 name: "quit",
2118 aliases: &["q"],
2119 doc: "Close the current view.",
2120 fun: quit,
2121 completer: None,
2122 },
2123 TypableCommand {
2124 name: "quit!",
2125 aliases: &["q!"],
2126 doc: "Close the current view forcefully (ignoring unsaved changes).",
2127 fun: force_quit,
2128 completer: None,
2129 },
2130 TypableCommand {
2131 name: "open",
2132 aliases: &["o"],
2133 doc: "Open a file from disk into the current view.",
2134 fun: open,
2135 completer: Some(completers::filename),
2136 },
2137 TypableCommand {
2138 name: "write",
2139 aliases: &["w"],
2140 doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
2141 fun: write,
2142 completer: Some(completers::filename),
2143 },
2144 TypableCommand {
2145 name: "new",
2146 aliases: &["n"],
2147 doc: "Create a new scratch buffer.",
2148 fun: new_file,
2149 completer: Some(completers::filename),
2150 },
2151 TypableCommand {
2152 name: "format",
2153 aliases: &["fmt"],
2154 doc: "Format the file using a formatter.",
2155 fun: format,
2156 completer: None,
2157 },
2158 TypableCommand {
2159 name: "indent-style",
2160 aliases: &[],
2161 doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
2162 fun: set_indent_style,
2163 completer: None,
2164 },
2165 TypableCommand {
2166 name: "line-ending",
2167 aliases: &[],
2168 doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
2169 fun: set_line_ending,
2170 completer: None,
2171 },
2172 TypableCommand {
2173 name: "earlier",
2174 aliases: &["ear"],
2175 doc: "Jump back to an earlier point in edit history. Accepts a number of steps or a time span.",
2176 fun: earlier,
2177 completer: None,
2178 },
2179 TypableCommand {
2180 name: "later",
2181 aliases: &["lat"],
2182 doc: "Jump to a later point in edit history. Accepts a number of steps or a time span.",
2183 fun: later,
2184 completer: None,
2185 },
2186 TypableCommand {
2187 name: "write-quit",
2188 aliases: &["wq", "x"],
2189 doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)",
2190 fun: write_quit,
2191 completer: Some(completers::filename),
2192 },
2193 TypableCommand {
2194 name: "write-quit!",
2195 aliases: &["wq!", "x!"],
2196 doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)",
2197 fun: force_write_quit,
2198 completer: Some(completers::filename),
2199 },
2200 TypableCommand {
2201 name: "write-all",
2202 aliases: &["wa"],
2203 doc: "Write changes from all views to disk.",
2204 fun: write_all,
2205 completer: None,
2206 },
2207 TypableCommand {
2208 name: "write-quit-all",
2209 aliases: &["wqa", "xa"],
2210 doc: "Write changes from all views to disk and close all views.",
2211 fun: write_all_quit,
2212 completer: None,
2213 },
2214 TypableCommand {
2215 name: "write-quit-all!",
2216 aliases: &["wqa!", "xa!"],
2217 doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).",
2218 fun: force_write_all_quit,
2219 completer: None,
2220 },
2221 TypableCommand {
2222 name: "quit-all",
2223 aliases: &["qa"],
2224 doc: "Close all views.",
2225 fun: quit_all,
2226 completer: None,
2227 },
2228 TypableCommand {
2229 name: "quit-all!",
2230 aliases: &["qa!"],
2231 doc: "Close all views forcefully (ignoring unsaved changes).",
2232 fun: force_quit_all,
2233 completer: None,
2234 },
2235 TypableCommand {
2236 name: "theme",
2237 aliases: &[],
2238 doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
2239 fun: theme,
2240 completer: Some(completers::theme),
2241 },
2242 TypableCommand {
2243 name: "clipboard-yank",
2244 aliases: &[],
2245 doc: "Yank main selection into system clipboard.",
2246 fun: yank_main_selection_to_clipboard,
2247 completer: None,
2248 },
2249 TypableCommand {
2250 name: "clipboard-yank-join",
2251 aliases: &[],
2252 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.
2253 fun: yank_joined_to_clipboard,
2254 completer: None,
2255 },
2256 TypableCommand {
2257 name: "primary-clipboard-yank",
2258 aliases: &[],
2259 doc: "Yank main selection into system primary clipboard.",
2260 fun: yank_main_selection_to_primary_clipboard,
2261 completer: None,
2262 },
2263 TypableCommand {
2264 name: "primary-clipboard-yank-join",
2265 aliases: &[],
2266 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.
2267 fun: yank_joined_to_primary_clipboard,
2268 completer: None,
2269 },
2270 TypableCommand {
2271 name: "clipboard-paste-after",
2272 aliases: &[],
2273 doc: "Paste system clipboard after selections.",
2274 fun: paste_clipboard_after,
2275 completer: None,
2276 },
2277 TypableCommand {
2278 name: "clipboard-paste-before",
2279 aliases: &[],
2280 doc: "Paste system clipboard before selections.",
2281 fun: paste_clipboard_before,
2282 completer: None,
2283 },
2284 TypableCommand {
2285 name: "clipboard-paste-replace",
2286 aliases: &[],
2287 doc: "Replace selections with content of system clipboard.",
2288 fun: replace_selections_with_clipboard,
2289 completer: None,
2290 },
2291 TypableCommand {
2292 name: "primary-clipboard-paste-after",
2293 aliases: &[],
2294 doc: "Paste primary clipboard after selections.",
2295 fun: paste_primary_clipboard_after,
2296 completer: None,
2297 },
2298 TypableCommand {
2299 name: "primary-clipboard-paste-before",
2300 aliases: &[],
2301 doc: "Paste primary clipboard before selections.",
2302 fun: paste_primary_clipboard_before,
2303 completer: None,
2304 },
2305 TypableCommand {
2306 name: "primary-clipboard-paste-replace",
2307 aliases: &[],
2308 doc: "Replace selections with content of system primary clipboard.",
2309 fun: replace_selections_with_primary_clipboard,
2310 completer: None,
2311 },
2312 TypableCommand {
2313 name: "show-clipboard-provider",
2314 aliases: &[],
2315 doc: "Show clipboard provider name in status bar.",
2316 fun: show_clipboard_provider,
2317 completer: None,
2318 },
2319 TypableCommand {
2320 name: "change-current-directory",
2321 aliases: &["cd"],
2322 doc: "Change the current working directory (:cd <dir>).",
2323 fun: change_current_directory,
2324 completer: Some(completers::directory),
2325 },
2326 TypableCommand {
2327 name: "show-directory",
2328 aliases: &["pwd"],
2329 doc: "Show the current working directory.",
2330 fun: show_current_directory,
2331 completer: None,
2332 },
2333 TypableCommand {
2334 name: "encoding",
2335 aliases: &[],
2336 doc: "Set encoding based on `https://encoding.spec.whatwg.org`",
2337 fun: set_encoding,
2338 completer: None,
2339 },
2340 TypableCommand {
2341 name: "reload",
2342 aliases: &[],
2343 doc: "Discard changes and reload from the source file.",
2344 fun: reload,
2345 completer: None,
2346 },
2347 TypableCommand {
2348 name: "tree-sitter-scopes",
2349 aliases: &[],
2350 doc: "Display tree sitter scopes, primarily for theming and development.",
2351 fun: tree_sitter_scopes,
2352 completer: None,
2353 },
2354 TypableCommand {
2355 name: "vsplit",
2356 aliases: &["vs"],
2357 doc: "Open the file in a vertical split.",
2358 fun: vsplit,
2359 completer: Some(completers::filename),
2360 },
2361 TypableCommand {
2362 name: "hsplit",
2363 aliases: &["hs", "sp"],
2364 doc: "Open the file in a horizontal split.",
2365 fun: hsplit,
2366 completer: Some(completers::filename),
2367 },
2368 TypableCommand {
2369 name: "tutor",
2370 aliases: &[],
2371 doc: "Open the tutorial.",
2372 fun: tutor,
2373 completer: None,
2374 },
2375 ];
2376
2377 pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2378 TYPABLE_COMMAND_LIST
2379 .iter()
2380 .flat_map(|cmd| {
2381 std::iter::once((cmd.name, cmd))
2382 .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2383 })
2384 .collect()
2385 });
2386 }
2387
command_mode(cx: &mut Context)2388 fn command_mode(cx: &mut Context) {
2389 let mut prompt = Prompt::new(
2390 ":".into(),
2391 Some(':'),
2392 |input: &str| {
2393 // we use .this over split_whitespace() because we care about empty segments
2394 let parts = input.split(' ').collect::<Vec<&str>>();
2395
2396 // simple heuristic: if there's no just one part, complete command name.
2397 // if there's a space, per command completion kicks in.
2398 if parts.len() <= 1 {
2399 let end = 0..;
2400 cmd::TYPABLE_COMMAND_LIST
2401 .iter()
2402 .filter(|command| command.name.contains(input))
2403 .map(|command| (end.clone(), Cow::Borrowed(command.name)))
2404 .collect()
2405 } else {
2406 let part = parts.last().unwrap();
2407
2408 if let Some(cmd::TypableCommand {
2409 completer: Some(completer),
2410 ..
2411 }) = cmd::COMMANDS.get(parts[0])
2412 {
2413 completer(part)
2414 .into_iter()
2415 .map(|(range, file)| {
2416 // offset ranges to input
2417 let offset = input.len() - part.len();
2418 let range = (range.start + offset)..;
2419 (range, file)
2420 })
2421 .collect()
2422 } else {
2423 Vec::new()
2424 }
2425 }
2426 }, // completion
2427 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
2428 if event != PromptEvent::Validate {
2429 return;
2430 }
2431
2432 let parts = input.split_whitespace().collect::<Vec<&str>>();
2433 if parts.is_empty() {
2434 return;
2435 }
2436
2437 if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2438 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2439 cx.editor.set_error(format!("{}", e));
2440 }
2441 } else {
2442 cx.editor
2443 .set_error(format!("no such command: '{}'", parts[0]));
2444 };
2445 },
2446 );
2447 prompt.doc_fn = Box::new(|input: &str| {
2448 let part = input.split(' ').next().unwrap_or_default();
2449
2450 if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
2451 return Some(doc);
2452 }
2453
2454 None
2455 });
2456
2457 cx.push_layer(Box::new(prompt));
2458 }
2459
file_picker(cx: &mut Context)2460 fn file_picker(cx: &mut Context) {
2461 let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
2462 let picker = ui::file_picker(root);
2463 cx.push_layer(Box::new(picker));
2464 }
2465
buffer_picker(cx: &mut Context)2466 fn buffer_picker(cx: &mut Context) {
2467 let current = view!(cx.editor).doc;
2468
2469 let picker = FilePicker::new(
2470 cx.editor
2471 .documents
2472 .iter()
2473 .map(|(id, doc)| (id, doc.path().cloned()))
2474 .collect(),
2475 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2476 let path = path.as_deref().map(helix_core::path::get_relative_path);
2477 match path.as_ref().and_then(|path| path.to_str()) {
2478 Some(path) => {
2479 if *id == current {
2480 format!("{} (*)", &path).into()
2481 } else {
2482 path.to_owned().into()
2483 }
2484 }
2485 None => "[scratch buffer]".into(),
2486 }
2487 },
2488 |editor: &mut Editor, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
2489 editor.switch(*id, Action::Replace);
2490 },
2491 |editor, (id, path)| {
2492 let doc = &editor.documents.get(*id)?;
2493 let &view_id = doc.selections().keys().next()?;
2494 let line = doc
2495 .selection(view_id)
2496 .primary()
2497 .cursor_line(doc.text().slice(..));
2498 Some((path.clone()?, Some((line, line))))
2499 },
2500 );
2501 cx.push_layer(Box::new(picker));
2502 }
2503
symbol_picker(cx: &mut Context)2504 fn symbol_picker(cx: &mut Context) {
2505 fn nested_to_flat(
2506 list: &mut Vec<lsp::SymbolInformation>,
2507 file: &lsp::TextDocumentIdentifier,
2508 symbol: lsp::DocumentSymbol,
2509 ) {
2510 #[allow(deprecated)]
2511 list.push(lsp::SymbolInformation {
2512 name: symbol.name,
2513 kind: symbol.kind,
2514 tags: symbol.tags,
2515 deprecated: symbol.deprecated,
2516 location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
2517 container_name: None,
2518 });
2519 for child in symbol.children.into_iter().flatten() {
2520 nested_to_flat(list, file, child);
2521 }
2522 }
2523 let (_, doc) = current!(cx.editor);
2524
2525 let language_server = match doc.language_server() {
2526 Some(language_server) => language_server,
2527 None => return,
2528 };
2529 let offset_encoding = language_server.offset_encoding();
2530
2531 let future = language_server.document_symbols(doc.identifier());
2532
2533 cx.callback(
2534 future,
2535 move |editor: &mut Editor,
2536 compositor: &mut Compositor,
2537 response: Option<lsp::DocumentSymbolResponse>| {
2538 if let Some(symbols) = response {
2539 // lsp has two ways to represent symbols (flat/nested)
2540 // convert the nested variant to flat, so that we have a homogeneous list
2541 let symbols = match symbols {
2542 lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
2543 lsp::DocumentSymbolResponse::Nested(symbols) => {
2544 let (_view, doc) = current!(editor);
2545 let mut flat_symbols = Vec::new();
2546 for symbol in symbols {
2547 nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
2548 }
2549 flat_symbols
2550 }
2551 };
2552
2553 let picker = FilePicker::new(
2554 symbols,
2555 |symbol| (&symbol.name).into(),
2556 move |editor: &mut Editor, symbol, _action| {
2557 push_jump(editor);
2558 let (view, doc) = current!(editor);
2559
2560 if let Some(range) =
2561 lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
2562 {
2563 // we flip the range so that the cursor sits on the start of the symbol
2564 // (for example start of the function).
2565 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
2566 align_view(doc, view, Align::Center);
2567 }
2568 },
2569 move |_editor, symbol| {
2570 let path = symbol.location.uri.to_file_path().unwrap();
2571 let line = Some((
2572 symbol.location.range.start.line as usize,
2573 symbol.location.range.end.line as usize,
2574 ));
2575 Some((path, line))
2576 },
2577 );
2578 compositor.push(Box::new(picker))
2579 }
2580 },
2581 )
2582 }
2583
code_action(cx: &mut Context)2584 pub fn code_action(cx: &mut Context) {
2585 let (view, doc) = current!(cx.editor);
2586
2587 let language_server = match doc.language_server() {
2588 Some(language_server) => language_server,
2589 None => return,
2590 };
2591
2592 let range = range_to_lsp_range(
2593 doc.text(),
2594 doc.selection(view.id).primary(),
2595 language_server.offset_encoding(),
2596 );
2597
2598 let future = language_server.code_actions(doc.identifier(), range);
2599 let offset_encoding = language_server.offset_encoding();
2600
2601 cx.callback(
2602 future,
2603 move |_editor: &mut Editor,
2604 compositor: &mut Compositor,
2605 response: Option<lsp::CodeActionResponse>| {
2606 if let Some(actions) = response {
2607 let picker = Picker::new(
2608 true,
2609 actions,
2610 |action| match action {
2611 lsp::CodeActionOrCommand::CodeAction(action) => {
2612 action.title.as_str().into()
2613 }
2614 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
2615 },
2616 move |editor, code_action, _action| match code_action {
2617 lsp::CodeActionOrCommand::Command(command) => {
2618 log::debug!("code action command: {:?}", command);
2619 editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2620 }
2621 lsp::CodeActionOrCommand::CodeAction(code_action) => {
2622 log::debug!("code action: {:?}", code_action);
2623 if let Some(ref workspace_edit) = code_action.edit {
2624 apply_workspace_edit(editor, offset_encoding, workspace_edit)
2625 }
2626 }
2627 },
2628 );
2629 compositor.push(Box::new(picker))
2630 }
2631 },
2632 )
2633 }
2634
apply_workspace_edit( editor: &mut Editor, offset_encoding: OffsetEncoding, workspace_edit: &lsp::WorkspaceEdit, )2635 fn apply_workspace_edit(
2636 editor: &mut Editor,
2637 offset_encoding: OffsetEncoding,
2638 workspace_edit: &lsp::WorkspaceEdit,
2639 ) {
2640 if let Some(ref changes) = workspace_edit.changes {
2641 log::debug!("workspace changes: {:?}", changes);
2642 editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2643 return;
2644 // 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
2645 // TODO: find some example that uses workspace changes, and test it
2646 // for (url, edits) in changes.iter() {
2647 // let file_path = url.origin().ascii_serialization();
2648 // let file_path = std::path::PathBuf::from(file_path);
2649 // let file = std::fs::File::open(file_path).unwrap();
2650 // let mut text = Rope::from_reader(file).unwrap();
2651 // let transaction = edits_to_changes(&text, edits);
2652 // transaction.apply(&mut text);
2653 // }
2654 }
2655
2656 if let Some(ref document_changes) = workspace_edit.document_changes {
2657 match document_changes {
2658 lsp::DocumentChanges::Edits(document_edits) => {
2659 for document_edit in document_edits {
2660 let path = document_edit
2661 .text_document
2662 .uri
2663 .to_file_path()
2664 .expect("unable to convert URI to filepath");
2665 let current_view_id = view!(editor).id;
2666 let doc = editor
2667 .document_by_path_mut(path)
2668 .expect("Document for document_changes not found");
2669
2670 // Need to determine a view for apply/append_changes_to_history
2671 let selections = doc.selections();
2672 let view_id = if selections.contains_key(¤t_view_id) {
2673 // use current if possible
2674 current_view_id
2675 } else {
2676 // Hack: we take the first available view_id
2677 selections
2678 .keys()
2679 .next()
2680 .copied()
2681 .expect("No view_id available")
2682 };
2683
2684 let edits = document_edit
2685 .edits
2686 .iter()
2687 .map(|edit| match edit {
2688 lsp::OneOf::Left(text_edit) => text_edit,
2689 lsp::OneOf::Right(annotated_text_edit) => {
2690 &annotated_text_edit.text_edit
2691 }
2692 })
2693 .cloned()
2694 .collect();
2695
2696 let transaction = helix_lsp::util::generate_transaction_from_edits(
2697 doc.text(),
2698 edits,
2699 offset_encoding,
2700 );
2701 doc.apply(&transaction, view_id);
2702 doc.append_changes_to_history(view_id);
2703 }
2704 }
2705 lsp::DocumentChanges::Operations(operations) => {
2706 log::debug!("document changes - operations: {:?}", operations);
2707 editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
2708 }
2709 }
2710 }
2711 }
2712
last_picker(cx: &mut Context)2713 fn last_picker(cx: &mut Context) {
2714 // TODO: last picker does not seem to work well with buffer_picker
2715 cx.callback = Some(Box::new(|compositor: &mut Compositor| {
2716 if let Some(picker) = compositor.last_picker.take() {
2717 compositor.push(picker);
2718 }
2719 // XXX: figure out how to show error when no last picker lifetime
2720 // cx.editor.set_error("no last picker".to_owned())
2721 }));
2722 }
2723
2724 // I inserts at the first nonwhitespace character of each line with a selection
prepend_to_line(cx: &mut Context)2725 fn prepend_to_line(cx: &mut Context) {
2726 goto_first_nonwhitespace(cx);
2727 let doc = doc_mut!(cx.editor);
2728 enter_insert_mode(doc);
2729 }
2730
2731 // A inserts at the end of each line with a selection
append_to_line(cx: &mut Context)2732 fn append_to_line(cx: &mut Context) {
2733 let (view, doc) = current!(cx.editor);
2734 enter_insert_mode(doc);
2735
2736 let selection = doc.selection(view.id).clone().transform(|range| {
2737 let text = doc.text().slice(..);
2738 let line = range.cursor_line(text);
2739 let pos = line_end_char_index(&text, line);
2740 Range::new(pos, pos)
2741 });
2742 doc.set_selection(view.id, selection);
2743 }
2744
2745 /// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for
2746 /// example because we just applied the same changes while saving.
2747 enum Modified {
2748 SetUnmodified,
2749 LeaveModified,
2750 }
2751
2752 // Creates an LspCallback that waits for formatting changes to be computed. When they're done,
2753 // it applies them, but only if the doc hasn't changed.
2754 //
2755 // TODO: provide some way to cancel this, probably as part of a more general job cancellation
2756 // scheme
make_format_callback( doc_id: DocumentId, doc_version: i32, modified: Modified, format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static, ) -> anyhow::Result<job::Callback>2757 async fn make_format_callback(
2758 doc_id: DocumentId,
2759 doc_version: i32,
2760 modified: Modified,
2761 format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static,
2762 ) -> anyhow::Result<job::Callback> {
2763 let format = format.await;
2764 let call: job::Callback = Box::new(move |editor: &mut Editor, _compositor: &mut Compositor| {
2765 let view_id = view!(editor).id;
2766 if let Some(doc) = editor.document_mut(doc_id) {
2767 if doc.version() == doc_version {
2768 doc.apply(&Transaction::from(format), view_id);
2769 doc.append_changes_to_history(view_id);
2770 if let Modified::SetUnmodified = modified {
2771 doc.reset_modified();
2772 }
2773 } else {
2774 log::info!("discarded formatting changes because the document changed");
2775 }
2776 }
2777 });
2778 Ok(call)
2779 }
2780
2781 enum Open {
2782 Below,
2783 Above,
2784 }
2785
open(cx: &mut Context, open: Open)2786 fn open(cx: &mut Context, open: Open) {
2787 let count = cx.count();
2788 let (view, doc) = current!(cx.editor);
2789 enter_insert_mode(doc);
2790
2791 let text = doc.text().slice(..);
2792 let contents = doc.text();
2793 let selection = doc.selection(view.id);
2794
2795 let mut ranges = SmallVec::with_capacity(selection.len());
2796 let mut offs = 0;
2797
2798 let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
2799 let line = range.cursor_line(text);
2800
2801 let line = match open {
2802 // adjust position to the end of the line (next line - 1)
2803 Open::Below => line + 1,
2804 // adjust position to the end of the previous line (current line - 1)
2805 Open::Above => line,
2806 };
2807
2808 // Index to insert newlines after, as well as the char width
2809 // to use to compensate for those inserted newlines.
2810 let (line_end_index, line_end_offset_width) = if line == 0 {
2811 (0, 0)
2812 } else {
2813 (
2814 line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)),
2815 doc.line_ending.len_chars(),
2816 )
2817 };
2818
2819 // TODO: share logic with insert_newline for indentation
2820 let indent_level = indent::suggested_indent_for_pos(
2821 doc.language_config(),
2822 doc.syntax(),
2823 text,
2824 line_end_index,
2825 true,
2826 );
2827 let indent = doc.indent_unit().repeat(indent_level);
2828 let indent_len = indent.len();
2829 let mut text = String::with_capacity(1 + indent_len);
2830 text.push_str(doc.line_ending.as_str());
2831 text.push_str(&indent);
2832 let text = text.repeat(count);
2833
2834 // calculate new selection ranges
2835 let pos = offs + line_end_index + line_end_offset_width;
2836 for i in 0..count {
2837 // pos -> beginning of reference line,
2838 // + (i * (1+indent_len)) -> beginning of i'th line from pos
2839 // + indent_len -> -> indent for i'th line
2840 ranges.push(Range::point(pos + (i * (1 + indent_len)) + indent_len));
2841 }
2842
2843 offs += text.chars().count();
2844
2845 (line_end_index, line_end_index, Some(text.into()))
2846 });
2847
2848 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
2849
2850 doc.apply(&transaction, view.id);
2851 }
2852
2853 // o inserts a new line after each line with a selection
open_below(cx: &mut Context)2854 fn open_below(cx: &mut Context) {
2855 open(cx, Open::Below)
2856 }
2857
2858 // O inserts a new line before each line with a selection
open_above(cx: &mut Context)2859 fn open_above(cx: &mut Context) {
2860 open(cx, Open::Above)
2861 }
2862
normal_mode(cx: &mut Context)2863 fn normal_mode(cx: &mut Context) {
2864 let (view, doc) = current!(cx.editor);
2865
2866 if doc.mode == Mode::Normal {
2867 return;
2868 }
2869
2870 doc.mode = Mode::Normal;
2871
2872 doc.append_changes_to_history(view.id);
2873
2874 // if leaving append mode, move cursor back by 1
2875 if doc.restore_cursor {
2876 let text = doc.text().slice(..);
2877 let selection = doc.selection(view.id).clone().transform(|range| {
2878 Range::new(
2879 range.from(),
2880 graphemes::prev_grapheme_boundary(text, range.to()),
2881 )
2882 });
2883 doc.set_selection(view.id, selection);
2884
2885 doc.restore_cursor = false;
2886 }
2887 }
2888
2889 // Store a jump on the jumplist.
push_jump(editor: &mut Editor)2890 fn push_jump(editor: &mut Editor) {
2891 let (view, doc) = current!(editor);
2892 let jump = (doc.id(), doc.selection(view.id).clone());
2893 view.jumps.push(jump);
2894 }
2895
goto_line(cx: &mut Context)2896 fn goto_line(cx: &mut Context) {
2897 if let Some(count) = cx.count {
2898 push_jump(cx.editor);
2899
2900 let (view, doc) = current!(cx.editor);
2901 let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
2902 // If the last line is blank, don't jump to it.
2903 doc.text().len_lines().saturating_sub(2)
2904 } else {
2905 doc.text().len_lines() - 1
2906 };
2907 let line_idx = std::cmp::min(count.get() - 1, max_line);
2908 let pos = doc.text().line_to_char(line_idx);
2909 doc.set_selection(view.id, Selection::point(pos));
2910 }
2911 }
2912
goto_last_line(cx: &mut Context)2913 fn goto_last_line(cx: &mut Context) {
2914 push_jump(cx.editor);
2915
2916 let (view, doc) = current!(cx.editor);
2917 let line_idx = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
2918 // If the last line is blank, don't jump to it.
2919 doc.text().len_lines().saturating_sub(2)
2920 } else {
2921 doc.text().len_lines() - 1
2922 };
2923 let pos = doc.text().line_to_char(line_idx);
2924 doc.set_selection(view.id, Selection::point(pos));
2925 }
2926
goto_last_accessed_file(cx: &mut Context)2927 fn goto_last_accessed_file(cx: &mut Context) {
2928 let alternate_file = view!(cx.editor).last_accessed_doc;
2929 if let Some(alt) = alternate_file {
2930 cx.editor.switch(alt, Action::Replace);
2931 } else {
2932 cx.editor.set_error("no last accessed buffer".to_owned())
2933 }
2934 }
2935
select_mode(cx: &mut Context)2936 fn select_mode(cx: &mut Context) {
2937 let (view, doc) = current!(cx.editor);
2938 let text = doc.text().slice(..);
2939
2940 // Make sure end-of-document selections are also 1-width.
2941 // (With the exception of being in an empty document, of course.)
2942 let selection = doc.selection(view.id).clone().transform(|range| {
2943 if range.is_empty() && range.head == text.len_chars() {
2944 Range::new(
2945 graphemes::prev_grapheme_boundary(text, range.anchor),
2946 range.head,
2947 )
2948 } else {
2949 range
2950 }
2951 });
2952 doc.set_selection(view.id, selection);
2953
2954 doc_mut!(cx.editor).mode = Mode::Select;
2955 }
2956
exit_select_mode(cx: &mut Context)2957 fn exit_select_mode(cx: &mut Context) {
2958 let doc = doc_mut!(cx.editor);
2959 if doc.mode == Mode::Select {
2960 doc.mode = Mode::Normal;
2961 }
2962 }
2963
goto_impl( editor: &mut Editor, compositor: &mut Compositor, locations: Vec<lsp::Location>, offset_encoding: OffsetEncoding, )2964 fn goto_impl(
2965 editor: &mut Editor,
2966 compositor: &mut Compositor,
2967 locations: Vec<lsp::Location>,
2968 offset_encoding: OffsetEncoding,
2969 ) {
2970 push_jump(editor);
2971
2972 fn jump_to(
2973 editor: &mut Editor,
2974 location: &lsp::Location,
2975 offset_encoding: OffsetEncoding,
2976 action: Action,
2977 ) {
2978 let path = location
2979 .uri
2980 .to_file_path()
2981 .expect("unable to convert URI to filepath");
2982 let _id = editor.open(path, action).expect("editor.open failed");
2983 let (view, doc) = current!(editor);
2984 let definition_pos = location.range.start;
2985 // TODO: convert inside server
2986 let new_pos =
2987 if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) {
2988 new_pos
2989 } else {
2990 return;
2991 };
2992 doc.set_selection(view.id, Selection::point(new_pos));
2993 align_view(doc, view, Align::Center);
2994 }
2995
2996 let cwdir = std::env::current_dir().expect("couldn't determine current directory");
2997
2998 match locations.as_slice() {
2999 [location] => {
3000 jump_to(editor, location, offset_encoding, Action::Replace);
3001 }
3002 [] => {
3003 editor.set_error("No definition found.".to_string());
3004 }
3005 _locations => {
3006 let picker = FilePicker::new(
3007 locations,
3008 move |location| {
3009 let file: Cow<'_, str> = (location.uri.scheme() == "file")
3010 .then(|| {
3011 location
3012 .uri
3013 .to_file_path()
3014 .map(|path| {
3015 // strip root prefix
3016 path.strip_prefix(&cwdir)
3017 .map(|path| path.to_path_buf())
3018 .unwrap_or(path)
3019 })
3020 .ok()
3021 .and_then(|path| path.to_str().map(|path| path.to_owned().into()))
3022 })
3023 .flatten()
3024 .unwrap_or_else(|| location.uri.as_str().into());
3025 let line = location.range.start.line;
3026 format!("{}:{}", file, line).into()
3027 },
3028 move |editor: &mut Editor, location, action| {
3029 jump_to(editor, location, offset_encoding, action)
3030 },
3031 |_editor, location| {
3032 let path = location.uri.to_file_path().unwrap();
3033 let line = Some((
3034 location.range.start.line as usize,
3035 location.range.end.line as usize,
3036 ));
3037 Some((path, line))
3038 },
3039 );
3040 compositor.push(Box::new(picker));
3041 }
3042 }
3043 }
3044
goto_definition(cx: &mut Context)3045 fn goto_definition(cx: &mut Context) {
3046 let (view, doc) = current!(cx.editor);
3047 let language_server = match doc.language_server() {
3048 Some(language_server) => language_server,
3049 None => return,
3050 };
3051
3052 let offset_encoding = language_server.offset_encoding();
3053
3054 let pos = pos_to_lsp_pos(
3055 doc.text(),
3056 doc.selection(view.id)
3057 .primary()
3058 .cursor(doc.text().slice(..)),
3059 offset_encoding,
3060 );
3061
3062 let future = language_server.goto_definition(doc.identifier(), pos, None);
3063
3064 cx.callback(
3065 future,
3066 move |editor: &mut Editor,
3067 compositor: &mut Compositor,
3068 response: Option<lsp::GotoDefinitionResponse>| {
3069 let items = match response {
3070 Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
3071 Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
3072 Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
3073 .into_iter()
3074 .map(|location_link| lsp::Location {
3075 uri: location_link.target_uri,
3076 range: location_link.target_range,
3077 })
3078 .collect(),
3079 None => Vec::new(),
3080 };
3081
3082 goto_impl(editor, compositor, items, offset_encoding);
3083 },
3084 );
3085 }
3086
goto_type_definition(cx: &mut Context)3087 fn goto_type_definition(cx: &mut Context) {
3088 let (view, doc) = current!(cx.editor);
3089 let language_server = match doc.language_server() {
3090 Some(language_server) => language_server,
3091 None => return,
3092 };
3093
3094 let offset_encoding = language_server.offset_encoding();
3095
3096 let pos = pos_to_lsp_pos(
3097 doc.text(),
3098 doc.selection(view.id)
3099 .primary()
3100 .cursor(doc.text().slice(..)),
3101 offset_encoding,
3102 );
3103
3104 let future = language_server.goto_type_definition(doc.identifier(), pos, None);
3105
3106 cx.callback(
3107 future,
3108 move |editor: &mut Editor,
3109 compositor: &mut Compositor,
3110 response: Option<lsp::GotoDefinitionResponse>| {
3111 let items = match response {
3112 Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
3113 Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
3114 Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
3115 .into_iter()
3116 .map(|location_link| lsp::Location {
3117 uri: location_link.target_uri,
3118 range: location_link.target_range,
3119 })
3120 .collect(),
3121 None => Vec::new(),
3122 };
3123
3124 goto_impl(editor, compositor, items, offset_encoding);
3125 },
3126 );
3127 }
3128
goto_implementation(cx: &mut Context)3129 fn goto_implementation(cx: &mut Context) {
3130 let (view, doc) = current!(cx.editor);
3131 let language_server = match doc.language_server() {
3132 Some(language_server) => language_server,
3133 None => return,
3134 };
3135
3136 let offset_encoding = language_server.offset_encoding();
3137
3138 let pos = pos_to_lsp_pos(
3139 doc.text(),
3140 doc.selection(view.id)
3141 .primary()
3142 .cursor(doc.text().slice(..)),
3143 offset_encoding,
3144 );
3145
3146 let future = language_server.goto_implementation(doc.identifier(), pos, None);
3147
3148 cx.callback(
3149 future,
3150 move |editor: &mut Editor,
3151 compositor: &mut Compositor,
3152 response: Option<lsp::GotoDefinitionResponse>| {
3153 let items = match response {
3154 Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
3155 Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
3156 Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
3157 .into_iter()
3158 .map(|location_link| lsp::Location {
3159 uri: location_link.target_uri,
3160 range: location_link.target_range,
3161 })
3162 .collect(),
3163 None => Vec::new(),
3164 };
3165
3166 goto_impl(editor, compositor, items, offset_encoding);
3167 },
3168 );
3169 }
3170
goto_reference(cx: &mut Context)3171 fn goto_reference(cx: &mut Context) {
3172 let (view, doc) = current!(cx.editor);
3173 let language_server = match doc.language_server() {
3174 Some(language_server) => language_server,
3175 None => return,
3176 };
3177
3178 let offset_encoding = language_server.offset_encoding();
3179
3180 let pos = pos_to_lsp_pos(
3181 doc.text(),
3182 doc.selection(view.id)
3183 .primary()
3184 .cursor(doc.text().slice(..)),
3185 offset_encoding,
3186 );
3187
3188 let future = language_server.goto_reference(doc.identifier(), pos, None);
3189
3190 cx.callback(
3191 future,
3192 move |editor: &mut Editor,
3193 compositor: &mut Compositor,
3194 items: Option<Vec<lsp::Location>>| {
3195 goto_impl(
3196 editor,
3197 compositor,
3198 items.unwrap_or_default(),
3199 offset_encoding,
3200 );
3201 },
3202 );
3203 }
3204
goto_pos(editor: &mut Editor, pos: usize)3205 fn goto_pos(editor: &mut Editor, pos: usize) {
3206 push_jump(editor);
3207
3208 let (view, doc) = current!(editor);
3209
3210 doc.set_selection(view.id, Selection::point(pos));
3211 align_view(doc, view, Align::Center);
3212 }
3213
goto_first_diag(cx: &mut Context)3214 fn goto_first_diag(cx: &mut Context) {
3215 let editor = &mut cx.editor;
3216 let (_, doc) = current!(editor);
3217
3218 let diag = if let Some(diag) = doc.diagnostics().first() {
3219 diag.range.start
3220 } else {
3221 return;
3222 };
3223
3224 goto_pos(editor, diag);
3225 }
3226
goto_last_diag(cx: &mut Context)3227 fn goto_last_diag(cx: &mut Context) {
3228 let editor = &mut cx.editor;
3229 let (_, doc) = current!(editor);
3230
3231 let diag = if let Some(diag) = doc.diagnostics().last() {
3232 diag.range.start
3233 } else {
3234 return;
3235 };
3236
3237 goto_pos(editor, diag);
3238 }
3239
goto_next_diag(cx: &mut Context)3240 fn goto_next_diag(cx: &mut Context) {
3241 let editor = &mut cx.editor;
3242 let (view, doc) = current!(editor);
3243
3244 let cursor_pos = doc
3245 .selection(view.id)
3246 .primary()
3247 .cursor(doc.text().slice(..));
3248 let diag = if let Some(diag) = doc
3249 .diagnostics()
3250 .iter()
3251 .map(|diag| diag.range.start)
3252 .find(|&pos| pos > cursor_pos)
3253 {
3254 diag
3255 } else if let Some(diag) = doc.diagnostics().first() {
3256 diag.range.start
3257 } else {
3258 return;
3259 };
3260
3261 goto_pos(editor, diag);
3262 }
3263
goto_prev_diag(cx: &mut Context)3264 fn goto_prev_diag(cx: &mut Context) {
3265 let editor = &mut cx.editor;
3266 let (view, doc) = current!(editor);
3267
3268 let cursor_pos = doc
3269 .selection(view.id)
3270 .primary()
3271 .cursor(doc.text().slice(..));
3272 let diag = if let Some(diag) = doc
3273 .diagnostics()
3274 .iter()
3275 .rev()
3276 .map(|diag| diag.range.start)
3277 .find(|&pos| pos < cursor_pos)
3278 {
3279 diag
3280 } else if let Some(diag) = doc.diagnostics().last() {
3281 diag.range.start
3282 } else {
3283 return;
3284 };
3285
3286 goto_pos(editor, diag);
3287 }
3288
signature_help(cx: &mut Context)3289 fn signature_help(cx: &mut Context) {
3290 let (view, doc) = current!(cx.editor);
3291
3292 let language_server = match doc.language_server() {
3293 Some(language_server) => language_server,
3294 None => return,
3295 };
3296
3297 let pos = pos_to_lsp_pos(
3298 doc.text(),
3299 doc.selection(view.id)
3300 .primary()
3301 .cursor(doc.text().slice(..)),
3302 language_server.offset_encoding(),
3303 );
3304
3305 let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
3306
3307 cx.callback(
3308 future,
3309 move |_editor: &mut Editor,
3310 _compositor: &mut Compositor,
3311 response: Option<lsp::SignatureHelp>| {
3312 if let Some(signature_help) = response {
3313 log::info!("{:?}", signature_help);
3314 // signatures
3315 // active_signature
3316 // active_parameter
3317 // render as:
3318
3319 // signature
3320 // ----------
3321 // doc
3322
3323 // with active param highlighted
3324 }
3325 },
3326 );
3327 }
3328
3329 // NOTE: Transactions in this module get appended to history when we switch back to normal mode.
3330 pub mod insert {
3331 use super::*;
3332 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3333 pub type PostHook = fn(&mut Context, char);
3334
3335 // It trigger completion when idle timer reaches deadline
3336 // Only trigger completion if the word under cursor is longer than n characters
idle_completion(cx: &mut Context)3337 pub fn idle_completion(cx: &mut Context) {
3338 let (view, doc) = current!(cx.editor);
3339 let text = doc.text().slice(..);
3340 let cursor = doc.selection(view.id).primary().cursor(text);
3341
3342 use helix_core::chars::char_is_word;
3343 let mut iter = text.chars_at(cursor);
3344 iter.reverse();
3345 for _ in 0..cx.editor.config.completion_trigger_len {
3346 match iter.next() {
3347 Some(c) if char_is_word(c) => {}
3348 _ => return,
3349 }
3350 }
3351 super::completion(cx);
3352 }
3353
language_server_completion(cx: &mut Context, ch: char)3354 fn language_server_completion(cx: &mut Context, ch: char) {
3355 // if ch matches completion char, trigger completion
3356 let doc = doc_mut!(cx.editor);
3357 let language_server = match doc.language_server() {
3358 Some(language_server) => language_server,
3359 None => return,
3360 };
3361
3362 let capabilities = language_server.capabilities();
3363
3364 if let Some(lsp::CompletionOptions {
3365 trigger_characters: Some(triggers),
3366 ..
3367 }) = &capabilities.completion_provider
3368 {
3369 // TODO: what if trigger is multiple chars long
3370 if triggers.iter().any(|trigger| trigger.contains(ch)) {
3371 cx.editor.clear_idle_timer();
3372 super::completion(cx);
3373 }
3374 }
3375 }
3376
signature_help(cx: &mut Context, ch: char)3377 fn signature_help(cx: &mut Context, ch: char) {
3378 // if ch matches signature_help char, trigger
3379 let doc = doc_mut!(cx.editor);
3380 let language_server = match doc.language_server() {
3381 Some(language_server) => language_server,
3382 None => return,
3383 };
3384
3385 let capabilities = language_server.capabilities();
3386
3387 if let lsp::ServerCapabilities {
3388 signature_help_provider:
3389 Some(lsp::SignatureHelpOptions {
3390 trigger_characters: Some(triggers),
3391 // TODO: retrigger_characters
3392 ..
3393 }),
3394 ..
3395 } = capabilities
3396 {
3397 // TODO: what if trigger is multiple chars long
3398 let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3399
3400 if is_trigger {
3401 super::signature_help(cx);
3402 }
3403 }
3404
3405 // SignatureHelp {
3406 // signatures: [
3407 // SignatureInformation {
3408 // label: "fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error>",
3409 // documentation: None,
3410 // parameters: Some(
3411 // [ParameterInformation { label: Simple("path: PathBuf"), documentation: None },
3412 // ParameterInformation { label: Simple("action: Action"), documentation: None }]
3413 // ),
3414 // active_parameter: Some(0)
3415 // }
3416 // ],
3417 // active_signature: None, active_parameter: Some(0)
3418 // }
3419 }
3420
3421 // The default insert hook: simply insert the character
3422 #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction>3423 fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
3424 let t = Tendril::from_char(ch);
3425 let transaction = Transaction::insert(doc, selection, t);
3426 Some(transaction)
3427 }
3428
3429 use helix_core::auto_pairs;
3430
insert_char(cx: &mut Context, c: char)3431 pub fn insert_char(cx: &mut Context, c: char) {
3432 let (view, doc) = current!(cx.editor);
3433
3434 let hooks: &[Hook] = match cx.editor.config.auto_pairs {
3435 true => &[auto_pairs::hook, insert],
3436 false => &[insert],
3437 };
3438
3439 let text = doc.text();
3440 let selection = doc.selection(view.id).clone().cursors(text.slice(..));
3441
3442 // run through insert hooks, stopping on the first one that returns Some(t)
3443 for hook in hooks {
3444 if let Some(transaction) = hook(text, &selection, c) {
3445 doc.apply(&transaction, view.id);
3446 break;
3447 }
3448 }
3449
3450 // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3451 // this could also generically look at Transaction, but it's a bit annoying to look at
3452 // Operation instead of Change.
3453 for hook in &[language_server_completion, signature_help] {
3454 // for hook in &[signature_help] {
3455 hook(cx, c);
3456 }
3457 }
3458
insert_tab(cx: &mut Context)3459 pub fn insert_tab(cx: &mut Context) {
3460 let (view, doc) = current!(cx.editor);
3461 // TODO: round out to nearest indentation level (for example a line with 3 spaces should
3462 // indent by one to reach 4 spaces).
3463
3464 let indent = Tendril::from(doc.indent_unit());
3465 let transaction = Transaction::insert(
3466 doc.text(),
3467 &doc.selection(view.id).clone().cursors(doc.text().slice(..)),
3468 indent,
3469 );
3470 doc.apply(&transaction, view.id);
3471 }
3472
insert_newline(cx: &mut Context)3473 pub fn insert_newline(cx: &mut Context) {
3474 let (view, doc) = current!(cx.editor);
3475 let text = doc.text().slice(..);
3476
3477 let contents = doc.text();
3478 let selection = doc.selection(view.id).clone().cursors(text);
3479 let mut ranges = SmallVec::with_capacity(selection.len());
3480
3481 // TODO: this is annoying, but we need to do it to properly calculate pos after edits
3482 let mut offs = 0;
3483
3484 let mut transaction = Transaction::change_by_selection(contents, &selection, |range| {
3485 let pos = range.head;
3486
3487 let prev = if pos == 0 {
3488 ' '
3489 } else {
3490 contents.char(pos - 1)
3491 };
3492 let curr = contents.get_char(pos).unwrap_or(' ');
3493
3494 // TODO: offset range.head by 1? when calculating?
3495 let indent_level = indent::suggested_indent_for_pos(
3496 doc.language_config(),
3497 doc.syntax(),
3498 text,
3499 pos.saturating_sub(1),
3500 true,
3501 );
3502 let indent = doc.indent_unit().repeat(indent_level);
3503 let mut text = String::with_capacity(1 + indent.len());
3504 text.push_str(doc.line_ending.as_str());
3505 text.push_str(&indent);
3506
3507 let head = pos + offs + text.chars().count();
3508
3509 // TODO: range replace or extend
3510 // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
3511 // can be used with cx.mode to do replace or extend on most changes
3512 ranges.push(Range::new(
3513 if range.is_empty() {
3514 head
3515 } else {
3516 range.anchor + offs
3517 },
3518 head,
3519 ));
3520
3521 // if between a bracket pair
3522 if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
3523 // another newline, indent the end bracket one level less
3524 let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
3525 text.push_str(doc.line_ending.as_str());
3526 text.push_str(&indent);
3527 }
3528
3529 offs += text.chars().count();
3530
3531 (pos, pos, Some(text.into()))
3532 });
3533
3534 transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
3535 //
3536
3537 doc.apply(&transaction, view.id);
3538 }
3539
3540 // TODO: handle indent-aware delete
delete_char_backward(cx: &mut Context)3541 pub fn delete_char_backward(cx: &mut Context) {
3542 let count = cx.count();
3543 let (view, doc) = current!(cx.editor);
3544 let text = doc.text().slice(..);
3545 let transaction =
3546 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3547 let pos = range.cursor(text);
3548 (
3549 graphemes::nth_prev_grapheme_boundary(text, pos, count),
3550 pos,
3551 None,
3552 )
3553 });
3554 doc.apply(&transaction, view.id);
3555 }
3556
delete_char_forward(cx: &mut Context)3557 pub fn delete_char_forward(cx: &mut Context) {
3558 let count = cx.count();
3559 let (view, doc) = current!(cx.editor);
3560 let text = doc.text().slice(..);
3561 let transaction =
3562 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3563 let pos = range.cursor(text);
3564 (
3565 pos,
3566 graphemes::nth_next_grapheme_boundary(text, pos, count),
3567 None,
3568 )
3569 });
3570 doc.apply(&transaction, view.id);
3571 }
3572
delete_word_backward(cx: &mut Context)3573 pub fn delete_word_backward(cx: &mut Context) {
3574 let count = cx.count();
3575 let (view, doc) = current!(cx.editor);
3576 let text = doc.text().slice(..);
3577
3578 let selection = doc
3579 .selection(view.id)
3580 .clone()
3581 .transform(|range| movement::move_prev_word_start(text, range, count));
3582 doc.set_selection(view.id, selection);
3583 delete_selection(cx)
3584 }
3585 }
3586
3587 // Undo / Redo
3588
3589 // TODO: each command could simply return a Option<transaction>, then the higher level handles
3590 // storing it?
3591
undo(cx: &mut Context)3592 fn undo(cx: &mut Context) {
3593 let (view, doc) = current!(cx.editor);
3594 let view_id = view.id;
3595 doc.undo(view_id);
3596 }
3597
redo(cx: &mut Context)3598 fn redo(cx: &mut Context) {
3599 let (view, doc) = current!(cx.editor);
3600 let view_id = view.id;
3601 doc.redo(view_id);
3602 }
3603
3604 // Yank / Paste
3605
yank(cx: &mut Context)3606 fn yank(cx: &mut Context) {
3607 let (view, doc) = current!(cx.editor);
3608 let text = doc.text().slice(..);
3609
3610 let values: Vec<String> = doc
3611 .selection(view.id)
3612 .fragments(text)
3613 .map(Cow::into_owned)
3614 .collect();
3615
3616 let msg = format!(
3617 "yanked {} selection(s) to register {}",
3618 values.len(),
3619 cx.register.unwrap_or('"')
3620 );
3621
3622 cx.editor
3623 .registers
3624 .write(cx.register.unwrap_or('"'), values);
3625
3626 cx.editor.set_status(msg);
3627 exit_select_mode(cx);
3628 }
3629
yank_joined_to_clipboard_impl( editor: &mut Editor, separator: &str, clipboard_type: ClipboardType, ) -> anyhow::Result<()>3630 fn yank_joined_to_clipboard_impl(
3631 editor: &mut Editor,
3632 separator: &str,
3633 clipboard_type: ClipboardType,
3634 ) -> anyhow::Result<()> {
3635 let (view, doc) = current!(editor);
3636 let text = doc.text().slice(..);
3637
3638 let values: Vec<String> = doc
3639 .selection(view.id)
3640 .fragments(text)
3641 .map(Cow::into_owned)
3642 .collect();
3643
3644 let msg = format!(
3645 "joined and yanked {} selection(s) to system clipboard",
3646 values.len(),
3647 );
3648
3649 let joined = values.join(separator);
3650
3651 editor
3652 .clipboard_provider
3653 .set_contents(joined, clipboard_type)
3654 .context("Couldn't set system clipboard content")?;
3655
3656 editor.set_status(msg);
3657
3658 Ok(())
3659 }
3660
yank_joined_to_clipboard(cx: &mut Context)3661 fn yank_joined_to_clipboard(cx: &mut Context) {
3662 let line_ending = current!(cx.editor).1.line_ending;
3663 let _ = yank_joined_to_clipboard_impl(
3664 &mut cx.editor,
3665 line_ending.as_str(),
3666 ClipboardType::Clipboard,
3667 );
3668 exit_select_mode(cx);
3669 }
3670
yank_main_selection_to_clipboard_impl( editor: &mut Editor, clipboard_type: ClipboardType, ) -> anyhow::Result<()>3671 fn yank_main_selection_to_clipboard_impl(
3672 editor: &mut Editor,
3673 clipboard_type: ClipboardType,
3674 ) -> anyhow::Result<()> {
3675 let (view, doc) = current!(editor);
3676 let text = doc.text().slice(..);
3677
3678 let value = doc.selection(view.id).primary().fragment(text);
3679
3680 if let Err(e) = editor
3681 .clipboard_provider
3682 .set_contents(value.into_owned(), clipboard_type)
3683 {
3684 bail!("Couldn't set system clipboard content: {}", e);
3685 }
3686
3687 editor.set_status("yanked main selection to system clipboard".to_owned());
3688 Ok(())
3689 }
3690
yank_main_selection_to_clipboard(cx: &mut Context)3691 fn yank_main_selection_to_clipboard(cx: &mut Context) {
3692 let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
3693 }
3694
yank_joined_to_primary_clipboard(cx: &mut Context)3695 fn yank_joined_to_primary_clipboard(cx: &mut Context) {
3696 let line_ending = current!(cx.editor).1.line_ending;
3697 let _ = yank_joined_to_clipboard_impl(
3698 &mut cx.editor,
3699 line_ending.as_str(),
3700 ClipboardType::Selection,
3701 );
3702 }
3703
yank_main_selection_to_primary_clipboard(cx: &mut Context)3704 fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
3705 let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
3706 exit_select_mode(cx);
3707 }
3708
3709 #[derive(Copy, Clone)]
3710 enum Paste {
3711 Before,
3712 After,
3713 }
3714
paste_impl( values: &[String], doc: &mut Document, view: &View, action: Paste, ) -> Option<Transaction>3715 fn paste_impl(
3716 values: &[String],
3717 doc: &mut Document,
3718 view: &View,
3719 action: Paste,
3720 ) -> Option<Transaction> {
3721 let repeat = std::iter::repeat(
3722 values
3723 .last()
3724 .map(|value| Tendril::from_slice(value))
3725 .unwrap(),
3726 );
3727
3728 // if any of values ends with a line ending, it's linewise paste
3729 let linewise = values
3730 .iter()
3731 .any(|value| get_line_ending_of_str(value).is_some());
3732
3733 // Only compiled once.
3734 #[allow(clippy::trivial_regex)]
3735 static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap());
3736 let mut values = values
3737 .iter()
3738 .map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
3739 .map(|value| Tendril::from(value.as_ref()))
3740 .chain(repeat);
3741
3742 let text = doc.text();
3743 let selection = doc.selection(view.id);
3744
3745 let transaction = Transaction::change_by_selection(text, selection, |range| {
3746 let pos = match (action, linewise) {
3747 // paste linewise before
3748 (Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())),
3749 // paste linewise after
3750 (Paste::After, true) => {
3751 let line = range.line_range(text.slice(..)).1;
3752 text.line_to_char((line + 1).min(text.len_lines()))
3753 }
3754 // paste insert
3755 (Paste::Before, false) => range.from(),
3756 // paste append
3757 (Paste::After, false) => range.to(),
3758 };
3759 (pos, pos, Some(values.next().unwrap()))
3760 });
3761
3762 Some(transaction)
3763 }
3764
paste_clipboard_impl( editor: &mut Editor, action: Paste, clipboard_type: ClipboardType, ) -> anyhow::Result<()>3765 fn paste_clipboard_impl(
3766 editor: &mut Editor,
3767 action: Paste,
3768 clipboard_type: ClipboardType,
3769 ) -> anyhow::Result<()> {
3770 let (view, doc) = current!(editor);
3771
3772 match editor
3773 .clipboard_provider
3774 .get_contents(clipboard_type)
3775 .map(|contents| paste_impl(&[contents], doc, view, action))
3776 {
3777 Ok(Some(transaction)) => {
3778 doc.apply(&transaction, view.id);
3779 doc.append_changes_to_history(view.id);
3780 Ok(())
3781 }
3782 Ok(None) => Ok(()),
3783 Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3784 }
3785 }
3786
paste_clipboard_after(cx: &mut Context)3787 fn paste_clipboard_after(cx: &mut Context) {
3788 let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
3789 }
3790
paste_clipboard_before(cx: &mut Context)3791 fn paste_clipboard_before(cx: &mut Context) {
3792 let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
3793 }
3794
paste_primary_clipboard_after(cx: &mut Context)3795 fn paste_primary_clipboard_after(cx: &mut Context) {
3796 let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
3797 }
3798
paste_primary_clipboard_before(cx: &mut Context)3799 fn paste_primary_clipboard_before(cx: &mut Context) {
3800 let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
3801 }
3802
replace_with_yanked(cx: &mut Context)3803 fn replace_with_yanked(cx: &mut Context) {
3804 let reg_name = cx.register.unwrap_or('"');
3805 let (view, doc) = current!(cx.editor);
3806 let registers = &mut cx.editor.registers;
3807
3808 if let Some(values) = registers.read(reg_name) {
3809 if !values.is_empty() {
3810 let repeat = std::iter::repeat(
3811 values
3812 .last()
3813 .map(|value| Tendril::from_slice(value))
3814 .unwrap(),
3815 );
3816 let mut values = values
3817 .iter()
3818 .map(|value| Tendril::from_slice(value))
3819 .chain(repeat);
3820 let selection = doc.selection(view.id);
3821 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3822 if !range.is_empty() {
3823 (range.from(), range.to(), Some(values.next().unwrap()))
3824 } else {
3825 (range.from(), range.to(), None)
3826 }
3827 });
3828
3829 doc.apply(&transaction, view.id);
3830 doc.append_changes_to_history(view.id);
3831 }
3832 }
3833 }
3834
replace_selections_with_clipboard_impl( editor: &mut Editor, clipboard_type: ClipboardType, ) -> anyhow::Result<()>3835 fn replace_selections_with_clipboard_impl(
3836 editor: &mut Editor,
3837 clipboard_type: ClipboardType,
3838 ) -> anyhow::Result<()> {
3839 let (view, doc) = current!(editor);
3840
3841 match editor.clipboard_provider.get_contents(clipboard_type) {
3842 Ok(contents) => {
3843 let selection = doc.selection(view.id);
3844 let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
3845 (range.from(), range.to(), Some(contents.as_str().into()))
3846 });
3847
3848 doc.apply(&transaction, view.id);
3849 doc.append_changes_to_history(view.id);
3850 Ok(())
3851 }
3852 Err(e) => Err(e.context("Couldn't get system clipboard contents")),
3853 }
3854 }
3855
replace_selections_with_clipboard(cx: &mut Context)3856 fn replace_selections_with_clipboard(cx: &mut Context) {
3857 let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
3858 }
3859
replace_selections_with_primary_clipboard(cx: &mut Context)3860 fn replace_selections_with_primary_clipboard(cx: &mut Context) {
3861 let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
3862 }
3863
paste_after(cx: &mut Context)3864 fn paste_after(cx: &mut Context) {
3865 let reg_name = cx.register.unwrap_or('"');
3866 let (view, doc) = current!(cx.editor);
3867 let registers = &mut cx.editor.registers;
3868
3869 if let Some(transaction) = registers
3870 .read(reg_name)
3871 .and_then(|values| paste_impl(values, doc, view, Paste::After))
3872 {
3873 doc.apply(&transaction, view.id);
3874 doc.append_changes_to_history(view.id);
3875 }
3876 }
3877
paste_before(cx: &mut Context)3878 fn paste_before(cx: &mut Context) {
3879 let reg_name = cx.register.unwrap_or('"');
3880 let (view, doc) = current!(cx.editor);
3881 let registers = &mut cx.editor.registers;
3882
3883 if let Some(transaction) = registers
3884 .read(reg_name)
3885 .and_then(|values| paste_impl(values, doc, view, Paste::Before))
3886 {
3887 doc.apply(&transaction, view.id);
3888 doc.append_changes_to_history(view.id);
3889 }
3890 }
3891
get_lines(doc: &Document, view_id: ViewId) -> Vec<usize>3892 fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
3893 let mut lines = Vec::new();
3894
3895 // Get all line numbers
3896 for range in doc.selection(view_id) {
3897 let (start, end) = range.line_range(doc.text().slice(..));
3898
3899 for line in start..=end {
3900 lines.push(line)
3901 }
3902 }
3903 lines.sort_unstable(); // sorting by usize so _unstable is preferred
3904 lines.dedup();
3905 lines
3906 }
3907
indent(cx: &mut Context)3908 fn indent(cx: &mut Context) {
3909 let count = cx.count();
3910 let (view, doc) = current!(cx.editor);
3911 let lines = get_lines(doc, view.id);
3912
3913 // Indent by one level
3914 let indent = Tendril::from(doc.indent_unit().repeat(count));
3915
3916 let transaction = Transaction::change(
3917 doc.text(),
3918 lines.into_iter().map(|line| {
3919 let pos = doc.text().line_to_char(line);
3920 (pos, pos, Some(indent.clone()))
3921 }),
3922 );
3923 doc.apply(&transaction, view.id);
3924 doc.append_changes_to_history(view.id);
3925 }
3926
unindent(cx: &mut Context)3927 fn unindent(cx: &mut Context) {
3928 let count = cx.count();
3929 let (view, doc) = current!(cx.editor);
3930 let lines = get_lines(doc, view.id);
3931 let mut changes = Vec::with_capacity(lines.len());
3932 let tab_width = doc.tab_width();
3933 let indent_width = count * tab_width;
3934
3935 for line_idx in lines {
3936 let line = doc.text().line(line_idx);
3937 let mut width = 0;
3938 let mut pos = 0;
3939
3940 for ch in line.chars() {
3941 match ch {
3942 ' ' => width += 1,
3943 '\t' => width = (width / tab_width + 1) * tab_width,
3944 _ => break,
3945 }
3946
3947 pos += 1;
3948
3949 if width >= indent_width {
3950 break;
3951 }
3952 }
3953
3954 // now delete from start to first non-blank
3955 if pos > 0 {
3956 let start = doc.text().line_to_char(line_idx);
3957 changes.push((start, start + pos, None))
3958 }
3959 }
3960
3961 let transaction = Transaction::change(doc.text(), changes.into_iter());
3962
3963 doc.apply(&transaction, view.id);
3964 doc.append_changes_to_history(view.id);
3965 }
3966
format_selections(cx: &mut Context)3967 fn format_selections(cx: &mut Context) {
3968 let (view, doc) = current!(cx.editor);
3969
3970 // via lsp if available
3971 // else via tree-sitter indentation calculations
3972
3973 let language_server = match doc.language_server() {
3974 Some(language_server) => language_server,
3975 None => return,
3976 };
3977
3978 let ranges: Vec<lsp::Range> = doc
3979 .selection(view.id)
3980 .iter()
3981 .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
3982 .collect();
3983
3984 // TODO: all of the TODO's and commented code inside the loop,
3985 // to make this actually work.
3986 for _range in ranges {
3987 let _language_server = match doc.language_server() {
3988 Some(language_server) => language_server,
3989 None => return,
3990 };
3991 // TODO: handle fails
3992 // TODO: concurrent map
3993
3994 // TODO: need to block to get the formatting
3995
3996 // let edits = block_on(language_server.text_document_range_formatting(
3997 // doc.identifier(),
3998 // range,
3999 // lsp::FormattingOptions::default(),
4000 // ))
4001 // .unwrap_or_default();
4002
4003 // let transaction = helix_lsp::util::generate_transaction_from_edits(
4004 // doc.text(),
4005 // edits,
4006 // language_server.offset_encoding(),
4007 // );
4008
4009 // doc.apply(&transaction, view.id);
4010 }
4011
4012 doc.append_changes_to_history(view.id);
4013 }
4014
join_selections(cx: &mut Context)4015 fn join_selections(cx: &mut Context) {
4016 use movement::skip_while;
4017 let (view, doc) = current!(cx.editor);
4018 let text = doc.text();
4019 let slice = doc.text().slice(..);
4020
4021 let mut changes = Vec::new();
4022 let fragment = Tendril::from(" ");
4023
4024 for selection in doc.selection(view.id) {
4025 let (start, mut end) = selection.line_range(slice);
4026 if start == end {
4027 end = (end + 1).min(text.len_lines() - 1);
4028 }
4029 let lines = start..end;
4030
4031 changes.reserve(lines.len());
4032
4033 for line in lines {
4034 let start = line_end_char_index(&slice, line);
4035 let mut end = text.line_to_char(line + 1);
4036 end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
4037
4038 // need to skip from start, not end
4039 let change = (start, end, Some(fragment.clone()));
4040 changes.push(change);
4041 }
4042 }
4043
4044 changes.sort_unstable_by_key(|(from, _to, _text)| *from);
4045 changes.dedup();
4046
4047 // TODO: joining multiple empty lines should be replaced by a single space.
4048 // need to merge change ranges that touch
4049
4050 let transaction = Transaction::change(doc.text(), changes.into_iter());
4051 // TODO: select inserted spaces
4052 // .with_selection(selection);
4053
4054 doc.apply(&transaction, view.id);
4055 doc.append_changes_to_history(view.id);
4056 }
4057
keep_selections(cx: &mut Context)4058 fn keep_selections(cx: &mut Context) {
4059 // keep selections matching regex
4060 let reg = cx.register.unwrap_or('/');
4061 let prompt = ui::regex_prompt(
4062 cx,
4063 "keep:".into(),
4064 Some(reg),
4065 move |view, doc, regex, event| {
4066 if event != PromptEvent::Update {
4067 return;
4068 }
4069 let text = doc.text().slice(..);
4070
4071 if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) {
4072 doc.set_selection(view.id, selection);
4073 }
4074 },
4075 );
4076
4077 cx.push_layer(Box::new(prompt));
4078 }
4079
keep_primary_selection(cx: &mut Context)4080 fn keep_primary_selection(cx: &mut Context) {
4081 let (view, doc) = current!(cx.editor);
4082 // TODO: handle count
4083
4084 let range = doc.selection(view.id).primary();
4085 doc.set_selection(view.id, Selection::single(range.anchor, range.head));
4086 }
4087
remove_primary_selection(cx: &mut Context)4088 fn remove_primary_selection(cx: &mut Context) {
4089 let (view, doc) = current!(cx.editor);
4090 // TODO: handle count
4091
4092 let selection = doc.selection(view.id);
4093 if selection.len() == 1 {
4094 cx.editor.set_error("no selections remaining".to_owned());
4095 return;
4096 }
4097 let index = selection.primary_index();
4098 let selection = selection.clone().remove(index);
4099
4100 doc.set_selection(view.id, selection);
4101 }
4102
completion(cx: &mut Context)4103 pub fn completion(cx: &mut Context) {
4104 // trigger on trigger char, or if user calls it
4105 // (or on word char typing??)
4106 // after it's triggered, if response marked is_incomplete, update on every subsequent keypress
4107 //
4108 // lsp calls are done via a callback: it sends a request and doesn't block.
4109 // when we get the response similarly to notification, trigger a call to the completion popup
4110 //
4111 // language_server.completion(params, |cx: &mut Context, _meta, response| {
4112 // // called at response time
4113 // // compositor, lookup completion layer
4114 // // downcast dyn Component to Completion component
4115 // // emit response to completion (completion.complete/handle(response))
4116 // })
4117 //
4118 // typing after prompt opens: usually start offset is tracked and everything between
4119 // start_offset..cursor is replaced. For our purposes we could keep the start state (doc,
4120 // selection) and revert to them before applying. This needs to properly reset changes/history
4121 // though...
4122 //
4123 // company-mode does this by matching the prefix of the completion and removing it.
4124
4125 // ignore isIncomplete for now
4126 // keep state while typing
4127 // the behavior should be, filter the menu based on input
4128 // if items returns empty at any point, remove the popup
4129 // if backspace past initial offset point, remove the popup
4130 //
4131 // debounce requests!
4132 //
4133 // need an idle timeout thing.
4134 // https://github.com/company-mode/company-mode/blob/master/company.el#L620-L622
4135 //
4136 // "The idle delay in seconds until completion starts automatically.
4137 // The prefix still has to satisfy `company-minimum-prefix-length' before that
4138 // happens. The value of nil means no idle completion."
4139
4140 let (view, doc) = current!(cx.editor);
4141
4142 let language_server = match doc.language_server() {
4143 Some(language_server) => language_server,
4144 None => return,
4145 };
4146
4147 let offset_encoding = language_server.offset_encoding();
4148 let text = doc.text().slice(..);
4149 let cursor = doc.selection(view.id).primary().cursor(text);
4150
4151 let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
4152
4153 let future = language_server.completion(doc.identifier(), pos, None);
4154
4155 let trigger_offset = cursor;
4156
4157 // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
4158 // completion filtering. For example logger.te| should filter the initial suggestion list with "te".
4159
4160 use helix_core::chars;
4161 let mut iter = text.chars_at(cursor);
4162 iter.reverse();
4163 let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4164 let start_offset = cursor.saturating_sub(offset);
4165 let prefix = text.slice(start_offset..cursor).to_string();
4166
4167 cx.callback(
4168 future,
4169 move |editor: &mut Editor,
4170 compositor: &mut Compositor,
4171 response: Option<lsp::CompletionResponse>| {
4172 let (_, doc) = current!(editor);
4173 if doc.mode() != Mode::Insert {
4174 // we're not in insert mode anymore
4175 return;
4176 }
4177
4178 let mut items = match response {
4179 Some(lsp::CompletionResponse::Array(items)) => items,
4180 // TODO: do something with is_incomplete
4181 Some(lsp::CompletionResponse::List(lsp::CompletionList {
4182 is_incomplete: _is_incomplete,
4183 items,
4184 })) => items,
4185 None => Vec::new(),
4186 };
4187
4188 if !prefix.is_empty() {
4189 items = items
4190 .into_iter()
4191 .filter(|item| {
4192 item.filter_text
4193 .as_ref()
4194 .unwrap_or(&item.label)
4195 .starts_with(&prefix)
4196 })
4197 .collect();
4198 }
4199
4200 if items.is_empty() {
4201 // editor.set_error("No completion available".to_string());
4202 return;
4203 }
4204 let size = compositor.size();
4205 let ui = compositor
4206 .find(std::any::type_name::<ui::EditorView>())
4207 .unwrap();
4208 if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
4209 ui.set_completion(
4210 editor,
4211 items,
4212 offset_encoding,
4213 start_offset,
4214 trigger_offset,
4215 size,
4216 );
4217 };
4218 },
4219 );
4220 }
4221
hover(cx: &mut Context)4222 fn hover(cx: &mut Context) {
4223 let (view, doc) = current!(cx.editor);
4224
4225 let language_server = match doc.language_server() {
4226 Some(language_server) => language_server,
4227 None => return,
4228 };
4229
4230 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
4231
4232 let pos = pos_to_lsp_pos(
4233 doc.text(),
4234 doc.selection(view.id)
4235 .primary()
4236 .cursor(doc.text().slice(..)),
4237 language_server.offset_encoding(),
4238 );
4239
4240 let future = language_server.text_document_hover(doc.identifier(), pos, None);
4241
4242 cx.callback(
4243 future,
4244 move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
4245 if let Some(hover) = response {
4246 // hover.contents / .range <- used for visualizing
4247 let contents = match hover.contents {
4248 lsp::HoverContents::Scalar(contents) => {
4249 // markedstring(string/languagestring to be highlighted)
4250 // TODO
4251 log::error!("hover contents {:?}", contents);
4252 return;
4253 }
4254 lsp::HoverContents::Array(contents) => {
4255 log::error!("hover contents {:?}", contents);
4256 return;
4257 }
4258 // TODO: render markdown
4259 lsp::HoverContents::Markup(contents) => contents.value,
4260 };
4261
4262 // skip if contents empty
4263
4264 let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
4265 let popup = Popup::new(contents);
4266 compositor.push(Box::new(popup));
4267 }
4268 },
4269 );
4270 }
4271
4272 // comments
toggle_comments(cx: &mut Context)4273 fn toggle_comments(cx: &mut Context) {
4274 let (view, doc) = current!(cx.editor);
4275 let token = doc
4276 .language_config()
4277 .and_then(|lc| lc.comment_token.as_ref())
4278 .map(|tc| tc.as_ref());
4279 let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
4280
4281 doc.apply(&transaction, view.id);
4282 doc.append_changes_to_history(view.id);
4283 exit_select_mode(cx);
4284 }
4285
rotate_selections(cx: &mut Context, direction: Direction)4286 fn rotate_selections(cx: &mut Context, direction: Direction) {
4287 let count = cx.count();
4288 let (view, doc) = current!(cx.editor);
4289 let mut selection = doc.selection(view.id).clone();
4290 let index = selection.primary_index();
4291 let len = selection.len();
4292 selection.set_primary_index(match direction {
4293 Direction::Forward => (index + count) % len,
4294 Direction::Backward => (index + (len.saturating_sub(count) % len)) % len,
4295 });
4296 doc.set_selection(view.id, selection);
4297 }
rotate_selections_forward(cx: &mut Context)4298 fn rotate_selections_forward(cx: &mut Context) {
4299 rotate_selections(cx, Direction::Forward)
4300 }
rotate_selections_backward(cx: &mut Context)4301 fn rotate_selections_backward(cx: &mut Context) {
4302 rotate_selections(cx, Direction::Backward)
4303 }
4304
rotate_selection_contents(cx: &mut Context, direction: Direction)4305 fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
4306 let count = cx.count;
4307 let (view, doc) = current!(cx.editor);
4308 let text = doc.text().slice(..);
4309
4310 let selection = doc.selection(view.id);
4311 let mut fragments: Vec<_> = selection
4312 .fragments(text)
4313 .map(|fragment| Tendril::from_slice(&fragment))
4314 .collect();
4315
4316 let group = count
4317 .map(|count| count.get())
4318 .unwrap_or(fragments.len()) // default to rotating everything as one group
4319 .min(fragments.len());
4320
4321 for chunk in fragments.chunks_mut(group) {
4322 // TODO: also modify main index
4323 match direction {
4324 Direction::Forward => chunk.rotate_right(1),
4325 Direction::Backward => chunk.rotate_left(1),
4326 };
4327 }
4328
4329 let transaction = Transaction::change(
4330 doc.text(),
4331 selection
4332 .ranges()
4333 .iter()
4334 .zip(fragments)
4335 .map(|(range, fragment)| (range.from(), range.to(), Some(fragment))),
4336 );
4337
4338 doc.apply(&transaction, view.id);
4339 doc.append_changes_to_history(view.id);
4340 }
rotate_selection_contents_forward(cx: &mut Context)4341 fn rotate_selection_contents_forward(cx: &mut Context) {
4342 rotate_selection_contents(cx, Direction::Forward)
4343 }
rotate_selection_contents_backward(cx: &mut Context)4344 fn rotate_selection_contents_backward(cx: &mut Context) {
4345 rotate_selection_contents(cx, Direction::Backward)
4346 }
4347
4348 // tree sitter node selection
4349
expand_selection(cx: &mut Context)4350 fn expand_selection(cx: &mut Context) {
4351 let (view, doc) = current!(cx.editor);
4352
4353 if let Some(syntax) = doc.syntax() {
4354 let text = doc.text().slice(..);
4355 let selection = object::expand_selection(syntax, text, doc.selection(view.id));
4356 doc.set_selection(view.id, selection);
4357 }
4358 }
4359
match_brackets(cx: &mut Context)4360 fn match_brackets(cx: &mut Context) {
4361 let (view, doc) = current!(cx.editor);
4362
4363 if let Some(syntax) = doc.syntax() {
4364 let pos = doc
4365 .selection(view.id)
4366 .primary()
4367 .cursor(doc.text().slice(..));
4368 if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
4369 let selection = Selection::point(pos);
4370 doc.set_selection(view.id, selection);
4371 };
4372 }
4373 }
4374
4375 //
4376
jump_forward(cx: &mut Context)4377 fn jump_forward(cx: &mut Context) {
4378 let count = cx.count();
4379 let (view, _doc) = current!(cx.editor);
4380
4381 if let Some((id, selection)) = view.jumps.forward(count) {
4382 view.doc = *id;
4383 let selection = selection.clone();
4384 let (view, doc) = current!(cx.editor); // refetch doc
4385 doc.set_selection(view.id, selection);
4386
4387 align_view(doc, view, Align::Center);
4388 };
4389 }
4390
jump_backward(cx: &mut Context)4391 fn jump_backward(cx: &mut Context) {
4392 let count = cx.count();
4393 let (view, doc) = current!(cx.editor);
4394
4395 if let Some((id, selection)) = view.jumps.backward(view.id, doc, count) {
4396 // manually set the alternate_file as we cannot use the Editor::switch function here.
4397 if view.doc != *id {
4398 view.last_accessed_doc = Some(view.doc)
4399 }
4400 view.doc = *id;
4401 let selection = selection.clone();
4402 let (view, doc) = current!(cx.editor); // refetch doc
4403 doc.set_selection(view.id, selection);
4404
4405 align_view(doc, view, Align::Center);
4406 };
4407 }
4408
rotate_view(cx: &mut Context)4409 fn rotate_view(cx: &mut Context) {
4410 cx.editor.focus_next()
4411 }
4412
jump_view_right(cx: &mut Context)4413 fn jump_view_right(cx: &mut Context) {
4414 cx.editor.focus_right()
4415 }
4416
jump_view_left(cx: &mut Context)4417 fn jump_view_left(cx: &mut Context) {
4418 cx.editor.focus_left()
4419 }
4420
jump_view_up(cx: &mut Context)4421 fn jump_view_up(cx: &mut Context) {
4422 cx.editor.focus_up()
4423 }
4424
jump_view_down(cx: &mut Context)4425 fn jump_view_down(cx: &mut Context) {
4426 cx.editor.focus_down()
4427 }
4428
4429 // split helper, clear it later
split(cx: &mut Context, action: Action)4430 fn split(cx: &mut Context, action: Action) {
4431 let (view, doc) = current!(cx.editor);
4432 let id = doc.id();
4433 let selection = doc.selection(view.id).clone();
4434 let offset = view.offset;
4435
4436 cx.editor.switch(id, action);
4437
4438 // match the selection in the previous view
4439 let (view, doc) = current!(cx.editor);
4440 view.offset = offset;
4441 doc.set_selection(view.id, selection);
4442 }
4443
hsplit(cx: &mut Context)4444 fn hsplit(cx: &mut Context) {
4445 split(cx, Action::HorizontalSplit);
4446 }
4447
vsplit(cx: &mut Context)4448 fn vsplit(cx: &mut Context) {
4449 split(cx, Action::VerticalSplit);
4450 }
4451
wclose(cx: &mut Context)4452 fn wclose(cx: &mut Context) {
4453 if cx.editor.tree.views().count() == 1 {
4454 if let Err(err) = cmd::buffers_remaining_impl(cx.editor) {
4455 cx.editor.set_error(err.to_string());
4456 return;
4457 }
4458 }
4459 let view_id = view!(cx.editor).id;
4460 // close current split
4461 cx.editor.close(view_id, /* close_buffer */ false);
4462 }
4463
select_register(cx: &mut Context)4464 fn select_register(cx: &mut Context) {
4465 cx.on_next_key(move |cx, event| {
4466 if let Some(ch) = event.char() {
4467 cx.editor.selected_register = Some(ch);
4468 }
4469 })
4470 }
4471
align_view_top(cx: &mut Context)4472 fn align_view_top(cx: &mut Context) {
4473 let (view, doc) = current!(cx.editor);
4474 align_view(doc, view, Align::Top);
4475 }
4476
align_view_center(cx: &mut Context)4477 fn align_view_center(cx: &mut Context) {
4478 let (view, doc) = current!(cx.editor);
4479 align_view(doc, view, Align::Center);
4480 }
4481
align_view_bottom(cx: &mut Context)4482 fn align_view_bottom(cx: &mut Context) {
4483 let (view, doc) = current!(cx.editor);
4484 align_view(doc, view, Align::Bottom);
4485 }
4486
align_view_middle(cx: &mut Context)4487 fn align_view_middle(cx: &mut Context) {
4488 let (view, doc) = current!(cx.editor);
4489 let text = doc.text().slice(..);
4490 let pos = doc.selection(view.id).primary().cursor(text);
4491 let pos = coords_at_pos(text, pos);
4492
4493 view.offset.col = pos
4494 .col
4495 .saturating_sub((view.inner_area().width as usize) / 2);
4496 }
4497
scroll_up(cx: &mut Context)4498 fn scroll_up(cx: &mut Context) {
4499 scroll(cx, cx.count(), Direction::Backward);
4500 }
4501
scroll_down(cx: &mut Context)4502 fn scroll_down(cx: &mut Context) {
4503 scroll(cx, cx.count(), Direction::Forward);
4504 }
4505
select_textobject_around(cx: &mut Context)4506 fn select_textobject_around(cx: &mut Context) {
4507 select_textobject(cx, textobject::TextObject::Around);
4508 }
4509
select_textobject_inner(cx: &mut Context)4510 fn select_textobject_inner(cx: &mut Context) {
4511 select_textobject(cx, textobject::TextObject::Inside);
4512 }
4513
select_textobject(cx: &mut Context, objtype: textobject::TextObject)4514 fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
4515 let count = cx.count();
4516 cx.on_next_key(move |cx, event| {
4517 if let Some(ch) = event.char() {
4518 let textobject = move |editor: &mut Editor| {
4519 let (view, doc) = current!(editor);
4520 let text = doc.text().slice(..);
4521
4522 let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
4523 let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
4524 Some(t) => t,
4525 None => return range,
4526 };
4527 textobject::textobject_treesitter(
4528 text,
4529 range,
4530 objtype,
4531 obj_name,
4532 syntax.tree().root_node(),
4533 lang_config,
4534 count,
4535 )
4536 };
4537
4538 let selection = doc.selection(view.id).clone().transform(|range| {
4539 match ch {
4540 'w' => textobject::textobject_word(text, range, objtype, count),
4541 'c' => textobject_treesitter("class", range),
4542 'f' => textobject_treesitter("function", range),
4543 'p' => textobject_treesitter("parameter", range),
4544 // TODO: cancel new ranges if inconsistent surround matches across lines
4545 ch if !ch.is_ascii_alphanumeric() => {
4546 textobject::textobject_surround(text, range, objtype, ch, count)
4547 }
4548 _ => range,
4549 }
4550 });
4551 doc.set_selection(view.id, selection);
4552 };
4553 textobject(&mut cx.editor);
4554 cx.editor.last_motion = Some(Motion(Box::new(textobject)));
4555 }
4556 })
4557 }
4558
surround_add(cx: &mut Context)4559 fn surround_add(cx: &mut Context) {
4560 cx.on_next_key(move |cx, event| {
4561 if let Some(ch) = event.char() {
4562 let (view, doc) = current!(cx.editor);
4563 let selection = doc.selection(view.id);
4564 let (open, close) = surround::get_pair(ch);
4565
4566 let mut changes = Vec::new();
4567 for range in selection.iter() {
4568 changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
4569 changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
4570 }
4571
4572 let transaction = Transaction::change(doc.text(), changes.into_iter());
4573 doc.apply(&transaction, view.id);
4574 doc.append_changes_to_history(view.id);
4575 }
4576 })
4577 }
4578
surround_replace(cx: &mut Context)4579 fn surround_replace(cx: &mut Context) {
4580 let count = cx.count();
4581 cx.on_next_key(move |cx, event| {
4582 if let Some(from) = event.char() {
4583 cx.on_next_key(move |cx, event| {
4584 if let Some(to) = event.char() {
4585 let (view, doc) = current!(cx.editor);
4586 let text = doc.text().slice(..);
4587 let selection = doc.selection(view.id);
4588
4589 let change_pos = match surround::get_surround_pos(text, selection, from, count)
4590 {
4591 Some(c) => c,
4592 None => return,
4593 };
4594
4595 let (open, close) = surround::get_pair(to);
4596 let transaction = Transaction::change(
4597 doc.text(),
4598 change_pos.iter().enumerate().map(|(i, &pos)| {
4599 (
4600 pos,
4601 pos + 1,
4602 Some(Tendril::from_char(if i % 2 == 0 { open } else { close })),
4603 )
4604 }),
4605 );
4606 doc.apply(&transaction, view.id);
4607 doc.append_changes_to_history(view.id);
4608 }
4609 });
4610 }
4611 })
4612 }
4613
surround_delete(cx: &mut Context)4614 fn surround_delete(cx: &mut Context) {
4615 let count = cx.count();
4616 cx.on_next_key(move |cx, event| {
4617 if let Some(ch) = event.char() {
4618 let (view, doc) = current!(cx.editor);
4619 let text = doc.text().slice(..);
4620 let selection = doc.selection(view.id);
4621
4622 let change_pos = match surround::get_surround_pos(text, selection, ch, count) {
4623 Some(c) => c,
4624 None => return,
4625 };
4626
4627 let transaction =
4628 Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
4629 doc.apply(&transaction, view.id);
4630 doc.append_changes_to_history(view.id);
4631 }
4632 })
4633 }
4634
4635 #[derive(Eq, PartialEq)]
4636 enum ShellBehavior {
4637 Replace,
4638 Ignore,
4639 Insert,
4640 Append,
4641 }
4642
shell_pipe(cx: &mut Context)4643 fn shell_pipe(cx: &mut Context) {
4644 shell(cx, "pipe:".into(), ShellBehavior::Replace);
4645 }
4646
shell_pipe_to(cx: &mut Context)4647 fn shell_pipe_to(cx: &mut Context) {
4648 shell(cx, "pipe-to:".into(), ShellBehavior::Ignore);
4649 }
4650
shell_insert_output(cx: &mut Context)4651 fn shell_insert_output(cx: &mut Context) {
4652 shell(cx, "insert-output:".into(), ShellBehavior::Insert);
4653 }
4654
shell_append_output(cx: &mut Context)4655 fn shell_append_output(cx: &mut Context) {
4656 shell(cx, "append-output:".into(), ShellBehavior::Append);
4657 }
4658
shell_keep_pipe(cx: &mut Context)4659 fn shell_keep_pipe(cx: &mut Context) {
4660 let prompt = Prompt::new(
4661 "keep-pipe:".into(),
4662 Some('|'),
4663 |_input: &str| Vec::new(),
4664 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4665 let shell = &cx.editor.config.shell;
4666 if event != PromptEvent::Validate {
4667 return;
4668 }
4669 if input.is_empty() {
4670 return;
4671 }
4672 let (view, doc) = current!(cx.editor);
4673 let selection = doc.selection(view.id);
4674
4675 let mut ranges = SmallVec::with_capacity(selection.len());
4676 let old_index = selection.primary_index();
4677 let mut index: Option<usize> = None;
4678 let text = doc.text().slice(..);
4679
4680 for (i, range) in selection.ranges().iter().enumerate() {
4681 let fragment = range.fragment(text);
4682 let (_output, success) = match shell_impl(shell, input, Some(fragment.as_bytes())) {
4683 Ok(result) => result,
4684 Err(err) => {
4685 cx.editor.set_error(err.to_string());
4686 return;
4687 }
4688 };
4689
4690 // if the process exits successfully, keep the selection
4691 if success {
4692 ranges.push(*range);
4693 if i >= old_index && index.is_none() {
4694 index = Some(ranges.len() - 1);
4695 }
4696 }
4697 }
4698
4699 if ranges.is_empty() {
4700 cx.editor.set_error("No selections remaining".to_string());
4701 return;
4702 }
4703
4704 let index = index.unwrap_or_else(|| ranges.len() - 1);
4705 doc.set_selection(view.id, Selection::new(ranges, index));
4706 },
4707 );
4708
4709 cx.push_layer(Box::new(prompt));
4710 }
4711
shell_impl( shell: &[String], cmd: &str, input: Option<&[u8]>, ) -> anyhow::Result<(Tendril, bool)>4712 fn shell_impl(
4713 shell: &[String],
4714 cmd: &str,
4715 input: Option<&[u8]>,
4716 ) -> anyhow::Result<(Tendril, bool)> {
4717 use std::io::Write;
4718 use std::process::{Command, Stdio};
4719 if shell.is_empty() {
4720 bail!("No shell set");
4721 }
4722
4723 let mut process = match Command::new(&shell[0])
4724 .args(&shell[1..])
4725 .arg(cmd)
4726 .stdin(Stdio::piped())
4727 .stdout(Stdio::piped())
4728 .stderr(Stdio::piped())
4729 .spawn()
4730 {
4731 Ok(process) => process,
4732 Err(e) => {
4733 log::error!("Failed to start shell: {}", e);
4734 return Err(e.into());
4735 }
4736 };
4737 if let Some(input) = input {
4738 let mut stdin = process.stdin.take().unwrap();
4739 stdin.write_all(input)?;
4740 }
4741 let output = process.wait_with_output()?;
4742
4743 if !output.stderr.is_empty() {
4744 log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
4745 }
4746
4747 let tendril = Tendril::try_from_byte_slice(&output.stdout)
4748 .map_err(|_| anyhow!("Process did not output valid UTF-8"))?;
4749 Ok((tendril, output.status.success()))
4750 }
4751
shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior)4752 fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4753 let pipe = match behavior {
4754 ShellBehavior::Replace | ShellBehavior::Ignore => true,
4755 ShellBehavior::Insert | ShellBehavior::Append => false,
4756 };
4757 let prompt = Prompt::new(
4758 prompt,
4759 Some('|'),
4760 |_input: &str| Vec::new(),
4761 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4762 let shell = &cx.editor.config.shell;
4763 if event != PromptEvent::Validate {
4764 return;
4765 }
4766 if input.is_empty() {
4767 return;
4768 }
4769 let (view, doc) = current!(cx.editor);
4770 let selection = doc.selection(view.id);
4771
4772 let mut changes = Vec::with_capacity(selection.len());
4773 let text = doc.text().slice(..);
4774
4775 for range in selection.ranges() {
4776 let fragment = range.fragment(text);
4777 let (output, success) =
4778 match shell_impl(shell, input, pipe.then(|| fragment.as_bytes())) {
4779 Ok(result) => result,
4780 Err(err) => {
4781 cx.editor.set_error(err.to_string());
4782 return;
4783 }
4784 };
4785
4786 if !success {
4787 cx.editor.set_error("Command failed".to_string());
4788 return;
4789 }
4790
4791 let (from, to) = match behavior {
4792 ShellBehavior::Replace => (range.from(), range.to()),
4793 ShellBehavior::Insert => (range.from(), range.from()),
4794 ShellBehavior::Append => (range.to(), range.to()),
4795 _ => (range.from(), range.from()),
4796 };
4797 changes.push((from, to, Some(output)));
4798 }
4799
4800 if behavior != ShellBehavior::Ignore {
4801 let transaction = Transaction::change(doc.text(), changes.into_iter());
4802 doc.apply(&transaction, view.id);
4803 doc.append_changes_to_history(view.id);
4804 }
4805 },
4806 );
4807
4808 cx.push_layer(Box::new(prompt));
4809 }
4810
suspend(_cx: &mut Context)4811 fn suspend(_cx: &mut Context) {
4812 #[cfg(not(windows))]
4813 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
4814 }
4815
add_newline_above(cx: &mut Context)4816 fn add_newline_above(cx: &mut Context) {
4817 add_newline_impl(cx, Open::Above);
4818 }
4819
add_newline_below(cx: &mut Context)4820 fn add_newline_below(cx: &mut Context) {
4821 add_newline_impl(cx, Open::Below)
4822 }
4823
add_newline_impl(cx: &mut Context, open: Open)4824 fn add_newline_impl(cx: &mut Context, open: Open) {
4825 let count = cx.count();
4826 let (view, doc) = current!(cx.editor);
4827 let selection = doc.selection(view.id);
4828 let text = doc.text();
4829 let slice = text.slice(..);
4830
4831 let changes = selection.into_iter().map(|range| {
4832 let (start, end) = range.line_range(slice);
4833 let line = match open {
4834 Open::Above => start,
4835 Open::Below => end + 1,
4836 };
4837 let pos = text.line_to_char(line);
4838 (
4839 pos,
4840 pos,
4841 Some(doc.line_ending.as_str().repeat(count).into()),
4842 )
4843 });
4844
4845 let transaction = Transaction::change(text, changes);
4846 doc.apply(&transaction, view.id);
4847 doc.append_changes_to_history(view.id);
4848 }
4849