on master help sign in
Support drawing popup frame (#4313)
9ba691cath3
book/src/configuration.md+1
@@ -65,6 +65,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
65| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
65| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
66| `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` |
66| `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` |
67| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
67| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
68+| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
68| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
69| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
69
70
70### `[editor.statusline]` Section
71### `[editor.statusline]` Section
helix-term/src/commands/dap.rs+7−2
@@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
8use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
8use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
9use helix_dap::{self as dap, Client};
9use helix_dap::{self as dap, Client};
10use helix_lsp::block_on;
10use helix_lsp::block_on;
11−use helix_view::editor::Breakpoint;
11+use helix_view::{editor::Breakpoint, graphics::Margin};
12
12
13use serde_json::{to_value, Value};
13use serde_json::{to_value, Value};
14use tokio_stream::wrappers::UnboundedReceiverStream;
14use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -581,7 +581,12 @@ pub fn dap_variables(cx: &mut Context) {
581 }
581 }
582
582
583 let contents = Text::from(tui::text::Text::from(variables));
583 let contents = Text::from(tui::text::Text::from(variables));
584+ let margin = if cx.editor.popup_border() {
585+ Margin::all(1)
586+ } else {
587+ Margin::none()
588+ };
584− let popup = Popup::new("dap-variables", contents);
589+ let popup = Popup::new("dap-variables", contents).margin(margin);
585 cx.replace_or_push_layer("dap-variables", popup);
590 cx.replace_or_push_layer("dap-variables", popup);
586}
591}
587
592
helix-term/src/commands/lsp.rs+11−1
@@ -23,6 +23,7 @@ use helix_core::{
23use helix_view::{
23use helix_view::{
24 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
24 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
25 editor::Action,
25 editor::Action,
26+ graphics::Margin,
26 theme::Style,
27 theme::Style,
27 Document, View,
28 Document, View,
28};
29};
@@ -744,7 +745,16 @@ pub fn code_action(cx: &mut Context) {
744 });
745 });
745 picker.move_down(); // pre-select the first item
746 picker.move_down(); // pre-select the first item
746
747
748+ let margin = if editor.menu_border() {
749+ Margin::vertical(1)
750+ } else {
751+ Margin::none()
752+ };
753+
747− let popup = Popup::new("code-action", picker).with_scrollbar(false);
754+ let popup = Popup::new("code-action", picker)
755+ .with_scrollbar(false)
756+ .margin(margin);
757+
748 compositor.replace_or_push("code-action", popup);
758 compositor.replace_or_push("code-action", popup);
749 };
759 };
750
760
helix-term/src/commands/typed.rs−1
@@ -574,7 +574,6 @@ fn set_line_ending(
574
574
575 Ok(())
575 Ok(())
576}
576}
577−
578fn earlier(
577fn earlier(
579 cx: &mut compositor::Context,
578 cx: &mut compositor::Context,
580 args: &[Cow<str>],
579 args: &[Cow<str>],
helix-term/src/ui/completion.rs+17−1
@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
2use helix_view::{
2use helix_view::{
3 document::SavePoint,
3 document::SavePoint,
4 editor::CompleteAction,
4 editor::CompleteAction,
5+ graphics::Margin,
5 theme::{Modifier, Style},
6 theme::{Modifier, Style},
6 ViewId,
7 ViewId,
7};
8};
@@ -326,9 +327,18 @@ impl Completion {
326 }
327 }
327 };
328 };
328 });
329 });
330+
331+ let margin = if editor.menu_border() {
332+ Margin::vertical(1)
333+ } else {
334+ Margin::none()
335+ };
336+
329 let popup = Popup::new(Self::ID, menu)
337 let popup = Popup::new(Self::ID, menu)
330 .with_scrollbar(false)
338 .with_scrollbar(false)
331− .ignore_escape_key(true);
339+ .ignore_escape_key(true)
340+ .margin(margin);
341+
332 let mut completion = Self {
342 let mut completion = Self {
333 popup,
343 popup,
334 start_offset,
344 start_offset,
@@ -569,6 +579,12 @@ impl Component for Completion {
569 // clear area
579 // clear area
570 let background = cx.editor.theme.get("ui.popup");
580 let background = cx.editor.theme.get("ui.popup");
571 surface.clear_with(doc_area, background);
581 surface.clear_with(doc_area, background);
582+
583+ if cx.editor.popup_border() {
584+ use tui::widgets::{Block, Borders, Widget};
585+ Widget::render(Block::default().borders(Borders::ALL), doc_area, surface);
586+ }
587+
572 markdown_doc.render(doc_area, surface, cx);
588 markdown_doc.render(doc_area, surface, cx);
573 }
589 }
574}
590}
helix-term/src/ui/lsp.rs+3−1
@@ -92,7 +92,9 @@ impl Component for SignatureHelp {
92 Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
92 Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
93 };
93 };
94 let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
94 let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
95+ let sig_doc_area = area
95− let sig_doc_area = area.clip_top(sig_text_area.height + 2);
96+ .clip_top(sig_text_area.height + 2)
97+ .clip_bottom(u16::from(cx.editor.popup_border()));
96 let sig_doc_para = Paragraph::new(sig_doc)
98 let sig_doc_para = Paragraph::new(sig_doc)
97 .wrap(Wrap { trim: false })
99 .wrap(Wrap { trim: false })
98 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
100 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
helix-term/src/ui/menu.rs+34−13
@@ -7,11 +7,18 @@ use crate::{
7use helix_core::fuzzy::MATCHER;
7use helix_core::fuzzy::MATCHER;
8use nucleo::pattern::{Atom, AtomKind, CaseMatching};
8use nucleo::pattern::{Atom, AtomKind, CaseMatching};
9use nucleo::{Config, Utf32Str};
9use nucleo::{Config, Utf32Str};
10+use tui::{
10−use tui::{buffer::Buffer as Surface, widgets::Table};
11+ buffer::Buffer as Surface,
12+ widgets::{Block, Borders, Table, Widget},
13+};
11
14
12pub use tui::widgets::{Cell, Row};
15pub use tui::widgets::{Cell, Row};
13
16
17+use helix_view::{
14−use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
18+ editor::SmartTabConfig,
19+ graphics::{Margin, Rect},
20+ Editor,
21+};
15use tui::layout::Constraint;
22use tui::layout::Constraint;
16
23
17pub trait Item: Sync + Send + 'static {
24pub trait Item: Sync + Send + 'static {
@@ -322,6 +329,15 @@ impl<T: Item + 'static> Component for Menu<T> {
322 let selected = theme.get("ui.menu.selected");
329 let selected = theme.get("ui.menu.selected");
323 surface.clear_with(area, style);
330 surface.clear_with(area, style);
324
331
332+ let render_borders = cx.editor.menu_border();
333+
334+ let area = if render_borders {
335+ Widget::render(Block::default().borders(Borders::ALL), area, surface);
336+ area.inner(&Margin::vertical(1))
337+ } else {
338+ area
339+ };
340+
325 let scroll = self.scroll;
341 let scroll = self.scroll;
326
342
327 let options: Vec<_> = self
343 let options: Vec<_> = self
@@ -362,15 +378,19 @@ impl<T: Item + 'static> Component for Menu<T> {
362 false,
378 false,
363 );
379 );
364
380
381+ let render_borders = cx.editor.menu_border();
382+
383+ if !render_borders {
365− if let Some(cursor) = self.cursor {
384+ if let Some(cursor) = self.cursor {
366− let offset_from_top = cursor - scroll;
385+ let offset_from_top = cursor - scroll;
367− let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
386+ let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
368− left.set_style(selected);
387+ left.set_style(selected);
369− let right = &mut surface[(
388+ let right = &mut surface[(
370− area.right().saturating_sub(1),
389+ area.right().saturating_sub(1),
371− area.y + offset_from_top as u16,
390+ area.y + offset_from_top as u16,
372− )];
391+ )];
373− right.set_style(selected);
392+ right.set_style(selected);
393+ }
374 }
394 }
375
395
376 let fits = len <= win_height;
396 let fits = len <= win_height;
@@ -385,12 +405,13 @@ impl<T: Item + 'static> Component for Menu<T> {
385 for i in 0..win_height {
405 for i in 0..win_height {
386 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
406 cell = &mut surface[(area.right() - 1, area.top() + i as u16)];
387
407
388− cell.set_symbol("▐"); // right half block
408+ let half_block = if render_borders { "▌" } else { "▐" };
389
409
390 if scroll_line <= i && i < scroll_line + scroll_height {
410 if scroll_line <= i && i < scroll_line + scroll_height {
391 // Draw scroll thumb
411 // Draw scroll thumb
412+ cell.set_symbol(half_block);
392 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
413 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
393− } else {
414+ } else if !render_borders {
394 // Draw scroll track
415 // Draw scroll track
395 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
416 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
396 }
417 }
helix-term/src/ui/popup.rs+27−7
@@ -3,7 +3,10 @@ use crate::{
3 compositor::{Callback, Component, Context, Event, EventResult},
3 compositor::{Callback, Component, Context, Event, EventResult},
4 ctrl, key,
4 ctrl, key,
5};
5};
6+use tui::{
6−use tui::buffer::Buffer as Surface;
7+ buffer::Buffer as Surface,
8+ widgets::{Block, Borders, Widget},
9+};
7
10
8use helix_core::Position;
11use helix_core::Position;
9use helix_view::{
12use helix_view::{
@@ -252,13 +255,29 @@ impl<T: Component> Component for Popup<T> {
252 let background = cx.editor.theme.get("ui.popup");
255 let background = cx.editor.theme.get("ui.popup");
253 surface.clear_with(area, background);
256 surface.clear_with(area, background);
254
257
258+ let render_borders = cx.editor.popup_border();
259+
260+ let inner = if self
261+ .contents
262+ .type_name()
263+ .starts_with("helix_term::ui::menu::Menu")
264+ {
265+ area
266+ } else {
255− let inner = area.inner(&self.margin);
267+ area.inner(&self.margin)
268+ };
269+
270+ let border = usize::from(render_borders);
271+ if render_borders {
272+ Widget::render(Block::default().borders(Borders::ALL), area, surface);
273+ }
274+
256 self.contents.render(inner, surface, cx);
275 self.contents.render(inner, surface, cx);
257
276
258 // render scrollbar if contents do not fit
277 // render scrollbar if contents do not fit
259 if self.has_scrollbar {
278 if self.has_scrollbar {
260− let win_height = inner.height as usize;
279+ let win_height = (inner.height as usize).saturating_sub(2 * border);
261− let len = self.child_size.1 as usize;
280+ let len = (self.child_size.1 as usize).saturating_sub(2 * border);
262 let fits = len <= win_height;
281 let fits = len <= win_height;
263 let scroll = self.scroll;
282 let scroll = self.scroll;
264 let scroll_style = cx.editor.theme.get("ui.menu.scroll");
283 let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@@ -274,14 +293,15 @@ impl<T: Component> Component for Popup<T> {
274
293
275 let mut cell;
294 let mut cell;
276 for i in 0..win_height {
295 for i in 0..win_height {
277− cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)];
296+ cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)];
278
297
279− cell.set_symbol("▐"); // right half block
298+ let half_block = if render_borders { "▌" } else { "▐" };
280
299
281 if scroll_line <= i && i < scroll_line + scroll_height {
300 if scroll_line <= i && i < scroll_line + scroll_height {
282 // Draw scroll thumb
301 // Draw scroll thumb
302+ cell.set_symbol(half_block);
283 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
303 cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
284− } else {
304+ } else if !render_borders {
285 // Draw scroll track
305 // Draw scroll track
286 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
306 cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
287 }
307 }
helix-view/src/editor.rs+23−2
@@ -43,9 +43,8 @@ pub use helix_core::diagnostic::Severity;
43use helix_core::{
43use helix_core::{
44 auto_pairs::AutoPairs,
44 auto_pairs::AutoPairs,
45 syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
45 syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap},
46− Change, LineEnding, NATIVE_LINE_ENDING,
46+ Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
47};
47};
48−use helix_core::{Position, Selection};
49use helix_dap as dap;
48use helix_dap as dap;
50use helix_lsp::lsp;
49use helix_lsp::lsp;
51
50
@@ -291,6 +290,8 @@ pub struct Config {
291 pub insert_final_newline: bool,
290 pub insert_final_newline: bool,
292 /// Enables smart tab
291 /// Enables smart tab
293 pub smart_tab: Option<SmartTabConfig>,
292 pub smart_tab: Option<SmartTabConfig>,
293+ /// Draw border around popups.
294+ pub popup_border: PopupBorderConfig,
294 /// Which indent heuristic to use when a new line is inserted
295 /// Which indent heuristic to use when a new line is inserted
295 #[serde(default)]
296 #[serde(default)]
296 pub indent_heuristic: IndentationHeuristic,
297 pub indent_heuristic: IndentationHeuristic,
@@ -797,6 +798,15 @@ impl From<LineEndingConfig> for LineEnding {
797 }
798 }
798}
799}
799
800
801+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
802+#[serde(rename_all = "kebab-case")]
803+pub enum PopupBorderConfig {
804+ None,
805+ All,
806+ Popup,
807+ Menu,
808+}
809+
800impl Default for Config {
810impl Default for Config {
801 fn default() -> Self {
811 fn default() -> Self {
802 Self {
812 Self {
@@ -844,6 +854,7 @@ impl Default for Config {
844 default_line_ending: LineEndingConfig::default(),
854 default_line_ending: LineEndingConfig::default(),
845 insert_final_newline: true,
855 insert_final_newline: true,
846 smart_tab: Some(SmartTabConfig::default()),
856 smart_tab: Some(SmartTabConfig::default()),
857+ popup_border: PopupBorderConfig::None,
847 indent_heuristic: IndentationHeuristic::default(),
858 indent_heuristic: IndentationHeuristic::default(),
848 }
859 }
849 }
860 }
@@ -1064,6 +1075,16 @@ impl Editor {
1064 }
1075 }
1065 }
1076 }
1066
1077
1078+ pub fn popup_border(&self) -> bool {
1079+ self.config().popup_border == PopupBorderConfig::All
1080+ || self.config().popup_border == PopupBorderConfig::Popup
1081+ }
1082+
1083+ pub fn menu_border(&self) -> bool {
1084+ self.config().popup_border == PopupBorderConfig::All
1085+ || self.config().popup_border == PopupBorderConfig::Menu
1086+ }
1087+
1067 pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
1088 pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
1068 motion(self);
1089 motion(self);
1069 self.last_motion = Some(Box::new(motion));
1090 self.last_motion = Some(Box::new(motion));