diff --git a/TODO.md b/TODO.md index ca1120c..291daf4 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,9 @@ - [x] add `C` and `D` for deleting line from current cursor position (and entering insert mode: `C`); add `Y` for copying line from current cursor position; add `r` for replacing character at cursor position; add `s` for deleting character at current cursor position and entering insert mode; add `dw` for deleting whole words; add `db` for deleting backwards (same as `dw` but backwards). - [x] diff view: make foreground of etched areas (where no lines exist) the same dimmed color as the normal gutter text - [x] copying is pretty slow; optimize performance to reduce latency. -- [x] enable margins in bar elements (i.e. for top and bottom bars) and give the lsp symbol (next to language) a default margin of 1 column on either side. +- [x] enable margins in top/bottom bar elements; give the bottom bar's far-right position indicator a default leading margin of 1 column and no trailing margin. +- [ ] better fuzzy search when searching revisions and bookmarks (or the git alternatives) in before and after picker for diff view. +- [ ] buffer picker (with search). - [ ] pull a pr description and display it in a buffer: - open a selector that allows you to pick a pr from remote - selector should be searchable diff --git a/docs/configuration.md b/docs/configuration.md index cb43959..4973a86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,8 +68,7 @@ Vamp.configure do |config| left: [:mode, :path, :diagnostics, :selections, :count, :pending, :buffer], center: [], right: [:language, :lsp, :position], mode: { padding: 1, leading_edge: "", trailing_edge: "▓░" }, - lsp: { margin: 1 }, - position: { padding: 1, leading_edge: "░▓", trailing_edge: "" } + position: { padding: 1, leading_margin: 1, trailing_margin: 0, leading_edge: "░▓", trailing_edge: "" } } config.bar.bottom = { left: [:tabs], center: [], right: [:battery], @@ -92,7 +91,7 @@ Each segment's `title` is a literal template supporting only `{index}`, `{view}` `:lsp` displays only ready servers assigned to the focused buffer, in configured stack order joined with ` + `. Disabled, initializing, failed, or unassigned servers do not appear. Names come from the initialization reply's `serverInfo.name`, falling back to the configured executable basename; no arbitrary named label can make an inactive server appear active. -`margin` adds unstyled space outside both edges of a segment, unlike the styled internal `padding`. It accepts integers from `0` through `64`, defaults to `0`, and defaults to `1` for `:lsp` in either bar. For example, `config.bar.bottom = { lsp: { margin: 0 } }` removes LSP's external spacing without changing its padding. Margins count toward anchor placement and collision handling; empty segments consume no margin, and margins yield along with padding and edges when only clipped content fits. +`margin` adds unstyled space outside both edges of a segment, unlike the styled internal `padding`. `leading_margin` and `trailing_margin` override their respective sides. All accept integers from `0` through `64`. Margins default to zero except the bottom bar's `:position`, which has one leading column and no trailing margin. LSP has no default margins. For example, `config.bar.bottom = { position: { leading_margin: 0 } }` removes position's external spacing without changing its padding. Margins count toward anchor placement and collision handling; empty segments consume no margin, and margins yield along with padding and edges when only clipped content fits. `padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64` (default `1`). `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs default to `"░▓"` / `"▓░"` edges; mode has only the trailing `"▓░"` edge, position and battery only the leading `"░▓"` edge, and other segments have none. `first_leading_edge` optionally overrides the first occurrence's leading glyphs: for tabs this is the actual first tab, not the first tab remaining after clipping; other segments have one occurrence. Tabs default this override to `""`, suppressing the first tab's left fade. Unicode widths are measured in terminal cells. Edge strings must be single-line and contain no control characters. Unknown style keys, invalid templates, and wrong types reject configuration. diff --git a/src/chrome.rs b/src/chrome.rs index 622c1c6..b8d704f 100644 --- a/src/chrome.rs +++ b/src/chrome.rs @@ -106,7 +106,7 @@ pub struct ChromeLine { pub struct SegmentConfig { pub title: String, pub padding: usize, - pub margin: usize, + pub margin: [usize; 2], pub leading_edge: String, pub trailing_edge: String, pub first_leading_edge: Option, @@ -117,7 +117,7 @@ impl Default for SegmentConfig { Self { title: String::new(), padding: 1, - margin: 0, + margin: [0; 2], leading_edge: String::new(), trailing_edge: String::new(), first_leading_edge: None, @@ -148,7 +148,13 @@ impl ChromeLine { right: vec!["language".into(), "lsp".into(), "position".into()], segments: DEFAULT_BOTTOM_SEGMENTS .iter() - .map(|name| ((*name).into(), default_segment(name))) + .map(|name| { + let mut segment = default_segment(name); + if *name == "position" { + segment.margin[0] = 1; + } + ((*name).into(), segment) + }) .collect(), } } @@ -169,7 +175,11 @@ impl ChromeLine { validate_name(name)?; } for (name, segment) in &self.segments { - for (option, value) in [("padding", segment.padding), ("margin", segment.margin)] { + for (option, value) in [ + ("padding", segment.padding), + ("leading_margin", segment.margin[0]), + ("trailing_margin", segment.margin[1]), + ] { if value > 64 { return Err(format!( "Chrome segment {name:?} {option} must be between 0 and 64" @@ -202,9 +212,6 @@ impl ChromeLine { fn default_segment(name: &str) -> SegmentConfig { let mut segment = SegmentConfig::default(); - if name == "lsp" { - segment.margin = 1; - } if matches!(name, "tabs" | "position" | "battery") { segment.leading_edge = "░▓".into(); } @@ -266,6 +273,8 @@ struct SegmentOptions { title: Option, padding: Option, margin: Option, + leading_margin: Option, + trailing_margin: Option, leading_edge: Option, trailing_edge: Option, first_leading_edge: Option, @@ -313,7 +322,13 @@ fn deserialize_line<'de, D: Deserializer<'de>>( segment.padding = padding; } if let Some(margin) = options.margin { - segment.margin = margin; + segment.margin = [margin; 2]; + } + if let Some(margin) = options.leading_margin { + segment.margin[0] = margin; + } + if let Some(margin) = options.trailing_margin { + segment.margin[1] = margin; } if let Some(edge) = options.leading_edge { segment.leading_edge = edge; diff --git a/src/ruby_config.rb b/src/ruby_config.rb index ebcc1fd..e50e8dd 100644 --- a/src/ruby_config.rb +++ b/src/ruby_config.rb @@ -35,7 +35,7 @@ module Vamp chrome_name!(name) style = chrome_hash!(value).to_h do |option, item| case option - when "padding", "margin" + when "padding", "margin", "leading_margin", "trailing_margin" unless item.is_a?(Integer) && item.between?(0, 64) raise ArgumentError, "#{option} must be an Integer between 0 and 64" end diff --git a/src/tui.rs b/src/tui.rs index 2c8b9b8..19fd84e 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1244,7 +1244,8 @@ fn chrome_items<'a>( for item in &mut items { item.width = item.text.width() + item.config.padding * 2 - + item.config.margin * 2 + + item.config.margin[0] + + item.config.margin[1] + if item.tab.is_none() || item.accent { item.leading_edge().width() + item.config.trailing_edge.width() } else { @@ -1359,11 +1360,12 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec>; 3], area: Re const PADDING: &str = " "; let padding = &PADDING[..item.config.padding]; - let margin = &PADDING[..item.config.margin]; + let leading_margin = &PADDING[..item.config.margin[0]]; + let trailing_margin = &PADDING[..item.config.margin[1]]; let edges = item.tab.is_none() || item.accent; let edge = Style::default().fg(Color::Cyan); Line::from(vec![ - Span::styled(margin, Style::reset()), + Span::styled(leading_margin, Style::reset()), Span::styled(if edges { item.leading_edge() } else { "" }, edge), Span::styled(padding, style), Span::styled(item.text.as_ref(), style), @@ -1376,7 +1378,7 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec>; 3], area: Re }, edge, ), - Span::styled(margin, Style::reset()), + Span::styled(trailing_margin, Style::reset()), ]) }; let cells = item_width.min(available); @@ -5975,6 +5977,44 @@ mod tests { } } + #[test] + fn bottom_position_has_only_a_leading_margin_and_lsp_has_none() { + let mut editor = Editor::open(None, vec![]).unwrap(); + editor.set_lsp_names(vec!["server".into()]); + configure_chrome(&mut editor, |top, bottom| { + top.left.clear(); + top.center.clear(); + top.right = vec!["position".into()]; + bottom.left.clear(); + bottom.center.clear(); + bottom.right = vec!["lsp".into(), "position".into()]; + }); + let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 2)).unwrap(); + terminal + .draw(|frame| { + for (line, y) in [ + (editor.top_bar_config(), 0), + (editor.bottom_bar_config(), 1), + ] { + let groups = chrome_groups(&editor, line, &SyntaxCache::new()); + let start = 40 - chrome_group_width(&groups[2]) as u16; + if y == 1 { + let gap = start + groups[2][0].width as u16; + draw_chrome(frame, groups, Rect::new(0, y, 40, 1)); + assert_eq!(frame.buffer_mut()[(start, y)].bg, Color::Cyan); + assert_eq!(frame.buffer_mut()[(gap, y)].symbol(), " "); + assert_eq!(frame.buffer_mut()[(gap, y)].bg, Color::Reset); + assert_eq!(frame.buffer_mut()[(gap + 1, y)].symbol(), "░"); + } else { + draw_chrome(frame, groups, Rect::new(0, y, 40, 1)); + assert_eq!(frame.buffer_mut()[(start, y)].symbol(), "░"); + } + assert_eq!(frame.buffer_mut()[(39, y)].bg, Color::Cyan); + } + }) + .unwrap(); + } + #[test] fn bar_margins_surround_styling_and_keep_anchors_and_narrow_content() { let directory = Directory::new(); @@ -5983,7 +6023,7 @@ mod tests { r#" Vamp.configure do |config| options = {left: [:badge], center: [:lsp], right: [:badge], - badge: {padding: 0, margin: 2, leading_edge: "<", trailing_edge: ">"}, lsp: {padding: 0}} + badge: {padding: 0, margin: 2, leading_edge: "<", trailing_edge: ">"}, lsp: {padding: 0, margin: 1}} config.bar.top = options config.bar.bottom = options end @@ -6036,7 +6076,7 @@ end line.center.clear(); line.right.clear(); let lsp = line.segments.get_mut("lsp").unwrap(); - lsp.margin = 64; + lsp.margin = [64; 2]; lsp.padding = 64; } }); @@ -6749,7 +6789,7 @@ end SegmentConfig { title: String::new(), padding: 0, - margin: 0, + margin: [0; 2], leading_edge: String::new(), trailing_edge: String::new(), first_leading_edge: None, @@ -6913,7 +6953,7 @@ end SegmentConfig { title: "{index}".into(), padding: 2, - margin: 0, + margin: [0; 2], leading_edge: "界▌".into(), trailing_edge: "▐界".into(), first_leading_edge: Some("<".into()), @@ -6924,7 +6964,7 @@ end SegmentConfig { title: String::new(), padding: 2, - margin: 0, + margin: [0; 2], leading_edge: "界(".into(), trailing_edge: ")界".into(), first_leading_edge: None, @@ -7021,7 +7061,7 @@ end SegmentConfig { title: "Not visible without a value".into(), padding: 64, - margin: 0, + margin: [0; 2], leading_edge: "░▒▓".into(), trailing_edge: "▓▒░".into(), first_leading_edge: None,