aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui/statusline.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui/statusline.rs')
-rw-r--r--helix-term/src/ui/statusline.rs310
1 files changed, 310 insertions, 0 deletions
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<Style>) {
+ buffer.0.push(Span::styled(
+ text,
+ style.map_or(*base_style, |s| (*base_style).patch(s)),
+ ));
+}
+
+fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ match element_id {
+ helix_view::editor::StatusLineElement::Mode => render_mode,
+ helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
+ helix_view::editor::StatusLineElement::FileName => render_file_name,
+ helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
+ helix_view::editor::StatusLineElement::FileType => render_file_type,
+ helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
+ helix_view::editor::StatusLineElement::Selections => render_selections,
+ helix_view::editor::StatusLineElement::Position => render_position,
+ }
+}
+
+fn render_mode<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let visible = context.focused;
+
+ write(
+ context,
+ format!(
+ " {} ",
+ if visible {
+ match context.doc.mode() {
+ Mode::Insert => "INS",
+ Mode::Select => "SEL",
+ Mode::Normal => "NOR",
+ }
+ } else {
+ // If not focused, explicitly leave an empty space instead of returning None.
+ " "
+ }
+ ),
+ if visible && context.editor.config().color_modes {
+ match context.doc.mode() {
+ Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
+ Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
+ Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
+ }
+ } else {
+ None
+ },
+ );
+}
+
+fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ write(
+ context,
+ context
+ .doc
+ .language_server()
+ .and_then(|srv| {
+ context
+ .spinners
+ .get(srv.id())
+ .and_then(|spinner| spinner.frame())
+ })
+ // Even if there's no spinner; reserve its space to avoid elements frequently shifting.
+ .unwrap_or(" ")
+ .to_string(),
+ None,
+ );
+}
+
+fn render_diagnostics<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let (warnings, errors) = context
+ .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
+ });
+
+ if warnings > 0 {
+ write(
+ context,
+ "●".to_string(),
+ Some(context.editor.theme.get("warning")),
+ );
+ write(context, format!(" {} ", warnings), None);
+ }
+
+ if errors > 0 {
+ write(
+ context,
+ "●".to_string(),
+ Some(context.editor.theme.get("error")),
+ );
+ write(context, format!(" {} ", errors), None);
+ }
+}
+
+fn render_selections<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let count = context.doc.selection(context.view.id).len();
+ write(
+ context,
+ format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
+ None,
+ );
+}
+
+fn render_position<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let position = coords_at_pos(
+ context.doc.text().slice(..),
+ context
+ .doc
+ .selection(context.view.id)
+ .primary()
+ .cursor(context.doc.text().slice(..)),
+ );
+
+ write(
+ context,
+ format!(" {}:{} ", position.row + 1, position.col + 1),
+ None,
+ );
+}
+
+fn render_file_encoding<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let enc = context.doc.encoding();
+
+ if enc != encoding::UTF_8 {
+ write(context, format!(" {} ", enc.name()), None);
+ }
+}
+
+fn render_file_type<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let file_type = context.doc.language_id().unwrap_or("text");
+
+ write(context, format!(" {} ", file_type), None);
+}
+
+fn render_file_name<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let title = {
+ let rel_path = context.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 context.doc.is_modified() { "[+]" } else { "" }
+ )
+ };
+
+ write(context, title, None);
+}