diff options
Diffstat (limited to 'helix-term/src/ui/editor.rs')
-rw-r--r-- | helix-term/src/ui/editor.rs | 292 |
1 files changed, 175 insertions, 117 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ac11d298..a2131abe 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -7,8 +7,10 @@ use crate::{ }; use helix_core::{ - coords_at_pos, - graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, + coords_at_pos, encoding, + graphemes::{ + ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, + }, movement::Direction, syntax::{self, HighlightEvent}, unicode::segmentation::UnicodeSegmentation, @@ -17,8 +19,8 @@ use helix_core::{ }; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, + editor::CursorShapeConfig, graphics::{CursorKind, Modifier, Rect, Style}, - info::Info, input::KeyEvent, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, @@ -31,10 +33,9 @@ use tui::buffer::Buffer as Surface; pub struct EditorView { keymaps: Keymaps, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, - last_insert: (commands::Command, Vec<KeyEvent>), + last_insert: (commands::MappableCommand, Vec<KeyEvent>), pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, - autoinfo: Option<Info>, } impl Default for EditorView { @@ -48,10 +49,9 @@ impl EditorView { Self { keymaps, on_next_key: None, - last_insert: (commands::Command::normal_mode, Vec::new()), + last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), - autoinfo: None, } } @@ -106,13 +106,12 @@ impl EditorView { } } - let highlights = - Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, &editor.syn_loader); + let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { Box::new(syntax::merge( highlights, - Self::doc_selection_highlights(doc, view, theme), + Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape), )) } else { Box::new(highlights) @@ -130,8 +129,7 @@ impl EditorView { let x = area.right(); let border_style = theme.get("ui.window"); for y in area.top()..area.bottom() { - surface - .get_mut(x, y) + surface[(x, y)] .set_symbol(tui::symbols::line::VERTICAL) //.set_symbol(" ") .set_style(border_style); @@ -154,8 +152,7 @@ impl EditorView { doc: &'doc Document, offset: Position, height: u16, - theme: &Theme, - loader: &syntax::Loader, + _theme: &Theme, ) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> { let text = doc.text().slice(..); let last_line = std::cmp::min( @@ -172,48 +169,34 @@ impl EditorView { start..end }; - // TODO: range doesn't actually restrict source, just highlight range - let highlights = match doc.syntax() { + match doc.syntax() { Some(syntax) => { - let scopes = theme.scopes(); - syntax - .highlight_iter(text.slice(..), Some(range), None, |language| { - loader.language_configuration_for_injection_string(language) - .and_then(|language_config| { - let config = language_config.highlight_config(scopes)?; - let config_ref = config.as_ref(); - // SAFETY: the referenced `HighlightConfiguration` behind - // the `Arc` is guaranteed to remain valid throughout the - // duration of the highlight. - let config_ref = unsafe { - std::mem::transmute::< - _, - &'static syntax::HighlightConfiguration, - >(config_ref) - }; - Some(config_ref) - }) - }) + let iter = syntax + // TODO: range doesn't actually restrict source, just highlight range + .highlight_iter(text.slice(..), Some(range), None) .map(|event| event.unwrap()) - .collect() // TODO: we collect here to avoid holding the lock, fix later + .map(move |event| match event { + // convert byte offsets to char offset + HighlightEvent::Source { start, end } => { + let start = + text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start)); + let end = + text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end)); + HighlightEvent::Source { start, end } + } + event => event, + }); + + Box::new(iter) } - None => vec![HighlightEvent::Source { - start: range.start, - end: range.end, - }], + None => Box::new( + [HighlightEvent::Source { + start: text.byte_to_char(range.start), + end: text.byte_to_char(range.end), + }] + .into_iter(), + ), } - .into_iter() - .map(move |event| match event { - // convert byte offsets to char offset - HighlightEvent::Source { start, end } => { - let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start)); - let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end)); - HighlightEvent::Source { start, end } - } - event => event, - }); - - Box::new(highlights) } /// Get highlight spans for document diagnostics @@ -245,11 +228,16 @@ impl EditorView { doc: &Document, view: &View, theme: &Theme, + cursor_shape_config: &CursorShapeConfig, ) -> Vec<(usize, std::ops::Range<usize>)> { let text = doc.text().slice(..); let selection = doc.selection(view.id); let primary_idx = selection.primary_index(); + let mode = doc.mode(); + let cursorkind = cursor_shape_config.from_mode(mode); + let cursor_is_block = cursorkind == CursorKind::Block; + let selection_scope = theme .find_scope_index("ui.selection") .expect("could not find `ui.selection` scope in the theme!"); @@ -257,7 +245,7 @@ impl EditorView { .find_scope_index("ui.cursor") .unwrap_or(selection_scope); - let cursor_scope = match doc.mode() { + let cursor_scope = match mode { Mode::Insert => theme.find_scope_index("ui.cursor.insert"), Mode::Select => theme.find_scope_index("ui.cursor.select"), Mode::Normal => Some(base_cursor_scope), @@ -273,7 +261,8 @@ impl EditorView { let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); for (i, range) in selection.iter().enumerate() { - let (cursor_scope, selection_scope) = if i == primary_idx { + let selection_is_primary = i == primary_idx; + let (cursor_scope, selection_scope) = if selection_is_primary { (primary_cursor_scope, primary_selection_scope) } else { (cursor_scope, selection_scope) @@ -281,7 +270,14 @@ impl EditorView { // Special-case: cursor at end of the rope. if range.head == range.anchor && range.head == text.len_chars() { - spans.push((cursor_scope, range.head..range.head + 1)); + if !selection_is_primary || cursor_is_block { + // Bar and underline cursors are drawn by the terminal + // BUG: If the editor area loses focus while having a bar or + // underline cursor (eg. when a regex prompt has focus) then + // the primary cursor will be invisible. This doesn't happen + // with block cursors since we manually draw *all* cursors. + spans.push((cursor_scope, range.head..range.head + 1)); + } continue; } @@ -290,11 +286,15 @@ impl EditorView { // Standard case. let cursor_start = prev_grapheme_boundary(text, range.head); spans.push((selection_scope, range.anchor..cursor_start)); - spans.push((cursor_scope, cursor_start..range.head)); + if !selection_is_primary || cursor_is_block { + spans.push((cursor_scope, cursor_start..range.head)); + } } else { // Reverse case. let cursor_end = next_grapheme_boundary(text, range.head); - spans.push((cursor_scope, range.head..cursor_end)); + if !selection_is_primary || cursor_is_block { + spans.push((cursor_scope, range.head..cursor_end)); + } spans.push((selection_scope, cursor_end..range.anchor)); } } @@ -320,6 +320,10 @@ impl EditorView { let text_style = theme.get("ui.text"); + // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch + // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light). + let text = text.slice(..); + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { @@ -336,17 +340,16 @@ impl EditorView { use helix_core::graphemes::{grapheme_width, RopeGraphemes}; - let style = spans.iter().fold(text_style, |acc, span| { - let style = theme.get(theme.scopes()[span.0].as_str()); - acc.patch(style) - }); - for grapheme in RopeGraphemes::new(text) { let out_of_bounds = visual_x < offset.col as u16 || visual_x >= viewport.width + offset.col as u16; if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { + let style = spans.iter().fold(text_style, |acc, span| { + acc.patch(theme.highlight(span.0)) + }); + // we still want to render an empty cell with the style surface.set_string( viewport.x + visual_x - offset.col as u16, @@ -377,6 +380,10 @@ impl EditorView { }; if !out_of_bounds { + let style = spans.iter().fold(text_style, |acc, span| { + acc.patch(theme.highlight(span.0)) + }); + // if we're offscreen just keep going until we hit a new line surface.set_string( viewport.x + visual_x - offset.col as u16, @@ -422,8 +429,7 @@ impl EditorView { .add_modifier(Modifier::DIM) }); - surface - .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) + surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)] .set_style(style); } } @@ -453,6 +459,8 @@ impl EditorView { let mut offset = 0; + let gutter_style = theme.get("ui.gutter"); + // avoid lots of small allocations by reusing a text buffer for each line let mut text = String::with_capacity(8); @@ -468,7 +476,7 @@ impl EditorView { viewport.y + i as u16, &text, *width, - style, + gutter_style.patch(style), ); } text.clear(); @@ -574,21 +582,6 @@ impl EditorView { } surface.set_string(viewport.x + 5, viewport.y, progress, base_style); - let rel_path = doc.relative_path(); - let path = rel_path - .as_ref() - .map(|p| p.to_string_lossy()) - .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); - - let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }); - surface.set_stringn( - viewport.x + 8, - viewport.y, - title, - viewport.width.saturating_sub(6) as usize, - base_style, - ); - //------------------------------- // Right side of the status line. //------------------------------- @@ -662,6 +655,13 @@ impl EditorView { base_style, )); + let enc = doc.encoding(); + if enc != encoding::UTF_8 { + right_side_text + .0 + .push(Span::styled(format!(" {} ", enc.name()), base_style)); + } + // Render to the statusline. surface.set_spans( viewport.x @@ -672,6 +672,31 @@ impl EditorView { &right_side_text, right_side_text.width() as u16, ); + + //------------------------------- + // Middle / File path / Title + //------------------------------- + let title = { + let rel_path = doc.relative_path(); + let path = rel_path + .as_ref() + .map(|p| p.to_string_lossy()) + .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); + format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }) + }; + + surface.set_string_truncated( + viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space + viewport.y, + title, + viewport + .width + .saturating_sub(6) + .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info + base_style, + true, + true, + ); } /// Handle events by looking them up in `self.keymaps`. Returns None @@ -684,12 +709,13 @@ impl EditorView { cxt: &mut commands::Context, event: KeyEvent, ) -> Option<KeymapResult> { + cxt.editor.autoinfo = None; let key_result = self.keymaps.get_mut(&mode).unwrap().get(event); - self.autoinfo = key_result.sticky.map(|node| node.infobox()); + cxt.editor.autoinfo = key_result.sticky.map(|node| node.infobox()); match &key_result.kind { KeymapResultKind::Matched(command) => command.execute(cxt), - KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), + KeymapResultKind::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), KeymapResultKind::MatchedSequence(commands) => { for command in commands { command.execute(cxt); @@ -789,8 +815,9 @@ impl EditorView { pub fn clear_completion(&mut self, editor: &mut Editor) { self.completion = None; + // Clear any savepoints - let (_, doc) = current!(editor); + let doc = doc_mut!(editor); doc.savepoint = None; editor.clear_idle_timer(); // don't retrigger } @@ -927,7 +954,7 @@ impl EditorView { return EventResult::Ignored; } - commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt); + commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt); EventResult::Consumed(None) } @@ -953,9 +980,9 @@ impl EditorView { if let Ok(pos) = doc.text().try_line_to_char(line) { doc.set_selection(view_id, Selection::point(pos)); if modifiers == crossterm::event::KeyModifiers::ALT { - commands::Command::dap_edit_log.execute(cxt); + commands::MappableCommand::dap_edit_log.execute(cxt); } else { - commands::Command::dap_edit_condition.execute(cxt); + commands::MappableCommand::dap_edit_condition.execute(cxt); } return EventResult::Consumed(None); @@ -977,7 +1004,8 @@ impl EditorView { } if modifiers == crossterm::event::KeyModifiers::ALT { - commands::Command::replace_selections_with_primary_clipboard.execute(cxt); + commands::MappableCommand::replace_selections_with_primary_clipboard + .execute(cxt); return EventResult::Consumed(None); } @@ -991,7 +1019,7 @@ impl EditorView { let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); doc.set_selection(view_id, Selection::point(pos)); editor.tree.focus = view_id; - commands::Command::paste_primary_clipboard_before.execute(cxt); + commands::MappableCommand::paste_primary_clipboard_before.execute(cxt); return EventResult::Consumed(None); } @@ -1004,14 +1032,18 @@ impl EditorView { } impl Component for EditorView { - fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { - let mut cxt = commands::Context { - editor: &mut cx.editor, + fn handle_event( + &mut self, + event: Event, + context: &mut crate::compositor::Context, + ) -> EventResult { + let mut cx = commands::Context { + editor: context.editor, count: None, register: None, callback: None, on_next_key_callback: None, - jobs: cx.jobs, + jobs: context.jobs, }; match event { @@ -1021,18 +1053,19 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(key) => { - cxt.editor.reset_idle_timer(); + cx.editor.reset_idle_timer(); let mut key = KeyEvent::from(key); canonicalize_key(&mut key); + // clear status - cxt.editor.status_msg = None; + cx.editor.status_msg = None; - let (_, doc) = current!(cxt.editor); + let doc = doc!(cx.editor); let mode = doc.mode(); if let Some(on_next_key) = self.on_next_key.take() { // if there's a command waiting input, do that first - on_next_key(&mut cxt, key); + on_next_key(&mut cx, key); } else { match mode { Mode::Insert => { @@ -1044,8 +1077,8 @@ impl Component for EditorView { if let Some(completion) = &mut self.completion { // use a fake context here let mut cx = Context { - editor: cxt.editor, - jobs: cxt.jobs, + editor: cx.editor, + jobs: cx.jobs, scroll: None, }; let res = completion.handle_event(event, &mut cx); @@ -1055,40 +1088,46 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn - self.clear_completion(cxt.editor); + self.clear_completion(cx.editor); } } } // if completion didn't take the event, we pass it onto commands if !consumed { - self.insert_mode(&mut cxt, key); + self.insert_mode(&mut cx, key); // lastly we recalculate completion if let Some(completion) = &mut self.completion { - completion.update(&mut cxt); + completion.update(&mut cx); if completion.is_empty() { - self.clear_completion(cxt.editor); + self.clear_completion(cx.editor); } } } } - mode => self.command_mode(mode, &mut cxt, key), + mode => self.command_mode(mode, &mut cx, key), } } - self.on_next_key = cxt.on_next_key_callback.take(); + self.on_next_key = cx.on_next_key_callback.take(); // appease borrowck - let callback = cxt.callback.take(); + let callback = cx.callback.take(); // if the command consumed the last view, skip the render. // on the next loop cycle the Application will then terminate. - if cxt.editor.should_close() { + if cx.editor.should_close() { return EventResult::Ignored; } - let (view, doc) = current!(cxt.editor); - view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff); + let (view, doc) = current!(cx.editor); + view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); + + // Store a history state if not in insert mode. This also takes care of + // commiting changes when leaving insert mode. + if doc.mode() != Mode::Insert { + doc.append_changes_to_history(view.id); + } // mode transitions match (mode, doc.mode()) { @@ -1117,7 +1156,7 @@ impl Component for EditorView { EventResult::Consumed(callback) } - Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt), + Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), } } @@ -1134,8 +1173,9 @@ impl Component for EditorView { } if cx.editor.config.auto_info { - if let Some(ref mut info) = self.autoinfo { + if let Some(mut info) = cx.editor.autoinfo.take() { info.render(area, surface, cx); + cx.editor.autoinfo = Some(info) } } @@ -1173,13 +1213,31 @@ impl Component for EditorView { disp.push_str(&s); } } + let style = cx.editor.theme.get("ui.text"); + let macro_width = if cx.editor.macro_recording.is_some() { + 3 + } else { + 0 + }; surface.set_string( - area.x + area.width.saturating_sub(key_width), + area.x + area.width.saturating_sub(key_width + macro_width), area.y + area.height.saturating_sub(1), disp.get(disp.len().saturating_sub(key_width as usize)..) .unwrap_or(&disp), - cx.editor.theme.get("ui.text"), + style, ); + if let Some((reg, _)) = cx.editor.macro_recording { + let disp = format!("[{}]", reg); + let style = style + .fg(helix_view::graphics::Color::Yellow) + .add_modifier(Modifier::BOLD); + surface.set_string( + area.x + area.width.saturating_sub(3), + area.y + area.height.saturating_sub(1), + &disp, + style, + ); + } } if let Some(completion) = self.completion.as_mut() { @@ -1188,11 +1246,11 @@ impl Component for EditorView { } fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { - // match view.doc.mode() { - // Mode::Insert => write!(stdout, "\x1B[6 q"), - // mode => write!(stdout, "\x1B[2 q"), - // }; - editor.cursor() + match editor.cursor() { + // All block cursors are drawn manually + (pos, CursorKind::Block) => (pos, CursorKind::Hidden), + cursor => cursor, + } } } |