Skip to content

Commit 0bb920b

Browse files
usagi-flowthomasskk
authored andcommitted
Customizable/configurable status line (helix-editor#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 helix-editor#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 helix-editor#2676 in 8d28f95. * fix(statusline): remove the spinner padding * refactor(statusline): remove unnecessary format!()
1 parent bbf309d commit 0bb920b

File tree

5 files changed

+401
-156
lines changed

5 files changed

+401
-156
lines changed

book/src/configuration.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,43 @@ hidden = false
4848
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` |
4949
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
5050

51+
### `[editor.statusline]` Section
52+
53+
Allows configuring the statusline at the bottom of the editor.
54+
55+
The configuration distinguishes between three areas of the status line:
56+
57+
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
58+
59+
Statusline elements can be defined as follows:
60+
61+
```toml
62+
[editor.statusline]
63+
left = ["mode", "spinner"]
64+
center = ["file-name"]
65+
right = ["diagnostics", "selections", "position", "file-encoding", "file-type"]
66+
```
67+
68+
The following elements can be configured:
69+
70+
| Key | Description |
71+
| ------ | ----------- |
72+
| `mode` | The current editor mode (`NOR`/`INS`/`SEL`) |
73+
| `spinner` | A progress spinner indicating LSP activity |
74+
| `file-name` | The path/name of the opened file |
75+
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
76+
| `file-type` | The type of the opened file |
77+
| `diagnostics` | The number of warnings and/or errors |
78+
| `selections` | The number of active selections |
79+
| `position` | The cursor position |
80+
5181
### `[editor.lsp]` Section
5282

5383
| Key | Description | Default |
5484
| --- | ----------- | ------- |
5585
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
5686

57-
[^1]: A progress spinner is always shown in the statusline beside the file path.
87+
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
5888

5989
### `[editor.cursor-shape]` Section
6090

helix-term/src/ui/editor.rs

Lines changed: 8 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use crate::{
77
};
88

99
use helix_core::{
10-
coords_at_pos, encoding,
1110
graphemes::{
1211
ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
1312
},
@@ -17,7 +16,7 @@ use helix_core::{
1716
LineEnding, Position, Range, Selection, Transaction,
1817
};
1918
use helix_view::{
20-
document::{Mode, SCRATCH_BUFFER_NAME},
19+
document::Mode,
2120
editor::{CompleteAction, CursorShapeConfig},
2221
graphics::{Color, CursorKind, Modifier, Rect, Style},
2322
input::KeyEvent,
@@ -29,6 +28,8 @@ use std::borrow::Cow;
2928
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
3029
use tui::buffer::Buffer as Surface;
3130

31+
use super::statusline;
32+
3233
pub struct EditorView {
3334
pub keymaps: Keymaps,
3435
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
@@ -163,7 +164,11 @@ impl EditorView {
163164
.area
164165
.clip_top(view.area.height.saturating_sub(1))
165166
.clip_bottom(1); // -1 from bottom to remove commandline
166-
self.render_statusline(editor, doc, view, statusline_area, surface, is_focused);
167+
168+
let mut context =
169+
statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners);
170+
171+
statusline::render(&mut context, statusline_area, surface);
167172
}
168173

169174
pub fn render_rulers(
@@ -732,158 +737,6 @@ impl EditorView {
732737
}
733738
}
734739

735-
pub fn render_statusline(
736-
&self,
737-
editor: &Editor,
738-
doc: &Document,
739-
view: &View,
740-
viewport: Rect,
741-
surface: &mut Surface,
742-
is_focused: bool,
743-
) {
744-
use tui::text::{Span, Spans};
745-
746-
//-------------------------------
747-
// Left side of the status line.
748-
//-------------------------------
749-
750-
let theme = &editor.theme;
751-
let (mode, mode_style) = match doc.mode() {
752-
Mode::Insert => (" INS ", theme.get("ui.statusline.insert")),
753-
Mode::Select => (" SEL ", theme.get("ui.statusline.select")),
754-
Mode::Normal => (" NOR ", theme.get("ui.statusline.normal")),
755-
};
756-
let progress = doc
757-
.language_server()
758-
.and_then(|srv| {
759-
self.spinners
760-
.get(srv.id())
761-
.and_then(|spinner| spinner.frame())
762-
})
763-
.unwrap_or("");
764-
765-
let base_style = if is_focused {
766-
theme.get("ui.statusline")
767-
} else {
768-
theme.get("ui.statusline.inactive")
769-
};
770-
// statusline
771-
surface.set_style(viewport.with_height(1), base_style);
772-
if is_focused {
773-
let color_modes = editor.config().color_modes;
774-
surface.set_string(
775-
viewport.x,
776-
viewport.y,
777-
mode,
778-
if color_modes { mode_style } else { base_style },
779-
);
780-
}
781-
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
782-
783-
//-------------------------------
784-
// Right side of the status line.
785-
//-------------------------------
786-
787-
let mut right_side_text = Spans::default();
788-
789-
// Compute the individual info strings and add them to `right_side_text`.
790-
791-
// Diagnostics
792-
let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
793-
use helix_core::diagnostic::Severity;
794-
match diag.severity {
795-
Some(Severity::Warning) => counts.0 += 1,
796-
Some(Severity::Error) | None => counts.1 += 1,
797-
_ => {}
798-
}
799-
counts
800-
});
801-
let (warnings, errors) = diags;
802-
let warning_style = theme.get("warning");
803-
let error_style = theme.get("error");
804-
for i in 0..2 {
805-
let (count, style) = match i {
806-
0 => (warnings, warning_style),
807-
1 => (errors, error_style),
808-
_ => unreachable!(),
809-
};
810-
if count == 0 {
811-
continue;
812-
}
813-
let style = base_style.patch(style);
814-
right_side_text.0.push(Span::styled("●", style));
815-
right_side_text
816-
.0
817-
.push(Span::styled(format!(" {} ", count), base_style));
818-
}
819-
820-
// Selections
821-
let sels_count = doc.selection(view.id).len();
822-
right_side_text.0.push(Span::styled(
823-
format!(
824-
" {} sel{} ",
825-
sels_count,
826-
if sels_count == 1 { "" } else { "s" }
827-
),
828-
base_style,
829-
));
830-
831-
// Position
832-
let pos = coords_at_pos(
833-
doc.text().slice(..),
834-
doc.selection(view.id)
835-
.primary()
836-
.cursor(doc.text().slice(..)),
837-
);
838-
right_side_text.0.push(Span::styled(
839-
format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
840-
base_style,
841-
));
842-
843-
let enc = doc.encoding();
844-
if enc != encoding::UTF_8 {
845-
right_side_text
846-
.0
847-
.push(Span::styled(format!(" {} ", enc.name()), base_style));
848-
}
849-
850-
// Render to the statusline.
851-
surface.set_spans(
852-
viewport.x
853-
+ viewport
854-
.width
855-
.saturating_sub(right_side_text.width() as u16),
856-
viewport.y,
857-
&right_side_text,
858-
right_side_text.width() as u16,
859-
);
860-
861-
//-------------------------------
862-
// Middle / File path / Title
863-
//-------------------------------
864-
let title = {
865-
let rel_path = doc.relative_path();
866-
let path = rel_path
867-
.as_ref()
868-
.map(|p| p.to_string_lossy())
869-
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
870-
format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
871-
};
872-
873-
surface.set_string_truncated(
874-
viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
875-
viewport.y,
876-
&title,
877-
viewport
878-
.width
879-
.saturating_sub(6)
880-
.saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
881-
|_| base_style,
882-
true,
883-
true,
884-
);
885-
}
886-
887740
/// Handle events by looking them up in `self.keymaps`. Returns None
888741
/// if event was handled (a command was executed or a subkeymap was
889742
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned

helix-term/src/ui/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod picker;
99
mod popup;
1010
mod prompt;
1111
mod spinner;
12+
mod statusline;
1213
mod text;
1314
mod tree;
1415

0 commit comments

Comments
 (0)