diff options
author | Dmitry Sharshakov | 2021-09-25 20:14:59 +0000 |
---|---|---|
committer | Dmitry Sharshakov | 2021-09-25 20:14:59 +0000 |
commit | bf53aff27d2d90b41bab01f4d628f0bd9fbcd589 (patch) | |
tree | 568d745540acd05ae7526e8a3eed7ee8e31e3cea /helix-term/src/ui | |
parent | 413e477dc2d4792596f99979140d2879ec3d4f4f (diff) | |
parent | df55eaae69d0388de26448e82f9ded483fca2f44 (diff) |
Merge branch 'master' into debug
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r-- | helix-term/src/ui/completion.rs | 45 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 45 | ||||
-rw-r--r-- | helix-term/src/ui/markdown.rs | 30 | ||||
-rw-r--r-- | helix-term/src/ui/menu.rs | 76 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 29 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 25 | ||||
-rw-r--r-- | helix-term/src/ui/popup.rs | 61 | ||||
-rw-r--r-- | helix-term/src/ui/text.rs | 20 |
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) } } |