diff --git a/book/src/configuration.md b/book/src/configuration.md index 4d7e440a09fe0..ce9e96a4f63be 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -103,6 +103,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] '(' = ')' @@ -167,3 +169,12 @@ nbsp = "⍽" tab = "→" newline = "⏎" ``` + +### `[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 9d5d084177898..314ad7222ef53 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -247,6 +247,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. @@ -371,3 +373,34 @@ Keys to use within prompt, Remapping currently not supported. | `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 f0b54e0b7fa49..e02744e6cb5da 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -417,7 +417,10 @@ impl MappableCommand { decrement, "Decrement", 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", ); } @@ -2095,6 +2098,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 a8ff8be91bf6e..46686e0c4b1bd 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -248,6 +248,8 @@ pub fn default() -> HashMap { "k" => hover, "r" => rename_symbol, "?" => 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 52e581632d7a4..72250808186be 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, EventResult}, 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, } } @@ -1162,6 +1164,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, @@ -1293,7 +1300,11 @@ 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 for (view, is_focused) in cx.editor.tree.views() { let doc = cx.editor.document(view.doc).unwrap(); @@ -1374,9 +1385,28 @@ 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 config.explorer.is_embed() { + explore.content.render(area, surface, cx); + } else if explore.content.is_focus() { + 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 0000000000000..3ecd078607ba3 --- /dev/null +++ b/helix-term/src/ui/explore.rs @@ -0,0 +1,866 @@ +use super::{Prompt, Tree, TreeItem, TreeOp}; +use crate::{ + compositor::{Component, Compositor, Context, EventResult}, + ctrl, key, shift, ui, +}; +use anyhow::{bail, ensure, Result}; +use crossterm::event::{Event, KeyEvent}; +use helix_core::Position; +use helix_view::{ + editor::Action, + graphics::{CursorKind, Modifier, Rect}, + 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.clone(), 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 { + let (action, line) = match self.prompt.as_ref() { + Some((action, p)) => (action, p.line().clone()), + _ => return EventResult::Ignored(None), + }; + match (action, event.into()) { + (PromptAction::Search(_), _) => return self.handle_search_event(event, cx), + (PromptAction::Filter, _) => return self.handle_filter_event(event, cx), + (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')) => { + let _ = self.prompt.take(); + } + _ => { + if let Some((_, prompt)) = self.prompt.as_mut() { + prompt.handle_event(Event::Key(event), cx); + } + } + } + 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 2dca870baa42d..8210a1cf99c4d 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; mod markdown; pub mod menu; @@ -9,9 +10,11 @@ mod popup; mod prompt; mod spinner; 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}; @@ -19,6 +22,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 0000000000000..ea8209ffcbf49 --- /dev/null +++ b/helix-term/src/ui/tree.rs @@ -0,0 +1,697 @@ +use std::cmp::Ordering; +use std::iter::Peekable; + +use anyhow::Result; + +use crate::{ + compositor::{Context, EventResult}, + ctrl, key, shift, +}; +use crossterm::event::{Event, KeyEvent}; +use helix_core::unicode::width::UnicodeWidthStr; +use helix_view::graphics::Rect; +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) + } +} + +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); + } + // 折叠 + // binary_search? + + let end = self + .items + .iter() + .skip(self.selected + 2) + .position(|elem| elem.level <= current_level) + .map_or(self.items.len(), |end| self.selected + 2 + end); + self.items[self.selected].folded = + self.items.drain(self.selected + 1..end).collect(); + + 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 level = self.current().level; + + // binary search? + + let start = self + .items + .iter() + .take(self.selected) + .rposition(|elem| elem.level + 1 == level); + let start = match start { + Some(start) => start, + None => return, + }; + let end = self + .items + .iter() + .skip(self.selected + 1) + .position(|elem| elem.level <= level) + .map_or(self.items.len(), |end| self.selected + 1 + end); + + self.items[start].folded = self.items.drain(start + 1..end).collect(); + self.selected = start + } + + pub fn fold_current_child(&mut self) { + let current_level = self.current().level; + let next_level = if let Some(next_level) = self.next_item().map(|elem| elem.level) { + next_level + } else { + return; + }; + if next_level <= current_level { + return; + } + let end = self + .items + .iter() + .skip(self.selected + 2) + .position(|elem| elem.level <= current_level) + .map_or(self.items.len(), |end| self.selected + 2 + end); + self.items[self.selected].folded = self.items.drain(self.selected + 1..end).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 + .items + .iter() + .skip(skip) + .position(|elem| elem.item.filter(cx, s, params)) + .map_or(self.save_view.0, |end| skip + end); + + 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 + .items + .iter() + .take(take) + .rposition(|elem| elem.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 + .items + .iter() + .skip(self.selected + 1) + .position(|elem| { + elem.level < level + || (elem.level == level && elem.item.cmp(&item) != Ordering::Less) + }) + .map_or(self.items.len(), |p| p + self.selected + 1), + + Ordering::Greater => self + .items + .iter() + .take(self.selected) + .rposition(|elem| { + elem.level < level + || (elem.level == level && elem.item.cmp(&item) != Ordering::Greater) + }) + .map_or(0, |p| p + 1), + 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; + 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; + } + + self.row = 0; + self.col = 0; + self.selected = 0; + + if retain.is_empty() { + if let Some((_, recycle)) = self.recycle.take() { + self.items = recycle; + } + 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)); + } + } + + 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 f4a48ba65da3c..9c7605d992e4c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -104,6 +104,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 { @@ -152,6 +215,8 @@ pub struct Config { pub rulers: Vec, #[serde(default)] pub whitespace: WhitespaceConfig, + /// explore config + pub explorer: ExplorerConfig, } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] @@ -387,6 +452,7 @@ impl Default for Config { lsp: LspConfig::default(), rulers: Vec::new(), whitespace: WhitespaceConfig::default(), + explorer: ExplorerConfig::default(), } } }