aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
authorNathan Vegdahl2021-06-20 23:09:14 +0000
committerNathan Vegdahl2021-06-20 23:09:14 +0000
commite686c3e4626fdafbcc2dab9d381eba83a5f6f974 (patch)
treea598e3fedc1f2ae78ebc6f132c81b37cedf5415d /helix-term/src/ui
parent4efd6713c5b30b33c497a1f85b77a7b0a7fd17e0 (diff)
parent985625763addd839a101263ae90cfb2f205830fc (diff)
Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection
Rebasing was making me manually fix conflicts on every commit, so merging instead.
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/completion.rs59
-rw-r--r--helix-term/src/ui/editor.rs12
-rw-r--r--helix-term/src/ui/markdown.rs32
-rw-r--r--helix-term/src/ui/mod.rs39
-rw-r--r--helix-term/src/ui/prompt.rs236
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,
)