Support drawing popup frame (#4313)

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
pull/9119/head
ath3 11 months ago committed by GitHub
parent 06d7dc628e
commit 9ba691cd3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -65,6 +65,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | | `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` | | `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` |
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` | | `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `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` | `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`
### `[editor.statusline]` Section ### `[editor.statusline]` Section

@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client}; use helix_dap::{self as dap, Client};
use helix_lsp::block_on; use helix_lsp::block_on;
use helix_view::editor::Breakpoint; use helix_view::{editor::Breakpoint, graphics::Margin};
use serde_json::{to_value, Value}; use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@ -581,7 +581,12 @@ pub fn dap_variables(cx: &mut Context) {
} }
let contents = Text::from(tui::text::Text::from(variables)); 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); cx.replace_or_push_layer("dap-variables", popup);
} }

@ -23,6 +23,7 @@ use helix_core::{
use helix_view::{ use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action, editor::Action,
graphics::Margin,
theme::Style, theme::Style,
Document, View, Document, View,
}; };
@ -744,7 +745,16 @@ pub fn code_action(cx: &mut Context) {
}); });
picker.move_down(); // pre-select the first item 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); compositor.replace_or_push("code-action", popup);
}; };

@ -574,7 +574,6 @@ fn set_line_ending(
Ok(()) Ok(())
} }
fn earlier( fn earlier(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[Cow<str>], args: &[Cow<str>],

@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult};
use helix_view::{ use helix_view::{
document::SavePoint, document::SavePoint,
editor::CompleteAction, editor::CompleteAction,
graphics::Margin,
theme::{Modifier, Style}, theme::{Modifier, Style},
ViewId, 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) let popup = Popup::new(Self::ID, menu)
.with_scrollbar(false) .with_scrollbar(false)
.ignore_escape_key(true); .ignore_escape_key(true)
.margin(margin);
let mut completion = Self { let mut completion = Self {
popup, popup,
start_offset, start_offset,
@ -569,6 +579,12 @@ impl Component for Completion {
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");
surface.clear_with(doc_area, background); 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); markdown_doc.render(doc_area, surface, cx);
} }
} }

@ -92,7 +92,9 @@ impl Component for SignatureHelp {
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
}; };
let sig_doc = sig_doc.parse(Some(&cx.editor.theme)); 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) let sig_doc_para = Paragraph::new(sig_doc)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0)); .scroll((cx.scroll.unwrap_or_default() as u16, 0));

@ -7,11 +7,18 @@ use crate::{
use helix_core::fuzzy::MATCHER; use helix_core::fuzzy::MATCHER;
use nucleo::pattern::{Atom, AtomKind, CaseMatching}; use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::{Config, Utf32Str}; use nucleo::{Config, Utf32Str};
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}; pub use tui::widgets::{Cell, Row};
use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; use helix_view::{
editor::SmartTabConfig,
graphics::{Margin, Rect},
Editor,
};
use tui::layout::Constraint; use tui::layout::Constraint;
pub trait Item: Sync + Send + 'static { pub trait Item: Sync + Send + 'static {
@ -322,6 +329,15 @@ impl<T: Item + 'static> Component for Menu<T> {
let selected = theme.get("ui.menu.selected"); let selected = theme.get("ui.menu.selected");
surface.clear_with(area, style); 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 scroll = self.scroll;
let options: Vec<_> = self let options: Vec<_> = self
@ -362,15 +378,19 @@ impl<T: Item + 'static> Component for Menu<T> {
false, false,
); );
if let Some(cursor) = self.cursor { let render_borders = cx.editor.menu_border();
let offset_from_top = cursor - scroll;
let left = &mut surface[(area.left(), area.y + offset_from_top as u16)]; if !render_borders {
left.set_style(selected); if let Some(cursor) = self.cursor {
let right = &mut surface[( let offset_from_top = cursor - scroll;
area.right().saturating_sub(1), let left = &mut surface[(area.left(), area.y + offset_from_top as u16)];
area.y + offset_from_top as u16, left.set_style(selected);
)]; let right = &mut surface[(
right.set_style(selected); area.right().saturating_sub(1),
area.y + offset_from_top as u16,
)];
right.set_style(selected);
}
} }
let fits = len <= win_height; let fits = len <= win_height;
@ -385,12 +405,13 @@ impl<T: Item + 'static> Component for Menu<T> {
for i in 0..win_height { for i in 0..win_height {
cell = &mut surface[(area.right() - 1, area.top() + i as u16)]; 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 { if scroll_line <= i && i < scroll_line + scroll_height {
// Draw scroll thumb // Draw scroll thumb
cell.set_symbol(half_block);
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else { } else if !render_borders {
// Draw scroll track // Draw scroll track
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
} }

@ -3,7 +3,10 @@ use crate::{
compositor::{Callback, Component, Context, Event, EventResult}, compositor::{Callback, Component, Context, Event, EventResult},
ctrl, key, ctrl, key,
}; };
use tui::buffer::Buffer as Surface; use tui::{
buffer::Buffer as Surface,
widgets::{Block, Borders, Widget},
};
use helix_core::Position; use helix_core::Position;
use helix_view::{ use helix_view::{
@ -252,13 +255,29 @@ impl<T: Component> Component for Popup<T> {
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background); 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(render_borders);
if render_borders {
Widget::render(Block::default().borders(Borders::ALL), area, surface);
}
self.contents.render(inner, surface, cx); self.contents.render(inner, surface, cx);
// render scrollbar if contents do not fit // render scrollbar if contents do not fit
if self.has_scrollbar { if self.has_scrollbar {
let win_height = inner.height as usize; let win_height = (inner.height as usize).saturating_sub(2 * border);
let len = self.child_size.1 as usize; let len = (self.child_size.1 as usize).saturating_sub(2 * border);
let fits = len <= win_height; let fits = len <= win_height;
let scroll = self.scroll; let scroll = self.scroll;
let scroll_style = cx.editor.theme.get("ui.menu.scroll"); let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@ -274,14 +293,15 @@ impl<T: Component> Component for Popup<T> {
let mut cell; let mut cell;
for i in 0..win_height { 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 { if scroll_line <= i && i < scroll_line + scroll_height {
// Draw scroll thumb // Draw scroll thumb
cell.set_symbol(half_block);
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else { } else if !render_borders {
// Draw scroll track // Draw scroll track
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
} }

@ -43,9 +43,8 @@ pub use helix_core::diagnostic::Severity;
use helix_core::{ use helix_core::{
auto_pairs::AutoPairs, auto_pairs::AutoPairs,
syntax::{self, AutoPairConfig, IndentationHeuristic, SoftWrap}, syntax::{self, AutoPairConfig, IndentationHeuristic, 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_dap as dap;
use helix_lsp::lsp; use helix_lsp::lsp;
@ -291,6 +290,8 @@ pub struct Config {
pub insert_final_newline: bool, pub insert_final_newline: bool,
/// Enables smart tab /// Enables smart tab
pub smart_tab: Option<SmartTabConfig>, pub smart_tab: Option<SmartTabConfig>,
/// Draw border around popups.
pub popup_border: PopupBorderConfig,
/// Which indent heuristic to use when a new line is inserted /// Which indent heuristic to use when a new line is inserted
#[serde(default)] #[serde(default)]
pub indent_heuristic: IndentationHeuristic, pub indent_heuristic: IndentationHeuristic,
@ -797,6 +798,15 @@ impl From<LineEndingConfig> 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 { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -844,6 +854,7 @@ impl Default for Config {
default_line_ending: LineEndingConfig::default(), default_line_ending: LineEndingConfig::default(),
insert_final_newline: true, insert_final_newline: true,
smart_tab: Some(SmartTabConfig::default()), smart_tab: Some(SmartTabConfig::default()),
popup_border: PopupBorderConfig::None,
indent_heuristic: IndentationHeuristic::default(), indent_heuristic: IndentationHeuristic::default(),
} }
} }
@ -1064,6 +1075,16 @@ impl Editor {
} }
} }
pub fn popup_border(&self) -> bool {
self.config().popup_border == PopupBorderConfig::All
|| self.config().popup_border == PopupBorderConfig::Popup
}
pub fn menu_border(&self) -> bool {
self.config().popup_border == PopupBorderConfig::All
|| self.config().popup_border == PopupBorderConfig::Menu
}
pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) { pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
motion(self); motion(self);
self.last_motion = Some(Box::new(motion)); self.last_motion = Some(Box::new(motion));

Loading…
Cancel
Save