diff --git a/book/src/configuration.md b/book/src/configuration.md index 0a6e5fdd0..4c849f262 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -48,13 +48,43 @@ hidden = false | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | +### `[editor.statusline]` Section + +Allows configuring the statusline at the bottom of the editor. + +The configuration distinguishes between three areas of the status line: + +`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]` + +Statusline elements can be defined as follows: + +```toml +[editor.statusline] +left = ["mode", "spinner"] +center = ["file-name"] +right = ["diagnostics", "selections", "position", "file-encoding", "file-type"] +``` + +The following elements can be configured: + +| Key | Description | +| ------ | ----------- | +| `mode` | The current editor mode (`NOR`/`INS`/`SEL`) | +| `spinner` | A progress spinner indicating LSP activity | +| `file-name` | The path/name of the opened file | +| `file-encoding` | The encoding of the opened file if it differs from UTF-8 | +| `file-type` | The type of the opened file | +| `diagnostics` | The number of warnings and/or errors | +| `selections` | The number of active selections | +| `position` | The cursor position | + ### `[editor.lsp]` Section | Key | Description | Default | | --- | ----------- | ------- | | `display-messages` | Display LSP progress messages below statusline[^1] | `false` | -[^1]: A progress spinner is always shown in the statusline beside the file path. +[^1]: By default, a progress spinner is shown in the statusline beside the file path. ### `[editor.cursor-shape]` Section diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a7c67a219..9b8bf8eb5 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 ca4cedb55..c7d409e96 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 000000000..895043cd1 --- /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