3 compositor::{Component, Context, EventResult},
5 keymap::{KeymapResult, KeymapResultKind, Keymaps},
6 ui::{Completion, ProgressSpinners},
10 coords_at_pos, encoding,
12 ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
15 syntax::{self, HighlightEvent},
16 unicode::segmentation::UnicodeSegmentation,
17 unicode::width::UnicodeWidthStr,
18 LineEnding, Position, Range, Selection,
21 document::{Mode, SCRATCH_BUFFER_NAME},
22 editor::CursorShapeConfig,
23 graphics::{CursorKind, Modifier, Rect, Style},
26 keyboard::{KeyCode, KeyModifiers},
27 Document, Editor, Theme, View,
31use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
32use tui::buffer::Buffer as Surface;
34pub struct EditorView {
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())
50 pub fn new(keymaps: Keymaps) -> Self {
54 last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
56 spinners: ProgressSpinners::default(),
61 pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
65 #[allow(clippy::too_many_arguments)]
71 surface: &mut Surface,
74 config: &helix_view::editor::Config,
76 let inner = view.inner_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(
84 Self::doc_selection_highlights(doc, view, theme, &config.cursor_shape),
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);
94 Self::render_focused_view_elements(view, doc, inner, theme, surface);
97 // if we're not at the edge of the screen, draw a right border
98 if viewport.right() != view.area.right() {
100 let border_style = theme.get("ui.window");
101 for y in area.top()..area.bottom() {
103 .set_symbol(tui::symbols::line::VERTICAL)
105 .set_style(border_style);
109 self.render_diagnostics(doc, view, inner, surface, theme);
111 let statusline_area = view
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);
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>(
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),
135 // calculate viewport byte ranges
136 let start = text.line_to_byte(offset.row);
137 let end = text.line_to_byte(last_line + 1);
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 } => {
152 text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
154 text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
155 HighlightEvent::Source { start, end }
163 [HighlightEvent::Source {
164 start: text.byte_to_char(range.start),
165 end: text.byte_to_char(range.end),
172 /// Get highlight spans for document diagnostics
173 pub fn doc_diagnostics_highlights(
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"))
182 "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
190 diagnostic.range.start..diagnostic.range.end,
196 /// Get highlight spans for selections in a document view.
197 pub fn doc_selection_highlights(
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),
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)
238 (cursor_scope, selection_scope)
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));
254 let range = range.min_width_1(text);
255 if range.head > range.anchor {
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));
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));
268 spans.push((selection_scope, cursor_end..range.anchor));
275 pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
279 surface: &mut Surface,
283 let text = doc.text().slice(..);
285 let mut spans = Vec::new();
286 let mut visual_x = 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 {
299 HighlightEvent::HighlightStart(span) => {
302 HighlightEvent::HighlightEnd => {
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
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() {
319 let style = spans.iter().fold(text_style, |acc, span| {
320 acc.patch(theme.highlight(span.0))
323 // we still want to render an empty cell with the style
325 viewport.x + visual_x - offset.col as u16,
335 // TODO: with proper iter this shouldn't be necessary
336 if line >= viewport.height {
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)
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)
353 let style = spans.iter().fold(text_style, |acc, span| {
354 acc.patch(theme.highlight(span.0))
357 // if we're offscreen just keep going until we hit a new line
359 viewport.x + visual_x - offset.col as u16,
366 visual_x = visual_x.saturating_add(width as u16);
374 /// Render brace match, etc (meant for the focused view only)
375 pub fn render_focused_view_elements(
380 surface: &mut Surface,
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
396 let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
398 .add_modifier(Modifier::REVERSED)
399 .add_modifier(Modifier::DIM)
402 surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
409 pub fn render_gutter(
413 surface: &mut Surface,
416 config: &helix_view::editor::Config,
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
427 .map(|range| range.cursor_line(text))
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) {
446 viewport.y + i as u16,
449 gutter_style.patch(style),
454 offset += *width as u16;
458 pub fn render_diagnostics(
463 surface: &mut Surface,
466 use helix_core::diagnostic::Severity;
470 widgets::{Paragraph, Widget, Wrap},
476 .cursor(doc.text().slice(..));
478 let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
479 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
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(
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,
499 lines.extend(text.lines);
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);
508 Rect::new(viewport.right() - width, viewport.y + 1, width, height),
513 pub fn render_statusline(
518 surface: &mut Surface,
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",
538 .and_then(|spinner| spinner.frame())
542 let base_style = if is_focused {
543 theme.get("ui.statusline")
545 theme.get("ui.statusline.inactive")
548 surface.set_style(viewport.with_height(1), base_style);
550 surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
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`.
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,
572 let (warnings, errors) = diags;
573 let warning_style = theme.get("warning");
574 let error_style = theme.get("error");
576 let (count, style) = match i {
577 0 => (warnings, warning_style),
578 1 => (errors, error_style),
584 let style = base_style.patch(style);
585 right_side_text.0.push(Span::styled("●", style));
588 .push(Span::styled(format!(" {} ", count), base_style));
592 let sels_count = doc.selection(view.id).len();
593 right_side_text.0.push(Span::styled(
597 if sels_count == 1 { "" } else { "s" }
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",
616 let pos = coords_at_pos(
617 doc.text().slice(..),
618 doc.selection(view.id)
620 .cursor(doc.text().slice(..)),
622 right_side_text.0.push(Span::styled(
623 format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
627 let enc = doc.encoding();
628 if enc != encoding::UTF_8 {
631 .push(Span::styled(format!(" {} ", enc.name()), base_style));
634 // Render to the statusline.
639 .saturating_sub(right_side_text.width() as u16),
642 right_side_text.width() as u16,
645 //-------------------------------
646 // Middle / File path / Title
647 //-------------------------------
649 let rel_path = doc.relative_path();
652 .map(|p| p.to_string_lossy())
653 .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
654 format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
657 surface.set_string_truncated(
658 viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
664 .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
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
675 fn handle_keymap_event(
678 cxt: &mut commands::Context,
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);
693 KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
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)
706 KeymapResultKind::Cancelled(pending) => {
709 Some(ch) => commands::insert::insert_char(cx, ch),
711 if let KeymapResultKind::Matched(command) =
712 self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
725 fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
728 key!(i @ '0'..='9') => {
729 let i = i.to_digit(10).unwrap() as usize;
731 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
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)
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);
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
760 pub fn set_completion(
763 items: Vec<helix_lsp::lsp::CompletionItem>,
764 offset_encoding: helix_lsp::OffsetEncoding,
766 trigger_offset: usize,
770 Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
772 if completion.is_empty() {
773 // skip if we got no completion results
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);
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
796 fn handle_mouse_event(
799 cxt: &mut commands::Context,
803 kind: MouseEventKind::Down(MouseButton::Left),
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))
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)));
823 doc.set_selection(view_id, Selection::point(pos));
826 editor.tree.focus = view_id;
828 return EventResult::Consumed(None);
835 kind: MouseEventKind::Drag(MouseButton::Left),
840 let (view, doc) = current!(cxt.editor);
842 let pos = match view.pos_at_screen_coords(doc, row, column) {
844 None => return EventResult::Ignored,
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)
855 kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown,
860 let current_view = cxt.editor.tree.focus;
862 let direction = match event.kind {
863 MouseEventKind::ScrollUp => Direction::Backward,
864 MouseEventKind::ScrollDown => Direction::Forward,
868 let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
869 view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
874 Some(view_id) => cxt.editor.tree.focus = view_id,
875 None => return EventResult::Ignored,
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)
887 kind: MouseEventKind::Up(MouseButton::Left),
890 if !cxt.editor.config.middle_click_paste {
891 return EventResult::Ignored;
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;
901 commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
903 EventResult::Consumed(None)
907 kind: MouseEventKind::Up(MouseButton::Middle),
913 let editor = &mut cxt.editor;
914 if !editor.config.middle_click_paste {
915 return EventResult::Ignored;
918 if modifiers == crossterm::event::KeyModifiers::ALT {
919 commands::MappableCommand::replace_selections_with_primary_clipboard
922 return EventResult::Consumed(None);
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))
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);
941 _ => EventResult::Ignored,
946impl Component for EditorView {
950 context: &mut crate::compositor::Context,
952 let mut cx = commands::Context {
953 editor: context.editor,
957 on_next_key_callback: None,
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)
968 cx.editor.reset_idle_timer();
969 let mut key = KeyEvent::from(key);
970 canonicalize_key(&mut key);
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);
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 {
996 let res = completion.handle_event(event, &mut cx);
998 if let EventResult::Consumed(callback) = res {
1001 if callback.is_some() {
1003 self.clear_completion(cx.editor);
1008 // if completion didn't take the event, we pass it onto commands
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();
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);
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!(),
1059 self.last_insert.1.clear();
1061 (Mode::Insert, Mode::Normal) => {
1062 // if exiting insert mode, remove completion
1063 self.completion = None;
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();
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")
1111 cx.editor.theme.get("ui.text")
1116 area.y + area.height.saturating_sub(1),
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));
1135 let style = cx.editor.theme.get("ui.text");
1136 let macro_width = if cx.editor.macro_recording.is_some() {
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)..)
1148 if let Some((reg, _)) = cx.editor.macro_recording {
1149 let disp = format!("[{}]", reg);
1151 .fg(helix_view::graphics::Color::Yellow)
1152 .add_modifier(Modifier::BOLD);
1154 area.x + area.width.saturating_sub(3),
1155 area.y + area.height.saturating_sub(1),
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),
1176fn canonicalize_key(key: &mut KeyEvent) {
1178 code: KeyCode::Char(_),
1182 key.modifiers.remove(KeyModifiers::SHIFT)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list