3 compositor::{Component, Context, EventResult},
5 keymap::{KeymapResult, Keymaps},
6 ui::{Completion, ProgressSpinners},
11 graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
13 syntax::{self, HighlightEvent},
14 unicode::segmentation::UnicodeSegmentation,
15 unicode::width::UnicodeWidthStr,
16 LineEnding, Position, Range, Selection,
18use helix_dap::{SourceBreakpoint, StackFrame};
22 graphics::{CursorKind, Modifier, Rect, Style},
25 keyboard::{KeyCode, KeyModifiers},
26 Document, Editor, Theme, View,
30use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
31use tui::buffer::Buffer as Surface;
33pub struct EditorView {
35 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
36 last_insert: (commands::Command, Vec<KeyEvent>),
37 completion: Option<Completion>,
38 spinners: ProgressSpinners,
39 autoinfo: Option<Info>,
42impl Default for EditorView {
43 fn default() -> Self {
44 Self::new(Keymaps::default())
49 pub fn new(keymaps: Keymaps) -> Self {
53 last_insert: (commands::Command::normal_mode, Vec::new()),
55 spinners: ProgressSpinners::default(),
60 pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
64 #[allow(clippy::too_many_arguments)]
70 surface: &mut Surface,
73 loader: &syntax::Loader,
74 config: &helix_view::editor::Config,
75 debugger: &Option<helix_dap::Client>,
77 let inner = view.inner_area();
80 let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, loader);
81 let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
82 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
83 Box::new(syntax::merge(
85 Self::doc_selection_highlights(doc, view, theme),
91 Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
93 doc, view, view.area, surface, theme, is_focused, config, debugger,
97 Self::render_focused_view_elements(view, doc, inner, theme, surface);
100 // if we're not at the edge of the screen, draw a right border
101 if viewport.right() != view.area.right() {
102 let x = area.right();
103 let border_style = theme.get("ui.window");
104 for y in area.top()..area.bottom() {
107 .set_symbol(tui::symbols::line::VERTICAL)
109 .set_style(border_style);
113 self.render_diagnostics(doc, view, inner, surface, theme, debugger);
115 let area = Rect::new(
117 view.area.y + view.area.height.saturating_sub(1),
121 self.render_statusline(doc, view, area, surface, theme, is_focused);
124 /// Get syntax highlights for a document in a view represented by the first line
125 /// and column (`offset`) and the last line. This is done instead of using a view
126 /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
127 #[allow(clippy::too_many_arguments)]
128 pub fn doc_syntax_highlights<'doc>(
133 loader: &syntax::Loader,
134 ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
135 let text = doc.text().slice(..);
136 let last_line = std::cmp::min(
137 // Saturating subs to make it inclusive zero indexing.
138 (offset.row + height as usize).saturating_sub(1),
139 doc.text().len_lines().saturating_sub(1),
143 // calculate viewport byte ranges
144 let start = text.line_to_byte(offset.row);
145 let end = text.line_to_byte(last_line + 1);
150 // TODO: range doesn't actually restrict source, just highlight range
151 let highlights = match doc.syntax() {
153 let scopes = theme.scopes();
155 .highlight_iter(text.slice(..), Some(range), None, |language| {
157 .language_config_for_scope(&format!("source.{}", language))
158 .and_then(|language_config| {
159 let config = language_config.highlight_config(scopes)?;
160 let config_ref = config.as_ref();
161 // SAFETY: the referenced `HighlightConfiguration` behind
162 // the `Arc` is guaranteed to remain valid throughout the
163 // duration of the highlight.
164 let config_ref = unsafe {
165 std::mem::transmute::<
167 &'static syntax::HighlightConfiguration,
173 .map(|event| event.unwrap())
174 .collect() // TODO: we collect here to avoid holding the lock, fix later
176 None => vec![HighlightEvent::Source {
182 .map(move |event| match event {
183 // convert byte offsets to char offset
184 HighlightEvent::Source { start, end } => {
185 let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
186 let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
187 HighlightEvent::Source { start, end }
195 /// Get highlight spans for document diagnostics
196 pub fn doc_diagnostics_highlights(
199 ) -> Vec<(usize, std::ops::Range<usize>)> {
200 let diagnostic_scope = theme
201 .find_scope_index("diagnostic")
202 .or_else(|| theme.find_scope_index("ui.cursor"))
203 .or_else(|| theme.find_scope_index("ui.selection"))
204 .expect("no selection scope found!");
211 diagnostic.range.start..diagnostic.range.end,
217 /// Get highlight spans for selections in a document view.
218 pub fn doc_selection_highlights(
222 ) -> Vec<(usize, std::ops::Range<usize>)> {
223 let text = doc.text().slice(..);
224 let selection = doc.selection(view.id);
225 let primary_idx = selection.primary_index();
227 let selection_scope = theme
228 .find_scope_index("ui.selection")
229 .expect("no selection scope found!");
230 let base_cursor_scope = theme
231 .find_scope_index("ui.cursor")
232 .unwrap_or(selection_scope);
234 let cursor_scope = match doc.mode() {
235 Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
236 Mode::Select => theme.find_scope_index("ui.cursor.select"),
237 Mode::Normal => Some(base_cursor_scope),
239 .unwrap_or(base_cursor_scope);
241 let primary_cursor_scope = theme
242 .find_scope_index("ui.cursor.primary")
243 .unwrap_or(cursor_scope);
244 let primary_selection_scope = theme
245 .find_scope_index("ui.selection.primary")
246 .unwrap_or(selection_scope);
248 let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
249 for (i, range) in selection.iter().enumerate() {
250 let (cursor_scope, selection_scope) = if i == primary_idx {
251 (primary_cursor_scope, primary_selection_scope)
253 (cursor_scope, selection_scope)
256 // Special-case: cursor at end of the rope.
257 if range.head == range.anchor && range.head == text.len_chars() {
258 spans.push((cursor_scope, range.head..range.head + 1));
262 let range = range.min_width_1(text);
263 if range.head > range.anchor {
265 let cursor_start = prev_grapheme_boundary(text, range.head);
266 spans.push((selection_scope, range.anchor..cursor_start));
267 spans.push((cursor_scope, cursor_start..range.head));
270 let cursor_end = next_grapheme_boundary(text, range.head);
271 spans.push((cursor_scope, range.head..cursor_end));
272 spans.push((selection_scope, cursor_end..range.anchor));
279 pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
283 surface: &mut Surface,
287 let text = doc.text().slice(..);
289 let mut spans = Vec::new();
290 let mut visual_x = 0u16;
292 let tab_width = doc.tab_width();
293 let tab = " ".repeat(tab_width);
295 let text_style = theme.get("ui.text");
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 let style = spans.iter().fold(text_style, |acc, span| {
314 let style = theme.get(theme.scopes()[span.0].as_str());
318 for grapheme in RopeGraphemes::new(text) {
319 let out_of_bounds = visual_x < offset.col as u16
320 || visual_x >= viewport.width + offset.col as u16;
322 if LineEnding::from_rope_slice(&grapheme).is_some() {
324 // we still want to render an empty cell with the style
326 viewport.x + visual_x - offset.col as u16,
336 // TODO: with proper iter this shouldn't be necessary
337 if line >= viewport.height {
341 let grapheme = Cow::from(grapheme);
343 let (grapheme, width) = if grapheme == "\t" {
344 // make sure we display tab as appropriate amount of spaces
345 (tab.as_str(), tab_width)
347 // Cow will prevent allocations if span contained in a single slice
348 // which should really be the majority case
349 let width = grapheme_width(&grapheme);
350 (grapheme.as_ref(), width)
354 // if we're offscreen just keep going until we hit a new line
356 viewport.x + visual_x - offset.col as u16,
363 visual_x = visual_x.saturating_add(width as u16);
371 /// Render brace match, etc (meant for the focused view only)
372 pub fn render_focused_view_elements(
377 surface: &mut Surface,
379 // Highlight matching braces
380 if let Some(syntax) = doc.syntax() {
381 let text = doc.text().slice(..);
382 use helix_core::match_brackets;
383 let pos = doc.selection(view.id).primary().cursor(text);
385 let pos = match_brackets::find(syntax, doc.text(), pos)
386 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
388 if let Some(pos) = pos {
389 // ensure col is on screen
390 if (pos.col as u16) < viewport.width + view.offset.col as u16
391 && pos.col >= view.offset.col
393 let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
395 .add_modifier(Modifier::REVERSED)
396 .add_modifier(Modifier::DIM)
400 .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
407 #[allow(clippy::too_many_arguments)]
408 pub fn render_gutter(
412 surface: &mut Surface,
415 config: &helix_view::editor::Config,
416 debugger: &Option<helix_dap::Client>,
418 let text = doc.text().slice(..);
419 let last_line = view.last_line(doc);
421 let linenr = theme.get("ui.linenr");
422 let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
424 let warning = theme.get("warning");
425 let error = theme.get("error");
426 let info = theme.get("info");
427 let hint = theme.get("hint");
429 // Whether to draw the line number for the last line of the
430 // document or not. We only draw it if it's not an empty line.
431 let draw_last = text.line_to_byte(last_line) < text.len_bytes();
433 let current_line = doc
435 .char_to_line(doc.selection(view.id).primary().anchor);
437 // it's used inside an iterator so the collect isn't needless:
438 // https://github.com/rust-lang/rust-clippy/issues/6164
439 #[allow(clippy::needless_collect)]
440 let cursors: Vec<_> = doc
443 .map(|range| range.cursor_line(text))
446 let mut breakpoints: Option<Vec<SourceBreakpoint>> = None;
447 let mut stack_pointer: Option<StackFrame> = None;
448 if let Some(debugger) = debugger {
449 if let Some(path) = doc.path() {
450 breakpoints = debugger.breakpoints.get(path).cloned();
451 stack_pointer = debugger.stack_pointer.clone()
455 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
456 use helix_core::diagnostic::Severity;
457 if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
460 viewport.y + i as u16,
463 match diagnostic.severity {
464 Some(Severity::Error) => error,
465 Some(Severity::Warning) | None => warning,
466 Some(Severity::Info) => info,
467 Some(Severity::Hint) => hint,
472 let selected = cursors.contains(&line);
474 if let Some(bps) = breakpoints.as_ref() {
475 if let Some(breakpoint) = bps.iter().find(|breakpoint| breakpoint.line - 1 == line)
477 if breakpoint.condition.is_some() {
478 surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, error);
479 } else if breakpoint.log_message.is_some() {
480 surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, info);
482 surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, warning);
487 if let Some(sp) = stack_pointer.as_ref() {
488 if let Some(src) = sp.source.as_ref() {
491 .map(|path| src.path == Some(path.clone()))
493 && sp.line - 1 == line
496 Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
497 helix_view::graphics::Style::default()
498 .bg(helix_view::graphics::Color::LightYellow),
504 let text = if line == last_line && !draw_last {
507 let line = match config.line_number {
508 LineNumber::Absolute => line + 1,
509 LineNumber::Relative => abs_diff(current_line, line),
511 format!("{:>5}", line)
515 viewport.y + i as u16,
518 if selected && is_focused {
527 pub fn render_diagnostics(
532 surface: &mut Surface,
534 debugger: &Option<helix_dap::Client>,
536 use helix_core::diagnostic::Severity;
540 widgets::{Paragraph, Widget},
546 .cursor(doc.text().slice(..));
548 let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
549 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
552 let warning = theme.get("warning");
553 let error = theme.get("error");
554 let info = theme.get("info");
555 let hint = theme.get("hint");
557 // Vec::with_capacity(diagnostics.len()); // rough estimate
558 let mut lines = Vec::new();
559 for diagnostic in diagnostics {
560 let text = Text::styled(
562 match diagnostic.severity {
563 Some(Severity::Error) => error,
564 Some(Severity::Warning) | None => warning,
565 Some(Severity::Info) => info,
566 Some(Severity::Hint) => hint,
569 lines.extend(text.lines);
572 if let Some(debugger) = debugger {
573 if let Some(path) = doc.path() {
574 if let Some(breakpoints) = debugger.breakpoints.get(path) {
575 let line = doc.text().char_to_line(cursor);
576 if let Some(breakpoint) = breakpoints
578 .find(|breakpoint| breakpoint.line - 1 == line)
580 if let Some(condition) = &breakpoint.condition {
582 Text::styled(condition, info.add_modifier(Modifier::UNDERLINED))
586 if let Some(log_message) = &breakpoint.log_message {
588 Text::styled(log_message, info.add_modifier(Modifier::UNDERLINED))
597 let paragraph = Paragraph::new(lines).alignment(Alignment::Right);
598 let width = 80.min(viewport.width);
599 let height = 15.min(viewport.height);
602 viewport.right() - width,
603 viewport.y as u16 + 1,
611 #[allow(clippy::too_many_arguments)]
612 pub fn render_statusline(
617 surface: &mut Surface,
621 //-------------------------------
622 // Left side of the status line.
623 //-------------------------------
625 let mode = match doc.mode() {
626 Mode::Insert => "INS",
627 Mode::Select => "SEL",
628 Mode::Normal => "NOR",
635 .and_then(|spinner| spinner.frame())
639 let style = if is_focused {
640 theme.get("ui.statusline")
642 theme.get("ui.statusline.inactive")
645 surface.set_style(Rect::new(viewport.x, viewport.y, viewport.width, 1), style);
647 surface.set_string(viewport.x + 1, viewport.y, mode, style);
649 surface.set_string(viewport.x + 5, viewport.y, progress, style);
651 if let Some(path) = doc.relative_path() {
652 let path = path.to_string_lossy();
654 let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
659 viewport.width.saturating_sub(6) as usize,
664 //-------------------------------
665 // Right side of the status line.
666 //-------------------------------
668 // Compute the individual info strings.
669 let diag_count = format!("{}", doc.diagnostics().len());
670 // let indent_info = match doc.indent_style {
671 // IndentStyle::Tabs => "tabs",
672 // IndentStyle::Spaces(1) => "spaces:1",
673 // IndentStyle::Spaces(2) => "spaces:2",
674 // IndentStyle::Spaces(3) => "spaces:3",
675 // IndentStyle::Spaces(4) => "spaces:4",
676 // IndentStyle::Spaces(5) => "spaces:5",
677 // IndentStyle::Spaces(6) => "spaces:6",
678 // IndentStyle::Spaces(7) => "spaces:7",
679 // IndentStyle::Spaces(8) => "spaces:8",
680 // _ => "indent:ERROR",
682 let position_info = {
683 let pos = coords_at_pos(
684 doc.text().slice(..),
685 doc.selection(view.id)
687 .cursor(doc.text().slice(..)),
689 format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
692 // Render them to the status line together.
693 let right_side_text = format!(
695 &diag_count[..diag_count.len().min(4)],
699 let text_len = right_side_text.len() as u16;
701 viewport.x + viewport.width.saturating_sub(text_len),
708 /// Handle events by looking them up in `self.keymaps`. Returns None
709 /// if event was handled (a command was executed or a subkeymap was
710 /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
712 fn handle_keymap_event(
715 cxt: &mut commands::Context,
717 ) -> Option<KeymapResult> {
718 self.autoinfo = None;
719 match self.keymaps.get_mut(&mode).unwrap().get(event) {
720 KeymapResult::Matched(command) => command.execute(cxt),
721 KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
722 k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
727 fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
728 if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
730 KeymapResult::NotFound => {
731 if let Some(ch) = event.char() {
732 commands::insert::insert_char(cx, ch)
735 KeymapResult::Cancelled(pending) => {
738 Some(ch) => commands::insert::insert_char(cx, ch),
740 if let KeymapResult::Matched(command) =
741 self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
754 fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
757 key!(i @ '0'..='9') => {
758 let i = i.to_digit(10).unwrap() as usize;
760 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
762 // special handling for repeat operator
764 // first execute whatever put us into insert mode
765 self.last_insert.0.execute(cxt);
766 // then replay the inputs
767 for &key in &self.last_insert.1.clone() {
768 self.insert_mode(cxt, key)
773 cxt.count = cxt.editor.count;
774 // TODO: edge case: 0j -> reset to 1
775 // if this fails, count was Some(0)
776 // debug_assert!(cxt.count != 0);
779 cxt.selected_register = cxt.editor.selected_register.take();
781 self.handle_keymap_event(mode, cxt, event);
782 if self.keymaps.pending().is_empty() {
783 cxt.editor.count = None
789 pub fn set_completion(
791 items: Vec<helix_lsp::lsp::CompletionItem>,
792 offset_encoding: helix_lsp::OffsetEncoding,
793 trigger_offset: usize,
796 let mut completion = Completion::new(items, offset_encoding, trigger_offset);
797 // TODO : propagate required size on resize to completion too
798 completion.required_size((size.width, size.height));
799 self.completion = Some(completion);
804 fn handle_mouse_event(
807 cxt: &mut commands::Context,
811 kind: MouseEventKind::Down(MouseButton::Left),
817 let editor = &mut cxt.editor;
819 let result = editor.tree.views().find_map(|(view, _focus)| {
820 view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
821 .map(|pos| (pos, view.id))
824 if let Some((pos, view_id)) = result {
825 let doc = &mut editor.documents[editor.tree.get(view_id).doc];
827 if modifiers == crossterm::event::KeyModifiers::ALT {
828 let selection = doc.selection(view_id).clone();
829 doc.set_selection(view_id, selection.push(Range::point(pos)));
831 doc.set_selection(view_id, Selection::point(pos));
834 editor.tree.focus = view_id;
836 return EventResult::Consumed(None);
843 kind: MouseEventKind::Drag(MouseButton::Left),
848 let (view, doc) = current!(cxt.editor);
850 let pos = match view.pos_at_screen_coords(doc, row, column) {
852 None => return EventResult::Ignored,
855 let mut selection = doc.selection(view.id).clone();
856 let primary = selection.primary_mut();
857 *primary = Range::new(primary.anchor, pos);
858 doc.set_selection(view.id, selection);
859 EventResult::Consumed(None)
863 kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown,
868 let current_view = cxt.editor.tree.focus;
870 let direction = match event.kind {
871 MouseEventKind::ScrollUp => Direction::Backward,
872 MouseEventKind::ScrollDown => Direction::Forward,
876 let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
877 view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
882 Some(view_id) => cxt.editor.tree.focus = view_id,
883 None => return EventResult::Ignored,
886 let offset = cxt.editor.config.scroll_lines.abs() as usize;
887 commands::scroll(cxt, offset, direction);
889 cxt.editor.tree.focus = current_view;
891 EventResult::Consumed(None)
895 kind: MouseEventKind::Up(MouseButton::Left),
898 if !cxt.editor.config.middle_click_paste {
899 return EventResult::Ignored;
902 let (view, doc) = current!(cxt.editor);
903 let range = doc.selection(view.id).primary();
905 if range.to() - range.from() <= 1 {
906 return EventResult::Ignored;
909 commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
911 EventResult::Consumed(None)
915 kind: MouseEventKind::Up(MouseButton::Middle),
921 let editor = &mut cxt.editor;
922 if !editor.config.middle_click_paste {
923 return EventResult::Ignored;
926 if modifiers == crossterm::event::KeyModifiers::ALT {
927 commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
929 return EventResult::Consumed(None);
932 let result = editor.tree.views().find_map(|(view, _focus)| {
933 view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
934 .map(|pos| (pos, view.id))
937 if let Some((pos, view_id)) = result {
938 let doc = &mut editor.documents[editor.tree.get(view_id).doc];
939 doc.set_selection(view_id, Selection::point(pos));
940 editor.tree.focus = view_id;
941 commands::Command::paste_primary_clipboard_before.execute(cxt);
942 return EventResult::Consumed(None);
948 _ => EventResult::Ignored,
953impl Component for EditorView {
954 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
955 let mut cxt = commands::Context {
956 selected_register: helix_view::RegisterSelection::default(),
957 editor: &mut cx.editor,
960 on_next_key_callback: None,
965 Event::Resize(_width, _height) => {
966 // Ignore this event, we handle resizing just before rendering to screen.
967 // Handling it here but not re-rendering will cause flashing
968 EventResult::Consumed(None)
971 let mut key = KeyEvent::from(key);
972 canonicalize_key(&mut key);
974 cxt.editor.status_msg = None;
976 let (_, doc) = current!(cxt.editor);
977 let mode = doc.mode();
979 if let Some(on_next_key) = self.on_next_key.take() {
980 // if there's a command waiting input, do that first
981 on_next_key(&mut cxt, key);
985 // record last_insert key
986 self.last_insert.1.push(key);
988 // let completion swallow the event if necessary
989 let mut consumed = false;
990 if let Some(completion) = &mut self.completion {
991 // use a fake context here
992 let mut cx = Context {
997 let res = completion.handle_event(event, &mut cx);
999 if let EventResult::Consumed(callback) = res {
1002 if callback.is_some() {
1004 self.completion = None;
1009 // if completion didn't take the event, we pass it onto commands
1011 self.insert_mode(&mut cxt, key);
1013 // lastly we recalculate completion
1014 if let Some(completion) = &mut self.completion {
1015 completion.update(&mut cxt);
1016 if completion.is_empty() {
1017 self.completion = None;
1022 mode => self.command_mode(mode, &mut cxt, key),
1026 self.on_next_key = cxt.on_next_key_callback.take();
1028 let callback = cxt.callback.take();
1030 // if the command consumed the last view, skip the render.
1031 // on the next loop cycle the Application will then terminate.
1032 if cxt.editor.should_close() {
1033 return EventResult::Ignored;
1036 let (view, doc) = current!(cxt.editor);
1037 view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff);
1040 match (mode, doc.mode()) {
1041 (Mode::Normal, Mode::Insert) => {
1042 // HAXX: if we just entered insert mode from normal, clear key buf
1043 // and record the command that got us into this mode.
1045 // how we entered insert mode is important, and we should track that so
1046 // we can repeat the side effect.
1048 self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
1049 KeymapResult::Matched(command) => command,
1050 // FIXME: insert mode can only be entered through single KeyCodes
1051 _ => unimplemented!(),
1053 self.last_insert.1.clear();
1055 (Mode::Insert, Mode::Normal) => {
1056 // if exiting insert mode, remove completion
1057 self.completion = None;
1062 EventResult::Consumed(callback)
1065 Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt),
1069 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
1070 // clear with background color
1071 surface.set_style(area, cx.editor.theme.get("ui.background"));
1073 // if the terminal size suddenly changed, we need to trigger a resize
1075 .resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline
1077 for (view, is_focused) in cx.editor.tree.views() {
1078 let doc = cx.editor.document(view.doc).unwrap();
1079 let loader = &cx.editor.syn_loader;
1089 &cx.editor.debugger,
1093 if let Some(ref mut info) = self.autoinfo {
1094 info.render(area, surface, cx);
1097 let key_width = 15u16; // for showing pending keys
1098 let mut status_msg_width = 0;
1100 // render status msg
1101 if let Some((status_msg, severity)) = &cx.editor.status_msg {
1102 status_msg_width = status_msg.width();
1103 use helix_view::editor::Severity;
1104 let style = if *severity == Severity::Error {
1105 cx.editor.theme.get("error")
1107 cx.editor.theme.get("ui.text")
1112 area.y + area.height.saturating_sub(1),
1118 if area.width.saturating_sub(status_msg_width as u16) > key_width {
1119 let mut disp = String::new();
1120 if let Some(count) = cx.editor.count {
1121 disp.push_str(&count.to_string())
1123 for key in self.keymaps.pending() {
1124 let s = key.to_string();
1125 if s.graphemes(true).count() > 1 {
1126 disp.push_str(&format!("<{}>", s));
1132 area.x + area.width.saturating_sub(key_width),
1133 area.y + area.height.saturating_sub(1),
1134 disp.get(disp.len().saturating_sub(key_width as usize)..)
1136 cx.editor.theme.get("ui.text"),
1140 if let Some(completion) = self.completion.as_mut() {
1141 completion.render(area, surface, cx);
1145 fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
1146 // match view.doc.mode() {
1147 // Mode::Insert => write!(stdout, "\x1B[6 q"),
1148 // mode => write!(stdout, "\x1B[2 q"),
1154fn canonicalize_key(key: &mut KeyEvent) {
1156 code: KeyCode::Char(_),
1160 key.modifiers.remove(KeyModifiers::SHIFT)
1165fn abs_diff(a: usize, b: usize) -> usize {
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