Merge pull request #7264 from the-mikedavis/merge-picker-and-filepicker
Merge FilePicker into Picker
This commit is contained in:
commit
06d63d6ac1
5 changed files with 488 additions and 554 deletions
|
@ -55,8 +55,8 @@ use crate::{
|
|||
job::Callback,
|
||||
keymap::ReverseKeymap,
|
||||
ui::{
|
||||
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
|
||||
FilePicker, Picker, Popup, Prompt, PromptEvent,
|
||||
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
|
||||
Popup, Prompt, PromptEvent,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -2156,7 +2156,7 @@ fn global_search(cx: &mut Context) {
|
|||
return;
|
||||
}
|
||||
|
||||
let picker = FilePicker::new(
|
||||
let picker = Picker::new(
|
||||
all_matches,
|
||||
current_path,
|
||||
move |cx, FileResult { path, line_num }, action| {
|
||||
|
@ -2184,11 +2184,9 @@ fn global_search(cx: &mut Context) {
|
|||
|
||||
doc.set_selection(view.id, Selection::single(start, end));
|
||||
align_view(doc, view, Align::Center);
|
||||
},
|
||||
|_editor, FileResult { path, line_num }| {
|
||||
}).with_preview(|_editor, FileResult { path, line_num }| {
|
||||
Some((path.clone().into(), Some((*line_num, *line_num))))
|
||||
},
|
||||
);
|
||||
});
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
},
|
||||
));
|
||||
|
@ -2579,22 +2577,18 @@ fn buffer_picker(cx: &mut Context) {
|
|||
// mru
|
||||
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
|
||||
|
||||
let picker = FilePicker::new(
|
||||
items,
|
||||
(),
|
||||
|cx, meta, action| {
|
||||
cx.editor.switch(meta.id, action);
|
||||
},
|
||||
|editor, meta| {
|
||||
let doc = &editor.documents.get(&meta.id)?;
|
||||
let &view_id = doc.selections().keys().next()?;
|
||||
let line = doc
|
||||
.selection(view_id)
|
||||
.primary()
|
||||
.cursor_line(doc.text().slice(..));
|
||||
Some((meta.id.into(), Some((line, line))))
|
||||
},
|
||||
);
|
||||
let picker = Picker::new(items, (), |cx, meta, action| {
|
||||
cx.editor.switch(meta.id, action);
|
||||
})
|
||||
.with_preview(|editor, meta| {
|
||||
let doc = &editor.documents.get(&meta.id)?;
|
||||
let &view_id = doc.selections().keys().next()?;
|
||||
let line = doc
|
||||
.selection(view_id)
|
||||
.primary()
|
||||
.cursor_line(doc.text().slice(..));
|
||||
Some((meta.id.into(), Some((line, line))))
|
||||
});
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
|
@ -2660,7 +2654,7 @@ fn jumplist_picker(cx: &mut Context) {
|
|||
}
|
||||
};
|
||||
|
||||
let picker = FilePicker::new(
|
||||
let picker = Picker::new(
|
||||
cx.editor
|
||||
.tree
|
||||
.views()
|
||||
|
@ -2678,12 +2672,12 @@ fn jumplist_picker(cx: &mut Context) {
|
|||
doc.set_selection(view.id, meta.selection.clone());
|
||||
view.ensure_cursor_in_view_center(doc, config.scrolloff);
|
||||
},
|
||||
|editor, meta| {
|
||||
let doc = &editor.documents.get(&meta.id)?;
|
||||
let line = meta.selection.primary().cursor_line(doc.text().slice(..));
|
||||
Some((meta.id.into(), Some((line, line))))
|
||||
},
|
||||
);
|
||||
)
|
||||
.with_preview(|editor, meta| {
|
||||
let doc = &editor.documents.get(&meta.id)?;
|
||||
let line = meta.selection.primary().cursor_line(doc.text().slice(..));
|
||||
Some((meta.id.into(), Some((line, line))))
|
||||
});
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::{Context, Editor};
|
|||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
job::{Callback, Jobs},
|
||||
ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
|
||||
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text},
|
||||
};
|
||||
use dap::{StackFrame, Thread, ThreadStates};
|
||||
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
|
||||
|
@ -73,21 +73,19 @@ fn thread_picker(
|
|||
let debugger = debugger!(editor);
|
||||
|
||||
let thread_states = debugger.thread_states.clone();
|
||||
let picker = FilePicker::new(
|
||||
threads,
|
||||
thread_states,
|
||||
move |cx, thread, _action| callback_fn(cx.editor, thread),
|
||||
move |editor, thread| {
|
||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||
let frame = frames.get(0)?;
|
||||
let path = frame.source.as_ref()?.path.clone()?;
|
||||
let pos = Some((
|
||||
frame.line.saturating_sub(1),
|
||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||
));
|
||||
Some((path.into(), pos))
|
||||
},
|
||||
);
|
||||
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
|
||||
callback_fn(cx.editor, thread)
|
||||
})
|
||||
.with_preview(move |editor, thread| {
|
||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||
let frame = frames.get(0)?;
|
||||
let path = frame.source.as_ref()?.path.clone()?;
|
||||
let pos = Some((
|
||||
frame.line.saturating_sub(1),
|
||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||
));
|
||||
Some((path.into(), pos))
|
||||
});
|
||||
compositor.push(Box::new(picker));
|
||||
},
|
||||
);
|
||||
|
@ -728,39 +726,35 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
|
|||
|
||||
let frames = debugger.stack_frames[&thread_id].clone();
|
||||
|
||||
let picker = FilePicker::new(
|
||||
frames,
|
||||
(),
|
||||
move |cx, frame, _action| {
|
||||
let debugger = debugger!(cx.editor);
|
||||
// TODO: this should be simpler to find
|
||||
let pos = debugger.stack_frames[&thread_id]
|
||||
.iter()
|
||||
.position(|f| f.id == frame.id);
|
||||
debugger.active_frame = pos;
|
||||
let picker = Picker::new(frames, (), move |cx, frame, _action| {
|
||||
let debugger = debugger!(cx.editor);
|
||||
// TODO: this should be simpler to find
|
||||
let pos = debugger.stack_frames[&thread_id]
|
||||
.iter()
|
||||
.position(|f| f.id == frame.id);
|
||||
debugger.active_frame = pos;
|
||||
|
||||
let frame = debugger.stack_frames[&thread_id]
|
||||
.get(pos.unwrap_or(0))
|
||||
.cloned();
|
||||
if let Some(frame) = &frame {
|
||||
jump_to_stack_frame(cx.editor, frame);
|
||||
}
|
||||
},
|
||||
move |_editor, frame| {
|
||||
frame
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| source.path.clone())
|
||||
.map(|path| {
|
||||
(
|
||||
path.into(),
|
||||
Some((
|
||||
frame.line.saturating_sub(1),
|
||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||
)),
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
let frame = debugger.stack_frames[&thread_id]
|
||||
.get(pos.unwrap_or(0))
|
||||
.cloned();
|
||||
if let Some(frame) = &frame {
|
||||
jump_to_stack_frame(cx.editor, frame);
|
||||
}
|
||||
})
|
||||
.with_preview(move |_editor, frame| {
|
||||
frame
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| source.path.clone())
|
||||
.map(|path| {
|
||||
(
|
||||
path.into(),
|
||||
Some((
|
||||
frame.line.saturating_sub(1),
|
||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||
)),
|
||||
)
|
||||
})
|
||||
});
|
||||
cx.push_layer(Box::new(picker))
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ use crate::{
|
|||
compositor::{self, Compositor},
|
||||
job::Callback,
|
||||
ui::{
|
||||
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
|
||||
Popup, PromptEvent,
|
||||
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
|
||||
PromptEvent,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -236,48 +236,44 @@ fn jump_to_location(
|
|||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
|
||||
type SymbolPicker = FilePicker<SymbolInformationItem>;
|
||||
type SymbolPicker = Picker<SymbolInformationItem>;
|
||||
|
||||
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
FilePicker::new(
|
||||
symbols,
|
||||
current_path.clone(),
|
||||
move |cx, item, action| {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
push_jump(view, doc);
|
||||
Picker::new(symbols, current_path.clone(), move |cx, item, action| {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
push_jump(view, doc);
|
||||
|
||||
if current_path.as_ref() != Some(&item.symbol.location.uri) {
|
||||
let uri = &item.symbol.location.uri;
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
||||
cx.editor.set_error(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = cx.editor.open(&path, action) {
|
||||
let err = format!("failed to open document: {}: {}", uri, err);
|
||||
log::error!("{}", err);
|
||||
if current_path.as_ref() != Some(&item.symbol.location.uri) {
|
||||
let uri = &item.symbol.location.uri;
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
||||
cx.editor.set_error(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = cx.editor.open(&path, action) {
|
||||
let err = format!("failed to open document: {}: {}", uri, err);
|
||||
log::error!("{}", err);
|
||||
cx.editor.set_error(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
if let Some(range) =
|
||||
lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
|
||||
{
|
||||
// we flip the range so that the cursor sits on the start of the symbol
|
||||
// (for example start of the function).
|
||||
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
},
|
||||
move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
|
||||
)
|
||||
if let Some(range) =
|
||||
lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
|
||||
{
|
||||
// we flip the range so that the cursor sits on the start of the symbol
|
||||
// (for example start of the function).
|
||||
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
})
|
||||
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
|
||||
.truncate_start(false)
|
||||
}
|
||||
|
||||
|
@ -292,7 +288,7 @@ fn diag_picker(
|
|||
diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
||||
current_path: Option<lsp::Url>,
|
||||
format: DiagnosticsFormat,
|
||||
) -> FilePicker<PickerDiagnostic> {
|
||||
) -> Picker<PickerDiagnostic> {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
|
||||
// flatten the map to a vec of (url, diag) pairs
|
||||
|
@ -318,7 +314,7 @@ fn diag_picker(
|
|||
error: cx.editor.theme.get("error"),
|
||||
};
|
||||
|
||||
FilePicker::new(
|
||||
Picker::new(
|
||||
flat_diag,
|
||||
(styles, format),
|
||||
move |cx,
|
||||
|
@ -345,11 +341,11 @@ fn diag_picker(
|
|||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
},
|
||||
move |_editor, PickerDiagnostic { url, diag, .. }| {
|
||||
let location = lsp::Location::new(url.clone(), diag.range);
|
||||
Some(location_to_file_location(&location))
|
||||
},
|
||||
)
|
||||
.with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
|
||||
let location = lsp::Location::new(url.clone(), diag.range);
|
||||
Some(location_to_file_location(&location))
|
||||
})
|
||||
.truncate_start(false)
|
||||
}
|
||||
|
||||
|
@ -1047,14 +1043,10 @@ fn goto_impl(
|
|||
editor.set_error("No definition found.");
|
||||
}
|
||||
_locations => {
|
||||
let picker = FilePicker::new(
|
||||
locations,
|
||||
cwdir,
|
||||
move |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||
},
|
||||
move |_editor, location| Some(location_to_file_location(location)),
|
||||
);
|
||||
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||
})
|
||||
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ pub use completion::{Completion, CompletionItem};
|
|||
pub use editor::EditorView;
|
||||
pub use markdown::Markdown;
|
||||
pub use menu::Menu;
|
||||
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
|
||||
pub use picker::{DynamicPicker, FileLocation, Picker};
|
||||
pub use popup::Popup;
|
||||
pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
|
@ -158,7 +158,7 @@ pub fn regex_prompt(
|
|||
cx.push_layer(Box::new(prompt));
|
||||
}
|
||||
|
||||
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
|
||||
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
|
||||
use ignore::{types::TypesBuilder, WalkBuilder};
|
||||
use std::time::Instant;
|
||||
|
||||
|
@ -217,21 +217,17 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
|
|||
|
||||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
|
||||
|
||||
FilePicker::new(
|
||||
files,
|
||||
root,
|
||||
move |cx, path: &PathBuf, action| {
|
||||
if let Err(e) = cx.editor.open(path, action) {
|
||||
let err = if let Some(err) = e.source() {
|
||||
format!("{}", err)
|
||||
} else {
|
||||
format!("unable to open \"{}\"", path.display())
|
||||
};
|
||||
cx.editor.set_error(err);
|
||||
}
|
||||
},
|
||||
|_editor, path| Some((path.clone().into(), None)),
|
||||
)
|
||||
Picker::new(files, root, move |cx, path: &PathBuf, action| {
|
||||
if let Err(e) = cx.editor.open(path, action) {
|
||||
let err = if let Some(err) = e.source() {
|
||||
format!("{}", err)
|
||||
} else {
|
||||
format!("unable to open \"{}\"", path.display())
|
||||
};
|
||||
cx.editor.set_error(err);
|
||||
}
|
||||
})
|
||||
.with_preview(|_editor, path| Some((path.clone().into(), None)))
|
||||
}
|
||||
|
||||
pub mod completers {
|
||||
|
|
|
@ -77,16 +77,6 @@ type FileCallback<T> = Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>;
|
|||
/// File path and range of lines (used to align and highlight lines)
|
||||
pub type FileLocation = (PathOrId, Option<(usize, usize)>);
|
||||
|
||||
pub struct FilePicker<T: Item> {
|
||||
picker: Picker<T>,
|
||||
pub truncate_start: bool,
|
||||
/// Caches paths to documents
|
||||
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: FileCallback<T>,
|
||||
}
|
||||
|
||||
pub enum CachedPreview {
|
||||
Document(Box<Document>),
|
||||
Binary,
|
||||
|
@ -124,325 +114,6 @@ impl Preview<'_, '_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Item + 'static> FilePicker<T> {
|
||||
pub fn new(
|
||||
options: Vec<T>,
|
||||
editor_data: T::Data,
|
||||
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
||||
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
|
||||
) -> Self {
|
||||
let truncate_start = true;
|
||||
let mut picker = Picker::new(options, editor_data, callback_fn);
|
||||
picker.truncate_start = truncate_start;
|
||||
|
||||
Self {
|
||||
picker,
|
||||
truncate_start,
|
||||
preview_cache: HashMap::new(),
|
||||
read_buffer: Vec::with_capacity(1024),
|
||||
file_fn: Box::new(preview_fn),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn truncate_start(mut self, truncate_start: bool) -> Self {
|
||||
self.truncate_start = truncate_start;
|
||||
self.picker.truncate_start = truncate_start;
|
||||
self
|
||||
}
|
||||
|
||||
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||
self.picker
|
||||
.selection()
|
||||
.and_then(|current| (self.file_fn)(editor, current))
|
||||
.and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
|
||||
}
|
||||
|
||||
/// 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_or_id: PathOrId,
|
||||
editor: &'editor Editor,
|
||||
) -> Preview<'picker, 'editor> {
|
||||
match path_or_id {
|
||||
PathOrId::Path(path) => {
|
||||
let path = &path;
|
||||
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, None, editor.config.clone())
|
||||
.map(|doc| CachedPreview::Document(Box::new(doc)))
|
||||
.unwrap_or(CachedPreview::NotFound)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or(CachedPreview::NotFound);
|
||||
self.preview_cache.insert(path.to_owned(), preview);
|
||||
Preview::Cached(&self.preview_cache[path])
|
||||
}
|
||||
PathOrId::Id(id) => {
|
||||
let doc = editor.documents.get(&id).unwrap();
|
||||
Preview::EditorDocument(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
|
||||
let Some((current_file, _)) = self.current_file(cx.editor) else {
|
||||
return EventResult::Consumed(None)
|
||||
};
|
||||
|
||||
// Try to find a document in the cache
|
||||
let doc = match ¤t_file {
|
||||
PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
|
||||
PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
|
||||
Some(CachedPreview::Document(ref mut doc)) => doc,
|
||||
_ => return EventResult::Consumed(None),
|
||||
},
|
||||
};
|
||||
|
||||
let mut callback: Option<compositor::Callback> = None;
|
||||
|
||||
// Then attempt to highlight it if it has no language set
|
||||
if doc.language_config().is_none() {
|
||||
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
|
||||
doc.language = Some(language_config.clone());
|
||||
let text = doc.text().clone();
|
||||
let loader = cx.editor.syn_loader.clone();
|
||||
let job = tokio::task::spawn_blocking(move || {
|
||||
let syntax = language_config
|
||||
.highlight_config(&loader.scopes())
|
||||
.and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
|
||||
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let Some(syntax) = syntax else {
|
||||
log::info!("highlighting picker item failed");
|
||||
return
|
||||
};
|
||||
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
|
||||
log::info!("picker closed before syntax highlighting finished");
|
||||
return
|
||||
};
|
||||
// Try to find a document in the cache
|
||||
let doc = match current_file {
|
||||
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
|
||||
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
|
||||
Some(CachedPreview::Document(ref mut doc)) => doc,
|
||||
_ => return,
|
||||
},
|
||||
};
|
||||
doc.syntax = Some(syntax);
|
||||
};
|
||||
Callback::EditorCompositor(Box::new(callback))
|
||||
});
|
||||
let tmp: compositor::Callback = Box::new(move |_, ctx| {
|
||||
ctx.jobs
|
||||
.callback(job.map(|res| res.map_err(anyhow::Error::from)))
|
||||
});
|
||||
callback = Some(Box::new(tmp))
|
||||
}
|
||||
}
|
||||
|
||||
// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
|
||||
// but it could be interesting in the future
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item + 'static> Component for FilePicker<T> {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// +---------+ +---------+
|
||||
// |prompt | |preview |
|
||||
// +---------+ | |
|
||||
// |picker | | |
|
||||
// | | | |
|
||||
// +---------+ +---------+
|
||||
|
||||
let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
|
||||
// -- 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 {
|
||||
area.width / 2
|
||||
} else {
|
||||
area.width
|
||||
};
|
||||
|
||||
let picker_area = area.with_width(picker_width);
|
||||
self.picker.render(picker_area, surface, cx);
|
||||
|
||||
if !render_preview {
|
||||
return;
|
||||
}
|
||||
|
||||
let preview_area = area.clip_left(picker_width);
|
||||
|
||||
// don't like this but the lifetime sucks
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
|
||||
// calculate the inner area inside the box
|
||||
let inner = block.inner(preview_area);
|
||||
// 1 column gap on either side
|
||||
let margin = Margin::horizontal(1);
|
||||
let inner = inner.inner(&margin);
|
||||
block.render(preview_area, surface);
|
||||
|
||||
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 = range
|
||||
.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 = ViewPosition {
|
||||
anchor: doc.text().line_to_char(first_line),
|
||||
horizontal_offset: 0,
|
||||
vertical_offset: 0,
|
||||
};
|
||||
|
||||
let mut highlights = EditorView::doc_syntax_highlights(
|
||||
doc,
|
||||
offset.anchor,
|
||||
area.height,
|
||||
&cx.editor.theme,
|
||||
);
|
||||
for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
|
||||
if spans.is_empty() {
|
||||
continue;
|
||||
}
|
||||
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
|
||||
}
|
||||
let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
|
||||
|
||||
if let Some((start, end)) = range {
|
||||
let style = cx
|
||||
.editor
|
||||
.theme
|
||||
.try_get("ui.highlight")
|
||||
.unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
|
||||
let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
|
||||
if (start..=end).contains(&pos.doc_line) {
|
||||
let area = Rect::new(
|
||||
renderer.viewport.x,
|
||||
renderer.viewport.y + pos.visual_line,
|
||||
renderer.viewport.width,
|
||||
1,
|
||||
);
|
||||
renderer.surface.set_style(area, style)
|
||||
}
|
||||
};
|
||||
decorations.push(Box::new(draw_highlight))
|
||||
}
|
||||
|
||||
render_document(
|
||||
surface,
|
||||
inner,
|
||||
doc,
|
||||
offset,
|
||||
// TODO: compute text annotations asynchronously here (like inlay hints)
|
||||
&TextAnnotations::default(),
|
||||
highlights,
|
||||
&cx.editor.theme,
|
||||
&mut decorations,
|
||||
&mut [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
|
||||
if let Event::IdleTimeout = event {
|
||||
return self.handle_idle_timeout(ctx);
|
||||
}
|
||||
// TODO: keybinds for scrolling preview
|
||||
self.picker.handle_event(event, ctx)
|
||||
}
|
||||
|
||||
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
|
||||
self.picker.cursor(area, ctx)
|
||||
}
|
||||
|
||||
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
|
||||
let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW {
|
||||
width / 2
|
||||
} else {
|
||||
width
|
||||
};
|
||||
self.picker.required_size((picker_width, height))?;
|
||||
Some((width, height))
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<&'static str> {
|
||||
Some("file-picker")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
struct PickerMatch {
|
||||
score: i64,
|
||||
index: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl PickerMatch {
|
||||
fn key(&self) -> impl Ord {
|
||||
(cmp::Reverse(self.score), self.len, self.index)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PickerMatch {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PickerMatch {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.key().cmp(&other.key())
|
||||
}
|
||||
}
|
||||
|
||||
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
|
||||
|
||||
pub struct Picker<T: Item> {
|
||||
options: Vec<T>,
|
||||
editor_data: T::Data,
|
||||
|
@ -457,17 +128,22 @@ pub struct Picker<T: Item> {
|
|||
// pattern: String,
|
||||
prompt: Prompt,
|
||||
previous_pattern: (String, FuzzyQuery),
|
||||
/// Whether to truncate the start (default true)
|
||||
pub truncate_start: bool,
|
||||
/// Whether to show the preview panel (default true)
|
||||
show_preview: bool,
|
||||
/// Constraints for tabular formatting
|
||||
widths: Vec<Constraint>,
|
||||
|
||||
callback_fn: PickerCallback<T>,
|
||||
|
||||
pub truncate_start: bool,
|
||||
/// Caches paths to documents
|
||||
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: Option<FileCallback<T>>,
|
||||
}
|
||||
|
||||
impl<T: Item> Picker<T> {
|
||||
impl<T: Item + 'static> Picker<T> {
|
||||
pub fn new(
|
||||
options: Vec<T>,
|
||||
editor_data: T::Data,
|
||||
|
@ -493,6 +169,9 @@ impl<T: Item> Picker<T> {
|
|||
callback_fn: Box::new(callback_fn),
|
||||
completion_height: 0,
|
||||
widths: Vec::new(),
|
||||
preview_cache: HashMap::new(),
|
||||
read_buffer: Vec::with_capacity(1024),
|
||||
file_fn: None,
|
||||
};
|
||||
|
||||
picker.calculate_column_widths();
|
||||
|
@ -513,6 +192,19 @@ impl<T: Item> Picker<T> {
|
|||
picker
|
||||
}
|
||||
|
||||
pub fn truncate_start(mut self, truncate_start: bool) -> Self {
|
||||
self.truncate_start = truncate_start;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_preview(
|
||||
mut self,
|
||||
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
|
||||
) -> Self {
|
||||
self.file_fn = Some(Box::new(preview_fn));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_options(&mut self, new_options: Vec<T>) {
|
||||
self.options = new_options;
|
||||
self.cursor = 0;
|
||||
|
@ -679,92 +371,127 @@ impl<T: Item> Picker<T> {
|
|||
}
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
}
|
||||
|
||||
// process:
|
||||
// - read all the files into a list, maxed out at a large value
|
||||
// - on input change:
|
||||
// - score all the names in relation to input
|
||||
|
||||
impl<T: Item + 'static> Component for Picker<T> {
|
||||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||
self.completion_height = viewport.1.saturating_sub(4);
|
||||
Some(viewport)
|
||||
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||
self.selection()
|
||||
.and_then(|current| (self.file_fn.as_ref()?)(editor, current))
|
||||
.and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
||||
let key_event = match event {
|
||||
Event::Key(event) => *event,
|
||||
Event::Paste(..) => return self.prompt_handle_event(event, cx),
|
||||
Event::Resize(..) => return EventResult::Consumed(None),
|
||||
_ => return EventResult::Ignored(None),
|
||||
/// 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_or_id: PathOrId,
|
||||
editor: &'editor Editor,
|
||||
) -> Preview<'picker, 'editor> {
|
||||
match path_or_id {
|
||||
PathOrId::Path(path) => {
|
||||
let path = &path;
|
||||
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, None, editor.config.clone())
|
||||
.map(|doc| CachedPreview::Document(Box::new(doc)))
|
||||
.unwrap_or(CachedPreview::NotFound)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or(CachedPreview::NotFound);
|
||||
self.preview_cache.insert(path.to_owned(), preview);
|
||||
Preview::Cached(&self.preview_cache[path])
|
||||
}
|
||||
PathOrId::Id(id) => {
|
||||
let doc = editor.documents.get(&id).unwrap();
|
||||
Preview::EditorDocument(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
|
||||
let Some((current_file, _)) = self.current_file(cx.editor) else {
|
||||
return EventResult::Consumed(None)
|
||||
};
|
||||
|
||||
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| {
|
||||
// remove the layer
|
||||
compositor.last_picker = compositor.pop();
|
||||
})));
|
||||
// Try to find a document in the cache
|
||||
let doc = match ¤t_file {
|
||||
PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
|
||||
PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
|
||||
Some(CachedPreview::Document(ref mut doc)) => doc,
|
||||
_ => return EventResult::Consumed(None),
|
||||
},
|
||||
};
|
||||
|
||||
// So that idle timeout retriggers
|
||||
cx.editor.reset_idle_timer();
|
||||
let mut callback: Option<compositor::Callback> = None;
|
||||
|
||||
match key_event {
|
||||
shift!(Tab) | key!(Up) | ctrl!('p') => {
|
||||
self.move_by(1, Direction::Backward);
|
||||
}
|
||||
key!(Tab) | key!(Down) | ctrl!('n') => {
|
||||
self.move_by(1, Direction::Forward);
|
||||
}
|
||||
key!(PageDown) | ctrl!('d') => {
|
||||
self.page_down();
|
||||
}
|
||||
key!(PageUp) | ctrl!('u') => {
|
||||
self.page_up();
|
||||
}
|
||||
key!(Home) => {
|
||||
self.to_start();
|
||||
}
|
||||
key!(End) => {
|
||||
self.to_end();
|
||||
}
|
||||
key!(Esc) | ctrl!('c') => {
|
||||
return close_fn;
|
||||
}
|
||||
alt!(Enter) => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(cx, option, Action::Load);
|
||||
}
|
||||
}
|
||||
key!(Enter) => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(cx, option, Action::Replace);
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
ctrl!('s') => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(cx, option, Action::HorizontalSplit);
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
ctrl!('v') => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(cx, option, Action::VerticalSplit);
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
ctrl!('t') => {
|
||||
self.toggle_preview();
|
||||
}
|
||||
_ => {
|
||||
self.prompt_handle_event(event, cx);
|
||||
// Then attempt to highlight it if it has no language set
|
||||
if doc.language_config().is_none() {
|
||||
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
|
||||
doc.language = Some(language_config.clone());
|
||||
let text = doc.text().clone();
|
||||
let loader = cx.editor.syn_loader.clone();
|
||||
let job = tokio::task::spawn_blocking(move || {
|
||||
let syntax = language_config
|
||||
.highlight_config(&loader.scopes())
|
||||
.and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
|
||||
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let Some(syntax) = syntax else {
|
||||
log::info!("highlighting picker item failed");
|
||||
return
|
||||
};
|
||||
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
|
||||
log::info!("picker closed before syntax highlighting finished");
|
||||
return
|
||||
};
|
||||
// Try to find a document in the cache
|
||||
let doc = match current_file {
|
||||
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
|
||||
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
|
||||
Some(CachedPreview::Document(ref mut doc)) => doc,
|
||||
_ => return,
|
||||
},
|
||||
};
|
||||
doc.syntax = Some(syntax);
|
||||
};
|
||||
Callback::EditorCompositor(Box::new(callback))
|
||||
});
|
||||
let tmp: compositor::Callback = Box::new(move |_, ctx| {
|
||||
ctx.jobs
|
||||
.callback(job.map(|res| res.map_err(anyhow::Error::from)))
|
||||
});
|
||||
callback = Some(Box::new(tmp))
|
||||
}
|
||||
}
|
||||
|
||||
EventResult::Consumed(None)
|
||||
// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
|
||||
// but it could be interesting in the future
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
}
|
||||
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let text_style = cx.editor.theme.get("ui.text");
|
||||
let selected = cx.editor.theme.get("ui.text.focus");
|
||||
let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
|
||||
|
@ -930,6 +657,178 @@ impl<T: Item + 'static> Component for Picker<T> {
|
|||
);
|
||||
}
|
||||
|
||||
fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// -- 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);
|
||||
|
||||
// don't like this but the lifetime sucks
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
|
||||
// calculate the inner area inside the box
|
||||
let inner = block.inner(area);
|
||||
// 1 column gap on either side
|
||||
let margin = Margin::horizontal(1);
|
||||
let inner = inner.inner(&margin);
|
||||
block.render(area, surface);
|
||||
|
||||
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 = range
|
||||
.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 = ViewPosition {
|
||||
anchor: doc.text().line_to_char(first_line),
|
||||
horizontal_offset: 0,
|
||||
vertical_offset: 0,
|
||||
};
|
||||
|
||||
let mut highlights = EditorView::doc_syntax_highlights(
|
||||
doc,
|
||||
offset.anchor,
|
||||
area.height,
|
||||
&cx.editor.theme,
|
||||
);
|
||||
for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
|
||||
if spans.is_empty() {
|
||||
continue;
|
||||
}
|
||||
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
|
||||
}
|
||||
let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();
|
||||
|
||||
if let Some((start, end)) = range {
|
||||
let style = cx
|
||||
.editor
|
||||
.theme
|
||||
.try_get("ui.highlight")
|
||||
.unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
|
||||
let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
|
||||
if (start..=end).contains(&pos.doc_line) {
|
||||
let area = Rect::new(
|
||||
renderer.viewport.x,
|
||||
renderer.viewport.y + pos.visual_line,
|
||||
renderer.viewport.width,
|
||||
1,
|
||||
);
|
||||
renderer.surface.set_style(area, style)
|
||||
}
|
||||
};
|
||||
decorations.push(Box::new(draw_highlight))
|
||||
}
|
||||
|
||||
render_document(
|
||||
surface,
|
||||
inner,
|
||||
doc,
|
||||
offset,
|
||||
// TODO: compute text annotations asynchronously here (like inlay hints)
|
||||
&TextAnnotations::default(),
|
||||
highlights,
|
||||
&cx.editor.theme,
|
||||
&mut decorations,
|
||||
&mut [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
|
||||
if let Event::IdleTimeout = event {
|
||||
return self.handle_idle_timeout(ctx);
|
||||
}
|
||||
// TODO: keybinds for scrolling preview
|
||||
|
||||
let key_event = match event {
|
||||
Event::Key(event) => *event,
|
||||
Event::Paste(..) => return self.prompt_handle_event(event, ctx),
|
||||
Event::Resize(..) => return EventResult::Consumed(None),
|
||||
_ => return EventResult::Ignored(None),
|
||||
};
|
||||
|
||||
let close_fn =
|
||||
EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
|
||||
// remove the layer
|
||||
compositor.last_picker = compositor.pop();
|
||||
})));
|
||||
|
||||
// So that idle timeout retriggers
|
||||
ctx.editor.reset_idle_timer();
|
||||
|
||||
match key_event {
|
||||
shift!(Tab) | key!(Up) | ctrl!('p') => {
|
||||
self.move_by(1, Direction::Backward);
|
||||
}
|
||||
key!(Tab) | key!(Down) | ctrl!('n') => {
|
||||
self.move_by(1, Direction::Forward);
|
||||
}
|
||||
key!(PageDown) | ctrl!('d') => {
|
||||
self.page_down();
|
||||
}
|
||||
key!(PageUp) | ctrl!('u') => {
|
||||
self.page_up();
|
||||
}
|
||||
key!(Home) => {
|
||||
self.to_start();
|
||||
}
|
||||
key!(End) => {
|
||||
self.to_end();
|
||||
}
|
||||
key!(Esc) | ctrl!('c') => {
|
||||
return close_fn;
|
||||
}
|
||||
alt!(Enter) => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(ctx, option, Action::Load);
|
||||
}
|
||||
}
|
||||
key!(Enter) => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(ctx, option, Action::Replace);
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
ctrl!('s') => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(ctx, option, Action::HorizontalSplit);
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
ctrl!('v') => {
|
||||
if let Some(option) = self.selection() {
|
||||
(self.callback_fn)(ctx, option, Action::VerticalSplit);
|
||||
}
|
||||
return close_fn;
|
||||
}
|
||||
ctrl!('t') => {
|
||||
self.toggle_preview();
|
||||
}
|
||||
_ => {
|
||||
self.prompt_handle_event(event, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
// calculate the inner area inside the box
|
||||
|
@ -940,8 +839,67 @@ impl<T: Item + 'static> Component for Picker<T> {
|
|||
|
||||
self.prompt.cursor(area, editor)
|
||||
}
|
||||
|
||||
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
|
||||
self.completion_height = height.saturating_sub(4);
|
||||
Some((width, height))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item + 'static> Component for Picker<T> {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// +---------+ +---------+
|
||||
// |prompt | |preview |
|
||||
// +---------+ | |
|
||||
// |picker | | |
|
||||
// | | | |
|
||||
// +---------+ +---------+
|
||||
|
||||
let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
|
||||
|
||||
let picker_width = if render_preview {
|
||||
area.width / 2
|
||||
} else {
|
||||
area.width
|
||||
};
|
||||
|
||||
let picker_area = area.with_width(picker_width);
|
||||
self.render_picker(picker_area, surface, cx);
|
||||
|
||||
if render_preview {
|
||||
let preview_area = area.clip_left(picker_width);
|
||||
self.render_preview(preview_area, surface, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
struct PickerMatch {
|
||||
score: i64,
|
||||
index: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl PickerMatch {
|
||||
fn key(&self) -> impl Ord {
|
||||
(cmp::Reverse(self.score), self.len, self.index)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PickerMatch {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PickerMatch {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.key().cmp(&other.key())
|
||||
}
|
||||
}
|
||||
|
||||
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
|
||||
|
||||
/// Returns a new list of options to replace the contents of the picker
|
||||
/// when called with the current picker query,
|
||||
pub type DynQueryCallback<T> =
|
||||
|
@ -950,7 +908,7 @@ pub type DynQueryCallback<T> =
|
|||
/// A picker that updates its contents via a callback whenever the
|
||||
/// query string changes. Useful for live grep, workspace symbols, etc.
|
||||
pub struct DynamicPicker<T: ui::menu::Item + Send> {
|
||||
file_picker: FilePicker<T>,
|
||||
file_picker: Picker<T>,
|
||||
query_callback: DynQueryCallback<T>,
|
||||
query: String,
|
||||
}
|
||||
|
@ -958,7 +916,7 @@ pub struct DynamicPicker<T: ui::menu::Item + Send> {
|
|||
impl<T: ui::menu::Item + Send> DynamicPicker<T> {
|
||||
pub const ID: &'static str = "dynamic-picker";
|
||||
|
||||
pub fn new(file_picker: FilePicker<T>, query_callback: DynQueryCallback<T>) -> Self {
|
||||
pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
|
||||
Self {
|
||||
file_picker,
|
||||
query_callback,
|
||||
|
@ -974,7 +932,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
|
|||
|
||||
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
||||
let event_result = self.file_picker.handle_event(event, cx);
|
||||
let current_query = self.file_picker.picker.prompt.line();
|
||||
let current_query = self.file_picker.prompt.line();
|
||||
|
||||
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
|
||||
return event_result;
|
||||
|
@ -990,7 +948,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
|
|||
// Wrapping of pickers in overlay is done outside the picker code,
|
||||
// so this is fragile and will break if wrapped in some other widget.
|
||||
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
|
||||
Some(overlay) => &mut overlay.content.file_picker.picker,
|
||||
Some(overlay) => &mut overlay.content.file_picker,
|
||||
None => return,
|
||||
};
|
||||
picker.set_options(new_options);
|
||||
|
|
Loading…
Reference in a new issue