on 6c11708 help sign in
helix/helix-term/src/ui/editor.rs
1184 lines 43 kB history raw
1use crate::{
2 commands,
3 compositor::{Component, Context, EventResult},
4 key,
5 keymap::{KeymapResult, KeymapResultKind, Keymaps},
6 ui::{Completion, ProgressSpinners},
7};
9use helix_core::{
10 coords_at_pos, encoding,
11 graphemes::{
12 ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
13 },
14 movement::Direction,
15 syntax::{self, HighlightEvent},
16 unicode::segmentation::UnicodeSegmentation,
17 unicode::width::UnicodeWidthStr,
18 LineEnding, Position, Range, Selection,
19};
20use helix_view::{
21 document::{Mode, SCRATCH_BUFFER_NAME},
22 editor::CursorShapeConfig,
23 graphics::{CursorKind, Modifier, Rect, Style},
24 info::Info,
25 input::KeyEvent,
26 keyboard::{KeyCode, KeyModifiers},
27 Document, Editor, Theme, View,
28};
29use std::borrow::Cow;
31use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
32use tui::buffer::Buffer as Surface;
34pub struct EditorView {
35 keymaps: Keymaps,
36 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
37 last_insert: (commands::MappableCommand, Vec<KeyEvent>),
38 pub(crate) completion: Option<Completion>,
39 spinners: ProgressSpinners,
40 autoinfo: Option<Info>,
43impl Default for EditorView {
44 fn default() -> Self {
45 Self::new(Keymaps::default())
46 }
49impl EditorView {
50 pub fn new(keymaps: Keymaps) -> Self {
51 Self {
52 keymaps,
53 on_next_key: None,
54 last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
55 completion: None,
56 spinners: ProgressSpinners::default(),
57 autoinfo: None,
58 }
59 }
61 pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
62 &mut self.spinners
63 }
65 #[allow(clippy::too_many_arguments)]
66 pub fn render_view(
67 &self,
68 doc: &Document,
69 view: &View,
70 viewport: Rect,
71 surface: &mut Surface,
72 theme: &Theme,
73 is_focused: bool,
74 config: &helix_view::editor::Config,
75 ) {
76 let inner = view.inner_area();
77 let area = view.area;
79 let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
80 let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
81 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
82 Box::new(syntax::merge(
83 highlights,
84 Self::doc_selection_highlights(doc, view, theme, &config.cursor_shape),
85 ))
86 } else {
87 Box::new(highlights)
88 };
90 Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
91 Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config);
93 if is_focused {
94 Self::render_focused_view_elements(view, doc, inner, theme, surface);
95 }
97 // if we're not at the edge of the screen, draw a right border
98 if viewport.right() != view.area.right() {
99 let x = area.right();
100 let border_style = theme.get("ui.window");
101 for y in area.top()..area.bottom() {
102 surface[(x, y)]
103 .set_symbol(tui::symbols::line::VERTICAL)
104 //.set_symbol(" ")
105 .set_style(border_style);
106 }
107 }
109 self.render_diagnostics(doc, view, inner, surface, theme);
111 let statusline_area = view
112 .area
113 .clip_top(view.area.height.saturating_sub(1))
114 .clip_bottom(1); // -1 from bottom to remove commandline
115 self.render_statusline(doc, view, statusline_area, surface, theme, is_focused);
116 }
118 /// Get syntax highlights for a document in a view represented by the first line
119 /// and column (`offset`) and the last line. This is done instead of using a view
120 /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
121 pub fn doc_syntax_highlights<'doc>(
122 doc: &'doc Document,
123 offset: Position,
124 height: u16,
125 _theme: &Theme,
126 ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
127 let text = doc.text().slice(..);
128 let last_line = std::cmp::min(
129 // Saturating subs to make it inclusive zero indexing.
130 (offset.row + height as usize).saturating_sub(1),
131 doc.text().len_lines().saturating_sub(1),
132 );
134 let range = {
135 // calculate viewport byte ranges
136 let start = text.line_to_byte(offset.row);
137 let end = text.line_to_byte(last_line + 1);
139 start..end
140 };
142 match doc.syntax() {
143 Some(syntax) => {
144 let iter = syntax
145 // TODO: range doesn't actually restrict source, just highlight range
146 .highlight_iter(text.slice(..), Some(range), None)
147 .map(|event| event.unwrap())
148 .map(move |event| match event {
149 // convert byte offsets to char offset
150 HighlightEvent::Source { start, end } => {
151 let start =
152 text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
153 let end =
154 text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
155 HighlightEvent::Source { start, end }
156 }
157 event => event,
158 });
160 Box::new(iter)
161 }
162 None => Box::new(
163 [HighlightEvent::Source {
164 start: text.byte_to_char(range.start),
165 end: text.byte_to_char(range.end),
166 }]
167 .into_iter(),
168 ),
169 }
170 }
172 /// Get highlight spans for document diagnostics
173 pub fn doc_diagnostics_highlights(
174 doc: &Document,
175 theme: &Theme,
176 ) -> Vec<(usize, std::ops::Range<usize>)> {
177 let diagnostic_scope = theme
178 .find_scope_index("diagnostic")
179 .or_else(|| theme.find_scope_index("ui.cursor"))
180 .or_else(|| theme.find_scope_index("ui.selection"))
181 .expect(
182 "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
183 );
185 doc.diagnostics()
186 .iter()
187 .map(|diagnostic| {
188 (
189 diagnostic_scope,
190 diagnostic.range.start..diagnostic.range.end,
191 )
192 })
193 .collect()
194 }
196 /// Get highlight spans for selections in a document view.
197 pub fn doc_selection_highlights(
198 doc: &Document,
199 view: &View,
200 theme: &Theme,
201 cursor_shape_config: &CursorShapeConfig,
202 ) -> Vec<(usize, std::ops::Range<usize>)> {
203 let text = doc.text().slice(..);
204 let selection = doc.selection(view.id);
205 let primary_idx = selection.primary_index();
207 let mode = doc.mode();
208 let cursorkind = cursor_shape_config.from_mode(mode);
209 let cursor_is_block = cursorkind == CursorKind::Block;
211 let selection_scope = theme
212 .find_scope_index("ui.selection")
213 .expect("could not find `ui.selection` scope in the theme!");
214 let base_cursor_scope = theme
215 .find_scope_index("ui.cursor")
216 .unwrap_or(selection_scope);
218 let cursor_scope = match mode {
219 Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
220 Mode::Select => theme.find_scope_index("ui.cursor.select"),
221 Mode::Normal => Some(base_cursor_scope),
222 }
223 .unwrap_or(base_cursor_scope);
225 let primary_cursor_scope = theme
226 .find_scope_index("ui.cursor.primary")
227 .unwrap_or(cursor_scope);
228 let primary_selection_scope = theme
229 .find_scope_index("ui.selection.primary")
230 .unwrap_or(selection_scope);
232 let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
233 for (i, range) in selection.iter().enumerate() {
234 let selection_is_primary = i == primary_idx;
235 let (cursor_scope, selection_scope) = if selection_is_primary {
236 (primary_cursor_scope, primary_selection_scope)
237 } else {
238 (cursor_scope, selection_scope)
239 };
241 // Special-case: cursor at end of the rope.
242 if range.head == range.anchor && range.head == text.len_chars() {
243 if !selection_is_primary || cursor_is_block {
244 // Bar and underline cursors are drawn by the terminal
245 // BUG: If the editor area loses focus while having a bar or
246 // underline cursor (eg. when a regex prompt has focus) then
247 // the primary cursor will be invisible. This doesn't happen
248 // with block cursors since we manually draw *all* cursors.
249 spans.push((cursor_scope, range.head..range.head + 1));
250 }
251 continue;
252 }
254 let range = range.min_width_1(text);
255 if range.head > range.anchor {
256 // Standard case.
257 let cursor_start = prev_grapheme_boundary(text, range.head);
258 spans.push((selection_scope, range.anchor..cursor_start));
259 if !selection_is_primary || cursor_is_block {
260 spans.push((cursor_scope, cursor_start..range.head));
261 }
262 } else {
263 // Reverse case.
264 let cursor_end = next_grapheme_boundary(text, range.head);
265 if !selection_is_primary || cursor_is_block {
266 spans.push((cursor_scope, range.head..cursor_end));
267 }
268 spans.push((selection_scope, cursor_end..range.anchor));
269 }
270 }
272 spans
273 }
275 pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
276 doc: &Document,
277 offset: Position,
278 viewport: Rect,
279 surface: &mut Surface,
280 theme: &Theme,
281 highlights: H,
282 ) {
283 let text = doc.text().slice(..);
285 let mut spans = Vec::new();
286 let mut visual_x = 0u16;
287 let mut line = 0u16;
288 let tab_width = doc.tab_width();
289 let tab = " ".repeat(tab_width);
291 let text_style = theme.get("ui.text");
293 // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
294 // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
295 let text = text.slice(..);
297 'outer: for event in highlights {
298 match event {
299 HighlightEvent::HighlightStart(span) => {
300 spans.push(span);
301 }
302 HighlightEvent::HighlightEnd => {
303 spans.pop();
304 }
305 HighlightEvent::Source { start, end } => {
306 // `unwrap_or_else` part is for off-the-end indices of
307 // the rope, to allow cursor highlighting at the end
308 // of the rope.
309 let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
311 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
313 for grapheme in RopeGraphemes::new(text) {
314 let out_of_bounds = visual_x < offset.col as u16
315 || visual_x >= viewport.width + offset.col as u16;
317 if LineEnding::from_rope_slice(&grapheme).is_some() {
318 if !out_of_bounds {
319 let style = spans.iter().fold(text_style, |acc, span| {
320 acc.patch(theme.highlight(span.0))
321 });
323 // we still want to render an empty cell with the style
324 surface.set_string(
325 viewport.x + visual_x - offset.col as u16,
326 viewport.y + line,
327 " ",
328 style,
329 );
330 }
332 visual_x = 0;
333 line += 1;
335 // TODO: with proper iter this shouldn't be necessary
336 if line >= viewport.height {
337 break 'outer;
338 }
339 } else {
340 let grapheme = Cow::from(grapheme);
342 let (grapheme, width) = if grapheme == "\t" {
343 // make sure we display tab as appropriate amount of spaces
344 (tab.as_str(), tab_width)
345 } else {
346 // Cow will prevent allocations if span contained in a single slice
347 // which should really be the majority case
348 let width = grapheme_width(&grapheme);
349 (grapheme.as_ref(), width)
350 };
352 if !out_of_bounds {
353 let style = spans.iter().fold(text_style, |acc, span| {
354 acc.patch(theme.highlight(span.0))
355 });
357 // if we're offscreen just keep going until we hit a new line
358 surface.set_string(
359 viewport.x + visual_x - offset.col as u16,
360 viewport.y + line,
361 grapheme,
362 style,
363 );
364 }
366 visual_x = visual_x.saturating_add(width as u16);
367 }
368 }
369 }
370 }
371 }
372 }
374 /// Render brace match, etc (meant for the focused view only)
375 pub fn render_focused_view_elements(
376 view: &View,
377 doc: &Document,
378 viewport: Rect,
379 theme: &Theme,
380 surface: &mut Surface,
381 ) {
382 // Highlight matching braces
383 if let Some(syntax) = doc.syntax() {
384 let text = doc.text().slice(..);
385 use helix_core::match_brackets;
386 let pos = doc.selection(view.id).primary().cursor(text);
388 let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
389 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
391 if let Some(pos) = pos {
392 // ensure col is on screen
393 if (pos.col as u16) < viewport.width + view.offset.col as u16
394 && pos.col >= view.offset.col
395 {
396 let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
397 Style::default()
398 .add_modifier(Modifier::REVERSED)
399 .add_modifier(Modifier::DIM)
400 });
402 surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
403 .set_style(style);
404 }
405 }
406 }
407 }
409 pub fn render_gutter(
410 doc: &Document,
411 view: &View,
412 viewport: Rect,
413 surface: &mut Surface,
414 theme: &Theme,
415 is_focused: bool,
416 config: &helix_view::editor::Config,
417 ) {
418 let text = doc.text().slice(..);
419 let last_line = view.last_line(doc);
421 // it's used inside an iterator so the collect isn't needless:
422 // https://github.com/rust-lang/rust-clippy/issues/6164
423 #[allow(clippy::needless_collect)]
424 let cursors: Vec<_> = doc
425 .selection(view.id)
426 .iter()
427 .map(|range| range.cursor_line(text))
428 .collect();
430 let mut offset = 0;
432 let gutter_style = theme.get("ui.gutter");
434 // avoid lots of small allocations by reusing a text buffer for each line
435 let mut text = String::with_capacity(8);
437 for (constructor, width) in view.gutters() {
438 let gutter = constructor(doc, view, theme, config, is_focused, *width);
439 text.reserve(*width); // ensure there's enough space for the gutter
440 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
441 let selected = cursors.contains(&line);
443 if let Some(style) = gutter(line, selected, &mut text) {
444 surface.set_stringn(
445 viewport.x + offset,
446 viewport.y + i as u16,
447 &text,
448 *width,
449 gutter_style.patch(style),
450 );
451 }
452 text.clear();
453 }
454 offset += *width as u16;
455 }
456 }
458 pub fn render_diagnostics(
459 &self,
460 doc: &Document,
461 view: &View,
462 viewport: Rect,
463 surface: &mut Surface,
464 theme: &Theme,
465 ) {
466 use helix_core::diagnostic::Severity;
467 use tui::{
468 layout::Alignment,
469 text::Text,
470 widgets::{Paragraph, Widget, Wrap},
471 };
473 let cursor = doc
474 .selection(view.id)
475 .primary()
476 .cursor(doc.text().slice(..));
478 let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
479 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
480 });
482 let warning = theme.get("warning");
483 let error = theme.get("error");
484 let info = theme.get("info");
485 let hint = theme.get("hint");
487 // Vec::with_capacity(diagnostics.len()); // rough estimate
488 let mut lines = Vec::new();
489 for diagnostic in diagnostics {
490 let text = Text::styled(
491 &diagnostic.message,
492 match diagnostic.severity {
493 Some(Severity::Error) => error,
494 Some(Severity::Warning) | None => warning,
495 Some(Severity::Info) => info,
496 Some(Severity::Hint) => hint,
497 },
498 );
499 lines.extend(text.lines);
500 }
502 let paragraph = Paragraph::new(lines)
503 .alignment(Alignment::Right)
504 .wrap(Wrap { trim: true });
505 let width = 100.min(viewport.width);
506 let height = 15.min(viewport.height);
507 paragraph.render(
508 Rect::new(viewport.right() - width, viewport.y + 1, width, height),
509 surface,
510 );
511 }
513 pub fn render_statusline(
514 &self,
515 doc: &Document,
516 view: &View,
517 viewport: Rect,
518 surface: &mut Surface,
519 theme: &Theme,
520 is_focused: bool,
521 ) {
522 use tui::text::{Span, Spans};
524 //-------------------------------
525 // Left side of the status line.
526 //-------------------------------
528 let mode = match doc.mode() {
529 Mode::Insert => "INS",
530 Mode::Select => "SEL",
531 Mode::Normal => "NOR",
532 };
533 let progress = doc
534 .language_server()
535 .and_then(|srv| {
536 self.spinners
537 .get(srv.id())
538 .and_then(|spinner| spinner.frame())
539 })
540 .unwrap_or("");
542 let base_style = if is_focused {
543 theme.get("ui.statusline")
544 } else {
545 theme.get("ui.statusline.inactive")
546 };
547 // statusline
548 surface.set_style(viewport.with_height(1), base_style);
549 if is_focused {
550 surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
551 }
552 surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
554 //-------------------------------
555 // Right side of the status line.
556 //-------------------------------
558 let mut right_side_text = Spans::default();
560 // Compute the individual info strings and add them to `right_side_text`.
562 // Diagnostics
563 let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
564 use helix_core::diagnostic::Severity;
565 match diag.severity {
566 Some(Severity::Warning) => counts.0 += 1,
567 Some(Severity::Error) | None => counts.1 += 1,
568 _ => {}
569 }
570 counts
571 });
572 let (warnings, errors) = diags;
573 let warning_style = theme.get("warning");
574 let error_style = theme.get("error");
575 for i in 0..2 {
576 let (count, style) = match i {
577 0 => (warnings, warning_style),
578 1 => (errors, error_style),
579 _ => unreachable!(),
580 };
581 if count == 0 {
582 continue;
583 }
584 let style = base_style.patch(style);
585 right_side_text.0.push(Span::styled("●", style));
586 right_side_text
587 .0
588 .push(Span::styled(format!(" {} ", count), base_style));
589 }
591 // Selections
592 let sels_count = doc.selection(view.id).len();
593 right_side_text.0.push(Span::styled(
594 format!(
595 " {} sel{} ",
596 sels_count,
597 if sels_count == 1 { "" } else { "s" }
598 ),
599 base_style,
600 ));
602 // let indent_info = match doc.indent_style {
603 // IndentStyle::Tabs => "tabs",
604 // IndentStyle::Spaces(1) => "spaces:1",
605 // IndentStyle::Spaces(2) => "spaces:2",
606 // IndentStyle::Spaces(3) => "spaces:3",
607 // IndentStyle::Spaces(4) => "spaces:4",
608 // IndentStyle::Spaces(5) => "spaces:5",
609 // IndentStyle::Spaces(6) => "spaces:6",
610 // IndentStyle::Spaces(7) => "spaces:7",
611 // IndentStyle::Spaces(8) => "spaces:8",
612 // _ => "indent:ERROR",
613 // };
615 // Position
616 let pos = coords_at_pos(
617 doc.text().slice(..),
618 doc.selection(view.id)
619 .primary()
620 .cursor(doc.text().slice(..)),
621 );
622 right_side_text.0.push(Span::styled(
623 format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
624 base_style,
625 ));
627 let enc = doc.encoding();
628 if enc != encoding::UTF_8 {
629 right_side_text
630 .0
631 .push(Span::styled(format!(" {} ", enc.name()), base_style));
632 }
634 // Render to the statusline.
635 surface.set_spans(
636 viewport.x
637 + viewport
638 .width
639 .saturating_sub(right_side_text.width() as u16),
640 viewport.y,
641 &right_side_text,
642 right_side_text.width() as u16,
643 );
645 //-------------------------------
646 // Middle / File path / Title
647 //-------------------------------
648 let title = {
649 let rel_path = doc.relative_path();
650 let path = rel_path
651 .as_ref()
652 .map(|p| p.to_string_lossy())
653 .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
654 format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
655 };
657 surface.set_string_truncated(
658 viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
659 viewport.y,
660 title,
661 viewport
662 .width
663 .saturating_sub(6)
664 .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
665 base_style,
666 true,
667 true,
668 );
669 }
671 /// Handle events by looking them up in `self.keymaps`. Returns None
672 /// if event was handled (a command was executed or a subkeymap was
673 /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned
674 /// otherwise.
675 fn handle_keymap_event(
676 &mut self,
677 mode: Mode,
678 cxt: &mut commands::Context,
679 event: KeyEvent,
680 ) -> Option<KeymapResult> {
681 self.autoinfo = None;
682 let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
683 self.autoinfo = key_result.sticky.map(|node| node.infobox());
685 match &key_result.kind {
686 KeymapResultKind::Matched(command) => command.execute(cxt),
687 KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
688 KeymapResultKind::MatchedSequence(commands) => {
689 for command in commands {
690 command.execute(cxt);
691 }
692 }
693 KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
694 }
695 None
696 }
698 fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
699 if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
700 match keyresult.kind {
701 KeymapResultKind::NotFound => {
702 if let Some(ch) = event.char() {
703 commands::insert::insert_char(cx, ch)
704 }
705 }
706 KeymapResultKind::Cancelled(pending) => {
707 for ev in pending {
708 match ev.char() {
709 Some(ch) => commands::insert::insert_char(cx, ch),
710 None => {
711 if let KeymapResultKind::Matched(command) =
712 self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
713 {
714 command.execute(cx);
715 }
716 }
717 }
718 }
719 }
720 _ => unreachable!(),
721 }
722 }
723 }
725 fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
726 match event {
727 // count handling
728 key!(i @ '0'..='9') => {
729 let i = i.to_digit(10).unwrap() as usize;
730 cxt.editor.count =
731 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
732 }
733 // special handling for repeat operator
734 key!('.') if self.keymaps.pending().is_empty() => {
735 // first execute whatever put us into insert mode
736 self.last_insert.0.execute(cxt);
737 // then replay the inputs
738 for &key in &self.last_insert.1.clone() {
739 self.insert_mode(cxt, key)
740 }
741 }
742 _ => {
743 // set the count
744 cxt.count = cxt.editor.count;
745 // TODO: edge case: 0j -> reset to 1
746 // if this fails, count was Some(0)
747 // debug_assert!(cxt.count != 0);
749 // set the register
750 cxt.register = cxt.editor.selected_register.take();
752 self.handle_keymap_event(mode, cxt, event);
753 if self.keymaps.pending().is_empty() {
754 cxt.editor.count = None
755 }
756 }
757 }
758 }
760 pub fn set_completion(
761 &mut self,
762 editor: &mut Editor,
763 items: Vec<helix_lsp::lsp::CompletionItem>,
764 offset_encoding: helix_lsp::OffsetEncoding,
765 start_offset: usize,
766 trigger_offset: usize,
767 size: Rect,
768 ) {
769 let mut completion =
770 Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
772 if completion.is_empty() {
773 // skip if we got no completion results
774 return;
775 }
777 // Immediately initialize a savepoint
778 doc_mut!(editor).savepoint();
780 // TODO : propagate required size on resize to completion too
781 completion.required_size((size.width, size.height));
782 self.completion = Some(completion);
783 }
785 pub fn clear_completion(&mut self, editor: &mut Editor) {
786 self.completion = None;
788 // Clear any savepoints
789 let doc = doc_mut!(editor);
790 doc.savepoint = None;
791 editor.clear_idle_timer(); // don't retrigger
792 }
795impl EditorView {
796 fn handle_mouse_event(
797 &mut self,
798 event: MouseEvent,
799 cxt: &mut commands::Context,
800 ) -> EventResult {
801 match event {
802 MouseEvent {
803 kind: MouseEventKind::Down(MouseButton::Left),
804 row,
805 column,
806 modifiers,
807 ..
808 } => {
809 let editor = &mut cxt.editor;
811 let result = editor.tree.views().find_map(|(view, _focus)| {
812 view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
813 .map(|pos| (pos, view.id))
814 });
816 if let Some((pos, view_id)) = result {
817 let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
819 if modifiers == crossterm::event::KeyModifiers::ALT {
820 let selection = doc.selection(view_id).clone();
821 doc.set_selection(view_id, selection.push(Range::point(pos)));
822 } else {
823 doc.set_selection(view_id, Selection::point(pos));
824 }
826 editor.tree.focus = view_id;
828 return EventResult::Consumed(None);
829 }
831 EventResult::Ignored
832 }
834 MouseEvent {
835 kind: MouseEventKind::Drag(MouseButton::Left),
836 row,
837 column,
838 ..
839 } => {
840 let (view, doc) = current!(cxt.editor);
842 let pos = match view.pos_at_screen_coords(doc, row, column) {
843 Some(pos) => pos,
844 None => return EventResult::Ignored,
845 };
847 let mut selection = doc.selection(view.id).clone();
848 let primary = selection.primary_mut();
849 *primary = Range::new(primary.anchor, pos);
850 doc.set_selection(view.id, selection);
851 EventResult::Consumed(None)
852 }
854 MouseEvent {
855 kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown,
856 row,
857 column,
858 ..
859 } => {
860 let current_view = cxt.editor.tree.focus;
862 let direction = match event.kind {
863 MouseEventKind::ScrollUp => Direction::Backward,
864 MouseEventKind::ScrollDown => Direction::Forward,
865 _ => unreachable!(),
866 };
868 let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
869 view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
870 .map(|_| view.id)
871 });
873 match result {
874 Some(view_id) => cxt.editor.tree.focus = view_id,
875 None => return EventResult::Ignored,
876 }
878 let offset = cxt.editor.config.scroll_lines.abs() as usize;
879 commands::scroll(cxt, offset, direction);
881 cxt.editor.tree.focus = current_view;
883 EventResult::Consumed(None)
884 }
886 MouseEvent {
887 kind: MouseEventKind::Up(MouseButton::Left),
888 ..
889 } => {
890 if !cxt.editor.config.middle_click_paste {
891 return EventResult::Ignored;
892 }
894 let (view, doc) = current!(cxt.editor);
895 let range = doc.selection(view.id).primary();
897 if range.to() - range.from() <= 1 {
898 return EventResult::Ignored;
899 }
901 commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
903 EventResult::Consumed(None)
904 }
906 MouseEvent {
907 kind: MouseEventKind::Up(MouseButton::Middle),
908 row,
909 column,
910 modifiers,
911 ..
912 } => {
913 let editor = &mut cxt.editor;
914 if !editor.config.middle_click_paste {
915 return EventResult::Ignored;
916 }
918 if modifiers == crossterm::event::KeyModifiers::ALT {
919 commands::MappableCommand::replace_selections_with_primary_clipboard
920 .execute(cxt);
922 return EventResult::Consumed(None);
923 }
925 let result = editor.tree.views().find_map(|(view, _focus)| {
926 view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
927 .map(|pos| (pos, view.id))
928 });
930 if let Some((pos, view_id)) = result {
931 let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
932 doc.set_selection(view_id, Selection::point(pos));
933 editor.tree.focus = view_id;
934 commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
935 return EventResult::Consumed(None);
936 }
938 EventResult::Ignored
939 }
941 _ => EventResult::Ignored,
942 }
943 }
946impl Component for EditorView {
947 fn handle_event(
948 &mut self,
949 event: Event,
950 context: &mut crate::compositor::Context,
951 ) -> EventResult {
952 let mut cx = commands::Context {
953 editor: context.editor,
954 count: None,
955 register: None,
956 callback: None,
957 on_next_key_callback: None,
958 jobs: context.jobs,
959 };
961 match event {
962 Event::Resize(_width, _height) => {
963 // Ignore this event, we handle resizing just before rendering to screen.
964 // Handling it here but not re-rendering will cause flashing
965 EventResult::Consumed(None)
966 }
967 Event::Key(key) => {
968 cx.editor.reset_idle_timer();
969 let mut key = KeyEvent::from(key);
970 canonicalize_key(&mut key);
972 // clear status
973 cx.editor.status_msg = None;
975 let doc = doc!(cx.editor);
976 let mode = doc.mode();
978 if let Some(on_next_key) = self.on_next_key.take() {
979 // if there's a command waiting input, do that first
980 on_next_key(&mut cx, key);
981 } else {
982 match mode {
983 Mode::Insert => {
984 // record last_insert key
985 self.last_insert.1.push(key);
987 // let completion swallow the event if necessary
988 let mut consumed = false;
989 if let Some(completion) = &mut self.completion {
990 // use a fake context here
991 let mut cx = Context {
992 editor: cx.editor,
993 jobs: cx.jobs,
994 scroll: None,
995 };
996 let res = completion.handle_event(event, &mut cx);
998 if let EventResult::Consumed(callback) = res {
999 consumed = true;
1001 if callback.is_some() {
1002 // assume close_fn
1003 self.clear_completion(cx.editor);
1008 // if completion didn't take the event, we pass it onto commands
1009 if !consumed {
1010 self.insert_mode(&mut cx, key);
1012 // lastly we recalculate completion
1013 if let Some(completion) = &mut self.completion {
1014 completion.update(&mut cx);
1015 if completion.is_empty() {
1016 self.clear_completion(cx.editor);
1021 mode => self.command_mode(mode, &mut cx, key),
1025 self.on_next_key = cx.on_next_key_callback.take();
1026 // appease borrowck
1027 let callback = cx.callback.take();
1029 // if the command consumed the last view, skip the render.
1030 // on the next loop cycle the Application will then terminate.
1031 if cx.editor.should_close() {
1032 return EventResult::Ignored;
1035 let (view, doc) = current!(cx.editor);
1036 view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
1038 // Store a history state if not in insert mode. This also takes care of
1039 // commiting changes when leaving insert mode.
1040 if doc.mode() != Mode::Insert {
1041 doc.append_changes_to_history(view.id);
1044 // mode transitions
1045 match (mode, doc.mode()) {
1046 (Mode::Normal, Mode::Insert) => {
1047 // HAXX: if we just entered insert mode from normal, clear key buf
1048 // and record the command that got us into this mode.
1050 // how we entered insert mode is important, and we should track that so
1051 // we can repeat the side effect.
1053 self.last_insert.0 =
1054 match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
1055 KeymapResultKind::Matched(command) => command,
1056 // FIXME: insert mode can only be entered through single KeyCodes
1057 _ => unimplemented!(),
1058 };
1059 self.last_insert.1.clear();
1061 (Mode::Insert, Mode::Normal) => {
1062 // if exiting insert mode, remove completion
1063 self.completion = None;
1065 _ => (),
1068 EventResult::Consumed(callback)
1071 Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
1075 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
1076 // clear with background color
1077 surface.set_style(area, cx.editor.theme.get("ui.background"));
1079 // if the terminal size suddenly changed, we need to trigger a resize
1080 cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
1082 for (view, is_focused) in cx.editor.tree.views() {
1083 let doc = cx.editor.document(view.doc).unwrap();
1084 self.render_view(
1085 doc,
1086 view,
1087 area,
1088 surface,
1089 &cx.editor.theme,
1090 is_focused,
1091 &cx.editor.config,
1092 );
1095 if cx.editor.config.auto_info {
1096 if let Some(ref mut info) = self.autoinfo {
1097 info.render(area, surface, cx);
1101 let key_width = 15u16; // for showing pending keys
1102 let mut status_msg_width = 0;
1104 // render status msg
1105 if let Some((status_msg, severity)) = &cx.editor.status_msg {
1106 status_msg_width = status_msg.width();
1107 use helix_view::editor::Severity;
1108 let style = if *severity == Severity::Error {
1109 cx.editor.theme.get("error")
1110 } else {
1111 cx.editor.theme.get("ui.text")
1112 };
1114 surface.set_string(
1115 area.x,
1116 area.y + area.height.saturating_sub(1),
1117 status_msg,
1118 style,
1119 );
1122 if area.width.saturating_sub(status_msg_width as u16) > key_width {
1123 let mut disp = String::new();
1124 if let Some(count) = cx.editor.count {
1125 disp.push_str(&count.to_string())
1127 for key in self.keymaps.pending() {
1128 let s = key.to_string();
1129 if s.graphemes(true).count() > 1 {
1130 disp.push_str(&format!("<{}>", s));
1131 } else {
1132 disp.push_str(&s);
1135 let style = cx.editor.theme.get("ui.text");
1136 let macro_width = if cx.editor.macro_recording.is_some() {
1138 } else {
1140 };
1141 surface.set_string(
1142 area.x + area.width.saturating_sub(key_width + macro_width),
1143 area.y + area.height.saturating_sub(1),
1144 disp.get(disp.len().saturating_sub(key_width as usize)..)
1145 .unwrap_or(&disp),
1146 style,
1147 );
1148 if let Some((reg, _)) = cx.editor.macro_recording {
1149 let disp = format!("[{}]", reg);
1150 let style = style
1151 .fg(helix_view::graphics::Color::Yellow)
1152 .add_modifier(Modifier::BOLD);
1153 surface.set_string(
1154 area.x + area.width.saturating_sub(3),
1155 area.y + area.height.saturating_sub(1),
1156 &disp,
1157 style,
1158 );
1162 if let Some(completion) = self.completion.as_mut() {
1163 completion.render(area, surface, cx);
1167 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1168 match editor.cursor() {
1169 // All block cursors are drawn manually
1170 (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
1171 cursor => cursor,
1176fn canonicalize_key(key: &mut KeyEvent) {
1177 if let KeyEvent {
1178 code: KeyCode::Char(_),
1179 modifiers: _,
1180 } = key
1182 key.modifiers.remove(KeyModifiers::SHIFT)