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 | 490 |
1 files changed, 176 insertions, 314 deletions
diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88c75fe1..501faea3 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -1,7 +1,5 @@ -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, @@ -59,16 +57,25 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface 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 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 element_ids = &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, @@ -79,12 +86,11 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface // 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) - }) - } + let element_ids = &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 @@ -98,12 +104,11 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface // Center of the status line. - 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.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; @@ -120,28 +125,23 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface ); } -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 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<'a, F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext<'a>, F) +fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + 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::FileBaseName => render_file_base_name, helix_view::editor::StatusLineElement::FileName => render_file_name, - helix_view::editor::StatusLineElement::FileAbsolutePath => render_file_absolute_path, - helix_view::editor::StatusLineElement::FileModificationIndicator => { - render_file_modification_indicator - } - 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, @@ -154,51 +154,52 @@ where helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers, helix_view::editor::StatusLineElement::Separator => render_separator, 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) +fn render_mode<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { 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 { - 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"), - } - } else { - Style::default() - }; - write(context, Span::styled(content, style)); + write( + context, + format!( + " {} ", + if visible { + match context.editor.mode() { + Mode::Insert => &modenames.insert, + Mode::Select => &modenames.select, + Mode::Normal => &modenames.normal, + } + } else { + // If not focused, explicitly leave an empty space instead of returning None. + " " + } + ), + if visible && config.color_modes { + match context.editor.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 + }, + ); } -// TODO think about handling multiple language servers -fn render_lsp_spinner<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_lsp_spinner<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { - let language_server = context.doc.language_servers().next(); write( context, - language_server + context + .doc + .language_server() .and_then(|srv| { context .spinners @@ -207,150 +208,111 @@ where }) // Even if there's no spinner; reserve its space to avoid elements frequently shifting. .unwrap_or(" ") - .into(), + .to_string(), + None, ); } -fn render_diagnostics<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_diagnostics<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { - use helix_core::diagnostic::Severity; - let (hints, info, warnings, errors) = - context - .doc - .diagnostics() - .iter() - .fold((0, 0, 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, - } - 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 (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_workspace_diagnostics<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_workspace_diagnostics<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + 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; + let (warnings, errors) = + context + .editor + .diagnostics + .values() + .flatten() + .fold((0, 0), |mut counts, diag| { + match diag.severity { + Some(DiagnosticSeverity::WARNING) => counts.0 += 1, + Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, + _ => {} + } + counts + }); - // 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 { + write(context, format!(" {} ", "W"), None); } - write(context, " W ".into()); + if warnings > 0 { + write( + context, + "●".to_string(), + Some(context.editor.theme.get("warning")), + ); + write(context, format!(" {} ", warnings), None); + } - 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 { + write( + context, + "●".to_string(), + Some(context.editor.theme.get("error")), + ); + write(context, format!(" {} ", errors), None); } } -fn render_selections<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_selections<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { - let selection = context.doc.selection(context.view.id); - let count = selection.len(); + let count = context.doc.selection(context.view.id).len(); write( context, - if count == 1 { - " 1 sel ".into() - } else { - format!(" {}/{count} sels ", selection.primary_index() + 1).into() - }, + format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }), + None, ); } -fn render_primary_selection_length<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_primary_selection_length<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let tot_sel = context.doc.selection(context.view.id).primary().len(); write( context, - format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }).into(), + format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }), + None, ); } @@ -365,52 +327,54 @@ fn get_position(context: &RenderContext) -> Position { ) } -fn render_position<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_position<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let position = get_position(context); write( context, - format!(" {}:{} ", position.row + 1, position.col + 1).into(), + format!(" {}:{} ", position.row + 1, position.col + 1), + None, ); } -fn render_total_line_numbers<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_total_line_numbers<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let total_line_numbers = context.doc.text().len_lines(); - write(context, format!(" {} ", total_line_numbers).into()); + write(context, format!(" {} ", total_line_numbers), None); } -fn render_position_percentage<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_position_percentage<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let position = get_position(context); let maxrows = context.doc.text().len_lines(); write( context, - format!("{}%", (position.row + 1) * 100 / maxrows).into(), + format!("{}%", (position.row + 1) * 100 / maxrows), + None, ); } -fn render_file_encoding<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_file_encoding<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let enc = context.doc.encoding(); if enc != encoding::UTF_8 { - write(context, format!(" {} ", enc.name()).into()); + write(context, format!(" {} ", enc.name()), None); } } -fn render_file_line_ending<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_file_line_ending<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { use helix_core::LineEnding::*; let line_ending = match context.doc.line_ending { @@ -430,21 +394,21 @@ where PS => "PS", // U+2029 -- ParagraphSeparator }; - write(context, format!(" {} ", line_ending).into()); + write(context, format!(" {} ", line_ending), None); } -fn render_file_type<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_file_type<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { - let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); + let file_type = context.doc.language_name().unwrap_or("text"); - write(context, format!(" {} ", file_type).into()); + write(context, format!(" {} ", file_type), None); } -fn render_file_name<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_file_name<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let title = { let rel_path = context.doc.relative_path(); @@ -452,134 +416,32 @@ where .as_ref() .map(|p| p.to_string_lossy()) .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); - format!(" {} ", path) - }; - - write(context, title.into()); -} - -fn render_file_absolute_path<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let title = { - let path = context.doc.path(); - let path = path - .as_ref() - .map(|p| p.to_string_lossy()) - .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); - format!(" {} ", path) - }; - - write(context, 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() { - "[+]" - } else { - " " + format!( + " {}{} ", + path, + if context.doc.is_modified() { "[+]" } else { "" } + ) }; - write(context, title.into()); + write(context, title, None); } -fn render_read_only_indicator<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_separator<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let title = if context.doc.readonly { - " [readonly] " - } else { - "" - }; - write(context, title.into()); -} - -fn render_file_base_name<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let title = { - let rel_path = context.doc.relative_path(); - let path = rel_path - .as_ref() - .and_then(|p| p.file_name().map(|s| s.to_string_lossy())) - .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); - format!(" {} ", path) - }; - - write(context, title.into()); -} - -fn render_separator<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let sep = &context.editor.config().statusline.separator; - let style = context.editor.theme.get("ui.statusline.separator"); - - write(context, Span::styled(sep.to_string(), style)); -} - -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_version_control<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - let head = context - .doc - .version_control_head() - .unwrap_or_default() - .to_string(); - - write(context, head.into()); -} - -fn render_register<'a, F>(context: &mut RenderContext<'a>, write: F) -where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, -{ - if let Some(reg) = context.editor.selected_register { - write(context, format!(" reg={} ", reg).into()) - } -} - -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() - } - }, + sep.to_string(), + Some(context.editor.theme.get("ui.statusline.separator")), ); } -fn render_cwd<'a, F>(context: &mut RenderContext<'a>, write: F) +fn render_spacer<F>(context: &mut RenderContext, write: F) where - F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy, + F: Fn(&mut RenderContext, String, Option<Style>) + 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()) + write(context, String::from(" "), None); } |