on master help sign in
Make gutters configurable (#1967)
b04c425Dr. David A. Kunz
book/src/configuration.md+1
@@ -37,6 +37,7 @@ hidden = false
37| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
37| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
38| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
38| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
39| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
39| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
40+| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers`, note that `diagnostics` also includes other features like breakpoints | `["diagnostics", "line-numbers"]` |
40| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
41| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
41| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
42| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
42| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
43| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
helix-term/src/ui/editor.rs+1−1
@@ -466,7 +466,7 @@ impl EditorView {
466 // avoid lots of small allocations by reusing a text buffer for each line
466 // avoid lots of small allocations by reusing a text buffer for each line
467 let mut text = String::with_capacity(8);
467 let mut text = String::with_capacity(8);
468
468
469− for (constructor, width) in view.gutters() {
469+ for (constructor, width) in &view.gutters {
470 let gutter = constructor(editor, doc, view, theme, is_focused, *width);
470 let gutter = constructor(editor, doc, view, theme, is_focused, *width);
471 text.reserve(*width); // ensure there's enough space for the gutter
471 text.reserve(*width); // ensure there's enough space for the gutter
472 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
472 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
helix-view/src/editor.rs+26−2
@@ -117,6 +117,8 @@ pub struct Config {
117 pub shell: Vec<String>,
117 pub shell: Vec<String>,
118 /// Line number mode.
118 /// Line number mode.
119 pub line_number: LineNumber,
119 pub line_number: LineNumber,
120+ /// Gutters. Default ["diagnostics", "line-numbers"]
121+ pub gutters: Vec<GutterType>,
120 /// Middle click paste support. Defaults to true.
122 /// Middle click paste support. Defaults to true.
121 pub middle_click_paste: bool,
123 pub middle_click_paste: bool,
122 /// Automatic insertion of pairs to parentheses, brackets,
124 /// Automatic insertion of pairs to parentheses, brackets,
@@ -238,6 +240,27 @@ impl std::str::FromStr for LineNumber {
238 }
240 }
239}
241}
240
242
243+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
244+#[serde(rename_all = "kebab-case")]
245+pub enum GutterType {
246+ /// Show diagnostics and other features like breakpoints
247+ Diagnostics,
248+ /// Show line numbers
249+ LineNumbers,
250+}
251+
252+impl std::str::FromStr for GutterType {
253+ type Err = anyhow::Error;
254+
255+ fn from_str(s: &str) -> Result<Self, Self::Err> {
256+ match s.to_lowercase().as_str() {
257+ "diagnostics" => Ok(Self::Diagnostics),
258+ "line-numbers" => Ok(Self::LineNumbers),
259+ _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
260+ }
261+ }
262+}
263+
241impl Default for Config {
264impl Default for Config {
242 fn default() -> Self {
265 fn default() -> Self {
243 Self {
266 Self {
@@ -250,6 +273,7 @@ impl Default for Config {
250 vec!["sh".to_owned(), "-c".to_owned()]
273 vec!["sh".to_owned(), "-c".to_owned()]
251 },
274 },
252 line_number: LineNumber::Absolute,
275 line_number: LineNumber::Absolute,
276+ gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
253 middle_click_paste: true,
277 middle_click_paste: true,
254 auto_pairs: AutoPairConfig::default(),
278 auto_pairs: AutoPairConfig::default(),
255 auto_completion: true,
279 auto_completion: true,
@@ -579,7 +603,7 @@ impl Editor {
579 return;
603 return;
580 }
604 }
581 Action::HorizontalSplit | Action::VerticalSplit => {
605 Action::HorizontalSplit | Action::VerticalSplit => {
582− let view = View::new(id);
606+ let view = View::new(id, self.config().gutters.clone());
583 let view_id = self.tree.split(
607 let view_id = self.tree.split(
584 view,
608 view,
585 match action {
609 match action {
@@ -701,7 +725,7 @@ impl Editor {
701 .map(|(&doc_id, _)| doc_id)
725 .map(|(&doc_id, _)| doc_id)
702 .next()
726 .next()
703 .unwrap_or_else(|| self.new_document(Document::default()));
727 .unwrap_or_else(|| self.new_document(Document::default()));
704− let view = View::new(doc_id);
728+ let view = View::new(doc_id, self.config().gutters.clone());
705 let view_id = self.tree.insert(view);
729 let view_id = self.tree.insert(view);
706 let doc = self.documents.get_mut(&doc_id).unwrap();
730 let doc = self.documents.get_mut(&doc_id).unwrap();
707 doc.selections.insert(view_id, Selection::point(0));
731 doc.selections.insert(view_id, Selection::point(0));
helix-view/src/gutter.rs+1−1
@@ -39,7 +39,7 @@ pub fn diagnostic<'doc>(
39 })
39 })
40}
40}
41
41
42−pub fn line_number<'doc>(
42+pub fn line_numbers<'doc>(
43 editor: &'doc Editor,
43 editor: &'doc Editor,
44 doc: &'doc Document,
44 doc: &'doc Document,
45 view: &View,
45 view: &View,
helix-view/src/tree.rs+17−4
@@ -568,6 +568,7 @@ impl<'a> Iterator for Traverse<'a> {
568#[cfg(test)]
568#[cfg(test)]
569mod test {
569mod test {
570 use super::*;
570 use super::*;
571+ use crate::editor::GutterType;
571 use crate::DocumentId;
572 use crate::DocumentId;
572
573
573 #[test]
574 #[test]
@@ -578,22 +579,34 @@ mod test {
578 width: 180,
579 width: 180,
579 height: 80,
580 height: 80,
580 });
581 });
581− let mut view = View::new(DocumentId::default());
582+ let mut view = View::new(
583+ DocumentId::default(),
584+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
585+ );
582 view.area = Rect::new(0, 0, 180, 80);
586 view.area = Rect::new(0, 0, 180, 80);
583 tree.insert(view);
587 tree.insert(view);
584
588
585 let l0 = tree.focus;
589 let l0 = tree.focus;
590+ let view = View::new(
586− let view = View::new(DocumentId::default());
591+ DocumentId::default(),
592+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
593+ );
587 tree.split(view, Layout::Vertical);
594 tree.split(view, Layout::Vertical);
588 let r0 = tree.focus;
595 let r0 = tree.focus;
589
596
590 tree.focus = l0;
597 tree.focus = l0;
598+ let view = View::new(
591− let view = View::new(DocumentId::default());
599+ DocumentId::default(),
600+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
601+ );
592 tree.split(view, Layout::Horizontal);
602 tree.split(view, Layout::Horizontal);
593 let l1 = tree.focus;
603 let l1 = tree.focus;
594
604
595 tree.focus = l0;
605 tree.focus = l0;
606+ let view = View::new(
596− let view = View::new(DocumentId::default());
607+ DocumentId::default(),
608+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
609+ );
597 tree.split(view, Layout::Vertical);
610 tree.split(view, Layout::Vertical);
598 let l2 = tree.focus;
611 let l2 = tree.focus;
599
612
helix-view/src/view.rs+64−17
@@ -11,6 +11,8 @@ use helix_core::{
11 visual_coords_at_pos, Position, RopeSlice, Selection,
11 visual_coords_at_pos, Position, RopeSlice, Selection,
12};
12};
13
13
14+use std::fmt;
15+
14type Jump = (DocumentId, Selection);
16type Jump = (DocumentId, Selection);
15
17
16#[derive(Debug, Clone)]
18#[derive(Debug, Clone)]
@@ -64,17 +66,11 @@ impl JumpList {
64 }
66 }
65}
67}
66
68
67−const GUTTERS: &[(Gutter, usize)] = &[
68− (gutter::diagnostics_or_breakpoints, 1),
69− (gutter::line_number, 5),
70−];
71−
72−#[derive(Debug)]
73pub struct View {
69pub struct View {
74 pub id: ViewId,
70 pub id: ViewId,
75− pub doc: DocumentId,
76 pub offset: Position,
71 pub offset: Position,
77 pub area: Rect,
72 pub area: Rect,
73+ pub doc: DocumentId,
78 pub jumps: JumpList,
74 pub jumps: JumpList,
79 /// the last accessed file before the current one
75 /// the last accessed file before the current one
80 pub last_accessed_doc: Option<DocumentId>,
76 pub last_accessed_doc: Option<DocumentId>,
@@ -85,10 +81,29 @@ pub struct View {
85 pub last_modified_docs: [Option<DocumentId>; 2],
81 pub last_modified_docs: [Option<DocumentId>; 2],
86 /// used to store previous selections of tree-sitter objecs
82 /// used to store previous selections of tree-sitter objecs
87 pub object_selections: Vec<Selection>,
83 pub object_selections: Vec<Selection>,
84+ pub gutters: Vec<(Gutter, usize)>,
85+}
86+
87+impl fmt::Debug for View {
88+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89+ f.debug_struct("View")
90+ .field("id", &self.id)
91+ .field("area", &self.area)
92+ .field("doc", &self.doc)
93+ .finish()
94+ }
88}
95}
89
96
90impl View {
97impl View {
91− pub fn new(doc: DocumentId) -> Self {
98+ pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
99+ let mut gutters: Vec<(Gutter, usize)> = vec![];
100+ use crate::editor::GutterType;
101+ for gutter_type in &gutter_types {
102+ match gutter_type {
103+ GutterType::Diagnostics => gutters.push((gutter::diagnostics_or_breakpoints, 1)),
104+ GutterType::LineNumbers => gutters.push((gutter::line_numbers, 5)),
105+ }
106+ }
92 Self {
107 Self {
93 id: ViewId::default(),
108 id: ViewId::default(),
94 doc,
109 doc,
@@ -98,17 +113,14 @@ impl View {
98 last_accessed_doc: None,
113 last_accessed_doc: None,
99 last_modified_docs: [None, None],
114 last_modified_docs: [None, None],
100 object_selections: Vec::new(),
115 object_selections: Vec::new(),
116+ gutters,
101 }
117 }
102 }
118 }
103
119
104− pub fn gutters(&self) -> &[(Gutter, usize)] {
105− GUTTERS
106− }
107−
108 pub fn inner_area(&self) -> Rect {
120 pub fn inner_area(&self) -> Rect {
109 // TODO: cache this
121 // TODO: cache this
110 let offset = self
122 let offset = self
111− .gutters()
123+ .gutters
112 .iter()
124 .iter()
113 .map(|(_, width)| *width as u16)
125 .map(|(_, width)| *width as u16)
114 .sum::<u16>()
126 .sum::<u16>()
@@ -324,11 +336,16 @@ mod tests {
324 use super::*;
336 use super::*;
325 use helix_core::Rope;
337 use helix_core::Rope;
326 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
338 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
339+ const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
327− // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
340+ // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
341+ use crate::editor::GutterType;
328
342
329 #[test]
343 #[test]
330 fn test_text_pos_at_screen_coords() {
344 fn test_text_pos_at_screen_coords() {
331− let mut view = View::new(DocumentId::default());
345+ let mut view = View::new(
346+ DocumentId::default(),
347+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
348+ );
332 view.area = Rect::new(40, 40, 40, 40);
349 view.area = Rect::new(40, 40, 40, 40);
333 let rope = Rope::from_str("abc\n\tdef");
350 let rope = Rope::from_str("abc\n\tdef");
334 let text = rope.slice(..);
351 let text = rope.slice(..);
@@ -372,9 +389,36 @@ mod tests {
372 assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
389 assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
373 }
390 }
374
391
392+ #[test]
393+ fn test_text_pos_at_screen_coords_without_line_numbers_gutter() {
394+ let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
395+ view.area = Rect::new(40, 40, 40, 40);
396+ let rope = Rope::from_str("abc\n\tdef");
397+ let text = rope.slice(..);
398+ assert_eq!(
399+ view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
400+ Some(4)
401+ );
402+ }
403+
404+ #[test]
405+ fn test_text_pos_at_screen_coords_without_any_gutters() {
406+ let mut view = View::new(DocumentId::default(), vec![]);
407+ view.area = Rect::new(40, 40, 40, 40);
408+ let rope = Rope::from_str("abc\n\tdef");
409+ let text = rope.slice(..);
410+ assert_eq!(
411+ view.text_pos_at_screen_coords(&text, 41, 40 + 1, 4),
412+ Some(4)
413+ );
414+ }
415+
375 #[test]
416 #[test]
376 fn test_text_pos_at_screen_coords_cjk() {
417 fn test_text_pos_at_screen_coords_cjk() {
377− let mut view = View::new(DocumentId::default());
418+ let mut view = View::new(
419+ DocumentId::default(),
420+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
421+ );
378 view.area = Rect::new(40, 40, 40, 40);
422 view.area = Rect::new(40, 40, 40, 40);
379 let rope = Rope::from_str("Hi! こんにちは皆さん");
423 let rope = Rope::from_str("Hi! こんにちは皆さん");
380 let text = rope.slice(..);
424 let text = rope.slice(..);
@@ -411,7 +455,10 @@ mod tests {
411
455
412 #[test]
456 #[test]
413 fn test_text_pos_at_screen_coords_graphemes() {
457 fn test_text_pos_at_screen_coords_graphemes() {
414− let mut view = View::new(DocumentId::default());
458+ let mut view = View::new(
459+ DocumentId::default(),
460+ vec![GutterType::Diagnostics, GutterType::LineNumbers],
461+ );
415 view.area = Rect::new(40, 40, 40, 40);
462 view.area = Rect::new(40, 40, 40, 40);
416 let rope = Rope::from_str("Hèl̀l̀ò world!");
463 let rope = Rope::from_str("Hèl̀l̀ò world!");
417 let text = rope.slice(..);
464 let text = rope.slice(..);