aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/completion.rs45
-rw-r--r--helix-term/src/ui/editor.rs45
-rw-r--r--helix-term/src/ui/markdown.rs30
-rw-r--r--helix-term/src/ui/menu.rs76
-rw-r--r--helix-term/src/ui/mod.rs29
-rw-r--r--helix-term/src/ui/picker.rs25
-rw-r--r--helix-term/src/ui/popup.rs61
-rw-r--r--helix-term/src/ui/text.rs20
8 files changed, 211 insertions, 120 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 90657764..6c9e3a80 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -262,8 +262,7 @@ impl Component for Completion {
.cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.offset.row) as u16;
-
- let mut doc = match &option.documentation {
+ let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
@@ -311,24 +310,42 @@ impl Component for Completion {
None => return,
};
- let half = area.height / 2;
- let height = 15.min(half);
- // we want to make sure the cursor is visible (not hidden behind the documentation)
- let y = if cursor_pos + area.y
- >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
- {
- 0
+ let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
+ let (popup_width, _popup_height) = self.popup.get_size();
+ let mut width = area
+ .width
+ .saturating_sub(popup_x)
+ .saturating_sub(popup_width);
+ let area = if width > 30 {
+ let mut height = area.height.saturating_sub(popup_y);
+ let x = popup_x + popup_width;
+ let y = popup_y;
+
+ if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
+ width = rel_width;
+ height = rel_height;
+ }
+ Rect::new(x, y, width, height)
} else {
- // -2 to subtract command line + statusline. a bit of a hack, because of splits.
- area.height.saturating_sub(height).saturating_sub(2)
- };
+ let half = area.height / 2;
+ let height = 15.min(half);
+ // we want to make sure the cursor is visible (not hidden behind the documentation)
+ let y = if cursor_pos + area.y
+ >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
+ {
+ 0
+ } else {
+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
+ area.height.saturating_sub(height).saturating_sub(2)
+ };
- let area = Rect::new(0, y, area.width, height);
+ Rect::new(0, y, area.width, height)
+ };
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
- doc.render(area, surface, cx);
+ markdown_doc.render(area, surface, cx);
}
}
}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 63694d0b..128fe948 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -3,7 +3,7 @@ use crate::{
compositor::{Component, Compositor, Context, EventResult},
job::Callback,
key,
- keymap::{KeymapResult, Keymaps},
+ keymap::{KeymapResult, KeymapResultKind, Keymaps},
ui::{Completion, ProgressSpinners},
};
@@ -165,8 +165,7 @@ impl EditorView {
let scopes = theme.scopes();
syntax
.highlight_iter(text.slice(..), Some(range), None, |language| {
- loader
- .language_config_for_scope(&format!("source.{}", language))
+ loader.language_configuration_for_injection_string(language)
.and_then(|language_config| {
let config = language_config.highlight_config(scopes)?;
let config_ref = config.as_ref();
@@ -852,7 +851,7 @@ impl EditorView {
/// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was
- /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
+ /// activated). Only KeymapResultKind::{NotFound, Cancelled} is returned
/// otherwise.
fn handle_keymap_event(
&mut self,
@@ -860,8 +859,6 @@ impl EditorView {
cxt: &mut commands::Context,
event: KeyEvent,
) -> Option<KeymapResult> {
- self.autoinfo = None;
-
if let Some(picker) = cxt.editor.debug_config_picker.clone() {
match event {
KeyEvent {
@@ -912,29 +909,32 @@ impl EditorView {
return None;
}
- match self.keymaps.get_mut(&mode).unwrap().get(event) {
- KeymapResult::Matched(command) => command.execute(cxt),
- KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
- k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
+ let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
+ self.autoinfo = key_result.sticky.map(|node| node.infobox());
+
+ match &key_result.kind {
+ KeymapResultKind::Matched(command) => command.execute(cxt),
+ KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
+ KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
}
None
}
fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
- match keyresult {
- KeymapResult::NotFound => {
+ match keyresult.kind {
+ KeymapResultKind::NotFound => {
if let Some(ch) = event.char() {
commands::insert::insert_char(cx, ch)
}
}
- KeymapResult::Cancelled(pending) => {
+ KeymapResultKind::Cancelled(pending) => {
for ev in pending {
match ev.char() {
Some(ch) => commands::insert::insert_char(cx, ch),
None => {
- if let KeymapResult::Matched(command) =
- self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
+ if let KeymapResultKind::Matched(command) =
+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev).kind
{
command.execute(cx);
}
@@ -972,7 +972,7 @@ impl EditorView {
// debug_assert!(cxt.count != 0);
// set the register
- cxt.selected_register = cxt.editor.selected_register.take();
+ cxt.register = cxt.editor.selected_register.take();
self.handle_keymap_event(mode, cxt, event);
if self.keymaps.pending().is_empty() {
@@ -1196,9 +1196,9 @@ impl EditorView {
impl Component for EditorView {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let mut cxt = commands::Context {
- selected_register: helix_view::RegisterSelection::default(),
editor: &mut cx.editor,
count: None,
+ register: None,
callback: None,
on_next_key_callback: None,
jobs: cx.jobs,
@@ -1288,11 +1288,12 @@ impl Component for EditorView {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
- self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
- KeymapResult::Matched(command) => command,
- // FIXME: insert mode can only be entered through single KeyCodes
- _ => unimplemented!(),
- };
+ self.last_insert.0 =
+ match self.keymaps.get_mut(&mode).unwrap().get(key).kind {
+ KeymapResultKind::Matched(command) => command,
+ // FIXME: insert mode can only be entered through single KeyCodes
+ _ => unimplemented!(),
+ };
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 28542cdc..4144ed3c 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -88,7 +88,7 @@ fn parse<'a>(
if let Some(theme) = theme {
let rope = Rope::from(text.as_ref());
let syntax = loader
- .language_config_for_scope(&format!("source.{}", language))
+ .language_configuration_for_injection_string(language)
.and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config));
@@ -215,10 +215,30 @@ impl Component for Markdown {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2;
- let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
- let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
- Some((width, height))
+ if padding >= viewport.1 || padding >= viewport.0 {
+ return None;
+ }
+ let contents = parse(&self.contents, None, &self.config_loader);
+ let max_text_width = (viewport.0 - padding).min(120);
+ let mut text_width = 0;
+ let mut height = padding;
+ for content in contents {
+ height += 1;
+ let content_width = content.width() as u16;
+ if content_width > max_text_width {
+ text_width = max_text_width;
+ height += content_width / max_text_width;
+ } else if content_width > text_width {
+ text_width = content_width;
+ }
+
+ if height >= viewport.1 {
+ height = viewport.1;
+ break;
+ }
+ }
+
+ Some((text_width + padding, height))
}
}
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 24dd3e61..dab0c34f 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -33,6 +33,8 @@ pub struct Menu<T: Item> {
scroll: usize,
size: (u16, u16),
+ viewport: (u16, u16),
+ recalculate: bool,
}
impl<T: Item> Menu<T> {
@@ -51,6 +53,8 @@ impl<T: Item> Menu<T> {
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
+ viewport: (0, 0),
+ recalculate: true,
};
// TODO: scoring on empty input should just use a fastpath
@@ -83,6 +87,7 @@ impl<T: Item> Menu<T> {
// reset cursor position
self.cursor = None;
self.scroll = 0;
+ self.recalculate = true;
}
pub fn move_up(&mut self) {
@@ -99,6 +104,41 @@ impl<T: Item> Menu<T> {
self.adjust_scroll();
}
+ fn recalculate_size(&mut self, viewport: (u16, u16)) {
+ let n = self
+ .options
+ .first()
+ .map(|option| option.row().cells.len())
+ .unwrap_or_default();
+ let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
+ let row = option.row();
+ // maintain max for each column
+ for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
+ let width = cell.content.width();
+ if width > *acc {
+ *acc = width;
+ }
+ }
+
+ acc
+ });
+ let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
+ let width = len.min(viewport.0 as usize);
+
+ self.widths = max_lens
+ .into_iter()
+ .map(|len| Constraint::Length(len as u16))
+ .collect();
+
+ let height = self.matches.len().min(10).min(viewport.1 as usize);
+
+ self.size = (width as u16, height as u16);
+
+ // adjust scroll offsets if size changed
+ self.adjust_scroll();
+ self.recalculate = false;
+ }
+
fn adjust_scroll(&mut self) {
let win_height = self.size.1 as usize;
if let Some(cursor) = self.cursor {
@@ -221,43 +261,13 @@ impl<T: Item + 'static> Component for Menu<T> {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let n = self
- .options
- .first()
- .map(|option| option.row().cells.len())
- .unwrap_or_default();
- let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
- let row = option.row();
- // maintain max for each column
- for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
- let width = cell.content.width();
- if width > *acc {
- *acc = width;
- }
- }
-
- acc
- });
- let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
- let width = len.min(viewport.0 as usize);
-
- self.widths = max_lens
- .into_iter()
- .map(|len| Constraint::Length(len as u16))
- .collect();
-
- let height = self.options.len().min(10).min(viewport.1 as usize);
-
- self.size = (width as u16, height as u16);
-
- // adjust scroll offsets if size changed
- self.adjust_scroll();
+ if viewport != self.viewport || self.recalculate {
+ self.recalculate_size(viewport);
+ }
Some(self.size)
}
- // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
-
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
let style = theme
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 37148ae2..e66673ca 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -20,7 +20,7 @@ pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
use helix_core::regex::Regex;
-use helix_core::register::Registers;
+use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
use std::path::PathBuf;
@@ -28,7 +28,8 @@ use std::path::PathBuf;
pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
- fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
+ history_register: Option<char>,
+ fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt {
let (view, doc) = current!(cx.editor);
let view_id = view.id;
@@ -36,7 +37,7 @@ pub fn regex_prompt(
Prompt::new(
prompt,
- None,
+ history_register,
|_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
@@ -46,6 +47,14 @@ pub fn regex_prompt(
}
PromptEvent::Validate => {
// TODO: push_jump to store selection just before jump
+
+ match Regex::new(input) {
+ Ok(regex) => {
+ let (view, doc) = current!(cx.editor);
+ fun(view, doc, regex, event);
+ }
+ Err(_err) => (), // TODO: mark command line as error
+ }
}
PromptEvent::Update => {
// skip empty input, TODO: trigger default
@@ -53,15 +62,23 @@ pub fn regex_prompt(
return;
}
- match Regex::new(input) {
+ let case_insensitive = if cx.editor.config.smart_case {
+ !input.chars().any(char::is_uppercase)
+ } else {
+ false
+ };
+
+ match RegexBuilder::new(input)
+ .case_insensitive(case_insensitive)
+ .build()
+ {
Ok(regex) => {
let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
// revert state to what it was before the last update
doc.set_selection(view.id, snapshot.clone());
- fun(view, doc, registers, regex);
+ fun(view, doc, regex, event);
view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
}
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 84b8dd72..ee1ec177 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -124,10 +124,13 @@ impl<T: 'static> Component for FilePicker<T> {
}) {
// align to middle
let first_line = line
- .map(|(s, e)| (s.min(doc.text().len_lines()), e.min(doc.text().len_lines())))
- .map(|(start, _)| start)
- .unwrap_or(0)
- .saturating_sub(inner.height as usize / 2);
+ .map(|(start, end)| {
+ let height = end.saturating_sub(start) + 1;
+ let middle = start + (height.saturating_sub(1) / 2);
+ middle.saturating_sub(inner.height as usize / 2).min(start)
+ })
+ .unwrap_or(0);
+
let offset = Position::new(first_line, 0);
let highlights = EditorView::doc_syntax_highlights(
@@ -268,17 +271,15 @@ impl<T> Picker<T> {
}
pub fn move_up(&mut self) {
- self.cursor = self.cursor.saturating_sub(1);
+ let len = self.matches.len();
+ let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
+ self.cursor = pos;
}
pub fn move_down(&mut self) {
- if self.matches.is_empty() {
- return;
- }
-
- if self.cursor < self.matches.len() - 1 {
- self.cursor += 1;
- }
+ let len = self.matches.len();
+ let pos = (self.cursor + 1) % len;
+ self.cursor = pos;
}
pub fn selection(&self) -> Option<&T> {
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index e126c845..1bab1eae 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -16,8 +16,6 @@ pub struct Popup<T: Component> {
}
impl<T: Component> Popup<T> {
- // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
- // rendering)
pub fn new(contents: T) -> Self {
Self {
contents,
@@ -31,6 +29,39 @@ impl<T: Component> Popup<T> {
self.position = pos;
}
+ pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
+ let position = self
+ .position
+ .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
+
+ let (width, height) = self.size;
+
+ // if there's a orientation preference, use that
+ // if we're on the top part of the screen, do below
+ // if we're on the bottom part, do above
+
+ // -- make sure frame doesn't stick out of bounds
+ let mut rel_x = position.col as u16;
+ let mut rel_y = position.row as u16;
+ if viewport.width <= rel_x + width {
+ rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
+ }
+
+ // TODO: be able to specify orientation preference. We want above for most popups, below
+ // for menus/autocomplete.
+ if viewport.height > rel_y + height {
+ rel_y += 1 // position below point
+ } else {
+ rel_y = rel_y.saturating_sub(height) // position above point
+ }
+
+ (rel_x, rel_y)
+ }
+
+ pub fn get_size(&self) -> (u16, u16) {
+ (self.size.0, self.size.1)
+ }
+
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
@@ -106,31 +137,15 @@ impl<T: Component> Component for Popup<T> {
}
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
- cx.scroll = Some(self.scroll);
+ // trigger required_size so we recalculate if the child changed
+ self.required_size((viewport.width, viewport.height));
- let position = self
- .position
- .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
-
- let (width, height) = self.size;
-
- // -- make sure frame doesn't stick out of bounds
- let mut rel_x = position.col as u16;
- let mut rel_y = position.row as u16;
- if viewport.width <= rel_x + width {
- rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
- };
+ cx.scroll = Some(self.scroll);
- // TODO: be able to specify orientation preference. We want above for most popups, below
- // for menus/autocomplete.
- if height <= rel_y {
- rel_y = rel_y.saturating_sub(height) // position above point
- } else {
- rel_y += 1 // position below point
- }
+ let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
// clip to viewport
- let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height));
+ let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
// clear area
let background = cx.editor.theme.get("ui.popup");
diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs
index 65a75a4a..4641fae1 100644
--- a/helix-term/src/ui/text.rs
+++ b/helix-term/src/ui/text.rs
@@ -5,11 +5,17 @@ use helix_view::graphics::Rect;
pub struct Text {
contents: String,
+ size: (u16, u16),
+ viewport: (u16, u16),
}
impl Text {
pub fn new(contents: String) -> Self {
- Self { contents }
+ Self {
+ contents,
+ size: (0, 0),
+ viewport: (0, 0),
+ }
}
}
impl Component for Text {
@@ -24,9 +30,13 @@ impl Component for Text {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let contents = tui::text::Text::from(self.contents.clone());
- let width = std::cmp::min(contents.width() as u16, viewport.0);
- let height = std::cmp::min(contents.height() as u16, viewport.1);
- Some((width, height))
+ if viewport != self.viewport {
+ let contents = tui::text::Text::from(self.contents.clone());
+ let width = std::cmp::min(contents.width() as u16, viewport.0);
+ let height = std::cmp::min(contents.height() as u16, viewport.1);
+ self.size = (width, height);
+ self.viewport = viewport;
+ }
+ Some(self.size)
}
}