summaryrefslogtreecommitdiff
path: root/helix-term/src/ui/editor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui/editor.rs')
-rw-r--r--helix-term/src/ui/editor.rs292
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,
+ }
}
}