diff --git a/book/src/configuration.md b/book/src/configuration.md index e4854cda1a707..ef7699e0ba6b7 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -57,6 +57,7 @@ on unix operating systems. | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | +| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | ### `[editor.statusline]` Section diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 33d33440cacd4..c44a6e4caafd2 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -9,7 +9,13 @@ use tui::text::{Span, Spans}; use super::{align_view, push_jump, Align, Context, Editor, Open}; use helix_core::{path, Selection}; -use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style}; +use helix_view::{ + apply_transaction, + document::Mode, + editor::{Action, PopupBorderConfig}, + graphics::Margin, + theme::Style, +}; use crate::{ compositor::{self, Compositor}, @@ -598,7 +604,20 @@ pub fn code_action(cx: &mut Context) { }); picker.move_down(); // pre-select the first item - let popup = Popup::new("code-action", picker).with_scrollbar(false); + let border_config = &editor.config().popup_border; + + let margin = if border_config == &PopupBorderConfig::All + || border_config == &PopupBorderConfig::Menu + { + Margin::vertical(1) + } else { + Margin::none() + }; + + let popup = Popup::new("code-action", picker) + .with_scrollbar(false) + .margin(margin); + compositor.replace_or_push("code-action", popup); }, ) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 229dcda18f178..284d43d275b81 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,5 +1,9 @@ use crate::compositor::{Component, Context, Event, EventResult}; -use helix_view::{apply_transaction, editor::CompleteAction}; +use helix_view::{ + apply_transaction, + editor::{CompleteAction, PopupBorderConfig}, + graphics::Margin, +}; use tui::buffer::Buffer as Surface; use tui::text::Spans; @@ -227,7 +231,21 @@ impl Completion { } }; }); - let popup = Popup::new(Self::ID, menu).with_scrollbar(false); + + let border_config = &editor.config().popup_border; + + let margin = if border_config == &PopupBorderConfig::All + || border_config == &PopupBorderConfig::Menu + { + Margin::vertical(1) + } else { + Margin::none() + }; + + let popup = Popup::new(Self::ID, menu) + .with_scrollbar(false) + .margin(margin); + let mut completion = Self { popup, start_offset, @@ -371,7 +389,7 @@ impl Component for Completion { "```{}\n{}\n```\n{}", language, option.detail.as_deref().unwrap_or_default(), - contents.clone() + contents ), cx.editor.syn_loader.clone(), ) @@ -381,15 +399,14 @@ impl Component for Completion { value: contents, })) => { // TODO: set language based on doc scope - Markdown::new( - format!( - "```{}\n{}\n```\n{}", - language, - option.detail.as_deref().unwrap_or_default(), - contents.clone() - ), - cx.editor.syn_loader.clone(), - ) + if let Some(detail) = &option.detail.as_deref() { + Markdown::new( + format!("```{}\n{}\n```\n{}", language, detail, contents), + cx.editor.syn_loader.clone(), + ) + } else { + Markdown::new(contents.to_string(), cx.editor.syn_loader.clone()) + } } None if option.detail.is_some() => { // TODO: copied from above @@ -442,6 +459,15 @@ impl Component for Completion { // clear area let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); + + let border_config = &cx.editor.config().popup_border; + + if border_config == &PopupBorderConfig::All + || border_config == &PopupBorderConfig::Popup + { + use tui::widgets::{Block, Borders, Widget}; + Widget::render(Block::default().borders(Borders::ALL), area, surface); + } markdown_doc.render(area, surface, cx); } } diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index 393d24c467bc1..13ba330b306ad 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use helix_core::syntax; +use helix_view::editor::PopupBorderConfig; use helix_view::graphics::{Margin, Rect, Style}; use tui::buffer::Buffer; use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; @@ -89,7 +90,13 @@ impl Component for SignatureHelp { Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), }; let sig_doc = sig_doc.parse(Some(&cx.editor.theme)); - let sig_doc_area = area.clip_top(sig_text_area.height + 2); + let mut sig_doc_area = area.clip_top(sig_text_area.height + 2); + + let border_config = &cx.editor.config().popup_border; + + if border_config == &PopupBorderConfig::All || border_config == &PopupBorderConfig::Popup { + sig_doc_area = sig_doc_area.clip_bottom(1); + } let sig_doc_para = Paragraph::new(sig_doc) .wrap(Wrap { trim: false }) .scroll((cx.scroll.unwrap_or_default() as u16, 0)); diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 961b7451a11bd..abc78b353c060 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -4,14 +4,18 @@ use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, text::Spans, widgets::Table}; +use tui::{ + buffer::Buffer as Surface, + text::Spans, + widgets::{Block, Borders, Table, Widget}, +}; pub use tui::widgets::{Cell, Row}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use helix_view::{graphics::Rect, Editor}; +use helix_view::{editor::PopupBorderConfig, graphics::Rect, Editor}; use tui::layout::Constraint; pub trait Item { @@ -293,7 +297,7 @@ impl Component for Menu { Some(self.size) } - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render(&mut self, mut area: Rect, surface: &mut Surface, cx: &mut Context) { let theme = &cx.editor.theme; let style = theme .try_get("ui.menu") @@ -301,6 +305,16 @@ impl Component for Menu { let selected = theme.get("ui.menu.selected"); surface.clear_with(area, style); + let border_config = &cx.editor.config().popup_border; + + let render_borders = + border_config == &PopupBorderConfig::All || border_config == &PopupBorderConfig::Menu; + + if render_borders { + Widget::render(Block::default().borders(Borders::ALL), area, surface); + area = area.clip_top(1).clip_bottom(1); + } + let scroll = self.scroll; let options: Vec<_> = self @@ -338,15 +352,17 @@ impl Component for Menu { }, ); - if let Some(cursor) = self.cursor { - let offset_from_top = cursor - scroll; - let left = &mut surface[(area.left(), area.y + offset_from_top as u16)]; - left.set_style(selected); - let right = &mut surface[( - area.right().saturating_sub(1), - area.y + offset_from_top as u16, - )]; - right.set_style(selected); + if !render_borders { + if let Some(cursor) = self.cursor { + let offset_from_top = cursor - scroll; + let left = &mut surface[(area.left(), area.y + offset_from_top as u16)]; + left.set_style(selected); + let right = &mut surface[( + area.right().saturating_sub(1), + area.y + offset_from_top as u16, + )]; + right.set_style(selected); + } } let fits = len <= win_height; @@ -361,12 +377,15 @@ impl Component for Menu { for i in 0..win_height { cell = &mut surface[(area.right() - 1, area.top() + i as u16)]; - cell.set_symbol("▐"); // right half block - if scroll_line <= i && i < scroll_line + scroll_height { // Draw scroll thumb + if render_borders { + cell.set_symbol("▌"); // left half block + } else { + cell.set_symbol("▐"); // right half block + } cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); - } else { + } else if !render_borders { // Draw scroll track cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 62a6785a4ef21..c6e534c398ea7 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -3,10 +3,16 @@ use crate::{ compositor::{Callback, Component, Context, Event, EventResult}, ctrl, key, }; -use tui::buffer::Buffer as Surface; +use tui::{ + buffer::Buffer as Surface, + widgets::{Block, Borders, Widget}, +}; use helix_core::Position; -use helix_view::graphics::{Margin, Rect}; +use helix_view::{ + editor::PopupBorderConfig, + graphics::{Margin, Rect}, +}; // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box) @@ -236,13 +242,25 @@ impl Component for Popup { let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - let inner = area.inner(&self.margin); + let border_config = &cx.editor.config().popup_border; + + let render_borders = + border_config == &PopupBorderConfig::All || border_config == &PopupBorderConfig::Popup; + + let (inner, border) = if render_borders { + Widget::render(Block::default().borders(Borders::ALL), area, surface); + (area, 1) + } else { + let inner = area.inner(&self.margin); + (inner, 0) + }; + self.contents.render(inner, surface, cx); // render scrollbar if contents do not fit if self.has_scrollbar { - let win_height = inner.height as usize; - let len = self.child_size.1 as usize; + let win_height = inner.height as usize - 2 * border; + let len = self.child_size.1 as usize - 2 * border; let fits = len <= win_height; let scroll = self.scroll; let scroll_style = cx.editor.theme.get("ui.menu.scroll"); @@ -258,14 +276,17 @@ impl Component for Popup { let mut cell; for i in 0..win_height { - cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)]; - - cell.set_symbol("▐"); // right half block + cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)]; if scroll_line <= i && i < scroll_line + scroll_height { // Draw scroll thumb + if render_borders { + cell.set_symbol("▌"); // left half block + } else { + cell.set_symbol("▐"); // right half block + } cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); - } else { + } else if !render_borders { // Draw scroll track cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 4e3a69a2d152e..bcc234fe8d733 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -174,6 +174,8 @@ pub struct Config { pub indent_guides: IndentGuidesConfig, /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, + /// Draw border around popups. + pub popup_border: PopupBorderConfig, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -582,6 +584,15 @@ impl Default for IndentGuidesConfig { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum PopupBorderConfig { + None, + All, + Popup, + Menu, +} + impl Default for Config { fn default() -> Self { Self { @@ -621,6 +632,7 @@ impl Default for Config { bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), color_modes: false, + popup_border: PopupBorderConfig::None, } } }