mirror of https://github.com/helix-editor/helix
Merge 9fe397bd79
into 362e97e927
commit
18763ba6fe
|
@ -286,6 +286,8 @@ This layer is a kludge of mappings, mostly pickers.
|
||||||
| ----- | ----------- | ------- |
|
| ----- | ----------- | ------- |
|
||||||
| `f` | Open file picker at LSP workspace root | `file_picker` |
|
| `f` | Open file picker at LSP workspace root | `file_picker` |
|
||||||
| `F` | Open file picker at current working directory | `file_picker_in_current_directory` |
|
| `F` | Open file picker at current working directory | `file_picker_in_current_directory` |
|
||||||
|
| `e` | Open file explorer at LSP workspace root | `file_explorer` |
|
||||||
|
| `E` | Open file explorer at the opened file's directory | `file_explorer_in_current_buffer_directory`|
|
||||||
| `b` | Open buffer picker | `buffer_picker` |
|
| `b` | Open buffer picker | `buffer_picker` |
|
||||||
| `j` | Open jumplist picker | `jumplist_picker` |
|
| `j` | Open jumplist picker | `jumplist_picker` |
|
||||||
| `g` | Open changed file picker | `changed_file_picker` |
|
| `g` | Open changed file picker | `changed_file_picker` |
|
||||||
|
@ -464,6 +466,18 @@ See the documentation page on [pickers](./pickers.md) for more info.
|
||||||
| `Ctrl-t` | Toggle preview |
|
| `Ctrl-t` | Toggle preview |
|
||||||
| `Escape`, `Ctrl-c` | Close picker |
|
| `Escape`, `Ctrl-c` | Close picker |
|
||||||
|
|
||||||
|
### File Explorer
|
||||||
|
|
||||||
|
There are additional keys accessible when using the File Explorer (`Space-e` and `Space-E`).
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ----- | ------------- |
|
||||||
|
| `Alt-m` | Move selected file or directory |
|
||||||
|
| `Alt-n` | Create a new file or directory |
|
||||||
|
| `Alt-d` | Delete the selected file or directory |
|
||||||
|
| `Alt-c` | Copy the selected file |
|
||||||
|
| `Alt-y` | Yank the path to the selected file or directory |
|
||||||
|
|
||||||
## Prompt
|
## Prompt
|
||||||
|
|
||||||
Keys to use within prompt, Remapping currently not supported.
|
Keys to use within prompt, Remapping currently not supported.
|
||||||
|
|
|
@ -3091,7 +3091,7 @@ fn file_explorer(cx: &mut Context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(picker) = ui::file_explorer(root, cx.editor) {
|
if let Ok(picker) = ui::file_explorer(None, root, cx.editor) {
|
||||||
cx.push_layer(Box::new(overlaid(picker)));
|
cx.push_layer(Box::new(overlaid(picker)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3118,7 +3118,7 @@ fn file_explorer_in_current_buffer_directory(cx: &mut Context) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(picker) = ui::file_explorer(path, cx.editor) {
|
if let Ok(picker) = ui::file_explorer(None, path, cx.editor) {
|
||||||
cx.push_layer(Box::new(overlaid(picker)));
|
cx.push_layer(Box::new(overlaid(picker)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3131,7 +3131,7 @@ fn file_explorer_in_current_directory(cx: &mut Context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(picker) = ui::file_explorer(cwd, cx.editor) {
|
if let Ok(picker) = ui::file_explorer(None, cwd, cx.editor) {
|
||||||
cx.push_layer(Box::new(overlaid(picker)));
|
cx.push_layer(Box::new(overlaid(picker)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,422 @@
|
||||||
|
use std::error::Error as _;
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use helix_core::hashmap;
|
||||||
|
use helix_view::{theme::Style, Editor};
|
||||||
|
use tui::text::Span;
|
||||||
|
|
||||||
|
use crate::{alt, compositor::Context, job::Callback};
|
||||||
|
|
||||||
|
use super::prompt::Movement;
|
||||||
|
use super::{
|
||||||
|
directory_content, overlay, picker::PickerKeyHandler, Picker, PickerColumn, Prompt, PromptEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// for each path: (path to item, is the path a directory?)
|
||||||
|
type ExplorerItem = (PathBuf, bool);
|
||||||
|
/// (file explorer root, directory style)
|
||||||
|
type ExplorerData = (PathBuf, Style);
|
||||||
|
|
||||||
|
type FileExplorer = Picker<ExplorerItem, ExplorerData>;
|
||||||
|
|
||||||
|
type KeyHandler = PickerKeyHandler<ExplorerItem, ExplorerData>;
|
||||||
|
|
||||||
|
/// Create a prompt that asks for the user's confirmation before overwriting a path
|
||||||
|
fn confirm_before_overwriting<F>(
|
||||||
|
// Path that we are overwriting
|
||||||
|
overwriting: PathBuf,
|
||||||
|
// Overwrite this path with
|
||||||
|
overwrite_with: PathBuf,
|
||||||
|
cx: &mut Context,
|
||||||
|
picker_root: PathBuf,
|
||||||
|
overwrite: F,
|
||||||
|
) -> Option<Result<String, String>>
|
||||||
|
where
|
||||||
|
F: Fn(&mut Context, PathBuf, &Path) -> Option<Result<String, String>> + Send + 'static,
|
||||||
|
{
|
||||||
|
// No need for confirmation, as the path does not exist. We can freely write to it
|
||||||
|
if !overwriting.exists() {
|
||||||
|
return overwrite(cx, picker_root, &overwrite_with);
|
||||||
|
}
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| {
|
||||||
|
let prompt = Prompt::new(
|
||||||
|
format!(
|
||||||
|
"Path {} already exists. Ovewrite? (y/n):",
|
||||||
|
overwriting.display()
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
None,
|
||||||
|
crate::ui::completers::none,
|
||||||
|
move |cx, input: &str, event: PromptEvent| {
|
||||||
|
if event != PromptEvent::Validate || input != "y" {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(result) = overwrite(cx, picker_root.clone(), &overwrite_with) {
|
||||||
|
cx.editor.set_result(result);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
compositor.push(Box::new(prompt));
|
||||||
|
}));
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_file_operation_prompt<F>(
|
||||||
|
cx: &mut Context,
|
||||||
|
// Currently selected path of the picker
|
||||||
|
path: &Path,
|
||||||
|
// Text value of the prompt
|
||||||
|
prompt: fn(&Path) -> String,
|
||||||
|
// How to move the cursor
|
||||||
|
movement: Option<Movement>,
|
||||||
|
// What to fill user's input with
|
||||||
|
prefill: fn(&Path) -> String,
|
||||||
|
// Action to take when the operation runs
|
||||||
|
file_op: F,
|
||||||
|
) where
|
||||||
|
F: Fn(&mut Context, &PathBuf, String) -> Option<Result<String, String>> + Send + 'static,
|
||||||
|
{
|
||||||
|
let selected_path = path.to_path_buf();
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
||||||
|
// to be able to move selected_path
|
||||||
|
let path = selected_path.clone();
|
||||||
|
let mut prompt = Prompt::new(
|
||||||
|
prompt(&path).into(),
|
||||||
|
None,
|
||||||
|
crate::ui::completers::none,
|
||||||
|
move |cx, input: &str, event: PromptEvent| {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(result) = file_op(cx, &path, input.to_owned()) {
|
||||||
|
cx.editor.set_result(result);
|
||||||
|
} else {
|
||||||
|
cx.editor.clear_status();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
prompt.set_line(prefill(&selected_path), editor);
|
||||||
|
|
||||||
|
if let Some(movement) = movement {
|
||||||
|
log::error!("{movement:?}");
|
||||||
|
prompt.move_cursor(movement);
|
||||||
|
}
|
||||||
|
|
||||||
|
compositor.push(Box::new(prompt));
|
||||||
|
}));
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_file_explorer(cursor: u32, cx: &mut Context, root: PathBuf) {
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
||||||
|
// replace the old file explorer with the new one
|
||||||
|
compositor.pop();
|
||||||
|
if let Ok(picker) = file_explorer(Some(cursor), root, editor) {
|
||||||
|
compositor.push(Box::new(overlay::overlaid(picker)));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_explorer(
|
||||||
|
cursor: Option<u32>,
|
||||||
|
root: PathBuf,
|
||||||
|
editor: &Editor,
|
||||||
|
) -> Result<FileExplorer, std::io::Error> {
|
||||||
|
let directory_style = editor.theme.get("ui.text.directory");
|
||||||
|
let directory_content = directory_content(&root)?;
|
||||||
|
|
||||||
|
let yank_path: KeyHandler = Box::new(|cx, (path, _), _, _| {
|
||||||
|
let register = cx
|
||||||
|
.editor
|
||||||
|
.selected_register
|
||||||
|
.unwrap_or(cx.editor.config().default_yank_register);
|
||||||
|
let path = helix_stdx::path::get_relative_path(path);
|
||||||
|
let path = path.to_string_lossy().to_string();
|
||||||
|
let message = format!("Yanked path {} to register {register}", path);
|
||||||
|
|
||||||
|
match cx.editor.registers.write(register, vec![path]) {
|
||||||
|
Ok(()) => cx.editor.set_status(message),
|
||||||
|
Err(err) => cx.editor.set_error(err.to_string()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let create: KeyHandler = Box::new(|cx, (path, _), data, cursor| {
|
||||||
|
create_file_operation_prompt(
|
||||||
|
cx,
|
||||||
|
path,
|
||||||
|
|_| "Create: ".into(),
|
||||||
|
None,
|
||||||
|
|path| {
|
||||||
|
path.parent()
|
||||||
|
.map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR))
|
||||||
|
.unwrap_or_default()
|
||||||
|
},
|
||||||
|
move |cx, _, to_create_string| {
|
||||||
|
let root = data.0.clone();
|
||||||
|
let to_create = helix_stdx::path::expand_tilde(PathBuf::from(&to_create_string));
|
||||||
|
|
||||||
|
confirm_before_overwriting(
|
||||||
|
to_create.to_path_buf(),
|
||||||
|
to_create.to_path_buf(),
|
||||||
|
cx,
|
||||||
|
root,
|
||||||
|
move |cx: &mut Context, root: PathBuf, to_create: &Path| {
|
||||||
|
if to_create_string.ends_with(std::path::MAIN_SEPARATOR) {
|
||||||
|
if let Err(err_create_dir) =
|
||||||
|
fs::create_dir_all(to_create).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Unable to create directory {}: {err}",
|
||||||
|
to_create.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Some(Err(err_create_dir));
|
||||||
|
}
|
||||||
|
refresh_file_explorer(cursor, cx, root);
|
||||||
|
|
||||||
|
return Some(Ok(format!("Created directory: {}", to_create.display())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// allows to create a path like /path/to/somewhere.txt even if "to" does not exist. Creates intermediate directories
|
||||||
|
let Some(to_create_parent) = to_create.parent() else {
|
||||||
|
return Some(Err(format!(
|
||||||
|
"Failed to get parent directory of {}",
|
||||||
|
to_create.display()
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err_create_parent) = fs::create_dir_all(to_create_parent) {
|
||||||
|
return Some(Err(format!(
|
||||||
|
"Could not create intermediate directories: {err_create_parent}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err_create_file) = fs::File::create(to_create).map_err(|err| {
|
||||||
|
format!("Unable to create file {}: {err}", to_create.display())
|
||||||
|
}) {
|
||||||
|
return Some(Err(err_create_file));
|
||||||
|
};
|
||||||
|
|
||||||
|
refresh_file_explorer(cursor, cx, root);
|
||||||
|
|
||||||
|
Some(Ok(format!("Created file: {}", to_create.display())))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let move_: KeyHandler = Box::new(|cx, (path, _), data, cursor| {
|
||||||
|
create_file_operation_prompt(
|
||||||
|
cx,
|
||||||
|
path,
|
||||||
|
|path| format!("Move {} -> ", path.display()),
|
||||||
|
// move cursor before the extension
|
||||||
|
// Yazi does this and it leads to good user experience
|
||||||
|
// Most of the time when we would like to rename a file we
|
||||||
|
// don't want to change its file extension
|
||||||
|
path.extension()
|
||||||
|
.inspect(|a| log::error!("{a:?}"))
|
||||||
|
// +1 to account for the dot in the extension (`.`)
|
||||||
|
.map(|ext| Movement::BackwardChar(ext.len() + 1)),
|
||||||
|
|path| path.display().to_string(),
|
||||||
|
move |cx, move_from, move_to_string| {
|
||||||
|
let root = data.0.clone();
|
||||||
|
let move_to = helix_stdx::path::expand_tilde(PathBuf::from(&move_to_string));
|
||||||
|
|
||||||
|
confirm_before_overwriting(
|
||||||
|
move_to.to_path_buf(),
|
||||||
|
move_from.to_path_buf(),
|
||||||
|
cx,
|
||||||
|
root,
|
||||||
|
move |cx: &mut Context, root: PathBuf, move_from: &Path| {
|
||||||
|
let move_to =
|
||||||
|
helix_stdx::path::expand_tilde(PathBuf::from(&move_to_string));
|
||||||
|
|
||||||
|
if let Err(err) = cx.editor.move_path(move_from, &move_to).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Unable to move {} {} -> {}: {err}",
|
||||||
|
if move_to_string.ends_with(std::path::MAIN_SEPARATOR) {
|
||||||
|
"directory"
|
||||||
|
} else {
|
||||||
|
"file"
|
||||||
|
},
|
||||||
|
move_from.display(),
|
||||||
|
move_to.display()
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return Some(Err(err));
|
||||||
|
};
|
||||||
|
refresh_file_explorer(cursor, cx, root);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let delete: KeyHandler = Box::new(|cx, (path, _), data, cursor| {
|
||||||
|
create_file_operation_prompt(
|
||||||
|
cx,
|
||||||
|
path,
|
||||||
|
|path| format!("Delete {}? (y/n): ", path.display()),
|
||||||
|
None,
|
||||||
|
|_| "".to_string(),
|
||||||
|
move |cx, to_delete, confirmation| {
|
||||||
|
let root = data.0.clone();
|
||||||
|
if confirmation != "y" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !to_delete.exists() {
|
||||||
|
return Some(Err(format!("Path {} does not exist", to_delete.display())));
|
||||||
|
};
|
||||||
|
|
||||||
|
if to_delete.is_dir() {
|
||||||
|
if let Err(err) = fs::remove_dir_all(to_delete).map_err(|err| {
|
||||||
|
format!("Unable to delete directory {}: {err}", to_delete.display())
|
||||||
|
}) {
|
||||||
|
return Some(Err(err));
|
||||||
|
};
|
||||||
|
refresh_file_explorer(cursor, cx, root);
|
||||||
|
|
||||||
|
return Some(Ok(format!("Deleted directory: {}", to_delete.display())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = fs::remove_file(to_delete)
|
||||||
|
.map_err(|err| format!("Unable to delete file {}: {err}", to_delete.display()))
|
||||||
|
{
|
||||||
|
return Some(Err(err));
|
||||||
|
};
|
||||||
|
refresh_file_explorer(cursor, cx, root);
|
||||||
|
|
||||||
|
Some(Ok(format!("Deleted file: {}", to_delete.display())))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let copy: KeyHandler = Box::new(|cx, (path, _), data, cursor| {
|
||||||
|
create_file_operation_prompt(
|
||||||
|
cx,
|
||||||
|
path,
|
||||||
|
|path| format!("Copy {} -> ", path.display()),
|
||||||
|
None,
|
||||||
|
|path| {
|
||||||
|
path.parent()
|
||||||
|
.map(|p| format!("{}{}", p.display(), std::path::MAIN_SEPARATOR))
|
||||||
|
.unwrap_or_default()
|
||||||
|
},
|
||||||
|
move |cx, copy_from, copy_to_string| {
|
||||||
|
let root = data.0.clone();
|
||||||
|
let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(©_to_string));
|
||||||
|
|
||||||
|
if copy_from.is_dir() || copy_to_string.ends_with(std::path::MAIN_SEPARATOR) {
|
||||||
|
// TODO: support copying directories (recursively)?. This isn't built-in to the standard library
|
||||||
|
return Some(Err(format!(
|
||||||
|
"Copying directories is not supported: {} is a directory",
|
||||||
|
copy_from.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let copy_to_str = copy_to_string.to_string();
|
||||||
|
|
||||||
|
confirm_before_overwriting(
|
||||||
|
copy_to.to_path_buf(),
|
||||||
|
copy_from.to_path_buf(),
|
||||||
|
cx,
|
||||||
|
root,
|
||||||
|
move |cx: &mut Context, picker_root: PathBuf, copy_from: &Path| {
|
||||||
|
let copy_to = helix_stdx::path::expand_tilde(PathBuf::from(©_to_str));
|
||||||
|
if let Err(err) = std::fs::copy(copy_from, ©_to).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Unable to copy from file {} to {}: {err}",
|
||||||
|
copy_from.display(),
|
||||||
|
copy_to.display()
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return Some(Err(err));
|
||||||
|
};
|
||||||
|
refresh_file_explorer(cursor, cx, picker_root);
|
||||||
|
|
||||||
|
Some(Ok(format!(
|
||||||
|
"Copied contents of file {} to {}",
|
||||||
|
copy_from.display(),
|
||||||
|
copy_to.display()
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let columns = [PickerColumn::new(
|
||||||
|
"path",
|
||||||
|
|(path, is_dir): &ExplorerItem, (root, directory_style): &ExplorerData| {
|
||||||
|
let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
|
||||||
|
if *is_dir {
|
||||||
|
Span::styled(format!("{}/", name), *directory_style).into()
|
||||||
|
} else {
|
||||||
|
name.into()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
|
||||||
|
let picker = Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
|
directory_content,
|
||||||
|
(root, directory_style),
|
||||||
|
move |cx, (path, is_dir): &ExplorerItem, action| {
|
||||||
|
if *is_dir {
|
||||||
|
let new_root = helix_stdx::path::normalize(path);
|
||||||
|
let callback = Box::pin(async move {
|
||||||
|
let call: Callback =
|
||||||
|
Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
||||||
|
if let Ok(picker) = file_explorer(None, new_root, editor) {
|
||||||
|
compositor.push(Box::new(overlay::overlaid(picker)));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
|
cx.jobs.callback(callback);
|
||||||
|
} else 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_cursor(cursor.unwrap_or_default())
|
||||||
|
.with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None)))
|
||||||
|
.with_key_handlers(hashmap! {
|
||||||
|
alt!('n') => create,
|
||||||
|
alt!('m') => move_,
|
||||||
|
alt!('d') => delete,
|
||||||
|
alt!('c') => copy,
|
||||||
|
alt!('y') => yank_path,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(picker)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
mod completion;
|
mod completion;
|
||||||
mod document;
|
mod document;
|
||||||
pub(crate) mod editor;
|
pub(crate) mod editor;
|
||||||
|
mod file_explorer;
|
||||||
mod info;
|
mod info;
|
||||||
pub mod lsp;
|
pub mod lsp;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
|
@ -19,6 +20,7 @@ use crate::filter_picker_entry;
|
||||||
use crate::job::{self, Callback};
|
use crate::job::{self, Callback};
|
||||||
pub use completion::Completion;
|
pub use completion::Completion;
|
||||||
pub use editor::EditorView;
|
pub use editor::EditorView;
|
||||||
|
pub use file_explorer::file_explorer;
|
||||||
use helix_stdx::rope;
|
use helix_stdx::rope;
|
||||||
use helix_view::theme::Style;
|
use helix_view::theme::Style;
|
||||||
pub use markdown::Markdown;
|
pub use markdown::Markdown;
|
||||||
|
@ -300,56 +302,6 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
|
||||||
picker
|
picker
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>;
|
|
||||||
|
|
||||||
pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std::io::Error> {
|
|
||||||
let directory_style = editor.theme.get("ui.text.directory");
|
|
||||||
let directory_content = directory_content(&root)?;
|
|
||||||
|
|
||||||
let columns = [PickerColumn::new(
|
|
||||||
"path",
|
|
||||||
|(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| {
|
|
||||||
let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
|
|
||||||
if *is_dir {
|
|
||||||
Span::styled(format!("{}/", name), *directory_style).into()
|
|
||||||
} else {
|
|
||||||
name.into()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)];
|
|
||||||
let picker = Picker::new(
|
|
||||||
columns,
|
|
||||||
0,
|
|
||||||
directory_content,
|
|
||||||
(root, directory_style),
|
|
||||||
move |cx, (path, is_dir): &(PathBuf, bool), action| {
|
|
||||||
if *is_dir {
|
|
||||||
let new_root = helix_stdx::path::normalize(path);
|
|
||||||
let callback = Box::pin(async move {
|
|
||||||
let call: Callback =
|
|
||||||
Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
|
||||||
if let Ok(picker) = file_explorer(new_root, editor) {
|
|
||||||
compositor.push(Box::new(overlay::overlaid(picker)));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
Ok(call)
|
|
||||||
});
|
|
||||||
cx.jobs.callback(callback);
|
|
||||||
} else 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, _is_dir)| Some((path.as_path().into(), None)));
|
|
||||||
|
|
||||||
Ok(picker)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn directory_content(path: &Path) -> Result<Vec<(PathBuf, bool)>, std::io::Error> {
|
fn directory_content(path: &Path) -> Result<Vec<(PathBuf, bool)>, std::io::Error> {
|
||||||
let mut content: Vec<_> = std::fs::read_dir(path)?
|
let mut content: Vec<_> = std::fs::read_dir(path)?
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
|
@ -47,6 +47,7 @@ use helix_core::{
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
editor::Action,
|
editor::Action,
|
||||||
graphics::{CursorKind, Margin, Modifier, Rect},
|
graphics::{CursorKind, Margin, Modifier, Rect},
|
||||||
|
input::KeyEvent,
|
||||||
theme::Style,
|
theme::Style,
|
||||||
view::ViewPosition,
|
view::ViewPosition,
|
||||||
Document, DocumentId, Editor,
|
Document, DocumentId, Editor,
|
||||||
|
@ -258,6 +259,7 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
|
||||||
widths: Vec<Constraint>,
|
widths: Vec<Constraint>,
|
||||||
|
|
||||||
callback_fn: PickerCallback<T>,
|
callback_fn: PickerCallback<T>,
|
||||||
|
custom_key_handlers: PickerKeyHandlers<T, D>,
|
||||||
|
|
||||||
pub truncate_start: bool,
|
pub truncate_start: bool,
|
||||||
/// Caches paths to documents
|
/// Caches paths to documents
|
||||||
|
@ -385,6 +387,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||||
completion_height: 0,
|
completion_height: 0,
|
||||||
widths,
|
widths,
|
||||||
preview_cache: HashMap::new(),
|
preview_cache: HashMap::new(),
|
||||||
|
custom_key_handlers: HashMap::new(),
|
||||||
read_buffer: Vec::with_capacity(1024),
|
read_buffer: Vec::with_capacity(1024),
|
||||||
file_fn: None,
|
file_fn: None,
|
||||||
preview_highlight_handler: PreviewHighlightHandler::<T, D>::default().spawn(),
|
preview_highlight_handler: PreviewHighlightHandler::<T, D>::default().spawn(),
|
||||||
|
@ -392,6 +395,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_key_handlers(mut self, handlers: PickerKeyHandlers<T, D>) -> Self {
|
||||||
|
self.custom_key_handlers = handlers;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn injector(&self) -> Injector<T, D> {
|
pub fn injector(&self) -> Injector<T, D> {
|
||||||
Injector {
|
Injector {
|
||||||
dst: self.matcher.injector(),
|
dst: self.matcher.injector(),
|
||||||
|
@ -483,6 +491,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||||
.saturating_sub(1);
|
.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_cursor(mut self, cursor: u32) -> Self {
|
||||||
|
self.cursor = cursor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selection(&self) -> Option<&T> {
|
pub fn selection(&self) -> Option<&T> {
|
||||||
self.matcher
|
self.matcher
|
||||||
.snapshot()
|
.snapshot()
|
||||||
|
@ -509,6 +522,17 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||||
self.show_preview = !self.show_preview;
|
self.show_preview = !self.show_preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn custom_key_event_handler(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult {
|
||||||
|
if let (Some(callback), Some(selected)) =
|
||||||
|
(self.custom_key_handlers.get(event), self.selection())
|
||||||
|
{
|
||||||
|
callback(cx, selected, Arc::clone(&self.editor_data), self.cursor);
|
||||||
|
EventResult::Consumed(None)
|
||||||
|
} else {
|
||||||
|
EventResult::Ignored(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
||||||
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
|
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
|
||||||
self.handle_prompt_change(matches!(event, Event::Paste(_)));
|
self.handle_prompt_change(matches!(event, Event::Paste(_)));
|
||||||
|
@ -1049,6 +1073,11 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
|
||||||
EventResult::Consumed(Some(callback))
|
EventResult::Consumed(Some(callback))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// handle custom keybindings, if exist
|
||||||
|
if let EventResult::Consumed(_) = self.custom_key_event_handler(&key_event, ctx) {
|
||||||
|
return EventResult::Consumed(None);
|
||||||
|
}
|
||||||
|
|
||||||
match key_event {
|
match key_event {
|
||||||
shift!(Tab) | key!(Up) | ctrl!('p') => {
|
shift!(Tab) | key!(Up) | ctrl!('p') => {
|
||||||
self.move_by(1, Direction::Backward);
|
self.move_by(1, Direction::Backward);
|
||||||
|
@ -1168,3 +1197,5 @@ impl<T: 'static + Send + Sync, D> Drop for Picker<T, D> {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
|
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
|
||||||
|
pub type PickerKeyHandler<T, D> = Box<dyn Fn(&mut Context, &T, Arc<D>, u32) + 'static>;
|
||||||
|
pub type PickerKeyHandlers<T, D> = HashMap<KeyEvent, PickerKeyHandler<T, D>>;
|
||||||
|
|
|
@ -1331,6 +1331,14 @@ impl Editor {
|
||||||
self.status_msg = Some((error, Severity::Error));
|
self.status_msg = Some((error, Severity::Error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_result<T: Into<Cow<'static, str>>>(&mut self, result: Result<T, T>) {
|
||||||
|
match result {
|
||||||
|
Ok(ok) => self.set_status(ok),
|
||||||
|
Err(err) => self.set_error(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_warning<T: Into<Cow<'static, str>>>(&mut self, warning: T) {
|
pub fn set_warning<T: Into<Cow<'static, str>>>(&mut self, warning: T) {
|
||||||
let warning = warning.into();
|
let warning = warning.into();
|
||||||
|
|
Loading…
Reference in New Issue