Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/statusline.rs')
| -rw-r--r-- | helix-term/src/ui/statusline.rs | 562 |
1 files changed, 219 insertions, 343 deletions
diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88c75fe1..8a87242f 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -1,11 +1,9 @@ -use helix_core::indent::IndentStyle; -use helix_core::{coords_at_pos, encoding, unicode::width::UnicodeWidthStr, Position}; +use helix_core::{coords_at_pos, encoding, Position}; use helix_lsp::lsp::DiagnosticSeverity; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, graphics::Rect, - theme::Style, Document, Editor, View, }; @@ -21,7 +19,6 @@ pub struct RenderContext<'a> { pub view: &'a View, pub focused: bool, pub spinners: &'a ProgressSpinners, - pub parts: RenderBuffer<'a>, } impl<'a> RenderContext<'a> { @@ -38,18 +35,10 @@ impl<'a> RenderContext<'a> { 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") @@ -59,76 +48,87 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface surface.set_style(viewport.with_height(1), base_style); - // Left side of the status line. - - let config = context.editor.config(); - - for element_id in &config.statusline.left { - let render = get_render_function(*element_id); - (render)(context, |context, span| { - append(&mut context.parts.left, span, base_style) - }); - } + let statusline = render_statusline(context, viewport.width as usize); surface.set_spans( viewport.x, viewport.y, - &context.parts.left, - context.parts.left.width() as u16, - ); - - // Right side of the status line. - - for element_id in &config.statusline.right { - let render = get_render_function(*element_id); - (render)(context, |context, span| { - append(&mut context.parts.right, span, base_style) - }) - } - - 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, + &statusline, + statusline.width() as u16, ); +} - // Center of the status line. +pub fn render_statusline<'a>(context: &mut RenderContext, width: usize) -> Spans<'a> { + let config = context.editor.config(); - for element_id in &config.statusline.center { - let render = get_render_function(*element_id); - (render)(context, |context, span| { - append(&mut context.parts.center, span, base_style) - }) + let element_ids = &config.statusline.left; + let mut left = element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .flat_map(|render| render(context).0) + .collect::<Vec<Span>>(); + + let element_ids = &config.statusline.center; + let mut center = element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .flat_map(|render| render(context).0) + .collect::<Vec<Span>>(); + + let element_ids = &config.statusline.right; + let mut right = element_ids + .iter() + .map(|element_id| get_render_function(*element_id)) + .flat_map(|render| render(context).0) + .collect::<Vec<Span>>(); + + let left_area_width: usize = left.iter().map(|s| s.width()).sum(); + let center_area_width: usize = center.iter().map(|s| s.width()).sum(); + let right_area_width: usize = right.iter().map(|s| s.width()).sum(); + + let min_spacing_between_areas = 1usize; + let sides_space_required = left_area_width + right_area_width + min_spacing_between_areas; + let total_space_required = sides_space_required + center_area_width + min_spacing_between_areas; + + let mut statusline: Vec<Span> = vec![]; + + if center_area_width > 0 && total_space_required <= width { + // SAFETY: this subtraction cannot underflow because `left_area_width + center_area_width + right_area_width` + // is smaller than `total_space_required`, which is smaller than `width` in this branch. + let total_spacers = width - (left_area_width + center_area_width + right_area_width); + // This is how much padding space it would take on either side to align the center area to the middle. + let center_margin = (width - center_area_width) / 2; + let left_spacers = if left_area_width < center_margin && right_area_width < center_margin { + // Align the center area to the middle if there is enough space on both sides. + center_margin - left_area_width + } else { + // Otherwise split the available space evenly and use it as margin. + // The center element won't be aligned to the middle but it will be evenly + // spaced between the left and right areas. + total_spacers / 2 + }; + let right_spacers = total_spacers - left_spacers; + + statusline.append(&mut left); + statusline.push(" ".repeat(left_spacers).into()); + statusline.append(&mut center); + statusline.push(" ".repeat(right_spacers).into()); + statusline.append(&mut right); + } else if right_area_width > 0 && sides_space_required <= width { + let side_areas_width = left_area_width + right_area_width; + statusline.append(&mut left); + statusline.push(" ".repeat(width - side_areas_width).into()); + statusline.append(&mut right); + } else if left_area_width <= width { + statusline.append(&mut left); } - // 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, - ); + statusline.into() } -fn append<'a>(buffer: &mut Spans<'a>, mut span: Span<'a>, base_style: Style) { - span.style = base_style.patch(span.style); - buffer.0.push(span); -} - -fn get_render_function<'a, F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext<'a>, F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn get_render_function<'a>( + element_id: StatusLineElementID, +) -> impl Fn(&RenderContext) -> Spans<'a> { match element_id { helix_view::editor::StatusLineElement::Mode => render_mode, helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner, @@ -141,7 +141,6 @@ where helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator, helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding, helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending, - helix_view::editor::StatusLineElement::FileIndentStyle => render_file_indent_style, helix_view::editor::StatusLineElement::FileType => render_file_type, helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics, helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics, @@ -156,48 +155,43 @@ where helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::VersionControl => render_version_control, helix_view::editor::StatusLineElement::Register => render_register, - helix_view::editor::StatusLineElement::CurrentWorkingDirectory => render_cwd, } } -fn render_mode<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_mode<'a>(context: &RenderContext) -> Spans<'a> { let visible = context.focused; let config = context.editor.config(); let modenames = &config.statusline.mode; - let mode_str = match context.editor.mode() { - Mode::Insert => &modenames.insert, - Mode::Select => &modenames.select, - Mode::Normal => &modenames.normal, - }; - let content = if visible { - format!(" {mode_str} ") - } else { - // If not focused, explicitly leave an empty space instead of returning None. - " ".repeat(mode_str.width() + 2) - }; - let style = if visible && config.color_modes { + let modename = if visible { match context.editor.mode() { - Mode::Insert => context.editor.theme.get("ui.statusline.insert"), - Mode::Select => context.editor.theme.get("ui.statusline.select"), - Mode::Normal => context.editor.theme.get("ui.statusline.normal"), + Mode::Insert => modenames.insert.clone(), + Mode::Select => modenames.select.clone(), + Mode::Normal => modenames.normal.clone(), } } else { - Style::default() + // If not focused, explicitly leave an empty space. + " ".into() }; - write(context, Span::styled(content, style)); + let modename = format!(" {} ", modename); + if visible && config.color_modes { + Span::styled( + modename, + match context.editor.mode() { + Mode::Insert => context.editor.theme.get("ui.statusline.insert"), + Mode::Select => context.editor.theme.get("ui.statusline.select"), + Mode::Normal => context.editor.theme.get("ui.statusline.normal"), + }, + ) + .into() + } else { + Span::raw(modename).into() + } } // TODO think about handling multiple language servers -fn render_lsp_spinner<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_lsp_spinner<'a>(context: &RenderContext) -> Spans<'a> { let language_server = context.doc.language_servers().next(); - write( - context, + Span::raw( language_server .and_then(|srv| { context @@ -207,151 +201,106 @@ where }) // Even if there's no spinner; reserve its space to avoid elements frequently shifting. .unwrap_or(" ") - .into(), - ); + .to_string(), + ) + .into() } -fn render_diagnostics<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - use helix_core::diagnostic::Severity; - let (hints, info, warnings, errors) = +fn render_diagnostics<'a>(context: &RenderContext) -> Spans<'a> { + 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 + }); + + let mut output = Spans::default(); + + if warnings > 0 { + output.0.push(Span::styled( + "●".to_string(), + context.editor.theme.get("warning"), + )); + output.0.push(Span::raw(format!(" {} ", warnings))); + } + + if errors > 0 { + output.0.push(Span::styled( + "●".to_string(), + context.editor.theme.get("error"), + )); + output.0.push(Span::raw(format!(" {} ", errors))); + } + + output +} + +fn render_workspace_diagnostics<'a>(context: &RenderContext) -> Spans<'a> { + let (warnings, errors) = context - .doc - .diagnostics() - .iter() - .fold((0, 0, 0, 0), |mut counts, diag| { + .editor + .diagnostics + .values() + .flatten() + .fold((0, 0), |mut counts, (diag, _)| { match diag.severity { - Some(Severity::Hint) | None => counts.0 += 1, - Some(Severity::Info) => counts.1 += 1, - Some(Severity::Warning) => counts.2 += 1, - Some(Severity::Error) => counts.3 += 1, + Some(DiagnosticSeverity::WARNING) => counts.0 += 1, + Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, + _ => {} } counts }); - for sev in &context.editor.config().statusline.diagnostics { - match sev { - Severity::Hint if hints > 0 => { - write(context, Span::styled("●", context.editor.theme.get("hint"))); - write(context, format!(" {} ", hints).into()); - } - Severity::Info if info > 0 => { - write(context, Span::styled("●", context.editor.theme.get("info"))); - write(context, format!(" {} ", info).into()); - } - Severity::Warning if warnings > 0 => { - write( - context, - Span::styled("●", context.editor.theme.get("warning")), - ); - write(context, format!(" {} ", warnings).into()); - } - Severity::Error if errors > 0 => { - write( - context, - Span::styled("●", context.editor.theme.get("error")), - ); - write(context, format!(" {} ", errors).into()); - } - _ => {} - } - } -} + let mut output = Spans::default(); -fn render_workspace_diagnostics<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - use helix_core::diagnostic::Severity; - let (hints, info, warnings, errors) = context.editor.diagnostics.values().flatten().fold( - (0u32, 0u32, 0u32, 0u32), - |mut counts, (diag, _)| { - match diag.severity { - // PERF: For large workspace diagnostics, this loop can be very tight. - // - // Most often the diagnostics will be for warnings and errors. - // Errors should tend to be fixed fast, leaving warnings as the most common. - Some(DiagnosticSeverity::WARNING) => counts.2 += 1, - Some(DiagnosticSeverity::ERROR) => counts.3 += 1, - Some(DiagnosticSeverity::HINT) => counts.0 += 1, - Some(DiagnosticSeverity::INFORMATION) => counts.1 += 1, - // Fallback to `hint`. - _ => counts.0 += 1, - } - counts - }, - ); - - let sevs_to_show = &context.editor.config().statusline.workspace_diagnostics; - - // Avoid showing the " W " if no diagnostic counts will be shown. - if !sevs_to_show.iter().any(|sev| match sev { - Severity::Hint => hints != 0, - Severity::Info => info != 0, - Severity::Warning => warnings != 0, - Severity::Error => errors != 0, - }) { - return; + if warnings > 0 || errors > 0 { + output.0.push(Span::raw(" W ")); } - write(context, " W ".into()); + if warnings > 0 { + output.0.push(Span::styled( + "●".to_string(), + context.editor.theme.get("warning"), + )); + output.0.push(Span::raw(format!(" {} ", warnings))); + } - for sev in sevs_to_show { - match sev { - Severity::Hint if hints > 0 => { - write(context, Span::styled("●", context.editor.theme.get("hint"))); - write(context, format!(" {} ", hints).into()); - } - Severity::Info if info > 0 => { - write(context, Span::styled("●", context.editor.theme.get("info"))); - write(context, format!(" {} ", info).into()); - } - Severity::Warning if warnings > 0 => { - write( - context, - Span::styled("●", context.editor.theme.get("warning")), - ); - write(context, format!(" {} ", warnings).into()); - } - Severity::Error if errors > 0 => { - write( - context, - Span::styled("●", context.editor.theme.get("error")), - ); - write(context, format!(" {} ", errors).into()); - } - _ => {} - } + if errors > 0 { + output.0.push(Span::styled( + "●".to_string(), + context.editor.theme.get("error"), + )); + output.0.push(Span::raw(format!(" {} ", errors))); } + + output } -fn render_selections<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let selection = context.doc.selection(context.view.id); - let count = selection.len(); - write( - context, - if count == 1 { - " 1 sel ".into() - } else { - format!(" {}/{count} sels ", selection.primary_index() + 1).into() - }, - ); +fn render_selections<'a>(context: &RenderContext) -> Spans<'a> { + let count = context.doc.selection(context.view.id).len(); + Span::raw(format!( + " {} sel{} ", + count, + if count == 1 { "" } else { "s" } + )) + .into() } -fn render_primary_selection_length<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_primary_selection_length<'a>(context: &RenderContext) -> Spans<'a> { let tot_sel = context.doc.selection(context.view.id).primary().len(); - write( - context, - format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }).into(), - ); + Span::raw(format!( + " {} char{} ", + tot_sel, + if tot_sel == 1 { "" } else { "s" } + )) + .into() } fn get_position(context: &RenderContext) -> Position { @@ -365,53 +314,33 @@ fn get_position(context: &RenderContext) -> Position { ) } -fn render_position<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_position<'a>(context: &RenderContext) -> Spans<'a> { let position = get_position(context); - write( - context, - format!(" {}:{} ", position.row + 1, position.col + 1).into(), - ); + Span::raw(format!(" {}:{} ", position.row + 1, position.col + 1)).into() } -fn render_total_line_numbers<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_total_line_numbers<'a>(context: &RenderContext) -> Spans<'a> { let total_line_numbers = context.doc.text().len_lines(); - - write(context, format!(" {} ", total_line_numbers).into()); + Span::raw(format!(" {} ", total_line_numbers)).into() } -fn render_position_percentage<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_position_percentage<'a>(context: &RenderContext) -> Spans<'a> { let position = get_position(context); let maxrows = context.doc.text().len_lines(); - write( - context, - format!("{}%", (position.row + 1) * 100 / maxrows).into(), - ); + Span::raw(format!("{}%", (position.row + 1) * 100 / maxrows)).into() } -fn render_file_encoding<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_file_encoding<'a>(context: &RenderContext) -> Spans<'a> { let enc = context.doc.encoding(); if enc != encoding::UTF_8 { - write(context, format!(" {} ", enc.name()).into()); + Span::raw(format!(" {} ", enc.name())).into() + } else { + Spans::default() } } -fn render_file_line_ending<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_file_line_ending<'a>(context: &RenderContext) -> Spans<'a> { use helix_core::LineEnding::*; let line_ending = match context.doc.line_ending { Crlf => "CRLF", @@ -430,22 +359,16 @@ where PS => "PS", // U+2029 -- ParagraphSeparator }; - write(context, format!(" {} ", line_ending).into()); + Span::raw(format!(" {} ", line_ending)).into() } -fn render_file_type<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_file_type<'a>(context: &RenderContext) -> Spans<'a> { let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); - write(context, format!(" {} ", file_type).into()); + Span::raw(format!(" {} ", file_type)).into() } -fn render_file_name<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_file_name<'a>(context: &RenderContext) -> Spans<'a> { let title = { let rel_path = context.doc.relative_path(); let path = rel_path @@ -455,13 +378,10 @@ where format!(" {} ", path) }; - write(context, title.into()); + Span::raw(title).into() } -fn render_file_absolute_path<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_file_absolute_path<'a>(context: &RenderContext) -> Spans<'a> { let title = { let path = context.doc.path(); let path = path @@ -471,38 +391,31 @@ where format!(" {} ", path) }; - write(context, title.into()); + Span::raw(title).into() } -fn render_file_modification_indicator<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let title = if context.doc.is_modified() { +fn render_file_modification_indicator<'a>(context: &RenderContext) -> Spans<'a> { + let title = (if context.doc.is_modified() { "[+]" } else { " " - }; + }) + .to_string(); - write(context, title.into()); + Span::raw(title).into() } -fn render_read_only_indicator<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_read_only_indicator<'a>(context: &RenderContext) -> Spans<'a> { let title = if context.doc.readonly { " [readonly] " } else { "" - }; - write(context, title.into()); + } + .to_string(); + Span::raw(title).into() } -fn render_file_base_name<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_file_base_name<'a>(context: &RenderContext) -> Spans<'a> { let title = { let rel_path = context.doc.relative_path(); let path = rel_path @@ -512,74 +425,37 @@ where format!(" {} ", path) }; - write(context, title.into()); + Span::raw(title).into() } -fn render_separator<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_separator<'a>(context: &RenderContext) -> Spans<'a> { let sep = &context.editor.config().statusline.separator; - let style = context.editor.theme.get("ui.statusline.separator"); - write(context, Span::styled(sep.to_string(), style)); + Span::styled( + sep.to_string(), + context.editor.theme.get("ui.statusline.separator"), + ) + .into() } -fn render_spacer<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - write(context, " ".into()); +fn render_spacer<'a>(_context: &RenderContext) -> Spans<'a> { + Span::raw(" ").into() } -fn render_version_control<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_version_control<'a>(context: &RenderContext) -> Spans<'a> { let head = context .doc .version_control_head() .unwrap_or_default() .to_string(); - write(context, head.into()); + Span::raw(head).into() } -fn render_register<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ +fn render_register<'a>(context: &RenderContext) -> Spans<'a> { if let Some(reg) = context.editor.selected_register { - write(context, format!(" reg={} ", reg).into()) + Span::raw(format!(" reg={} ", reg)).into() + } else { + Spans::default() } } - -fn render_file_indent_style<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let style = context.doc.indent_style; - - write( - context, - match style { - IndentStyle::Tabs => " tabs ".into(), - IndentStyle::Spaces(indent) => { - format!(" {} space{} ", indent, if indent == 1 { "" } else { "s" }).into() - } - }, - ); -} - -fn render_cwd<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let cwd = helix_stdx::env::current_working_dir(); - let cwd = cwd - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - write(context, cwd.into()) -} |