aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/completion.rs6
-rw-r--r--helix-term/src/ui/editor.rs140
-rw-r--r--helix-term/src/ui/markdown.rs6
-rw-r--r--helix-term/src/ui/menu.rs4
-rw-r--r--helix-term/src/ui/mod.rs1
-rw-r--r--helix-term/src/ui/picker.rs14
-rw-r--r--helix-term/src/ui/popup.rs29
-rw-r--r--helix-term/src/ui/prompt.rs4
8 files changed, 98 insertions, 106 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index dd782d29..a55201ff 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -168,7 +168,7 @@ impl Completion {
}
};
});
- let popup = Popup::new(menu);
+ let popup = Popup::new("completion", menu);
let mut completion = Self {
popup,
start_offset,
@@ -328,8 +328,8 @@ impl Component for Completion {
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
- width = rel_width;
- height = rel_height;
+ width = rel_width.min(width);
+ height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index e8f8fd9b..7d57e581 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -17,7 +17,6 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
- editor::LineNumber,
graphics::{CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
@@ -32,7 +31,7 @@ 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>,
@@ -49,7 +48,7 @@ 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,
@@ -310,17 +309,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,
@@ -351,6 +349,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,
@@ -417,22 +419,6 @@ impl EditorView {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
- let linenr = theme.get("ui.linenr");
- let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
-
- let warning = theme.get("warning");
- let error = theme.get("error");
- let info = theme.get("info");
- let hint = theme.get("hint");
-
- // Whether to draw the line number for the last line of the
- // document or not. We only draw it if it's not an empty line.
- let draw_last = text.line_to_byte(last_line) < text.len_bytes();
-
- let current_line = doc
- .text()
- .char_to_line(doc.selection(view.id).primary().cursor(text));
-
// it's used inside an iterator so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)]
@@ -442,51 +428,31 @@ impl EditorView {
.map(|range| range.cursor_line(text))
.collect();
- for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
- use helix_core::diagnostic::Severity;
- if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
- surface.set_stringn(
- viewport.x,
- viewport.y + i as u16,
- "●",
- 1,
- match diagnostic.severity {
- Some(Severity::Error) => error,
- Some(Severity::Warning) | None => warning,
- Some(Severity::Info) => info,
- Some(Severity::Hint) => hint,
- },
- );
- }
+ let mut offset = 0;
- let selected = cursors.contains(&line);
+ let gutter_style = theme.get("ui.gutter");
- let text = if line == last_line && !draw_last {
- " ~".into()
- } else {
- let line = match config.line_number {
- LineNumber::Absolute => line + 1,
- LineNumber::Relative => {
- if current_line == line {
- line + 1
- } else {
- abs_diff(current_line, line)
- }
- }
- };
- format!("{:>5}", line)
- };
- surface.set_stringn(
- viewport.x + 1,
- viewport.y + i as u16,
- text,
- 5,
- if selected && is_focused {
- linenr_select
- } else {
- linenr
- },
- );
+ // avoid lots of small allocations by reusing a text buffer for each line
+ let mut text = String::with_capacity(8);
+
+ for (constructor, width) in view.gutters() {
+ let gutter = constructor(doc, view, theme, config, is_focused, *width);
+ text.reserve(*width); // ensure there's enough space for the gutter
+ for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
+ let selected = cursors.contains(&line);
+
+ if let Some(style) = gutter(line, selected, &mut text) {
+ surface.set_stringn(
+ viewport.x + offset,
+ viewport.y + i as u16,
+ &text,
+ *width,
+ gutter_style.patch(style),
+ );
+ }
+ text.clear();
+ }
+ offset += *width as u16;
}
}
@@ -916,7 +882,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)
}
@@ -934,7 +900,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);
}
@@ -948,7 +915,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);
}
@@ -963,7 +930,7 @@ 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,
+ editor: cx.editor,
count: None,
register: None,
callback: None,
@@ -1140,13 +1107,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() {
@@ -1172,12 +1157,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT)
}
}
-
-#[inline]
-const fn abs_diff(a: usize, b: usize) -> usize {
- if a > b {
- a - b
- } else {
- b - a
- }
-}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 649703b5..46657fb9 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -228,6 +228,7 @@ impl Component for Markdown {
return None;
}
let contents = parse(&self.contents, None, &self.config_loader);
+ // TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
@@ -240,11 +241,6 @@ impl Component for Markdown {
} else if content_width > text_width {
text_width = content_width;
}
-
- if height >= viewport.1 {
- height = viewport.1;
- break;
- }
}
Some((text_width + padding, height))
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index e891c149..69053db3 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -190,7 +190,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -202,7 +202,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn;
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index cdf42311..f57e2e2b 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -186,6 +186,7 @@ pub mod completers {
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
+ names.push("base16_default".into());
let mut names: Vec<_> = names
.into_iter()
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 6b1c5832..1ef94df0 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -46,7 +46,7 @@ pub struct FilePicker<T> {
}
pub enum CachedPreview {
- Document(Document),
+ Document(Box<Document>),
Binary,
LargeFile,
NotFound,
@@ -140,7 +140,7 @@ impl<T> FilePicker<T> {
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, Some(&editor.theme), None)
- .map(CachedPreview::Document)
+ .map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
@@ -404,13 +404,13 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.last_picker = compositor.pop();
})));
match key_event.into() {
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
}
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@@ -421,19 +421,19 @@ impl<T: 'static> Component for Picker<T> {
}
key!(Enter) => {
if let Some(option) = self.selection() {
- (self.callback_fn)(&mut cx.editor, option, Action::Replace);
+ (self.callback_fn)(cx.editor, option, Action::Replace);
}
return close_fn;
}
ctrl!('s') => {
if let Some(option) = self.selection() {
- (self.callback_fn)(&mut cx.editor, option, Action::HorizontalSplit);
+ (self.callback_fn)(cx.editor, option, Action::HorizontalSplit);
}
return close_fn;
}
ctrl!('v') => {
if let Some(option) = self.selection() {
- (self.callback_fn)(&mut cx.editor, option, Action::VerticalSplit);
+ (self.callback_fn)(cx.editor, option, Action::VerticalSplit);
}
return close_fn;
}
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 8f7921a1..bf7510a2 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -15,16 +15,20 @@ pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
size: (u16, u16),
+ child_size: (u16, u16),
scroll: usize,
+ id: &'static str,
}
impl<T: Component> Popup<T> {
- pub fn new(contents: T) -> Self {
+ pub fn new(id: &'static str, contents: T) -> Self {
Self {
contents,
position: None,
size: (0, 0),
+ child_size: (0, 0),
scroll: 0,
+ id,
}
}
@@ -68,6 +72,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
+
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = (self.scroll + offset).min(max_offset as usize);
} else {
self.scroll = self.scroll.saturating_sub(offset);
}
@@ -93,7 +100,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -115,13 +122,21 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
- fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+ let max_width = 120.min(viewport.0);
+ let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
let (width, height) = self
.contents
- .required_size((120, 26)) // max width, max height
+ .required_size((max_width, max_height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
- self.size = (width, height);
+ self.child_size = (width, height);
+ self.size = (width.min(max_width), height.min(max_height));
+
+ // re-clamp scroll offset
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = self.scroll.min(max_offset as usize);
Some(self.size)
}
@@ -143,4 +158,8 @@ impl<T: Component> Component for Popup<T> {
self.contents.render(area, surface, cx);
}
+
+ fn id(&self) -> Option<&'static str> {
+ Some(self.id)
+ }
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index e90b0772..07e1b33c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
- shift!(BackTab) => {
+ shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}