on 2158366 help sign in
helix/helix-term/src/ui/editor.rs
1171 lines 43 kB history raw
1use crate::{
2 commands,
3 compositor::{Component, Context, EventResult},
4 key,
5 keymap::{KeymapResult, Keymaps},
6 ui::{Completion, ProgressSpinners},
7};
9use helix_core::{
10 coords_at_pos,
11 graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
12 movement::Direction,
13 syntax::{self, HighlightEvent},
14 unicode::segmentation::UnicodeSegmentation,
15 unicode::width::UnicodeWidthStr,
16 LineEnding, Position, Range, Selection,
17};
18use helix_dap::{SourceBreakpoint, StackFrame};
19use helix_view::{
20 document::Mode,
21 editor::LineNumber,
22 graphics::{CursorKind, Modifier, Rect, Style},
23 info::Info,
24 input::KeyEvent,
25 keyboard::{KeyCode, KeyModifiers},
26 Document, Editor, Theme, View,
27};
28use std::borrow::Cow;
30use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
31use tui::buffer::Buffer as Surface;
33pub struct EditorView {
34 keymaps: Keymaps,
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())
45 }
48impl EditorView {
49 pub fn new(keymaps: Keymaps) -> Self {
50 Self {
51 keymaps,
52 on_next_key: None,
53 last_insert: (commands::Command::normal_mode, Vec::new()),
54 completion: None,
55 spinners: ProgressSpinners::default(),
56 autoinfo: None,
57 }
58 }
60 pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
61 &mut self.spinners
62 }
64 #[allow(clippy::too_many_arguments)]
65 pub fn render_view(
66 &self,
67 doc: &Document,
68 view: &View,
69 viewport: Rect,
70 surface: &mut Surface,
71 theme: &Theme,
72 is_focused: bool,
73 loader: &syntax::Loader,
74 config: &helix_view::editor::Config,
75 debugger: &Option<helix_dap::Client>,
76 ) {
77 let inner = view.inner_area();
78 let area = view.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(
84 highlights,
85 Self::doc_selection_highlights(doc, view, theme),
86 ))
87 } else {
88 Box::new(highlights)
89 };
91 Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
92 Self::render_gutter(
93 doc, view, view.area, surface, theme, is_focused, config, debugger,
94 );
96 if is_focused {
97 Self::render_focused_view_elements(view, doc, inner, theme, surface);
98 }
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() {
105 surface
106 .get_mut(x, y)
107 .set_symbol(tui::symbols::line::VERTICAL)
108 //.set_symbol(" ")
109 .set_style(border_style);
110 }
111 }
113 self.render_diagnostics(doc, view, inner, surface, theme, debugger);
115 let area = Rect::new(
116 view.area.x,
117 view.area.y + view.area.height.saturating_sub(1),
118 view.area.width,
119 1,
120 );
121 self.render_statusline(doc, view, area, surface, theme, is_focused);
122 }
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>(
129 doc: &'doc Document,
130 offset: Position,
131 height: u16,
132 theme: &Theme,
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),
140 );
142 let range = {
143 // calculate viewport byte ranges
144 let start = text.line_to_byte(offset.row);
145 let end = text.line_to_byte(last_line + 1);
147 start..end
148 };
150 // TODO: range doesn't actually restrict source, just highlight range
151 let highlights = match doc.syntax() {
152 Some(syntax) => {
153 let scopes = theme.scopes();
154 syntax
155 .highlight_iter(text.slice(..), Some(range), None, |language| {
156 loader
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::<
166 _,
167 &'static syntax::HighlightConfiguration,
168 >(config_ref)
169 };
170 Some(config_ref)
171 })
172 })
173 .map(|event| event.unwrap())
174 .collect() // TODO: we collect here to avoid holding the lock, fix later
175 }
176 None => vec![HighlightEvent::Source {
177 start: range.start,
178 end: range.end,
179 }],
180 }
181 .into_iter()
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 }
188 }
189 event => event,
190 });
192 Box::new(highlights)
193 }
195 /// Get highlight spans for document diagnostics
196 pub fn doc_diagnostics_highlights(
197 doc: &Document,
198 theme: &Theme,
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!");
206 doc.diagnostics()
207 .iter()
208 .map(|diagnostic| {
209 (
210 diagnostic_scope,
211 diagnostic.range.start..diagnostic.range.end,
212 )
213 })
214 .collect()
215 }
217 /// Get highlight spans for selections in a document view.
218 pub fn doc_selection_highlights(
219 doc: &Document,
220 view: &View,
221 theme: &Theme,
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),
238 }
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)
252 } else {
253 (cursor_scope, selection_scope)
254 };
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));
259 continue;
260 }
262 let range = range.min_width_1(text);
263 if range.head > range.anchor {
264 // Standard case.
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));
268 } else {
269 // Reverse case.
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));
273 }
274 }
276 spans
277 }
279 pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
280 doc: &Document,
281 offset: Position,
282 viewport: Rect,
283 surface: &mut Surface,
284 theme: &Theme,
285 highlights: H,
286 ) {
287 let text = doc.text().slice(..);
289 let mut spans = Vec::new();
290 let mut visual_x = 0u16;
291 let mut line = 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 {
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 let style = spans.iter().fold(text_style, |acc, span| {
314 let style = theme.get(theme.scopes()[span.0].as_str());
315 acc.patch(style)
316 });
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() {
323 if !out_of_bounds {
324 // we still want to render an empty cell with the style
325 surface.set_string(
326 viewport.x + visual_x - offset.col as u16,
327 viewport.y + line,
328 " ",
329 style,
330 );
331 }
333 visual_x = 0;
334 line += 1;
336 // TODO: with proper iter this shouldn't be necessary
337 if line >= viewport.height {
338 break 'outer;
339 }
340 } else {
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)
346 } else {
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)
351 };
353 if !out_of_bounds {
354 // if we're offscreen just keep going until we hit a new line
355 surface.set_string(
356 viewport.x + visual_x - offset.col as u16,
357 viewport.y + line,
358 grapheme,
359 style,
360 );
361 }
363 visual_x = visual_x.saturating_add(width as u16);
364 }
365 }
366 }
367 }
368 }
369 }
371 /// Render brace match, etc (meant for the focused view only)
372 pub fn render_focused_view_elements(
373 view: &View,
374 doc: &Document,
375 viewport: Rect,
376 theme: &Theme,
377 surface: &mut Surface,
378 ) {
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
392 {
393 let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
394 Style::default()
395 .add_modifier(Modifier::REVERSED)
396 .add_modifier(Modifier::DIM)
397 });
399 surface
400 .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
401 .set_style(style);
402 }
403 }
404 }
405 }
407 #[allow(clippy::too_many_arguments)]
408 pub fn render_gutter(
409 doc: &Document,
410 view: &View,
411 viewport: Rect,
412 surface: &mut Surface,
413 theme: &Theme,
414 is_focused: bool,
415 config: &helix_view::editor::Config,
416 debugger: &Option<helix_dap::Client>,
417 ) {
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
434 .text()
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
441 .selection(view.id)
442 .iter()
443 .map(|range| range.cursor_line(text))
444 .collect();
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()
452 }
453 }
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) {
458 surface.set_stringn(
459 viewport.x,
460 viewport.y + i as u16,
461 "●",
462 1,
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,
468 },
469 );
470 }
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)
476 {
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);
481 } else {
482 surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, warning);
483 }
484 }
485 }
487 if let Some(sp) = stack_pointer.as_ref() {
488 if let Some(src) = sp.source.as_ref() {
489 if doc
490 .path()
491 .map(|path| src.path == Some(path.clone()))
492 .unwrap_or(false)
493 && sp.line - 1 == line
494 {
495 surface.set_style(
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),
499 );
500 }
501 }
502 }
504 let text = if line == last_line && !draw_last {
505 " ~".into()
506 } else {
507 let line = match config.line_number {
508 LineNumber::Absolute => line + 1,
509 LineNumber::Relative => abs_diff(current_line, line),
510 };
511 format!("{:>5}", line)
512 };
513 surface.set_stringn(
514 viewport.x + 1,
515 viewport.y + i as u16,
516 text,
517 5,
518 if selected && is_focused {
519 linenr_select
520 } else {
521 linenr
522 },
523 );
524 }
525 }
527 pub fn render_diagnostics(
528 &self,
529 doc: &Document,
530 view: &View,
531 viewport: Rect,
532 surface: &mut Surface,
533 theme: &Theme,
534 debugger: &Option<helix_dap::Client>,
535 ) {
536 use helix_core::diagnostic::Severity;
537 use tui::{
538 layout::Alignment,
539 text::Text,
540 widgets::{Paragraph, Widget},
541 };
543 let cursor = doc
544 .selection(view.id)
545 .primary()
546 .cursor(doc.text().slice(..));
548 let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
549 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
550 });
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(
561 &diagnostic.message,
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,
567 },
568 );
569 lines.extend(text.lines);
570 }
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
577 .iter()
578 .find(|breakpoint| breakpoint.line - 1 == line)
579 {
580 if let Some(condition) = &breakpoint.condition {
581 lines.extend(
582 Text::styled(condition, info.add_modifier(Modifier::UNDERLINED))
583 .lines,
584 );
585 }
586 if let Some(log_message) = &breakpoint.log_message {
587 lines.extend(
588 Text::styled(log_message, info.add_modifier(Modifier::UNDERLINED))
589 .lines,
590 );
591 }
592 }
593 }
594 }
595 }
597 let paragraph = Paragraph::new(lines).alignment(Alignment::Right);
598 let width = 80.min(viewport.width);
599 let height = 15.min(viewport.height);
600 paragraph.render(
601 Rect::new(
602 viewport.right() - width,
603 viewport.y as u16 + 1,
604 width,
605 height,
606 ),
607 surface,
608 );
609 }
611 #[allow(clippy::too_many_arguments)]
612 pub fn render_statusline(
613 &self,
614 doc: &Document,
615 view: &View,
616 viewport: Rect,
617 surface: &mut Surface,
618 theme: &Theme,
619 is_focused: bool,
620 ) {
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",
629 };
630 let progress = doc
631 .language_server()
632 .and_then(|srv| {
633 self.spinners
634 .get(srv.id())
635 .and_then(|spinner| spinner.frame())
636 })
637 .unwrap_or("");
639 let style = if is_focused {
640 theme.get("ui.statusline")
641 } else {
642 theme.get("ui.statusline.inactive")
643 };
644 // statusline
645 surface.set_style(Rect::new(viewport.x, viewport.y, viewport.width, 1), style);
646 if is_focused {
647 surface.set_string(viewport.x + 1, viewport.y, mode, style);
648 }
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 { "" });
655 surface.set_stringn(
656 viewport.x + 8,
657 viewport.y,
658 title,
659 viewport.width.saturating_sub(6) as usize,
660 style,
661 );
662 }
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",
681 // };
682 let position_info = {
683 let pos = coords_at_pos(
684 doc.text().slice(..),
685 doc.selection(view.id)
686 .primary()
687 .cursor(doc.text().slice(..)),
688 );
689 format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
690 };
692 // Render them to the status line together.
693 let right_side_text = format!(
694 "{} {} ",
695 &diag_count[..diag_count.len().min(4)],
696 // indent_info,
697 position_info
698 );
699 let text_len = right_side_text.len() as u16;
700 surface.set_string(
701 viewport.x + viewport.width.saturating_sub(text_len),
702 viewport.y,
703 right_side_text,
704 style,
705 );
706 }
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
711 /// otherwise.
712 fn handle_keymap_event(
713 &mut self,
714 mode: Mode,
715 cxt: &mut commands::Context,
716 event: KeyEvent,
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),
723 }
724 None
725 }
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) {
729 match keyresult {
730 KeymapResult::NotFound => {
731 if let Some(ch) = event.char() {
732 commands::insert::insert_char(cx, ch)
733 }
734 }
735 KeymapResult::Cancelled(pending) => {
736 for ev in pending {
737 match ev.char() {
738 Some(ch) => commands::insert::insert_char(cx, ch),
739 None => {
740 if let KeymapResult::Matched(command) =
741 self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
742 {
743 command.execute(cx);
744 }
745 }
746 }
747 }
748 }
749 _ => unreachable!(),
750 }
751 }
752 }
754 fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
755 match event {
756 // count handling
757 key!(i @ '0'..='9') => {
758 let i = i.to_digit(10).unwrap() as usize;
759 cxt.editor.count =
760 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
761 }
762 // special handling for repeat operator
763 key!('.') => {
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)
769 }
770 }
771 _ => {
772 // set the count
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);
778 // set the register
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
784 }
785 }
786 }
787 }
789 pub fn set_completion(
790 &mut self,
791 items: Vec<helix_lsp::lsp::CompletionItem>,
792 offset_encoding: helix_lsp::OffsetEncoding,
793 trigger_offset: usize,
794 size: Rect,
795 ) {
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);
800 }
803impl EditorView {
804 fn handle_mouse_event(
805 &mut self,
806 event: MouseEvent,
807 cxt: &mut commands::Context,
808 ) -> EventResult {
809 match event {
810 MouseEvent {
811 kind: MouseEventKind::Down(MouseButton::Left),
812 row,
813 column,
814 modifiers,
815 ..
816 } => {
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))
822 });
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)));
830 } else {
831 doc.set_selection(view_id, Selection::point(pos));
832 }
834 editor.tree.focus = view_id;
836 return EventResult::Consumed(None);
837 }
839 EventResult::Ignored
840 }
842 MouseEvent {
843 kind: MouseEventKind::Drag(MouseButton::Left),
844 row,
845 column,
846 ..
847 } => {
848 let (view, doc) = current!(cxt.editor);
850 let pos = match view.pos_at_screen_coords(doc, row, column) {
851 Some(pos) => pos,
852 None => return EventResult::Ignored,
853 };
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)
860 }
862 MouseEvent {
863 kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown,
864 row,
865 column,
866 ..
867 } => {
868 let current_view = cxt.editor.tree.focus;
870 let direction = match event.kind {
871 MouseEventKind::ScrollUp => Direction::Backward,
872 MouseEventKind::ScrollDown => Direction::Forward,
873 _ => unreachable!(),
874 };
876 let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
877 view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
878 .map(|_| view.id)
879 });
881 match result {
882 Some(view_id) => cxt.editor.tree.focus = view_id,
883 None => return EventResult::Ignored,
884 }
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)
892 }
894 MouseEvent {
895 kind: MouseEventKind::Up(MouseButton::Left),
896 ..
897 } => {
898 if !cxt.editor.config.middle_click_paste {
899 return EventResult::Ignored;
900 }
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;
907 }
909 commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
911 EventResult::Consumed(None)
912 }
914 MouseEvent {
915 kind: MouseEventKind::Up(MouseButton::Middle),
916 row,
917 column,
918 modifiers,
919 ..
920 } => {
921 let editor = &mut cxt.editor;
922 if !editor.config.middle_click_paste {
923 return EventResult::Ignored;
924 }
926 if modifiers == crossterm::event::KeyModifiers::ALT {
927 commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
929 return EventResult::Consumed(None);
930 }
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))
935 });
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);
943 }
945 EventResult::Ignored
946 }
948 _ => EventResult::Ignored,
949 }
950 }
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,
958 count: None,
959 callback: None,
960 on_next_key_callback: None,
961 jobs: cx.jobs,
962 };
964 match event {
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)
969 }
970 Event::Key(key) => {
971 let mut key = KeyEvent::from(key);
972 canonicalize_key(&mut key);
973 // clear status
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);
982 } else {
983 match mode {
984 Mode::Insert => {
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 {
993 editor: cxt.editor,
994 jobs: cxt.jobs,
995 scroll: None,
996 };
997 let res = completion.handle_event(event, &mut cx);
999 if let EventResult::Consumed(callback) = res {
1000 consumed = true;
1002 if callback.is_some() {
1003 // assume close_fn
1004 self.completion = None;
1009 // if completion didn't take the event, we pass it onto commands
1010 if !consumed {
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();
1027 // appease borrowck
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);
1039 // mode transitions
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!(),
1052 };
1053 self.last_insert.1.clear();
1055 (Mode::Insert, Mode::Normal) => {
1056 // if exiting insert mode, remove completion
1057 self.completion = None;
1059 _ => (),
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
1074 cx.editor
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;
1080 self.render_view(
1081 doc,
1082 view,
1083 area,
1084 surface,
1085 &cx.editor.theme,
1086 is_focused,
1087 loader,
1088 &cx.editor.config,
1089 &cx.editor.debugger,
1090 );
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")
1106 } else {
1107 cx.editor.theme.get("ui.text")
1108 };
1110 surface.set_string(
1111 area.x,
1112 area.y + area.height.saturating_sub(1),
1113 status_msg,
1114 style,
1115 );
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));
1127 } else {
1128 disp.push_str(&s);
1131 surface.set_string(
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)..)
1135 .unwrap_or(&disp),
1136 cx.editor.theme.get("ui.text"),
1137 );
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"),
1149 // };
1150 editor.cursor()
1154fn canonicalize_key(key: &mut KeyEvent) {
1155 if let KeyEvent {
1156 code: KeyCode::Char(_),
1157 modifiers: _,
1158 } = key
1160 key.modifiers.remove(KeyModifiers::SHIFT)
1164#[inline]
1165fn abs_diff(a: usize, b: usize) -> usize {
1166 if a > b {
1167 a - b
1168 } else {
1169 b - a