diff --git a/book/src/configuration.md b/book/src/configuration.md
index affd497c..003420c0 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -147,6 +147,8 @@ auto-pairs = false # defaults to `true`
The default pairs are (){}[]''""``
, 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 |
diff --git a/book/src/keymap.md b/book/src/keymap.md
index a26fd7ea..d2c0496c 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -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 |
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 906f201a..585f9f2f 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -433,7 +433,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",
);
}
@@ -2235,6 +2238,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::() {
+ 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::() {
+ 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::() {
+ editor.explorer.take();
+ }
+ }));
+}
+
fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index fd7ea457..9f3f6aad 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -264,6 +264,8 @@ pub fn default() -> HashMap {
"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,
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index fa437a7e..33beed14 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -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),
pub(crate) completion: Option,
spinners: ProgressSpinners,
+ pub(crate) explorer: Option>,
}
#[derive(Debug, Clone)]
@@ -59,6 +60,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
+ explorer: None,
}
}
@@ -1159,6 +1161,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,
@@ -1292,7 +1299,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();
@@ -1368,9 +1385,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, 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),
diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs
new file mode 100644
index 00000000..d2978c8b
--- /dev/null
+++ b/helix-term/src/ui/explore.rs
@@ -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> {
+ 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,
+// 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,
+ state: State,
+ prompt: Option<(PromptAction, Prompt)>,
+ #[allow(clippy::type_complexity)]
+ on_next_key: Option EventResult>>,
+ #[allow(clippy::type_complexity)]
+ repeat_motion: Option>,
+}
+
+impl Explorer {
+ pub fn new(cx: &mut Context) -> Result {
+ 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 {
+ let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into());
+ let parent = FileInfo::parent(¤t_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 {
+ // // 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> {
+ 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 {
+ 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 {
+ ¤t.path
+ } else {
+ current
+ .path
+ .parent()
+ .ok_or_else(|| anyhow::anyhow!("can not get parent dir"))?
+ };
+ let p = helix_core::path::get_normalized_path(¤t_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::() {
+ 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, 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, max_line: usize) -> Result> {
+ 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
+}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 01ffe243..f5dea047 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -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;
diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs
new file mode 100644
index 00000000..fd2af8c6
--- /dev/null
+++ b/helix-term/src/ui/tree.rs
@@ -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::>()
+ .concat()
+ .contains(s)
+ }
+
+ fn get_childs(&self) -> Result> {
+ Ok(vec![])
+ }
+}
+
+fn tree_item_cmp(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(mut items: Vec, level: usize) -> Vec> {
+ fn get_childs(iter: &mut Peekable, elem: &mut Elem)
+ where
+ T: TreeItem,
+ Iter: Iterator- ,
+ {
+ 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: &mut Elem, depth: usize) -> Result {
+ 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(dist: &mut Vec>, mut t: Elem) {
+ let childs = std::mem::take(&mut t.folded);
+ dist.push(t);
+ for child in childs {
+ expand_elems(dist, child)
+ }
+}
+
+pub enum TreeOp {
+ Noop,
+ Restore,
+ InsertChild(Vec),
+ GetChildsAndInsert,
+ ReplaceTree(Vec),
+}
+
+pub struct Elem {
+ item: T,
+ level: usize,
+ folded: Vec,
+}
+
+impl Clone for Elem {
+ fn clone(&self) -> Self {
+ Self {
+ item: self.item.clone(),
+ level: self.level,
+ folded: self.folded.clone(),
+ }
+ }
+}
+
+impl Elem {
+ pub fn new(item: T, level: usize) -> Self {
+ Self {
+ item,
+ level,
+ folded: vec![],
+ }
+ }
+
+ pub fn item(&self) -> &T {
+ &self.item
+ }
+}
+
+pub struct Tree {
+ items: Vec>,
+ recycle: Option<(String, Vec>)>,
+ 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>,
+ #[allow(clippy::type_complexity)]
+ on_opened_fn:
+ Option TreeOp + 'static>>,
+ #[allow(clippy::type_complexity)]
+ on_folded_fn: Option>,
+ #[allow(clippy::type_complexity)]
+ on_next_key: Option>,
+}
+
+impl Tree {
+ pub fn new(items: Vec>) -> 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) {
+ 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) -> Self {
+ Self::new(vec_to_tree(items, 0))
+ }
+
+ pub fn build_from_root(t: T, depth: usize) -> Result {
+ 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(mut self, f: F) -> Self
+ where
+ F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static,
+ {
+ self.on_opened_fn = Some(Box::new(f));
+ self
+ }
+
+ pub fn with_folded_fn(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> {
+ 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 {
+ let item = &self.items[index];
+ self.find(index, true, |p| p.level < item.level)
+ }
+
+ // rev start: start - 1
+ fn find(&self, start: usize, rev: bool, f: F) -> Option
+ where
+ F: FnMut(&Elem) -> bool,
+ {
+ let iter = self.items.iter();
+ if rev {
+ iter.take(start).rposition(f)
+ } else {
+ iter.skip(start).position(f).map(|p| p + start)
+ }
+ }
+}
+
+impl Tree {
+ 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 {
+ &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 Tree {
+ 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 Tree {
+ pub fn filter(&mut self, s: &str, cx: &mut Context, params: &mut T::Params) {
+ fn filter_recursion(
+ elems: &Vec>,
+ mut index: usize,
+ s: &str,
+ cx: &mut Context,
+ params: &mut T::Params,
+ ) -> (Vec>, 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;
+ }
+ }
+}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index ed1813b3..61bb96e2 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -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)]
@@ -556,6 +621,7 @@ impl Default for Config {
whitespace: WhitespaceConfig::default(),
indent_guides: IndentGuidesConfig::default(),
color_modes: false,
+ explorer: ExplorerConfig::default(),
}
}
}
diff --git a/runtime/themes/autumn.toml b/runtime/themes/autumn.toml
index b13aeef2..c8909fd1 100644
--- a/runtime/themes/autumn.toml
+++ b/runtime/themes/autumn.toml
@@ -69,6 +69,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
diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml
index 460e7363..b8b2108a 100644
--- a/runtime/themes/base16_default_dark.toml
+++ b/runtime/themes/base16_default_dark.toml
@@ -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)
diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml
index 6874c39e..e23bafeb 100644
--- a/runtime/themes/base16_default_light.toml
+++ b/runtime/themes/base16_default_light.toml
@@ -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)
diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml
index 1f5328dc..f08a2c83 100644
--- a/runtime/themes/base16_terminal.toml
+++ b/runtime/themes/base16_terminal.toml
@@ -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" }
diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml
index 44bbaf92..181d67ab 100644
--- a/runtime/themes/bogster.toml
+++ b/runtime/themes/bogster.toml
@@ -72,3 +72,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" }
diff --git a/runtime/themes/boo_berry.toml b/runtime/themes/boo_berry.toml
index a582e87e..e413319a 100644
--- a/runtime/themes/boo_berry.toml
+++ b/runtime/themes/boo_berry.toml
@@ -58,6 +58,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_dim = "#47345E"
diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml
index 29a55281..7f1c8afa 100644
--- a/runtime/themes/dark_plus.toml
+++ b/runtime/themes/dark_plus.toml
@@ -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"
diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml
index 90bdb446..3e49b09b 100644
--- a/runtime/themes/dracula.toml
+++ b/runtime/themes/dracula.toml
@@ -55,6 +55,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"
diff --git a/runtime/themes/dracula_at_night.toml b/runtime/themes/dracula_at_night.toml
index 4a32f930..5298a39e 100644
--- a/runtime/themes/dracula_at_night.toml
+++ b/runtime/themes/dracula_at_night.toml
@@ -55,6 +55,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"
diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml
index 625e7df9..01c1cc73 100644
--- a/runtime/themes/everforest_dark.toml
+++ b/runtime/themes/everforest_dark.toml
@@ -80,6 +80,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]
diff --git a/runtime/themes/everforest_light.toml b/runtime/themes/everforest_light.toml
index 43f94618..523c258d 100644
--- a/runtime/themes/everforest_light.toml
+++ b/runtime/themes/everforest_light.toml
@@ -80,6 +80,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]
diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml
index aa7a464a..450a7720 100644
--- a/runtime/themes/gruvbox.toml
+++ b/runtime/themes/gruvbox.toml
@@ -69,6 +69,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"
diff --git a/runtime/themes/gruvbox_light.toml b/runtime/themes/gruvbox_light.toml
index dc7e3db1..96da57ee 100644
--- a/runtime/themes/gruvbox_light.toml
+++ b/runtime/themes/gruvbox_light.toml
@@ -70,6 +70,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"
diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml
index 8e70587e..1da3cb64 100644
--- a/runtime/themes/ingrid.toml
+++ b/runtime/themes/ingrid.toml
@@ -67,3 +67,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" }
diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml
index 0935ceff..a30ca63c 100644
--- a/runtime/themes/monokai.toml
+++ b/runtime/themes/monokai.toml
@@ -87,6 +87,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"
diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml
index 39fe6993..2c88776c 100644
--- a/runtime/themes/monokai_pro.toml
+++ b/runtime/themes/monokai_pro.toml
@@ -97,6 +97,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"
diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml
index 4360d622..0df3aab7 100644
--- a/runtime/themes/monokai_pro_machine.toml
+++ b/runtime/themes/monokai_pro_machine.toml
@@ -97,6 +97,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"
diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml
index c6feae8c..9672f701 100644
--- a/runtime/themes/monokai_pro_octagon.toml
+++ b/runtime/themes/monokai_pro_octagon.toml
@@ -97,6 +97,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"
diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml
index 0dad50d4..156371d1 100644
--- a/runtime/themes/monokai_pro_ristretto.toml
+++ b/runtime/themes/monokai_pro_ristretto.toml
@@ -97,6 +97,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"
diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml
index a42a2681..2261b68b 100644
--- a/runtime/themes/monokai_pro_spectrum.toml
+++ b/runtime/themes/monokai_pro_spectrum.toml
@@ -97,6 +97,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"
diff --git a/runtime/themes/night_owl.toml b/runtime/themes/night_owl.toml
index 55b09fbc..ba52e3cd 100644
--- a/runtime/themes/night_owl.toml
+++ b/runtime/themes/night_owl.toml
@@ -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'
diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml
index a2aff585..3b83ed32 100644
--- a/runtime/themes/nord.toml
+++ b/runtime/themes/nord.toml
@@ -106,6 +106,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"
diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml
index 1e7d9af1..49747f61 100644
--- a/runtime/themes/onedark.toml
+++ b/runtime/themes/onedark.toml
@@ -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"
diff --git a/runtime/themes/onelight.toml b/runtime/themes/onelight.toml
index a21c8ba2..07f58342 100644
--- a/runtime/themes/onelight.toml
+++ b/runtime/themes/onelight.toml
@@ -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"
diff --git a/runtime/themes/pop-dark.toml b/runtime/themes/pop-dark.toml
index bac0d0f9..7704ac8f 100644
--- a/runtime/themes/pop-dark.toml
+++ b/runtime/themes/pop-dark.toml
@@ -121,6 +121,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'
diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml
index 06be3083..d8124e03 100644
--- a/runtime/themes/rose_pine.toml
+++ b/runtime/themes/rose_pine.toml
@@ -66,6 +66,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"
diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml
index 129ae9c7..8bb69635 100644
--- a/runtime/themes/rose_pine_dawn.toml
+++ b/runtime/themes/rose_pine_dawn.toml
@@ -63,6 +63,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"
diff --git a/runtime/themes/serika-dark.toml b/runtime/themes/serika-dark.toml
index 88e4cf6d..f6cff99e 100644
--- a/runtime/themes/serika-dark.toml
+++ b/runtime/themes/serika-dark.toml
@@ -72,6 +72,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"
diff --git a/runtime/themes/serika-light.toml b/runtime/themes/serika-light.toml
index a00274bb..d6749400 100644
--- a/runtime/themes/serika-light.toml
+++ b/runtime/themes/serika-light.toml
@@ -72,6 +72,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]
diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml
index 1083b0c8..367a231d 100644
--- a/runtime/themes/solarized_dark.toml
+++ b/runtime/themes/solarized_dark.toml
@@ -103,6 +103,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"
diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml
index 737829a6..5ae05815 100644
--- a/runtime/themes/solarized_light.toml
+++ b/runtime/themes/solarized_light.toml
@@ -120,6 +120,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'
diff --git a/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml
index 0956afd2..8ac960a3 100644
--- a/runtime/themes/spacebones_light.toml
+++ b/runtime/themes/spacebones_light.toml
@@ -74,6 +74,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"
diff --git a/runtime/themes/tokyonight.toml b/runtime/themes/tokyonight.toml
index 4f1ea1cf..b6c16458 100644
--- a/runtime/themes/tokyonight.toml
+++ b/runtime/themes/tokyonight.toml
@@ -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"
diff --git a/runtime/themes/tokyonight_storm.toml b/runtime/themes/tokyonight_storm.toml
index c47ac54b..9da56546 100644
--- a/runtime/themes/tokyonight_storm.toml
+++ b/runtime/themes/tokyonight_storm.toml
@@ -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"