diff --git a/book/src/configuration.md b/book/src/configuration.md index eb2cf473cffd..5354982b6f79 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -64,6 +64,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | | `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | +| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | ### `[editor.statusline]` Section diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index ce28a3ca6d2d..4e29faca76a5 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -19,6 +19,7 @@ | `:format`, `:fmt` | Format the file using the LSP formatter. | | `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) | | `:line-ending` | Set the document's default line ending. Options: crlf, lf. | +| `:popup-borders` | Set the borders for popups and menus | | `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. | | `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. | | `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) | diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index e26dc08dcf01..5cf1132d3303 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_dap::{self as dap, Client}; use helix_lsp::block_on; -use helix_view::editor::Breakpoint; +use helix_view::{editor::Breakpoint, graphics::Margin}; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -577,7 +577,12 @@ pub fn dap_variables(cx: &mut Context) { } let contents = Text::from(tui::text::Text::from(variables)); - let popup = Popup::new("dap-variables", contents); + let margin = if cx.editor.popup_border { + Margin::all(1) + } else { + Margin::none() + }; + let popup = Popup::new("dap-variables", contents).margin(margin); cx.replace_or_push_layer("dap-variables", popup); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 57be1267ca52..134cf493650e 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -23,6 +23,7 @@ use helix_core::{ use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, editor::Action, + graphics::Margin, theme::Style, Document, View, }; @@ -744,7 +745,16 @@ 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 margin = if editor.menu_border { + 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/commands/typed.rs b/helix-term/src/commands/typed.rs index 463bce259b27..8c3d90667eba 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -559,6 +559,46 @@ fn set_line_ending( Ok(()) } +fn set_borders( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + if args.is_empty() { + let borders = (cx.editor.popup_border, cx.editor.menu_border); + cx.editor.set_status(match borders { + (true, true) => "all", + (true, false) => "popup", + (false, true) => "menu", + (false, false) => "none", + }); + + return Ok(()); + } + + let arg = args.get(0).context("argument missing")?.to_lowercase(); + + if let Some(borders) = match arg { + arg if arg.starts_with("all") => Some((true, true)), + arg if arg.starts_with("popup") => Some((true, false)), + arg if arg.starts_with("menu") => Some((false, true)), + arg if arg.starts_with("none") => Some((false, false)), + _ => None, + } { + cx.editor.popup_border = borders.0; + cx.editor.menu_border = borders.1; + } else { + cx.editor + .set_status("Valid options are: none, popup, menu, all"); + }; + + Ok(()) +} + fn earlier( cx: &mut compositor::Context, args: &[Cow], @@ -2436,6 +2476,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: set_line_ending, signature: CommandSignature::none(), }, + TypableCommand { + name: "popup-borders", + aliases: &[], + doc: "Set the borders for popups and menus", + fun: set_borders, + signature: CommandSignature::none(), + }, TypableCommand { name: "earlier", aliases: &["ear"], diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index e06147224465..0850c67a2b00 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult}; use helix_view::{ document::SavePoint, editor::CompleteAction, + graphics::Margin, theme::{Modifier, Style}, ViewId, }; @@ -326,9 +327,18 @@ impl Completion { } }; }); + + let margin = if editor.menu_border { + Margin::vertical(1) + } else { + Margin::none() + }; + let popup = Popup::new(Self::ID, menu) .with_scrollbar(false) - .ignore_escape_key(true); + .ignore_escape_key(true) + .margin(margin); + let mut completion = Self { popup, start_offset, @@ -569,6 +579,12 @@ impl Component for Completion { // clear area let background = cx.editor.theme.get("ui.popup"); surface.clear_with(doc_area, background); + + if cx.editor.popup_border { + use tui::widgets::{Block, Borders, Widget}; + Widget::render(Block::default().borders(Borders::ALL), doc_area, surface); + } + markdown_doc.render(doc_area, surface, cx); } } diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index 880df6d8ea3b..c8e0b0bc768e 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -92,7 +92,9 @@ 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 sig_doc_area = area + .clip_top(sig_text_area.height + 2) + .clip_bottom(u16::from(cx.editor.popup_border)); 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 c73e7bed259a..6a19d36afdb4 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -4,14 +4,21 @@ use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, widgets::Table}; +use tui::{ + buffer::Buffer as Surface, + 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::{editor::SmartTabConfig, graphics::Rect, Editor}; +use helix_view::{ + editor::SmartTabConfig, + graphics::{Margin, Rect}, + Editor, +}; use tui::layout::Constraint; pub trait Item { @@ -325,6 +332,15 @@ impl Component for Menu { let selected = theme.get("ui.menu.selected"); surface.clear_with(area, style); + let render_borders = cx.editor.menu_border; + + let area = if render_borders { + Widget::render(Block::default().borders(Borders::ALL), area, surface); + area.inner(&Margin::vertical(1)) + } else { + area + }; + let scroll = self.scroll; let options: Vec<_> = self @@ -365,15 +381,17 @@ impl Component for Menu { false, ); - 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; @@ -388,12 +406,13 @@ 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 + let half_block = if render_borders { "▌" } else { "▐" }; if scroll_line <= i && i < scroll_line + scroll_height { // Draw scroll thumb + cell.set_symbol(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 dff7b23192a2..30244d18ec5c 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -3,7 +3,10 @@ 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::{ @@ -252,13 +255,29 @@ impl Component for Popup { let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); - let inner = area.inner(&self.margin); + let render_borders = cx.editor.popup_border; + + let inner = if self + .contents + .type_name() + .starts_with("helix_term::ui::menu::Menu") + { + area + } else { + area.inner(&self.margin) + }; + + let border = usize::from(cx.editor.popup_border); + if render_borders { + Widget::render(Block::default().borders(Borders::ALL), area, surface); + } + 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"); @@ -274,14 +293,15 @@ 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 = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)]; - cell.set_symbol("▐"); // right half block + let half_block = if render_borders { "▌" } else { "▐" }; if scroll_line <= i && i < scroll_line + scroll_height { // Draw scroll thumb + cell.set_symbol(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 af00864a793d..fe2648091672 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -43,9 +43,8 @@ pub use helix_core::diagnostic::Severity; use helix_core::{ auto_pairs::AutoPairs, syntax::{self, AutoPairConfig, SoftWrap}, - Change, LineEnding, NATIVE_LINE_ENDING, + Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING, }; -use helix_core::{Position, Selection}; use helix_dap as dap; use helix_lsp::lsp; @@ -289,6 +288,8 @@ pub struct Config { pub default_line_ending: LineEndingConfig, /// Enables smart tab pub smart_tab: Option, + /// Draw border around popups. + pub popup_border: PopupBorderConfig, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] @@ -797,6 +798,15 @@ impl From for LineEnding { } } +#[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 { @@ -843,6 +853,7 @@ impl Default for Config { workspace_lsp_roots: Vec::new(), default_line_ending: LineEndingConfig::default(), smart_tab: Some(SmartTabConfig::default()), + popup_border: PopupBorderConfig::None, } } } @@ -950,6 +961,8 @@ pub struct Editor { /// field is set and any old requests are automatically /// canceled as a result pub completion_request_handle: Option>, + pub popup_border: bool, + pub menu_border: bool, } pub type Motion = Box; @@ -1063,6 +1076,10 @@ impl Editor { needs_redraw: false, cursor_cache: Cell::new(None), completion_request_handle: None, + popup_border: conf.popup_border == PopupBorderConfig::All + || conf.popup_border == PopupBorderConfig::Popup, + menu_border: conf.popup_border == PopupBorderConfig::All + || conf.popup_border == PopupBorderConfig::Menu, } } @@ -1093,6 +1110,10 @@ impl Editor { pub fn refresh_config(&mut self) { let config = self.config(); self.auto_pairs = (&config.auto_pairs).into(); + self.popup_border = config.popup_border == PopupBorderConfig::All + || config.popup_border == PopupBorderConfig::Popup; + self.menu_border = config.popup_border == PopupBorderConfig::All + || config.popup_border == PopupBorderConfig::Menu; self.reset_idle_timer(); self._refresh(); }