tree helper and file explorer

imgbot
cossonleo 2 years ago
parent 28c5e2170e
commit 0e04c4c93c

@ -147,6 +147,8 @@ auto-pairs = false # defaults to `true`
The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table:
Example
```toml
[editor.auto-pairs]
'(' = ')'
@ -229,3 +231,12 @@ Example:
render = true
character = "╎"
```
### `[editor.explorer]` Section
Sets explorer side width and style.
| Key | Description | Default |
| --- | ----------- | ------- |
| `column-width` | explorer side width | 30 |
| `style` | explorer item style, tree or list | tree |
| `position` | explorer widget position, embed or overlay | overlay |

@ -275,6 +275,8 @@ This layer is a kludge of mappings, mostly pickers.
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
| `?` | Open command palette | `command_palette` |
| `e` | Open or focus explorer | `toggle_or_focus_explorer` |
| `E` | open explorer recursion | `open_explorer_recursion` |
> TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
@ -402,3 +404,35 @@ Keys to use within prompt, Remapping currently not supported.
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected |
# File explorer
Keys to use within explorer, Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Escape` | Back to editor |
| `Ctrl-c` | Close explorer |
| `Enter` | Open file or toggle dir selected |
| `b` | Back to current root's parent |
| `f` | Filter items |
| `z` | Fold currrent level |
| `k`, `Shift-Tab`, `Up` | select previous item |
| `j`, `Tab`, `Down` | select next item |
| `h` | Scroll left |
| `l` | Scroll right |
| `G` | Move to last item |
| `Ctrl-d` | Move down half page |
| `Ctrl-u` | Move up half page |
| `Shift-d` | Move down a page |
| `Shift-u` | Move up a page |
| `/` | Search item |
| `?` | Search item reverse |
| `n` | Repeat last search |
| `Shift-n` | Repeat last search reverse |
| `gg` | Move to first item |
| `ge` | Move to last item |
| `gc` | Make current dir as root dir |
| `mf` | Create new file under current item's parent |
| `md` | Create new dir under current item's parent |
| `rf` | Remove file selected |
| `rd` | Remove dir selected |

@ -432,7 +432,10 @@ impl MappableCommand {
decrement, "Decrement item under cursor",
record_macro, "Record macro",
replay_macro, "Replay macro",
command_palette, "Open command palette",
command_palette, "Open command pallete",
toggle_or_focus_explorer, "toggle or focus explorer",
open_explorer_recursion, "open explorer recursion",
close_explorer, "close explorer",
);
}
@ -2213,6 +2216,43 @@ fn file_picker_in_current_directory(cx: &mut Context) {
cx.push_layer(Box::new(overlayed(picker)));
}
fn toggle_or_focus_explorer(cx: &mut Context) {
cx.callback = Some(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
match editor.explorer.as_mut() {
Some(explore) => explore.content.focus(),
None => match ui::Explorer::new(cx) {
Ok(explore) => editor.explorer = Some(overlayed(explore)),
Err(err) => cx.editor.set_error(format!("{}", err)),
},
}
}
},
));
}
fn open_explorer_recursion(cx: &mut Context) {
cx.callback = Some(Box::new(
|compositor: &mut Compositor, cx: &mut compositor::Context| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
match ui::Explorer::new_explorer_recursion() {
Ok(explore) => editor.explorer = Some(overlayed(explore)),
Err(err) => cx.editor.set_error(format!("{}", err)),
}
}
},
));
}
fn close_explorer(cx: &mut Context) {
cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
editor.explorer.take();
}
}));
}
fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;

@ -264,6 +264,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor,
"?" => command_palette,
"e" => toggle_or_focus_explorer,
"E" => open_explorer_recursion,
},
"z" => { "View"
"z" | "c" => align_view_center,

@ -3,7 +3,7 @@ use crate::{
compositor::{Component, Context, Event, EventResult},
job, key,
keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners},
ui::{overlay::Overlay, Completion, Explorer, ProgressSpinners},
};
use helix_core::{
@ -36,6 +36,7 @@ pub struct EditorView {
last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
pub(crate) explorer: Option<Overlay<Explorer>>,
}
#[derive(Debug, Clone)]
@ -59,6 +60,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
explorer: None,
}
}
@ -1118,6 +1120,11 @@ impl Component for EditorView {
event: Event,
context: &mut crate::compositor::Context,
) -> EventResult {
if let Some(explore) = self.explorer.as_mut() {
if let EventResult::Consumed(callback) = explore.handle_event(event, context) {
return EventResult::Consumed(callback);
}
}
let mut cx = commands::Context {
editor: context.editor,
count: None,
@ -1260,7 +1267,17 @@ impl Component for EditorView {
surface.set_style(area, cx.editor.theme.get("ui.background"));
let config = cx.editor.config();
// if the terminal size suddenly changed, we need to trigger a resize
cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
let mut editor_area = area.clip_bottom(1);
if self.explorer.is_some() && (config.explorer.is_embed()) {
editor_area = editor_area.clip_left(config.explorer.column_width as u16 + 2);
}
cx.editor.resize(editor_area); // -1 from bottom for commandline
if let Some(explore) = self.explorer.as_mut() {
if !explore.content.is_focus() && config.explorer.is_embed() {
explore.content.render(area, surface, cx);
}
}
for (view, is_focused) in cx.editor.tree.views() {
let doc = cx.editor.document(view.doc).unwrap();
@ -1336,9 +1353,30 @@ impl Component for EditorView {
if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx);
}
if let Some(explore) = self.explorer.as_mut() {
if explore.content.is_focus() {
if config.explorer.is_embed() {
explore.content.render(area, surface, cx);
} else {
explore.render(area, surface, cx);
}
}
}
}
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
if let Some(explore) = &self.explorer {
if explore.content.is_focus() {
if editor.config().explorer.is_overlay() {
return explore.cursor(_area, editor);
}
let cursor = explore.content.cursor(_area, editor);
if cursor.0.is_some() {
return cursor;
}
}
}
match editor.cursor() {
// All block cursors are drawn manually
(pos, CursorKind::Block) => (pos, CursorKind::Hidden),

@ -0,0 +1,867 @@
use super::{Prompt, Tree, TreeItem, TreeOp};
use crate::{
compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift, ui,
};
use anyhow::{bail, ensure, Result};
use helix_core::Position;
use helix_view::{
editor::Action,
graphics::{CursorKind, Modifier, Rect},
input::{Event, KeyEvent},
Editor,
};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use tui::{
buffer::Buffer as Surface,
text::{Span, Spans},
widgets::{Block, Borders, Widget},
};
macro_rules! get_theme {
($theme: expr, $s1: expr, $s2: expr) => {
$theme.try_get($s1).unwrap_or_else(|| $theme.get($s2))
};
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum FileType {
File,
Dir,
Exe,
Placeholder,
Parent,
Root,
}
#[derive(Debug, Clone)]
struct FileInfo {
file_type: FileType,
path: PathBuf,
}
impl FileInfo {
fn new(path: PathBuf, file_type: FileType) -> Self {
Self { path, file_type }
}
fn root(path: PathBuf) -> Self {
Self {
file_type: FileType::Root,
path,
}
}
fn parent(path: &Path) -> Self {
let p = path.parent().unwrap_or_else(|| Path::new(""));
Self {
file_type: FileType::Parent,
path: p.to_path_buf(),
}
}
fn get_text(&self) -> Cow<'static, str> {
match self.file_type {
FileType::Parent => "..".into(),
FileType::Placeholder => "---".into(),
FileType::Root => return format!("{}", self.path.display()).into(),
FileType::File | FileType::Exe | FileType::Dir => self
.path
.file_name()
.map_or("/".into(), |p| p.to_string_lossy().into_owned().into()),
}
}
}
impl TreeItem for FileInfo {
type Params = State;
fn text(&self, cx: &mut Context, selected: bool, state: &mut State) -> Spans {
let text = self.get_text();
let theme = &cx.editor.theme;
let style = match self.file_type {
FileType::Parent | FileType::Dir | FileType::Root => "ui.explorer.dir",
FileType::File | FileType::Exe | FileType::Placeholder => "ui.explorer.file",
};
let mut style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text"));
if selected {
let patch = match state.focus {
true => "ui.explorer.focus",
false => "ui.explorer.unfocus",
};
if let Some(patch) = theme.try_get(patch) {
style = style.patch(patch);
} else {
style = style.add_modifier(Modifier::REVERSED);
}
}
Spans::from(Span::styled(text, style))
}
fn is_child(&self, other: &Self) -> bool {
if let FileType::Parent = other.file_type {
return false;
}
if let FileType::Placeholder = self.file_type {
self.path == other.path
} else {
self.path.parent().map_or(false, |p| p == other.path)
}
}
fn cmp(&self, other: &Self) -> Ordering {
use FileType::*;
match (self.file_type, other.file_type) {
(Parent, _) => return Ordering::Less,
(_, Parent) => return Ordering::Greater,
(Root, _) => return Ordering::Less,
(_, Root) => return Ordering::Greater,
_ => {}
};
if self.path == other.path {
match (self.file_type, other.file_type) {
(_, Placeholder) => return Ordering::Less,
(Placeholder, _) => return Ordering::Greater,
_ => {}
};
}
if let (Some(p1), Some(p2)) = (self.path.parent(), other.path.parent()) {
if p1 == p2 {
match (self.file_type, other.file_type) {
(Dir, File | Exe) => return Ordering::Less,
(File | Exe, Dir) => return Ordering::Greater,
_ => {}
};
}
}
self.path.cmp(&other.path)
}
fn get_childs(&self) -> Result<Vec<Self>> {
match self.file_type {
FileType::Root | FileType::Dir => {}
_ => return Ok(vec![]),
};
let mut ret: Vec<_> = std::fs::read_dir(&self.path)?
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
entry.metadata().ok().map(|meta| {
let is_exe = false;
let file_type = match (meta.is_dir(), is_exe) {
(true, _) => FileType::Dir,
(_, false) => FileType::File,
(_, true) => FileType::Exe,
};
Self {
file_type,
path: self.path.join(entry.file_name()),
}
})
})
.collect();
if ret.is_empty() {
ret.push(Self {
path: self.path.clone(),
file_type: FileType::Placeholder,
})
}
Ok(ret)
}
fn filter(&self, _cx: &mut Context, s: &str, _params: &mut Self::Params) -> bool {
if s.is_empty() {
false
} else {
self.get_text().contains(s)
}
}
}
// #[derive(Default, Debug, Clone)]
// struct PathState {
// root: PathBuf,
// sub_items: Vec<FileInfo>,
// selected: usize,
// save_view: (usize, usize), // (selected, row)
// row: usize,
// col: usize,
// max_len: usize,
// }
// impl PathState {
// fn mkdir(&mut self, dir: &str) -> Result<()> {
// self.new_path(dir, FileType::Dir)
// }
// fn create_file(&mut self, f: &str) -> Result<()> {
// self.new_path(f, FileType::File)
// }
// fn remove_current_file(&mut self) -> Result<()> {
// let item = &self.sub_items[self.selected];
// std::fs::remove_file(item.path_with_root(&self.root))?;
// self.sub_items.remove(self.selected);
// if self.selected >= self.sub_items.len() {
// self.selected = self.sub_items.len() - 1;
// }
// Ok(())
// }
// }
#[derive(Clone, Copy, Debug)]
enum PromptAction {
Search(bool), // search next/search pre
Mkdir,
CreateFile,
RemoveDir,
RemoveFile,
Filter,
}
#[derive(Clone, Debug)]
struct State {
focus: bool,
current_root: PathBuf,
}
impl State {
fn new(focus: bool, current_root: PathBuf) -> Self {
Self {
focus,
current_root,
}
}
}
pub struct Explorer {
tree: Tree<FileInfo>,
state: State,
prompt: Option<(PromptAction, Prompt)>,
#[allow(clippy::type_complexity)]
on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, KeyEvent) -> EventResult>>,
#[allow(clippy::type_complexity)]
repeat_motion: Option<Box<dyn FnMut(&mut Self, PromptAction, &mut Context) + 'static>>,
}
impl Explorer {
pub fn new(cx: &mut Context) -> Result<Self> {
let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into());
let items = Self::get_items(current_root.clone(), cx)?;
Ok(Self {
tree: Tree::build_tree(items).with_enter_fn(Self::toggle_current),
state: State::new(true, current_root),
repeat_motion: None,
prompt: None,
on_next_key: None,
})
}
pub fn new_explorer_recursion() -> Result<Self> {
let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into());
let parent = FileInfo::parent(&current_root);
let root = FileInfo::root(current_root.clone());
let mut tree =
Tree::build_from_root(root, usize::MAX / 2)?.with_enter_fn(Self::toggle_current);
tree.insert_current_level(parent);
Ok(Self {
tree,
state: State::new(true, current_root),
repeat_motion: None,
prompt: None,
on_next_key: None,
})
// let mut root = vec![, FileInfo::root(p)];
}
// pub fn new_with_uri(uri: String) -> Result<Self> {
// // support remote file?
// let p = Path::new(&uri);
// ensure!(p.exists(), "path: {uri} is not exist");
// ensure!(p.is_dir(), "path: {uri} is not dir");
// Ok(Self::default().with_list(get_sub(p, None)?))
// }
pub fn focus(&mut self) {
self.state.focus = true
}
pub fn unfocus(&mut self) {
self.state.focus = false;
}
pub fn is_focus(&self) -> bool {
self.state.focus
}
fn get_items(p: PathBuf, cx: &mut Context) -> Result<Vec<FileInfo>> {
let mut items = vec![FileInfo::parent(p.as_path())];
let root = FileInfo::root(p);
let childs = root.get_childs()?;
if cx.editor.config().explorer.is_tree() {
items.push(root)
}
items.extend(childs);
Ok(items)
}
fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) {
if area.height <= 2 || area.width < 60 {
return;
}
let item = self.tree.current().item();
if item.file_type == FileType::Placeholder {
return;
}
let head_area = render_block(area.clip_bottom(area.height - 2), surface, Borders::BOTTOM);
let path_str = format!("{}", item.path.display());
surface.set_stringn(
head_area.x,
head_area.y,
path_str,
head_area.width as usize,
get_theme!(editor.theme, "ui.explorer.dir", "ui.text"),
);
let body_area = area.clip_top(2);
let style = editor.theme.get("ui.text");
if let Ok(preview_content) = get_preview(&item.path, body_area.height as usize) {
preview_content
.into_iter()
.enumerate()
.for_each(|(row, line)| {
surface.set_stringn(
body_area.x,
body_area.y + row as u16,
line,
body_area.width as usize,
style,
);
})
}
}
fn new_search_prompt(&mut self, search_next: bool) {
self.tree.save_view();
self.prompt = Some((
PromptAction::Search(search_next),
Prompt::new("search: ".into(), None, ui::completers::none, |_, _, _| {}),
))
}
fn new_filter_prompt(&mut self) {
self.tree.save_view();
self.prompt = Some((
PromptAction::Filter,
Prompt::new("filter: ".into(), None, ui::completers::none, |_, _, _| {}),
))
}
fn new_mkdir_prompt(&mut self) {
self.prompt = Some((
PromptAction::Mkdir,
Prompt::new("mkdir: ".into(), None, ui::completers::none, |_, _, _| {}),
));
}
fn new_create_file_prompt(&mut self) {
self.prompt = Some((
PromptAction::CreateFile,
Prompt::new(
"create file: ".into(),
None,
ui::completers::none,
|_, _, _| {},
),
));
}
fn new_remove_file_prompt(&mut self, cx: &mut Context) {
let item = self.tree.current_item();
let check = || {
ensure!(item.file_type != FileType::Placeholder, "The path is empty");
ensure!(
item.file_type != FileType::Parent,
"can not remove parent dir"
);
ensure!(item.path.is_file(), "The path is not a file");
let doc = cx.editor.document_by_path(&item.path);
ensure!(doc.is_none(), "The file is opened");
Ok(())
};
if let Err(e) = check() {
cx.editor.set_error(format!("{e}"));
return;
}
let p = format!("remove file: {}, YES? ", item.path.display());
self.prompt = Some((
PromptAction::RemoveFile,
Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}),
));
}
fn new_remove_dir_prompt(&mut self, cx: &mut Context) {
let item = self.tree.current_item();
let check = || {
ensure!(item.file_type != FileType::Placeholder, "The path is empty");
ensure!(
item.file_type != FileType::Parent,
"can not remove parent dir"
);
ensure!(item.path.is_dir(), "The path is not a dir");
let doc = cx.editor.documents().find(|doc| {
doc.path()
.map(|p| p.starts_with(&item.path))
.unwrap_or(false)
});
ensure!(doc.is_none(), "There are files opened under the dir");
Ok(())
};
if let Err(e) = check() {
cx.editor.set_error(format!("{e}"));
return;
}
let p = format!("remove dir: {}, YES? ", item.path.display());
self.prompt = Some((
PromptAction::RemoveDir,
Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}),
));
}
fn toggle_current(
item: &mut FileInfo,
cx: &mut Context,
state: &mut State,
) -> TreeOp<FileInfo> {
if item.file_type == FileType::Placeholder {
return TreeOp::Noop;
}
if item.path == Path::new("") {
return TreeOp::Noop;
}
let meta = match std::fs::metadata(&item.path) {
Ok(meta) => meta,
Err(e) => {
cx.editor.set_error(format!("{e}"));
return TreeOp::Noop;
}
};
if meta.is_file() {
if let Err(e) = cx.editor.open(&item.path, Action::Replace) {
cx.editor.set_error(format!("{e}"));
}
state.focus = false;
return TreeOp::Noop;
}
if item.path.is_dir() {
if cx.editor.config().explorer.is_list() || item.file_type == FileType::Parent {
match Self::get_items(item.path.clone(), cx) {
Ok(items) => {
state.current_root = item.path.clone();
return TreeOp::ReplaceTree(items);
}
Err(e) => cx.editor.set_error(format!("{e}")),
}
} else {
return TreeOp::GetChildsAndInsert;
}
}
cx.editor.set_error("unkonw file type");
TreeOp::Noop
}
fn render_float(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let background = cx.editor.theme.get("ui.background");
let column_width = cx.editor.config().explorer.column_width as u16;
surface.clear_with(area, background);
let area = render_block(area, surface, Borders::ALL);
let mut preview_area = area.clip_left(column_width + 1);
if let Some((_, prompt)) = self.prompt.as_mut() {
let area = preview_area.clip_bottom(2);
let promp_area =
render_block(preview_area.clip_top(area.height), surface, Borders::TOP);
prompt.render(promp_area, surface, cx);
preview_area = area;
}
self.render_preview(preview_area, surface, cx.editor);
let list_area = render_block(area.clip_right(preview_area.width), surface, Borders::RIGHT);
self.tree.render(list_area, surface, cx, &mut self.state);
}
fn render_embed(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let config = &cx.editor.config().explorer;
let side_area = area
.with_width(area.width.min(config.column_width as u16 + 2))
.clip_bottom(1);
let background = cx.editor.theme.get("ui.background");
surface.clear_with(side_area, background);
let preview_area = area.clip_left(side_area.width).clip_bottom(2);
let prompt_area = area.clip_top(side_area.height);
let list_area =
render_block(side_area.clip_left(1), surface, Borders::RIGHT).clip_bottom(1);
self.tree.render(list_area, surface, cx, &mut self.state);
{
let statusline = if self.is_focus() {
cx.editor.theme.get("ui.statusline")
} else {
cx.editor.theme.get("ui.statusline.inactive")
};
let area = side_area.clip_top(list_area.height).clip_right(1);
surface.clear_with(area, statusline);
// surface.set_string_truncated(
// area.x,
// area.y,
// &self.path_state.root.to_string_lossy(),
// area.width as usize,
// |_| statusline,
// true,
// true,
// );
}
if self.is_focus() {
if preview_area.width < 30 || preview_area.height < 3 {
return;
}
let width = preview_area.width.min(90);
let mut y = self.tree.row().saturating_sub(1) as u16;
let height = (preview_area.height).min(25);
if (height + y) > preview_area.height {
y = preview_area.height - height;
}
let area = Rect::new(preview_area.x, y, width, height);
surface.clear_with(area, background);
let area = render_block(area, surface, Borders::all());
self.render_preview(area, surface, cx.editor);
}
if let Some((_, prompt)) = self.prompt.as_mut() {
prompt.render_prompt(prompt_area, surface, cx)
}
}
fn handle_filter_event(&mut self, event: KeyEvent, cx: &mut Context) -> EventResult {
let (action, mut prompt) = self.prompt.take().unwrap();
match event.into() {
key!(Tab) | key!(Down) | ctrl!('j') => {
self.tree.clean_recycle();
return self
.tree
.handle_event(Event::Key(event), cx, &mut self.state);
}
key!(Enter) => {
self.tree.clean_recycle();
return self
.tree
.handle_event(Event::Key(event), cx, &mut self.state);
}
key!(Esc) | ctrl!('c') => self.tree.restore_recycle(),
_ => {
if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) {
self.tree.filter(prompt.line(), cx, &mut self.state);
}
self.prompt = Some((action, prompt));
}
};
EventResult::Consumed(None)
}
fn handle_search_event(&mut self, event: KeyEvent, cx: &mut Context) -> EventResult {
let (action, mut prompt) = self.prompt.take().unwrap();
let search_next = match action {
PromptAction::Search(search_next) => search_next,
_ => return EventResult::Ignored(None),
};
match event.into() {
key!(Tab) | key!(Down) | ctrl!('j') => {
return self
.tree
.handle_event(Event::Key(event), cx, &mut self.state)
}
key!(Enter) => {
let search_str = prompt.line().clone();
if !search_str.is_empty() {
self.repeat_motion = Some(Box::new(move |explorer, action, cx| {
if let PromptAction::Search(is_next) = action {
explorer.tree.save_view();
if is_next == search_next {
explorer
.tree
.search_next(cx, &search_str, &mut explorer.state);
} else {
explorer
.tree
.search_pre(cx, &search_str, &mut explorer.state);
}
}
}))
} else {
self.repeat_motion = None;
}
return self
.tree
.handle_event(Event::Key(event), cx, &mut self.state);
}
key!(Esc) | ctrl!('c') => self.tree.restore_view(),
_ => {
if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) {
if search_next {
self.tree.search_next(cx, prompt.line(), &mut self.state);
} else {
self.tree.search_pre(cx, prompt.line(), &mut self.state);
}
}
self.prompt = Some((action, prompt));
}
};
EventResult::Consumed(None)
}
fn handle_prompt_event(&mut self, event: KeyEvent, cx: &mut Context) -> EventResult {
match &self.prompt {
Some((PromptAction::Search(_), _)) => return self.handle_search_event(event, cx),
Some((PromptAction::Filter, _)) => return self.handle_filter_event(event, cx),
_ => {}
};
let (action, mut prompt) = match self.prompt.take() {
Some((action, p)) => (action, p),
_ => return EventResult::Ignored(None),
};
let line = prompt.line();
match (action, event.into()) {
(PromptAction::Mkdir, key!(Enter)) => {
if let Err(e) = self.new_path(line, true) {
cx.editor.set_error(format!("{e}"))
}
}
(PromptAction::CreateFile, key!(Enter)) => {
if let Err(e) = self.new_path(line, false) {
cx.editor.set_error(format!("{e}"))
}
}
(PromptAction::RemoveDir, key!(Enter)) => {
let item = self.tree.current_item();
if let Err(e) = std::fs::remove_dir_all(&item.path) {
cx.editor.set_error(format!("{e}"));
} else {
self.tree.fold_current_child();
self.tree.remove_current();
}
}
(PromptAction::RemoveFile, key!(Enter)) => {
if line == "YES" {
let item = self.tree.current_item();
if let Err(e) = std::fs::remove_file(&item.path) {
cx.editor.set_error(format!("{e}"));
} else {
self.tree.remove_current();
}
}
}
(_, key!(Esc) | ctrl!('c')) => {}
_ => {
prompt.handle_event(Event::Key(event), cx);
self.prompt = Some((action, prompt));
}
}
EventResult::Consumed(None)
}
fn new_path(&mut self, file_name: &str, is_dir: bool) -> Result<()> {
let current = self.tree.current_item();
let current_parent = if current.file_type == FileType::Placeholder {
&current.path
} else {
current
.path
.parent()
.ok_or_else(|| anyhow::anyhow!("can not get parent dir"))?
};
let p = helix_core::path::get_normalized_path(&current_parent.join(file_name));
match p.parent() {
Some(p) if p == current_parent => {}
_ => bail!("The file name is not illegal"),
};
let f = if is_dir {
std::fs::create_dir(&p)?;
FileInfo::new(p, FileType::Dir)
} else {
let mut fd = std::fs::OpenOptions::new();
fd.create_new(true).write(true).open(&p)?;
FileInfo::new(p, FileType::File)
};
if current.file_type == FileType::Placeholder {
self.tree.replace_current(f);
} else {
self.tree.insert_current_level(f);
}
Ok(())
}
}
impl Component for Explorer {
/// Process input events, return true if handled.
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
Event::Key(event) => event,
Event::Resize(..) => return EventResult::Consumed(None),
_ => return EventResult::Ignored(None),
};
if !self.is_focus() {
return EventResult::Ignored(None);
}
if let Some(mut on_next_key) = self.on_next_key.take() {
return on_next_key(cx, self, key_event);
}
if let EventResult::Consumed(c) = self.handle_prompt_event(key_event, cx) {
return EventResult::Consumed(c);
}
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(editor) = compositor.find::<ui::EditorView>() {
editor.explorer = None;
}
})));
match key_event.into() {
key!(Esc) => self.unfocus(),
ctrl!('c') => return close_fn,
key!('n') => {
if let Some(mut repeat_motion) = self.repeat_motion.take() {
repeat_motion(self, PromptAction::Search(true), cx);
self.repeat_motion = Some(repeat_motion);
}
}
shift!('N') => {
if let Some(mut repeat_motion) = self.repeat_motion.take() {
repeat_motion(self, PromptAction::Search(false), cx);
self.repeat_motion = Some(repeat_motion);
}
}
key!('b') => {
if let Some(p) = self.state.current_root.parent() {
match Self::get_items(p.to_path_buf(), cx) {
Ok(items) => {
self.state.current_root = p.to_path_buf();
self.tree = Tree::build_tree(items).with_enter_fn(Self::toggle_current);
}
Err(e) => cx.editor.set_error(format!("{e}")),
}
}
}
key!('f') => self.new_filter_prompt(),
key!('/') => self.new_search_prompt(true),
key!('?') => self.new_search_prompt(false),
key!('m') => {
self.on_next_key = Some(Box::new(|_, explorer, event| {
match event.into() {
key!('d') => explorer.new_mkdir_prompt(),
key!('f') => explorer.new_create_file_prompt(),
_ => return EventResult::Ignored(None),
};
EventResult::Consumed(None)
}));
}
key!('r') => {
self.on_next_key = Some(Box::new(|cx, explorer, event| {
match event.into() {
key!('d') => explorer.new_remove_dir_prompt(cx),
key!('f') => explorer.new_remove_file_prompt(cx),
_ => return EventResult::Ignored(None),
};
EventResult::Consumed(None)
}));
}
_ => {
self.tree
.handle_event(Event::Key(key_event), cx, &mut self.state);
}
}
EventResult::Consumed(None)
}
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
if area.width < 10 || area.height < 5 {
cx.editor.set_error("explorer render area is too small");
return;
}
let config = &cx.editor.config().explorer;
if config.is_embed() {
self.render_embed(area, surface, cx);
} else {
self.render_float(area, surface, cx);
}
}
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
let prompt = match self.prompt.as_ref() {
Some((_, prompt)) => prompt,
None => return (None, CursorKind::Hidden),
};
let config = &editor.config().explorer;
let (x, y) = if config.is_overlay() {
let colw = config.column_width as u16;
if area.width > colw {
(area.x + colw + 2, area.y + area.height - 2)
} else {
return (None, CursorKind::Hidden);
}
} else {
(area.x, area.y + area.height - 1)
};
prompt.cursor(Rect::new(x, y, area.width, 1), editor)
}
}
fn get_preview(p: impl AsRef<Path>, max_line: usize) -> Result<Vec<String>> {
let p = p.as_ref();
if p.is_dir() {
return Ok(p
.read_dir()?
.filter_map(|entry| entry.ok())
.take(max_line)
.map(|entry| {
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
format!("{}/", entry.file_name().to_string_lossy())
} else {
format!("{}", entry.file_name().to_string_lossy())
}
})
.collect());
}
ensure!(p.is_file(), "path: {} is not file or dir", p.display());
use std::fs::OpenOptions;
use std::io::BufRead;
let mut fd = OpenOptions::new();
fd.read(true);
let fd = fd.open(p)?;
Ok(std::io::BufReader::new(fd)
.lines()
.take(max_line)
.filter_map(|line| line.ok())
.map(|line| line.replace('\t', " "))
.collect())
}
fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect {
let block = Block::default().borders(borders);
let inner = block.inner(area);
block.render(area, surface);
inner
}

@ -1,5 +1,6 @@
mod completion;
pub(crate) mod editor;
mod explore;
mod info;
pub mod lsp;
mod markdown;
@ -11,9 +12,11 @@ mod prompt;
mod spinner;
mod statusline;
mod text;
mod tree;
pub use completion::Completion;
pub use editor::EditorView;
pub use explore::Explorer;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{FileLocation, FilePicker, Picker};
@ -21,6 +24,7 @@ pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
pub use tree::{Tree, TreeItem, TreeOp};
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;

@ -0,0 +1,682 @@
use std::cmp::Ordering;
use std::iter::Peekable;
use anyhow::Result;
use crate::{
compositor::{Context, EventResult},
ctrl, key, shift,
};
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::{
graphics::Rect,
input::{Event, KeyEvent},
};
use tui::{buffer::Buffer as Surface, text::Spans};
pub trait TreeItem: Sized {
type Params;
fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans;
fn is_child(&self, other: &Self) -> bool;
fn cmp(&self, other: &Self) -> Ordering;
fn filter(&self, cx: &mut Context, s: &str, params: &mut Self::Params) -> bool {
self.text(cx, false, params)
.0
.into_iter()
.map(|s| s.content)
.collect::<Vec<_>>()
.concat()
.contains(s)
}
fn get_childs(&self) -> Result<Vec<Self>> {
Ok(vec![])
}
}
fn tree_item_cmp<T: TreeItem>(item1: &T, item2: &T) -> Ordering {
if item1.is_child(item2) {
return Ordering::Greater;
}
if item2.is_child(item1) {
return Ordering::Less;
}
T::cmp(item1, item2)
}
fn vec_to_tree<T: TreeItem>(mut items: Vec<T>, level: usize) -> Vec<Elem<T>> {
fn get_childs<T, Iter>(iter: &mut Peekable<Iter>, elem: &mut Elem<T>)
where
T: TreeItem,
Iter: Iterator<Item = T>,
{
let level = elem.level + 1;
loop {
if !iter.peek().map_or(false, |next| next.is_child(&elem.item)) {
break;
}
let mut child = Elem::new(iter.next().unwrap(), level);
if iter.peek().map_or(false, |nc| nc.is_child(&child.item)) {
get_childs(iter, &mut child);
}
elem.folded.push(child);
}
}
items.sort_by(tree_item_cmp);
let mut elems = Vec::with_capacity(items.len());
let mut iter = items.into_iter().peekable();
while let Some(item) = iter.next() {
let mut elem = Elem::new(item, level);
if iter.peek().map_or(false, |next| next.is_child(&elem.item)) {
get_childs(&mut iter, &mut elem);
}
expand_elems(&mut elems, elem);
}
elems
}
// return total elems's count contain self
fn get_elems_recursion<T: TreeItem>(t: &mut Elem<T>, depth: usize) -> Result<usize> {
let mut childs = t.item.get_childs()?;
childs.sort_by(tree_item_cmp);
let mut elems = Vec::with_capacity(childs.len());
let level = t.level + 1;
let mut total = 1;
for child in childs {
let mut elem = Elem::new(child, level);
let count = if depth > 0 {
get_elems_recursion(&mut elem, depth - 1)?
} else {
1
};
elems.push(elem);
total += count;
}
t.folded = elems;
Ok(total)
}
fn expand_elems<T: TreeItem>(dist: &mut Vec<Elem<T>>, mut t: Elem<T>) {
let childs = std::mem::take(&mut t.folded);
dist.push(t);
for child in childs {
expand_elems(dist, child)
}
}
pub enum TreeOp<T> {
Noop,
Restore,
InsertChild(Vec<T>),
GetChildsAndInsert,
ReplaceTree(Vec<T>),
}
pub struct Elem<T> {
item: T,
level: usize,
folded: Vec<Self>,
}
impl<T: Clone> Clone for Elem<T> {
fn clone(&self) -> Self {
Self {
item: self.item.clone(),
level: self.level,
folded: self.folded.clone(),
}
}
}
impl<T> Elem<T> {
pub fn new(item: T, level: usize) -> Self {
Self {
item,
level,
folded: vec![],
}
}
pub fn item(&self) -> &T {
&self.item
}
}
pub struct Tree<T: TreeItem> {
items: Vec<Elem<T>>,
recycle: Option<(String, Vec<Elem<T>>)>,
selected: usize,
save_view: (usize, usize), // (selected, row)
row: usize,
col: usize,
max_len: usize,
count: usize,
tree_symbol_style: String,
#[allow(clippy::type_complexity)]
pre_render: Option<Box<dyn Fn(&mut Self, Rect) + 'static>>,
#[allow(clippy::type_complexity)]
on_opened_fn:
Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp<T> + 'static>>,
#[allow(clippy::type_complexity)]
on_folded_fn: Option<Box<dyn FnMut(&mut T, &mut Context, &mut T::Params) + 'static>>,
#[allow(clippy::type_complexity)]
on_next_key: Option<Box<dyn FnMut(&mut Context, &mut Self, KeyEvent)>>,
}
impl<T: TreeItem> Tree<T> {
pub fn new(items: Vec<Elem<T>>) -> Self {
Self {
items,
recycle: None,
selected: 0,
save_view: (0, 0),
row: 0,
col: 0,
max_len: 0,
count: 0,
tree_symbol_style: "ui.text".into(),
pre_render: None,
on_opened_fn: None,
on_folded_fn: None,
on_next_key: None,
}
}
pub fn replace_with_new_items(&mut self, items: Vec<T>) {
let old = std::mem::replace(self, Self::new(vec_to_tree(items, 0)));
self.on_opened_fn = old.on_opened_fn;
self.on_folded_fn = old.on_folded_fn;
self.tree_symbol_style = old.tree_symbol_style;
}
pub fn build_tree(items: Vec<T>) -> Self {
Self::new(vec_to_tree(items, 0))
}
pub fn build_from_root(t: T, depth: usize) -> Result<Self> {
let mut elem = Elem::new(t, 0);
let count = get_elems_recursion(&mut elem, depth)?;
let mut elems = Vec::with_capacity(count);
expand_elems(&mut elems, elem);
Ok(Self::new(elems))
}
pub fn with_enter_fn<F>(mut self, f: F) -> Self
where
F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp<T> + 'static,
{
self.on_opened_fn = Some(Box::new(f));
self
}
pub fn with_folded_fn<F>(mut self, f: F) -> Self
where
F: FnMut(&mut T, &mut Context, &mut T::Params) + 'static,
{
self.on_folded_fn = Some(Box::new(f));
self
}
pub fn tree_symbol_style(mut self, style: String) -> Self {
self.tree_symbol_style = style;
self
}
fn next_item(&self) -> Option<&Elem<T>> {
self.items.get(self.selected + 1)
}
fn next_not_descendant_pos(&self, index: usize) -> usize {
let item = &self.items[index];
self.find(index + 1, false, |n| n.level <= item.level)
.unwrap_or(self.items.len())
}
fn find_parent(&self, index: usize) -> Option<usize> {
let item = &self.items[index];
self.find(index, true, |p| p.level < item.level)
}
// rev start: start - 1
fn find<F>(&self, start: usize, rev: bool, f: F) -> Option<usize>
where
F: FnMut(&Elem<T>) -> bool,
{
let iter = self.items.iter();
if rev {
iter.take(start).rposition(f)
} else {
iter.skip(start).position(f).map(|p| p + start)
}
}
}
impl<T: TreeItem> Tree<T> {
pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params) {
if self.items.is_empty() {
return;
}
if let Some(next_level) = self.next_item().map(|elem| elem.level) {
let current = &mut self.items[self.selected];
let current_level = current.level;
if next_level > current_level {
if let Some(mut on_folded_fn) = self.on_folded_fn.take() {
on_folded_fn(&mut current.item, cx, params);
self.on_folded_fn = Some(on_folded_fn);
}
self.fold_current_child();
return;
}
}
if let Some(mut on_open_fn) = self.on_opened_fn.take() {
let mut f = || {
let current = &mut self.items[self.selected];
let items = match on_open_fn(&mut current.item, cx, params) {
TreeOp::Restore => {
let inserts = std::mem::take(&mut current.folded);
let _: Vec<_> = self
.items
.splice(self.selected + 1..self.selected + 1, inserts)
.collect();
return;
}
TreeOp::InsertChild(items) => items,
TreeOp::GetChildsAndInsert => match current.item.get_childs() {
Ok(items) => items,
Err(e) => return cx.editor.set_error(format!("{e}")),
},
TreeOp::ReplaceTree(items) => return self.replace_with_new_items(items),
TreeOp::Noop => return,
};
current.folded = vec![];
let inserts = vec_to_tree(items, current.level + 1);
let _: Vec<_> = self
.items
.splice(self.selected + 1..self.selected + 1, inserts)
.collect();
};
f();
self.on_opened_fn = Some(on_open_fn)
} else {
let current = &mut self.items[self.selected];
let inserts = std::mem::take(&mut current.folded);
let _: Vec<_> = self
.items
.splice(self.selected + 1..self.selected + 1, inserts)
.collect();
}
}
pub fn fold_current_level(&mut self) {
let start = match self.find_parent(self.selected) {
Some(start) => start,
None => return,
};
self.selected = start;
self.fold_current_child();
}
pub fn fold_current_child(&mut self) {
if self.selected + 1 >= self.items.len() {
return;
}
let pos = self.next_not_descendant_pos(self.selected);
if self.selected < pos {
self.items[self.selected].folded = self.items.drain(self.selected + 1..pos).collect();
}
}
pub fn search_next(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) {
let skip = self.save_view.0 + 1;
self.selected = self
.find(skip, false, |e| e.item.filter(cx, s, params))
.unwrap_or(self.save_view.0);
self.row = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0);
}
pub fn search_pre(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) {
let take = self.save_view.0;
self.selected = self
.find(take, true, |e| e.item.filter(cx, s, params))
.unwrap_or(self.save_view.0);
self.row = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0);
}
pub fn move_down(&mut self, rows: usize) {
let len = self.items.len();
if len > 0 {
self.selected = std::cmp::min(self.selected + rows, len.saturating_sub(1));
self.row = std::cmp::min(self.selected, self.row + rows);
}
}
pub fn move_up(&mut self, rows: usize) {
let len = self.items.len();
if len > 0 {
self.selected = self.selected.saturating_sub(rows);
self.row = std::cmp::min(self.selected, self.row.saturating_sub(rows));
}
}
pub fn move_left(&mut self, cols: usize) {
self.col = self.col.saturating_sub(cols);
}
pub fn move_right(&mut self, cols: usize) {
self.pre_render = Some(Box::new(move |tree: &mut Self, area: Rect| {
let max_scroll = tree.max_len.saturating_sub(area.width as usize);
tree.col = max_scroll.min(tree.col + cols);
}));
}
pub fn move_down_half_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_down((area.height / 2) as usize);
}));
}
pub fn move_up_half_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_up((area.height / 2) as usize);
}));
}
pub fn move_down_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_down((area.height) as usize);
}));
}
pub fn move_up_page(&mut self) {
self.pre_render = Some(Box::new(|tree: &mut Self, area: Rect| {
tree.move_up((area.height) as usize);
}));
}
pub fn save_view(&mut self) {
self.save_view = (self.selected, self.row);
}
pub fn restore_view(&mut self) {
(self.selected, self.row) = self.save_view;
}
pub fn current(&self) -> &Elem<T> {
&self.items[self.selected]
}
pub fn current_item(&self) -> &T {
&self.items[self.selected].item
}
pub fn row(&self) -> usize {
self.row
}
pub fn remove_current(&mut self) -> T {
let elem = self.items.remove(self.selected);
self.selected = self.selected.saturating_sub(1);
elem.item
}
pub fn replace_current(&mut self, item: T) {
self.items[self.selected].item = item;
}
pub fn insert_current_level(&mut self, item: T) {
let current = self.current();
let level = current.level;
let pos = match current.item.cmp(&item) {
Ordering::Less => self
.find(self.selected + 1, false, |e| {
e.level < level || (e.level == level && e.item.cmp(&item) != Ordering::Less)
})
.unwrap_or(self.items.len()),
Ordering::Greater => {
match self.find(self.selected, true, |elem| {
elem.level < level
|| (elem.level == level && elem.item.cmp(&item) != Ordering::Greater)
}) {
Some(p) if self.items[p].level == level => self.next_not_descendant_pos(p),
Some(p) => p + 1,
None => 0,
}
}
Ordering::Equal => self.selected + 1,
};
self.items.insert(pos, Elem::new(item, level));
}
}
impl<T: TreeItem> Tree<T> {
pub fn render(
&mut self,
area: Rect,
surface: &mut Surface,
cx: &mut Context,
params: &mut T::Params,
) {
if let Some(pre_render) = self.pre_render.take() {
pre_render(self, area);
}
self.max_len = 0;
self.row = std::cmp::min(self.row, area.height.saturating_sub(1) as usize);
let style = cx.editor.theme.get(&self.tree_symbol_style);
let last_item_index = self.items.len().saturating_sub(1);
let skip = self.selected.saturating_sub(self.row);
let iter = self
.items
.iter()
.skip(skip)
.take(area.height as usize)
.enumerate();
for (index, elem) in iter {
let row = index as u16;
let mut area = Rect::new(area.x, area.y + row, area.width, 1);
let indent = if elem.level > 0 {
if index + skip != last_item_index {
format!("{}├─", "│ ".repeat(elem.level - 1))
} else {
format!("└─{}", "┴─".repeat(elem.level - 1))
}
} else {
"".to_string()
};
let indent_len = indent.chars().count();
if indent_len > self.col {
let indent: String = indent.chars().skip(self.col).collect();
if !indent.is_empty() {
surface.set_stringn(area.x, area.y, &indent, area.width as usize, style);
area = area.clip_left(indent.width() as u16);
}
};
let mut start_index = self.col.saturating_sub(indent_len);
let mut text = elem.item.text(cx, skip + index == self.selected, params);
self.max_len = self.max_len.max(text.width() + indent.len());
for span in text.0.iter_mut() {
if area.width == 0 {
return;
}
if start_index == 0 {
surface.set_span(area.x, area.y, span, area.width);
area = area.clip_left(span.width() as u16);
} else {
let span_width = span.width();
if start_index > span_width {
start_index -= span_width;
} else {
let content: String = span
.content
.chars()
.filter(|c| {
if start_index > 0 {
start_index = start_index.saturating_sub(c.to_string().width());
false
} else {
true
}
})
.collect();
surface.set_string_truncated(
area.x,
area.y,
&content,
area.width as usize,
|_| span.style,
false,
false,
);
start_index = 0
}
}
}
}
}
pub fn handle_event(
&mut self,
event: Event,
cx: &mut Context,
params: &mut T::Params,
) -> EventResult {
let key_event = match event {
Event::Key(event) => event,
Event::Resize(..) => return EventResult::Consumed(None),
_ => return EventResult::Ignored(None),
};
if let Some(mut on_next_key) = self.on_next_key.take() {
on_next_key(cx, self, key_event);
return EventResult::Consumed(None);
}
let count = std::mem::replace(&mut self.count, 0);
match key_event.into() {
key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10,
key!('k') | shift!(Tab) | key!(Up) | ctrl!('k') => self.move_up(1.max(count)),
key!('j') | key!(Tab) | key!(Down) | ctrl!('j') => self.move_down(1.max(count)),
key!('z') => self.fold_current_level(),
key!('h') => self.move_left(1.max(count)),
key!('l') => self.move_right(1.max(count)),
shift!('G') => self.move_down(usize::MAX / 2),
key!(Enter) => self.on_enter(cx, params),
ctrl!('d') => self.move_down_half_page(),
ctrl!('u') => self.move_up_half_page(),
shift!('D') => self.move_down_page(),
shift!('U') => self.move_up_page(),
key!('g') => {
self.on_next_key = Some(Box::new(|_, tree, event| match event.into() {
key!('g') => tree.move_up(usize::MAX / 2),
key!('e') => tree.move_down(usize::MAX / 2),
_ => {}
}));
}
_ => return EventResult::Ignored(None),
}
EventResult::Consumed(None)
}
}
impl<T: TreeItem + Clone> Tree<T> {
pub fn filter(&mut self, s: &str, cx: &mut Context, params: &mut T::Params) {
fn filter_recursion<T>(
elems: &Vec<Elem<T>>,
mut index: usize,
s: &str,
cx: &mut Context,
params: &mut T::Params,
) -> (Vec<Elem<T>>, usize)
where
T: TreeItem + Clone,
{
let mut retain = vec![];
let elem = &elems[index];
loop {
let child = match elems.get(index + 1) {
Some(child) if child.item.is_child(&elem.item) => child,
_ => break,
};
index += 1;
let next = elems.get(index + 1);
if next.map_or(false, |n| n.item.is_child(&child.item)) {
let (sub_retain, current_index) = filter_recursion(elems, index, s, cx, params);
retain.extend(sub_retain);
index = current_index;
} else if child.item.filter(cx, s, params) {
retain.push(child.clone());
}
}
if !retain.is_empty() || elem.item.filter(cx, s, params) {
retain.insert(0, elem.clone());
}
(retain, index)
}
if s.is_empty() {
if let Some((_, recycle)) = self.recycle.take() {
self.items = recycle;
self.restore_view();
return;
}
}
let mut retain = vec![];
let mut index = 0;
let items = match &self.recycle {
Some((pre, _)) if pre == s => return,
Some((pre, recycle)) if pre.contains(s) => recycle,
_ => &self.items,
};
while let Some(elem) = items.get(index) {
let next = items.get(index + 1);
if next.map_or(false, |n| n.item.is_child(&elem.item)) {
let (sub_items, current_index) = filter_recursion(items, index, s, cx, params);
index = current_index;
retain.extend(sub_items);
} else if elem.item.filter(cx, s, params) {
retain.push(elem.clone())
}
index += 1;
}
if retain.is_empty() {
if let Some((_, recycle)) = self.recycle.take() {
self.items = recycle;
self.restore_view();
}
return;
}
let recycle = std::mem::replace(&mut self.items, retain);
if let Some(r) = self.recycle.as_mut() {
r.0 = s.into()
} else {
self.recycle = Some((s.into(), recycle));
self.save_view();
}
self.selected = self
.find(0, false, |elem| elem.item.filter(cx, s, params))
.unwrap_or(0);
self.row = self.selected;
}
pub fn clean_recycle(&mut self) {
self.recycle = None;
}
pub fn restore_recycle(&mut self) {
if let Some((_, recycle)) = self.recycle.take() {
self.items = recycle;
}
}
}

@ -109,6 +109,69 @@ impl Default for FilePickerConfig {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExplorerStyle {
Tree,
List,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExplorerPosition {
Embed,
Overlay,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ExplorerConfig {
pub style: ExplorerStyle,
pub position: ExplorerPosition,
/// explorer column width
pub column_width: usize,
}
impl ExplorerConfig {
pub fn is_embed(&self) -> bool {
match self.position {
ExplorerPosition::Embed => true,
ExplorerPosition::Overlay => false,
}
}
pub fn is_overlay(&self) -> bool {
match self.position {
ExplorerPosition::Embed => false,
ExplorerPosition::Overlay => true,
}
}
pub fn is_list(&self) -> bool {
match self.style {
ExplorerStyle::List => true,
ExplorerStyle::Tree => false,
}
}
pub fn is_tree(&self) -> bool {
match self.style {
ExplorerStyle::List => false,
ExplorerStyle::Tree => true,
}
}
}
impl Default for ExplorerConfig {
fn default() -> Self {
Self {
style: ExplorerStyle::Tree,
position: ExplorerPosition::Overlay,
column_width: 30,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
@ -166,6 +229,8 @@ pub struct Config {
pub indent_guides: IndentGuidesConfig,
/// Whether to color modes with different colors. Defaults to `false`.
pub color_modes: bool,
/// explore config
pub explorer: ExplorerConfig,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
@ -549,6 +614,7 @@ impl Default for Config {
whitespace: WhitespaceConfig::default(),
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
explorer: ExplorerConfig::default(),
}
}
}

@ -68,6 +68,12 @@
"warning" = "my_yellow2"
"error" = "my_red"
"ui.explorer.file" = { fg = "my_white" }
"ui.explorer.dir" = { fg = "my_yellow1" }
"ui.explorer.exe" = { fg = "my_green" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { modifiers = ["reversed"] }
[palette]
my_black = "#212121" # Cursorline
my_gray0 = "#262626" # Default Background

@ -54,6 +54,12 @@
"warning" = "base09"
"error" = "base08"
"ui.explorer.file" = { fg = "base05" }
"ui.explorer.dir" = { fg = "base0D" }
"ui.explorer.exe" = { fg = "base05" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base02" }
[palette]
base00 = "#181818" # Default Background
base01 = "#282828" # Lighter Background (Used for status bars, line number and folding marks)

@ -54,6 +54,12 @@
"warning" = "base09"
"error" = "base08"
"ui.explorer.file" = { fg = "base05" }
"ui.explorer.dir" = { fg = "base0D" }
"ui.explorer.exe" = { fg = "base05" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base02" }
[palette]
base00 = "#f8f8f8" # Default Background
base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)

@ -50,3 +50,9 @@
"debug" = "gray"
"warning" = "yellow"
"error" = "light-red"
# "ui.explorer.file" = { fg = "base05" }
"ui.explorer.dir" = { fg = "light-blue" }
# "ui.explorer.exe" = { fg = "base05" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-gray" }

@ -70,3 +70,9 @@
# make diagnostic underlined, to distinguish with selection text.
diagnostic = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "#e5ded6" }
"ui.explorer.dir" = { fg = "#59dcd8" }
"ui.explorer.exe" = { fg = "#e5ded6" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#313f4e" }

@ -57,6 +57,12 @@
"hint" = { fg = "lilac" }
"diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "lilac" }
"ui.explorer.dir" = { fg = "mint" }
"ui.explorer.exe" = { fg = "lilac" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "berry_saturated" }
[palette]
berry = "#3A2A4D"
berry_saturated = "#2B1C3D"

@ -90,6 +90,12 @@
diagnostic = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "dark_blue2" }
[palette]
white = "#ffffff"
orange = "#ce9178"

@ -54,6 +54,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "cyan" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "secondary_highlight" }
[palette]
background = "#282a36"
background_dark = "#21222c"

@ -53,6 +53,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "cyan" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "comment" }
[palette]
background = "#0e1419"
background_dark = "#21222c"

@ -78,6 +78,11 @@
"error" = "red"
"diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "grey1" }
[palette]

@ -78,6 +78,11 @@
"error" = "red"
"diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "grey1" }
[palette]

@ -67,6 +67,12 @@
"markup.link.text" = "red1"
"markup.raw" = "red1"
"ui.explorer.file" = { fg = "fg1" }
"ui.explorer.dir" = { fg = "blue0" }
"ui.explorer.exe" = { fg = "fg1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette]
bg0 = "#282828" # main background
bg1 = "#3c3836"

@ -68,6 +68,12 @@
"markup.link.text" = "red1"
"markup.raw" = "red1"
"ui.explorer.file" = { fg = "fg1" }
"ui.explorer.dir" = { fg = "blue0" }
"ui.explorer.exe" = { fg = "fg1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette]
bg0 = "#fbf1c7" # main background
bg1 = "#ebdbb2"

@ -64,3 +64,9 @@
"error" = "#D74E50"
"info" = "#839A53"
"hint" = "#A6B6CE"
"ui.explorer.file" = { fg = "#7B91B3" }
"ui.explorer.dir" = { fg = "#89BEB7" }
"ui.explorer.exe" = { fg = "#7B91B3" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#F3EAE9" }

@ -85,6 +85,12 @@
diagnostic = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "fn_declaration" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "#75715e" }
[palette]
type = "#A6E22E"
keyword = "#F92672"

@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow"
"markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette]
# primary colors
"red" = "#ff6188"

@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow"
"markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette]
# primary colors
"red" = "#ff6d7e"

@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow"
"markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette]
# primary colors
"red" = "#ff657a"

@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow"
"markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette]
# primary colors
"red" = "#fd6883"

@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] }
"markup.link.text" = "yellow"
"markup.quote" = "green"
"ui.explorer.file" = { fg = "base8" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base8" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base4" }
[palette]
# primary colors
"red" = "#fc618d"

@ -87,6 +87,12 @@
'diff.delta' = { fg = 'blue' }
'diff.delta.moved' = { fg = 'blue', modifiers = ['italic'] }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "selection" }
[palette]
background = '#011627'
background2 = '#112630'

@ -105,6 +105,12 @@
"diff.delta" = "nord12"
"diff.minus" = "nord11"
"ui.explorer.file" = { fg = "nord6" }
"ui.explorer.dir" = { fg = "nord8" }
"ui.explorer.exe" = { fg = "nord6" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "nord2" }
[palette]
nord0 = "#2e3440"
nord1 = "#3b4252"

@ -78,6 +78,12 @@ diagnostic = { modifiers = ["underlined"] }
"ui.menu.selected" = { fg = "black", bg = "blue" }
"ui.menu.scroll" = { fg = "white", bg = "light-gray" }
"ui.explorer.file" = { fg = "white" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "white" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-gray" }
[palette]
yellow = "#E5C07B"

@ -106,6 +106,12 @@
"ui.menu" = { fg = "black", bg = "light-white" }
"ui.menu.selected" = { fg = "white", bg = "light-blue" }
"ui.explorer.file" = { fg = "black" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "black" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "light-white" }
[palette]
white = "#FAFAFA"
yellow = "#A06600"

@ -120,6 +120,12 @@ namespace = { fg = 'orangeL' }
'diff.delta' = { fg = '#4d4ddd' }
'diff.delta.moved' = { fg = '#dd4ddd' }
"ui.explorer.file" = { fg = "greyT" }
"ui.explorer.dir" = { fg = "blueL" }
"ui.explorer.exe" = { fg = "greyT" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "greyL" }
[palette]
white = '#FFFFFF'
greyH = '#CFCFCF'

@ -65,6 +65,12 @@
"markup.quote" = { fg = "rose" }
"markup.raw" = { fg = "foam" }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "rose" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "highlight" }
[palette]
base = "#191724"
surface = "#1f1d2e"

@ -62,6 +62,12 @@
"markup.quote" = { fg = "rose" }
"markup.raw" = { fg = "foam" }
"ui.explorer.file" = { fg = "text" }
"ui.explorer.dir" = { fg = "rose" }
"ui.explorer.exe" = { fg = "text" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "highlight" }
[palette]
base = "#faf4ed"
surface = "#fffaf3"

@ -70,6 +70,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "fg" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette]
bg0 = "#323437"

@ -70,6 +70,11 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "fg" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "fg" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette]

@ -101,6 +101,12 @@
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "base1" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base0175" }
[palette]
# 深色 越来越深
base03 = "#002b36"

@ -118,6 +118,12 @@
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "base1" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "base1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "base0175" }
[palette]
red = '#dc322f'
green = '#859900'

@ -73,6 +73,12 @@
"diagnostic" = { modifiers = ["underlined"] }
"ui.explorer.file" = { fg = "fg1" }
"ui.explorer.dir" = { fg = "#715ab1" }
"ui.explorer.exe" = { fg = "fg1" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "bg3" }
[palette]
base = "#655370"
base-dim = "#a094a2"

@ -63,6 +63,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "cyan" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "background_highlight" }
[palette]
red = "#f7768e"
orange = "#ff9e64"

@ -63,6 +63,12 @@
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "cyan" }
"ui.explorer.file" = { fg = "foreground" }
"ui.explorer.dir" = { fg = "blue" }
"ui.explorer.exe" = { fg = "foreground" }
"ui.explorer.focus" = { modifiers = ["reversed"] }
"ui.explorer.unfocus" = { bg = "background_highlight" }
[palette]
red = "#f7768e"
orange = "#ff9e64"

Loading…
Cancel
Save