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.rs134
-rw-r--r--helix-term/src/ui/editor.rs134
-rw-r--r--helix-term/src/ui/menu.rs43
-rw-r--r--helix-term/src/ui/mod.rs23
-rw-r--r--helix-term/src/ui/picker.rs157
-rw-r--r--helix-term/src/ui/prompt.rs37
6 files changed, 355 insertions, 173 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index c75b24f1..dd782d29 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
use std::borrow::Cow;
use helix_core::Transaction;
-use helix_view::{graphics::Rect, Document, Editor, View};
+use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@@ -30,31 +30,32 @@ impl menu::Item for CompletionItem {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
- Some(lsp::CompletionItemKind::Text) => "text",
- Some(lsp::CompletionItemKind::Method) => "method",
- Some(lsp::CompletionItemKind::Function) => "function",
- Some(lsp::CompletionItemKind::Constructor) => "constructor",
- Some(lsp::CompletionItemKind::Field) => "field",
- Some(lsp::CompletionItemKind::Variable) => "variable",
- Some(lsp::CompletionItemKind::Class) => "class",
- Some(lsp::CompletionItemKind::Interface) => "interface",
- Some(lsp::CompletionItemKind::Module) => "module",
- Some(lsp::CompletionItemKind::Property) => "property",
- Some(lsp::CompletionItemKind::Unit) => "unit",
- Some(lsp::CompletionItemKind::Value) => "value",
- Some(lsp::CompletionItemKind::Enum) => "enum",
- Some(lsp::CompletionItemKind::Keyword) => "keyword",
- Some(lsp::CompletionItemKind::Snippet) => "snippet",
- Some(lsp::CompletionItemKind::Color) => "color",
- Some(lsp::CompletionItemKind::File) => "file",
- Some(lsp::CompletionItemKind::Reference) => "reference",
- Some(lsp::CompletionItemKind::Folder) => "folder",
- Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
- Some(lsp::CompletionItemKind::Constant) => "constant",
- Some(lsp::CompletionItemKind::Struct) => "struct",
- Some(lsp::CompletionItemKind::Event) => "event",
- Some(lsp::CompletionItemKind::Operator) => "operator",
- Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
+ Some(lsp::CompletionItemKind::TEXT) => "text",
+ Some(lsp::CompletionItemKind::METHOD) => "method",
+ Some(lsp::CompletionItemKind::FUNCTION) => "function",
+ Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
+ Some(lsp::CompletionItemKind::FIELD) => "field",
+ Some(lsp::CompletionItemKind::VARIABLE) => "variable",
+ Some(lsp::CompletionItemKind::CLASS) => "class",
+ Some(lsp::CompletionItemKind::INTERFACE) => "interface",
+ Some(lsp::CompletionItemKind::MODULE) => "module",
+ Some(lsp::CompletionItemKind::PROPERTY) => "property",
+ Some(lsp::CompletionItemKind::UNIT) => "unit",
+ Some(lsp::CompletionItemKind::VALUE) => "value",
+ Some(lsp::CompletionItemKind::ENUM) => "enum",
+ Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
+ Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
+ Some(lsp::CompletionItemKind::COLOR) => "color",
+ Some(lsp::CompletionItemKind::FILE) => "file",
+ Some(lsp::CompletionItemKind::REFERENCE) => "reference",
+ Some(lsp::CompletionItemKind::FOLDER) => "folder",
+ Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
+ Some(lsp::CompletionItemKind::CONSTANT) => "constant",
+ Some(lsp::CompletionItemKind::STRUCT) => "struct",
+ Some(lsp::CompletionItemKind::EVENT) => "event",
+ Some(lsp::CompletionItemKind::OPERATOR) => "operator",
+ Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
+ Some(kind) => unimplemented!("{:?}", kind),
None => "",
}),
// self.detail.as_deref().unwrap_or("")
@@ -83,13 +84,13 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Self {
- // let items: Vec<CompletionItem> = Vec::new();
let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
- view: &View,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
+ start_offset: usize,
+ trigger_offset: usize,
) -> Transaction {
if let Some(edit) = &item.text_edit {
let edit = match edit {
@@ -105,63 +106,52 @@ impl Completion {
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
+ // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
+ // in these cases we need to check for a common prefix and remove it
+ let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
+ let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(),
- vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
+ vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
}
}
+ let (view, doc) = current!(editor);
+
+ // if more text was entered, remove it
+ doc.restore(view.id);
+
match event {
PromptEvent::Abort => {}
PromptEvent::Update => {
- let (view, doc) = current!(editor);
-
// always present here
let item = item.unwrap();
- // if more text was entered, remove it
- // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- if trigger_offset < cursor {
- let remove = Transaction::change(
- doc.text(),
- vec![(trigger_offset, cursor, None)].into_iter(),
- );
- doc.apply(&remove, view.id);
- }
+ let transaction = item_to_transaction(
+ doc,
+ item,
+ offset_encoding,
+ start_offset,
+ trigger_offset,
+ );
+
+ // initialize a savepoint
+ doc.savepoint();
- let transaction = item_to_transaction(doc, view, item, offset_encoding);
doc.apply(&transaction, view.id);
}
PromptEvent::Validate => {
- let (view, doc) = current!(editor);
-
// always present here
let item = item.unwrap();
- // if more text was entered, remove it
- // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
- let cursor = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- if trigger_offset < cursor {
- let remove = Transaction::change(
- doc.text(),
- vec![(trigger_offset, cursor, None)].into_iter(),
- );
- doc.apply(&remove, view.id);
- }
-
- let transaction = item_to_transaction(doc, view, item, offset_encoding);
+ let transaction = item_to_transaction(
+ doc,
+ item,
+ offset_encoding,
+ start_offset,
+ trigger_offset,
+ );
doc.apply(&transaction, view.id);
if let Some(additional_edits) = &item.additional_text_edits {
@@ -210,7 +200,7 @@ impl Completion {
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
- if self.start_offset <= cursor {
+ if self.trigger_offset <= cursor {
let fragment = doc.text().slice(self.start_offset..cursor);
let text = Cow::from(fragment);
// TODO: logic is same as ui/picker
@@ -274,12 +264,10 @@ impl Component for Completion {
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
- let cursor_pos = doc
- .selection(view.id)
- .primary()
- .cursor(doc.text().slice(..));
- let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- - view.offset.row) as u16;
+ let text = doc.text().slice(..);
+ let cursor_pos = doc.selection(view.id).primary().cursor(text);
+ let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
+ let cursor_pos = (coords.row - view.offset.row) as u16;
let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 037f04b8..26a0358d 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -689,6 +689,8 @@ impl EditorView {
theme: &Theme,
is_focused: bool,
) {
+ use tui::text::{Span, Spans};
+
//-------------------------------
// Left side of the status line.
//-------------------------------
@@ -707,17 +709,17 @@ impl EditorView {
})
.unwrap_or("");
- let style = if is_focused {
+ let base_style = if is_focused {
theme.get("ui.statusline")
} else {
theme.get("ui.statusline.inactive")
};
// statusline
- surface.set_style(viewport.with_height(1), style);
+ surface.set_style(viewport.with_height(1), base_style);
if is_focused {
- surface.set_string(viewport.x + 1, viewport.y, mode, style);
+ surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
}
- surface.set_string(viewport.x + 5, viewport.y, progress, style);
+ surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
if let Some(path) = doc.relative_path() {
let path = path.to_string_lossy();
@@ -728,7 +730,7 @@ impl EditorView {
viewport.y,
title,
viewport.width.saturating_sub(6) as usize,
- style,
+ base_style,
);
}
@@ -736,8 +738,50 @@ impl EditorView {
// Right side of the status line.
//-------------------------------
- // Compute the individual info strings.
- let diag_count = format!("{}", doc.diagnostics().len());
+ let mut right_side_text = Spans::default();
+
+ // Compute the individual info strings and add them to `right_side_text`.
+
+ // Diagnostics
+ let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
+ use helix_core::diagnostic::Severity;
+ match diag.severity {
+ Some(Severity::Warning) => counts.0 += 1,
+ Some(Severity::Error) | None => counts.1 += 1,
+ _ => {}
+ }
+ counts
+ });
+ let (warnings, errors) = diags;
+ let warning_style = theme.get("warning");
+ let error_style = theme.get("error");
+ for i in 0..2 {
+ let (count, style) = match i {
+ 0 => (warnings, warning_style),
+ 1 => (errors, error_style),
+ _ => unreachable!(),
+ };
+ if count == 0 {
+ continue;
+ }
+ let style = base_style.patch(style);
+ right_side_text.0.push(Span::styled("●", style));
+ right_side_text
+ .0
+ .push(Span::styled(format!(" {} ", count), base_style));
+ }
+
+ // Selections
+ let sels_count = doc.selection(view.id).len();
+ right_side_text.0.push(Span::styled(
+ format!(
+ " {} sel{} ",
+ sels_count,
+ if sels_count == 1 { "" } else { "s" }
+ ),
+ base_style,
+ ));
+
// let indent_info = match doc.indent_style {
// IndentStyle::Tabs => "tabs",
// IndentStyle::Spaces(1) => "spaces:1",
@@ -750,29 +794,28 @@ impl EditorView {
// IndentStyle::Spaces(8) => "spaces:8",
// _ => "indent:ERROR",
// };
- let position_info = {
- let pos = coords_at_pos(
- doc.text().slice(..),
- doc.selection(view.id)
- .primary()
- .cursor(doc.text().slice(..)),
- );
- format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
- };
- // Render them to the status line together.
- let right_side_text = format!(
- "{} {} ",
- &diag_count[..diag_count.len().min(4)],
- // indent_info,
- position_info
+ // Position
+ let pos = coords_at_pos(
+ doc.text().slice(..),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
);
- let text_len = right_side_text.len() as u16;
- surface.set_string(
- viewport.x + viewport.width.saturating_sub(text_len),
+ right_side_text.0.push(Span::styled(
+ format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
+ base_style,
+ ));
+
+ // Render to the statusline.
+ surface.set_spans(
+ viewport.x
+ + viewport
+ .width
+ .saturating_sub(right_side_text.width() as u16),
viewport.y,
- right_side_text,
- style,
+ &right_side_text,
+ right_side_text.width() as u16,
);
}
@@ -984,7 +1027,7 @@ impl EditorView {
pub fn set_completion(
&mut self,
- editor: &Editor,
+ editor: &mut Editor,
items: Vec<helix_lsp::lsp::CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
@@ -999,10 +1042,21 @@ impl EditorView {
return;
}
+ // Immediately initialize a savepoint
+ doc_mut!(editor).savepoint();
+
// TODO : propagate required size on resize to completion too
completion.required_size((size.width, size.height));
self.completion = Some(completion);
}
+
+ pub fn clear_completion(&mut self, editor: &mut Editor) {
+ self.completion = None;
+ // Clear any savepoints
+ let (_, doc) = current!(editor);
+ doc.savepoint = None;
+ editor.clear_idle_timer(); // don't retrigger
+ }
}
impl EditorView {
@@ -1022,12 +1076,12 @@ impl EditorView {
let editor = &mut cxt.editor;
let result = editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
});
if let Some((pos, view_id)) = result {
- let doc = &mut editor.documents[editor.tree.get(view_id).doc];
+ let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
if modifiers == crossterm::event::KeyModifiers::ALT {
let selection = doc.selection(view_id).clone();
@@ -1096,7 +1150,7 @@ impl EditorView {
};
let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
+ view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
.map(|_| view.id)
});
@@ -1182,12 +1236,12 @@ impl EditorView {
}
let result = editor.tree.views().find_map(|(view, _focus)| {
- view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
+ view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
.map(|pos| (pos, view.id))
});
if let Some((pos, view_id)) = result {
- let doc = &mut editor.documents[editor.tree.get(view_id).doc];
+ let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
commands::Command::paste_primary_clipboard_before.execute(cxt);
@@ -1254,8 +1308,7 @@ impl Component for EditorView {
if callback.is_some() {
// assume close_fn
- self.completion = None;
- cxt.editor.clear_idle_timer(); // don't retrigger
+ self.clear_completion(cxt.editor);
}
}
}
@@ -1268,8 +1321,7 @@ impl Component for EditorView {
if let Some(completion) = &mut self.completion {
completion.update(&mut cxt);
if completion.is_empty() {
- self.completion = None;
- cxt.editor.clear_idle_timer(); // don't retrigger
+ self.clear_completion(cxt.editor);
}
}
}
@@ -1397,8 +1449,10 @@ impl Component for EditorView {
info.render(area, surface, cx);
}
- if let Some(ref mut info) = self.autoinfo {
- info.render(area, surface, cx);
+ if cx.editor.config.auto_info {
+ if let Some(ref mut info) = self.autoinfo {
+ info.render(area, surface, cx);
+ }
}
let key_width = 15u16; // for showing pending keys
@@ -1469,7 +1523,7 @@ fn canonicalize_key(key: &mut KeyEvent) {
}
#[inline]
-fn abs_diff(a: usize, b: usize) -> usize {
+const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {
a - b
} else {
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 055593fd..3c492d14 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -64,25 +64,23 @@ impl<T: Item> Menu<T> {
}
pub fn score(&mut self, pattern: &str) {
- // need to borrow via pattern match otherwise it complains about simultaneous borrow
- let Self {
- ref mut matcher,
- ref mut matches,
- ref options,
- ..
- } = *self;
-
// reuse the matches allocation
- matches.clear();
- matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
- let text = option.filter_text();
- // TODO: using fuzzy_indices could give us the char idx for match highlighting
- matcher
- .fuzzy_match(text, pattern)
- .map(|score| (index, score))
- }));
+ self.matches.clear();
+ self.matches.extend(
+ self.options
+ .iter()
+ .enumerate()
+ .filter_map(|(index, option)| {
+ let text = option.filter_text();
+ // TODO: using fuzzy_indices could give us the char idx for match highlighting
+ self.matcher
+ .fuzzy_match(text, pattern)
+ .map(|score| (index, score))
+ }),
+ );
// matches.sort_unstable_by_key(|(_, score)| -score);
- matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
+ self.matches
+ .sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
// reset cursor position
self.cursor = None;
@@ -100,7 +98,8 @@ impl<T: Item> Menu<T> {
pub fn move_up(&mut self) {
let len = self.matches.len();
- let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
+ let max_index = len.saturating_sub(1);
+ let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len;
self.cursor = Some(pos);
self.adjust_scroll();
}
@@ -216,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
} => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -233,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('j'),
+ modifiers: KeyModifiers::CONTROL,
} => {
self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index e66673ca..00c70cea 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -29,6 +29,7 @@ pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
+ completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt {
let (view, doc) = current!(cx.editor);
@@ -38,7 +39,7 @@ pub fn regex_prompt(
Prompt::new(
prompt,
history_register,
- |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
+ completion_fn,
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
PromptEvent::Abort => {
@@ -92,9 +93,25 @@ pub fn regex_prompt(
}
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
- use ignore::Walk;
+ use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;
- let files = Walk::new(&root).filter_map(|entry| {
+
+ // We want to exclude files that the editor can't handle yet
+ let mut type_builder = TypesBuilder::new();
+ let mut walk_builder = WalkBuilder::new(&root);
+ let walk_builder = match type_builder.add(
+ "compressed",
+ "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
+ ) {
+ Err(_) => &walk_builder,
+ _ => {
+ type_builder.negate("all");
+ let excluded_types = type_builder.build().unwrap();
+ walk_builder.types(excluded_types)
+ }
+ };
+
+ let files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?;
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if entry.path().is_dir() {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 341235ee..3e805fac 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
-use std::{borrow::Cow, collections::HashMap, path::PathBuf};
+use std::{
+ borrow::Cow,
+ collections::HashMap,
+ io::Read,
+ path::{Path, PathBuf},
+};
use crate::ui::{Prompt, PromptEvent};
use helix_core::Position;
@@ -23,18 +28,58 @@ use helix_view::{
};
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
+/// Biggest file size to preview in bytes
+pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
-/// File path and line number (used to align and highlight a line)
+/// File path and range of lines (used to align and highlight lines)
type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> {
picker: Picker<T>,
/// Caches paths to documents
- preview_cache: HashMap<PathBuf, Document>,
+ preview_cache: HashMap<PathBuf, CachedPreview>,
+ read_buffer: Vec<u8>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
}
+pub enum CachedPreview {
+ Document(Document),
+ Binary,
+ LargeFile,
+ NotFound,
+}
+
+// We don't store this enum in the cache so as to avoid lifetime constraints
+// from borrowing a document already opened in the editor.
+pub enum Preview<'picker, 'editor> {
+ Cached(&'picker CachedPreview),
+ EditorDocument(&'editor Document),
+}
+
+impl Preview<'_, '_> {
+ fn document(&self) -> Option<&Document> {
+ match self {
+ Preview::EditorDocument(doc) => Some(doc),
+ Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
+ _ => None,
+ }
+ }
+
+ /// Alternate text to show for the preview.
+ fn placeholder(&self) -> &str {
+ match *self {
+ Self::EditorDocument(_) => "<File preview>",
+ Self::Cached(preview) => match preview {
+ CachedPreview::Document(_) => "<File preview>",
+ CachedPreview::Binary => "<Binary file>",
+ CachedPreview::LargeFile => "<File too large to preview>",
+ CachedPreview::NotFound => "<File not found>",
+ },
+ }
+ }
+}
+
impl<T> FilePicker<T> {
pub fn new(
options: Vec<T>,
@@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(),
+ read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn),
}
}
@@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
})
}
- fn calculate_preview(&mut self, editor: &Editor) {
- if let Some((path, _line)) = self.current_file(editor) {
- if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
- // TODO: enable syntax highlighting; blocked by async rendering
- let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
- self.preview_cache.insert(path, doc);
- }
+ /// Get (cached) preview for a given path. If a document corresponding
+ /// to the path is already open in the editor, it is used instead.
+ fn get_preview<'picker, 'editor>(
+ &'picker mut self,
+ path: &Path,
+ editor: &'editor Editor,
+ ) -> Preview<'picker, 'editor> {
+ if let Some(doc) = editor.document_by_path(path) {
+ return Preview::EditorDocument(doc);
+ }
+
+ if self.preview_cache.contains_key(path) {
+ return Preview::Cached(&self.preview_cache[path]);
}
+
+ let data = std::fs::File::open(path).and_then(|file| {
+ let metadata = file.metadata()?;
+ // Read up to 1kb to detect the content type
+ let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
+ let content_type = content_inspector::inspect(&self.read_buffer[..n]);
+ self.read_buffer.clear();
+ Ok((metadata, content_type))
+ });
+ let preview = data
+ .map(
+ |(metadata, content_type)| match (metadata.len(), content_type) {
+ (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
+ (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
+ _ => {
+ // TODO: enable syntax highlighting; blocked by async rendering
+ Document::open(path, None, Some(&editor.theme), None)
+ .map(CachedPreview::Document)
+ .unwrap_or(CachedPreview::NotFound)
+ }
+ },
+ )
+ .unwrap_or(CachedPreview::NotFound);
+ self.preview_cache.insert(path.to_owned(), preview);
+ Preview::Cached(&self.preview_cache[path])
}
}
@@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> {
// |picker | | |
// | | | |
// +---------+ +---------+
- self.calculate_preview(cx.editor);
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
+ let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background);
let picker_width = if render_preview {
@@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
horizontal: 1,
};
let inner = inner.inner(&margin);
-
block.render(preview_area, surface);
- if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
- cx.editor
- .document_by_path(&path)
- .or_else(|| self.preview_cache.get(&path))
- .zip(Some(range))
- }) {
+ if let Some((path, range)) = self.current_file(cx.editor) {
+ let preview = self.get_preview(&path, cx.editor);
+ let doc = match preview.document() {
+ Some(doc) => doc,
+ None => {
+ let alt_text = preview.placeholder();
+ let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
+ let y = inner.y + inner.height / 2;
+ surface.set_stringn(x, y, alt_text, inner.width as usize, text);
+ return;
+ }
+ };
+
// align to middle
- let first_line = line
+ let first_line = range
.map(|(start, end)| {
let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2);
@@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
);
// highlight the line
- if let Some((start, end)) = line {
+ if let Some((start, end)) = range {
let offset = start.saturating_sub(first_line) as u16;
surface.set_style(
Rect::new(
@@ -234,37 +317,28 @@ impl<T> Picker<T> {
}
pub fn score(&mut self) {
- // need to borrow via pattern match otherwise it complains about simultaneous borrow
- let Self {
- ref mut matcher,
- ref mut matches,
- ref filters,
- ref format_fn,
- ..
- } = *self;
-
let pattern = &self.prompt.line;
// reuse the matches allocation
- matches.clear();
- matches.extend(
+ self.matches.clear();
+ self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
- if !filters.is_empty() {
- filters.binary_search(&index).ok()?;
+ if !self.filters.is_empty() {
+ self.filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
- let text = (format_fn)(option);
+ let text = (self.format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
- matcher
+ self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
- matches.sort_unstable_by_key(|(_, score)| -score);
+ self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position
self.cursor = 0;
@@ -338,6 +412,10 @@ impl<T: 'static> Component for Picker<T> {
..
}
| KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
@@ -351,6 +429,10 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::Tab, ..
}
| KeyEvent {
+ code: KeyCode::Char('j'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
@@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
- code: KeyCode::Char('h'),
+ code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
text_style
},
true,
+ true,
);
}
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 56335fb3..593fd934 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -186,6 +186,11 @@ impl Prompt {
self.exit_selection();
}
+ pub fn insert_str(&mut self, s: &str) {
+ self.line.insert_str(self.cursor, s);
+ self.cursor += s.len();
+ }
+
pub fn move_cursor(&mut self, movement: Movement) {
let pos = self.eval_movement(movement);
self.cursor = pos
@@ -475,6 +480,26 @@ impl Component for Prompt {
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
KeyEvent {
+ code: KeyCode::Char('s'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ use helix_core::textobject;
+ let range = textobject::textobject_word(
+ text,
+ doc.selection(view.id).primary(),
+ textobject::TextObject::Inside,
+ 1,
+ );
+ let line = text.slice(range.from()..range.to()).to_string();
+ if !line.is_empty() {
+ self.insert_str(line.as_str());
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
+ }
+ }
+ KeyEvent {
code: KeyCode::Enter,
..
} => {
@@ -502,6 +527,7 @@ impl Component for Prompt {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
@@ -515,15 +541,22 @@ impl Component for Prompt {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
code: KeyCode::Tab, ..
- } => self.change_completion_selection(CompletionDirection::Forward),
+ } => {
+ self.change_completion_selection(CompletionDirection::Forward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
+ }
KeyEvent {
code: KeyCode::BackTab,
..
- } => self.change_completion_selection(CompletionDirection::Backward),
+ } => {
+ self.change_completion_selection(CompletionDirection::Backward);
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update)
+ }
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,