From dbf68e0370981dc4ad0fa74596b57347f7048fab Mon Sep 17 00:00:00 2001 From: Mr. E Date: Mon, 18 Jul 2022 02:57:01 +0200 Subject: Customizable/configurable status line (#2434) * feat(statusline): add the file type (language id) to the status line * refactor(statusline): move the statusline implementation into an own struct * refactor(statusline): split the statusline implementation into different functions * refactor(statusline): Append elements using a consistent API This is a preparation for the configurability which is about to be implemented. * refactor(statusline): implement render_diagnostics() This avoid cluttering the render() function and will simplify configurability. * feat(statusline): make the status line configurable * refactor(statusline): make clippy happy * refactor(statusline): avoid intermediate StatusLineObject Use a more functional approach to obtain render functions and write to the buffers, and avoid an intermediate StatusLineElement object. * fix(statusline): avoid rendering the left elements twice * refactor(statusline): make clippy happy again * refactor(statusline): rename `buffer` into `parts` * refactor(statusline): ensure the match is exhaustive * fix(statusline): avoid an overflow when calculating the maximal center width * chore(statusline): Describe the statusline configurability in the book * chore(statusline): Correct and add documentation * refactor(statusline): refactor some code following the code review Avoid very small helper functions for the diagnositcs and inline them instead. Rename the config field `status_line` to `statusline` to remain consistent with `bufferline`. * chore(statusline): adjust documentation following the config field refactoring * revert(statusline): revert regression introduced by c0a1870 * chore(statusline): slight adjustment in the configuration documentation * feat(statusline): integrate changes from #2676 after rebasing * refactor(statusline): remove the StatusLine struct Because none of the functions need `Self` and all of them are in an own file, there is no explicit need for the struct. * fix(statusline): restore the configurability of color modes The configuration was ignored after reintegrating the changes of #2676 in 8d28f95. * fix(statusline): remove the spinner padding * refactor(statusline): remove unnecessary format!()--- helix-term/src/ui/editor.rs | 163 ++------------------- helix-term/src/ui/mod.rs | 1 + helix-term/src/ui/statusline.rs | 310 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 155 deletions(-) create mode 100644 helix-term/src/ui/statusline.rs (limited to 'helix-term') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a7c67a21..9b8bf8eb 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -7,7 +7,6 @@ use crate::{ }; use helix_core::{ - coords_at_pos, encoding, graphemes::{ ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary, }, @@ -17,7 +16,7 @@ use helix_core::{ LineEnding, Position, Range, Selection, Transaction, }; use helix_view::{ - document::{Mode, SCRATCH_BUFFER_NAME}, + document::Mode, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::KeyEvent, @@ -29,6 +28,8 @@ use std::borrow::Cow; use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; use tui::buffer::Buffer as Surface; +use super::statusline; + pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option>, @@ -161,7 +162,11 @@ impl EditorView { .area .clip_top(view.area.height.saturating_sub(1)) .clip_bottom(1); // -1 from bottom to remove commandline - self.render_statusline(editor, doc, view, statusline_area, surface, is_focused); + + let mut context = + statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners); + + statusline::render(&mut context, statusline_area, surface); } pub fn render_rulers( @@ -730,158 +735,6 @@ impl EditorView { } } - pub fn render_statusline( - &self, - editor: &Editor, - doc: &Document, - view: &View, - viewport: Rect, - surface: &mut Surface, - is_focused: bool, - ) { - use tui::text::{Span, Spans}; - - //------------------------------- - // Left side of the status line. - //------------------------------- - - let theme = &editor.theme; - let (mode, mode_style) = match doc.mode() { - Mode::Insert => (" INS ", theme.get("ui.statusline.insert")), - Mode::Select => (" SEL ", theme.get("ui.statusline.select")), - Mode::Normal => (" NOR ", theme.get("ui.statusline.normal")), - }; - let progress = doc - .language_server() - .and_then(|srv| { - self.spinners - .get(srv.id()) - .and_then(|spinner| spinner.frame()) - }) - .unwrap_or(""); - - let base_style = if is_focused { - theme.get("ui.statusline") - } else { - theme.get("ui.statusline.inactive") - }; - // statusline - surface.set_style(viewport.with_height(1), base_style); - if is_focused { - let color_modes = editor.config().color_modes; - surface.set_string( - viewport.x, - viewport.y, - mode, - if color_modes { mode_style } else { base_style }, - ); - } - surface.set_string(viewport.x + 5, viewport.y, progress, base_style); - - //------------------------------- - // Right side of the status line. - //------------------------------- - - let mut right_side_text = Spans::default(); - - // Compute the individual info strings and add them to `right_side_text`. - - // Diagnostics - let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| { - use helix_core::diagnostic::Severity; - match diag.severity { - Some(Severity::Warning) => counts.0 += 1, - Some(Severity::Error) | None => counts.1 += 1, - _ => {} - } - counts - }); - let (warnings, errors) = diags; - let warning_style = theme.get("warning"); - let error_style = theme.get("error"); - for i in 0..2 { - let (count, style) = match i { - 0 => (warnings, warning_style), - 1 => (errors, error_style), - _ => unreachable!(), - }; - if count == 0 { - continue; - } - let style = base_style.patch(style); - right_side_text.0.push(Span::styled("●", style)); - right_side_text - .0 - .push(Span::styled(format!(" {} ", count), base_style)); - } - - // Selections - let sels_count = doc.selection(view.id).len(); - right_side_text.0.push(Span::styled( - format!( - " {} sel{} ", - sels_count, - if sels_count == 1 { "" } else { "s" } - ), - base_style, - )); - - // Position - let pos = coords_at_pos( - doc.text().slice(..), - doc.selection(view.id) - .primary() - .cursor(doc.text().slice(..)), - ); - right_side_text.0.push(Span::styled( - format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing. - 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 - + viewport - .width - .saturating_sub(right_side_text.width() as u16), - viewport.y, - &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 /// if event was handled (a command was executed or a subkeymap was /// activated). Only KeymapResult::{NotFound, Cancelled} is returned diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ca4cedb5..c7d409e9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -8,6 +8,7 @@ mod picker; mod popup; mod prompt; mod spinner; +mod statusline; mod text; pub use completion::Completion; diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs new file mode 100644 index 00000000..895043cd --- /dev/null +++ b/helix-term/src/ui/statusline.rs @@ -0,0 +1,310 @@ +use helix_core::{coords_at_pos, encoding}; +use helix_view::{ + document::{Mode, SCRATCH_BUFFER_NAME}, + graphics::Rect, + theme::Style, + Document, Editor, View, +}; + +use crate::ui::ProgressSpinners; + +use helix_view::editor::StatusLineElement as StatusLineElementID; +use tui::buffer::Buffer as Surface; +use tui::text::{Span, Spans}; + +pub struct RenderContext<'a> { + pub editor: &'a Editor, + pub doc: &'a Document, + pub view: &'a View, + pub focused: bool, + pub spinners: &'a ProgressSpinners, + pub parts: RenderBuffer<'a>, +} + +impl<'a> RenderContext<'a> { + pub fn new( + editor: &'a Editor, + doc: &'a Document, + view: &'a View, + focused: bool, + spinners: &'a ProgressSpinners, + ) -> Self { + RenderContext { + editor, + doc, + view, + focused, + spinners, + parts: RenderBuffer::default(), + } + } +} + +#[derive(Default)] +pub struct RenderBuffer<'a> { + pub left: Spans<'a>, + pub center: Spans<'a>, + pub right: Spans<'a>, +} + +pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) { + let base_style = if context.focused { + context.editor.theme.get("ui.statusline") + } else { + context.editor.theme.get("ui.statusline.inactive") + }; + + surface.set_style(viewport.with_height(1), base_style); + + let write_left = |context: &mut RenderContext, text, style| { + append(&mut context.parts.left, text, &base_style, style) + }; + let write_center = |context: &mut RenderContext, text, style| { + append(&mut context.parts.center, text, &base_style, style) + }; + let write_right = |context: &mut RenderContext, text, style| { + append(&mut context.parts.right, text, &base_style, style) + }; + + // Left side of the status line. + + let element_ids = &context.editor.config().statusline.left; + element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .for_each(|render| render(context, write_left)); + + surface.set_spans( + viewport.x, + viewport.y, + &context.parts.left, + context.parts.left.width() as u16, + ); + + // Right side of the status line. + + let element_ids = &context.editor.config().statusline.right; + element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .for_each(|render| render(context, write_right)); + + surface.set_spans( + viewport.x + + viewport + .width + .saturating_sub(context.parts.right.width() as u16), + viewport.y, + &context.parts.right, + context.parts.right.width() as u16, + ); + + // Center of the status line. + + let element_ids = &context.editor.config().statusline.center; + element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .for_each(|render| render(context, write_center)); + + // Width of the empty space between the left and center area and between the center and right area. + let spacing = 1u16; + + let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16; + let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing); + let center_width = center_max_width.min(context.parts.center.width() as u16); + + surface.set_spans( + viewport.x + viewport.width / 2 - center_width / 2, + viewport.y, + &context.parts.center, + center_width, + ); +} + +fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option