Skip to content

Commit

Permalink
Customizable/configurable status line (#2434)
Browse files Browse the repository at this point in the history
* 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 #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 #2676
in 8d28f95.

* fix(statusline): remove the spinner padding

* refactor(statusline): remove unnecessary format!()
  • Loading branch information
usagi-flow authored Jul 18, 2022
1 parent 43761d4 commit dbf68e0
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 156 deletions.
32 changes: 31 additions & 1 deletion book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
163 changes: 8 additions & 155 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
Expand All @@ -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<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod picker;
mod popup;
mod prompt;
mod spinner;
mod statusline;
mod text;

pub use completion::Completion;
Expand Down
Loading

0 comments on commit dbf68e0

Please sign in to comment.