Skip to content

Commit b558388

Browse files
dgkfShafkath Shuhan
authored andcommitted
Add configuration for min width of line-numbers gutter (helix-editor#4724)
1 parent 784116d commit b558388

File tree

5 files changed

+310
-92
lines changed

5 files changed

+310
-92
lines changed

book/src/configuration.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,55 @@ render = true
256256
character = "" # Some characters that work well: "▏", "┆", "┊", "⸽"
257257
skip-levels = 1
258258
```
259+
260+
### `[editor.gutters]` Section
261+
262+
For simplicity, `editor.gutters` accepts an array of gutter types, which will
263+
use default settings for all gutter components.
264+
265+
```toml
266+
[editor]
267+
gutters = ["diff", "diagnostics", "line-numbers", "spacer"]
268+
```
269+
270+
To customize the behavior of gutters, the `[editor.gutters]` section must
271+
be used. This section contains top level settings, as well as settings for
272+
specific gutter components as sub-sections.
273+
274+
| Key | Description | Default |
275+
| --- | --- | --- |
276+
| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
277+
278+
Example:
279+
280+
```toml
281+
[editor.gutters]
282+
layout = ["diff", "diagnostics", "line-numbers", "spacer"]
283+
```
284+
285+
#### `[editor.gutters.line-numbers]` Section
286+
287+
Options for the line number gutter
288+
289+
| Key | Description | Default |
290+
| --- | --- | --- |
291+
| `min-width` | The minimum number of characters to use | `3` |
292+
293+
Example:
294+
295+
```toml
296+
[editor.gutters.line-numbers]
297+
min-width = 1
298+
```
299+
300+
#### `[editor.gutters.diagnotics]` Section
301+
302+
Currently unused
303+
304+
#### `[editor.gutters.diff]` Section
305+
306+
Currently unused
307+
308+
#### `[editor.gutters.spacer]` Section
309+
310+
Currently unused

helix-view/src/editor.rs

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,96 @@ where
7171
)
7272
}
7373

74+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75+
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
76+
pub struct GutterConfig {
77+
/// Gutter Layout
78+
pub layout: Vec<GutterType>,
79+
/// Options specific to the "line-numbers" gutter
80+
pub line_numbers: GutterLineNumbersConfig,
81+
}
82+
83+
impl Default for GutterConfig {
84+
fn default() -> Self {
85+
Self {
86+
layout: vec![
87+
GutterType::Diagnostics,
88+
GutterType::Spacer,
89+
GutterType::LineNumbers,
90+
GutterType::Spacer,
91+
GutterType::Diff,
92+
],
93+
line_numbers: GutterLineNumbersConfig::default(),
94+
}
95+
}
96+
}
97+
98+
impl From<Vec<GutterType>> for GutterConfig {
99+
fn from(x: Vec<GutterType>) -> Self {
100+
GutterConfig {
101+
layout: x,
102+
..Default::default()
103+
}
104+
}
105+
}
106+
107+
fn deserialize_gutter_seq_or_struct<'de, D>(deserializer: D) -> Result<GutterConfig, D::Error>
108+
where
109+
D: Deserializer<'de>,
110+
{
111+
struct GutterVisitor;
112+
113+
impl<'de> serde::de::Visitor<'de> for GutterVisitor {
114+
type Value = GutterConfig;
115+
116+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
117+
write!(
118+
formatter,
119+
"an array of gutter names or a detailed gutter configuration"
120+
)
121+
}
122+
123+
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
124+
where
125+
S: serde::de::SeqAccess<'de>,
126+
{
127+
let mut gutters = Vec::new();
128+
while let Some(gutter) = seq.next_element::<&str>()? {
129+
gutters.push(
130+
gutter
131+
.parse::<GutterType>()
132+
.map_err(serde::de::Error::custom)?,
133+
)
134+
}
135+
136+
Ok(gutters.into())
137+
}
138+
139+
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
140+
where
141+
M: serde::de::MapAccess<'de>,
142+
{
143+
let deserializer = serde::de::value::MapAccessDeserializer::new(map);
144+
Deserialize::deserialize(deserializer)
145+
}
146+
}
147+
148+
deserializer.deserialize_any(GutterVisitor)
149+
}
150+
151+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152+
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
153+
pub struct GutterLineNumbersConfig {
154+
/// Minimum number of characters to use for line number gutter. Defaults to 3.
155+
pub min_width: usize,
156+
}
157+
158+
impl Default for GutterLineNumbersConfig {
159+
fn default() -> Self {
160+
Self { min_width: 3 }
161+
}
162+
}
163+
74164
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75165
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
76166
pub struct FilePickerConfig {
@@ -132,8 +222,8 @@ pub struct Config {
132222
pub cursorline: bool,
133223
/// Highlight the columns cursors are currently on. Defaults to false.
134224
pub cursorcolumn: bool,
135-
/// Gutters. Default ["diagnostics", "line-numbers"]
136-
pub gutters: Vec<GutterType>,
225+
#[serde(deserialize_with = "deserialize_gutter_seq_or_struct")]
226+
pub gutters: GutterConfig,
137227
/// Middle click paste support. Defaults to true.
138228
pub middle_click_paste: bool,
139229
/// Automatic insertion of pairs to parentheses, brackets,
@@ -606,13 +696,7 @@ impl Default for Config {
606696
line_number: LineNumber::Absolute,
607697
cursorline: false,
608698
cursorcolumn: false,
609-
gutters: vec![
610-
GutterType::Diagnostics,
611-
GutterType::Spacer,
612-
GutterType::LineNumbers,
613-
GutterType::Spacer,
614-
GutterType::Diff,
615-
],
699+
gutters: GutterConfig::default(),
616700
middle_click_paste: true,
617701
auto_pairs: AutoPairConfig::default(),
618702
auto_completion: true,
@@ -844,6 +928,7 @@ impl Editor {
844928
let config = self.config();
845929
self.auto_pairs = (&config.auto_pairs).into();
846930
self.reset_idle_timer();
931+
self._refresh();
847932
}
848933

849934
pub fn clear_idle_timer(&mut self) {
@@ -984,6 +1069,7 @@ impl Editor {
9841069
for (view, _) in self.tree.views_mut() {
9851070
let doc = doc_mut!(self, &view.doc);
9861071
view.sync_changes(doc);
1072+
view.gutters = config.gutters.clone();
9871073
view.ensure_cursor_in_view(doc, config.scrolloff)
9881074
}
9891075
}

helix-view/src/gutter.rs

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ impl GutterType {
3535
}
3636
}
3737

38-
pub fn width(self, _view: &View, doc: &Document) -> usize {
38+
pub fn width(self, view: &View, doc: &Document) -> usize {
3939
match self {
4040
GutterType::Diagnostics => 1,
41-
GutterType::LineNumbers => line_numbers_width(_view, doc),
41+
GutterType::LineNumbers => line_numbers_width(view, doc),
4242
GutterType::Spacer => 1,
4343
GutterType::Diff => 1,
4444
}
@@ -140,12 +140,13 @@ pub fn line_numbers<'doc>(
140140
is_focused: bool,
141141
) -> GutterFn<'doc> {
142142
let text = doc.text().slice(..);
143-
let last_line = view.last_line(doc);
144-
let width = GutterType::LineNumbers.width(view, doc);
143+
let width = line_numbers_width(view, doc);
144+
145+
let last_line_in_view = view.last_line(doc);
145146

146147
// Whether to draw the line number for the last line of the
147148
// document or not. We only draw it if it's not an empty line.
148-
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
149+
let draw_last = text.line_to_byte(last_line_in_view) < text.len_bytes();
149150

150151
let linenr = theme.get("ui.linenr");
151152
let linenr_select = theme.get("ui.linenr.selected");
@@ -158,7 +159,7 @@ pub fn line_numbers<'doc>(
158159
let mode = editor.mode;
159160

160161
Box::new(move |line: usize, selected: bool, out: &mut String| {
161-
if line == last_line && !draw_last {
162+
if line == last_line_in_view && !draw_last {
162163
write!(out, "{:>1$}", '~', width).unwrap();
163164
Some(linenr)
164165
} else {
@@ -187,14 +188,19 @@ pub fn line_numbers<'doc>(
187188
})
188189
}
189190

190-
pub fn line_numbers_width(_view: &View, doc: &Document) -> usize {
191+
/// The width of a "line-numbers" gutter
192+
///
193+
/// The width of the gutter depends on the number of lines in the document,
194+
/// whether there is content on the last line (the `~` line), and the
195+
/// `editor.gutters.line-numbers.min-width` settings.
196+
fn line_numbers_width(view: &View, doc: &Document) -> usize {
191197
let text = doc.text();
192198
let last_line = text.len_lines().saturating_sub(1);
193199
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
194200
let last_drawn = if draw_last { last_line + 1 } else { last_line };
195-
196-
// set a lower bound to 2-chars to minimize ambiguous relative line numbers
197-
std::cmp::max(count_digits(last_drawn), 2)
201+
let digits = count_digits(last_drawn);
202+
let n_min = view.gutters.line_numbers.min_width;
203+
digits.max(n_min)
198204
}
199205

200206
pub fn padding<'doc>(
@@ -282,3 +288,82 @@ pub fn diagnostics_or_breakpoints<'doc>(
282288
breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
283289
})
284290
}
291+
292+
#[cfg(test)]
293+
mod tests {
294+
use super::*;
295+
use crate::document::Document;
296+
use crate::editor::{GutterConfig, GutterLineNumbersConfig};
297+
use crate::graphics::Rect;
298+
use crate::DocumentId;
299+
use helix_core::Rope;
300+
301+
#[test]
302+
fn test_default_gutter_widths() {
303+
let mut view = View::new(DocumentId::default(), GutterConfig::default());
304+
view.area = Rect::new(40, 40, 40, 40);
305+
306+
let rope = Rope::from_str("abc\n\tdef");
307+
let doc = Document::from(rope, None);
308+
309+
assert_eq!(view.gutters.layout.len(), 5);
310+
assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
311+
assert_eq!(view.gutters.layout[1].width(&view, &doc), 1);
312+
assert_eq!(view.gutters.layout[2].width(&view, &doc), 3);
313+
assert_eq!(view.gutters.layout[3].width(&view, &doc), 1);
314+
assert_eq!(view.gutters.layout[4].width(&view, &doc), 1);
315+
}
316+
317+
#[test]
318+
fn test_configured_gutter_widths() {
319+
let gutters = GutterConfig {
320+
layout: vec![GutterType::Diagnostics],
321+
..Default::default()
322+
};
323+
324+
let mut view = View::new(DocumentId::default(), gutters);
325+
view.area = Rect::new(40, 40, 40, 40);
326+
327+
let rope = Rope::from_str("abc\n\tdef");
328+
let doc = Document::from(rope, None);
329+
330+
assert_eq!(view.gutters.layout.len(), 1);
331+
assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
332+
333+
let gutters = GutterConfig {
334+
layout: vec![GutterType::Diagnostics, GutterType::LineNumbers],
335+
line_numbers: GutterLineNumbersConfig { min_width: 10 },
336+
};
337+
338+
let mut view = View::new(DocumentId::default(), gutters);
339+
view.area = Rect::new(40, 40, 40, 40);
340+
341+
let rope = Rope::from_str("abc\n\tdef");
342+
let doc = Document::from(rope, None);
343+
344+
assert_eq!(view.gutters.layout.len(), 2);
345+
assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
346+
assert_eq!(view.gutters.layout[1].width(&view, &doc), 10);
347+
}
348+
349+
#[test]
350+
fn test_line_numbers_gutter_width_resizes() {
351+
let gutters = GutterConfig {
352+
layout: vec![GutterType::Diagnostics, GutterType::LineNumbers],
353+
line_numbers: GutterLineNumbersConfig { min_width: 1 },
354+
};
355+
356+
let mut view = View::new(DocumentId::default(), gutters);
357+
view.area = Rect::new(40, 40, 40, 40);
358+
359+
let rope = Rope::from_str("a\nb");
360+
let doc_short = Document::from(rope, None);
361+
362+
let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np");
363+
let doc_long = Document::from(rope, None);
364+
365+
assert_eq!(view.gutters.layout.len(), 2);
366+
assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1);
367+
assert_eq!(view.gutters.layout[1].width(&view, &doc_long), 2);
368+
}
369+
}

0 commit comments

Comments
 (0)