diff options
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r-- | helix-term/src/ui/completion.rs | 59 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 12 | ||||
-rw-r--r-- | helix-term/src/ui/markdown.rs | 32 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 39 | ||||
-rw-r--r-- | helix-term/src/ui/prompt.rs | 236 |
5 files changed, 284 insertions, 94 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 06ed966d..80f7d590 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -238,6 +238,9 @@ impl Component for Completion { .language() .and_then(|scope| scope.strip_prefix("source.")) .unwrap_or(""); + let cursor_pos = doc.selection(view.id).cursor(); + let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row + - view.first_line) as u16; let doc = match &option.documentation { Some(lsp::Documentation::String(contents)) @@ -246,42 +249,60 @@ impl Component for Completion { value: contents, })) => { // TODO: convert to wrapped text - Markdown::new(format!( - "```{}\n{}\n```\n{}", - language, - option.detail.as_deref().unwrap_or_default(), - contents.clone() - )) + Markdown::new( + format!( + "```{}\n{}\n```\n{}", + language, + option.detail.as_deref().unwrap_or_default(), + contents.clone() + ), + cx.editor.syn_loader.clone(), + ) } Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, value: contents, })) => { // TODO: set language based on doc scope - Markdown::new(format!( - "```{}\n{}\n```\n{}", - language, - option.detail.as_deref().unwrap_or_default(), - contents.clone() - )) + Markdown::new( + format!( + "```{}\n{}\n```\n{}", + language, + option.detail.as_deref().unwrap_or_default(), + contents.clone() + ), + cx.editor.syn_loader.clone(), + ) } None if option.detail.is_some() => { // TODO: copied from above // TODO: set language based on doc scope - Markdown::new(format!( - "```{}\n{}\n```", - language, - option.detail.as_deref().unwrap_or_default(), - )) + Markdown::new( + format!( + "```{}\n{}\n```", + language, + option.detail.as_deref().unwrap_or_default(), + ), + cx.editor.syn_loader.clone(), + ) } None => return, }; let half = area.height / 2; let height = 15.min(half); - // -2 to subtract command line + statusline. a bit of a hack, because of splits. - let area = Rect::new(0, area.height - height - 2, area.width, height); + // we want to make sure the cursor is visible (not hidden behind the documentation) + let y = if cursor_pos + view.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); // clear area let background = cx.editor.theme.get("ui.popup"); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index da8f0f53..faede58c 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -11,13 +11,12 @@ use helix_core::{ syntax::{self, HighlightEvent}, LineEnding, Position, Range, }; -use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; use helix_view::{document::Mode, Document, Editor, Theme, View}; use std::borrow::Cow; use crossterm::{ cursor, - event::{read, Event, EventStream}, + event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, }; use tui::{ backend::CrosstermBackend, @@ -130,7 +129,7 @@ impl EditorView { })], }; let mut spans = Vec::new(); - let mut visual_x = 0; + let mut visual_x = 0u16; let mut line = 0u16; let tab_width = doc.tab_width(); @@ -186,7 +185,7 @@ impl EditorView { break 'outer; } } else if grapheme == "\t" { - visual_x += (tab_width as u16); + visual_x = visual_x.saturating_add(tab_width as u16); } else { let out_of_bounds = visual_x < view.first_col as u16 || visual_x >= viewport.width + view.first_col as u16; @@ -198,7 +197,7 @@ impl EditorView { if out_of_bounds { // if we're offscreen just keep going until we hit a new line - visual_x += width; + visual_x = visual_x.saturating_add(width); continue; } @@ -608,8 +607,7 @@ impl Component for EditorView { cx.editor.resize(Rect::new(0, 0, width, height - 1)); EventResult::Consumed(None) } - Event::Key(key) => { - let mut key = KeyEvent::from(key); + Event::Key(mut key) => { canonicalize_key(&mut key); // clear status cx.editor.status_msg = None; diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 3ce3a5b8..72a3e4ff 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -7,25 +7,34 @@ use tui::{ text::Text, }; -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; -use helix_core::Position; +use helix_core::{syntax, Position}; use helix_view::{Editor, Theme}; pub struct Markdown { contents: String, + + config_loader: Arc<syntax::Loader>, } // TODO: pre-render and self reference via Pin // better yet, just use Tendril + subtendril for references impl Markdown { - pub fn new(contents: String) -> Self { - Self { contents } + pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self { + Self { + contents, + config_loader, + } } } -fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> { +fn parse<'a>( + contents: &'a str, + theme: Option<&Theme>, + loader: &syntax::Loader, +) -> tui::text::Text<'a> { use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use tui::text::{Span, Spans, Text}; @@ -79,9 +88,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> { use helix_core::Rope; let rope = Rope::from(text.as_ref()); - let syntax = syntax::LOADER - .get() - .unwrap() + let syntax = loader .language_config_for_scope(&format!("source.{}", language)) .and_then(|config| config.highlight_config(theme.scopes())) .map(|config| Syntax::new(&rope, config)); @@ -101,9 +108,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> { } HighlightEvent::Source { start, end } => { let style = match highlights.first() { - Some(span) => { - theme.get(theme.scopes()[span.0].as_str()) - } + Some(span) => theme.get(&theme.scopes()[span.0]), None => text_style, }; @@ -159,7 +164,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> { } } Event::Code(text) | Event::Html(text) => { - log::warn!("code {:?}", text); let mut span = to_span(text); span.style = code_style; spans.push(span); @@ -198,7 +202,7 @@ impl Component for Markdown { fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { use tui::widgets::{Paragraph, Widget, Wrap}; - let text = parse(&self.contents, Some(&cx.editor.theme)); + let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader); let par = Paragraph::new(text) .wrap(Wrap { trim: false }) @@ -209,7 +213,7 @@ impl Component for Markdown { } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let contents = parse(&self.contents, None); + 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); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 39e11cd6..e0177b7c 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -115,10 +115,43 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> { pub mod completers { use crate::ui::prompt::Completion; - use std::borrow::Cow; + use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; + use fuzzy_matcher::FuzzyMatcher; + use helix_view::theme; + use std::cmp::Reverse; + use std::{borrow::Cow, sync::Arc}; pub type Completer = fn(&str) -> Vec<Completion>; + pub fn theme(input: &str) -> Vec<Completion> { + let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes")); + names.extend(theme::Loader::read_names( + &helix_core::config_dir().join("themes"), + )); + names.push("default".into()); + + let mut names: Vec<_> = names + .into_iter() + .map(|name| ((0..), Cow::from(name))) + .collect(); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = names + .into_iter() + .filter_map(|(range, name)| { + matcher + .fuzzy_match(&name, &input) + .map(|score| (name, score)) + }) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); + names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + + names + } + // TODO: we could return an iter/lazy thing so it can fetch as many as it needs. pub fn filename(input: &str) -> Vec<Completion> { // Rust's filename handling is really annoying. @@ -178,10 +211,6 @@ pub mod completers { // if empty, return a list of dirs and files in current dir if let Some(file_name) = file_name { - use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; - use fuzzy_matcher::FuzzyMatcher; - use std::cmp::Reverse; - let matcher = Matcher::default(); // inefficient, but we need to calculate the scores, filter out None, then sort. diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 991b328d..7ca4308c 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -6,6 +6,11 @@ use helix_view::{Editor, Theme}; use std::{borrow::Cow, ops::RangeFrom}; use tui::terminal::CursorKind; +use helix_core::{ + unicode::segmentation::{GraphemeCursor, GraphemeIncomplete}, + unicode::width::UnicodeWidthStr, +}; + pub type Completion = (RangeFrom<usize>, Cow<'static, str>); pub struct Prompt { @@ -34,6 +39,17 @@ pub enum CompletionDirection { Backward, } +#[derive(Debug, Clone, Copy)] +pub enum Movement { + BackwardChar(usize), + BackwardWord(usize), + ForwardChar(usize), + ForwardWord(usize), + StartOfLine, + EndOfLine, + None, +} + impl Prompt { pub fn new( prompt: String, @@ -52,30 +68,120 @@ impl Prompt { } } + /// Compute the cursor position after applying movement + /// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611 + fn eval_movement(&self, movement: Movement) -> usize { + match movement { + Movement::BackwardChar(rep) => { + let mut position = self.cursor; + for _ in 0..rep { + let mut cursor = GraphemeCursor::new(position, self.line.len(), false); + if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) { + position = pos; + } else { + break; + } + } + position + } + Movement::BackwardWord(rep) => { + let char_indices: Vec<(usize, char)> = self.line.char_indices().collect(); + if char_indices.is_empty() { + return self.cursor; + } + let mut char_position = char_indices + .iter() + .position(|(idx, _)| *idx == self.cursor) + .unwrap_or(char_indices.len() - 1); + + for _ in 0..rep { + if char_position == 0 { + break; + } + + let mut found = None; + for prev in (0..char_position - 1).rev() { + if char_indices[prev].1.is_whitespace() { + found = Some(prev + 1); + break; + } + } + + char_position = found.unwrap_or(0); + } + char_indices[char_position].0 + } + Movement::ForwardWord(rep) => { + let char_indices: Vec<(usize, char)> = self.line.char_indices().collect(); + if char_indices.is_empty() { + return self.cursor; + } + let mut char_position = char_indices + .iter() + .position(|(idx, _)| *idx == self.cursor) + .unwrap_or_else(|| char_indices.len()); + + for _ in 0..rep { + // Skip any non-whitespace characters + while char_position < char_indices.len() + && !char_indices[char_position].1.is_whitespace() + { + char_position += 1; + } + + // Skip any whitespace characters + while char_position < char_indices.len() + && char_indices[char_position].1.is_whitespace() + { + char_position += 1; + } + + // We are now on the start of the next word + } + char_indices + .get(char_position) + .map(|(i, _)| *i) + .unwrap_or_else(|| self.line.len()) + } + Movement::ForwardChar(rep) => { + let mut position = self.cursor; + for _ in 0..rep { + let mut cursor = GraphemeCursor::new(position, self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + position = pos; + } else { + break; + } + } + position + } + Movement::StartOfLine => 0, + Movement::EndOfLine => { + let mut cursor = + GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + pos + } else { + self.cursor + } + } + Movement::None => self.cursor, + } + } + pub fn insert_char(&mut self, c: char) { - let pos = if self.line.is_empty() { - 0 - } else { - self.line - .char_indices() - .nth(self.cursor) - .map(|(pos, _)| pos) - .unwrap_or_else(|| self.line.len()) - }; - self.line.insert(pos, c); - self.cursor += 1; + self.line.insert(self.cursor, c); + let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + self.cursor = pos; + } self.completion = (self.completion_fn)(&self.line); self.exit_selection(); } - pub fn move_char_left(&mut self) { - self.cursor = self.cursor.saturating_sub(1) - } - - pub fn move_char_right(&mut self) { - if self.cursor < self.line.len() { - self.cursor += 1; - } + pub fn move_cursor(&mut self, movement: Movement) { + let pos = self.eval_movement(movement); + self.cursor = pos } pub fn move_start(&mut self) { @@ -87,39 +193,29 @@ impl Prompt { } pub fn delete_char_backwards(&mut self) { - if self.cursor > 0 { - let pos = self - .line - .char_indices() - .nth(self.cursor - 1) - .map(|(pos, _)| pos) - .expect("line is not empty"); - self.line.remove(pos); - self.cursor -= 1; - self.completion = (self.completion_fn)(&self.line); - } + let pos = self.eval_movement(Movement::BackwardChar(1)); + self.line.replace_range(pos..self.cursor, ""); + self.cursor = pos; + self.exit_selection(); + self.completion = (self.completion_fn)(&self.line); } pub fn delete_word_backwards(&mut self) { - use helix_core::get_general_category; - let mut chars = self.line.char_indices().rev(); - // TODO add skipping whitespace logic here - let (mut i, cat) = match chars.next() { - Some((i, c)) => (i, get_general_category(c)), - None => return, - }; - self.cursor -= 1; - for (nn, nc) in chars { - if get_general_category(nc) != cat { - break; - } - i = nn; - self.cursor -= 1; - } - self.line.drain(i..); + let pos = self.eval_movement(Movement::BackwardWord(1)); + self.line.replace_range(pos..self.cursor, ""); + self.cursor = pos; + + self.exit_selection(); self.completion = (self.completion_fn)(&self.line); + } + + pub fn kill_to_end_of_line(&mut self) { + let pos = self.eval_movement(Movement::EndOfLine); + self.line.replace_range(self.cursor..pos, ""); + self.exit_selection(); + self.completion = (self.completion_fn)(&self.line); } pub fn clear(&mut self) { @@ -293,32 +389,72 @@ impl Component for Prompt { (self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); } KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { code: KeyCode::Esc, .. } => { (self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort); return close_fn; } KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { code: KeyCode::Right, .. - } => self.move_char_right(), + } => self.move_cursor(Movement::ForwardChar(1)), KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::CONTROL, + } + | KeyEvent { code: KeyCode::Left, .. - } => self.move_char_left(), + } => self.move_cursor(Movement::BackwardChar(1)), KeyEvent { + code: KeyCode::End, + modifiers: KeyModifiers::NONE, + } + | KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::CONTROL, } => self.move_end(), KeyEvent { + code: KeyCode::Home, + modifiers: KeyModifiers::NONE, + } + | KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL, } => self.move_start(), KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::ALT, + } + | KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::ALT, + } => self.move_cursor(Movement::BackwardWord(1)), + KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::ALT, + } + | KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::ALT, + } => self.move_cursor(Movement::ForwardWord(1)), + KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::CONTROL, } => self.delete_word_backwards(), KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + } => self.kill_to_end_of_line(), + KeyEvent { code: KeyCode::Backspace, modifiers: KeyModifiers::NONE, } => { @@ -363,7 +499,9 @@ impl Component for Prompt { ( Some(Position::new( area.y as usize + line, - area.x as usize + self.prompt.len() + self.cursor, + area.x as usize + + self.prompt.len() + + UnicodeWidthStr::width(&self.line[..self.cursor]), )), CursorKind::Block, ) |