From b652f96449d721c9f21e5bc1719b884e69a510bb Mon Sep 17 00:00:00 2001 From: cossonleo Date: Sun, 3 Apr 2022 15:55:08 +0800 Subject: [PATCH 001/191] tree helper and file explorer --- book/src/configuration.md | 11 + book/src/keymap.md | 34 + helix-term/src/commands.rs | 42 +- helix-term/src/keymap/default.rs | 2 + helix-term/src/ui/editor.rs | 42 +- helix-term/src/ui/explore.rs | 867 ++++++++++++++++++++++ helix-term/src/ui/mod.rs | 4 + helix-term/src/ui/tree.rs | 682 +++++++++++++++++ helix-view/src/editor.rs | 66 ++ runtime/themes/autumn.toml | 6 + runtime/themes/base16_default_dark.toml | 6 + runtime/themes/base16_default_light.toml | 6 + runtime/themes/base16_terminal.toml | 6 + runtime/themes/bogster.toml | 6 + runtime/themes/boo_berry.toml | 6 + runtime/themes/dark_plus.toml | 6 + runtime/themes/dracula.toml | 6 + runtime/themes/dracula_at_night.toml | 6 + runtime/themes/everforest_dark.toml | 5 + runtime/themes/everforest_light.toml | 5 + runtime/themes/gruvbox.toml | 6 + runtime/themes/gruvbox_light.toml | 6 + runtime/themes/ingrid.toml | 6 + runtime/themes/monokai.toml | 6 + runtime/themes/monokai_pro.toml | 6 + runtime/themes/monokai_pro_machine.toml | 6 + runtime/themes/monokai_pro_octagon.toml | 6 + runtime/themes/monokai_pro_ristretto.toml | 6 + runtime/themes/monokai_pro_spectrum.toml | 6 + runtime/themes/night_owl.toml | 6 + runtime/themes/nord.toml | 6 + runtime/themes/onedark.toml | 6 + runtime/themes/onelight.toml | 6 + runtime/themes/pop-dark.toml | 6 + runtime/themes/rose_pine.toml | 6 + runtime/themes/rose_pine_dawn.toml | 6 + runtime/themes/serika-dark.toml | 6 + runtime/themes/serika-light.toml | 5 + runtime/themes/solarized_dark.toml | 6 + runtime/themes/solarized_light.toml | 6 + runtime/themes/spacebones_light.toml | 6 + runtime/themes/tokyonight.toml | 6 + runtime/themes/tokyonight_storm.toml | 6 + 43 files changed, 1948 insertions(+), 3 deletions(-) create mode 100644 helix-term/src/ui/explore.rs create mode 100644 helix-term/src/ui/tree.rs 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 f84393ac..1fd20bed 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 16f7e601..a036407c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -432,7 +432,10 @@ impl MappableCommand { decrement, "Decrement item under cursor", record_macro, "Record macro", replay_macro, "Replay macro", - command_palette, "Open command palette", + command_palette, "Open command pallete", + toggle_or_focus_explorer, "toggle or focus explorer", + open_explorer_recursion, "open explorer recursion", + close_explorer, "close explorer", ); } @@ -2213,6 +2216,43 @@ fn file_picker_in_current_directory(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } +fn toggle_or_focus_explorer(cx: &mut Context) { + cx.callback = Some(Box::new( + |compositor: &mut Compositor, cx: &mut compositor::Context| { + if let Some(editor) = compositor.find::() { + 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 8a16dc1b..71f0f154 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 60cab905..07b27f59 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, } } @@ -1118,6 +1120,11 @@ impl Component for EditorView { event: Event, context: &mut crate::compositor::Context, ) -> EventResult { + if let Some(explore) = self.explorer.as_mut() { + if let EventResult::Consumed(callback) = explore.handle_event(event, context) { + return EventResult::Consumed(callback); + } + } let mut cx = commands::Context { editor: context.editor, count: None, @@ -1260,7 +1267,17 @@ impl Component for EditorView { surface.set_style(area, cx.editor.theme.get("ui.background")); let config = cx.editor.config(); // if the terminal size suddenly changed, we need to trigger a resize - cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline + let mut editor_area = area.clip_bottom(1); + if self.explorer.is_some() && (config.explorer.is_embed()) { + editor_area = editor_area.clip_left(config.explorer.column_width as u16 + 2); + } + cx.editor.resize(editor_area); // -1 from bottom for commandline + + if let Some(explore) = self.explorer.as_mut() { + if !explore.content.is_focus() && config.explorer.is_embed() { + explore.content.render(area, surface, cx); + } + } for (view, is_focused) in cx.editor.tree.views() { let doc = cx.editor.document(view.doc).unwrap(); @@ -1336,9 +1353,30 @@ impl Component for EditorView { if let Some(completion) = self.completion.as_mut() { completion.render(area, surface, cx); } + + if let Some(explore) = self.explorer.as_mut() { + if explore.content.is_focus() { + if config.explorer.is_embed() { + explore.content.render(area, surface, cx); + } else { + explore.render(area, surface, cx); + } + } + } } fn cursor(&self, _area: Rect, editor: &Editor) -> (Option, 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..ccdcb6cc --- /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 560e9155..046ca26d 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..e767e35d --- /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, // select item index + save_view: (usize, usize), // (selected, row) + winline: usize, // view row + 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), + winline: 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 = std::cmp::max(2, 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.winline = (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.winline = (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.winline = std::cmp::min(self.selected, self.winline + 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.winline = std::cmp::min(self.selected, self.winline.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.winline); + } + + pub fn restore_view(&mut self) { + (self.selected, self.winline) = 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.winline + } + + 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.winline = std::cmp::min(self.winline, 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.winline); + 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.winline = 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 520a425c..bc549249 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)] @@ -549,6 +614,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 d9c2f8a6..b0a8c5e1 100644 --- a/runtime/themes/autumn.toml +++ b/runtime/themes/autumn.toml @@ -68,6 +68,12 @@ "warning" = "my_yellow2" "error" = "my_red" +"ui.explorer.file" = { fg = "my_white" } +"ui.explorer.dir" = { fg = "my_yellow1" } +"ui.explorer.exe" = { fg = "my_green" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { modifiers = ["reversed"] } + [palette] my_black = "#212121" # Cursorline my_gray0 = "#262626" # Default Background 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 76e24648..a57f6451 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -70,3 +70,9 @@ # make diagnostic underlined, to distinguish with selection text. diagnostic = { modifiers = ["underlined"] } + +"ui.explorer.file" = { fg = "#e5ded6" } +"ui.explorer.dir" = { fg = "#59dcd8" } +"ui.explorer.exe" = { fg = "#e5ded6" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "#313f4e" } diff --git a/runtime/themes/boo_berry.toml b/runtime/themes/boo_berry.toml index 1509a1dc..a7536b06 100644 --- a/runtime/themes/boo_berry.toml +++ b/runtime/themes/boo_berry.toml @@ -57,6 +57,12 @@ "hint" = { fg = "lilac" } "diagnostic" = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "lilac" } +"ui.explorer.dir" = { fg = "mint" } +"ui.explorer.exe" = { fg = "lilac" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "berry_saturated" } + [palette] berry = "#3A2A4D" berry_saturated = "#2B1C3D" diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 451c28b5..c0588194 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 e3f5c625..139d9c67 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -54,6 +54,12 @@ "markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.raw" = { fg = "foreground" } +"ui.explorer.file" = { fg = "foreground" } +"ui.explorer.dir" = { fg = "cyan" } +"ui.explorer.exe" = { fg = "foreground" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "secondary_highlight" } + [palette] background = "#282a36" background_dark = "#21222c" diff --git a/runtime/themes/dracula_at_night.toml b/runtime/themes/dracula_at_night.toml index 776641a7..59ec273a 100644 --- a/runtime/themes/dracula_at_night.toml +++ b/runtime/themes/dracula_at_night.toml @@ -53,6 +53,12 @@ "markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.raw" = { fg = "foreground" } +"ui.explorer.file" = { fg = "foreground" } +"ui.explorer.dir" = { fg = "cyan" } +"ui.explorer.exe" = { fg = "foreground" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "comment" } + [palette] background = "#0e1419" background_dark = "#21222c" diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml index ef74ea9e..892da1b2 100644 --- a/runtime/themes/everforest_dark.toml +++ b/runtime/themes/everforest_dark.toml @@ -78,6 +78,11 @@ "error" = "red" "diagnostic" = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "fg" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "fg" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "grey1" } [palette] diff --git a/runtime/themes/everforest_light.toml b/runtime/themes/everforest_light.toml index 60557ba0..bf52688c 100644 --- a/runtime/themes/everforest_light.toml +++ b/runtime/themes/everforest_light.toml @@ -78,6 +78,11 @@ "error" = "red" "diagnostic" = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "fg" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "fg" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "grey1" } [palette] diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index 00ec2a8c..54955942 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -67,6 +67,12 @@ "markup.link.text" = "red1" "markup.raw" = "red1" +"ui.explorer.file" = { fg = "fg1" } +"ui.explorer.dir" = { fg = "blue0" } +"ui.explorer.exe" = { fg = "fg1" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "bg3" } + [palette] bg0 = "#282828" # main background bg1 = "#3c3836" diff --git a/runtime/themes/gruvbox_light.toml b/runtime/themes/gruvbox_light.toml index a4817b48..c2c32c53 100644 --- a/runtime/themes/gruvbox_light.toml +++ b/runtime/themes/gruvbox_light.toml @@ -68,6 +68,12 @@ "markup.link.text" = "red1" "markup.raw" = "red1" +"ui.explorer.file" = { fg = "fg1" } +"ui.explorer.dir" = { fg = "blue0" } +"ui.explorer.exe" = { fg = "fg1" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "bg3" } + [palette] bg0 = "#fbf1c7" # main background bg1 = "#ebdbb2" diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml index 58713704..6487567b 100644 --- a/runtime/themes/ingrid.toml +++ b/runtime/themes/ingrid.toml @@ -64,3 +64,9 @@ "error" = "#D74E50" "info" = "#839A53" "hint" = "#A6B6CE" + +"ui.explorer.file" = { fg = "#7B91B3" } +"ui.explorer.dir" = { fg = "#89BEB7" } +"ui.explorer.exe" = { fg = "#7B91B3" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "#F3EAE9" } diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index 2d11601c..6462bc16 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -85,6 +85,12 @@ diagnostic = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "text" } +"ui.explorer.dir" = { fg = "fn_declaration" } +"ui.explorer.exe" = { fg = "text" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "#75715e" } + [palette] type = "#A6E22E" keyword = "#F92672" diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml index 7c457d45..4ac2649f 100644 --- a/runtime/themes/monokai_pro.toml +++ b/runtime/themes/monokai_pro.toml @@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] } "markup.link.text" = "yellow" "markup.quote" = "green" +"ui.explorer.file" = { fg = "base8" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base8" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base4" } + [palette] # primary colors "red" = "#ff6188" diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml index bfc7031d..bfa7bbdf 100644 --- a/runtime/themes/monokai_pro_machine.toml +++ b/runtime/themes/monokai_pro_machine.toml @@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] } "markup.link.text" = "yellow" "markup.quote" = "green" +"ui.explorer.file" = { fg = "base8" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base8" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base4" } + [palette] # primary colors "red" = "#ff6d7e" diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml index 889e7624..bf291e1e 100644 --- a/runtime/themes/monokai_pro_octagon.toml +++ b/runtime/themes/monokai_pro_octagon.toml @@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] } "markup.link.text" = "yellow" "markup.quote" = "green" +"ui.explorer.file" = { fg = "base8" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base8" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base4" } + [palette] # primary colors "red" = "#ff657a" diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml index f8ad8422..1d4c3962 100644 --- a/runtime/themes/monokai_pro_ristretto.toml +++ b/runtime/themes/monokai_pro_ristretto.toml @@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] } "markup.link.text" = "yellow" "markup.quote" = "green" +"ui.explorer.file" = { fg = "base8" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base8" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base4" } + [palette] # primary colors "red" = "#fd6883" diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml index 9f5864fc..ae8a87c6 100644 --- a/runtime/themes/monokai_pro_spectrum.toml +++ b/runtime/themes/monokai_pro_spectrum.toml @@ -95,6 +95,12 @@ diagnostic = { modifiers = ["underlined"] } "markup.link.text" = "yellow" "markup.quote" = "green" +"ui.explorer.file" = { fg = "base8" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base8" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base4" } + [palette] # primary colors "red" = "#fc618d" 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 2df30814..4e0636c6 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -105,6 +105,12 @@ "diff.delta" = "nord12" "diff.minus" = "nord11" +"ui.explorer.file" = { fg = "nord6" } +"ui.explorer.dir" = { fg = "nord8" } +"ui.explorer.exe" = { fg = "nord6" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "nord2" } + [palette] nord0 = "#2e3440" nord1 = "#3b4252" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 3beb962f..16eab897 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 4d501a20..f730c3ce 100644 --- a/runtime/themes/pop-dark.toml +++ b/runtime/themes/pop-dark.toml @@ -120,6 +120,12 @@ namespace = { fg = 'orangeL' } 'diff.delta' = { fg = '#4d4ddd' } 'diff.delta.moved' = { fg = '#dd4ddd' } +"ui.explorer.file" = { fg = "greyT" } +"ui.explorer.dir" = { fg = "blueL" } +"ui.explorer.exe" = { fg = "greyT" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "greyL" } + [palette] white = '#FFFFFF' greyH = '#CFCFCF' diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index 1e494946..81feff11 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -65,6 +65,12 @@ "markup.quote" = { fg = "rose" } "markup.raw" = { fg = "foam" } +"ui.explorer.file" = { fg = "text" } +"ui.explorer.dir" = { fg = "rose" } +"ui.explorer.exe" = { fg = "text" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "highlight" } + [palette] base = "#191724" surface = "#1f1d2e" diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml index b28f6412..ccf088fc 100644 --- a/runtime/themes/rose_pine_dawn.toml +++ b/runtime/themes/rose_pine_dawn.toml @@ -62,6 +62,12 @@ "markup.quote" = { fg = "rose" } "markup.raw" = { fg = "foam" } +"ui.explorer.file" = { fg = "text" } +"ui.explorer.dir" = { fg = "rose" } +"ui.explorer.exe" = { fg = "text" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "highlight" } + [palette] base = "#faf4ed" surface = "#fffaf3" diff --git a/runtime/themes/serika-dark.toml b/runtime/themes/serika-dark.toml index e13c2f87..a2f292ab 100644 --- a/runtime/themes/serika-dark.toml +++ b/runtime/themes/serika-dark.toml @@ -70,6 +70,12 @@ "markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.raw" = { fg = "fg" } +"ui.explorer.file" = { fg = "fg" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "fg" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "bg3" } + [palette] bg0 = "#323437" diff --git a/runtime/themes/serika-light.toml b/runtime/themes/serika-light.toml index c0a4b8d8..58ef86f4 100644 --- a/runtime/themes/serika-light.toml +++ b/runtime/themes/serika-light.toml @@ -70,6 +70,11 @@ "markup.quote" = { fg = "yellow", modifiers = ["italic"] } "markup.raw" = { fg = "fg" } +"ui.explorer.file" = { fg = "fg" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "fg" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "bg3" } [palette] diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index aa6dbbf8..1bc5f82e 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -101,6 +101,12 @@ "hint" = { fg = "base01", modifiers= ["bold", "underlined"] } "diagnostic" = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "base1" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base1" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base0175" } + [palette] # 深色 越来越深 base03 = "#002b36" diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml index 79fd8364..d22d6381 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -118,6 +118,12 @@ "hint" = { fg = "base01", modifiers= ["bold", "underlined"] } "diagnostic" = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "base1" } +"ui.explorer.dir" = { fg = "blue" } +"ui.explorer.exe" = { fg = "base1" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "base0175" } + [palette] red = '#dc322f' green = '#859900' diff --git a/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml index e12ef285..30de53cf 100644 --- a/runtime/themes/spacebones_light.toml +++ b/runtime/themes/spacebones_light.toml @@ -73,6 +73,12 @@ "diagnostic" = { modifiers = ["underlined"] } +"ui.explorer.file" = { fg = "fg1" } +"ui.explorer.dir" = { fg = "#715ab1" } +"ui.explorer.exe" = { fg = "fg1" } +"ui.explorer.focus" = { modifiers = ["reversed"] } +"ui.explorer.unfocus" = { bg = "bg3" } + [palette] base = "#655370" base-dim = "#a094a2" 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" -- 2.38.5 From d9d4daa87de012cf398a821c141e8c8cd085b44e Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 1 Feb 2023 23:51:07 +0800 Subject: [PATCH 002/191] feat(ui/explore): implement "focus current file" --- helix-term/src/commands.rs | 19 +++++--- helix-term/src/keymap/default.rs | 2 +- helix-term/src/ui/explore.rs | 31 ++++++------ helix-term/src/ui/tree.rs | 84 ++++++++++++++++++++++++++++---- 4 files changed, 104 insertions(+), 32 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a036407c..c7768094 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -433,8 +433,8 @@ impl MappableCommand { record_macro, "Record macro", replay_macro, "Replay macro", command_palette, "Open command pallete", - toggle_or_focus_explorer, "toggle or focus explorer", - open_explorer_recursion, "open explorer recursion", + toggle_or_focus_explorer, "Toggle or focus explorer", + focus_current_file, "Focus current file in explorer", close_explorer, "close explorer", ); } @@ -2232,13 +2232,20 @@ fn toggle_or_focus_explorer(cx: &mut Context) { )); } -fn open_explorer_recursion(cx: &mut Context) { +fn focus_current_file(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)), + match editor.explorer.as_mut() { + Some(explore) => explore.content.focus_current_file(cx), + None => match ui::Explorer::new(cx) { + Ok(explore) => { + let mut explorer = overlayed(explore); + explorer.content.focus_current_file(cx); + editor.explorer = Some(explorer); + } + Err(err) => cx.editor.set_error(format!("{}", err)), + }, } } }, diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 71f0f154..167ef510 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -265,7 +265,7 @@ pub fn default() -> HashMap { "h" => select_references_to_symbol_under_cursor, "?" => command_palette, "e" => toggle_or_focus_explorer, - "E" => open_explorer_recursion, + "E" => focus_current_file, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index ccdcb6cc..af54d129 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -77,6 +77,7 @@ impl FileInfo { 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; @@ -179,6 +180,10 @@ impl TreeItem for FileInfo { self.get_text().contains(s) } } + + fn text_string(&self) -> String { + self.get_text().to_string() + } } // #[derive(Default, Debug, Clone)] @@ -262,21 +267,15 @@ impl Explorer { }) } - 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 focus_current_file(&mut self, cx: &mut Context) { + let current_document_path = doc!(cx.editor).path().cloned(); + match current_document_path { + None => cx.editor.set_error("No opened document."), + Some(path) => { + self.tree.focus_path(cx, path, &self.state.current_root); + self.focus(); + } + } } // pub fn new_with_uri(uri: String) -> Result { @@ -289,7 +288,7 @@ impl Explorer { // } pub fn focus(&mut self) { - self.state.focus = true + self.state.focus = true; } pub fn unfocus(&mut self) { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index e767e35d..4bc53191 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1,5 +1,5 @@ -use std::cmp::Ordering; use std::iter::Peekable; +use std::{cmp::Ordering, path::PathBuf}; use anyhow::Result; @@ -18,6 +18,7 @@ pub trait TreeItem: Sized { type Params; fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; + fn text_string(&self) -> String; fn is_child(&self, other: &Self) -> bool; fn cmp(&self, other: &Self) -> Ordering; @@ -253,15 +254,76 @@ impl Tree { iter.skip(start).position(f).map(|p| p + start) } } + + pub fn focus_path(&mut self, cx: &mut Context, current_path: PathBuf, current_root: &PathBuf) { + let current_path = current_path.as_path().to_string_lossy().to_string(); + let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; + let nodes = current_path + .strip_prefix(current_root.as_str()) + .expect( + format!( + "Failed to strip prefix '{}' from '{}'", + current_root, current_path + ) + .as_str(), + ) + .split(std::path::MAIN_SEPARATOR) + .enumerate() + .collect::>(); + + let len = nodes.len(); + + // `preivous_item_index` is necessary to avoid choosing the first file + // that is not the current file. + // For example, consider a project that contains multiple `Cargo.toml`. + // Without `previous_item_index`, the first `Cargo.toml` will always be chosen, + // regardless of which `Cargo.toml` the user wishes to find in the explorer. + let mut previous_item_index = 0; + for (index, node) in nodes { + let current_level = index + 1; + let is_last = index == len - 1; + match self + .items + .iter() + .enumerate() + .position(|(item_index, item)| { + item_index >= previous_item_index + && item.item.text_string().eq(node) + && item.level == current_level + }) { + Some(index) => { + if is_last { + self.selected = index + } else { + let item = &self.items[index]; + let items = match item.item.get_childs() { + Ok(items) => items, + Err(e) => return cx.editor.set_error(format!("{e}")), + }; + let inserts = vec_to_tree(items, current_level + 1); + previous_item_index = index; + let _: Vec<_> = self.items.splice(index + 1..index + 1, inserts).collect(); + } + } + None => cx.editor.set_error(format!( + "The following file does not exist anymore: '{}'. node = {}", + current_path, node + )), + } + } + + // Center the selection + self.winline = self.max_len / 2; + } } impl Tree { - pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params) { + pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params, selected_index: usize) { 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 = &mut self.items[selected_index]; let current_level = current.level; if next_level > current_level { if let Some(mut on_folded_fn) = self.on_folded_fn.take() { @@ -275,13 +337,13 @@ impl Tree { if let Some(mut on_open_fn) = self.on_opened_fn.take() { let mut f = || { - let current = &mut self.items[self.selected]; + let current = &mut self.items[selected_index]; 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) + .splice(selected_index + 1..selected_index + 1, inserts) .collect(); return; } @@ -297,17 +359,17 @@ impl Tree { let inserts = vec_to_tree(items, current.level + 1); let _: Vec<_> = self .items - .splice(self.selected + 1..self.selected + 1, inserts) + .splice(selected_index + 1..selected_index + 1, inserts) .collect(); }; f(); self.on_opened_fn = Some(on_open_fn) } else { - let current = &mut self.items[self.selected]; + let current = &mut self.items[selected_index]; let inserts = std::mem::take(&mut current.folded); let _: Vec<_> = self .items - .splice(self.selected + 1..self.selected + 1, inserts) + .splice(selected_index + 1..selected_index + 1, inserts) .collect(); } } @@ -430,6 +492,10 @@ impl Tree { self.items[self.selected].item = item; } + pub fn set_selected(&mut self, selected: usize) { + self.selected = selected + } + pub fn insert_current_level(&mut self, item: T) { let current = self.current(); let level = current.level; @@ -567,7 +633,7 @@ impl Tree { 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), + key!(Enter) => self.on_enter(cx, params, self.selected), ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), shift!('D') => self.move_down_page(), -- 2.38.5 From c446c396453f3fd8f52aab900202b9835806e22e Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 6 Feb 2023 22:11:48 +0800 Subject: [PATCH 003/191] feat(explorer/position): right According to https://github.com/helix-editor/helix/pull/5768#issuecomment-1413162928 --- helix-term/src/ui/editor.rs | 30 ++++++++++----- helix-term/src/ui/explore.rs | 74 +++++++++++++++++++++++++++--------- helix-view/src/editor.rs | 19 ++++++--- 3 files changed, 89 insertions(+), 34 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 07b27f59..73bc7173 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -17,7 +17,7 @@ use helix_core::{ }; use helix_view::{ document::Mode, - editor::{CompleteAction, CursorShapeConfig}, + editor::{CompleteAction, CursorShapeConfig, ExplorerPosition}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, @@ -1266,16 +1266,26 @@ impl Component for EditorView { // clear with background color 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 - 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); - } + let editor_area = area.clip_bottom(1); + let explorer_column_width = config.explorer.column_width as u16 + 2; + let editor_area = if self.explorer.is_some() { + match config.explorer.position { + ExplorerPosition::Overlay => editor_area, + ExplorerPosition::Left => editor_area.clip_left(explorer_column_width), + ExplorerPosition::Right => editor_area.clip_right(explorer_column_width), + } + } else { + editor_area + }; 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); + if let Some(explorer) = self.explorer.as_mut() { + if !explorer.content.is_focus() { + if let Some(position) = config.explorer.is_embed() { + explorer.content.render_embed(area, surface, cx, &position); + } } } @@ -1356,8 +1366,8 @@ impl Component for EditorView { if let Some(explore) = self.explorer.as_mut() { if explore.content.is_focus() { - if config.explorer.is_embed() { - explore.content.render(area, surface, cx); + if let Some(position) = config.explorer.is_embed() { + explore.content.render_embed(area, surface, cx, &position); } else { explore.render(area, surface, cx); } diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index af54d129..3d5ccbbf 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{bail, ensure, Result}; use helix_core::Position; use helix_view::{ - editor::Action, + editor::{Action, ExplorerPositionEmbed}, graphics::{CursorKind, Modifier, Rect}, input::{Event, KeyEvent}, Editor, @@ -496,19 +496,39 @@ impl Explorer { self.tree.render(list_area, surface, cx, &mut self.state); } - fn render_embed(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + pub fn render_embed( + &mut self, + area: Rect, + surface: &mut Surface, + cx: &mut Context, + position: &ExplorerPositionEmbed, + ) { 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 width = area.width.min(config.column_width as u16 + 2); + + let side_area = match position { + ExplorerPositionEmbed::Left => Rect { width, ..area }, + ExplorerPositionEmbed::Right => Rect { + x: area.width - width, + width, + ..area + }, + } + .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); + let list_area = match position { + ExplorerPositionEmbed::Left => { + render_block(side_area.clip_left(1), surface, Borders::RIGHT).clip_bottom(1) + } + ExplorerPositionEmbed::Right => { + render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1) + } + }; self.tree.render(list_area, surface, cx, &mut self.state); { @@ -517,7 +537,11 @@ impl Explorer { } else { cx.editor.theme.get("ui.statusline.inactive") }; - let area = side_area.clip_top(list_area.height).clip_right(1); + let area = side_area.clip_top(list_area.height); + let area = match position { + ExplorerPositionEmbed::Left => area.clip_right(1), + ExplorerPositionEmbed::Right => area.clip_left(1), + }; surface.clear_with(area, statusline); // surface.set_string_truncated( // area.x, @@ -531,16 +555,30 @@ impl Explorer { } if self.is_focus() { + const PREVIEW_AREA_MAX_WIDTH: u16 = 90; + const PREVIEW_AREA_MAX_HEIGHT: u16 = 25; + let preview_area_width = (area.width - side_area.width).min(PREVIEW_AREA_MAX_WIDTH); + let preview_area_height = area.height.min(PREVIEW_AREA_MAX_HEIGHT); + + let preview_area = match position { + ExplorerPositionEmbed::Left => area.clip_left(side_area.width).clip_bottom(2), + ExplorerPositionEmbed::Right => (Rect { + x: area.width - side_area.width - preview_area_width, + ..area + }) + .clip_right(side_area.width) + .clip_bottom(2), + }; 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); + let y = self.tree.row().saturating_sub(1) as u16; + let y = if (preview_area_height + y) > preview_area.height { + preview_area.height - preview_area_height + } else { + y + }; + let area = Rect::new(preview_area.x, y, preview_area_width, preview_area_height); surface.clear_with(area, background); let area = render_block(area, surface, Borders::all()); self.render_preview(area, surface, cx.editor); @@ -800,8 +838,8 @@ impl Component for Explorer { return; } let config = &cx.editor.config().explorer; - if config.is_embed() { - self.render_embed(area, surface, cx); + if let Some(position) = config.is_embed() { + self.render_embed(area, surface, cx, &position); } else { self.render_float(area, surface, cx); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bc549249..4bb386fa 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -119,8 +119,14 @@ pub enum ExplorerStyle { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ExplorerPosition { - Embed, Overlay, + Left, + Right, +} + +pub enum ExplorerPositionEmbed { + Left, + Right, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -133,17 +139,18 @@ pub struct ExplorerConfig { } impl ExplorerConfig { - pub fn is_embed(&self) -> bool { + pub fn is_embed(&self) -> Option { match self.position { - ExplorerPosition::Embed => true, - ExplorerPosition::Overlay => false, + ExplorerPosition::Overlay => None, + ExplorerPosition::Left => Some(ExplorerPositionEmbed::Left), + ExplorerPosition::Right => Some(ExplorerPositionEmbed::Right), } } pub fn is_overlay(&self) -> bool { match self.position { - ExplorerPosition::Embed => false, ExplorerPosition::Overlay => true, + _ => false, } } @@ -166,7 +173,7 @@ impl Default for ExplorerConfig { fn default() -> Self { Self { style: ExplorerStyle::Tree, - position: ExplorerPosition::Overlay, + position: ExplorerPosition::Left, column_width: 30, } } -- 2.38.5 From d04a1ce2148d7afd11573267d62c730f6ccfa1de Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 11 Feb 2023 15:17:43 +0800 Subject: [PATCH 004/191] refactor(tree): change internal implementation Previous: Vec+Tree hybrid, hard to debug and understand Now: Pure Tree structure, easy to understand and test --- helix-term/src/ui/explore.rs | 52 +- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/tree.rs | 1049 ++++++++++++++++++++++------------ 3 files changed, 716 insertions(+), 387 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 3d5ccbbf..e1d8e06c 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -1,4 +1,4 @@ -use super::{Prompt, Tree, TreeItem, TreeOp}; +use super::{Prompt, TreeItem, TreeOp, TreeView}; use crate::{ compositor::{Component, Compositor, Context, EventResult}, ctrl, key, shift, ui, @@ -86,18 +86,20 @@ impl TreeItem for FileInfo { 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 style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text")); + let style = if !selected { + style + } else { 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); + style.patch(patch) } else { - style = style.add_modifier(Modifier::REVERSED); + style.add_modifier(Modifier::REVERSED) } - } + }; Spans::from(Span::styled(text, style)) } @@ -142,7 +144,7 @@ impl TreeItem for FileInfo { self.path.cmp(&other.path) } - fn get_childs(&self) -> Result> { + fn get_children(&self) -> Result> { match self.file_type { FileType::Root | FileType::Dir => {} _ => return Ok(vec![]), @@ -173,7 +175,7 @@ impl TreeItem for FileInfo { Ok(ret) } - fn filter(&self, _cx: &mut Context, s: &str, _params: &mut Self::Params) -> bool { + fn filter(&self, s: &str) -> bool { if s.is_empty() { false } else { @@ -184,6 +186,13 @@ impl TreeItem for FileInfo { fn text_string(&self) -> String { self.get_text().to_string() } + + fn is_parent(&self) -> bool { + match self.file_type { + FileType::Dir | FileType::Parent | FileType::Root => true, + _ => false, + } + } } // #[derive(Default, Debug, Clone)] @@ -245,7 +254,7 @@ impl State { } pub struct Explorer { - tree: Tree, + tree: TreeView, state: State, prompt: Option<(PromptAction, Prompt)>, #[allow(clippy::type_complexity)] @@ -257,9 +266,10 @@ pub struct Explorer { 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)?; + let root = FileInfo::root(current_root.clone()); + let children = root.get_children()?; Ok(Self { - tree: Tree::build_tree(items).with_enter_fn(Self::toggle_current), + tree: TreeView::build_tree(root, children).with_enter_fn(Self::toggle_current), state: State::new(true, current_root), repeat_motion: None, prompt: None, @@ -300,14 +310,13 @@ impl Explorer { } 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) + todo!() + // let mut items = vec![FileInfo::parent(p.as_path())]; + + // if cx.editor.config().explorer.is_tree() { + // items.push(root) + // } + // Ok(items) } fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) { @@ -794,7 +803,10 @@ impl Component for Explorer { 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); + let root = FileInfo::root(self.state.current_root.clone()); + let children = root.get_children().expect("TODO: handle error"); + self.tree = TreeView::build_tree(root, children) + .with_enter_fn(Self::toggle_current); } Err(e) => cx.editor.set_error(format!("{e}")), } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 046ca26d..40a0eb32 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -24,7 +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}; +pub use tree::{TreeItem, TreeOp, TreeView}; 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 index 4bc53191..362ea43f 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1,7 +1,9 @@ use std::iter::Peekable; +use std::slice::Iter; use std::{cmp::Ordering, path::PathBuf}; use anyhow::Result; +use helix_view::theme::Modifier; use crate::{ compositor::{Context, EventResult}, @@ -20,19 +22,14 @@ pub trait TreeItem: Sized { fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; fn text_string(&self) -> String; fn is_child(&self, other: &Self) -> bool; + fn is_parent(&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 filter(&self, s: &str) -> bool { + self.text_string().contains(s) } - fn get_childs(&self) -> Result> { + fn get_children(&self) -> Result> { Ok(vec![]) } } @@ -48,47 +45,28 @@ fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { 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); - } - } - +fn vec_to_tree(mut items: Vec) -> Vec> { 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 + index_elems( + 0, + items + .into_iter() + .map(|item| Tree::new(item, vec![])) + .collect(), + ) } // return total elems's count contain self -fn get_elems_recursion(t: &mut Elem, depth: usize) -> Result { - let mut childs = t.item.get_childs()?; +fn get_elems_recursion(t: &mut Tree, depth: usize) -> Result { + let mut childs = t.item.get_children()?; childs.sort_by(tree_item_cmp); let mut elems = Vec::with_capacity(childs.len()); - let level = t.level + 1; + // let level = t.level + 1; + let level = todo!(); + let mut total = 1; for child in childs { - let mut elem = Elem::new(child, level); + let mut elem = Tree::new(child, level); let count = if depth > 0 { get_elems_recursion(&mut elem, depth - 1)? } else { @@ -97,12 +75,12 @@ fn get_elems_recursion(t: &mut Elem, depth: usize) -> Result(dist: &mut Vec>, mut t: Elem) { - let childs = std::mem::take(&mut t.folded); +fn expand_elems(dist: &mut Vec>, mut t: Tree) { + let childs = std::mem::take(&mut t.children); dist.push(t); for child in childs { expand_elems(dist, child) @@ -117,39 +95,121 @@ pub enum TreeOp { ReplaceTree(Vec), } -pub struct Elem { +#[derive(Debug, PartialEq, Eq)] +pub struct Tree { item: T, - level: usize, - folded: Vec, + parent_index: Option, + index: usize, + children: Vec, + + /// Why do we need this property? + /// Can't we just use `!children.is_empty()`? + /// + /// Because we might have for example an open folder that is empty, + /// and user just added a new file under that folder, + /// and the user refreshes the whole tree. + /// + /// Without `open`, we will not refresh any node without children, + /// and thus the folder still appears empty after refreshing. + is_opened: bool, } -impl Clone for Elem { +impl Clone for Tree { fn clone(&self) -> Self { Self { item: self.item.clone(), - level: self.level, - folded: self.folded.clone(), + index: self.index, + children: self.children.clone(), + is_opened: false, + parent_index: self.parent_index, } } } -impl Elem { - pub fn new(item: T, level: usize) -> Self { +struct TreeIter<'a, T> { + current_index: usize, + tree: &'a Tree, +} + +impl<'a, T> Iterator for TreeIter<'a, T> { + type Item = &'a Tree; + + fn next(&mut self) -> Option { + let index = self.current_index; + self.current_index += 1; + + self.tree.find_by_index(index) + } +} + +impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { + fn next_back(&mut self) -> Option { + let index = self.current_index; + self.current_index -= 1; + self.tree.find_by_index(index) + } +} + +impl<'a, T> ExactSizeIterator for TreeIter<'a, T> { + fn len(&self) -> usize { + self.tree.len() + } +} + +impl Tree { + fn iter(&self) -> TreeIter { + TreeIter { + tree: self, + current_index: 0, + } + } + pub fn new(item: T, children: Vec>) -> Self { Self { item, - level, - folded: vec![], + index: 0, + parent_index: None, + children: index_elems(1, children), + is_opened: false, } } pub fn item(&self) -> &T { &self.item } + + fn find_by_index(&self, index: usize) -> Option<&Tree> { + if self.index == index { + Some(self) + } else { + self.children + .iter() + .find_map(|elem| elem.find_by_index(index)) + } + } + + fn find_by_index_mut(&mut self, index: usize) -> Option<&mut Tree> { + if self.index == index { + Some(self) + } else { + self.children + .iter_mut() + .find_map(|elem| elem.find_by_index_mut(index)) + } + } + + fn len(&self) -> usize { + (1 as usize).saturating_add(self.children.iter().map(|elem| elem.len()).sum()) + } + + fn regenerate_index(&mut self) { + let items = std::mem::take(&mut self.children); + self.children = index_elems(1, items); + } } -pub struct Tree { - items: Vec>, - recycle: Option<(String, Vec>)>, +pub struct TreeView { + tree: Tree, + recycle: Option<(String, Vec>)>, selected: usize, // select item index save_view: (usize, usize), // (selected, row) winline: usize, // view row @@ -168,10 +228,10 @@ pub struct Tree { on_next_key: Option>, } -impl Tree { - pub fn new(items: Vec>) -> Self { +impl TreeView { + pub fn new(root: T, items: Vec>) -> Self { Self { - items, + tree: Tree::new(root, items), recycle: None, selected: 0, save_view: (0, 0), @@ -188,22 +248,15 @@ impl Tree { } 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)) + todo!() + // let old = std::mem::replace(self, Self::new(vec_to_tree(items))); + // 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_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 build_tree(root: T, items: Vec) -> Self { + Self::new(root, vec_to_tree(items)) } pub fn with_enter_fn(mut self, f: F) -> Self @@ -227,34 +280,19 @@ impl Tree { 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 + fn find(&self, start: usize, reverse: bool, f: F) -> Option where - F: FnMut(&Elem) -> bool, + F: FnMut(&Tree) -> bool, { - let iter = self.items.iter(); - if rev { + let iter = self.tree.iter(); + if reverse { iter.take(start).rposition(f) } else { iter.skip(start).position(f).map(|p| p + start) } } + /// TODO: current_path should not be PathBuf, but Vec so that Tree can be generic pub fn focus_path(&mut self, cx: &mut Context, current_path: PathBuf, current_root: &PathBuf) { let current_path = current_path.as_path().to_string_lossy().to_string(); let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; @@ -278,125 +316,162 @@ impl Tree { // For example, consider a project that contains multiple `Cargo.toml`. // Without `previous_item_index`, the first `Cargo.toml` will always be chosen, // regardless of which `Cargo.toml` the user wishes to find in the explorer. - let mut previous_item_index = 0; - for (index, node) in nodes { - let current_level = index + 1; - let is_last = index == len - 1; - match self - .items - .iter() - .enumerate() - .position(|(item_index, item)| { - item_index >= previous_item_index - && item.item.text_string().eq(node) - && item.level == current_level - }) { - Some(index) => { - if is_last { - self.selected = index - } else { - let item = &self.items[index]; - let items = match item.item.get_childs() { - Ok(items) => items, - Err(e) => return cx.editor.set_error(format!("{e}")), - }; - let inserts = vec_to_tree(items, current_level + 1); - previous_item_index = index; - let _: Vec<_> = self.items.splice(index + 1..index + 1, inserts).collect(); - } - } - None => cx.editor.set_error(format!( - "The following file does not exist anymore: '{}'. node = {}", - current_path, node - )), - } - } + // let mut previous_item_index = 0; + // for (index, node) in nodes { + // let current_level = index + 1; + // let is_last = index == len - 1; + // match self + // .items + // .iter() + // .enumerate() + // .position(|(item_index, item)| { + // item_index >= previous_item_index + // && item.item.text_string().eq(node) + // && item.level == current_level + // }) { + // Some(index) => { + // if is_last { + // self.selected = index + // } else { + // let item = &self.items[index]; + // let items = match item.item.get_childs() { + // Ok(items) => items, + // Err(e) => return cx.editor.set_error(format!("{e}")), + // }; + // let inserts = vec_to_tree(items, current_level + 1); + // previous_item_index = index; + // let _: Vec<_> = self.items.splice(index + 1..index + 1, inserts).collect(); + // } + // } + // None => cx.editor.set_error(format!( + // "The following file does not exist anymore: '{}'. node = {}", + // current_path, node + // )), + // } + // } // Center the selection self.winline = self.max_len / 2; } + + fn regenerate_index(&mut self) { + self.tree.regenerate_index(); + } + + fn go_to_parent(&mut self) { + if let Some(parent) = self.current_parent() { + self.selected = parent.index + } + } + + fn go_to_children(&mut self, cx: &mut Context) { + let current = self.current_mut(); + if current.is_opened { + self.selected += 1; + return; + } + let items = match current.item.get_children() { + Ok(items) => items, + Err(e) => return cx.editor.set_error(format!("{e}")), + }; + if items.is_empty() { + return; + } + current.is_opened = true; + current.children = vec_to_tree(items); + self.selected += 1; + self.regenerate_index() + } } -impl Tree { +impl TreeView { pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params, selected_index: usize) { - if self.items.is_empty() { + // if let Some(next_level) = self.next_item().map(|elem| elem.level) { + // let current = self.find_by_index(selected_index); + // 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; + // } + // } + // + let mut selected_item = self.find_by_index_mut(selected_index); + if selected_item.is_opened { + selected_item.is_opened = false; + selected_item.children = vec![]; + self.regenerate_index(); return; } - if let Some(next_level) = self.next_item().map(|elem| elem.level) { - let current = &mut self.items[selected_index]; - 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[selected_index]; - let items = match on_open_fn(&mut current.item, cx, params) { + let current = &mut self.find_by_index_mut(selected_index); + match on_open_fn(&mut current.item, cx, params) { TreeOp::Restore => { - let inserts = std::mem::take(&mut current.folded); - let _: Vec<_> = self - .items - .splice(selected_index + 1..selected_index + 1, inserts) - .collect(); + panic!(); + // let inserts = std::mem::take(&mut current.folded); + // let _: Vec<_> = self + // .items + // .splice(selected_index + 1..selected_index + 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, + TreeOp::InsertChild(items) => { + items; + } + TreeOp::GetChildsAndInsert => { + let items = match current.item.get_children() { + Ok(items) => items, + Err(e) => return cx.editor.set_error(format!("{e}")), + }; + current.is_opened = true; + current.children = vec_to_tree(items); + } + TreeOp::ReplaceTree(items) => { + return self.replace_with_new_items(items); + } + TreeOp::Noop => {} }; - current.folded = vec![]; - let inserts = vec_to_tree(items, current.level + 1); - let _: Vec<_> = self - .items - .splice(selected_index + 1..selected_index + 1, inserts) - .collect(); + + // current.folded = vec![]; + // let inserts = vec_to_tree(items, current.level + 1); + // let _: Vec<_> = self + // .items + // .splice(selected_index + 1..selected_index + 1, inserts) + // .collect(); }; f(); + self.regenerate_index(); self.on_opened_fn = Some(on_open_fn) } else { - let current = &mut self.items[selected_index]; - let inserts = std::mem::take(&mut current.folded); - let _: Vec<_> = self - .items - .splice(selected_index + 1..selected_index + 1, inserts) - .collect(); + panic!(); + self.find_by_index_mut(selected_index).children = vec![]; + // let current = &mut self.items[selected_index]; + // let inserts = std::mem::take(&mut current.folded); + // let _: Vec<_> = self + // .items + // .splice(selected_index + 1..selected_index + 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(); + if let Some(parent) = self.current_parent_mut() { + parent.is_opened = false; + parent.children = vec![]; + self.selected = parent.index; + self.regenerate_index() } } pub fn search_next(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) { let skip = std::cmp::max(2, self.save_view.0 + 1); self.selected = self - .find(skip, false, |e| e.item.filter(cx, s, params)) + .find(skip, false, |e| e.item.filter(s)) .unwrap_or(self.save_view.0); self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); @@ -405,14 +480,14 @@ impl Tree { 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)) + .find(take, true, |e| e.item.filter(s)) .unwrap_or(self.save_view.0); self.winline = (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(); + let len = self.tree.len(); if len > 0 { self.selected = std::cmp::min(self.selected + rows, len.saturating_sub(1)); self.winline = std::cmp::min(self.selected, self.winline + rows); @@ -420,9 +495,9 @@ impl Tree { } pub fn move_up(&mut self, rows: usize) { - let len = self.items.len(); + let len = self.tree.len(); if len > 0 { - self.selected = self.selected.saturating_sub(rows); + self.selected = std::cmp::max(0, self.selected.saturating_sub(rows)); self.winline = std::cmp::min(self.selected, self.winline.saturating_sub(rows)); } } @@ -470,12 +545,43 @@ impl Tree { (self.selected, self.winline) = self.save_view; } - pub fn current(&self) -> &Elem { - &self.items[self.selected] + fn find_by_index(&self, index: usize) -> &Tree { + self.tree + .iter() + .find_map(|item| item.find_by_index(index)) + .unwrap() + } + + fn find_by_index_mut(&mut self, index: usize) -> &mut Tree { + self.tree.find_by_index_mut(index).unwrap() + } + + pub fn current(&self) -> &Tree { + self.find_by_index(self.selected) + } + + pub fn current_mut(&mut self) -> &mut Tree { + self.find_by_index_mut(self.selected) + } + + fn current_parent(&self) -> Option<&Tree> { + if let Some(parent_index) = self.current().parent_index { + Some(self.find_by_index(parent_index)) + } else { + None + } + } + + fn current_parent_mut(&mut self) -> Option<&mut Tree> { + if let Some(parent_index) = self.current().parent_index { + Some(self.find_by_index_mut(parent_index)) + } else { + None + } } pub fn current_item(&self) -> &T { - &self.items[self.selected].item + &self.current().item } pub fn row(&self) -> usize { @@ -483,13 +589,14 @@ impl Tree { } pub fn remove_current(&mut self) -> T { - let elem = self.items.remove(self.selected); - self.selected = self.selected.saturating_sub(1); - elem.item + todo!() + // let elem = self.tree.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; + self.current_mut().item = item } pub fn set_selected(&mut self, selected: usize) { @@ -497,32 +604,16 @@ impl Tree { } 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)); + let current = self.current_mut(); + current.children.push(Tree::new(item, vec![])); + current + .children + .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)); + self.regenerate_index() } } -impl Tree { +impl TreeView { pub fn render( &mut self, area: Rect, @@ -537,76 +628,178 @@ impl Tree { self.max_len = 0; self.winline = std::cmp::min(self.winline, 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 last_item_index = self.tree.len().saturating_sub(1); let skip = self.selected.saturating_sub(self.winline); - let iter = self - .items + + let params = RenderElemParams { + tree: &self.tree, + prefix: &"".to_string(), + is_last: true, + level: 0, + selected: self.selected, + }; + + let rendered = render_tree(params); + + let iter = rendered .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)) - } + + struct Indent(String); + struct Node { + name: String, + selected: bool, + } + + struct RenderElemParams<'a, T> { + tree: &'a Tree, + prefix: &'a String, + is_last: bool, + level: u16, + selected: usize, + } + + cx.editor.set_error(format!("seleted = {}", self.selected)); + + fn render_tree( + RenderElemParams { + tree, + prefix, + is_last, + level, + selected, + }: RenderElemParams, + ) -> Vec<(Indent, Node)> { + let indent = if level > 0 { + let bar = if is_last { "└" } else { "├" }; + let branch = if tree.is_opened { "┬" } else { "─" }; + format!("{}{}{}", prefix, bar, branch) } else { "".to_string() }; + let folded_length = tree.children.len(); + let head = ( + Indent(indent), + Node { + selected: selected == tree.index, + name: format!( + "{}{}", + tree.item.text_string(), + if tree.item.is_parent() { + format!("{}", std::path::MAIN_SEPARATOR) + } else { + "".to_string() + } + ), + }, + ); + let prefix = format!("{}{}", prefix, if is_last { " " } else { "│" }); + vec![head] + .into_iter() + .chain( + tree.children + .iter() + .enumerate() + .flat_map(|(local_index, elem)| { + let is_last = local_index == folded_length - 1; + render_tree(RenderElemParams { + tree: elem, + prefix: &prefix, + is_last, + level: level + 1, + selected, + }) + }), + ) + .collect() + } - 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); - } + for (index, (indent, node)) in iter { + let area = Rect::new(area.x, area.y + index as u16, area.width, 1); + let indent_len = indent.0.chars().count() as u16; + surface.set_stringn(area.x, area.y, indent.0.clone(), indent_len as usize, style); + + let style = if node.selected { + style.add_modifier(Modifier::REVERSED) + } else { + style }; - 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 - } - } - } + surface.set_stringn( + area.x.saturating_add(indent_len).saturating_add(1), + area.y, + node.name.clone(), + area.width + .saturating_sub(indent_len) + .saturating_sub(1) + .into(), + style, + ); } + // let mut text = elem.item.text(cx, skip + index == self.selected, params); + // 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( @@ -629,15 +822,12 @@ impl Tree { 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!('z') => self.fold_current_child(), + key!('h') => self.go_to_parent(), + key!('l') => self.go_to_children(cx), key!(Enter) => self.on_enter(cx, params, self.selected), 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), @@ -652,97 +842,224 @@ impl Tree { } } -impl Tree { +impl TreeView { 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; - } - } + todo!() + // 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(s) { + // retain.push(child.clone()); + // } + // } + // if !retain.is_empty() || elem.item.filter(s) { + // retain.insert(0, elem.clone()); + // } + // (retain, index) + // } + + // if s.is_empty() { + // if let Some((_, recycle)) = self.recycle.take() { + // // self.tree = 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.tree, + // }; + // 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(s) { + // retain.push(elem.clone()) + // } + // index += 1; + // } + + // if retain.is_empty() { + // if let Some((_, recycle)) = self.recycle.take() { + // self.tree = recycle; + // self.restore_view(); + // } + // return; + // } + + // let recycle = std::mem::replace(&mut self.tree, 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(s)).unwrap_or(0); + // self.winline = self.selected; + } - 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; - } + pub fn clean_recycle(&mut self) { + self.recycle = None; + } - if retain.is_empty() { - if let Some((_, recycle)) = self.recycle.take() { - self.items = recycle; - self.restore_view(); - } - return; - } + pub fn restore_recycle(&mut self) { + todo!(); + // if let Some((_, recycle)) = self.recycle.take() { + // self.tree = recycle; + // } + } +} - 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(); - } +/// Recalculate the index of each item of a tree. +/// +/// For example: +/// +/// ``` +/// foo (0) +/// bar (1) +/// spam (2) +/// jar (3) +/// yo (4) +/// ``` +fn index_elems(start_index: usize, elems: Vec>) -> Vec> { + fn index_elems<'a, T>( + current_index: usize, + elems: Vec>, + parent_index: Option, + ) -> (usize, Vec>) { + elems + .into_iter() + .fold((current_index, vec![]), |(current_index, trees), elem| { + let index = current_index; + let item = elem.item; + let (current_index, folded) = + index_elems(current_index + 1, elem.children, Some(index)); + let tree = Tree { + item, + children: folded, + index, + is_opened: elem.is_opened, + parent_index, + }; + ( + current_index, + trees.into_iter().chain(vec![tree].into_iter()).collect(), + ) + }) + } + index_elems(start_index, elems, None).1 +} - self.selected = self - .find(0, false, |elem| elem.item.filter(cx, s, params)) - .unwrap_or(0); - self.winline = self.selected; +#[cfg(test)] +mod test_tree { + use super::{index_elems, Tree}; + + #[test] + fn test_indexs_elems_1() { + let result = index_elems( + 0, + vec![ + Tree::new("foo", vec![Tree::new("bar", vec![])]), + Tree::new( + "spam", + vec![Tree::new("jar", vec![Tree::new("yo", vec![])])], + ), + ], + ); + assert_eq!( + result, + vec![ + Tree { + item: "foo", + is_opened: false, + index: 0, + parent_index: None, + children: vec![Tree { + item: "bar", + is_opened: false, + index: 1, + parent_index: Some(0), + children: vec![] + }] + }, + Tree { + item: "spam", + is_opened: false, + index: 2, + parent_index: None, + children: vec![Tree { + item: "jar", + is_opened: false, + index: 3, + parent_index: Some(2), + children: vec![Tree { + item: "yo", + is_opened: false, + index: 4, + children: vec![], + parent_index: Some(3) + }] + }] + } + ] + ) } - pub fn clean_recycle(&mut self) { - self.recycle = None; + #[test] + fn test_iter_1() { + let tree = Tree::new( + "spam", + vec![ + Tree::new("jar", vec![Tree::new("yo", vec![])]), + Tree::new("foo", vec![Tree::new("bar", vec![])]), + ], + ); + + let mut iter = tree.iter(); + assert_eq!(iter.next().map(|tree| tree.item), Some("spam")); + assert_eq!(iter.next().map(|tree| tree.item), Some("jar")); + assert_eq!(iter.next().map(|tree| tree.item), Some("yo")); + assert_eq!(iter.next().map(|tree| tree.item), Some("foo")); + assert_eq!(iter.next().map(|tree| tree.item), Some("bar")); } - pub fn restore_recycle(&mut self) { - if let Some((_, recycle)) = self.recycle.take() { - self.items = recycle; - } + #[test] + fn test_len_1() { + let tree = Tree::new( + "spam", + vec![ + Tree::new("jar", vec![Tree::new("yo", vec![])]), + Tree::new("foo", vec![Tree::new("bar", vec![])]), + ], + ); + + assert_eq!(tree.len(), 5) } } -- 2.38.5 From aa397ef8013a73b1deb567bf4d41ca9f72714bcb Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 12 Feb 2023 18:44:52 +0800 Subject: [PATCH 005/191] feat(explore): reveal current file --- helix-term/src/commands.rs | 8 +- helix-term/src/keymap/default.rs | 2 +- helix-term/src/ui/explore.rs | 54 +++++++---- helix-term/src/ui/tree.rs | 158 ++++++++++++++++++------------- 4 files changed, 131 insertions(+), 91 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c7768094..ae725291 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -434,7 +434,7 @@ impl MappableCommand { replay_macro, "Replay macro", command_palette, "Open command pallete", toggle_or_focus_explorer, "Toggle or focus explorer", - focus_current_file, "Focus current file in explorer", + reveal_current_file, "Reveal current file in explorer", close_explorer, "close explorer", ); } @@ -2232,16 +2232,16 @@ fn toggle_or_focus_explorer(cx: &mut Context) { )); } -fn focus_current_file(cx: &mut Context) { +fn reveal_current_file(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_current_file(cx), + Some(explore) => explore.content.reveal_current_file(cx), None => match ui::Explorer::new(cx) { Ok(explore) => { let mut explorer = overlayed(explore); - explorer.content.focus_current_file(cx); + explorer.content.reveal_current_file(cx); editor.explorer = Some(explorer); } Err(err) => cx.editor.set_error(format!("{}", err)), diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 167ef510..cc00e30d 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -265,7 +265,7 @@ pub fn default() -> HashMap { "h" => select_references_to_symbol_under_cursor, "?" => command_palette, "e" => toggle_or_focus_explorer, - "E" => focus_current_file, + "E" => reveal_current_file, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index e1d8e06c..2e852e6e 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -277,13 +277,31 @@ impl Explorer { }) } - pub fn focus_current_file(&mut self, cx: &mut Context) { + pub fn reveal_current_file(&mut self, cx: &mut Context) { let current_document_path = doc!(cx.editor).path().cloned(); match current_document_path { None => cx.editor.set_error("No opened document."), - Some(path) => { - self.tree.focus_path(cx, path, &self.state.current_root); - self.focus(); + Some(current_path) => { + let current_root = &self.state.current_root; + let current_path = current_path.as_path().to_string_lossy().to_string(); + let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; + let segments = current_path + .strip_prefix(current_root.as_str()) + .expect( + format!( + "Failed to strip prefix '{}' from '{}'", + current_root, current_path + ) + .as_str(), + ) + .split(std::path::MAIN_SEPARATOR) + .collect::>(); + match self.tree.reveal_item(segments) { + Ok(_) => { + self.focus(); + } + Err(error) => cx.editor.set_error(error), + } } } } @@ -798,20 +816,20 @@ impl Component for Explorer { 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(); - let root = FileInfo::root(self.state.current_root.clone()); - let children = root.get_children().expect("TODO: handle error"); - self.tree = TreeView::build_tree(root, children) - .with_enter_fn(Self::toggle_current); - } - Err(e) => cx.editor.set_error(format!("{e}")), - } - } - } + // 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(); + // let root = FileInfo::root(self.state.current_root.clone()); + // let children = root.get_children().expect("TODO: handle error"); + // self.tree = TreeView::build_tree(root, children) + // .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), diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 362ea43f..4fad6dcd 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1,5 +1,3 @@ -use std::iter::Peekable; -use std::slice::Iter; use std::{cmp::Ordering, path::PathBuf}; use anyhow::Result; @@ -210,9 +208,16 @@ impl Tree { pub struct TreeView { tree: Tree, recycle: Option<(String, Vec>)>, - selected: usize, // select item index - save_view: (usize, usize), // (selected, row) - winline: usize, // view row + /// Selected item idex + selected: usize, + + /// (selected, row) + save_view: (usize, usize), + + /// View row + winline: usize, + + area_height: usize, col: usize, max_len: usize, count: usize, @@ -239,6 +244,7 @@ impl TreeView { col: 0, max_len: 0, count: 0, + area_height: 0, tree_symbol_style: "ui.text".into(), pre_render: None, on_opened_fn: None, @@ -292,66 +298,73 @@ impl TreeView { } } - /// TODO: current_path should not be PathBuf, but Vec so that Tree can be generic - pub fn focus_path(&mut self, cx: &mut Context, current_path: PathBuf, current_root: &PathBuf) { - let current_path = current_path.as_path().to_string_lossy().to_string(); - let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; - let nodes = current_path - .strip_prefix(current_root.as_str()) - .expect( - format!( - "Failed to strip prefix '{}' from '{}'", - current_root, current_path - ) - .as_str(), - ) - .split(std::path::MAIN_SEPARATOR) - .enumerate() - .collect::>(); - - let len = nodes.len(); - - // `preivous_item_index` is necessary to avoid choosing the first file - // that is not the current file. - // For example, consider a project that contains multiple `Cargo.toml`. - // Without `previous_item_index`, the first `Cargo.toml` will always be chosen, - // regardless of which `Cargo.toml` the user wishes to find in the explorer. - // let mut previous_item_index = 0; - // for (index, node) in nodes { - // let current_level = index + 1; - // let is_last = index == len - 1; - // match self - // .items - // .iter() - // .enumerate() - // .position(|(item_index, item)| { - // item_index >= previous_item_index - // && item.item.text_string().eq(node) - // && item.level == current_level - // }) { - // Some(index) => { - // if is_last { - // self.selected = index - // } else { - // let item = &self.items[index]; - // let items = match item.item.get_childs() { - // Ok(items) => items, - // Err(e) => return cx.editor.set_error(format!("{e}")), - // }; - // let inserts = vec_to_tree(items, current_level + 1); - // previous_item_index = index; - // let _: Vec<_> = self.items.splice(index + 1..index + 1, inserts).collect(); - // } - // } - // None => cx.editor.set_error(format!( - // "The following file does not exist anymore: '{}'. node = {}", - // current_path, node - // )), - // } - // } + /// Reveal item in the tree based on the given `segments`. + /// + /// The name of the root should be excluded. + /// + /// Example `segments`: + /// ``` + /// vec!["helix-term", "src", "ui", "tree.rs"] + /// ``` + pub fn reveal_item(&mut self, segments: Vec<&str>) -> Result<(), String> { + // Expand the tree + segments.iter().fold( + Ok(&mut self.tree), + |current_tree, segment| match current_tree { + Err(err) => Err(err), + Ok(current_tree) => { + match current_tree + .children + .iter_mut() + .find(|tree| tree.item.text_string().eq(segment)) + { + Some(tree) => { + if !tree.is_opened { + tree.children = vec_to_tree( + tree.item.get_children().map_err(|err| err.to_string())?, + ); + if !tree.children.is_empty() { + tree.is_opened = true; + } + } + Ok(tree) + } + None => Err(format!( + "Unable to find path: '{}'. current_segment = {}", + segments.join("/"), + segment + )), + } + } + }, + )?; - // Center the selection - self.winline = self.max_len / 2; + // Locate the item + self.regenerate_index(); + self.selected = segments + .iter() + .fold(&self.tree, |tree, segment| { + tree.children + .iter() + .find(|tree| tree.item.text_string().eq(segment)) + .expect("Should be unreachable") + }) + .index; + + self.align_view_center(); + Ok(()) + } + + fn align_view_center(&mut self) { + self.winline = self.area_height / 2 + } + + fn align_view_top(&mut self) { + self.winline = 0 + } + + fn align_view_bottom(&mut self) { + self.winline = self.area_height } fn regenerate_index(&mut self) { @@ -626,11 +639,14 @@ impl TreeView { } self.max_len = 0; - self.winline = std::cmp::min(self.winline, area.height.saturating_sub(1) as usize); + self.area_height = area.height.saturating_sub(1) as usize; + self.winline = std::cmp::min(self.winline, self.area_height); let style = cx.editor.theme.get(&self.tree_symbol_style); let last_item_index = self.tree.len().saturating_sub(1); let skip = self.selected.saturating_sub(self.winline); + cx.editor.set_error(format!("winline = {}", self.winline)); + let params = RenderElemParams { tree: &self.tree, prefix: &"".to_string(), @@ -661,8 +677,6 @@ impl TreeView { selected: usize, } - cx.editor.set_error(format!("seleted = {}", self.selected)); - fn render_tree( RenderElemParams { tree, @@ -822,7 +836,15 @@ impl TreeView { 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_child(), + key!('z') => { + self.on_next_key = Some(Box::new(|_, tree, event| match event.into() { + key!('f') => tree.fold_current_child(), + key!('z') => tree.align_view_center(), + key!('t') => tree.align_view_top(), + key!('b') => tree.align_view_bottom(), + _ => {} + })); + } key!('h') => self.go_to_parent(), key!('l') => self.go_to_children(cx), key!(Enter) => self.on_enter(cx, params, self.selected), -- 2.38.5 From bdab93e856148f2624175fdb0c697fd23e197ec9 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 12 Feb 2023 19:59:17 +0800 Subject: [PATCH 006/191] feat(explore): search --- helix-term/src/ui/explore.rs | 61 ++++++++++++++++-------------------- helix-term/src/ui/tree.rs | 48 +++++++++++++++++----------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 2e852e6e..462a0aa8 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -78,30 +78,30 @@ impl FileInfo { 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 style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text")); - let style = if !selected { - style - } else { - let patch = match state.focus { - true => "ui.explorer.focus", - false => "ui.explorer.unfocus", - }; - if let Some(patch) = theme.try_get(patch) { - style.patch(patch) - } else { - style.add_modifier(Modifier::REVERSED) - } - }; - Spans::from(Span::styled(text, style)) - } + // 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 style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text")); + // let style = if !selected { + // style + // } else { + // let patch = match state.focus { + // true => "ui.explorer.focus", + // false => "ui.explorer.unfocus", + // }; + // if let Some(patch) = theme.try_get(patch) { + // style.patch(patch) + // } else { + // 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 { @@ -175,14 +175,6 @@ impl TreeItem for FileInfo { Ok(ret) } - fn filter(&self, s: &str) -> bool { - if s.is_empty() { - false - } else { - self.get_text().contains(s) - } - } - fn text_string(&self) -> String { self.get_text().to_string() } @@ -667,7 +659,7 @@ impl Explorer { } else { explorer .tree - .search_pre(cx, &search_str, &mut explorer.state); + .search_previous(cx, &search_str, &mut explorer.state); } } })) @@ -684,7 +676,8 @@ impl Explorer { 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.tree + .search_previous(cx, prompt.line(), &mut self.state); } } self.prompt = Some((action, prompt)); diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 4fad6dcd..c9a97b84 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -17,14 +17,16 @@ 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 text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; fn text_string(&self) -> String; fn is_child(&self, other: &Self) -> bool; fn is_parent(&self) -> bool; fn cmp(&self, other: &Self) -> Ordering; fn filter(&self, s: &str) -> bool { - self.text_string().contains(s) + self.text_string() + .to_lowercase() + .contains(&s.to_lowercase()) } fn get_children(&self) -> Result> { @@ -124,8 +126,10 @@ impl Clone for Tree { } } +#[derive(Clone)] struct TreeIter<'a, T> { - current_index: usize, + current_index_forward: usize, + current_index_reverse: isize, tree: &'a Tree, } @@ -133,32 +137,40 @@ impl<'a, T> Iterator for TreeIter<'a, T> { type Item = &'a Tree; fn next(&mut self) -> Option { - let index = self.current_index; - self.current_index += 1; + let index = self.current_index_forward; + if index > self.tree.len().saturating_sub(1) { + None + } else { + self.current_index_forward = self.current_index_forward.saturating_add(1); + self.tree.find_by_index(index) + } + } - self.tree.find_by_index(index) + fn size_hint(&self) -> (usize, Option) { + (self.tree.len(), Some(self.tree.len())) } } impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { fn next_back(&mut self) -> Option { - let index = self.current_index; - self.current_index -= 1; - self.tree.find_by_index(index) + let index = self.current_index_reverse; + if index < 0 { + None + } else { + self.current_index_reverse = self.current_index_reverse.saturating_sub(1); + self.tree.find_by_index(index as usize) + } } } -impl<'a, T> ExactSizeIterator for TreeIter<'a, T> { - fn len(&self) -> usize { - self.tree.len() - } -} +impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} impl Tree { fn iter(&self) -> TreeIter { TreeIter { tree: self, - current_index: 0, + current_index_forward: 0, + current_index_reverse: (self.len() - 1) as isize, } } pub fn new(item: T, children: Vec>) -> Self { @@ -294,7 +306,7 @@ impl TreeView { if reverse { iter.take(start).rposition(f) } else { - iter.skip(start).position(f).map(|p| p + start) + iter.skip(start).position(f).map(|index| index + start) } } @@ -490,7 +502,7 @@ impl TreeView { self.winline = (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) { + pub fn search_previous(&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(s)) @@ -645,8 +657,6 @@ impl TreeView { let last_item_index = self.tree.len().saturating_sub(1); let skip = self.selected.saturating_sub(self.winline); - cx.editor.set_error(format!("winline = {}", self.winline)); - let params = RenderElemParams { tree: &self.tree, prefix: &"".to_string(), -- 2.38.5 From 82fe4a309daec0e6fce55e164bf6c7e871e7487c Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 12 Feb 2023 20:24:50 +0800 Subject: [PATCH 007/191] test(ui/tree): find --- helix-term/src/ui/tree.rs | 196 +++++++++++++++++++++++++++++--------- 1 file changed, 149 insertions(+), 47 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index c9a97b84..8144c734 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -7,7 +7,7 @@ use crate::{ compositor::{Context, EventResult}, ctrl, key, shift, }; -use helix_core::unicode::width::UnicodeWidthStr; +use helix_core::{movement::Direction, unicode::width::UnicodeWidthStr}; use helix_view::{ graphics::Rect, input::{Event, KeyEvent}, @@ -142,7 +142,7 @@ impl<'a, T> Iterator for TreeIter<'a, T> { None } else { self.current_index_forward = self.current_index_forward.saturating_add(1); - self.tree.find_by_index(index) + self.tree.get(index) } } @@ -158,7 +158,7 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { None } else { self.current_index_reverse = self.current_index_reverse.saturating_sub(1); - self.tree.find_by_index(index as usize) + self.tree.get(index as usize) } } } @@ -166,13 +166,6 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} impl Tree { - fn iter(&self) -> TreeIter { - TreeIter { - tree: self, - current_index_forward: 0, - current_index_reverse: (self.len() - 1) as isize, - } - } pub fn new(item: T, children: Vec>) -> Self { Self { item, @@ -182,28 +175,50 @@ impl Tree { is_opened: false, } } + fn iter(&self) -> TreeIter { + TreeIter { + tree: self, + current_index_forward: 0, + current_index_reverse: (self.len() - 1) as isize, + } + } + + /// Find an element in the tree with given `predicate`. + /// `start_index` is inclusive if direction is `Forward`. + /// `start_index` is exclusive if direction is `Backward`. + pub fn find(&self, start_index: usize, direction: Direction, predicate: F) -> Option + where + F: FnMut(&Tree) -> bool, + { + let iter = self.iter(); + match direction { + Direction::Forward => iter + .skip(start_index) + .position(predicate) + .map(|index| index + start_index), + Direction::Backward => iter.take(start_index).rposition(predicate), + } + } pub fn item(&self) -> &T { &self.item } - fn find_by_index(&self, index: usize) -> Option<&Tree> { + fn get(&self, index: usize) -> Option<&Tree> { if self.index == index { Some(self) } else { - self.children - .iter() - .find_map(|elem| elem.find_by_index(index)) + self.children.iter().find_map(|elem| elem.get(index)) } } - fn find_by_index_mut(&mut self, index: usize) -> Option<&mut Tree> { + fn get_mut(&mut self, index: usize) -> Option<&mut Tree> { if self.index == index { Some(self) } else { self.children .iter_mut() - .find_map(|elem| elem.find_by_index_mut(index)) + .find_map(|elem| elem.get_mut(index)) } } @@ -298,18 +313,6 @@ impl TreeView { self } - fn find(&self, start: usize, reverse: bool, f: F) -> Option - where - F: FnMut(&Tree) -> bool, - { - let iter = self.tree.iter(); - if reverse { - iter.take(start).rposition(f) - } else { - iter.skip(start).position(f).map(|index| index + start) - } - } - /// Reveal item in the tree based on the given `segments`. /// /// The name of the root should be excluded. @@ -424,7 +427,7 @@ impl TreeView { // } // } // - let mut selected_item = self.find_by_index_mut(selected_index); + let mut selected_item = self.get_mut(selected_index); if selected_item.is_opened { selected_item.is_opened = false; selected_item.children = vec![]; @@ -434,7 +437,7 @@ impl TreeView { if let Some(mut on_open_fn) = self.on_opened_fn.take() { let mut f = || { - let current = &mut self.find_by_index_mut(selected_index); + let current = &mut self.get_mut(selected_index); match on_open_fn(&mut current.item, cx, params) { TreeOp::Restore => { panic!(); @@ -474,7 +477,7 @@ impl TreeView { self.on_opened_fn = Some(on_open_fn) } else { panic!(); - self.find_by_index_mut(selected_index).children = vec![]; + self.get_mut(selected_index).children = vec![]; // let current = &mut self.items[selected_index]; // let inserts = std::mem::take(&mut current.folded); // let _: Vec<_> = self @@ -496,7 +499,8 @@ impl TreeView { pub fn search_next(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) { let skip = std::cmp::max(2, self.save_view.0 + 1); self.selected = self - .find(skip, false, |e| e.item.filter(s)) + .tree + .find(skip, Direction::Forward, |e| e.item.filter(s)) .unwrap_or(self.save_view.0); self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); @@ -505,7 +509,8 @@ impl TreeView { pub fn search_previous(&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(s)) + .tree + .find(take, Direction::Backward, |e| e.item.filter(s)) .unwrap_or(self.save_view.0); self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); @@ -570,28 +575,25 @@ impl TreeView { (self.selected, self.winline) = self.save_view; } - fn find_by_index(&self, index: usize) -> &Tree { - self.tree - .iter() - .find_map(|item| item.find_by_index(index)) - .unwrap() + fn get(&self, index: usize) -> &Tree { + self.tree.get(index).unwrap() } - fn find_by_index_mut(&mut self, index: usize) -> &mut Tree { - self.tree.find_by_index_mut(index).unwrap() + fn get_mut(&mut self, index: usize) -> &mut Tree { + self.tree.get_mut(index).unwrap() } pub fn current(&self) -> &Tree { - self.find_by_index(self.selected) + self.get(self.selected) } pub fn current_mut(&mut self) -> &mut Tree { - self.find_by_index_mut(self.selected) + self.get_mut(self.selected) } fn current_parent(&self) -> Option<&Tree> { if let Some(parent_index) = self.current().parent_index { - Some(self.find_by_index(parent_index)) + Some(self.get(parent_index)) } else { None } @@ -599,7 +601,7 @@ impl TreeView { fn current_parent_mut(&mut self) -> Option<&mut Tree> { if let Some(parent_index) = self.current().parent_index { - Some(self.find_by_index_mut(parent_index)) + Some(self.get_mut(parent_index)) } else { None } @@ -1011,10 +1013,12 @@ fn index_elems(start_index: usize, elems: Vec>) -> Vec> { #[cfg(test)] mod test_tree { + use helix_core::movement::Direction; + use super::{index_elems, Tree}; #[test] - fn test_indexs_elems_1() { + fn test_indexs_elems() { let result = index_elems( 0, vec![ @@ -1065,7 +1069,7 @@ mod test_tree { } #[test] - fn test_iter_1() { + fn test_iter() { let tree = Tree::new( "spam", vec![ @@ -1083,7 +1087,25 @@ mod test_tree { } #[test] - fn test_len_1() { + fn test_iter_double_ended() { + let tree = Tree::new( + "spam", + vec![ + Tree::new("jar", vec![Tree::new("yo", vec![])]), + Tree::new("foo", vec![Tree::new("bar", vec![])]), + ], + ); + + let mut iter = tree.iter(); + assert_eq!(iter.next_back().map(|tree| tree.item), Some("bar")); + assert_eq!(iter.next_back().map(|tree| tree.item), Some("foo")); + assert_eq!(iter.next_back().map(|tree| tree.item), Some("yo")); + assert_eq!(iter.next_back().map(|tree| tree.item), Some("jar")); + assert_eq!(iter.next_back().map(|tree| tree.item), Some("spam")); + } + + #[test] + fn test_len() { let tree = Tree::new( "spam", vec![ @@ -1094,4 +1116,84 @@ mod test_tree { assert_eq!(tree.len(), 5) } + + #[test] + fn test_find_forward() { + let tree = Tree::new( + ".cargo", + vec![ + Tree::new("jar", vec![Tree::new("Cargo.toml", vec![])]), + Tree::new("Cargo.toml", vec![Tree::new("bar", vec![])]), + ], + ); + let result = tree.find(0, Direction::Forward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(0)); + + let result = tree.find(1, Direction::Forward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(2)); + + let result = tree.find(2, Direction::Forward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(2)); + + let result = tree.find(3, Direction::Forward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(3)); + + let result = tree.find(4, Direction::Forward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, None); + } + + #[test] + fn test_find_backward() { + let tree = Tree::new( + ".cargo", + vec![ + Tree::new("jar", vec![Tree::new("Cargo.toml", vec![])]), + Tree::new("Cargo.toml", vec![Tree::new("bar", vec![])]), + ], + ); + let result = tree.find(0, Direction::Backward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, None); + + let result = tree.find(1, Direction::Backward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(0)); + + let result = tree.find(2, Direction::Backward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(0)); + + let result = tree.find(3, Direction::Backward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(2)); + + let result = tree.find(4, Direction::Backward, |tree| { + tree.item.to_lowercase().contains(&"cargo".to_lowercase()) + }); + + assert_eq!(result, Some(3)); + } } -- 2.38.5 From 0f8b641a5d67027cfc69aaeeb22e46f7fbd26eaf Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 12 Feb 2023 21:09:21 +0800 Subject: [PATCH 008/191] feat(tree): filter --- helix-term/src/ui/explore.rs | 7 +- helix-term/src/ui/tree.rs | 175 +++++++++++++++++++---------------- 2 files changed, 96 insertions(+), 86 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 462a0aa8..f8faa82d 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -618,10 +618,9 @@ impl Explorer { .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); + if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { + self.tree.filter(prompt.line(), cx, &mut self.state); + } } key!(Esc) | ctrl!('c') => self.tree.restore_recycle(), _ => { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 8144c734..1381762d 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -165,6 +165,32 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} +impl Tree { + pub fn filter

(tree: &Tree, predicate: &P) -> Option> + where + P: Fn(&T) -> bool, + { + let children = tree + .children + .iter() + .filter_map(|tree| Self::filter(tree, predicate)) + .collect::>(); + if predicate(&tree.item) || !children.is_empty() { + let mut tree = Tree { + item: tree.item.clone(), + parent_index: tree.parent_index, + index: tree.index, + is_opened: tree.is_opened, + children, + }; + tree.regenerate_index(); + Some(tree) + } else { + None + } + } +} + impl Tree { pub fn new(item: T, children: Vec>) -> Self { Self { @@ -234,7 +260,7 @@ impl Tree { pub struct TreeView { tree: Tree, - recycle: Option<(String, Vec>)>, + recycle: Option<(String, Tree)>, /// Selected item idex selected: usize, @@ -878,85 +904,31 @@ impl TreeView { impl TreeView { pub fn filter(&mut self, s: &str, cx: &mut Context, params: &mut T::Params) { - todo!() - // 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(s) { - // retain.push(child.clone()); - // } - // } - // if !retain.is_empty() || elem.item.filter(s) { - // retain.insert(0, elem.clone()); - // } - // (retain, index) - // } - - // if s.is_empty() { - // if let Some((_, recycle)) = self.recycle.take() { - // // self.tree = 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.tree, - // }; - // 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(s) { - // retain.push(elem.clone()) - // } - // index += 1; - // } - - // if retain.is_empty() { - // if let Some((_, recycle)) = self.recycle.take() { - // self.tree = recycle; - // self.restore_view(); - // } - // return; - // } + if s.is_empty() { + self.restore_recycle(); + return; + } - // let recycle = std::mem::replace(&mut self.tree, retain); - // if let Some(r) = self.recycle.as_mut() { - // r.0 = s.into() - // } else { - // self.recycle = Some((s.into(), recycle)); - // self.save_view(); - // } + let new_tree = Tree::filter(&self.tree, &|item: &T| { + item.text_string() + .to_lowercase() + .contains(&s.to_lowercase()) + }) + .unwrap_or_else(|| Tree { + item: self.tree.item.clone(), + children: vec![], + ..self.tree.clone() + }); + let recycle = std::mem::replace(&mut self.tree, new_tree); + 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(s)).unwrap_or(0); - // self.winline = self.selected; + self.selected = 0; + self.winline = 0 } pub fn clean_recycle(&mut self) { @@ -964,10 +936,10 @@ impl TreeView { } pub fn restore_recycle(&mut self) { - todo!(); - // if let Some((_, recycle)) = self.recycle.take() { - // self.tree = recycle; - // } + if let Some((_, recycle)) = self.recycle.take() { + self.tree = recycle; + } + self.restore_view(); } } @@ -1196,4 +1168,43 @@ mod test_tree { assert_eq!(result, Some(3)); } + + #[test] + fn test_filter() { + let tree = Tree::new( + ".cargo", + vec![ + Tree::new("spam", vec![Tree::new("Cargo.toml", vec![])]), + Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), + Tree::new("hello", vec![]), + ], + ); + + let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("cargo")); + assert_eq!( + result, + Some(Tree::new( + ".cargo", + vec![ + Tree::new("spam", vec![Tree::new("Cargo.toml", vec![])]), + Tree::new("Cargo.toml", vec![]), + ], + )) + ); + + let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("pam")); + assert_eq!( + result, + Some(Tree::new( + ".cargo", + vec![ + Tree::new("spam", vec![]), + Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), + ], + )) + ); + + let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("helix")); + assert_eq!(result, None) + } } -- 2.38.5 From 458fa1ca58bb4f3ed7ce4930e5103db3e8f383d6 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 08:46:57 +0800 Subject: [PATCH 009/191] feat(explore): add folder/file --- helix-term/src/ui/explore.rs | 131 +++++++++++++++++++++-------------- helix-term/src/ui/tree.rs | 51 +++++++++----- 2 files changed, 112 insertions(+), 70 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index f8faa82d..5e80137d 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -175,7 +175,7 @@ impl TreeItem for FileInfo { Ok(ret) } - fn text_string(&self) -> String { + fn name(&self) -> String { self.get_text().to_string() } @@ -220,11 +220,11 @@ impl TreeItem for FileInfo { // } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] enum PromptAction { Search(bool), // search next/search pre - Mkdir, - CreateFile, + CreateFolder { folder_path: PathBuf }, + CreateFile { folder_path: PathBuf }, RemoveDir, RemoveFile, Filter, @@ -258,10 +258,8 @@ pub struct Explorer { impl Explorer { pub fn new(cx: &mut Context) -> Result { let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); - let root = FileInfo::root(current_root.clone()); - let children = root.get_children()?; Ok(Self { - tree: TreeView::build_tree(root, children).with_enter_fn(Self::toggle_current), + tree: Self::new_tree(current_root.clone())?, state: State::new(true, current_root), repeat_motion: None, prompt: None, @@ -269,6 +267,22 @@ impl Explorer { }) } + fn new_tree(root: PathBuf) -> Result> { + let root = FileInfo::root(root.clone()); + let children = root.get_children()?; + Ok(TreeView::build_tree(root, children).with_enter_fn(Self::toggle_current)) + } + + fn change_root(&mut self, cx: &mut Context, root: PathBuf) { + match Self::new_tree(root.clone()) { + Ok(tree) => { + self.state.current_root = root; + self.tree = tree; + } + Err(e) => cx.editor.set_error(format!("{e}")), + } + } + pub fn reveal_current_file(&mut self, cx: &mut Context) { let current_document_path = doc!(cx.editor).path().cloned(); match current_document_path { @@ -381,23 +395,50 @@ impl Explorer { )) } - fn new_mkdir_prompt(&mut self) { + fn new_create_folder_prompt(&mut self) -> Result<()> { + let folder_path = self.current_parent_folder_path()?; self.prompt = Some(( - PromptAction::Mkdir, - Prompt::new("mkdir: ".into(), None, ui::completers::none, |_, _, _| {}), + PromptAction::CreateFolder { + folder_path: folder_path.clone(), + }, + Prompt::new( + format!(" New folder: {}/", folder_path.to_string_lossy()).into(), + None, + ui::completers::none, + |_, _, _| {}, + ), )); + Ok(()) } - fn new_create_file_prompt(&mut self) { + fn new_create_file_prompt(&mut self) -> Result<()> { + let folder_path = self.current_parent_folder_path()?; self.prompt = Some(( - PromptAction::CreateFile, + PromptAction::CreateFile { + folder_path: folder_path.clone(), + }, Prompt::new( - "create file: ".into(), + format!(" New file: {}/", folder_path.to_string_lossy()).into(), None, ui::completers::none, |_, _, _| {}, ), )); + Ok(()) + } + + fn current_parent_folder_path(&self) -> Result { + let current_item = self.tree.current_item(); + Ok(current_item + .path + .parent() + .ok_or_else(|| { + anyhow::anyhow!(format!( + "Unable to get parent directory of '{}'", + current_item.path.to_string_lossy() + )) + })? + .to_path_buf()) } fn new_remove_file_prompt(&mut self, cx: &mut Context) { @@ -696,14 +737,14 @@ impl Explorer { _ => 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) { + match (&action, event.into()) { + (PromptAction::CreateFolder { folder_path }, key!(Enter)) => { + if let Err(e) = self.new_path(folder_path.clone(), line, true) { cx.editor.set_error(format!("{e}")) } } - (PromptAction::CreateFile, key!(Enter)) => { - if let Err(e) = self.new_path(line, false) { + (PromptAction::CreateFile { folder_path }, key!(Enter)) => { + if let Err(e) = self.new_path(folder_path.clone(), line, false) { cx.editor.set_error(format!("{e}")) } } @@ -735,23 +776,15 @@ impl Explorer { EventResult::Consumed(None) } - fn new_path(&mut self, file_name: &str, is_dir: bool) -> Result<()> { + fn new_path(&mut self, current_parent: PathBuf, 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 { + let file = if is_dir { std::fs::create_dir(&p)?; FileInfo::new(p, FileType::Dir) } else { @@ -760,9 +793,9 @@ impl Explorer { FileInfo::new(p, FileType::File) }; if current.file_type == FileType::Placeholder { - self.tree.replace_current(f); + self.tree.replace_current(file); } else { - self.tree.insert_current_level(f); + self.tree.add_sibling_to_current_item(file)?; } Ok(()) } @@ -808,33 +841,25 @@ impl Component for Explorer { 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(); - // let root = FileInfo::root(self.state.current_root.clone()); - // let children = root.get_children().expect("TODO: handle error"); - // self.tree = TreeView::build_tree(root, children) - // .with_enter_fn(Self::toggle_current); - // } - // Err(e) => cx.editor.set_error(format!("{e}")), - // } - // } - // } + key!(Backspace) => { + if let Some(parent) = self.state.current_root.parent().clone() { + self.change_root(cx, parent.to_path_buf()) + } + } 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!('a') => { + if let Err(error) = self.new_create_file_prompt() { + cx.editor.set_error(error.to_string()) + } + } + shift!('A') => { + if let Err(error) = self.new_create_folder_prompt() { + cx.editor.set_error(error.to_string()) + } } + key!('o') => self.change_root(cx, self.tree.current_item().path.clone()), key!('r') => { self.on_next_key = Some(Box::new(|cx, explorer, event| { match event.into() { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 1381762d..cbc97a0b 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -18,15 +18,13 @@ pub trait TreeItem: Sized { type Params; // fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; - fn text_string(&self) -> String; + fn name(&self) -> String; fn is_child(&self, other: &Self) -> bool; fn is_parent(&self) -> bool; fn cmp(&self, other: &Self) -> Ordering; fn filter(&self, s: &str) -> bool { - self.text_string() - .to_lowercase() - .contains(&s.to_lowercase()) + self.name().to_lowercase().contains(&s.to_lowercase()) } fn get_children(&self) -> Result> { @@ -357,7 +355,7 @@ impl TreeView { match current_tree .children .iter_mut() - .find(|tree| tree.item.text_string().eq(segment)) + .find(|tree| tree.item.name().eq(segment)) { Some(tree) => { if !tree.is_opened { @@ -387,7 +385,7 @@ impl TreeView { .fold(&self.tree, |tree, segment| { tree.children .iter() - .find(|tree| tree.item.text_string().eq(segment)) + .find(|tree| tree.item.name().eq(segment)) .expect("Should be unreachable") }) .index; @@ -656,13 +654,34 @@ impl TreeView { self.selected = selected } - pub fn insert_current_level(&mut self, item: T) { - let current = self.current_mut(); - current.children.push(Tree::new(item, vec![])); - current - .children - .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)); - self.regenerate_index() + pub fn add_sibling_to_current_item(&mut self, item: T) -> Result<()> { + let current = self.current(); + match current.parent_index { + None => Err(anyhow::anyhow!(format!( + "Current item = '{}' has no parent", + current.item.name() + ))), + Some(parent_index) => { + let parent = self.get_mut(parent_index); + let item_name = item.name(); + parent.children.push(Tree::new(item, vec![])); + parent + .children + .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)); + self.regenerate_index(); + let parent = self.get_mut(parent_index); + + // Focus the added sibling + if let Some(tree) = parent + .children + .iter() + .find(|tree| tree.item.name().eq(&item_name)) + { + self.selected = tree.index + }; + Ok(()) + } + } } } @@ -738,7 +757,7 @@ impl TreeView { selected: selected == tree.index, name: format!( "{}{}", - tree.item.text_string(), + tree.item.name(), if tree.item.is_parent() { format!("{}", std::path::MAIN_SEPARATOR) } else { @@ -910,9 +929,7 @@ impl TreeView { } let new_tree = Tree::filter(&self.tree, &|item: &T| { - item.text_string() - .to_lowercase() - .contains(&s.to_lowercase()) + item.name().to_lowercase().contains(&s.to_lowercase()) }) .unwrap_or_else(|| Tree { item: self.tree.item.clone(), -- 2.38.5 From 2af8b410074e8b39e41dcee63b72d87351353c83 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 10:44:27 +0800 Subject: [PATCH 010/191] feat(explore): remove files/folder --- helix-term/src/ui/explore.rs | 119 ++++++++++++------ helix-term/src/ui/tree.rs | 238 ++++++++++++++++++----------------- 2 files changed, 203 insertions(+), 154 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 5e80137d..a04a605f 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -223,8 +223,14 @@ impl TreeItem for FileInfo { #[derive(Clone, Debug)] enum PromptAction { Search(bool), // search next/search pre - CreateFolder { folder_path: PathBuf }, - CreateFile { folder_path: PathBuf }, + CreateFolder { + folder_path: PathBuf, + parent_index: usize, + }, + CreateFile { + folder_path: PathBuf, + parent_index: usize, + }, RemoveDir, RemoveFile, Filter, @@ -306,7 +312,7 @@ impl Explorer { Ok(_) => { self.focus(); } - Err(error) => cx.editor.set_error(error), + Err(error) => cx.editor.set_error(error.to_string()), } } } @@ -396,9 +402,10 @@ impl Explorer { } fn new_create_folder_prompt(&mut self) -> Result<()> { - let folder_path = self.current_parent_folder_path()?; + let (parent_index, folder_path) = self.nearest_folder()?; self.prompt = Some(( PromptAction::CreateFolder { + parent_index, folder_path: folder_path.clone(), }, Prompt::new( @@ -412,9 +419,10 @@ impl Explorer { } fn new_create_file_prompt(&mut self) -> Result<()> { - let folder_path = self.current_parent_folder_path()?; + let (parent_index, folder_path) = self.nearest_folder()?; self.prompt = Some(( PromptAction::CreateFile { + parent_index, folder_path: folder_path.clone(), }, Prompt::new( @@ -427,18 +435,36 @@ impl Explorer { Ok(()) } - fn current_parent_folder_path(&self) -> Result { - let current_item = self.tree.current_item(); - Ok(current_item - .path - .parent() - .ok_or_else(|| { + fn nearest_folder(&self) -> Result<(usize, PathBuf)> { + let current = self.tree.current(); + if current.item().is_parent() { + Ok((current.index(), current.item().path.to_path_buf())) + } else { + let parent_index = current.parent_index().ok_or_else(|| { + anyhow::anyhow!(format!( + "Unable to get parent index of '{}'", + current.item().path.to_string_lossy() + )) + })?; + let parent_path = current.item().path.parent().ok_or_else(|| { anyhow::anyhow!(format!( - "Unable to get parent directory of '{}'", - current_item.path.to_string_lossy() + "Unable to get parent path of '{}'", + current.item().path.to_string_lossy() )) - })? - .to_path_buf()) + })?; + Ok((parent_index, parent_path.to_path_buf())) + } + } + + fn new_remove_prompt(&mut self, cx: &mut Context) { + let item = self.tree.current().item(); + match item.file_type { + FileType::Dir => self.new_remove_dir_prompt(cx), + FileType::Exe | FileType::File => self.new_remove_file_prompt(cx), + FileType::Placeholder => cx.editor.set_error("Placeholder is not removable."), + FileType::Parent => cx.editor.set_error("Parent is not removable."), + FileType::Root => cx.editor.set_error("Root is not removable"), + } } fn new_remove_file_prompt(&mut self, cx: &mut Context) { @@ -458,7 +484,7 @@ impl Explorer { cx.editor.set_error(format!("{e}")); return; } - let p = format!("remove file: {}, YES? ", item.path.display()); + let p = format!(" Delete file: '{}'? y/n: ", item.path.display()); self.prompt = Some(( PromptAction::RemoveFile, Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), @@ -486,7 +512,7 @@ impl Explorer { cx.editor.set_error(format!("{e}")); return; } - let p = format!("remove dir: {}, YES? ", item.path.display()); + let p = format!(" Delete folder: '{}'? y/n: ", item.path.display()); self.prompt = Some(( PromptAction::RemoveDir, Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), @@ -738,27 +764,41 @@ impl Explorer { }; let line = prompt.line(); match (&action, event.into()) { - (PromptAction::CreateFolder { folder_path }, key!(Enter)) => { - if let Err(e) = self.new_path(folder_path.clone(), line, true) { + ( + PromptAction::CreateFolder { + folder_path, + parent_index, + }, + key!(Enter), + ) => { + if let Err(e) = self.new_path(folder_path.clone(), line, true, *parent_index) { cx.editor.set_error(format!("{e}")) } } - (PromptAction::CreateFile { folder_path }, key!(Enter)) => { - if let Err(e) = self.new_path(folder_path.clone(), line, false) { + ( + PromptAction::CreateFile { + folder_path, + parent_index, + }, + key!(Enter), + ) => { + if let Err(e) = self.new_path(folder_path.clone(), line, false, *parent_index) { 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(); + if line == "y" { + 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" { + if line == "y" { let item = self.tree.current_item(); if let Err(e) = std::fs::remove_file(&item.path) { cx.editor.set_error(format!("{e}")); @@ -776,7 +816,13 @@ impl Explorer { EventResult::Consumed(None) } - fn new_path(&mut self, current_parent: PathBuf, file_name: &str, is_dir: bool) -> Result<()> { + fn new_path( + &mut self, + current_parent: PathBuf, + file_name: &str, + is_dir: bool, + parent_index: usize, + ) -> Result<()> { let current = self.tree.current_item(); let p = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); match p.parent() { @@ -795,7 +841,7 @@ impl Explorer { if current.file_type == FileType::Placeholder { self.tree.replace_current(file); } else { - self.tree.add_sibling_to_current_item(file)?; + self.tree.add_child(parent_index, file)?; } Ok(()) } @@ -841,11 +887,6 @@ impl Component for Explorer { self.repeat_motion = Some(repeat_motion); } } - key!(Backspace) => { - if let Some(parent) = self.state.current_root.parent().clone() { - self.change_root(cx, parent.to_path_buf()) - } - } key!('f') => self.new_filter_prompt(), key!('/') => self.new_search_prompt(true), key!('?') => self.new_search_prompt(false), @@ -859,7 +900,13 @@ impl Component for Explorer { cx.editor.set_error(error.to_string()) } } - key!('o') => self.change_root(cx, self.tree.current_item().path.clone()), + key!('[') => { + if let Some(parent) = self.state.current_root.parent().clone() { + self.change_root(cx, parent.to_path_buf()) + } + } + key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), + key!('d') => self.new_remove_prompt(cx), key!('r') => { self.on_next_key = Some(Box::new(|cx, explorer, event| { match event.into() { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index cbc97a0b..16251669 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -54,37 +54,6 @@ fn vec_to_tree(mut items: Vec) -> Vec> { ) } -// return total elems's count contain self -fn get_elems_recursion(t: &mut Tree, depth: usize) -> Result { - let mut childs = t.item.get_children()?; - childs.sort_by(tree_item_cmp); - let mut elems = Vec::with_capacity(childs.len()); - // let level = t.level + 1; - let level = todo!(); - - let mut total = 1; - for child in childs { - let mut elem = Tree::new(child, level); - let count = if depth > 0 { - get_elems_recursion(&mut elem, depth - 1)? - } else { - 1 - }; - elems.push(elem); - total += count; - } - t.children = elems; - Ok(total) -} - -fn expand_elems(dist: &mut Vec>, mut t: Tree) { - let childs = std::mem::take(&mut t.children); - dist.push(t); - for child in childs { - expand_elems(dist, child) - } -} - pub enum TreeOp { Noop, Restore, @@ -187,6 +156,24 @@ impl Tree { None } } + + pub fn parent_index(&self) -> Option { + self.parent_index + } + + pub fn index(&self) -> usize { + self.index + } +} + +impl Tree { + fn open(&mut self) -> Result<()> { + self.children = vec_to_tree(self.item.get_children()?); + if !self.children.is_empty() { + self.is_opened = true; + } + Ok(()) + } } impl Tree { @@ -195,7 +182,7 @@ impl Tree { item, index: 0, parent_index: None, - children: index_elems(1, children), + children: index_elems(0, children), is_opened: false, } } @@ -252,7 +239,26 @@ impl Tree { fn regenerate_index(&mut self) { let items = std::mem::take(&mut self.children); - self.children = index_elems(1, items); + self.children = index_elems(0, items); + } + + fn remove(&mut self, index: usize) { + let children = std::mem::replace(&mut self.children, vec![]); + self.children = children + .into_iter() + .filter_map(|tree| { + if tree.index == index { + None + } else { + Some(tree) + } + }) + .map(|mut tree| { + tree.remove(index); + tree + }) + .collect(); + self.regenerate_index() } } @@ -345,7 +351,7 @@ impl TreeView { /// ``` /// vec!["helix-term", "src", "ui", "tree.rs"] /// ``` - pub fn reveal_item(&mut self, segments: Vec<&str>) -> Result<(), String> { + pub fn reveal_item(&mut self, segments: Vec<&str>) -> Result<()> { // Expand the tree segments.iter().fold( Ok(&mut self.tree), @@ -359,20 +365,15 @@ impl TreeView { { Some(tree) => { if !tree.is_opened { - tree.children = vec_to_tree( - tree.item.get_children().map_err(|err| err.to_string())?, - ); - if !tree.children.is_empty() { - tree.is_opened = true; - } + tree.open()?; } Ok(tree) } - None => Err(format!( + None => Err(anyhow::anyhow!(format!( "Unable to find path: '{}'. current_segment = {}", segments.join("/"), segment - )), + ))), } } }, @@ -416,23 +417,19 @@ impl TreeView { } } - fn go_to_children(&mut self, cx: &mut Context) { + fn go_to_children(&mut self, cx: &mut Context) -> Result<()> { let current = self.current_mut(); if current.is_opened { self.selected += 1; - return; - } - let items = match current.item.get_children() { - Ok(items) => items, - Err(e) => return cx.editor.set_error(format!("{e}")), - }; - if items.is_empty() { - return; + Ok(()) + } else { + current.open()?; + if !current.children.is_empty() { + self.selected += 1; + self.regenerate_index(); + } + Ok(()) } - current.is_opened = true; - current.children = vec_to_tree(items); - self.selected += 1; - self.regenerate_index() } } @@ -639,11 +636,8 @@ impl TreeView { self.winline } - pub fn remove_current(&mut self) -> T { - todo!() - // let elem = self.tree.remove(self.selected); - // self.selected = self.selected.saturating_sub(1); - // elem.item + pub fn remove_current(&mut self) { + self.tree.remove(self.selected) } pub fn replace_current(&mut self, item: T) { @@ -654,25 +648,26 @@ impl TreeView { self.selected = selected } - pub fn add_sibling_to_current_item(&mut self, item: T) -> Result<()> { - let current = self.current(); - match current.parent_index { + pub fn add_child(&mut self, index: usize, item: T) -> Result<()> { + match self.tree.get_mut(index) { None => Err(anyhow::anyhow!(format!( - "Current item = '{}' has no parent", - current.item.name() + "No item found at index = {}", + index ))), - Some(parent_index) => { - let parent = self.get_mut(parent_index); + Some(tree) => { let item_name = item.name(); - parent.children.push(Tree::new(item, vec![])); - parent - .children + if !tree.is_opened { + tree.open()?; + } + tree.children.push(Tree::new(item, vec![])); + tree.children .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)); self.regenerate_index(); - let parent = self.get_mut(parent_index); + + let tree = self.get_mut(index); // Focus the added sibling - if let Some(tree) = parent + if let Some(tree) = tree .children .iter() .find(|tree| tree.item.name().eq(&item_name)) @@ -903,7 +898,10 @@ impl TreeView { })); } key!('h') => self.go_to_parent(), - key!('l') => self.go_to_children(cx), + key!('l') => match self.go_to_children(cx) { + Ok(_) => {} + Err(err) => cx.editor.set_error(err.to_string()), + }, key!(Enter) => self.on_enter(cx, params, self.selected), ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), @@ -971,25 +969,24 @@ impl TreeView { /// jar (3) /// yo (4) /// ``` -fn index_elems(start_index: usize, elems: Vec>) -> Vec> { +fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { fn index_elems<'a, T>( current_index: usize, elems: Vec>, - parent_index: Option, + parent_index: usize, ) -> (usize, Vec>) { elems .into_iter() .fold((current_index, vec![]), |(current_index, trees), elem| { let index = current_index; let item = elem.item; - let (current_index, folded) = - index_elems(current_index + 1, elem.children, Some(index)); + let (current_index, folded) = index_elems(current_index + 1, elem.children, index); let tree = Tree { item, children: folded, index, is_opened: elem.is_opened, - parent_index, + parent_index: Some(parent_index), }; ( current_index, @@ -997,7 +994,7 @@ fn index_elems(start_index: usize, elems: Vec>) -> Vec> { ) }) } - index_elems(start_index, elems, None).1 + index_elems(parent_index + 1, elems, parent_index).1 } #[cfg(test)] @@ -1008,8 +1005,8 @@ mod test_tree { #[test] fn test_indexs_elems() { - let result = index_elems( - 0, + let result = Tree::new( + "root", vec![ Tree::new("foo", vec![Tree::new("bar", vec![])]), Tree::new( @@ -1018,43 +1015,12 @@ mod test_tree { ), ], ); - assert_eq!( - result, - vec![ - Tree { - item: "foo", - is_opened: false, - index: 0, - parent_index: None, - children: vec![Tree { - item: "bar", - is_opened: false, - index: 1, - parent_index: Some(0), - children: vec![] - }] - }, - Tree { - item: "spam", - is_opened: false, - index: 2, - parent_index: None, - children: vec![Tree { - item: "jar", - is_opened: false, - index: 3, - parent_index: Some(2), - children: vec![Tree { - item: "yo", - is_opened: false, - index: 4, - children: vec![], - parent_index: Some(3) - }] - }] - } - ] - ) + assert_eq!(result.get(0).unwrap().item, "root"); + assert_eq!(result.get(1).unwrap().item, "foo"); + assert_eq!(result.get(2).unwrap().item, "bar"); + assert_eq!(result.get(3).unwrap().item, "spam"); + assert_eq!(result.get(4).unwrap().item, "jar"); + assert_eq!(result.get(5).unwrap().item, "yo"); } #[test] @@ -1224,4 +1190,40 @@ mod test_tree { let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("helix")); assert_eq!(result, None) } + + #[test] + fn test_remove() { + let mut tree = Tree::new( + ".cargo", + vec![ + Tree::new("spam", vec![Tree::new("Cargo.toml", vec![])]), + Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), + Tree::new("hello", vec![]), + ], + ); + + tree.remove(2); + + assert_eq!( + tree, + Tree::new( + ".cargo", + vec![ + Tree::new("spam", vec![]), + Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), + Tree::new("hello", vec![]), + ], + ) + ); + + tree.remove(2); + + assert_eq!( + tree, + Tree::new( + ".cargo", + vec![Tree::new("spam", vec![]), Tree::new("hello", vec![]),], + ) + ) + } } -- 2.38.5 From 44b46dda6aedabe76b13de406f9c9a9784b2640c Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 15:51:02 +0800 Subject: [PATCH 011/191] feat(explore): rename file/folder --- helix-term/src/ui/explore.rs | 81 ++++++++++++++++++++++-------------- helix-term/src/ui/tree.rs | 47 ++++++++++++++++++++- 2 files changed, 95 insertions(+), 33 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index a04a605f..d4d3ae69 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -233,6 +233,7 @@ enum PromptAction { }, RemoveDir, RemoveFile, + RenameFile, Filter, } @@ -289,32 +290,34 @@ impl Explorer { } } + fn reveal_file(&mut self, cx: &mut Context, path: PathBuf) { + let current_root = &self.state.current_root; + let current_path = path.as_path().to_string_lossy().to_string(); + let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; + let segments = current_path + .strip_prefix(current_root.as_str()) + .expect( + format!( + "Failed to strip prefix '{}' from '{}'", + current_root, current_path + ) + .as_str(), + ) + .split(std::path::MAIN_SEPARATOR) + .collect::>(); + match self.tree.reveal_item(segments) { + Ok(_) => { + self.focus(); + } + Err(error) => cx.editor.set_error(error.to_string()), + } + } + pub fn reveal_current_file(&mut self, cx: &mut Context) { let current_document_path = doc!(cx.editor).path().cloned(); match current_document_path { None => cx.editor.set_error("No opened document."), - Some(current_path) => { - let current_root = &self.state.current_root; - let current_path = current_path.as_path().to_string_lossy().to_string(); - let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; - let segments = current_path - .strip_prefix(current_root.as_str()) - .expect( - format!( - "Failed to strip prefix '{}' from '{}'", - current_root, current_path - ) - .as_str(), - ) - .split(std::path::MAIN_SEPARATOR) - .collect::>(); - match self.tree.reveal_item(segments) { - Ok(_) => { - self.focus(); - } - Err(error) => cx.editor.set_error(error.to_string()), - } - } + Some(current_path) => self.reveal_file(cx, current_path), } } @@ -467,6 +470,20 @@ impl Explorer { } } + fn new_rename_prompt(&mut self) { + let name = self.tree.current_item().path.to_string_lossy(); + self.prompt = Some(( + PromptAction::RenameFile, + Prompt::new( + format!(" Rename to ").into(), + None, + ui::completers::none, + |_, _, _| {}, + ) + .with_line(name.to_string()), + )); + } + fn new_remove_file_prompt(&mut self, cx: &mut Context) { let item = self.tree.current_item(); let check = || { @@ -807,6 +824,15 @@ impl Explorer { } } } + (PromptAction::RenameFile, key!(Enter)) => { + let item = self.tree.current_item(); + if let Err(e) = std::fs::rename(&item.path, line) { + cx.editor.set_error(format!("{e}")); + } else { + self.tree.remove_current(); + self.reveal_file(cx, PathBuf::from(line)) + } + } (_, key!(Esc) | ctrl!('c')) => {} _ => { prompt.handle_event(Event::Key(event), cx); @@ -907,16 +933,7 @@ impl Component for Explorer { } key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), key!('d') => self.new_remove_prompt(cx), - 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) - })); - } + key!('r') => self.new_rename_prompt(), _ => { self.tree .handle_event(Event::Key(key_event), cx, &mut self.state); diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 16251669..c147b1e3 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -174,18 +174,61 @@ impl Tree { } Ok(()) } + + fn refresh(&mut self) -> Result<()> { + if !self.is_opened { + return Ok(()); + } + let latest_children = vec_to_tree(self.item.get_children()?); + let filtered = std::mem::replace(&mut self.children, vec![]) + .into_iter() + // Remove children that does not exists in latest_children + .filter(|tree| { + latest_children + .iter() + .any(|child| tree.item.name().eq(&child.item.name())) + }) + .map(|mut tree| { + tree.refresh()?; + Ok(tree) + }) + .collect::>>()?; + + // Add new children + let new_nodes = latest_children + .into_iter() + .filter(|child| { + !filtered + .iter() + .any(|child_| child.item.name().eq(&child_.item.name())) + }) + .collect::>(); + + self.children = filtered.into_iter().chain(new_nodes).collect(); + + self.sort(); + + Ok(()) + } + + fn sort(&mut self) { + self.children + .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)) + } } impl Tree { pub fn new(item: T, children: Vec>) -> Self { + let is_opened = !children.is_empty(); Self { item, index: 0, parent_index: None, children: index_elems(0, children), - is_opened: false, + is_opened, } } + fn iter(&self) -> TreeIter { TreeIter { tree: self, @@ -352,6 +395,8 @@ impl TreeView { /// vec!["helix-term", "src", "ui", "tree.rs"] /// ``` pub fn reveal_item(&mut self, segments: Vec<&str>) -> Result<()> { + self.tree.refresh()?; + // Expand the tree segments.iter().fold( Ok(&mut self.tree), -- 2.38.5 From 5a5a1de4b8420844e26bd3e8d090c3376240c354 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 16:10:08 +0800 Subject: [PATCH 012/191] fix(explore/rename): should regenarate index --- helix-term/src/ui/tree.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index c147b1e3..d8e00741 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -208,6 +208,8 @@ impl Tree { self.sort(); + self.regenerate_index(); + Ok(()) } -- 2.38.5 From 52a26ff72c1922fccb95ac5fadf4485ac12840b7 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 16:13:47 +0800 Subject: [PATCH 013/191] feat(explore): refresh --- helix-term/src/ui/explore.rs | 5 +++++ helix-term/src/ui/tree.rs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index d4d3ae69..1f2d31bb 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -934,6 +934,11 @@ impl Component for Explorer { key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), key!('d') => self.new_remove_prompt(cx), key!('r') => self.new_rename_prompt(), + shift!('R') => { + if let Err(error) = self.tree.refresh() { + cx.editor.set_error(error.to_string()) + } + } _ => { self.tree .handle_event(Event::Key(key_event), cx, &mut self.state); diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index d8e00741..b0e129f4 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -478,6 +478,10 @@ impl TreeView { Ok(()) } } + + pub fn refresh(&mut self) -> Result<()> { + self.tree.refresh() + } } impl TreeView { -- 2.38.5 From ec2059bf93ce841348ad55eeb8315314ae05255d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 17:13:26 +0800 Subject: [PATCH 014/191] style(ui/tree): highlight ancestor --- helix-term/src/ui/tree.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index b0e129f4..4a42c63d 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -747,7 +747,7 @@ impl TreeView { self.area_height = area.height.saturating_sub(1) as usize; self.winline = std::cmp::min(self.winline, self.area_height); let style = cx.editor.theme.get(&self.tree_symbol_style); - let last_item_index = self.tree.len().saturating_sub(1); + let ancestor_style = cx.editor.theme.get("ui.text.focus"); let skip = self.selected.saturating_sub(self.winline); let params = RenderElemParams { @@ -770,6 +770,7 @@ impl TreeView { struct Node { name: String, selected: bool, + descendant_selected: bool, } struct RenderElemParams<'a, T> { @@ -801,6 +802,7 @@ impl TreeView { Indent(indent), Node { selected: selected == tree.index, + descendant_selected: selected != tree.index && tree.get(selected).is_some(), name: format!( "{}{}", tree.item.name(), @@ -851,7 +853,11 @@ impl TreeView { .saturating_sub(indent_len) .saturating_sub(1) .into(), - style, + if node.descendant_selected { + ancestor_style + } else { + style + }, ); } // let mut text = elem.item.text(cx, skip + index == self.selected, params); -- 2.38.5 From ddb7564809dd068deabf48513e29de40bba0df3f Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 17:58:25 +0800 Subject: [PATCH 015/191] feat(explore): add help --- helix-term/src/ui/explore.rs | 78 +++++++++++++++++++++++++++--------- helix-term/src/ui/tree.rs | 19 +++++++++ 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 1f2d31bb..179ca418 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -254,6 +254,7 @@ impl State { pub struct Explorer { tree: TreeView, + show_help: bool, state: State, prompt: Option<(PromptAction, Prompt)>, #[allow(clippy::type_complexity)] @@ -267,6 +268,7 @@ impl Explorer { let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); Ok(Self { tree: Self::new_tree(current_root.clone())?, + show_help: false, state: State::new(true, current_root), repeat_motion: None, prompt: None, @@ -365,27 +367,47 @@ impl Explorer { surface.set_stringn( head_area.x, head_area.y, - path_str, + if self.show_help { + "[HELP]".to_string() + } else { + 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, - ); - }) - } + let content = if self.show_help { + vec![ + "? Toggle help", + "a Add file", + "A Add folder", + "r Rename file/folder", + "d Delete file", + "/ Search", + "f Filter", + "[ Change root to parent", + "] Change root to current", + "R Refresh tree", + ] + .into_iter() + .map(|s| s.to_string()) + .chain(ui::tree::tree_view_help()) + .collect() + } else { + get_preview(&item.path, body_area.height as usize) + .unwrap_or_else(|err| vec![err.to_string()]) + }; + 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) { @@ -596,7 +618,15 @@ impl Explorer { 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); + surface.set_stringn( + list_area.x, + list_area.y, + " Explorer: press ? for help", + list_area.width.into(), + cx.editor.theme.get("ui.text"), + ); + self.tree + .render(list_area.clip_top(1), surface, cx, &mut self.state); } pub fn render_embed( @@ -632,7 +662,15 @@ impl Explorer { render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1) } }; - self.tree.render(list_area, surface, cx, &mut self.state); + surface.set_stringn( + list_area.x, + list_area.y, + " Explorer: press ? for help", + list_area.width.into(), + cx.editor.theme.get("ui.text"), + ); + self.tree + .render(list_area.clip_top(1), surface, cx, &mut self.state); { let statusline = if self.is_focus() { @@ -871,6 +909,10 @@ impl Explorer { } Ok(()) } + + fn toggle_help(&mut self) { + self.show_help = !self.show_help + } } impl Component for Explorer { @@ -915,7 +957,7 @@ impl Component for Explorer { } key!('f') => self.new_filter_prompt(), key!('/') => self.new_search_prompt(true), - key!('?') => self.new_search_prompt(false), + key!('?') => self.toggle_help(), key!('a') => { if let Err(error) = self.new_create_file_prompt() { cx.editor.set_error(error.to_string()) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 4a42c63d..bd5eff7a 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -484,6 +484,25 @@ impl TreeView { } } +pub fn tree_view_help() -> Vec { + vec![ + "j Down", + "k Up", + "h Go to parent", + "l Expand", + "zz Align view center", + "zt Align view top", + "zb Align view bottom", + "gg Go to top", + "ge Go to end", + "^d Page down", + "^u Page up", + ] + .into_iter() + .map(|s| s.to_string()) + .collect() +} + impl TreeView { pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params, selected_index: usize) { // if let Some(next_level) = self.next_item().map(|elem| elem.level) { -- 2.38.5 From 2bafac0c4e077f0328ee700154200e74e4c5a781 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 20:55:52 +0800 Subject: [PATCH 016/191] feat(explore): go to previous root --- helix-term/src/commands.rs | 4 +- helix-term/src/ui/explore.rs | 250 +++++++++-------------------------- helix-term/src/ui/tree.rs | 8 +- helix-view/src/editor.rs | 15 --- 4 files changed, 70 insertions(+), 207 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ae725291..c0db885b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2222,7 +2222,7 @@ fn toggle_or_focus_explorer(cx: &mut Context) { if let Some(editor) = compositor.find::() { match editor.explorer.as_mut() { Some(explore) => explore.content.focus(), - None => match ui::Explorer::new(cx) { + None => match ui::Explorer::new() { Ok(explore) => editor.explorer = Some(overlayed(explore)), Err(err) => cx.editor.set_error(format!("{}", err)), }, @@ -2238,7 +2238,7 @@ fn reveal_current_file(cx: &mut Context) { if let Some(editor) = compositor.find::() { match editor.explorer.as_mut() { Some(explore) => explore.content.reveal_current_file(cx), - None => match ui::Explorer::new(cx) { + None => match ui::Explorer::new() { Ok(explore) => { let mut explorer = overlayed(explore); explorer.content.reveal_current_file(cx); diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 179ca418..a78a4ea6 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -7,7 +7,7 @@ use anyhow::{bail, ensure, Result}; use helix_core::Position; use helix_view::{ editor::{Action, ExplorerPositionEmbed}, - graphics::{CursorKind, Modifier, Rect}, + graphics::{CursorKind, Rect}, input::{Event, KeyEvent}, Editor, }; @@ -16,7 +16,6 @@ use std::cmp::Ordering; use std::path::{Path, PathBuf}; use tui::{ buffer::Buffer as Surface, - text::{Span, Spans}, widgets::{Block, Borders, Widget}, }; @@ -29,10 +28,7 @@ macro_rules! get_theme { #[derive(Debug, Clone, Copy, PartialEq)] enum FileType { File, - Dir, - Exe, - Placeholder, - Parent, + Folder, Root, } @@ -54,20 +50,10 @@ impl FileInfo { } } - 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 + FileType::File | FileType::Folder => self .path .file_name() .map_or("/".into(), |p| p.to_string_lossy().into_owned().into()), @@ -78,65 +64,23 @@ impl FileInfo { 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 style = theme.try_get(style).unwrap_or_else(|| theme.get("ui.text")); - // let style = if !selected { - // style - // } else { - // let patch = match state.focus { - // true => "ui.explorer.focus", - // false => "ui.explorer.unfocus", - // }; - // if let Some(patch) = theme.try_get(patch) { - // style.patch(patch) - // } else { - // 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) - } + 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, + (Folder, File) => return Ordering::Less, + (File, Folder) => return Ordering::Greater, _ => {} }; } @@ -146,18 +90,16 @@ impl TreeItem for FileInfo { fn get_children(&self) -> Result> { match self.file_type { - FileType::Root | FileType::Dir => {} + FileType::Root | FileType::Folder => {} _ => return Ok(vec![]), }; - let mut ret: Vec<_> = std::fs::read_dir(&self.path)? + let 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, + let file_type = match meta.is_dir() { + true => FileType::Folder, + false => FileType::File, }; Self { file_type, @@ -166,12 +108,6 @@ impl TreeItem for FileInfo { }) }) .collect(); - if ret.is_empty() { - ret.push(Self { - path: self.path.clone(), - file_type: FileType::Placeholder, - }) - } Ok(ret) } @@ -181,48 +117,17 @@ impl TreeItem for FileInfo { fn is_parent(&self) -> bool { match self.file_type { - FileType::Dir | FileType::Parent | FileType::Root => true, + FileType::Folder | FileType::Root => true, _ => false, } } } -// #[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, Debug)] enum PromptAction { - Search(bool), // search next/search pre + Search { + search_next: bool, + }, // search next/search pre CreateFolder { folder_path: PathBuf, parent_index: usize, @@ -254,6 +159,7 @@ impl State { pub struct Explorer { tree: TreeView, + history: Vec>, show_help: bool, state: State, prompt: Option<(PromptAction, Prompt)>, @@ -264,10 +170,11 @@ pub struct Explorer { } impl Explorer { - pub fn new(cx: &mut Context) -> Result { + pub fn new() -> Result { let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); Ok(Self { - tree: Self::new_tree(current_root.clone())?, + tree: Self::new_tree_view(current_root.clone())?, + history: vec![], show_help: false, state: State::new(true, current_root), repeat_motion: None, @@ -276,17 +183,27 @@ impl Explorer { }) } - fn new_tree(root: PathBuf) -> Result> { + fn new_tree_view(root: PathBuf) -> Result> { let root = FileInfo::root(root.clone()); let children = root.get_children()?; Ok(TreeView::build_tree(root, children).with_enter_fn(Self::toggle_current)) } + fn push_history(&mut self, tree_view: TreeView) { + self.history.push(tree_view); + const MAX_HISTORY_SIZE: usize = 20; + Vec::truncate(&mut self.history, MAX_HISTORY_SIZE) + } + fn change_root(&mut self, cx: &mut Context, root: PathBuf) { - match Self::new_tree(root.clone()) { + if self.state.current_root.eq(&root) { + return; + } + match Self::new_tree_view(root.clone()) { Ok(tree) => { + let old_tree = std::mem::replace(&mut self.tree, tree); + self.push_history(old_tree); self.state.current_root = root; - self.tree = tree; } Err(e) => cx.editor.set_error(format!("{e}")), } @@ -323,15 +240,6 @@ impl Explorer { } } - // 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; } @@ -344,24 +252,11 @@ impl Explorer { self.state.focus } - fn get_items(p: PathBuf, cx: &mut Context) -> Result> { - todo!() - // let mut items = vec![FileInfo::parent(p.as_path())]; - - // if cx.editor.config().explorer.is_tree() { - // items.push(root) - // } - // 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( @@ -387,8 +282,9 @@ impl Explorer { "d Delete file", "/ Search", "f Filter", - "[ Change root to parent", - "] Change root to current", + "[ Change root to parent folder", + "] Change root to current folder", + "^o Go to previous root", "R Refresh tree", ] .into_iter() @@ -413,8 +309,8 @@ impl Explorer { 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, |_, _, _| {}), + PromptAction::Search { search_next }, + Prompt::new(" Search: ".into(), None, ui::completers::none, |_, _, _| {}), )) } @@ -422,7 +318,7 @@ impl Explorer { self.tree.save_view(); self.prompt = Some(( PromptAction::Filter, - Prompt::new("filter: ".into(), None, ui::completers::none, |_, _, _| {}), + Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {}), )) } @@ -484,10 +380,8 @@ impl Explorer { fn new_remove_prompt(&mut self, cx: &mut Context) { let item = self.tree.current().item(); match item.file_type { - FileType::Dir => self.new_remove_dir_prompt(cx), - FileType::Exe | FileType::File => self.new_remove_file_prompt(cx), - FileType::Placeholder => cx.editor.set_error("Placeholder is not removable."), - FileType::Parent => cx.editor.set_error("Parent is not removable."), + FileType::Folder => self.new_remove_dir_prompt(cx), + FileType::File => self.new_remove_file_prompt(cx), FileType::Root => cx.editor.set_error("Root is not removable"), } } @@ -509,11 +403,6 @@ impl Explorer { 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"); @@ -533,11 +422,6 @@ impl Explorer { 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() @@ -563,9 +447,6 @@ impl Explorer { cx: &mut Context, state: &mut State, ) -> TreeOp { - if item.file_type == FileType::Placeholder { - return TreeOp::Noop; - } if item.path == Path::new("") { return TreeOp::Noop; } @@ -585,17 +466,7 @@ impl Explorer { } 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; - } + return TreeOp::GetChildsAndInsert; } cx.editor.set_error("unkonw file type"); TreeOp::Noop @@ -758,7 +629,7 @@ impl Explorer { 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, + PromptAction::Search { search_next } => search_next, _ => return EventResult::Ignored(None), }; match event.into() { @@ -771,7 +642,10 @@ impl Explorer { 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 { + if let PromptAction::Search { + search_next: is_next, + } = action + { explorer.tree.save_view(); if is_next == search_next { explorer @@ -809,7 +683,7 @@ impl Explorer { 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::Search { .. }, _)) => return self.handle_search_event(event, cx), Some((PromptAction::Filter, _)) => return self.handle_filter_event(event, cx), _ => {} }; @@ -887,32 +761,33 @@ impl Explorer { is_dir: bool, parent_index: usize, ) -> Result<()> { - let current = self.tree.current_item(); - let p = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); - match p.parent() { + let path = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); + match path.parent() { Some(p) if p == current_parent => {} _ => bail!("The file name is not illegal"), }; let file = if is_dir { - std::fs::create_dir(&p)?; - FileInfo::new(p, FileType::Dir) + std::fs::create_dir(&path)?; + FileInfo::new(path, FileType::Folder) } else { let mut fd = std::fs::OpenOptions::new(); - fd.create_new(true).write(true).open(&p)?; - FileInfo::new(p, FileType::File) + fd.create_new(true).write(true).open(&path)?; + FileInfo::new(path, FileType::File) }; - if current.file_type == FileType::Placeholder { - self.tree.replace_current(file); - } else { - self.tree.add_child(parent_index, file)?; - } + self.tree.add_child(parent_index, file)?; Ok(()) } fn toggle_help(&mut self) { self.show_help = !self.show_help } + + fn go_to_previous_root(&mut self) { + if let Some(tree) = self.history.pop() { + self.tree = tree + } + } } impl Component for Explorer { @@ -945,13 +820,13 @@ impl Component for Explorer { ctrl!('c') => return close_fn, key!('n') => { if let Some(mut repeat_motion) = self.repeat_motion.take() { - repeat_motion(self, PromptAction::Search(true), cx); + repeat_motion(self, PromptAction::Search { search_next: 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); + repeat_motion(self, PromptAction::Search { search_next: false }, cx); self.repeat_motion = Some(repeat_motion); } } @@ -974,6 +849,7 @@ impl Component for Explorer { } } key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), + ctrl!('o') => self.go_to_previous_root(), key!('d') => self.new_remove_prompt(cx), key!('r') => self.new_rename_prompt(), shift!('R') => { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index bd5eff7a..19bfc4d4 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -59,7 +59,7 @@ pub enum TreeOp { Restore, InsertChild(Vec), GetChildsAndInsert, - ReplaceTree(Vec), + ReplaceTree { root: T, children: Vec }, } #[derive(Debug, PartialEq, Eq)] @@ -550,8 +550,10 @@ impl TreeView { current.is_opened = true; current.children = vec_to_tree(items); } - TreeOp::ReplaceTree(items) => { - return self.replace_with_new_items(items); + TreeOp::ReplaceTree { root, children } => { + self.tree = Tree::new(root, vec_to_tree(children)); + self.selected = 0; + self.winline = 0; } TreeOp::Noop => {} }; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 4bb386fa..bfb852de 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -132,7 +132,6 @@ pub enum ExplorerPositionEmbed { #[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, @@ -154,25 +153,11 @@ impl ExplorerConfig { } } - 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::Left, column_width: 30, } -- 2.38.5 From 35ffc6036d3b6d1a425a35187f97226298e3936d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 21:17:07 +0800 Subject: [PATCH 017/191] feat(explore): increase/decrease explorer size --- helix-term/src/commands.rs | 4 ++-- helix-term/src/ui/explore.rs | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c0db885b..ae725291 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2222,7 +2222,7 @@ fn toggle_or_focus_explorer(cx: &mut Context) { if let Some(editor) = compositor.find::() { match editor.explorer.as_mut() { Some(explore) => explore.content.focus(), - None => match ui::Explorer::new() { + None => match ui::Explorer::new(cx) { Ok(explore) => editor.explorer = Some(overlayed(explore)), Err(err) => cx.editor.set_error(format!("{}", err)), }, @@ -2238,7 +2238,7 @@ fn reveal_current_file(cx: &mut Context) { if let Some(editor) = compositor.find::() { match editor.explorer.as_mut() { Some(explore) => explore.content.reveal_current_file(cx), - None => match ui::Explorer::new() { + None => match ui::Explorer::new(cx) { Ok(explore) => { let mut explorer = overlayed(explore); explorer.content.reveal_current_file(cx); diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index a78a4ea6..3d290756 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -167,10 +167,11 @@ pub struct Explorer { on_next_key: Option EventResult>>, #[allow(clippy::type_complexity)] repeat_motion: Option>, + column_width: u16, } impl Explorer { - pub fn new() -> Result { + pub fn new(cx: &mut Context) -> Result { let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); Ok(Self { tree: Self::new_tree_view(current_root.clone())?, @@ -180,6 +181,7 @@ impl Explorer { repeat_motion: None, prompt: None, on_next_key: None, + column_width: cx.editor.config().explorer.column_width as u16, }) } @@ -474,11 +476,10 @@ impl Explorer { 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); + let mut preview_area = area.clip_left(self.column_width + 1); if let Some((_, prompt)) = self.prompt.as_mut() { let area = preview_area.clip_bottom(2); let promp_area = @@ -507,9 +508,7 @@ impl Explorer { cx: &mut Context, position: &ExplorerPositionEmbed, ) { - let config = &cx.editor.config().explorer; - - let width = area.width.min(config.column_width as u16 + 2); + let width = area.width.min(self.column_width + 2); let side_area = match position { ExplorerPositionEmbed::Left => Rect { width, ..area }, @@ -857,6 +856,8 @@ impl Component for Explorer { cx.editor.set_error(error.to_string()) } } + key!('-') => self.column_width = self.column_width.saturating_sub(1), + key!('+') => self.column_width = self.column_width.saturating_add(1), _ => { self.tree .handle_event(Event::Key(key_event), cx, &mut self.state); @@ -886,7 +887,7 @@ impl Component for Explorer { }; let config = &editor.config().explorer; let (x, y) = if config.is_overlay() { - let colw = config.column_width as u16; + let colw = self.column_width as u16; if area.width > colw { (area.x + colw + 2, area.y + area.height - 2) } else { -- 2.38.5 From 2c221f0af1ff7379aea79c41ea6d7d02c3c0ed0d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 21:32:01 +0800 Subject: [PATCH 018/191] fix(explore): help page overflow --- helix-term/src/ui/explore.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 3d290756..d4a60d10 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -255,9 +255,9 @@ impl Explorer { } fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) { - if area.height <= 2 || area.width < 60 { - return; - } + // if area.height <= 2 || area.width < 60 { + // return; + // } let item = self.tree.current().item(); let head_area = render_block(area.clip_bottom(area.height - 2), surface, Borders::BOTTOM); let path_str = format!("{}", item.path.display()); @@ -567,19 +567,19 @@ impl Explorer { if self.is_focus() { const PREVIEW_AREA_MAX_WIDTH: u16 = 90; - const PREVIEW_AREA_MAX_HEIGHT: u16 = 25; + const PREVIEW_AREA_MAX_HEIGHT: u16 = 30; let preview_area_width = (area.width - side_area.width).min(PREVIEW_AREA_MAX_WIDTH); let preview_area_height = area.height.min(PREVIEW_AREA_MAX_HEIGHT); let preview_area = match position { - ExplorerPositionEmbed::Left => area.clip_left(side_area.width).clip_bottom(2), + ExplorerPositionEmbed::Left => area.clip_left(side_area.width), ExplorerPositionEmbed::Right => (Rect { x: area.width - side_area.width - preview_area_width, ..area }) - .clip_right(side_area.width) - .clip_bottom(2), - }; + .clip_right(side_area.width), + } + .clip_bottom(2); if preview_area.width < 30 || preview_area.height < 3 { return; } -- 2.38.5 From 790192dfd48fb4c4d7de583ce917c1c20d1e6e84 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 21:36:15 +0800 Subject: [PATCH 019/191] doc(explorer): up to date --- book/src/configuration.md | 3 +-- book/src/keymap.md | 34 +++------------------------------- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 003420c0..60bf4c17 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -238,5 +238,4 @@ 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 | + | `position` | explorer widget position, `overlay`, `right`, or `left` | `left` | diff --git a/book/src/keymap.md b/book/src/keymap.md index 1fd20bed..61145be7 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -276,7 +276,7 @@ This layer is a kludge of mappings, mostly pickers. | `/` | 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` | +| `E` | Reveal current file in explorer | `reveal_current_file` | > TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file. @@ -406,33 +406,5 @@ Keys to use within prompt, Remapping currently not supported. | `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 | +Press `?` to see keymaps. Remapping currently not supported. + -- 2.38.5 From b38a9419550832d98d5ff930c8e26527e163350a Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 13 Feb 2023 22:03:28 +0800 Subject: [PATCH 020/191] feat(explore): close without clearing previous state --- helix-term/src/ui/editor.rs | 12 ++++++++++-- helix-term/src/ui/explore.rs | 30 ++++++++++++++++++++++-------- helix-view/src/editor.rs | 1 - 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 73bc7173..12b7bf5a 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1269,8 +1269,16 @@ impl Component for EditorView { // if the terminal size suddenly changed, we need to trigger a resize let editor_area = area.clip_bottom(1); - let explorer_column_width = config.explorer.column_width as u16 + 2; - let editor_area = if self.explorer.is_some() { + + let editor_area = if let Some(explorer) = &self.explorer { + let explorer_column_width = if explorer.content.is_opened() { + explorer.content.column_width().saturating_add(2) + } else { + 0 + }; + // For future developer: + // We should have a Dock trait that allows a component to dock to the top/left/bottom/right + // of another component. match config.explorer.position { ExplorerPosition::Overlay => editor_area, ExplorerPosition::Left => editor_area.clip_left(explorer_column_width), diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index d4a60d10..ed0f7110 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -145,6 +145,7 @@ enum PromptAction { #[derive(Clone, Debug)] struct State { focus: bool, + open: bool, current_root: PathBuf, } @@ -153,6 +154,7 @@ impl State { Self { focus, current_root, + open: true, } } } @@ -244,12 +246,18 @@ impl Explorer { pub fn focus(&mut self) { self.state.focus = true; + self.state.open = true; } pub fn unfocus(&mut self) { self.state.focus = false; } + pub fn close(&mut self) { + self.state.focus = false; + self.state.open = false; + } + pub fn is_focus(&self) -> bool { self.state.focus } @@ -287,7 +295,8 @@ impl Explorer { "[ Change root to parent folder", "] Change root to current folder", "^o Go to previous root", - "R Refresh tree", + "R Refresh", + "q Close", ] .into_iter() .map(|s| s.to_string()) @@ -508,6 +517,9 @@ impl Explorer { cx: &mut Context, position: &ExplorerPositionEmbed, ) { + if !self.state.open { + return; + } let width = area.width.min(self.column_width + 2); let side_area = match position { @@ -787,6 +799,14 @@ impl Explorer { self.tree = tree } } + + pub fn is_opened(&self) -> bool { + self.state.open + } + + pub fn column_width(&self) -> u16 { + self.column_width + } } impl Component for Explorer { @@ -808,15 +828,9 @@ impl Component for Explorer { 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!('q') => self.close(), key!('n') => { if let Some(mut repeat_motion) = self.repeat_motion.take() { repeat_motion(self, PromptAction::Search { search_next: true }, cx); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bfb852de..3968c002 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -152,7 +152,6 @@ impl ExplorerConfig { _ => false, } } - } impl Default for ExplorerConfig { -- 2.38.5 From 56056e8556a751b8c105b302ce02dd87e5128236 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Feb 2023 09:41:48 +0800 Subject: [PATCH 021/191] fix(explore): increase size will cause panic --- helix-term/src/ui/explore.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index ed0f7110..2c1e7af7 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -147,6 +147,7 @@ struct State { focus: bool, open: bool, current_root: PathBuf, + area_width: u16, } impl State { @@ -155,6 +156,7 @@ impl State { focus, current_root, open: true, + area_width: 0, } } } @@ -296,6 +298,8 @@ impl Explorer { "] Change root to current folder", "^o Go to previous root", "R Refresh", + "+ Increase size", + "- Decrease size", "q Close", ] .into_iter() @@ -522,6 +526,8 @@ impl Explorer { } let width = area.width.min(self.column_width + 2); + self.state.area_width = area.width; + let side_area = match position { ExplorerPositionEmbed::Left => Rect { width, ..area }, ExplorerPositionEmbed::Right => Rect { @@ -545,7 +551,7 @@ impl Explorer { } }; surface.set_stringn( - list_area.x, + list_area.x.saturating_sub(1), list_area.y, " Explorer: press ? for help", list_area.width.into(), @@ -807,6 +813,18 @@ impl Explorer { pub fn column_width(&self) -> u16 { self.column_width } + + fn increase_size(&mut self) { + const EDITOR_MIN_WIDTH: u16 = 10; + self.column_width = std::cmp::min( + self.state.area_width.saturating_sub(EDITOR_MIN_WIDTH), + self.column_width.saturating_add(1), + ) + } + + fn decrease_size(&mut self) { + self.column_width = self.column_width.saturating_sub(1) + } } impl Component for Explorer { @@ -870,8 +888,8 @@ impl Component for Explorer { cx.editor.set_error(error.to_string()) } } - key!('-') => self.column_width = self.column_width.saturating_sub(1), - key!('+') => self.column_width = self.column_width.saturating_add(1), + key!('-') => self.decrease_size(), + key!('+') => self.increase_size(), _ => { self.tree .handle_event(Event::Key(key_event), cx, &mut self.state); -- 2.38.5 From a079477a23d96f90ddcaa93b3443d3f43dcd4f81 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Feb 2023 12:44:36 +0800 Subject: [PATCH 022/191] fix(compile): warnings --- helix-term/src/commands.rs | 26 ++-- helix-term/src/keymap/default.rs | 4 +- helix-term/src/ui/explore.rs | 156 ++++++++++----------- helix-term/src/ui/tree.rs | 232 ++++++++++++------------------- 4 files changed, 175 insertions(+), 243 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ae725291..9de52928 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -433,7 +433,7 @@ impl MappableCommand { record_macro, "Record macro", replay_macro, "Replay macro", command_palette, "Open command pallete", - toggle_or_focus_explorer, "Toggle or focus explorer", + open_or_focus_explorer, "Open or focus explorer", reveal_current_file, "Reveal current file in explorer", close_explorer, "close explorer", ); @@ -2216,7 +2216,7 @@ fn file_picker_in_current_directory(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } -fn toggle_or_focus_explorer(cx: &mut Context) { +fn open_or_focus_explorer(cx: &mut Context) { cx.callback = Some(Box::new( |compositor: &mut Compositor, cx: &mut compositor::Context| { if let Some(editor) = compositor.find::() { @@ -2236,17 +2236,19 @@ fn reveal_current_file(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() { + (|| match editor.explorer.as_mut() { Some(explore) => explore.content.reveal_current_file(cx), - None => match ui::Explorer::new(cx) { - Ok(explore) => { - let mut explorer = overlayed(explore); - explorer.content.reveal_current_file(cx); - editor.explorer = Some(explorer); - } - Err(err) => cx.editor.set_error(format!("{}", err)), - }, - } + None => { + editor.explorer = Some(overlayed(ui::Explorer::new(cx)?)); + let explorer = editor.explorer.as_mut().unwrap(); + explorer.content.reveal_current_file(cx)?; + explorer.content.focus(); + Ok(()) + } + })() + .unwrap_or_else(|err| { + cx.editor.set_error(err.to_string()) + }) } }, )); diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index cc00e30d..ecce1a5c 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -264,8 +264,8 @@ pub fn default() -> HashMap { "r" => rename_symbol, "h" => select_references_to_symbol_under_cursor, "?" => command_palette, - "e" => toggle_or_focus_explorer, - "E" => reveal_current_file, + "e" => reveal_current_file, + "E" => open_or_focus_explorer, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 2c1e7af7..375a6d3b 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -1,6 +1,6 @@ use super::{Prompt, TreeItem, TreeOp, TreeView}; use crate::{ - compositor::{Component, Compositor, Context, EventResult}, + compositor::{Component, Context, EventResult}, ctrl, key, shift, ui, }; use anyhow::{bail, ensure, Result}; @@ -148,6 +148,7 @@ struct State { open: bool, current_root: PathBuf, area_width: u16, + filter: String, } impl State { @@ -157,6 +158,7 @@ impl State { current_root, open: true, area_width: 0, + filter: "".to_string(), } } } @@ -215,7 +217,7 @@ impl Explorer { } } - fn reveal_file(&mut self, cx: &mut Context, path: PathBuf) { + fn reveal_file(&mut self, path: PathBuf) -> Result<()> { let current_root = &self.state.current_root; let current_path = path.as_path().to_string_lossy().to_string(); let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; @@ -230,19 +232,16 @@ impl Explorer { ) .split(std::path::MAIN_SEPARATOR) .collect::>(); - match self.tree.reveal_item(segments) { - Ok(_) => { - self.focus(); - } - Err(error) => cx.editor.set_error(error.to_string()), - } + self.tree.reveal_item(segments)?; + self.focus(); + Ok(()) } - pub fn reveal_current_file(&mut self, cx: &mut Context) { + pub fn reveal_current_file(&mut self, cx: &mut Context) -> Result<()> { let current_document_path = doc!(cx.editor).path().cloned(); match current_document_path { - None => cx.editor.set_error("No opened document."), - Some(current_path) => self.reveal_file(cx, current_path), + None => Err(anyhow::anyhow!("No opened document.")), + Some(current_path) => self.reveal_file(current_path), } } @@ -457,11 +456,7 @@ impl Explorer { )); } - fn toggle_current( - item: &mut FileInfo, - cx: &mut Context, - state: &mut State, - ) -> TreeOp { + fn toggle_current(item: &mut FileInfo, cx: &mut Context, state: &mut State) -> TreeOp { if item.path == Path::new("") { return TreeOp::Noop; } @@ -629,13 +624,13 @@ impl Explorer { } key!(Enter) => { if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { - self.tree.filter(prompt.line(), cx, &mut self.state); + self.tree.filter(prompt.line()); } } 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.tree.filter(prompt.line()); } self.prompt = Some((action, prompt)); } @@ -658,20 +653,16 @@ impl Explorer { key!(Enter) => { let search_str = prompt.line().clone(); if !search_str.is_empty() { - self.repeat_motion = Some(Box::new(move |explorer, action, cx| { + self.repeat_motion = Some(Box::new(move |explorer, action, _| { if let PromptAction::Search { search_next: is_next, } = action { explorer.tree.save_view(); if is_next == search_next { - explorer - .tree - .search_next(cx, &search_str, &mut explorer.state); + explorer.tree.search_next(&search_str); } else { - explorer - .tree - .search_previous(cx, &search_str, &mut explorer.state); + explorer.tree.search_previous(&search_str); } } })) @@ -686,10 +677,9 @@ impl Explorer { _ => { if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { if search_next { - self.tree.search_next(cx, prompt.line(), &mut self.state); + self.tree.search_next(prompt.line()); } else { - self.tree - .search_previous(cx, prompt.line(), &mut self.state); + self.tree.search_previous(prompt.line()); } } self.prompt = Some((action, prompt)); @@ -704,71 +694,67 @@ impl Explorer { 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::CreateFolder { - folder_path, - parent_index, - }, - key!(Enter), - ) => { - if let Err(e) = self.new_path(folder_path.clone(), line, true, *parent_index) { - cx.editor.set_error(format!("{e}")) - } - } - ( - PromptAction::CreateFile { - folder_path, - parent_index, - }, - key!(Enter), - ) => { - if let Err(e) = self.new_path(folder_path.clone(), line, false, *parent_index) { - cx.editor.set_error(format!("{e}")) - } - } - (PromptAction::RemoveDir, key!(Enter)) => { - if line == "y" { - 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(); + fn handle_prompt_event( + explorer: &mut Explorer, + event: KeyEvent, + cx: &mut Context, + ) -> Result { + let (action, mut prompt) = match explorer.prompt.take() { + Some((action, p)) => (action, p), + _ => return Ok(EventResult::Ignored(None)), + }; + let line = prompt.line(); + match (&action, event.into()) { + ( + PromptAction::CreateFolder { + folder_path, + parent_index, + }, + key!(Enter), + ) => explorer.new_path(folder_path.clone(), line, true, *parent_index)?, + ( + PromptAction::CreateFile { + folder_path, + parent_index, + }, + key!(Enter), + ) => explorer.new_path(folder_path.clone(), line, false, *parent_index)?, + (PromptAction::RemoveDir, key!(Enter)) => { + if line == "y" { + let item = explorer.tree.current_item(); + std::fs::remove_dir_all(&item.path)?; + explorer.tree.fold_current_child(); + explorer.tree.remove_current(); } } - } - (PromptAction::RemoveFile, key!(Enter)) => { - if line == "y" { - 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(); + (PromptAction::RemoveFile, key!(Enter)) => { + if line == "y" { + let item = explorer.tree.current_item(); + std::fs::remove_file(&item.path).map_err(anyhow::Error::from)?; + explorer.tree.remove_current(); } } - } - (PromptAction::RenameFile, key!(Enter)) => { - let item = self.tree.current_item(); - if let Err(e) = std::fs::rename(&item.path, line) { - cx.editor.set_error(format!("{e}")); - } else { - self.tree.remove_current(); - self.reveal_file(cx, PathBuf::from(line)) + (PromptAction::RenameFile, key!(Enter)) => { + let item = explorer.tree.current_item(); + std::fs::rename(&item.path, line)?; + explorer.tree.remove_current(); + explorer.reveal_file(PathBuf::from(line))?; + } + (_, key!(Esc) | ctrl!('c')) => {} + _ => { + prompt.handle_event(Event::Key(event), cx); + explorer.prompt = Some((action, prompt)); } } - (_, key!(Esc) | ctrl!('c')) => {} - _ => { - prompt.handle_event(Event::Key(event), cx); - self.prompt = Some((action, prompt)); + Ok(EventResult::Consumed(None)) + } + match handle_prompt_event(self, event, cx) { + Ok(event_result) => event_result, + Err(err) => { + cx.editor.set_error(err.to_string()); + EventResult::Consumed(None) } } - EventResult::Consumed(None) } fn new_path( diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 19bfc4d4..7763fb63 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, path::PathBuf}; +use std::cmp::Ordering; use anyhow::Result; use helix_view::theme::Modifier; @@ -7,12 +7,12 @@ use crate::{ compositor::{Context, EventResult}, ctrl, key, shift, }; -use helix_core::{movement::Direction, unicode::width::UnicodeWidthStr}; +use helix_core::movement::Direction; use helix_view::{ graphics::Rect, input::{Event, KeyEvent}, }; -use tui::{buffer::Buffer as Surface, text::Spans}; +use tui::buffer::Buffer as Surface; pub trait TreeItem: Sized { type Params; @@ -54,12 +54,9 @@ fn vec_to_tree(mut items: Vec) -> Vec> { ) } -pub enum TreeOp { +pub enum TreeOp { Noop, - Restore, - InsertChild(Vec), GetChildsAndInsert, - ReplaceTree { root: T, children: Vec }, } #[derive(Debug, PartialEq, Eq)] @@ -132,7 +129,7 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} -impl Tree { +impl Tree { pub fn filter

(tree: &Tree, predicate: &P) -> Option> where P: Fn(&T) -> bool, @@ -142,7 +139,7 @@ impl Tree { .iter() .filter_map(|tree| Self::filter(tree, predicate)) .collect::>(); - if predicate(&tree.item) || !children.is_empty() { + if tree.item.is_parent() || predicate(&tree.item) || !children.is_empty() { let mut tree = Tree { item: tree.item.clone(), parent_index: tree.parent_index, @@ -156,14 +153,6 @@ impl Tree { None } } - - pub fn parent_index(&self) -> Option { - self.parent_index - } - - pub fn index(&self) -> usize { - self.index - } } impl Tree { @@ -305,6 +294,14 @@ impl Tree { .collect(); self.regenerate_index() } + + pub fn parent_index(&self) -> Option { + self.parent_index + } + + pub fn index(&self) -> usize { + self.index + } } pub struct TreeView { @@ -327,8 +324,7 @@ pub struct TreeView { #[allow(clippy::type_complexity)] pre_render: Option>, #[allow(clippy::type_complexity)] - on_opened_fn: - Option TreeOp + 'static>>, + on_opened_fn: Option TreeOp + 'static>>, #[allow(clippy::type_complexity)] on_folded_fn: Option>, #[allow(clippy::type_complexity)] @@ -355,21 +351,13 @@ impl TreeView { } } - pub fn replace_with_new_items(&mut self, items: Vec) { - todo!() - // let old = std::mem::replace(self, Self::new(vec_to_tree(items))); - // 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(root: T, items: Vec) -> Self { Self::new(root, vec_to_tree(items)) } pub fn with_enter_fn(mut self, f: F) -> Self where - F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static, + F: FnMut(&mut T, &mut Context, &mut T::Params) -> TreeOp + 'static, { self.on_opened_fn = Some(Box::new(f)); self @@ -464,7 +452,7 @@ impl TreeView { } } - fn go_to_children(&mut self, cx: &mut Context) -> Result<()> { + fn go_to_children(&mut self) -> Result<()> { let current = self.current_mut(); if current.is_opened { self.selected += 1; @@ -530,18 +518,6 @@ impl TreeView { let mut f = || { let current = &mut self.get_mut(selected_index); match on_open_fn(&mut current.item, cx, params) { - TreeOp::Restore => { - panic!(); - // let inserts = std::mem::take(&mut current.folded); - // let _: Vec<_> = self - // .items - // .splice(selected_index + 1..selected_index + 1, inserts) - // .collect(); - return; - } - TreeOp::InsertChild(items) => { - items; - } TreeOp::GetChildsAndInsert => { let items = match current.item.get_children() { Ok(items) => items, @@ -550,33 +526,12 @@ impl TreeView { current.is_opened = true; current.children = vec_to_tree(items); } - TreeOp::ReplaceTree { root, children } => { - self.tree = Tree::new(root, vec_to_tree(children)); - self.selected = 0; - self.winline = 0; - } TreeOp::Noop => {} }; - - // current.folded = vec![]; - // let inserts = vec_to_tree(items, current.level + 1); - // let _: Vec<_> = self - // .items - // .splice(selected_index + 1..selected_index + 1, inserts) - // .collect(); }; f(); self.regenerate_index(); self.on_opened_fn = Some(on_open_fn) - } else { - panic!(); - self.get_mut(selected_index).children = vec![]; - // let current = &mut self.items[selected_index]; - // let inserts = std::mem::take(&mut current.folded); - // let _: Vec<_> = self - // .items - // .splice(selected_index + 1..selected_index + 1, inserts) - // .collect(); } } @@ -589,7 +544,7 @@ impl TreeView { } } - pub fn search_next(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) { + pub fn search_next(&mut self, s: &str) { let skip = std::cmp::max(2, self.save_view.0 + 1); self.selected = self .tree @@ -599,7 +554,7 @@ impl TreeView { self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); } - pub fn search_previous(&mut self, cx: &mut Context, s: &str, params: &mut T::Params) { + pub fn search_previous(&mut self, s: &str) { let take = self.save_view.0; self.selected = self .tree @@ -881,69 +836,6 @@ impl TreeView { }, ); } - // let mut text = elem.item.text(cx, skip + index == self.selected, params); - // 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( @@ -976,7 +868,7 @@ impl TreeView { })); } key!('h') => self.go_to_parent(), - key!('l') => match self.go_to_children(cx) { + key!('l') => match self.go_to_children() { Ok(_) => {} Err(err) => cx.editor.set_error(err.to_string()), }, @@ -998,7 +890,7 @@ impl TreeView { } impl TreeView { - pub fn filter(&mut self, s: &str, cx: &mut Context, params: &mut T::Params) { + pub fn filter(&mut self, s: &str) { if s.is_empty() { self.restore_recycle(); return; @@ -1079,7 +971,9 @@ fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { mod test_tree { use helix_core::movement::Direction; - use super::{index_elems, Tree}; + use crate::ui::TreeItem; + + use super::Tree; #[test] fn test_indexs_elems() { @@ -1232,41 +1126,91 @@ mod test_tree { #[test] fn test_filter() { + #[derive(Clone, Debug, PartialEq, Eq)] + struct MyItem<'a>(bool, &'a str); + impl<'a> TreeItem for MyItem<'a> { + type Params = (); + fn name(&self) -> String { + self.0.to_string() + } + fn is_child(&self, _: &Self) -> bool { + !self.0 + } + fn is_parent(&self) -> bool { + self.0 + } + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.1.cmp(other.1) + } + } let tree = Tree::new( - ".cargo", + MyItem(false, ".cargo"), vec![ - Tree::new("spam", vec![Tree::new("Cargo.toml", vec![])]), - Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), - Tree::new("hello", vec![]), + Tree::new( + MyItem(true, "spam"), + vec![Tree::new(MyItem(false, "Cargo.toml"), vec![])], + ), + Tree::new( + MyItem(true, "Cargo.toml"), + vec![Tree::new(MyItem(false, "pam"), vec![])], + ), + Tree::new(MyItem(false, "hello"), vec![]), ], ); - let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("cargo")); + let result = Tree::filter(&tree, &|item| item.1.to_lowercase().contains("cargo")); assert_eq!( result, Some(Tree::new( - ".cargo", + MyItem(false, ".cargo"), vec![ - Tree::new("spam", vec![Tree::new("Cargo.toml", vec![])]), - Tree::new("Cargo.toml", vec![]), + Tree::new( + MyItem(true, "spam"), + vec![Tree::new(MyItem(false, "Cargo.toml"), vec![])] + ), + Tree { + is_opened: true, + ..Tree::new(MyItem(true, "Cargo.toml"), vec![]) + }, ], )) ); - let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("pam")); + let result = Tree::filter(&tree, &|item| item.1.to_lowercase().contains("pam")); assert_eq!( result, Some(Tree::new( - ".cargo", + MyItem(false, ".cargo"), vec![ - Tree::new("spam", vec![]), - Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), + Tree { + is_opened: true, + ..Tree::new(MyItem(true, "spam"), vec![]) + }, + Tree::new( + MyItem(true, "Cargo.toml"), + vec![Tree::new(MyItem(false, "pam"), vec![])] + ), ], )) ); - let result = Tree::filter(&tree, &|item| item.to_lowercase().contains("helix")); - assert_eq!(result, None) + let result = Tree::filter(&tree, &|item| item.1.to_lowercase().contains("helix")); + assert_eq!( + result, + Some(Tree::new( + MyItem(false, ".cargo"), + vec![ + Tree { + is_opened: true, + ..Tree::new(MyItem(true, "spam"), vec![]) + }, + Tree { + is_opened: true, + ..Tree::new(MyItem(true, "Cargo.toml"), vec![]) + } + ], + )) + ) } #[test] -- 2.38.5 From 85fa1c56b76357c5ff69fee19904d731e0de06dd Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Feb 2023 17:11:28 +0800 Subject: [PATCH 023/191] feat(explore): - filter - close document if the file is deleted or renamed --- helix-term/src/ui/explore.rs | 70 +++++++++++--------- helix-term/src/ui/tree.rs | 121 ++++++++++++++++++++++++----------- 2 files changed, 126 insertions(+), 65 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 375a6d3b..c1ec0067 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -9,7 +9,7 @@ use helix_view::{ editor::{Action, ExplorerPositionEmbed}, graphics::{CursorKind, Rect}, input::{Event, KeyEvent}, - Editor, + DocumentId, Editor, }; use std::borrow::Cow; use std::cmp::Ordering; @@ -137,8 +137,8 @@ enum PromptAction { parent_index: usize, }, RemoveDir, - RemoveFile, - RenameFile, + RemoveFile(Option), + RenameFile(Option), Filter, } @@ -232,7 +232,7 @@ impl Explorer { ) .split(std::path::MAIN_SEPARATOR) .collect::>(); - self.tree.reveal_item(segments)?; + self.tree.reveal_item(segments, &self.state.filter)?; self.focus(); Ok(()) } @@ -332,7 +332,8 @@ impl Explorer { self.tree.save_view(); self.prompt = Some(( PromptAction::Filter, - Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {}), + Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {}) + .with_line(self.state.filter.clone()), )) } @@ -400,17 +401,17 @@ impl Explorer { } } - fn new_rename_prompt(&mut self) { - let name = self.tree.current_item().path.to_string_lossy(); + fn new_rename_prompt(&mut self, cx: &mut Context) { + let path = self.tree.current_item().path.clone(); self.prompt = Some(( - PromptAction::RenameFile, + PromptAction::RenameFile(cx.editor.document_by_path(&path).map(|doc| doc.id())), Prompt::new( format!(" Rename to ").into(), None, ui::completers::none, |_, _, _| {}, ) - .with_line(name.to_string()), + .with_line(path.to_string_lossy().to_string()), )); } @@ -419,18 +420,18 @@ impl Explorer { let check = || { 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(()) + Ok(doc.map(|doc| doc.id())) }; - if let Err(e) = check() { - cx.editor.set_error(format!("{e}")); - return; + match check() { + Err(err) => cx.editor.set_error(format!("{err}")), + Ok(document_id) => { + let p = format!(" Delete file: '{}'? y/n: ", item.path.display()); + self.prompt = Some(( + PromptAction::RemoveFile(document_id), + Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), + )); + } } - let p = format!(" Delete file: '{}'? y/n: ", 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) { @@ -506,7 +507,7 @@ impl Explorer { cx.editor.theme.get("ui.text"), ); self.tree - .render(list_area.clip_top(1), surface, cx, &mut self.state); + .render(list_area.clip_top(1), surface, cx, &self.state.filter); } pub fn render_embed( @@ -553,7 +554,7 @@ impl Explorer { cx.editor.theme.get("ui.text"), ); self.tree - .render(list_area.clip_top(1), surface, cx, &mut self.state); + .render(list_area.clip_top(1), surface, cx, &self.state.filter); { let statusline = if self.is_focus() { @@ -618,9 +619,10 @@ impl Explorer { match event.into() { key!(Tab) | key!(Down) | ctrl!('j') => { self.tree.clean_recycle(); + let filter = self.state.filter.clone(); return self .tree - .handle_event(Event::Key(event), cx, &mut self.state); + .handle_event(Event::Key(event), cx, &mut self.state, &filter); } key!(Enter) => { if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { @@ -632,6 +634,7 @@ impl Explorer { if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { self.tree.filter(prompt.line()); } + self.state.filter = prompt.line().clone(); self.prompt = Some((action, prompt)); } }; @@ -646,9 +649,10 @@ impl Explorer { }; match event.into() { key!(Tab) | key!(Down) | ctrl!('j') => { + let filter = self.state.filter.clone(); return self .tree - .handle_event(Event::Key(event), cx, &mut self.state) + .handle_event(Event::Key(event), cx, &mut self.state, &filter); } key!(Enter) => { let search_str = prompt.line().clone(); @@ -727,18 +731,24 @@ impl Explorer { explorer.tree.remove_current(); } } - (PromptAction::RemoveFile, key!(Enter)) => { + (PromptAction::RemoveFile(document_id), key!(Enter)) => { if line == "y" { let item = explorer.tree.current_item(); std::fs::remove_file(&item.path).map_err(anyhow::Error::from)?; explorer.tree.remove_current(); + if let Some(id) = document_id { + cx.editor.close_document(*id, true)? + } } } - (PromptAction::RenameFile, key!(Enter)) => { + (PromptAction::RenameFile(document_id), key!(Enter)) => { let item = explorer.tree.current_item(); std::fs::rename(&item.path, line)?; explorer.tree.remove_current(); explorer.reveal_file(PathBuf::from(line))?; + if let Some(id) = document_id { + cx.editor.close_document(*id, true)? + } } (_, key!(Esc) | ctrl!('c')) => {} _ => { @@ -778,7 +788,8 @@ impl Explorer { fd.create_new(true).write(true).open(&path)?; FileInfo::new(path, FileType::File) }; - self.tree.add_child(parent_index, file)?; + self.tree + .add_child(parent_index, file, &self.state.filter)?; Ok(()) } @@ -868,17 +879,18 @@ impl Component for Explorer { key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), ctrl!('o') => self.go_to_previous_root(), key!('d') => self.new_remove_prompt(cx), - key!('r') => self.new_rename_prompt(), + key!('r') => self.new_rename_prompt(cx), shift!('R') => { - if let Err(error) = self.tree.refresh() { + if let Err(error) = self.tree.refresh(&self.state.filter) { cx.editor.set_error(error.to_string()) } } key!('-') => self.decrease_size(), key!('+') => self.increase_size(), _ => { + let filter = self.state.filter.clone(); self.tree - .handle_event(Event::Key(key_event), cx, &mut self.state); + .handle_event(Event::Key(key_event), cx, &mut self.state, &filter); } } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 7763fb63..229b92bd 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1,4 +1,4 @@ -use std::cmp::Ordering; +use std::{borrow::Cow, cmp::Ordering}; use anyhow::Result; use helix_view::theme::Modifier; @@ -156,19 +156,31 @@ impl Tree { } impl Tree { - fn open(&mut self) -> Result<()> { - self.children = vec_to_tree(self.item.get_children()?); - if !self.children.is_empty() { + fn open(&mut self, filter: &String) -> Result<()> { + if self.item.is_parent() { + self.children = vec_to_tree( + self.item + .get_children()? + .into_iter() + .filter(|item| item.name().to_lowercase().contains(&filter.to_lowercase())) + .collect(), + ); self.is_opened = true; } Ok(()) } - fn refresh(&mut self) -> Result<()> { + fn refresh(&mut self, filter: &String) -> Result<()> { if !self.is_opened { return Ok(()); } - let latest_children = vec_to_tree(self.item.get_children()?); + let latest_children = vec_to_tree( + self.item + .get_children()? + .into_iter() + .filter(|item| item.name().to_lowercase().contains(&filter.to_lowercase())) + .collect(), + ); let filtered = std::mem::replace(&mut self.children, vec![]) .into_iter() // Remove children that does not exists in latest_children @@ -178,7 +190,7 @@ impl Tree { .any(|child| tree.item.name().eq(&child.item.name())) }) .map(|mut tree| { - tree.refresh()?; + tree.refresh(filter)?; Ok(tree) }) .collect::>>()?; @@ -249,12 +261,47 @@ impl Tree { &self.item } - fn get(&self, index: usize) -> Option<&Tree> { + fn get<'a>(&'a self, index: usize) -> Option<&'a Tree> { if self.index == index { Some(self) } else { self.children.iter().find_map(|elem| elem.get(index)) } + // self.traverse(None, &|result, current_index, tree| { + // result.or_else(|| { + // if index == current_index { + // Some(tree) + // } else { + // None + // } + // }) + // }) + } + fn traverse<'a, U, F>(&'a self, init: U, f: &F) -> U + where + F: Fn(U, usize, &'a Tree) -> U, + { + fn traverse<'a, T, U, F>( + tree: &'a Tree, + current_index: usize, + init: U, + f: &F, + ) -> (usize, U) + where + F: Fn(U, usize, &'a Tree) -> U, + { + let mut result = f(init, current_index, &tree); + let mut current_index = current_index; + for tree in &tree.children { + (current_index, result) = traverse(tree, current_index + 1, result, f) + } + (current_index, result) + // tree.children.iter().fold( + // (current_index, f(init, 0, &tree)), + // |(current_index, result), tree| traverse(tree, current_index + 1, result, &f), + // ) + } + traverse(self, 0, init, f).1 } fn get_mut(&mut self, index: usize) -> Option<&mut Tree> { @@ -384,8 +431,8 @@ impl TreeView { /// ``` /// vec!["helix-term", "src", "ui", "tree.rs"] /// ``` - pub fn reveal_item(&mut self, segments: Vec<&str>) -> Result<()> { - self.tree.refresh()?; + pub fn reveal_item(&mut self, segments: Vec<&str>, filter: &String) -> Result<()> { + self.tree.refresh(filter)?; // Expand the tree segments.iter().fold( @@ -400,7 +447,7 @@ impl TreeView { { Some(tree) => { if !tree.is_opened { - tree.open()?; + tree.open(filter)?; } Ok(tree) } @@ -452,13 +499,13 @@ impl TreeView { } } - fn go_to_children(&mut self) -> Result<()> { + fn go_to_children(&mut self, filter: &String) -> Result<()> { let current = self.current_mut(); if current.is_opened { self.selected += 1; Ok(()) } else { - current.open()?; + current.open(filter)?; if !current.children.is_empty() { self.selected += 1; self.regenerate_index(); @@ -467,8 +514,8 @@ impl TreeView { } } - pub fn refresh(&mut self) -> Result<()> { - self.tree.refresh() + pub fn refresh(&mut self, filter: &String) -> Result<()> { + self.tree.refresh(filter) } } @@ -492,7 +539,13 @@ pub fn tree_view_help() -> Vec { } impl TreeView { - pub fn on_enter(&mut self, cx: &mut Context, params: &mut T::Params, selected_index: usize) { + pub fn on_enter( + &mut self, + cx: &mut Context, + params: &mut T::Params, + selected_index: usize, + filter: &String, + ) { // if let Some(next_level) = self.next_item().map(|elem| elem.level) { // let current = self.find_by_index(selected_index); // let current_level = current.level; @@ -516,15 +569,12 @@ impl TreeView { if let Some(mut on_open_fn) = self.on_opened_fn.take() { let mut f = || { - let current = &mut self.get_mut(selected_index); + let current = self.current_mut(); match on_open_fn(&mut current.item, cx, params) { TreeOp::GetChildsAndInsert => { - let items = match current.item.get_children() { - Ok(items) => items, - Err(e) => return cx.editor.set_error(format!("{e}")), - }; - current.is_opened = true; - current.children = vec_to_tree(items); + if let Err(err) = current.open(filter) { + cx.editor.set_error(format!("{err}")) + } } TreeOp::Noop => {} }; @@ -675,7 +725,7 @@ impl TreeView { self.selected = selected } - pub fn add_child(&mut self, index: usize, item: T) -> Result<()> { + pub fn add_child(&mut self, index: usize, item: T, filter: &String) -> Result<()> { match self.tree.get_mut(index) { None => Err(anyhow::anyhow!(format!( "No item found at index = {}", @@ -684,7 +734,7 @@ impl TreeView { Some(tree) => { let item_name = item.name(); if !tree.is_opened { - tree.open()?; + tree.open(filter)?; } tree.children.push(Tree::new(item, vec![])); tree.children @@ -707,14 +757,8 @@ impl TreeView { } } -impl TreeView { - pub fn render( - &mut self, - area: Rect, - surface: &mut Surface, - cx: &mut Context, - params: &mut T::Params, - ) { +impl TreeView { + pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { if let Some(pre_render) = self.pre_render.take() { pre_render(self, area); } @@ -732,6 +776,7 @@ impl TreeView { is_last: true, level: 0, selected: self.selected, + filter, }; let rendered = render_tree(params); @@ -755,6 +800,7 @@ impl TreeView { is_last: bool, level: u16, selected: usize, + filter: &'a str, } fn render_tree( @@ -764,6 +810,7 @@ impl TreeView { is_last, level, selected, + filter, }: RenderElemParams, ) -> Vec<(Indent, Node)> { let indent = if level > 0 { @@ -805,6 +852,7 @@ impl TreeView { is_last, level: level + 1, selected, + filter, }) }), ) @@ -843,6 +891,7 @@ impl TreeView { event: Event, cx: &mut Context, params: &mut T::Params, + filter: &String, ) -> EventResult { let key_event = match event { Event::Key(event) => event, @@ -868,11 +917,11 @@ impl TreeView { })); } key!('h') => self.go_to_parent(), - key!('l') => match self.go_to_children() { + key!('l') => match self.go_to_children(filter) { Ok(_) => {} Err(err) => cx.editor.set_error(err.to_string()), }, - key!(Enter) => self.on_enter(cx, params, self.selected), + key!(Enter) => self.on_enter(cx, params, self.selected, filter), ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('g') => { @@ -976,7 +1025,7 @@ mod test_tree { use super::Tree; #[test] - fn test_indexs_elems() { + fn test_get() { let result = Tree::new( "root", vec![ -- 2.38.5 From 9bd534bb6fb438734ee48911fa04a56418baad34 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Feb 2023 18:53:56 +0800 Subject: [PATCH 024/191] fix(explore): filter --- helix-term/src/ui/explore.rs | 82 ++++++++++++++++-------------------- helix-term/src/ui/tree.rs | 58 +++---------------------- 2 files changed, 41 insertions(+), 99 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index c1ec0067..d2bb1b5b 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -240,7 +240,7 @@ impl Explorer { pub fn reveal_current_file(&mut self, cx: &mut Context) -> Result<()> { let current_document_path = doc!(cx.editor).path().cloned(); match current_document_path { - None => Err(anyhow::anyhow!("No opened document.")), + None => Ok(()), Some(current_path) => self.reveal_file(current_path), } } @@ -499,15 +499,26 @@ impl Explorer { self.render_preview(preview_area, surface, cx.editor); let list_area = render_block(area.clip_right(preview_area.width), surface, Borders::RIGHT); + self.render_tree(list_area, surface, cx) + } + + fn render_tree(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { surface.set_stringn( - list_area.x, - list_area.y, + area.x, + area.y, " Explorer: press ? for help", - list_area.width.into(), + area.width.into(), + cx.editor.theme.get("ui.text"), + ); + surface.set_stringn( + area.x, + area.y.saturating_add(1), + format!(" [FILTER]: {}", self.state.filter), + area.width.into(), cx.editor.theme.get("ui.text"), ); self.tree - .render(list_area.clip_top(1), surface, cx, &self.state.filter); + .render(area.clip_top(2), surface, cx, &self.state.filter); } pub fn render_embed( @@ -546,15 +557,7 @@ impl Explorer { render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1) } }; - surface.set_stringn( - list_area.x.saturating_sub(1), - list_area.y, - " Explorer: press ? for help", - list_area.width.into(), - cx.editor.theme.get("ui.text"), - ); - self.tree - .render(list_area.clip_top(1), surface, cx, &self.state.filter); + self.render_tree(list_area, surface, cx); { let statusline = if self.is_focus() { @@ -568,15 +571,6 @@ impl Explorer { ExplorerPositionEmbed::Right => area.clip_left(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() { @@ -616,28 +610,27 @@ impl Explorer { 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(); - let filter = self.state.filter.clone(); - return self - .tree - .handle_event(Event::Key(event), cx, &mut self.state, &filter); - } - key!(Enter) => { - if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { - self.tree.filter(prompt.line()); + (|| -> Result<()> { + match event.into() { + key!(Enter) => { + if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { + self.tree.refresh(prompt.line())?; + } } - } - key!(Esc) | ctrl!('c') => self.tree.restore_recycle(), - _ => { - if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { - self.tree.filter(prompt.line()); + key!(Esc) | ctrl!('c') => { + self.state.filter.clear(); } - self.state.filter = prompt.line().clone(); - self.prompt = Some((action, prompt)); - } - }; + _ => { + if let EventResult::Consumed(_) = prompt.handle_event(Event::Key(event), cx) { + self.tree.refresh(prompt.line())?; + } + self.state.filter = prompt.line().clone(); + self.prompt = Some((action, prompt)); + } + }; + Ok(()) + })() + .unwrap_or_else(|err| cx.editor.set_error(format!("{err}"))); EventResult::Consumed(None) } @@ -673,9 +666,6 @@ impl Explorer { } 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(), _ => { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 229b92bd..f7f48091 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, cmp::Ordering}; +use std::cmp::Ordering; use anyhow::Result; use helix_view::theme::Modifier; @@ -139,7 +139,7 @@ impl Tree { .iter() .filter_map(|tree| Self::filter(tree, predicate)) .collect::>(); - if tree.item.is_parent() || predicate(&tree.item) || !children.is_empty() { + if tree.item.is_parent() || predicate(&tree.item) { let mut tree = Tree { item: tree.item.clone(), parent_index: tree.parent_index, @@ -178,7 +178,9 @@ impl Tree { self.item .get_children()? .into_iter() - .filter(|item| item.name().to_lowercase().contains(&filter.to_lowercase())) + .filter(|item| { + item.is_parent() || item.name().to_lowercase().contains(&filter.to_lowercase()) + }) .collect(), ); let filtered = std::mem::replace(&mut self.children, vec![]) @@ -267,15 +269,6 @@ impl Tree { } else { self.children.iter().find_map(|elem| elem.get(index)) } - // self.traverse(None, &|result, current_index, tree| { - // result.or_else(|| { - // if index == current_index { - // Some(tree) - // } else { - // None - // } - // }) - // }) } fn traverse<'a, U, F>(&'a self, init: U, f: &F) -> U where @@ -353,7 +346,6 @@ impl Tree { pub struct TreeView { tree: Tree, - recycle: Option<(String, Tree)>, /// Selected item idex selected: usize, @@ -382,7 +374,6 @@ impl TreeView { pub fn new(root: T, items: Vec>) -> Self { Self { tree: Tree::new(root, items), - recycle: None, selected: 0, save_view: (0, 0), winline: 0, @@ -938,45 +929,6 @@ impl TreeView { } } -impl TreeView { - pub fn filter(&mut self, s: &str) { - if s.is_empty() { - self.restore_recycle(); - return; - } - - let new_tree = Tree::filter(&self.tree, &|item: &T| { - item.name().to_lowercase().contains(&s.to_lowercase()) - }) - .unwrap_or_else(|| Tree { - item: self.tree.item.clone(), - children: vec![], - ..self.tree.clone() - }); - let recycle = std::mem::replace(&mut self.tree, new_tree); - if let Some(r) = self.recycle.as_mut() { - r.0 = s.into() - } else { - self.recycle = Some((s.into(), recycle)); - self.save_view(); - } - - self.selected = 0; - self.winline = 0 - } - - pub fn clean_recycle(&mut self) { - self.recycle = None; - } - - pub fn restore_recycle(&mut self) { - if let Some((_, recycle)) = self.recycle.take() { - self.tree = recycle; - } - self.restore_view(); - } -} - /// Recalculate the index of each item of a tree. /// /// For example: -- 2.38.5 From 72495363f1de906401b9bf9fc8475237ed02a91e Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Feb 2023 19:29:15 +0800 Subject: [PATCH 025/191] fix(explore): 'h' does not realign preview properly --- helix-term/src/ui/tree.rs | 74 ++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index f7f48091..23d6a95c 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -454,15 +454,17 @@ impl TreeView { // Locate the item self.regenerate_index(); - self.selected = segments - .iter() - .fold(&self.tree, |tree, segment| { - tree.children - .iter() - .find(|tree| tree.item.name().eq(segment)) - .expect("Should be unreachable") - }) - .index; + self.set_selected( + segments + .iter() + .fold(&self.tree, |tree, segment| { + tree.children + .iter() + .find(|tree| tree.item.name().eq(segment)) + .expect("Should be unreachable") + }) + .index, + ); self.align_view_center(); Ok(()) @@ -486,7 +488,7 @@ impl TreeView { fn go_to_parent(&mut self) { if let Some(parent) = self.current_parent() { - self.selected = parent.index + self.set_selected(parent.index) } } @@ -587,20 +589,22 @@ impl TreeView { pub fn search_next(&mut self, s: &str) { let skip = std::cmp::max(2, self.save_view.0 + 1); - self.selected = self - .tree - .find(skip, Direction::Forward, |e| e.item.filter(s)) - .unwrap_or(self.save_view.0); + self.set_selected( + self.tree + .find(skip, Direction::Forward, |e| e.item.filter(s)) + .unwrap_or(self.save_view.0), + ); self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); } pub fn search_previous(&mut self, s: &str) { let take = self.save_view.0; - self.selected = self - .tree - .find(take, Direction::Backward, |e| e.item.filter(s)) - .unwrap_or(self.save_view.0); + self.set_selected( + self.tree + .find(take, Direction::Backward, |e| e.item.filter(s)) + .unwrap_or(self.save_view.0), + ); self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); } @@ -608,16 +612,33 @@ impl TreeView { pub fn move_down(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { - self.selected = std::cmp::min(self.selected + rows, len.saturating_sub(1)); - self.winline = std::cmp::min(self.selected, self.winline + rows); + self.set_selected(std::cmp::min(self.selected + rows, len.saturating_sub(1))) } } + fn set_selected(&mut self, selected: usize) { + if selected > self.selected { + // Move down + self.winline = std::cmp::min( + selected, + self.winline + .saturating_add(selected.saturating_sub(self.selected)), + ); + } else { + // Move up + self.winline = std::cmp::min( + selected, + self.winline + .saturating_sub(self.selected.saturating_sub(selected)), + ); + } + self.selected = selected; + } + pub fn move_up(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { - self.selected = std::cmp::max(0, self.selected.saturating_sub(rows)); - self.winline = std::cmp::min(self.selected, self.winline.saturating_sub(rows)); + self.set_selected(std::cmp::max(0, self.selected.saturating_sub(rows))) } } @@ -712,10 +733,6 @@ impl TreeView { self.current_mut().item = item } - pub fn set_selected(&mut self, selected: usize) { - self.selected = selected - } - pub fn add_child(&mut self, index: usize, item: T, filter: &String) -> Result<()> { match self.tree.get_mut(index) { None => Err(anyhow::anyhow!(format!( @@ -732,7 +749,7 @@ impl TreeView { .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)); self.regenerate_index(); - let tree = self.get_mut(index); + let tree = self.get(index); // Focus the added sibling if let Some(tree) = tree @@ -740,7 +757,8 @@ impl TreeView { .iter() .find(|tree| tree.item.name().eq(&item_name)) { - self.selected = tree.index + let index = tree.index; + self.set_selected(index) }; Ok(()) } -- 2.38.5 From 94e2c2989b6fc664754b849bb53a56dc324daf73 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 15 Feb 2023 09:23:38 +0800 Subject: [PATCH 026/191] fix(command): space e does not focus explorer when no files are opened --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9de52928..d0776822 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2241,8 +2241,8 @@ fn reveal_current_file(cx: &mut Context) { None => { editor.explorer = Some(overlayed(ui::Explorer::new(cx)?)); let explorer = editor.explorer.as_mut().unwrap(); - explorer.content.reveal_current_file(cx)?; explorer.content.focus(); + explorer.content.reveal_current_file(cx)?; Ok(()) } })() -- 2.38.5 From 374b8ddd4e43d008c361348c307367c678da5963 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 15 Feb 2023 09:25:01 +0800 Subject: [PATCH 027/191] style(explore): make Right the default position Refer https://twitter.com/JustinWGrote/status/1346575528560455682 --- helix-view/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3968c002..56b6d998 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -157,7 +157,7 @@ impl ExplorerConfig { impl Default for ExplorerConfig { fn default() -> Self { Self { - position: ExplorerPosition::Left, + position: ExplorerPosition::Right, column_width: 30, } } -- 2.38.5 From c8578ba3ccdde295adf92dfbae00cf0051ad3aea Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 15 Feb 2023 09:26:36 +0800 Subject: [PATCH 028/191] fix: warnings --- helix-term/src/ui/explore.rs | 3 ++- helix-term/src/ui/tree.rs | 29 ++--------------------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index d2bb1b5b..96a064bf 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -863,7 +863,8 @@ impl Component for Explorer { } key!('[') => { if let Some(parent) = self.state.current_root.parent().clone() { - self.change_root(cx, parent.to_path_buf()) + let path = parent.to_path_buf(); + self.change_root(cx, path) } } key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 23d6a95c..35929a82 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -270,32 +270,6 @@ impl Tree { self.children.iter().find_map(|elem| elem.get(index)) } } - fn traverse<'a, U, F>(&'a self, init: U, f: &F) -> U - where - F: Fn(U, usize, &'a Tree) -> U, - { - fn traverse<'a, T, U, F>( - tree: &'a Tree, - current_index: usize, - init: U, - f: &F, - ) -> (usize, U) - where - F: Fn(U, usize, &'a Tree) -> U, - { - let mut result = f(init, current_index, &tree); - let mut current_index = current_index; - for tree in &tree.children { - (current_index, result) = traverse(tree, current_index + 1, result, f) - } - (current_index, result) - // tree.children.iter().fold( - // (current_index, f(init, 0, &tree)), - // |(current_index, result), tree| traverse(tree, current_index + 1, result, &f), - // ) - } - traverse(self, 0, init, f).1 - } fn get_mut(&mut self, index: usize) -> Option<&mut Tree> { if self.index == index { @@ -488,7 +462,8 @@ impl TreeView { fn go_to_parent(&mut self) { if let Some(parent) = self.current_parent() { - self.set_selected(parent.index) + let index = parent.index; + self.set_selected(index) } } -- 2.38.5 From 30bac647ef5279c9f7e2f8721291ad84a2073316 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 15 Feb 2023 09:31:10 +0800 Subject: [PATCH 029/191] Revert "style(explore): make Right the default position" This reverts commit 374b8ddd4e43d008c361348c307367c678da5963. --- helix-view/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 56b6d998..3968c002 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -157,7 +157,7 @@ impl ExplorerConfig { impl Default for ExplorerConfig { fn default() -> Self { Self { - position: ExplorerPosition::Right, + position: ExplorerPosition::Left, column_width: 30, } } -- 2.38.5 From ef73559a8e0a9ad222ed1e459ddbd5bb505e32ca Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 15 Feb 2023 09:58:54 +0800 Subject: [PATCH 030/191] fix(explore): cannot focus explorer if no opened document --- helix-term/src/commands.rs | 4 +- helix-term/src/ui/explore.rs | 3 +- helix-term/src/ui/tree.rs | 194 +++++------------------------------ 3 files changed, 26 insertions(+), 175 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d0776822..102940f7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2241,9 +2241,7 @@ fn reveal_current_file(cx: &mut Context) { None => { editor.explorer = Some(overlayed(ui::Explorer::new(cx)?)); let explorer = editor.explorer.as_mut().unwrap(); - explorer.content.focus(); - explorer.content.reveal_current_file(cx)?; - Ok(()) + explorer.content.reveal_current_file(cx) } })() .unwrap_or_else(|err| { diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 96a064bf..a442f023 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -233,11 +233,11 @@ impl Explorer { .split(std::path::MAIN_SEPARATOR) .collect::>(); self.tree.reveal_item(segments, &self.state.filter)?; - self.focus(); Ok(()) } pub fn reveal_current_file(&mut self, cx: &mut Context) -> Result<()> { + self.focus(); let current_document_path = doc!(cx.editor).path().cloned(); match current_document_path { None => Ok(()), @@ -717,7 +717,6 @@ impl Explorer { if line == "y" { let item = explorer.tree.current_item(); std::fs::remove_dir_all(&item.path)?; - explorer.tree.fold_current_child(); explorer.tree.remove_current(); } } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 35929a82..117ff1f1 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -129,60 +129,25 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} -impl Tree { - pub fn filter

(tree: &Tree, predicate: &P) -> Option> - where - P: Fn(&T) -> bool, - { - let children = tree - .children - .iter() - .filter_map(|tree| Self::filter(tree, predicate)) - .collect::>(); - if tree.item.is_parent() || predicate(&tree.item) { - let mut tree = Tree { - item: tree.item.clone(), - parent_index: tree.parent_index, - index: tree.index, - is_opened: tree.is_opened, - children, - }; - tree.regenerate_index(); - Some(tree) - } else { - None - } - } -} - impl Tree { fn open(&mut self, filter: &String) -> Result<()> { if self.item.is_parent() { - self.children = vec_to_tree( - self.item - .get_children()? - .into_iter() - .filter(|item| item.name().to_lowercase().contains(&filter.to_lowercase())) - .collect(), - ); + self.children = self.get_filtered_children(filter)?; self.is_opened = true; } Ok(()) } + fn close(&mut self) { + self.is_opened = false; + self.children = vec![]; + } + fn refresh(&mut self, filter: &String) -> Result<()> { if !self.is_opened { return Ok(()); } - let latest_children = vec_to_tree( - self.item - .get_children()? - .into_iter() - .filter(|item| { - item.is_parent() || item.name().to_lowercase().contains(&filter.to_lowercase()) - }) - .collect(), - ); + let latest_children = self.get_filtered_children(filter)?; let filtered = std::mem::replace(&mut self.children, vec![]) .into_iter() // Remove children that does not exists in latest_children @@ -216,6 +181,18 @@ impl Tree { Ok(()) } + fn get_filtered_children(&self, filter: &String) -> Result>> { + Ok(vec_to_tree( + self.item + .get_children()? + .into_iter() + .filter(|item| { + item.is_parent() || item.name().to_lowercase().contains(&filter.to_lowercase()) + }) + .collect(), + )) + } + fn sort(&mut self) { self.children .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)) @@ -514,23 +491,9 @@ impl TreeView { selected_index: usize, filter: &String, ) { - // if let Some(next_level) = self.next_item().map(|elem| elem.level) { - // let current = self.find_by_index(selected_index); - // 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; - // } - // } - // - let mut selected_item = self.get_mut(selected_index); + let selected_item = self.get_mut(selected_index); if selected_item.is_opened { - selected_item.is_opened = false; - selected_item.children = vec![]; + selected_item.close(); self.regenerate_index(); return; } @@ -553,15 +516,6 @@ impl TreeView { } } - pub fn fold_current_child(&mut self) { - if let Some(parent) = self.current_parent_mut() { - parent.is_opened = false; - parent.children = vec![]; - self.selected = parent.index; - self.regenerate_index() - } - } - pub fn search_next(&mut self, s: &str) { let skip = std::cmp::max(2, self.save_view.0 + 1); self.set_selected( @@ -684,14 +638,6 @@ impl TreeView { } } - fn current_parent_mut(&mut self) -> Option<&mut Tree> { - if let Some(parent_index) = self.current().parent_index { - Some(self.get_mut(parent_index)) - } else { - None - } - } - pub fn current_item(&self) -> &T { &self.current().item } @@ -718,10 +664,10 @@ impl TreeView { let item_name = item.name(); if !tree.is_opened { tree.open(filter)?; + } else { + tree.refresh(filter)?; } - tree.children.push(Tree::new(item, vec![])); - tree.children - .sort_by(|a, b| tree_item_cmp(&a.item, &b.item)); + self.regenerate_index(); let tree = self.get(index); @@ -893,7 +839,6 @@ impl TreeView { key!('j') | key!(Tab) | key!(Down) | ctrl!('j') => self.move_down(1.max(count)), key!('z') => { self.on_next_key = Some(Box::new(|_, tree, event| match event.into() { - key!('f') => tree.fold_current_child(), key!('z') => tree.align_view_center(), key!('t') => tree.align_view_top(), key!('b') => tree.align_view_bottom(), @@ -965,8 +910,6 @@ fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { mod test_tree { use helix_core::movement::Direction; - use crate::ui::TreeItem; - use super::Tree; #[test] @@ -1118,95 +1061,6 @@ mod test_tree { assert_eq!(result, Some(3)); } - #[test] - fn test_filter() { - #[derive(Clone, Debug, PartialEq, Eq)] - struct MyItem<'a>(bool, &'a str); - impl<'a> TreeItem for MyItem<'a> { - type Params = (); - fn name(&self) -> String { - self.0.to_string() - } - fn is_child(&self, _: &Self) -> bool { - !self.0 - } - fn is_parent(&self) -> bool { - self.0 - } - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.1.cmp(other.1) - } - } - let tree = Tree::new( - MyItem(false, ".cargo"), - vec![ - Tree::new( - MyItem(true, "spam"), - vec![Tree::new(MyItem(false, "Cargo.toml"), vec![])], - ), - Tree::new( - MyItem(true, "Cargo.toml"), - vec![Tree::new(MyItem(false, "pam"), vec![])], - ), - Tree::new(MyItem(false, "hello"), vec![]), - ], - ); - - let result = Tree::filter(&tree, &|item| item.1.to_lowercase().contains("cargo")); - assert_eq!( - result, - Some(Tree::new( - MyItem(false, ".cargo"), - vec![ - Tree::new( - MyItem(true, "spam"), - vec![Tree::new(MyItem(false, "Cargo.toml"), vec![])] - ), - Tree { - is_opened: true, - ..Tree::new(MyItem(true, "Cargo.toml"), vec![]) - }, - ], - )) - ); - - let result = Tree::filter(&tree, &|item| item.1.to_lowercase().contains("pam")); - assert_eq!( - result, - Some(Tree::new( - MyItem(false, ".cargo"), - vec![ - Tree { - is_opened: true, - ..Tree::new(MyItem(true, "spam"), vec![]) - }, - Tree::new( - MyItem(true, "Cargo.toml"), - vec![Tree::new(MyItem(false, "pam"), vec![])] - ), - ], - )) - ); - - let result = Tree::filter(&tree, &|item| item.1.to_lowercase().contains("helix")); - assert_eq!( - result, - Some(Tree::new( - MyItem(false, ".cargo"), - vec![ - Tree { - is_opened: true, - ..Tree::new(MyItem(true, "spam"), vec![]) - }, - Tree { - is_opened: true, - ..Tree::new(MyItem(true, "Cargo.toml"), vec![]) - } - ], - )) - ) - } - #[test] fn test_remove() { let mut tree = Tree::new( -- 2.38.5 From 0f8e0a51ba4854d3f7a9d112ff0cd23bfe1adacb Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 15 Feb 2023 10:08:12 +0800 Subject: [PATCH 031/191] fix(tree): deleting last file causes panic --- helix-term/src/ui/tree.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 117ff1f1..8c6458ca 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -548,15 +548,13 @@ impl TreeView { fn set_selected(&mut self, selected: usize) { if selected > self.selected { // Move down - self.winline = std::cmp::min( - selected, + self.winline = selected.min( self.winline .saturating_add(selected.saturating_sub(self.selected)), ); } else { // Move up - self.winline = std::cmp::min( - selected, + self.winline = selected.min( self.winline .saturating_sub(self.selected.saturating_sub(selected)), ); @@ -567,7 +565,7 @@ impl TreeView { pub fn move_up(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { - self.set_selected(std::cmp::max(0, self.selected.saturating_sub(rows))) + self.set_selected(self.selected.saturating_sub(rows).max(0)) } } @@ -647,7 +645,8 @@ impl TreeView { } pub fn remove_current(&mut self) { - self.tree.remove(self.selected) + self.tree.remove(self.selected); + self.set_selected(self.selected.min(self.tree.len().saturating_sub(1))); } pub fn replace_current(&mut self, item: T) { @@ -695,7 +694,7 @@ impl TreeView { self.max_len = 0; self.area_height = area.height.saturating_sub(1) as usize; - self.winline = std::cmp::min(self.winline, self.area_height); + self.winline = self.winline.min(self.area_height); let style = cx.editor.theme.get(&self.tree_symbol_style); let ancestor_style = cx.editor.theme.get("ui.text.focus"); let skip = self.selected.saturating_sub(self.winline); -- 2.38.5 From 4dfa8696bdd75de403cbfa74b230b3db7ebe7698 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 16 Feb 2023 09:28:41 +0800 Subject: [PATCH 032/191] style(tree): increase indentation --- helix-term/src/ui/explore.rs | 4 +-- helix-term/src/ui/tree.rs | 54 +++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 39cbc212..af431e23 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -506,14 +506,14 @@ impl Explorer { surface.set_stringn( area.x, area.y, - " Explorer: press ? for help", + "Explorer: press ? for help", area.width.into(), cx.editor.theme.get("ui.text"), ); surface.set_stringn( area.x, area.y.saturating_add(1), - format!(" [FILTER]: {}", self.state.filter), + format!("[FILTER]: {}", self.state.filter), area.width.into(), cx.editor.theme.get("ui.text"), ); diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 18275caf..acae40db 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -702,7 +702,6 @@ impl TreeView { let params = RenderElemParams { tree: &self.tree, prefix: &"".to_string(), - is_last: true, level: 0, selected: self.selected, filter, @@ -726,8 +725,7 @@ impl TreeView { struct RenderElemParams<'a, T> { tree: &'a Tree, prefix: &'a String, - is_last: bool, - level: u16, + level: usize, selected: usize, filter: &'a str, } @@ -736,20 +734,25 @@ impl TreeView { RenderElemParams { tree, prefix, - is_last, level, selected, filter, }: RenderElemParams, ) -> Vec<(Indent, Node)> { let indent = if level > 0 { - let bar = if is_last { "└" } else { "├" }; - let branch = if tree.is_opened { "┬" } else { "─" }; - format!("{}{}{}", prefix, bar, branch) + let indicator = if tree.item().is_parent() { + if tree.is_opened { + "" + } else { + "" + } + } else { + " " + }; + format!("{}{}", prefix, indicator) } else { "".to_string() }; - let folded_length = tree.children.len(); let head = ( Indent(indent), Node { @@ -766,25 +769,18 @@ impl TreeView { ), }, ); - let prefix = format!("{}{}", prefix, if is_last { " " } else { "│" }); + let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); vec![head] .into_iter() - .chain( - tree.children - .iter() - .enumerate() - .flat_map(|(local_index, elem)| { - let is_last = local_index == folded_length - 1; - render_tree(RenderElemParams { - tree: elem, - prefix: &prefix, - is_last, - level: level + 1, - selected, - filter, - }) - }), - ) + .chain(tree.children.iter().flat_map(|elem| { + render_tree(RenderElemParams { + tree: elem, + prefix: &prefix, + level: level + 1, + selected, + filter, + }) + })) .collect() } @@ -798,8 +794,14 @@ impl TreeView { } else { style }; + let x = area.x.saturating_add(indent_len); + let x = if indent_len > 0 { + x.saturating_add(1) + } else { + x + }; surface.set_stringn( - area.x.saturating_add(indent_len).saturating_add(1), + x, area.y, node.name.clone(), area.width -- 2.38.5 From c88164f2fa8325f9199df5868bc88c3401f364da Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 16 Feb 2023 11:54:53 +0800 Subject: [PATCH 033/191] feat(tree-view): add unit tests --- Cargo.lock | 44 ++ changes | 17 +- helix-term/Cargo.toml | 1 + helix-term/src/ui/explore.rs | 8 +- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/tree.rs | 755 +++++++++++++++++++++++++++-------- 6 files changed, 655 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b70f34c4..4d722d6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "cxx" version = "1.0.82" @@ -342,6 +352,12 @@ dependencies = [ "parking_lot_core 0.9.4", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dirs" version = "4.0.0" @@ -1217,6 +1233,7 @@ dependencies = [ "indoc", "log", "once_cell", + "pretty_assertions", "pulldown-cmark", "serde", "serde_json", @@ -1609,6 +1626,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1675,6 +1701,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.47" @@ -2553,3 +2591,9 @@ dependencies = [ "helix-view", "toml", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/changes b/changes index a49fafae..a436eafd 100644 --- a/changes +++ b/changes @@ -23,9 +23,18 @@ TODO - [x] bug: "h" does not realign preview - [x] bug: reveal file does not realign preview - [] "l" goes back to previous child if any history -- [] refactor, add tree.expand_children() method +- [x] Merge conflicts +- [x] Remove comments +- [x] fix warnings +- [x] refactor, add tree.expand_children() method +- [] add integration testing (test explorer rendering) - [] search highlight matching word -- [] fix warnings - [] Error didn't clear -- [] Remove comments -- [] Merge conflicts +- [] bind "o" to open/close file/folder +- [] on focus indication +- [] should preview be there by default? +- [] support creating files and folder and the same time (`mkdir -p`) +- [] Fix panic bugs (see github comments) +- [] Sticky ancestors +- [] Ctrl-o should work for 'h', 'gg', 'ge', etc +- [] explorer(preview): content not sorted diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 603f37d3..4204e4dc 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -80,3 +80,4 @@ helix-loader = { version = "0.6", path = "../helix-loader" } smallvec = "1.10" indoc = "2.0.0" tempfile = "3.3.0" +pretty_assertions = "1.3.0" diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index af431e23..b7691522 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -1,4 +1,4 @@ -use super::{Prompt, TreeItem, TreeOp, TreeView}; +use super::{Prompt, TreeOp, TreeView, TreeViewItem}; use crate::{ compositor::{Component, Context, EventResult}, ctrl, key, shift, ui, @@ -61,13 +61,9 @@ impl FileInfo { } } -impl TreeItem for FileInfo { +impl TreeViewItem for FileInfo { type Params = State; - fn is_child(&self, other: &Self) -> bool { - 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) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3f962dd5..0dba41e1 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,7 +29,7 @@ pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; -pub use tree::{TreeItem, TreeOp, TreeView}; +pub use tree::{TreeViewItem, TreeOp, TreeView}; 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 index acae40db..8dcb197f 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -14,12 +14,11 @@ use helix_view::{ }; use tui::buffer::Buffer as Surface; -pub trait TreeItem: Sized { +pub trait TreeViewItem: Sized { type Params; // fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; fn name(&self) -> String; - fn is_child(&self, other: &Self) -> bool; fn is_parent(&self) -> bool; fn cmp(&self, other: &Self) -> Ordering; @@ -27,23 +26,14 @@ pub trait TreeItem: Sized { self.name().to_lowercase().contains(&s.to_lowercase()) } - fn get_children(&self) -> Result> { - Ok(vec![]) - } + fn get_children(&self) -> Result>; } -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; - } - +fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { T::cmp(item1, item2) } -fn vec_to_tree(mut items: Vec) -> Vec> { +pub fn vec_to_tree(mut items: Vec) -> Vec> { items.sort_by(tree_item_cmp); index_elems( 0, @@ -129,7 +119,7 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} -impl Tree { +impl Tree { fn open(&mut self, filter: &String) -> Result<()> { if self.item.is_parent() { self.children = self.get_filtered_children(filter)?; @@ -295,7 +285,7 @@ impl Tree { } } -pub struct TreeView { +pub struct TreeView { tree: Tree, /// Selected item idex selected: usize, @@ -306,14 +296,12 @@ pub struct TreeView { /// View row winline: usize, - area_height: usize, - col: usize, + previous_area: Rect, + column: 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>, @@ -321,19 +309,18 @@ pub struct TreeView { on_next_key: Option>, } -impl TreeView { +impl TreeView { pub fn new(root: T, items: Vec>) -> Self { Self { tree: Tree::new(root, items), selected: 0, save_view: (0, 0), winline: 0, - col: 0, + column: 0, max_len: 0, count: 0, - area_height: 0, + previous_area: Rect::new(0, 0, 0, 0), tree_symbol_style: "ui.text".into(), - pre_render: None, on_opened_fn: None, on_folded_fn: None, on_next_key: None, @@ -422,7 +409,7 @@ impl TreeView { } fn align_view_center(&mut self) { - self.winline = self.area_height / 2 + self.winline = self.previous_area.height as usize / 2 } fn align_view_top(&mut self) { @@ -430,7 +417,7 @@ impl TreeView { } fn align_view_bottom(&mut self) { - self.winline = self.area_height + self.winline = self.previous_area.height as usize } fn regenerate_index(&mut self) { @@ -447,12 +434,12 @@ impl TreeView { fn go_to_children(&mut self, filter: &String) -> Result<()> { let current = self.current_mut(); if current.is_opened { - self.selected += 1; + self.set_selected(self.selected + 1); Ok(()) } else { current.open(filter)?; if !current.children.is_empty() { - self.selected += 1; + self.set_selected(self.selected + 1); self.regenerate_index(); } Ok(()) @@ -462,19 +449,33 @@ impl TreeView { pub fn refresh(&mut self, filter: &String) -> Result<()> { self.tree.refresh(filter) } + + fn go_to_first(&mut self) { + self.move_up(usize::MAX / 2) + } + + fn go_to_last(&mut self) { + self.move_down(usize::MAX / 2) + } + + fn set_previous_area(&mut self, area: Rect) { + self.previous_area = area + } } pub fn tree_view_help() -> Vec { vec![ - "j Down", - "k Up", - "h Go to parent", - "l Expand", + "j/↓ Down", + "k/↑ Up", + "h/← Go to parent", + "l/→ Expand", + "L Scroll right", + "H Scroll left", "zz Align view center", "zt Align view top", "zb Align view bottom", - "gg Go to top", - "ge Go to end", + "gg Go to first", + "ge Go to last", "^d Page down", "^u Page up", ] @@ -483,7 +484,7 @@ pub fn tree_view_help() -> Vec { .collect() } -impl TreeView { +impl TreeView { pub fn on_enter( &mut self, cx: &mut Context, @@ -570,38 +571,30 @@ impl TreeView { } pub fn move_left(&mut self, cols: usize) { - self.col = self.col.saturating_sub(cols); + self.column = self.column.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); - })); + let max_scroll = self + .max_len + .saturating_sub(self.previous_area.width as usize); + self.column = max_scroll.min(self.column + 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); - })); + self.move_down(self.previous_area.height as usize / 2) } 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); - })); + self.move_up(self.previous_area.height as usize / 2); } 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); - })); + self.move_down(self.previous_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); - })); + self.move_up(self.previous_area.height as usize); } pub fn save_view(&mut self) { @@ -686,129 +679,108 @@ impl TreeView { } } -impl TreeView { - pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { - if let Some(pre_render) = self.pre_render.take() { - pre_render(self, area); - } +struct RenderedLine { + indent: String, + name: String, + selected: bool, + descendant_selected: bool, +} +struct RenderTreeParams<'a, T> { + tree: &'a Tree, + prefix: &'a String, + level: usize, + selected: usize, + filter: &'a str, + column_start: usize, + max_width: usize, +} + +fn render_tree( + RenderTreeParams { + tree, + prefix, + level, + selected, + filter, + column_start, + max_width, + }: RenderTreeParams, +) -> Vec { + let indent = if level > 0 { + let indicator = if tree.item().is_parent() { + if tree.is_opened { + "" + } else { + "" + } + } else { + " " + }; + format!("{}{} ", prefix, indicator) + } else { + "".to_string() + }; + let indent = indent[column_start..].to_string(); + let indent_len = indent.len(); + let name = tree.item.name(); + println!("{max_width}"); + let head = RenderedLine { + indent, + selected: selected == tree.index, + descendant_selected: selected != tree.index && tree.get(selected).is_some(), + name: name[..(max_width - indent_len).clamp(0, name.len())].to_string(), + }; + let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); + vec![head] + .into_iter() + .chain(tree.children.iter().flat_map(|elem| { + render_tree(RenderTreeParams { + tree: elem, + prefix: &prefix, + level: level + 1, + selected, + filter, + column_start, + max_width, + }) + })) + .collect() +} +impl TreeView { + pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { self.max_len = 0; - self.area_height = area.height.saturating_sub(1) as usize; - self.winline = self.winline.min(self.area_height); let style = cx.editor.theme.get(&self.tree_symbol_style); let ancestor_style = cx.editor.theme.get("ui.text.focus"); - let skip = self.selected.saturating_sub(self.winline); - let params = RenderElemParams { - tree: &self.tree, - prefix: &"".to_string(), - level: 0, - selected: self.selected, - filter, - }; - - let rendered = render_tree(params); - - let iter = rendered - .iter() - .skip(skip) - .take(area.height as usize) - .enumerate(); + let iter = self.render_lines(area, filter).into_iter().enumerate(); - struct Indent(String); - struct Node { - name: String, - selected: bool, - descendant_selected: bool, - } - - struct RenderElemParams<'a, T> { - tree: &'a Tree, - prefix: &'a String, - level: usize, - selected: usize, - filter: &'a str, - } - - fn render_tree( - RenderElemParams { - tree, - prefix, - level, - selected, - filter, - }: RenderElemParams, - ) -> Vec<(Indent, Node)> { - let indent = if level > 0 { - let indicator = if tree.item().is_parent() { - if tree.is_opened { - "" - } else { - "" - } - } else { - " " - }; - format!("{}{}", prefix, indicator) - } else { - "".to_string() - }; - let head = ( - Indent(indent), - Node { - selected: selected == tree.index, - descendant_selected: selected != tree.index && tree.get(selected).is_some(), - name: format!( - "{}{}", - tree.item.name(), - if tree.item.is_parent() { - format!("{}", std::path::MAIN_SEPARATOR) - } else { - "".to_string() - } - ), - }, - ); - let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); - vec![head] - .into_iter() - .chain(tree.children.iter().flat_map(|elem| { - render_tree(RenderElemParams { - tree: elem, - prefix: &prefix, - level: level + 1, - selected, - filter, - }) - })) - .collect() - } - - for (index, (indent, node)) in iter { + for (index, line) in iter { let area = Rect::new(area.x, area.y + index as u16, area.width, 1); - let indent_len = indent.0.chars().count() as u16; - surface.set_stringn(area.x, area.y, indent.0.clone(), indent_len as usize, style); + let indent_len = line.indent.chars().count() as u16; + surface.set_stringn( + area.x, + area.y, + line.indent.clone(), + indent_len as usize, + style, + ); - let style = if node.selected { + let style = if line.selected { style.add_modifier(Modifier::REVERSED) } else { style }; let x = area.x.saturating_add(indent_len); - let x = if indent_len > 0 { - x.saturating_add(1) - } else { - x - }; surface.set_stringn( x, area.y, - node.name.clone(), + line.name.clone(), area.width .saturating_sub(indent_len) .saturating_sub(1) .into(), - if node.descendant_selected { + if line.descendant_selected { ancestor_style } else { style @@ -817,6 +789,50 @@ impl TreeView { } } + fn render_to_string(&mut self, filter: &String) -> String { + let area = self.previous_area; + let lines = self.render_lines(area, filter); + lines + .into_iter() + .map(|line| { + let name = if line.selected { + format!("({})", line.name) + } else if line.descendant_selected { + format!("[{}]", line.name) + } else { + line.name + }; + format!("{}{}", line.indent, name) + }) + .collect::>() + .join("\n") + } + + fn render_lines(&mut self, area: Rect, filter: &String) -> Vec { + self.previous_area = area; + self.winline = self + .winline + .min(self.previous_area.height.saturating_sub(1) as usize); + let skip = self.selected.saturating_sub(self.winline); + let params = RenderTreeParams { + tree: &self.tree, + prefix: &"".to_string(), + level: 0, + selected: self.selected, + filter, + column_start: self.column, + max_width: self.previous_area.width as usize, + }; + + let lines = render_tree(params); + + lines + .into_iter() + .skip(skip) + .take(area.height as usize) + .collect() + } + pub fn handle_event( &mut self, event: &Event, @@ -836,8 +852,8 @@ impl TreeView { let count = std::mem::replace(&mut self.count, 0); match key_event { 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!('k') | key!(Up) => self.move_up(1.max(count)), + key!('j') | key!(Down) => self.move_down(1.max(count)), key!('z') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { key!('z') => tree.align_view_center(), @@ -846,18 +862,20 @@ impl TreeView { _ => {} })); } - key!('h') => self.go_to_parent(), - key!('l') => match self.go_to_children(filter) { + key!('h') | key!(Left) => self.go_to_parent(), + key!('l') | key!(Right) => match self.go_to_children(filter) { Ok(_) => {} Err(err) => cx.editor.set_error(err.to_string()), }, + shift!('H') => self.move_left(1), + shift!('L') => self.move_right(1), key!(Enter) => self.on_enter(cx, params, self.selected, filter), ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('g') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.move_up(usize::MAX / 2), - key!('e') => tree.move_down(usize::MAX / 2), + key!('g') => tree.go_to_first(), + key!('e') => tree.go_to_last(), _ => {} })); } @@ -907,6 +925,421 @@ fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { index_elems(parent_index + 1, elems, parent_index).1 } +#[cfg(test)] +mod test_tree_view { + use helix_view::graphics::Rect; + + use super::{vec_to_tree, TreeView, TreeViewItem}; + use pretty_assertions::assert_eq; + + #[derive(Clone)] + struct Item<'a> { + name: &'a str, + } + + fn item<'a>(name: &'a str) -> Item<'a> { + Item { name } + } + + impl<'a> TreeViewItem for Item<'a> { + type Params = (); + + fn name(&self) -> String { + self.name.to_string() + } + + fn is_parent(&self) -> bool { + self.name.len() > 2 + } + + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name.cmp(other.name) + } + + fn get_children(&self) -> anyhow::Result> { + if self.is_parent() { + let (left, right) = self.name.split_at(self.name.len() / 2); + Ok(vec![item(left), item(right)]) + } else { + Ok(vec![]) + } + } + } + + fn dummy_tree_view<'a>() -> TreeView> { + let root = item("who_lives_in_a_pineapple_under_the_sea"); + let mut view = TreeView::new( + root, + vec_to_tree(vec![ + item("gary_the_snail"), + item("krabby_patty"), + item("larry_the_lobster"), + item("patrick_star"), + item("sandy_cheeks"), + item("spongebob_squarepants"), + item("mrs_puff"), + item("king_neptune"), + item("karen"), + item("plankton"), + ]), + ); + + view.set_previous_area(dummy_area()); + + view + } + + fn dummy_area() -> Rect { + Rect::new(0, 0, 50, 5) + } + + fn render<'a>(view: &mut TreeView>) -> String { + view.render_to_string(&"".to_string()) + } + + #[test] + fn test_init() { + let mut view = dummy_tree_view(); + + // Expect the items to be sorted + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_move_up_down() { + let mut view = dummy_tree_view(); + view.move_down(1); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + (gary_the_snail) + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_down(3); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) +" + .trim() + ); + + view.move_down(1); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.move_up(1); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + (krabby_patty) + larry_the_lobster +" + .trim() + ); + + view.move_up(3); + assert_eq!( + render(&mut view), + " + (gary_the_snail) + karen + king_neptune + krabby_patty + larry_the_lobster +" + .trim() + ); + + view.move_up(1); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_align_view() { + let mut view = dummy_tree_view(); + view.move_down(5); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.align_view_center(); + assert_eq!( + render(&mut view), + " + king_neptune + krabby_patty + (larry_the_lobster) + mrs_puff + patrick_star +" + .trim() + ); + + view.align_view_bottom(); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + } + + #[test] + fn test_go_to_first_last() { + let mut view = dummy_tree_view(); + + view.go_to_last(); + assert_eq!( + render(&mut view), + " + mrs_puff + patrick_star + plankton + sandy_cheeks + (spongebob_squarepants) +" + .trim() + ); + + view.go_to_first(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_move_half() { + let mut view = dummy_tree_view(); + view.move_down_half_page(); + assert_eq!(view.selected, 2); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + (karen) + king_neptune + krabby_patty +" + .trim() + ); + + view.move_down_half_page(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) +" + .trim() + ); + + view.move_down_half_page(); + assert_eq!( + render(&mut view), + " + karen + king_neptune + krabby_patty + larry_the_lobster + (mrs_puff) +" + .trim() + ); + + view.move_up_half_page(); + assert_eq!( + render(&mut view), + " + karen + king_neptune + (krabby_patty) + larry_the_lobster + mrs_puff +" + .trim() + ); + + view.move_up_half_page(); + assert_eq!( + render(&mut view), + " + (karen) + king_neptune + krabby_patty + larry_the_lobster + mrs_puff +" + .trim() + ); + + view.move_up_half_page(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn go_to_children_parent() { + let filter = "".to_string(); + let mut view = dummy_tree_view(); + view.move_down(1); + view.go_to_children(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + [gary_the_snail] +  (e_snail) +  gary_th + karen + " + .trim() + ); + + view.move_down(1); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + [gary_the_snail] +  e_snail +  (gary_th) + karen + " + .trim() + ); + + view.go_to_parent(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + (gary_the_snail) +  e_snail +  gary_th + karen + " + .trim() + ); + + view.go_to_last(); + view.go_to_parent(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail +  e_snail +  gary_th + karen + " + .trim() + ); + } + + #[test] + fn test_move_left_right() { + let mut view = dummy_tree_view(); + view.set_previous_area(dummy_area().with_width(20)); + + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pinea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_right(1); + assert_eq!( + render(&mut view), + " +" + .trim() + ) + } +} + #[cfg(test)] mod test_tree { use helix_core::movement::Direction; -- 2.38.5 From 64059fba4759a8c05996c79db2a3b21cf44ccf7e Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 19 Feb 2023 18:24:26 +0800 Subject: [PATCH 034/191] feat(tree): move left/right --- helix-term/src/ui/explore.rs | 46 +++--- helix-term/src/ui/tree.rs | 293 +++++++++++++++++++++++++++++------ 2 files changed, 273 insertions(+), 66 deletions(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index b7691522..b887c5f2 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -281,26 +281,36 @@ impl Explorer { let body_area = area.clip_top(2); let style = editor.theme.get("ui.text"); let content = if self.show_help { - vec![ - "? Toggle help", - "a Add file", - "A Add folder", - "r Rename file/folder", - "d Delete file", - "/ Search", - "f Filter", - "[ Change root to parent folder", - "] Change root to current folder", - "^o Go to previous root", - "R Refresh", - "+ Increase size", - "- Decrease size", - "q Close", + let instructions = vec![ + ("?", "Toggle help"), + ("a", "Add file"), + ("A", "Add folder"), + ("r", "Rename file/folder"), + ("d", "Delete file"), + ("/", "Search"), + ("f", "Filter"), + ("[", "Change root to parent folder"), + ("]", "Change root to current folder"), + ("^o", "Go to previous root"), + ("R", "Refresh"), + ("+", "Increase size"), + ("-", "Decrease size"), + ("q", "Close"), ] .into_iter() - .map(|s| s.to_string()) - .chain(ui::tree::tree_view_help()) - .collect() + .chain(ui::tree::tree_view_help().into_iter()) + .collect::>(); + let max_left_length = instructions + .iter() + .map(|(key, _)| key.chars().count()) + .max() + .unwrap_or(0); + instructions + .into_iter() + .map(|(key, description)| { + format!("{:width$}{}", key, description, width = max_left_length + 1) + }) + .collect::>() } else { get_preview(&item.path, body_area.height as usize) .unwrap_or_else(|err| vec![err.to_string()]) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 8dcb197f..0aaabf0b 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -287,17 +287,22 @@ impl Tree { pub struct TreeView { tree: Tree, + /// Selected item idex selected: usize, /// (selected, row) save_view: (usize, usize), - /// View row + /// For implementing vertical scroll winline: usize, previous_area: Rect, + + /// For implementing horizontal scoll column: usize, + + /// For implementing horizontal scoll max_len: usize, count: usize, tree_symbol_style: String, @@ -424,14 +429,14 @@ impl TreeView { self.tree.regenerate_index(); } - fn go_to_parent(&mut self) { + fn move_to_parent(&mut self) { if let Some(parent) = self.current_parent() { let index = parent.index; self.set_selected(index) } } - fn go_to_children(&mut self, filter: &String) -> Result<()> { + fn move_to_children(&mut self, filter: &String) -> Result<()> { let current = self.current_mut(); if current.is_opened { self.set_selected(self.selected + 1); @@ -450,38 +455,45 @@ impl TreeView { self.tree.refresh(filter) } - fn go_to_first(&mut self) { + fn move_to_first(&mut self) { self.move_up(usize::MAX / 2) } - fn go_to_last(&mut self) { + fn move_to_last(&mut self) { self.move_down(usize::MAX / 2) } fn set_previous_area(&mut self, area: Rect) { self.previous_area = area } + + fn move_leftmost(&mut self) { + self.move_left(usize::MAX / 2); + } + + fn move_rightmost(&mut self) { + self.move_right(usize::MAX / 2) + } } -pub fn tree_view_help() -> Vec { +pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { vec![ - "j/↓ Down", - "k/↑ Up", - "h/← Go to parent", - "l/→ Expand", - "L Scroll right", - "H Scroll left", - "zz Align view center", - "zt Align view top", - "zb Align view bottom", - "gg Go to first", - "ge Go to last", - "^d Page down", - "^u Page up", + ("j, down", "Down"), + ("k, up", "Up"), + ("h, left", "Go to parent"), + ("l, right", "Expand"), + ("L", "Scroll right"), + ("H", "Scroll left"), + ("zz", "Align view center"), + ("zt", "Align view top"), + ("zb", "Align view bottom"), + ("gg", "Go to first line"), + ("ge", "Go to last line"), + ("gh", "Go to line start"), + ("gl", "Go to line end"), + ("C-d", "Page down"), + ("C-u", "Page up"), ] - .into_iter() - .map(|s| s.to_string()) - .collect() } impl TreeView { @@ -577,7 +589,8 @@ impl TreeView { pub fn move_right(&mut self, cols: usize) { let max_scroll = self .max_len - .saturating_sub(self.previous_area.width as usize); + .saturating_sub(self.previous_area.width as usize) + .saturating_add(1); self.column = max_scroll.min(self.column + cols); } @@ -691,8 +704,6 @@ struct RenderTreeParams<'a, T> { level: usize, selected: usize, filter: &'a str, - column_start: usize, - max_width: usize, } fn render_tree( @@ -702,8 +713,6 @@ fn render_tree( level, selected, filter, - column_start, - max_width, }: RenderTreeParams, ) -> Vec { let indent = if level > 0 { @@ -720,15 +729,12 @@ fn render_tree( } else { "".to_string() }; - let indent = indent[column_start..].to_string(); - let indent_len = indent.len(); let name = tree.item.name(); - println!("{max_width}"); let head = RenderedLine { indent, selected: selected == tree.index, descendant_selected: selected != tree.index && tree.get(selected).is_some(), - name: name[..(max_width - indent_len).clamp(0, name.len())].to_string(), + name, }; let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); vec![head] @@ -740,8 +746,6 @@ fn render_tree( level: level + 1, selected, filter, - column_start, - max_width, }) })) .collect() @@ -749,7 +753,6 @@ fn render_tree( impl TreeView { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { - self.max_len = 0; let style = cx.editor.theme.get(&self.tree_symbol_style); let ancestor_style = cx.editor.theme.get("ui.text.focus"); @@ -820,14 +823,49 @@ impl TreeView { level: 0, selected: self.selected, filter, - column_start: self.column, - max_width: self.previous_area.width as usize, }; let lines = render_tree(params); + self.max_len = lines + .iter() + .map(|line| { + line.indent + .chars() + .count() + .saturating_add(line.name.chars().count()) + }) + .max() + .unwrap_or(0); + + let max_width = self.previous_area.width as usize; + lines .into_iter() + // Horizontal scroll + .map(|line| { + let skip = self.column; + let indent_len = line.indent.chars().count(); + RenderedLine { + indent: if line.indent.is_empty() { + "".to_string() + } else { + line.indent + .chars() + .skip(skip) + .take(max_width) + .collect::() + }, + name: line + .name + .chars() + .skip(skip.saturating_sub(indent_len)) + .take((max_width.saturating_sub(indent_len)).clamp(0, line.name.len())) + .collect::(), + ..line + } + }) + // Vertical scroll .skip(skip) .take(area.height as usize) .collect() @@ -862,8 +900,8 @@ impl TreeView { _ => {} })); } - key!('h') | key!(Left) => self.go_to_parent(), - key!('l') | key!(Right) => match self.go_to_children(filter) { + key!('h') | key!(Left) => self.move_to_parent(), + key!('l') | key!(Right) => match self.move_to_children(filter) { Ok(_) => {} Err(err) => cx.editor.set_error(err.to_string()), }, @@ -874,8 +912,10 @@ impl TreeView { ctrl!('u') => self.move_up_half_page(), key!('g') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.go_to_first(), - key!('e') => tree.go_to_last(), + key!('g') => tree.move_to_first(), + key!('e') => tree.move_to_last(), + key!('h') => tree.move_leftmost(), + key!('l') => tree.move_rightmost(), _ => {} })); } @@ -1141,10 +1181,10 @@ mod test_tree_view { } #[test] - fn test_go_to_first_last() { + fn test_move_to_first_last() { let mut view = dummy_tree_view(); - view.go_to_last(); + view.move_to_last(); assert_eq!( render(&mut view), " @@ -1157,7 +1197,7 @@ mod test_tree_view { .trim() ); - view.go_to_first(); + view.move_to_first(); assert_eq!( render(&mut view), " @@ -1255,11 +1295,11 @@ mod test_tree_view { } #[test] - fn go_to_children_parent() { + fn move_to_children_parent() { let filter = "".to_string(); let mut view = dummy_tree_view(); view.move_down(1); - view.go_to_children(&filter).unwrap(); + view.move_to_children(&filter).unwrap(); assert_eq!( render(&mut view), " @@ -1285,7 +1325,7 @@ mod test_tree_view { .trim() ); - view.go_to_parent(); + view.move_to_parent(); assert_eq!( render(&mut view), " @@ -1298,8 +1338,8 @@ mod test_tree_view { .trim() ); - view.go_to_last(); - view.go_to_parent(); + view.move_to_last(); + view.move_to_parent(); assert_eq!( render(&mut view), " @@ -1334,6 +1374,163 @@ mod test_tree_view { assert_eq!( render(&mut view), " +(ho_lives_in_a_pineap) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_right(1); + assert_eq!( + render(&mut view), + " +(o_lives_in_a_pineapp) +gary_the_snail +karen +king_neptune +krabby_patty +" + .trim() + ); + + view.move_right(1); + assert_eq!( + render(&mut view), + " +(_lives_in_a_pineappl) +ary_the_snail +aren +ing_neptune +rabby_patty +" + .trim() + ); + + view.move_left(1); + assert_eq!( + render(&mut view), + " +(o_lives_in_a_pineapp) +gary_the_snail +karen +king_neptune +krabby_patty +" + .trim() + ); + + view.move_leftmost(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pinea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_left(1); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pinea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_rightmost(); + assert_eq!(render(&mut view), "(apple_under_the_sea)\n\n\n\n"); + } + + #[test] + fn test_move_to_parent_child() { + let mut view = dummy_tree_view(); + let filter = "".to_string(); + + view.move_to_children(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + (gary_the_snail) + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_to_children(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + [gary_the_snail] +  (e_snail) +  gary_th + karen +" + .trim() + ); + + view.move_down(1); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + [gary_the_snail] +  e_snail +  (gary_th) + karen +" + .trim() + ); + + view.move_to_parent(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + (gary_the_snail) +  e_snail +  gary_th + karen +" + .trim() + ); + + view.move_to_parent(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail +  e_snail +  gary_th + karen +" + .trim() + ); + + view.move_to_parent(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail +  e_snail +  gary_th + karen " .trim() ) -- 2.38.5 From 2a60662e8bcb0979b44504d1b5fb485422b32249 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 20 Feb 2023 11:27:06 +0800 Subject: [PATCH 035/191] feat(explore): add focus indicator --- helix-term/src/ui/explore.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index b887c5f2..9da83af1 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -509,12 +509,18 @@ impl Explorer { } fn render_tree(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let title_style = cx.editor.theme.get("ui.text"); + let title_style = if self.is_focus() { + title_style.add_modifier(Modifier::BOLD) + } else { + title_style + }; surface.set_stringn( area.x, area.y, "Explorer: press ? for help", area.width.into(), - cx.editor.theme.get("ui.text"), + title_style, ); surface.set_stringn( area.x, -- 2.38.5 From 2e654a0775cfebb0d2b770657a87fee69ba5d1a2 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 20 Feb 2023 11:29:03 +0800 Subject: [PATCH 036/191] refactor(explore): move search function to Tree --- helix-term/src/ui/editor.rs | 12 ++ helix-term/src/ui/explore.rs | 40 ++-- helix-term/src/ui/tree.rs | 370 +++++++++++++++++++++++++++++++---- 3 files changed, 361 insertions(+), 61 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9b447947..e52f8a7e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1383,6 +1383,12 @@ impl Component for EditorView { if let Some(explorer) = self.explorer.as_mut() { if !explorer.content.is_focus() { if let Some(position) = config.explorer.is_embed() { + let area = if use_bufferline { + area.clip_top(1) + } + else { + area + }; explorer.content.render_embed(area, surface, cx, &position); } } @@ -1470,6 +1476,12 @@ impl Component for EditorView { if let Some(explore) = self.explorer.as_mut() { if explore.content.is_focus() { if let Some(position) = config.explorer.is_embed() { + let area = if use_bufferline { + area.clip_top(1) + } + else { + area + }; explore.content.render_embed(area, surface, cx, &position); } else { explore.render(area, surface, cx); diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 9da83af1..62da7da7 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -9,6 +9,7 @@ use helix_view::{ editor::{Action, ExplorerPositionEmbed}, graphics::{CursorKind, Rect}, input::{Event, KeyEvent}, + theme::Modifier, DocumentId, Editor, }; use std::borrow::Cow; @@ -287,11 +288,10 @@ impl Explorer { ("A", "Add folder"), ("r", "Rename file/folder"), ("d", "Delete file"), - ("/", "Search"), ("f", "Filter"), ("[", "Change root to parent folder"), ("]", "Change root to current folder"), - ("^o", "Go to previous root"), + ("C-o", "Go to previous root"), ("R", "Refresh"), ("+", "Increase size"), ("-", "Decrease size"), @@ -327,7 +327,7 @@ impl Explorer { } fn new_search_prompt(&mut self, search_next: bool) { - self.tree.save_view(); + // self.tree.save_view(); self.prompt = Some(( PromptAction::Search { search_next }, Prompt::new(" Search: ".into(), None, ui::completers::none, |_, _, _| {}), @@ -335,7 +335,7 @@ impl Explorer { } fn new_filter_prompt(&mut self, cx: &mut Context) { - self.tree.save_view(); + // self.tree.save_view(); self.prompt = Some(( PromptAction::Filter, Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {}) @@ -603,7 +603,7 @@ impl Explorer { if preview_area.width < 30 || preview_area.height < 3 { return; } - let y = self.tree.row().saturating_sub(1) as u16; + let y = self.tree.winline().saturating_sub(1) as u16; let y = if (preview_area_height + y) > preview_area.height { preview_area.height - preview_area_height } else { @@ -667,11 +667,11 @@ impl Explorer { search_next: is_next, } = action { - explorer.tree.save_view(); + // explorer.tree.save_view(); if is_next == search_next { - explorer.tree.search_next(&search_str); + // explorer.tree.search_next(&search_str); } else { - explorer.tree.search_previous(&search_str); + // explorer.tree.search_previous(&search_str); } } })) @@ -679,13 +679,13 @@ impl Explorer { self.repeat_motion = None; } } - key!(Esc) | ctrl!('c') => self.tree.restore_view(), + // 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(prompt.line()); + // self.tree.search_next(prompt.line()); } else { - self.tree.search_previous(prompt.line()); + // self.tree.search_previous(prompt.line()); } } self.prompt = Some((action, prompt)); @@ -828,6 +828,10 @@ impl Explorer { impl Component for Explorer { /// Process input events, return true if handled. fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { + let filter = self.state.filter.clone(); + if self.tree.prompting() { + return self.tree.handle_event(event, cx, &mut self.state, &filter); + } let key_event = match event { Event::Key(event) => event, Event::Resize(..) => return EventResult::Consumed(None), @@ -847,20 +851,7 @@ impl Component for Explorer { match key_event { key!(Esc) => self.unfocus(), key!('q') => self.close(), - key!('n') => { - if let Some(mut repeat_motion) = self.repeat_motion.take() { - repeat_motion(self, PromptAction::Search { search_next: 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 { search_next: false }, cx); - self.repeat_motion = Some(repeat_motion); - } - } key!('f') => self.new_filter_prompt(cx), - key!('/') => self.new_search_prompt(true), key!('?') => self.toggle_help(), key!('a') => { if let Err(error) = self.new_create_file_prompt() { @@ -890,7 +881,6 @@ impl Component for Explorer { key!('-') => self.decrease_size(), key!('+') => self.increase_size(), _ => { - let filter = self.state.filter.clone(); self.tree .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 0aaabf0b..2d1917b2 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -4,8 +4,8 @@ use anyhow::Result; use helix_view::theme::Modifier; use crate::{ - compositor::{Context, EventResult}, - ctrl, key, shift, + compositor::{Component, Context, EventResult}, + ctrl, key, shift, ui, }; use helix_core::movement::Direction; use helix_view::{ @@ -14,6 +14,8 @@ use helix_view::{ }; use tui::buffer::Buffer as Surface; +use super::Prompt; + pub trait TreeViewItem: Sized { type Params; @@ -33,7 +35,7 @@ fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { T::cmp(item1, item2) } -pub fn vec_to_tree(mut items: Vec) -> Vec> { +fn vec_to_tree(mut items: Vec) -> Vec> { items.sort_by(tree_item_cmp); index_elems( 0, @@ -212,17 +214,26 @@ impl Tree { /// Find an element in the tree with given `predicate`. /// `start_index` is inclusive if direction is `Forward`. /// `start_index` is exclusive if direction is `Backward`. - pub fn find(&self, start_index: usize, direction: Direction, predicate: F) -> Option + fn find(&self, start_index: usize, direction: Direction, predicate: F) -> Option where - F: FnMut(&Tree) -> bool, + F: Clone + FnMut(&Tree) -> bool, { - let iter = self.iter(); match direction { - Direction::Forward => iter + Direction::Forward => match self + .iter() .skip(start_index) - .position(predicate) - .map(|index| index + start_index), - Direction::Backward => iter.take(start_index).rposition(predicate), + .position(predicate.clone()) + .map(|index| index + start_index) + { + Some(index) => Some(index), + None => self.iter().position(predicate), + }, + + Direction::Backward => match self.iter().take(start_index).rposition(predicate.clone()) + { + Some(index) => Some(index), + None => self.iter().rposition(predicate), + }, } } @@ -285,14 +296,29 @@ impl Tree { } } +#[derive(Clone, Debug)] +enum PromptAction { + Search { search_next: bool }, + Filter, +} + +#[derive(Clone, Debug)] +struct SavedView { + selected: usize, + winline: usize, +} + pub struct TreeView { tree: Tree, + prompt: Option<(PromptAction, Prompt)>, + + search_str: String, + /// Selected item idex selected: usize, - /// (selected, row) - save_view: (usize, usize), + saved_view: Option, /// For implementing vertical scroll winline: usize, @@ -319,7 +345,7 @@ impl TreeView { Self { tree: Tree::new(root, items), selected: 0, - save_view: (0, 0), + saved_view: None, winline: 0, column: 0, max_len: 0, @@ -329,6 +355,8 @@ impl TreeView { on_opened_fn: None, on_folded_fn: None, on_next_key: None, + prompt: None, + search_str: "".to_owned(), } } @@ -493,6 +521,9 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("gl", "Go to line end"), ("C-d", "Page down"), ("C-u", "Page up"), + ("/", "Search"), + ("n", "Go to next search match"), + ("N", "Go to previous search match"), ] } @@ -529,29 +560,47 @@ impl TreeView { } } - pub fn search_next(&mut self, s: &str) { - let skip = std::cmp::max(2, self.save_view.0 + 1); + fn set_search_str(&mut self, s: String) { + self.search_str = s; + self.saved_view = None; + } + + fn saved_view(&self) -> SavedView { + self.saved_view.clone().unwrap_or_else(|| SavedView { + selected: self.selected, + winline: self.winline, + }) + } + + fn search_next(&mut self, s: &str) { + let saved_view = self.saved_view(); + let skip = std::cmp::max(2, saved_view.selected + 1); self.set_selected( self.tree .find(skip, Direction::Forward, |e| e.item.filter(s)) - .unwrap_or(self.save_view.0), + .unwrap_or(saved_view.selected), ); - - self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); } - pub fn search_previous(&mut self, s: &str) { - let take = self.save_view.0; + fn search_previous(&mut self, s: &str) { + let saved_view = self.saved_view(); + let take = saved_view.selected; self.set_selected( self.tree .find(take, Direction::Backward, |e| e.item.filter(s)) - .unwrap_or(self.save_view.0), + .unwrap_or(saved_view.selected), ); + } + + fn move_to_next_search_match(&mut self) { + self.search_next(&self.search_str.clone()) + } - self.winline = (self.save_view.1 + self.selected).saturating_sub(self.save_view.0); + fn move_to_previous_next_match(&mut self) { + self.search_previous(&self.search_str.clone()) } - pub fn move_down(&mut self, rows: usize) { + fn move_down(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { self.set_selected(std::cmp::min(self.selected + rows, len.saturating_sub(1))) @@ -575,18 +624,18 @@ impl TreeView { self.selected = selected; } - pub fn move_up(&mut self, rows: usize) { + fn move_up(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { self.set_selected(self.selected.saturating_sub(rows).max(0)) } } - pub fn move_left(&mut self, cols: usize) { + fn move_left(&mut self, cols: usize) { self.column = self.column.saturating_sub(cols); } - pub fn move_right(&mut self, cols: usize) { + fn move_right(&mut self, cols: usize) { let max_scroll = self .max_len .saturating_sub(self.previous_area.width as usize) @@ -594,28 +643,34 @@ impl TreeView { self.column = max_scroll.min(self.column + cols); } - pub fn move_down_half_page(&mut self) { + fn move_down_half_page(&mut self) { self.move_down(self.previous_area.height as usize / 2) } - pub fn move_up_half_page(&mut self) { + fn move_up_half_page(&mut self) { self.move_up(self.previous_area.height as usize / 2); } - pub fn move_down_page(&mut self) { + fn move_down_page(&mut self) { self.move_down(self.previous_area.height as usize); } - pub fn move_up_page(&mut self) { + fn move_up_page(&mut self) { self.move_up(self.previous_area.height as usize); } - pub fn save_view(&mut self) { - self.save_view = (self.selected, self.winline); + fn save_view(&mut self) { + self.saved_view = Some(SavedView { + selected: self.selected, + winline: self.winline, + }) } - pub fn restore_view(&mut self) { - (self.selected, self.winline) = self.save_view; + fn restore_view(&mut self) { + SavedView { + selected: self.selected, + winline: self.winline, + } = self.saved_view(); } fn get(&self, index: usize) -> &Tree { @@ -646,7 +701,7 @@ impl TreeView { &self.current().item } - pub fn row(&self) -> usize { + pub fn winline(&self) -> usize { self.winline } @@ -754,12 +809,27 @@ fn render_tree( impl TreeView { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { let style = cx.editor.theme.get(&self.tree_symbol_style); + let prompt_area = area.with_height(1); + if let Some((_, prompt)) = self.prompt.as_mut() { + surface.set_style(prompt_area, style.add_modifier(Modifier::REVERSED)); + prompt.render_prompt(prompt_area, surface, cx) + } else { + surface.set_stringn( + prompt_area.x, + prompt_area.y, + format!("[SEARCH]: {}", self.search_str.clone()), + prompt_area.width as usize, + style, + ); + } + let ancestor_style = cx.editor.theme.get("ui.text.focus"); + let area = area.clip_top(1); let iter = self.render_lines(area, filter).into_iter().enumerate(); for (index, line) in iter { - let area = Rect::new(area.x, area.y + index as u16, area.width, 1); + let area = Rect::new(area.x, area.y.saturating_add(index as u16), area.width, 1); let indent_len = line.indent.chars().count() as u16; surface.set_stringn( area.x, @@ -887,6 +957,10 @@ impl TreeView { on_next_key(cx, self, key_event); return EventResult::Consumed(None); } + + if let EventResult::Consumed(c) = self.handle_prompt_event(key_event, cx) { + return EventResult::Consumed(c); + } let count = std::mem::replace(&mut self.count, 0); match key_event { key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10, @@ -919,11 +993,65 @@ impl TreeView { _ => {} })); } + key!('/') => self.new_search_prompt(true), + key!('n') => self.move_to_next_search_match(), + shift!('N') => self.move_to_previous_next_match(), _ => return EventResult::Ignored(None), } 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), + _ => EventResult::Ignored(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 { + key!(Enter) => { + self.set_search_str(prompt.line().clone()); + EventResult::Consumed(None) + } + key!(Esc) => EventResult::Consumed(None), + _ => { + let event = prompt.handle_event(&Event::Key(*event), cx); + let line = prompt.line(); + if search_next { + self.search_next(line) + } else { + self.search_previous(line) + } + self.prompt = Some((action, prompt)); + event + } + } + } + + fn new_search_prompt(&mut self, search_next: bool) { + self.save_view(); + self.prompt = Some(( + PromptAction::Search { search_next }, + Prompt::new( + "[SEARCH]: ".into(), + None, + ui::completers::theme, + |_, _, _| {}, + ), + )) + } + + pub fn prompting(&self) -> bool { + self.prompt.is_some() + } } /// Recalculate the index of each item of a tree. @@ -1535,6 +1663,173 @@ krabby_patty .trim() ) } + + #[test] + fn test_search_next() { + let mut view = dummy_tree_view(); + + view.search_next("pat"); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) +" + .trim() + ); + + view.search_next("larr"); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.move_to_last(); + view.search_next("who_lives"); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + } + + #[test] + fn test_search_previous() { + let mut view = dummy_tree_view(); + + view.search_previous("larry"); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + krabby_patty + (larry_the_lobster) +" + .trim() + ); + + view.move_to_last(); + view.search_previous("krab"); + assert_eq!( + render(&mut view), + " + gary_the_snail + karen + king_neptune + (krabby_patty) + larry_the_lobster +" + .trim() + ); + } + + #[test] + fn test_move_to_next_search_match() { + let mut view = dummy_tree_view(); + view.set_search_str("pat".to_string()); + view.move_to_next_search_match(); + + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) + " + .trim() + ); + + view.move_to_next_search_match(); + assert_eq!( + render(&mut view), + " + king_neptune + krabby_patty + larry_the_lobster + mrs_puff + (patrick_star) + " + .trim() + ); + + view.move_to_next_search_match(); + assert_eq!( + render(&mut view), + " + king_neptune + (krabby_patty) + larry_the_lobster + mrs_puff + patrick_star + " + .trim() + ); + } + + #[test] + fn test_move_to_previous_search_match() { + let mut view = dummy_tree_view(); + view.set_search_str("pat".to_string()); + view.move_to_previous_next_match(); + + assert_eq!( + render(&mut view), + " + king_neptune + krabby_patty + larry_the_lobster + mrs_puff + (patrick_star) + " + .trim() + ); + + view.move_to_previous_next_match(); + assert_eq!( + render(&mut view), + " + king_neptune + (krabby_patty) + larry_the_lobster + mrs_puff + patrick_star + " + .trim() + ); + + view.move_to_previous_next_match(); + assert_eq!( + render(&mut view), + " + king_neptune + krabby_patty + larry_the_lobster + mrs_puff + (patrick_star) + " + .trim() + ); + } } #[cfg(test)] @@ -1579,6 +1874,9 @@ mod test_tree { assert_eq!(iter.next().map(|tree| tree.item), Some("yo")); assert_eq!(iter.next().map(|tree| tree.item), Some("foo")); assert_eq!(iter.next().map(|tree| tree.item), Some("bar")); + + // Expect the iterator to be cyclic, so next() should jump to first item + assert_eq!(iter.next().map(|tree| tree.item), Some("spam")) } #[test] -- 2.38.5 From 2e7709e5052dc9376cb42bcf8f1c4f915f326119 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 21 Feb 2023 10:27:50 +0800 Subject: [PATCH 037/191] MULTI - refactor(explore):Move filter to Tree - feat(explore): Implement mkdir -p (but not tested yet) - feat(ui/tree): Implement jump backward - test(ui/tree): Refresh --- changes | 4 + helix-term/src/ui/explore.rs | 174 +++----------------- helix-term/src/ui/tree.rs | 302 ++++++++++++++++++++++++++++------- 3 files changed, 272 insertions(+), 208 deletions(-) diff --git a/changes b/changes index a436eafd..c3e5b379 100644 --- a/changes +++ b/changes @@ -27,6 +27,9 @@ TODO - [x] Remove comments - [x] fix warnings - [x] refactor, add tree.expand_children() method +- [] Change '[' to "go to previous root" +- [] Change 'b' to "go to parent" +- [] Use C-o for jumping to previous position - [] add integration testing (test explorer rendering) - [] search highlight matching word - [] Error didn't clear @@ -37,4 +40,5 @@ TODO - [] Fix panic bugs (see github comments) - [] Sticky ancestors - [] Ctrl-o should work for 'h', 'gg', 'ge', etc +- [] explorer(previow): overflow where bufferline is there - [] explorer(preview): content not sorted diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 62da7da7..2c030512 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, EventResult}, ctrl, key, shift, ui, }; -use anyhow::{bail, ensure, Result}; +use anyhow::{ensure, Result}; use helix_core::Position; use helix_view::{ editor::{Action, ExplorerPositionEmbed}, @@ -122,21 +122,11 @@ impl TreeViewItem for FileInfo { #[derive(Clone, Debug)] enum PromptAction { - Search { - search_next: bool, - }, // search next/search pre - CreateFolder { - folder_path: PathBuf, - parent_index: usize, - }, - CreateFile { - folder_path: PathBuf, - parent_index: usize, - }, + CreateFolder { folder_path: PathBuf }, + CreateFile { folder_path: PathBuf }, RemoveDir, RemoveFile(Option), RenameFile(Option), - Filter, } #[derive(Clone, Debug)] @@ -288,11 +278,9 @@ impl Explorer { ("A", "Add folder"), ("r", "Rename file/folder"), ("d", "Delete file"), - ("f", "Filter"), - ("[", "Change root to parent folder"), + ("b", "Change root to parent folder"), ("]", "Change root to current folder"), - ("C-o", "Go to previous root"), - ("R", "Refresh"), + ("[", "Go to previous root"), ("+", "Increase size"), ("-", "Decrease size"), ("q", "Close"), @@ -326,28 +314,10 @@ impl Explorer { }) } - 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, cx: &mut Context) { - // self.tree.save_view(); - self.prompt = Some(( - PromptAction::Filter, - Prompt::new(" Filter: ".into(), None, ui::completers::none, |_, _, _| {}) - .with_line(self.state.filter.clone(), cx.editor), - )) - } - fn new_create_folder_prompt(&mut self) -> Result<()> { - let (parent_index, folder_path) = self.nearest_folder()?; + let folder_path = self.nearest_folder()?; self.prompt = Some(( PromptAction::CreateFolder { - parent_index, folder_path: folder_path.clone(), }, Prompt::new( @@ -361,10 +331,9 @@ impl Explorer { } fn new_create_file_prompt(&mut self) -> Result<()> { - let (parent_index, folder_path) = self.nearest_folder()?; + let folder_path = self.nearest_folder()?; self.prompt = Some(( PromptAction::CreateFile { - parent_index, folder_path: folder_path.clone(), }, Prompt::new( @@ -377,24 +346,18 @@ impl Explorer { Ok(()) } - fn nearest_folder(&self) -> Result<(usize, PathBuf)> { + fn nearest_folder(&self) -> Result { let current = self.tree.current(); if current.item().is_parent() { - Ok((current.index(), current.item().path.to_path_buf())) + Ok(current.item().path.to_path_buf()) } else { - let parent_index = current.parent_index().ok_or_else(|| { - anyhow::anyhow!(format!( - "Unable to get parent index of '{}'", - current.item().path.to_string_lossy() - )) - })?; let parent_path = current.item().path.parent().ok_or_else(|| { anyhow::anyhow!(format!( "Unable to get parent path of '{}'", current.item().path.to_string_lossy() )) })?; - Ok((parent_index, parent_path.to_path_buf())) + Ok(parent_path.to_path_buf()) } } @@ -522,15 +485,8 @@ impl Explorer { area.width.into(), title_style, ); - surface.set_stringn( - area.x, - area.y.saturating_add(1), - format!("[FILTER]: {}", self.state.filter), - area.width.into(), - cx.editor.theme.get("ui.text"), - ); self.tree - .render(area.clip_top(2), surface, cx, &self.state.filter); + .render(area.clip_top(1), surface, cx, &self.state.filter); } pub fn render_embed( @@ -646,60 +602,7 @@ impl Explorer { 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 { - key!(Tab) | key!(Down) | ctrl!('j') => { - let filter = self.state.filter.clone(); - return self - .tree - .handle_event(&Event::Key(*event), cx, &mut self.state, &filter); - } - key!(Enter) => { - let search_str = prompt.line().clone(); - if !search_str.is_empty() { - self.repeat_motion = Some(Box::new(move |explorer, action, _| { - if let PromptAction::Search { - search_next: is_next, - } = action - { - // explorer.tree.save_view(); - if is_next == search_next { - // explorer.tree.search_next(&search_str); - } else { - // explorer.tree.search_previous(&search_str); - } - } - })) - } else { - self.repeat_motion = None; - } - } - // 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(prompt.line()); - } else { - // self.tree.search_previous(prompt.line()); - } - } - 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), - _ => {} - }; fn handle_prompt_event( explorer: &mut Explorer, event: &KeyEvent, @@ -711,20 +614,12 @@ impl Explorer { }; let line = prompt.line(); match (&action, event) { - ( - PromptAction::CreateFolder { - folder_path, - parent_index, - }, - key!(Enter), - ) => explorer.new_path(folder_path.clone(), line, true, *parent_index)?, - ( - PromptAction::CreateFile { - folder_path, - parent_index, - }, - key!(Enter), - ) => explorer.new_path(folder_path.clone(), line, false, *parent_index)?, + (PromptAction::CreateFolder { folder_path }, key!(Enter)) => { + explorer.new_path(folder_path.clone(), line, true)? + } + (PromptAction::CreateFile { folder_path }, key!(Enter)) => { + explorer.new_path(folder_path.clone(), line, false)? + } (PromptAction::RemoveDir, key!(Enter)) => { if line == "y" { let item = explorer.tree.current_item(); @@ -768,30 +663,19 @@ impl Explorer { } } - fn new_path( - &mut self, - current_parent: PathBuf, - file_name: &str, - is_dir: bool, - parent_index: usize, - ) -> Result<()> { + fn new_path(&mut self, current_parent: PathBuf, file_name: &str, is_dir: bool) -> Result<()> { let path = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); - match path.parent() { - Some(p) if p == current_parent => {} - _ => bail!("The file name is not illegal"), - }; - let file = if is_dir { - std::fs::create_dir(&path)?; - FileInfo::new(path, FileType::Folder) + if is_dir { + std::fs::create_dir_all(&path)?; } else { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } let mut fd = std::fs::OpenOptions::new(); fd.create_new(true).write(true).open(&path)?; - FileInfo::new(path, FileType::File) }; - self.tree - .add_child(parent_index, file, &self.state.filter)?; - Ok(()) + self.reveal_file(path) } fn toggle_help(&mut self) { @@ -851,7 +735,6 @@ impl Component for Explorer { match key_event { key!(Esc) => self.unfocus(), key!('q') => self.close(), - key!('f') => self.new_filter_prompt(cx), key!('?') => self.toggle_help(), key!('a') => { if let Err(error) = self.new_create_file_prompt() { @@ -863,21 +746,16 @@ impl Component for Explorer { cx.editor.set_error(error.to_string()) } } - key!('[') => { + key!('b') => { if let Some(parent) = self.state.current_root.parent().clone() { let path = parent.to_path_buf(); self.change_root(cx, path) } } key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), - ctrl!('o') => self.go_to_previous_root(), + key!('[') => self.go_to_previous_root(), key!('d') => self.new_remove_prompt(cx), key!('r') => self.new_rename_prompt(cx), - shift!('R') => { - if let Err(error) = self.tree.refresh(&self.state.filter) { - cx.editor.set_error(error.to_string()) - } - } key!('-') => self.decrease_size(), key!('+') => self.increase_size(), _ => { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 2d1917b2..8405af7c 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -296,12 +296,6 @@ impl Tree { } } -#[derive(Clone, Debug)] -enum PromptAction { - Search { search_next: bool }, - Filter, -} - #[derive(Clone, Debug)] struct SavedView { selected: usize, @@ -311,13 +305,19 @@ struct SavedView { pub struct TreeView { tree: Tree, - prompt: Option<(PromptAction, Prompt)>, + search_prompt: Option<(Direction, Prompt)>, + + filter_prompt: Option, search_str: String, + filter: String, + /// Selected item idex selected: usize, + history: Vec, + saved_view: Option, /// For implementing vertical scroll @@ -345,6 +345,7 @@ impl TreeView { Self { tree: Tree::new(root, items), selected: 0, + history: vec![], saved_view: None, winline: 0, column: 0, @@ -355,8 +356,10 @@ impl TreeView { on_opened_fn: None, on_folded_fn: None, on_next_key: None, - prompt: None, - search_str: "".to_owned(), + search_prompt: None, + filter_prompt: None, + search_str: "".into(), + filter: "".into(), } } @@ -394,7 +397,7 @@ impl TreeView { /// vec!["helix-term", "src", "ui", "tree.rs"] /// ``` pub fn reveal_item(&mut self, segments: Vec<&str>, filter: &String) -> Result<()> { - self.tree.refresh(filter)?; + self.refresh(filter)?; // Expand the tree segments.iter().fold( @@ -480,7 +483,9 @@ impl TreeView { } pub fn refresh(&mut self, filter: &String) -> Result<()> { - self.tree.refresh(filter) + self.tree.refresh(filter)?; + self.set_selected(self.selected); + Ok(()) } fn move_to_first(&mut self) { @@ -510,8 +515,20 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("k, up", "Up"), ("h, left", "Go to parent"), ("l, right", "Expand"), - ("L", "Scroll right"), + ("f", "Filter"), + ("/", "Search"), + ("n", "Go to next search match"), + ("N", "Go to previous search match"), + ("R", "Refresh"), ("H", "Scroll left"), + ("L", "Scroll right"), + ("Home", "Scroll to the leftmost"), + ("End", "Scroll to the rightmost"), + ("C-o", "Jump backward"), + ("C-d", "Half page down"), + ("C-u", "Half page up"), + ("PageUp", "Full page up"), + ("PageDown", "Full page down"), ("zz", "Align view center"), ("zt", "Align view top"), ("zb", "Align view bottom"), @@ -519,11 +536,6 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("ge", "Go to last line"), ("gh", "Go to line start"), ("gl", "Go to line end"), - ("C-d", "Page down"), - ("C-u", "Page up"), - ("/", "Search"), - ("n", "Go to next search match"), - ("N", "Go to previous search match"), ] } @@ -608,6 +620,15 @@ impl TreeView { } fn set_selected(&mut self, selected: usize) { + let previous_selected = self.selected; + self.set_selected_without_history(selected); + if previous_selected.abs_diff(selected) > 1 { + self.history.push(previous_selected) + } + } + + fn set_selected_without_history(&mut self, selected: usize) { + let selected = selected.clamp(0, self.tree.len().saturating_sub(1)); if selected > self.selected { // Move down self.winline = selected.min( @@ -621,7 +642,13 @@ impl TreeView { .saturating_sub(self.selected.saturating_sub(selected)), ); } - self.selected = selected; + self.selected = selected + } + + fn jump_backward(&mut self) { + if let Some(index) = self.history.pop() { + self.set_selected_without_history(index); + } } fn move_up(&mut self, rows: usize) { @@ -674,11 +701,15 @@ impl TreeView { } fn get(&self, index: usize) -> &Tree { - self.tree.get(index).unwrap() + self.tree + .get(index) + .expect(format!("Tree: index {index} is out of bound").as_str()) } fn get_mut(&mut self, index: usize) -> &mut Tree { - self.tree.get_mut(index).unwrap() + self.tree + .get_mut(index) + .expect(format!("Tree: index {index} is out of bound").as_str()) } pub fn current(&self) -> &Tree { @@ -809,23 +840,38 @@ fn render_tree( impl TreeView { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { let style = cx.editor.theme.get(&self.tree_symbol_style); - let prompt_area = area.with_height(1); - if let Some((_, prompt)) = self.prompt.as_mut() { - surface.set_style(prompt_area, style.add_modifier(Modifier::REVERSED)); - prompt.render_prompt(prompt_area, surface, cx) + + let filter_prompt_area = area.with_height(1); + if let Some(prompt) = self.filter_prompt.as_mut() { + surface.set_style(filter_prompt_area, style.add_modifier(Modifier::REVERSED)); + prompt.render_prompt(filter_prompt_area, surface, cx) + } else { + surface.set_stringn( + filter_prompt_area.x, + filter_prompt_area.y, + format!("[FILTER]: {}", self.filter.clone()), + filter_prompt_area.width as usize, + style, + ); + } + + let search_prompt_area = area.clip_top(1).with_height(1); + if let Some((_, prompt)) = self.search_prompt.as_mut() { + surface.set_style(search_prompt_area, style.add_modifier(Modifier::REVERSED)); + prompt.render_prompt(search_prompt_area, surface, cx) } else { surface.set_stringn( - prompt_area.x, - prompt_area.y, + search_prompt_area.x, + search_prompt_area.y, format!("[SEARCH]: {}", self.search_str.clone()), - prompt_area.width as usize, + search_prompt_area.width as usize, style, ); } let ancestor_style = cx.editor.theme.get("ui.text.focus"); - let area = area.clip_top(1); + let area = area.clip_top(2); let iter = self.render_lines(area, filter).into_iter().enumerate(); for (index, line) in iter { @@ -958,9 +1004,14 @@ impl TreeView { return EventResult::Consumed(None); } - if let EventResult::Consumed(c) = self.handle_prompt_event(key_event, cx) { + if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) { + return EventResult::Consumed(c); + } + + if let EventResult::Consumed(c) = self.handle_filter_event(key_event, cx) { return EventResult::Consumed(c); } + let count = std::mem::replace(&mut self.count, 0); match key_event { key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10, @@ -993,64 +1044,115 @@ impl TreeView { _ => {} })); } - key!('/') => self.new_search_prompt(true), + key!('/') => self.new_search_prompt(Direction::Forward), key!('n') => self.move_to_next_search_match(), shift!('N') => self.move_to_previous_next_match(), + key!('f') => self.new_filter_prompt(cx), + key!(PageDown) => self.move_down_page(), + key!(PageUp) => self.move_up_page(), + shift!('R') => { + let filter = self.filter.clone(); + if let Err(error) = self.refresh(&filter) { + cx.editor.set_error(error.to_string()) + } + } + key!(Home) => self.move_leftmost(), + key!(End) => self.move_rightmost(), + ctrl!('o') => self.jump_backward(), _ => return EventResult::Ignored(None), } 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), - _ => EventResult::Ignored(None), + fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { + if let Some(mut prompt) = self.filter_prompt.take() { + (|| -> Result<()> { + match event { + key!(Enter) => { + if let EventResult::Consumed(_) = + prompt.handle_event(&Event::Key(*event), cx) + { + self.refresh(prompt.line())?; + } + } + key!(Esc) | ctrl!('c') => { + self.filter.clear(); + self.refresh(&"".to_string())?; + } + _ => { + if let EventResult::Consumed(_) = + prompt.handle_event(&Event::Key(*event), cx) + { + self.refresh(prompt.line())?; + } + self.filter = prompt.line().clone(); + self.filter_prompt = Some(prompt); + } + }; + Ok(()) + })() + .unwrap_or_else(|err| cx.editor.set_error(format!("{err}"))); + EventResult::Consumed(None) + } else { + EventResult::Ignored(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 { - key!(Enter) => { - self.set_search_str(prompt.line().clone()); - EventResult::Consumed(None) - } - key!(Esc) => EventResult::Consumed(None), - _ => { - let event = prompt.handle_event(&Event::Key(*event), cx); - let line = prompt.line(); - if search_next { - self.search_next(line) - } else { - self.search_previous(line) + if let Some((direction, mut prompt)) = self.search_prompt.take() { + match event { + key!(Enter) => { + self.set_search_str(prompt.line().clone()); + EventResult::Consumed(None) + } + key!(Esc) => EventResult::Consumed(None), + _ => { + let event = prompt.handle_event(&Event::Key(*event), cx); + let line = prompt.line(); + match direction { + Direction::Forward => { + self.search_next(line); + } + Direction::Backward => self.search_previous(line), + } + self.search_prompt = Some((direction, prompt)); + event } - self.prompt = Some((action, prompt)); - event } + } else { + EventResult::Ignored(None) } } - fn new_search_prompt(&mut self, search_next: bool) { + fn new_search_prompt(&mut self, direction: Direction) { self.save_view(); - self.prompt = Some(( - PromptAction::Search { search_next }, + self.search_prompt = Some(( + direction, Prompt::new( "[SEARCH]: ".into(), None, - ui::completers::theme, + ui::completers::none, |_, _, _| {}, ), )) } + fn new_filter_prompt(&mut self, cx: &mut Context) { + self.save_view(); + self.filter_prompt = Some( + Prompt::new( + "[FILTER]: ".into(), + None, + ui::completers::none, + |_, _, _| {}, + ) + .with_line(self.filter.clone(), cx.editor), + ) + } + pub fn prompting(&self) -> bool { - self.prompt.is_some() + self.filter_prompt.is_some() || self.search_prompt.is_some() } } @@ -1830,6 +1932,86 @@ krabby_patty .trim() ); } + + #[test] + fn test_refresh() { + let mut view = dummy_tree_view(); + + // 1. Move to the last child item on the tree + view.move_to_last(); + view.move_to_children(&"".to_string()).unwrap(); + view.move_to_last(); + view.move_to_children(&"".to_string()).unwrap(); + view.move_to_last(); + view.move_to_children(&"".to_string()).unwrap(); + view.move_to_last(); + view.move_to_children(&"".to_string()).unwrap(); + + // 1a. Expect the current selected item is the last child on the tree + assert_eq!( + render(&mut view), + " +  epants +  [squar] + sq +  [uar] + (ar)" + .trim_start_matches(|c| c == '\n') + ); + + // 2. Refreshes the tree with a filter that will remove the last child + view.refresh(&"ar".to_string()).unwrap(); + + // 3. Get the current item + let item = view.current_item(); + + // 3a. Expects no failure + assert_eq!(item.name, "who_lives_in_a_pine") + } + + #[test] + fn test_jump_backward() { + let mut view = dummy_tree_view(); + view.move_down_half_page(); + view.move_down_half_page(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) + " + .trim() + ); + + view.jump_backward(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + (karen) + king_neptune + krabby_patty + " + .trim() + ); + + view.jump_backward(); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty + " + .trim() + ); + } } #[cfg(test)] -- 2.38.5 From a259c205c0fe8a4271f20862989f53b282bb8ba9 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Feb 2023 10:05:54 +0800 Subject: [PATCH 038/191] fix(explore): help overflow - render with Info --- changes | 21 +++-- helix-term/src/ui/explore.rs | 168 +++++++++++++++-------------------- helix-term/src/ui/tree.rs | 81 +++-------------- 3 files changed, 97 insertions(+), 173 deletions(-) diff --git a/changes b/changes index c3e5b379..f2ce7add 100644 --- a/changes +++ b/changes @@ -27,18 +27,23 @@ TODO - [x] Remove comments - [x] fix warnings - [x] refactor, add tree.expand_children() method -- [] Change '[' to "go to previous root" -- [] Change 'b' to "go to parent" -- [] Use C-o for jumping to previous position -- [] add integration testing (test explorer rendering) + +New: +- [x] Change '[' to "go to previous root" +- [x] Change 'b' to "go to parent" +- [x] Use C-o for jumping to previous position +- [x] on focus indication +- [x] support creating files and folder and the same time (`mkdir -p`) +- [x] Ctrl-o should work for 'h', 'gg', 'ge', etc +- [x] add unit test for TreeView +- [x] explorer(help): overflow +- [] add integration test for Explorer - [] search highlight matching word - [] Error didn't clear - [] bind "o" to open/close file/folder -- [] on focus indication - [] should preview be there by default? -- [] support creating files and folder and the same time (`mkdir -p`) - [] Fix panic bugs (see github comments) - [] Sticky ancestors -- [] Ctrl-o should work for 'h', 'gg', 'ge', etc -- [] explorer(previow): overflow where bufferline is there +- [] explorer(preview): overflow where bufferline is there - [] explorer(preview): content not sorted +- [] explorer(preview): implement scrolling C-j/C-k diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index 2c030512..f5d1982c 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -8,6 +8,7 @@ use helix_core::Position; use helix_view::{ editor::{Action, ExplorerPositionEmbed}, graphics::{CursorKind, Rect}, + info::Info, input::{Event, KeyEvent}, theme::Modifier, DocumentId, Editor, @@ -158,8 +159,6 @@ pub struct Explorer { prompt: Option<(PromptAction, Prompt)>, #[allow(clippy::type_complexity)] on_next_key: Option EventResult>>, - #[allow(clippy::type_complexity)] - repeat_motion: Option>, column_width: u16, } @@ -169,9 +168,8 @@ impl Explorer { Ok(Self { tree: Self::new_tree_view(current_root.clone())?, history: vec![], - show_help: false, + show_help: true, state: State::new(true, current_root), - repeat_motion: None, prompt: None, on_next_key: None, column_width: cx.editor.config().explorer.column_width as u16, @@ -251,58 +249,21 @@ impl Explorer { } 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(); 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, - if self.show_help { - "[HELP]".to_string() - } else { - path_str - }, + 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"); - let content = if self.show_help { - let instructions = vec![ - ("?", "Toggle help"), - ("a", "Add file"), - ("A", "Add folder"), - ("r", "Rename file/folder"), - ("d", "Delete file"), - ("b", "Change root to parent folder"), - ("]", "Change root to current folder"), - ("[", "Go to previous root"), - ("+", "Increase size"), - ("-", "Decrease size"), - ("q", "Close"), - ] - .into_iter() - .chain(ui::tree::tree_view_help().into_iter()) - .collect::>(); - let max_left_length = instructions - .iter() - .map(|(key, _)| key.chars().count()) - .max() - .unwrap_or(0); - instructions - .into_iter() - .map(|(key, description)| { - format!("{:width$}{}", key, description, width = max_left_length + 1) - }) - .collect::>() - } else { - get_preview(&item.path, body_area.height as usize) - .unwrap_or_else(|err| vec![err.to_string()]) - }; + let content = get_preview(&item.path, body_area.height as usize) + .unwrap_or_else(|err| vec![err.to_string()]); content.into_iter().enumerate().for_each(|(row, line)| { surface.set_stringn( body_area.x, @@ -465,7 +426,11 @@ impl Explorer { prompt.render(promp_area, surface, cx); preview_area = area; } - self.render_preview(preview_area, surface, cx.editor); + if self.show_help { + self.render_help(preview_area, surface, cx); + } else { + self.render_preview(preview_area, surface, cx.editor); + } let list_area = render_block(area.clip_right(preview_area.width), surface, Borders::RIGHT); self.render_tree(list_area, surface, cx) @@ -542,33 +507,43 @@ impl Explorer { } if self.is_focus() { - const PREVIEW_AREA_MAX_WIDTH: u16 = 90; - const PREVIEW_AREA_MAX_HEIGHT: u16 = 30; - let preview_area_width = (area.width - side_area.width).min(PREVIEW_AREA_MAX_WIDTH); - let preview_area_height = area.height.min(PREVIEW_AREA_MAX_HEIGHT); - - let preview_area = match position { - ExplorerPositionEmbed::Left => area.clip_left(side_area.width), - ExplorerPositionEmbed::Right => (Rect { - x: area.width - side_area.width - preview_area_width, - ..area - }) - .clip_right(side_area.width), - } - .clip_bottom(2); - if preview_area.width < 30 || preview_area.height < 3 { - return; - } - let y = self.tree.winline().saturating_sub(1) as u16; - let y = if (preview_area_height + y) > preview_area.height { - preview_area.height - preview_area_height + if self.show_help { + let help_area = match position { + ExplorerPositionEmbed::Left => area, + ExplorerPositionEmbed::Right => { + area.clip_right(list_area.width.saturating_add(2)) + } + }; + self.render_help(help_area, surface, cx); } else { - y - }; - let area = Rect::new(preview_area.x, y, preview_area_width, preview_area_height); - surface.clear_with(area, background); - let area = render_block(area, surface, Borders::all()); - self.render_preview(area, surface, cx.editor); + const PREVIEW_AREA_MAX_WIDTH: u16 = 90; + const PREVIEW_AREA_MAX_HEIGHT: u16 = 30; + let preview_area_width = (area.width - side_area.width).min(PREVIEW_AREA_MAX_WIDTH); + let preview_area_height = area.height.min(PREVIEW_AREA_MAX_HEIGHT); + + let preview_area = match position { + ExplorerPositionEmbed::Left => area.clip_left(side_area.width), + ExplorerPositionEmbed::Right => (Rect { + x: area.width - side_area.width - preview_area_width, + ..area + }) + .clip_right(side_area.width), + } + .clip_bottom(2); + if preview_area.width < 30 || preview_area.height < 3 { + return; + } + let y = self.tree.winline().saturating_sub(1) as u16; + let y = if (preview_area_height + y) > preview_area.height { + preview_area.height - preview_area_height + } else { + y + }; + let area = Rect::new(preview_area.x, y, preview_area_width, preview_area_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() { @@ -576,30 +551,27 @@ impl Explorer { } } - fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { - let (action, mut prompt) = self.prompt.take().unwrap(); - (|| -> Result<()> { - match event { - key!(Enter) => { - if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { - self.tree.refresh(prompt.line())?; - } - } - key!(Esc) | ctrl!('c') => { - self.state.filter.clear(); - } - _ => { - if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { - self.tree.refresh(prompt.line())?; - } - self.state.filter = prompt.line().clone(); - self.prompt = Some((action, prompt)); - } - }; - Ok(()) - })() - .unwrap_or_else(|err| cx.editor.set_error(format!("{err}"))); - EventResult::Consumed(None) + fn render_help(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + Info::new( + "Explorer", + &[ + ("?", "Toggle help"), + ("a", "Add file"), + ("A", "Add folder"), + ("r", "Rename file/folder"), + ("d", "Delete file"), + ("b", "Change root to parent folder"), + ("]", "Change root to current folder"), + ("[", "Go to previous root"), + ("+", "Increase size"), + ("-", "Decrease size"), + ("q", "Close"), + ] + .into_iter() + .chain(ui::tree::tree_view_help().into_iter()) + .collect::>(), + ) + .render(area, surface, cx) } fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { @@ -624,14 +596,14 @@ impl Explorer { if line == "y" { let item = explorer.tree.current_item(); std::fs::remove_dir_all(&item.path)?; - explorer.tree.remove_current(); + explorer.tree.refresh()?; } } (PromptAction::RemoveFile(document_id), key!(Enter)) => { if line == "y" { let item = explorer.tree.current_item(); std::fs::remove_file(&item.path).map_err(anyhow::Error::from)?; - explorer.tree.remove_current(); + explorer.tree.refresh()?; if let Some(id) = document_id { cx.editor.close_document(*id, true)? } @@ -640,7 +612,7 @@ impl Explorer { (PromptAction::RenameFile(document_id), key!(Enter)) => { let item = explorer.tree.current_item(); std::fs::rename(&item.path, line)?; - explorer.tree.remove_current(); + explorer.tree.refresh()?; explorer.reveal_file(PathBuf::from(line))?; if let Some(id) = document_id { cx.editor.close_document(*id, true)? diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 8405af7c..fb52d0ba 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -268,7 +268,7 @@ impl Tree { self.children = index_elems(0, items); } - fn remove(&mut self, index: usize) { + pub fn remove(&mut self, index: usize) { let children = std::mem::replace(&mut self.children, vec![]); self.children = children .into_iter() @@ -286,20 +286,11 @@ impl Tree { .collect(); self.regenerate_index() } - - pub fn parent_index(&self) -> Option { - self.parent_index - } - - pub fn index(&self) -> usize { - self.index - } } #[derive(Clone, Debug)] struct SavedView { selected: usize, - winline: usize, } pub struct TreeView { @@ -397,7 +388,7 @@ impl TreeView { /// vec!["helix-term", "src", "ui", "tree.rs"] /// ``` pub fn reveal_item(&mut self, segments: Vec<&str>, filter: &String) -> Result<()> { - self.refresh(filter)?; + self.refresh_with_filter(filter)?; // Expand the tree segments.iter().fold( @@ -482,7 +473,11 @@ impl TreeView { } } - pub fn refresh(&mut self, filter: &String) -> Result<()> { + pub fn refresh(&mut self) -> Result<()> { + self.refresh_with_filter(&self.filter.clone()) + } + + fn refresh_with_filter(&mut self, filter: &String) -> Result<()> { self.tree.refresh(filter)?; self.set_selected(self.selected); Ok(()) @@ -496,6 +491,7 @@ impl TreeView { self.move_down(usize::MAX / 2) } + #[cfg(test)] fn set_previous_area(&mut self, area: Rect) { self.previous_area = area } @@ -580,7 +576,6 @@ impl TreeView { fn saved_view(&self) -> SavedView { self.saved_view.clone().unwrap_or_else(|| SavedView { selected: self.selected, - winline: self.winline, }) } @@ -689,17 +684,9 @@ impl TreeView { fn save_view(&mut self) { self.saved_view = Some(SavedView { selected: self.selected, - winline: self.winline, }) } - fn restore_view(&mut self) { - SavedView { - selected: self.selected, - winline: self.winline, - } = self.saved_view(); - } - fn get(&self, index: usize) -> &Tree { self.tree .get(index) @@ -735,47 +722,6 @@ impl TreeView { pub fn winline(&self) -> usize { self.winline } - - pub fn remove_current(&mut self) { - self.tree.remove(self.selected); - self.set_selected(self.selected.min(self.tree.len().saturating_sub(1))); - } - - pub fn replace_current(&mut self, item: T) { - self.current_mut().item = item - } - - pub fn add_child(&mut self, index: usize, item: T, filter: &String) -> Result<()> { - match self.tree.get_mut(index) { - None => Err(anyhow::anyhow!(format!( - "No item found at index = {}", - index - ))), - Some(tree) => { - let item_name = item.name(); - if !tree.is_opened { - tree.open(filter)?; - } else { - tree.refresh(filter)?; - } - - self.regenerate_index(); - - let tree = self.get(index); - - // Focus the added sibling - if let Some(tree) = tree - .children - .iter() - .find(|tree| tree.item.name().eq(&item_name)) - { - let index = tree.index; - self.set_selected(index) - }; - Ok(()) - } - } - } } struct RenderedLine { @@ -908,6 +854,7 @@ impl TreeView { } } + #[cfg(test)] fn render_to_string(&mut self, filter: &String) -> String { let area = self.previous_area; let lines = self.render_lines(area, filter); @@ -1052,7 +999,7 @@ impl TreeView { key!(PageUp) => self.move_up_page(), shift!('R') => { let filter = self.filter.clone(); - if let Err(error) = self.refresh(&filter) { + if let Err(error) = self.refresh_with_filter(&filter) { cx.editor.set_error(error.to_string()) } } @@ -1073,18 +1020,18 @@ impl TreeView { if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { - self.refresh(prompt.line())?; + self.refresh_with_filter(prompt.line())?; } } key!(Esc) | ctrl!('c') => { self.filter.clear(); - self.refresh(&"".to_string())?; + self.refresh_with_filter(&"".to_string())?; } _ => { if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { - self.refresh(prompt.line())?; + self.refresh_with_filter(prompt.line())?; } self.filter = prompt.line().clone(); self.filter_prompt = Some(prompt); @@ -1960,7 +1907,7 @@ krabby_patty ); // 2. Refreshes the tree with a filter that will remove the last child - view.refresh(&"ar".to_string()).unwrap(); + view.refresh_with_filter(&"ar".to_string()).unwrap(); // 3. Get the current item let item = view.current_item(); -- 2.38.5 From bcb1672378dda84ed782ed34cf795e4fd2f4cafe Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Feb 2023 10:36:17 +0800 Subject: [PATCH 039/191] fix(explore): - preview panics when term height becomes too small - preview content not sorted --- changes | 5 +- helix-term/src/ui/explore.rs | 92 ++++++++++++++++++++++-------------- helix-term/src/ui/tree.rs | 17 +++---- 3 files changed, 70 insertions(+), 44 deletions(-) diff --git a/changes b/changes index f2ce7add..c833831d 100644 --- a/changes +++ b/changes @@ -37,6 +37,10 @@ New: - [x] Ctrl-o should work for 'h', 'gg', 'ge', etc - [x] add unit test for TreeView - [x] explorer(help): overflow +- [x] n/N wrap around +- [x] fix(filter): crash +- [x] fix(explorer/preview): panic if not tall enough +- [x] explorer(preview): content not sorted - [] add integration test for Explorer - [] search highlight matching word - [] Error didn't clear @@ -45,5 +49,4 @@ New: - [] Fix panic bugs (see github comments) - [] Sticky ancestors - [] explorer(preview): overflow where bufferline is there -- [] explorer(preview): content not sorted - [] explorer(preview): implement scrolling C-j/C-k diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explore.rs index f5d1982c..2457cdd7 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explore.rs @@ -13,9 +13,9 @@ use helix_view::{ theme::Modifier, DocumentId, Editor, }; -use std::borrow::Cow; use std::cmp::Ordering; use std::path::{Path, PathBuf}; +use std::{borrow::Cow, fs::DirEntry}; use tui::{ buffer::Buffer as Surface, widgets::{Block, Borders, Widget}, @@ -27,24 +27,20 @@ macro_rules! get_theme { }; } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] enum FileType { File, Folder, Root, } -#[derive(Debug, Clone)] +#[derive(PartialEq, Eq, 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, @@ -63,9 +59,13 @@ impl FileInfo { } } -impl TreeViewItem for FileInfo { - type Params = State; +impl PartialOrd for FileInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for FileInfo { fn cmp(&self, other: &Self) -> Ordering { use FileType::*; match (self.file_type, other.file_type) { @@ -85,6 +85,10 @@ impl TreeViewItem for FileInfo { } self.path.cmp(&other.path) } +} + +impl TreeViewItem for FileInfo { + type Params = State; fn get_children(&self) -> Result> { match self.file_type { @@ -93,18 +97,7 @@ impl TreeViewItem for FileInfo { }; let ret: Vec<_> = std::fs::read_dir(&self.path)? .filter_map(|entry| entry.ok()) - .filter_map(|entry| { - entry.metadata().ok().map(|meta| { - let file_type = match meta.is_dir() { - true => FileType::Folder, - false => FileType::File, - }; - Self { - file_type, - path: self.path.join(entry.file_name()), - } - }) - }) + .filter_map(|entry| dir_entry_to_file_info(entry, &self.path)) .collect(); Ok(ret) } @@ -121,6 +114,19 @@ impl TreeViewItem for FileInfo { } } +fn dir_entry_to_file_info(entry: DirEntry, path: &PathBuf) -> Option { + entry.metadata().ok().map(|meta| { + let file_type = match meta.is_dir() { + true => FileType::Folder, + false => FileType::File, + }; + FileInfo { + file_type, + path: path.join(entry.file_name()), + } + }) +} + #[derive(Clone, Debug)] enum PromptAction { CreateFolder { folder_path: PathBuf }, @@ -250,7 +256,11 @@ impl Explorer { fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) { let item = self.tree.current().item(); - let head_area = render_block(area.clip_bottom(area.height - 2), surface, Borders::BOTTOM); + let head_area = render_block( + area.clip_bottom(area.height.saturating_sub(2)), + surface, + Borders::BOTTOM, + ); let path_str = format!("{}", item.path.display()); surface.set_stringn( head_area.x, @@ -518,13 +528,17 @@ impl Explorer { } else { const PREVIEW_AREA_MAX_WIDTH: u16 = 90; const PREVIEW_AREA_MAX_HEIGHT: u16 = 30; - let preview_area_width = (area.width - side_area.width).min(PREVIEW_AREA_MAX_WIDTH); + let preview_area_width = + (area.width.saturating_sub(side_area.width)).min(PREVIEW_AREA_MAX_WIDTH); let preview_area_height = area.height.min(PREVIEW_AREA_MAX_HEIGHT); let preview_area = match position { ExplorerPositionEmbed::Left => area.clip_left(side_area.width), ExplorerPositionEmbed::Right => (Rect { - x: area.width - side_area.width - preview_area_width, + x: area + .width + .saturating_sub(side_area.width) + .saturating_sub(preview_area_width), ..area }) .clip_right(side_area.width), @@ -535,7 +549,7 @@ impl Explorer { } let y = self.tree.winline().saturating_sub(1) as u16; let y = if (preview_area_height + y) > preview_area.height { - preview_area.height - preview_area_height + preview_area.height.saturating_sub(preview_area_height) } else { y }; @@ -761,12 +775,12 @@ impl Component for Explorer { let (x, y) = if config.is_overlay() { let colw = self.column_width as u16; if area.width > colw { - (area.x + colw + 2, area.y + area.height - 2) + (area.x + colw + 2, area.y + area.height.saturating_sub(2)) } else { return (None, CursorKind::Hidden); } } else { - (area.x, area.y + area.height - 1) + (area.x, area.y + area.height.saturating_sub(1)) }; prompt.cursor(Rect::new(x, y, area.width, 1), editor) } @@ -775,16 +789,24 @@ impl Component for Explorer { fn get_preview(p: impl AsRef, max_line: usize) -> Result> { let p = p.as_ref(); if p.is_dir() { - return Ok(p + let mut entries = p .read_dir()? - .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + entry + .ok() + .map(|entry| dir_entry_to_file_info(entry, &p.to_path_buf())) + .flatten() + }) .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::>(); + + entries.sort(); + + return Ok(entries + .into_iter() + .map(|entry| match entry.file_type { + FileType::Folder => format!("{}/", entry.name()), + _ => entry.name(), }) .collect()); } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index fb52d0ba..d83577d4 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -16,13 +16,13 @@ use tui::buffer::Buffer as Surface; use super::Prompt; -pub trait TreeViewItem: Sized { +pub trait TreeViewItem: Sized + Ord { type Params; // fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; fn name(&self) -> String; fn is_parent(&self) -> bool; - fn cmp(&self, other: &Self) -> Ordering; + // fn cmp(&self, other: &Self) -> Ordering; fn filter(&self, s: &str) -> bool { self.name().to_lowercase().contains(&s.to_lowercase()) @@ -36,7 +36,8 @@ fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { } fn vec_to_tree(mut items: Vec) -> Vec> { - items.sort_by(tree_item_cmp); + items.sort(); + // items.sort_by(tree_item_cmp); index_elems( 0, items @@ -1149,7 +1150,7 @@ mod test_tree_view { use super::{vec_to_tree, TreeView, TreeViewItem}; use pretty_assertions::assert_eq; - #[derive(Clone)] + #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] struct Item<'a> { name: &'a str, } @@ -1169,10 +1170,6 @@ mod test_tree_view { self.name.len() > 2 } - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.name.cmp(other.name) - } - fn get_children(&self) -> anyhow::Result> { if self.is_parent() { let (left, right) = self.name.split_at(self.name.len() / 2); @@ -1181,6 +1178,10 @@ mod test_tree_view { Ok(vec![]) } } + + fn filter(&self, s: &str) -> bool { + self.name().to_lowercase().contains(&s.to_lowercase()) + } } fn dummy_tree_view<'a>() -> TreeView> { -- 2.38.5 From 6321dc9adec5c7026ed75f50345f278910ffe2ab Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Feb 2023 14:05:53 +0800 Subject: [PATCH 040/191] chore: rename explore to explorer --- helix-term/src/ui/{explore.rs => explorer.rs} | 3 +++ helix-term/src/ui/mod.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) rename helix-term/src/ui/{explore.rs => explorer.rs} (99%) diff --git a/helix-term/src/ui/explore.rs b/helix-term/src/ui/explorer.rs similarity index 99% rename from helix-term/src/ui/explore.rs rename to helix-term/src/ui/explorer.rs index 2457cdd7..fae77db6 100644 --- a/helix-term/src/ui/explore.rs +++ b/helix-term/src/ui/explorer.rs @@ -831,3 +831,6 @@ fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect { block.render(area, surface); inner } + +#[cfg(test)] +mod test_explore {} diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 0dba41e1..12701f64 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,7 +1,7 @@ mod completion; mod document; pub(crate) mod editor; -mod explore; +mod explorer; mod fuzzy_match; mod info; pub mod lsp; @@ -21,7 +21,7 @@ use crate::filter_picker_entry; use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; -pub use explore::Explorer; +pub use explorer::Explorer; pub use markdown::Markdown; pub use menu::Menu; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; -- 2.38.5 From 7b63fda7d2f7e6e7ed63e85300a7d7435f79648b Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 23 Feb 2023 10:28:33 +0800 Subject: [PATCH 041/191] test(explorer): add integration tests --- Cargo.lock | 80 +++ changes | 4 +- helix-term/.gitignore | 1 + helix-term/Cargo.toml | 1 + helix-term/src/ui/explorer.rs | 981 ++++++++++++++++++++++++++++------ helix-term/src/ui/tree.rs | 93 ++-- 6 files changed, 952 insertions(+), 208 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7be16699..5129c135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,20 @@ dependencies = [ "num-traits", ] +[[package]] +name = "build-fs-tree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85199b032e7d08f84570a62dc4b59d4ef37e094939d634e9dddd161515ec3ba9" +dependencies = [ + "derive_more", + "pipe-trait", + "serde", + "serde_yaml", + "text-block-macros", + "thiserror", +] + [[package]] name = "bumpalo" version = "3.11.1" @@ -235,6 +249,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -352,6 +372,19 @@ dependencies = [ "parking_lot_core 0.9.4", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -1214,6 +1247,7 @@ version = "0.6.0" dependencies = [ "anyhow", "arc-swap", + "build-fs-tree", "chrono", "content_inspector", "crossterm", @@ -1701,6 +1735,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pipe-trait" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1be1ec9e59f0360aefe84efa6f699198b685ab0d5718081e9f72aa2344289e2" + [[package]] name = "pretty_assertions" version = "1.3.0" @@ -1849,6 +1889,15 @@ dependencies = [ "str_indices", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.9" @@ -1882,6 +1931,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" + [[package]] name = "serde" version = "1.0.152" @@ -1933,6 +1988,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -2093,6 +2161,12 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "text-block-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8b59b4da1c1717deaf1de80f0179a9d8b4ac91c986d5fd9f4a8ff177b84049" + [[package]] name = "textwrap" version = "0.16.0" @@ -2336,6 +2410,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unsafe-libyaml" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" + [[package]] name = "url" version = "2.3.1" diff --git a/changes b/changes index c833831d..ccb65caa 100644 --- a/changes +++ b/changes @@ -41,7 +41,7 @@ New: - [x] fix(filter): crash - [x] fix(explorer/preview): panic if not tall enough - [x] explorer(preview): content not sorted -- [] add integration test for Explorer +- [x] add integration test for Explorer - [] search highlight matching word - [] Error didn't clear - [] bind "o" to open/close file/folder @@ -50,3 +50,5 @@ New: - [] Sticky ancestors - [] explorer(preview): overflow where bufferline is there - [] explorer(preview): implement scrolling C-j/C-k +- [] symlink not showing +- [] remove unwrap and expect diff --git a/helix-term/.gitignore b/helix-term/.gitignore index ea8c4bf7..3a070c39 100644 --- a/helix-term/.gitignore +++ b/helix-term/.gitignore @@ -1 +1,2 @@ /target +test-explorer diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 4204e4dc..823568b6 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -81,3 +81,4 @@ smallvec = "1.10" indoc = "2.0.0" tempfile = "3.3.0" pretty_assertions = "1.3.0" +build-fs-tree = "0.4.1" diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index fae77db6..b297c610 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, EventResult}, ctrl, key, shift, ui, }; -use anyhow::{ensure, Result}; +use anyhow::{bail, ensure, Result}; use helix_core::Position; use helix_view::{ editor::{Action, ExplorerPositionEmbed}, @@ -11,7 +11,7 @@ use helix_view::{ info::Info, input::{Event, KeyEvent}, theme::Modifier, - DocumentId, Editor, + Editor, }; use std::cmp::Ordering; use std::path::{Path, PathBuf}; @@ -129,11 +129,11 @@ fn dir_entry_to_file_info(entry: DirEntry, path: &PathBuf) -> Option { #[derive(Clone, Debug)] enum PromptAction { - CreateFolder { folder_path: PathBuf }, - CreateFile { folder_path: PathBuf }, - RemoveDir, - RemoveFile(Option), - RenameFile(Option), + CreateFolder, + CreateFile, + RemoveFolder, + RemoveFile, + RenameFile, } #[derive(Clone, Debug)] @@ -174,7 +174,7 @@ impl Explorer { Ok(Self { tree: Self::new_tree_view(current_root.clone())?, history: vec![], - show_help: true, + show_help: false, state: State::new(true, current_root), prompt: None, on_next_key: None, @@ -182,6 +182,19 @@ impl Explorer { }) } + #[cfg(test)] + fn from_path(root: PathBuf, column_width: u16) -> Result { + Ok(Self { + tree: Self::new_tree_view(root.clone())?, + history: vec![], + show_help: true, + state: State::new(true, root), + prompt: None, + on_next_key: None, + column_width, + }) + } + fn new_tree_view(root: PathBuf) -> Result> { let root = FileInfo::root(root.clone()); let children = root.get_children()?; @@ -194,35 +207,46 @@ impl Explorer { Vec::truncate(&mut self.history, MAX_HISTORY_SIZE) } - fn change_root(&mut self, cx: &mut Context, root: PathBuf) { + fn change_root(&mut self, root: PathBuf) -> Result<()> { if self.state.current_root.eq(&root) { - return; - } - match Self::new_tree_view(root.clone()) { - Ok(tree) => { - let old_tree = std::mem::replace(&mut self.tree, tree); - self.push_history(old_tree); - self.state.current_root = root; - } - Err(e) => cx.editor.set_error(format!("{e}")), + return Ok(()); } + let tree = Self::new_tree_view(root.clone())?; + let old_tree = std::mem::replace(&mut self.tree, tree); + self.push_history(old_tree); + self.state.current_root = root; + Ok(()) } fn reveal_file(&mut self, path: PathBuf) -> Result<()> { let current_root = &self.state.current_root; let current_path = path.as_path().to_string_lossy().to_string(); let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; - let segments = current_path - .strip_prefix(current_root.as_str()) - .expect( - format!( - "Failed to strip prefix '{}' from '{}'", - current_root, current_path - ) - .as_str(), - ) - .split(std::path::MAIN_SEPARATOR) - .collect::>(); + let segments = { + let stripped = match current_path.strip_prefix(current_root.as_str()) { + Some(stripped) => Ok(stripped), + None => { + let parent = path + .parent() + .ok_or_else(|| anyhow::anyhow!("Failed get parent of '{current_path}'"))?; + self.change_root(parent.into())?; + current_path + .strip_prefix((parent.to_string_lossy().to_string() + "/").as_str()) + .ok_or_else(|| { + anyhow::anyhow!( + "Failed to strip prefix (parent) '{}' from '{}'", + parent.to_string_lossy(), + current_path + ) + }) + } + }?; + + stripped + .split(std::path::MAIN_SEPARATOR) + .map(|s| s.to_string()) + .collect::>() + }; self.tree.reveal_item(segments, &self.state.filter)?; Ok(()) } @@ -241,11 +265,11 @@ impl Explorer { self.state.open = true; } - pub fn unfocus(&mut self) { + fn unfocus(&mut self) { self.state.focus = false; } - pub fn close(&mut self) { + fn close(&mut self) { self.state.focus = false; self.state.open = false; } @@ -288,9 +312,7 @@ impl Explorer { fn new_create_folder_prompt(&mut self) -> Result<()> { let folder_path = self.nearest_folder()?; self.prompt = Some(( - PromptAction::CreateFolder { - folder_path: folder_path.clone(), - }, + PromptAction::CreateFolder, Prompt::new( format!(" New folder: {}/", folder_path.to_string_lossy()).into(), None, @@ -304,9 +326,7 @@ impl Explorer { fn new_create_file_prompt(&mut self) -> Result<()> { let folder_path = self.nearest_folder()?; self.prompt = Some(( - PromptAction::CreateFile { - folder_path: folder_path.clone(), - }, + PromptAction::CreateFile, Prompt::new( format!(" New file: {}/", folder_path.to_string_lossy()).into(), None, @@ -332,19 +352,19 @@ impl Explorer { } } - fn new_remove_prompt(&mut self, cx: &mut Context) { + fn new_remove_prompt(&mut self) -> Result<()> { let item = self.tree.current().item(); match item.file_type { - FileType::Folder => self.new_remove_dir_prompt(cx), - FileType::File => self.new_remove_file_prompt(cx), - FileType::Root => cx.editor.set_error("Root is not removable"), + FileType::Folder => self.new_remove_folder_prompt(), + FileType::File => self.new_remove_file_prompt(), + FileType::Root => bail!("Root is not removable"), } } fn new_rename_prompt(&mut self, cx: &mut Context) { let path = self.tree.current_item().path.clone(); self.prompt = Some(( - PromptAction::RenameFile(cx.editor.document_by_path(&path).map(|doc| doc.id())), + PromptAction::RenameFile, Prompt::new( format!(" Rename to ").into(), None, @@ -355,72 +375,67 @@ impl Explorer { )); } - fn new_remove_file_prompt(&mut self, cx: &mut Context) { + fn new_remove_file_prompt(&mut self) -> Result<()> { let item = self.tree.current_item(); - let check = || { - ensure!(item.path.is_file(), "The path is not a file"); - let doc = cx.editor.document_by_path(&item.path); - Ok(doc.map(|doc| doc.id())) - }; - match check() { - Err(err) => cx.editor.set_error(format!("{err}")), - Ok(document_id) => { - let p = format!(" Delete file: '{}'? y/n: ", item.path.display()); - self.prompt = Some(( - PromptAction::RemoveFile(document_id), - Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), - )); - } - } + ensure!( + item.path.is_file(), + "The path '{}' is not a file", + item.path.to_string_lossy() + ); + self.prompt = Some(( + PromptAction::RemoveFile, + Prompt::new( + format!(" Delete file: '{}'? y/n: ", item.path.display()).into(), + None, + ui::completers::none, + |_, _, _| {}, + ), + )); + Ok(()) } - fn new_remove_dir_prompt(&mut self, cx: &mut Context) { + fn new_remove_folder_prompt(&mut self) -> Result<()> { let item = self.tree.current_item(); - let check = || { - 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!(" Delete folder: '{}'? y/n: ", item.path.display()); + ensure!( + item.path.is_dir(), + "The path '{}' is not a folder", + item.path.to_string_lossy() + ); + self.prompt = Some(( - PromptAction::RemoveDir, - Prompt::new(p.into(), None, ui::completers::none, |_, _, _| {}), + PromptAction::RemoveFolder, + Prompt::new( + format!(" Delete folder: '{}'? y/n: ", item.path.display()).into(), + None, + ui::completers::none, + |_, _, _| {}, + ), )); + Ok(()) } fn toggle_current(item: &mut FileInfo, cx: &mut Context, state: &mut State) -> TreeOp { - 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; + (|| -> Result { + if item.path == Path::new("") { + return Ok(TreeOp::Noop); } - }; - if meta.is_file() { - if let Err(e) = cx.editor.open(&item.path, Action::Replace) { - cx.editor.set_error(format!("{e}")); + let meta = std::fs::metadata(&item.path)?; + if meta.is_file() { + cx.editor.open(&item.path, Action::Replace)?; + state.focus = false; + return Ok(TreeOp::Noop); } - state.focus = false; - return TreeOp::Noop; - } - if item.path.is_dir() { - return TreeOp::GetChildsAndInsert; - } - cx.editor.set_error("unkonw file type"); - TreeOp::Noop + if item.path.is_dir() { + return Ok(TreeOp::GetChildsAndInsert); + } + + Err(anyhow::anyhow!("Unknown file type: {:?}", meta.file_type())) + })() + .unwrap_or_else(|err| { + cx.editor.set_error(format!("{err}")); + TreeOp::Noop + }) } fn render_float(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { @@ -547,12 +562,13 @@ impl Explorer { if preview_area.width < 30 || preview_area.height < 3 { return; } - let y = self.tree.winline().saturating_sub(1) as u16; + let y = self.tree.winline() as u16; let y = if (preview_area_height + y) > preview_area.height { preview_area.height.saturating_sub(preview_area_height) } else { y - }; + } + .saturating_add(1); let area = Rect::new(preview_area.x, y, preview_area_width, preview_area_height); surface.clear_with(area, background); let area = render_block(area, surface, Borders::all()); @@ -599,38 +615,26 @@ impl Explorer { _ => return Ok(EventResult::Ignored(None)), }; let line = prompt.line(); + + let current_item_path = explorer.tree.current_item().path.clone(); match (&action, event) { - (PromptAction::CreateFolder { folder_path }, key!(Enter)) => { - explorer.new_path(folder_path.clone(), line, true)? - } - (PromptAction::CreateFile { folder_path }, key!(Enter)) => { - explorer.new_path(folder_path.clone(), line, false)? - } - (PromptAction::RemoveDir, key!(Enter)) => { + (PromptAction::CreateFolder, key!(Enter)) => explorer.new_folder(line)?, + (PromptAction::CreateFile, key!(Enter)) => explorer.new_file(line)?, + (PromptAction::RemoveFolder, key!(Enter)) => { if line == "y" { - let item = explorer.tree.current_item(); - std::fs::remove_dir_all(&item.path)?; - explorer.tree.refresh()?; + close_documents(current_item_path, cx)?; + explorer.remove_folder()?; } } - (PromptAction::RemoveFile(document_id), key!(Enter)) => { + (PromptAction::RemoveFile, key!(Enter)) => { if line == "y" { - let item = explorer.tree.current_item(); - std::fs::remove_file(&item.path).map_err(anyhow::Error::from)?; - explorer.tree.refresh()?; - if let Some(id) = document_id { - cx.editor.close_document(*id, true)? - } + close_documents(current_item_path, cx)?; + explorer.remove_file()?; } } - (PromptAction::RenameFile(document_id), key!(Enter)) => { - let item = explorer.tree.current_item(); - std::fs::rename(&item.path, line)?; - explorer.tree.refresh()?; - explorer.reveal_file(PathBuf::from(line))?; - if let Some(id) = document_id { - cx.editor.close_document(*id, true)? - } + (PromptAction::RenameFile, key!(Enter)) => { + close_documents(current_item_path, cx)?; + explorer.rename_current(line)?; } (_, key!(Esc) | ctrl!('c')) => {} _ => { @@ -649,18 +653,21 @@ impl Explorer { } } - fn new_path(&mut self, current_parent: PathBuf, file_name: &str, is_dir: bool) -> Result<()> { + fn new_file(&mut self, file_name: &str) -> Result<()> { + let current_parent = self.nearest_folder()?; let path = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut fd = std::fs::OpenOptions::new(); + fd.create_new(true).write(true).open(&path)?; + self.reveal_file(path) + } - if is_dir { - std::fs::create_dir_all(&path)?; - } else { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - let mut fd = std::fs::OpenOptions::new(); - fd.create_new(true).write(true).open(&path)?; - }; + fn new_folder(&mut self, file_name: &str) -> Result<()> { + let current_parent = self.nearest_folder()?; + let path = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); + std::fs::create_dir_all(&path)?; self.reveal_file(path) } @@ -674,6 +681,19 @@ impl Explorer { } } + fn change_root_to_current_folder(&mut self) -> Result<()> { + self.change_root(self.tree.current_item().path.clone()) + } + + fn change_root_parent_folder(&mut self) -> Result<()> { + if let Some(parent) = self.state.current_root.parent().clone() { + let path = parent.to_path_buf(); + self.change_root(path) + } else { + Ok(()) + } + } + pub fn is_opened(&self) -> bool { self.state.open } @@ -693,6 +713,53 @@ impl Explorer { fn decrease_size(&mut self) { self.column_width = self.column_width.saturating_sub(1) } + + fn rename_current(&mut self, line: &String) -> Result<()> { + let item = self.tree.current_item(); + let path = PathBuf::from(line); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(&item.path, &path)?; + self.tree.refresh()?; + self.reveal_file(path.into()) + } + + fn remove_folder(&mut self) -> Result<()> { + let item = self.tree.current_item(); + std::fs::remove_dir_all(&item.path)?; + self.tree.refresh() + } + + fn remove_file(&mut self) -> Result<()> { + let item = self.tree.current_item(); + std::fs::remove_file(&item.path)?; + self.tree.refresh() + } +} + +fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { + let ids = cx + .editor + .documents + .iter() + .filter_map(|(id, doc)| { + if doc + .path() + .map(|p| p.starts_with(¤t_item_path)) + .unwrap_or(false) + { + Some(id.clone()) + } else { + None + } + }) + .collect::>(); + + for id in ids { + cx.editor.close_document(id, true)?; + } + Ok(()) } impl Component for Explorer { @@ -718,37 +785,28 @@ impl Component for Explorer { return EventResult::Consumed(c); } - match key_event { - key!(Esc) => self.unfocus(), - key!('q') => self.close(), - key!('?') => self.toggle_help(), - key!('a') => { - if let Err(error) = self.new_create_file_prompt() { - cx.editor.set_error(error.to_string()) - } - } - shift!('A') => { - if let Err(error) = self.new_create_folder_prompt() { - cx.editor.set_error(error.to_string()) - } - } - key!('b') => { - if let Some(parent) = self.state.current_root.parent().clone() { - let path = parent.to_path_buf(); - self.change_root(cx, path) + (|| -> Result<()> { + match key_event { + key!(Esc) => self.unfocus(), + key!('q') => self.close(), + key!('?') => self.toggle_help(), + key!('a') => self.new_create_file_prompt()?, + shift!('A') => self.new_create_folder_prompt()?, + key!('b') => self.change_root_parent_folder()?, + key!(']') => self.change_root_to_current_folder()?, + key!('[') => self.go_to_previous_root(), + key!('d') => self.new_remove_prompt()?, + key!('r') => self.new_rename_prompt(cx), + key!('-') => self.decrease_size(), + key!('+') => self.increase_size(), + _ => { + self.tree + .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); } - } - key!(']') => self.change_root(cx, self.tree.current_item().path.clone()), - key!('[') => self.go_to_previous_root(), - key!('d') => self.new_remove_prompt(cx), - key!('r') => self.new_rename_prompt(cx), - key!('-') => self.decrease_size(), - key!('+') => self.increase_size(), - _ => { - self.tree - .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); - } - } + }; + Ok(()) + })() + .unwrap_or_else(|err| cx.editor.set_error(format!("{err}"))); EventResult::Consumed(None) } @@ -833,4 +891,599 @@ fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect { } #[cfg(test)] -mod test_explore {} +mod test_explorer { + use super::Explorer; + use helix_view::graphics::Rect; + use pretty_assertions::assert_eq; + use std::{fs, path::PathBuf}; + + fn dummy_file_tree<'a>(name: &'a str) -> PathBuf { + use build_fs_tree::{dir, file, Build, MergeableFileSystemTree}; + let tree = MergeableFileSystemTree::<&str, &str>::from(dir! { + "index.html" => file!("") + "scripts" => dir! { + "main.js" => file!("") + } + "styles" => dir! { + "style.css" => file!("") + "public" => dir! { + "file" => file!("") + } + } + ".gitignore" => file!("") + }); + let path: PathBuf = format!("test-explorer/{}", name).into(); + if path.exists() { + fs::remove_dir_all(path.clone()).unwrap(); + } + tree.build(&path).unwrap(); + path + } + + fn render<'a>(explorer: &mut Explorer) -> String { + explorer + .tree + .render_to_string(Rect::new(0, 0, 50, 10), &"".to_string()) + } + + fn new_explorer<'a>(name: &'a str) -> (PathBuf, Explorer) { + let path = dummy_file_tree(name); + (path.clone(), Explorer::from_path(path, 30).unwrap()) + } + + #[test] + fn test_reveal_file() { + let (path, mut explorer) = new_explorer("reveal_file"); + + // 0a. Expect the "scripts" folder is not opened + assert_eq!( + render(&mut explorer), + " +(test-explorer/reveal_file) + scripts + styles + .gitignore + index.html +" + .trim() + ); + + // 1. Reveal "scripts/main.js" + explorer.reveal_file(path.join("scripts/main.js")).unwrap(); + + // 1a. Expect the "scripts" folder is opened, and "main.js" is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/reveal_file] + [scripts] + (main.js) + styles + .gitignore + index.html +" + .trim() + ); + + // 2. Change root to "scripts" + explorer.tree.move_up(1); + explorer.change_root_to_current_folder().unwrap(); + + // 2a. Expect the current root is "scripts" + assert_eq!( + render(&mut explorer), + " +(test-explorer/reveal_file/scripts) + main.js +" + .trim() + ); + + // 3. Reveal "styles/public/file", which is outside of the current root + explorer + .reveal_file(path.join("styles/public/file")) + .unwrap(); + + // 3a. Expect the current root is "public", and "file" is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/reveal_file/styles/public] + (file) +" + .trim() + ); + } + + #[test] + fn test_rename() { + let (path, mut explorer) = new_explorer("rename"); + + explorer.tree.move_down(3); + assert_eq!( + render(&mut explorer), + " +[test-explorer/rename] + scripts + styles + (.gitignore) + index.html +" + .trim() + ); + + // 1. Rename the current file to a name that is lexicographically greater than "index.html" + explorer + .rename_current(&path.join("who.is").to_string_lossy().into()) + .unwrap(); + + // 1a. Expect the file is renamed, and is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/rename] + scripts + styles + index.html + (who.is) +" + .trim() + ); + + assert!(path.join("who.is").exists()); + + // 2. Rename the current file into an existing folder + explorer + .rename_current(&path.join("styles/lol").to_string_lossy().into()) + .unwrap(); + + // 2a. Expect the file is moved to the folder, and is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/rename] + scripts + [styles] +  public + (lol) + style.css + index.html +" + .trim() + ); + + assert!(path.join("styles/lol").exists()); + + // 3. Rename the current file into a non-existent folder + explorer + .rename_current(&path.join("new_folder/sponge/bob").to_string_lossy().into()) + .unwrap(); + + // 3a. Expect the non-existent folder to be created, + // and the file is moved into it, + // and the renamed file is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/rename] + [new_folder] +  [sponge] + (bob) + scripts + styles +  public + style.css + index.html +" + .trim() + ); + + assert!(path.join("new_folder/sponge/bob").exists()); + + // 4. Change current root to "new_folder/sponge" + explorer.tree.move_up(1); + explorer.change_root_to_current_folder().unwrap(); + + // 4a. Expect the current root to be "sponge" + assert_eq!( + render(&mut explorer), + " +(test-explorer/rename/new_folder/sponge) + bob +" + .trim() + ); + + // 5. Move cursor to "bob", and move it outside of the current root + explorer.tree.move_down(1); + explorer + .rename_current(&path.join("scripts/bob").to_string_lossy().into()) + .unwrap(); + + // 5a. Expect the current root to be "scripts" + assert_eq!( + render(&mut explorer), + " +[test-explorer/rename/scripts] + (bob) + main.js +" + .trim() + ); + } + + #[test] + fn test_new_folder() { + let (path, mut explorer) = new_explorer("new_folder"); + + // 1. Add a new folder at the root + explorer.new_folder("yoyo").unwrap(); + + // 1a. Expect the new folder is added, and is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/new_folder] + scripts + styles + (yoyo) + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("yoyo")).is_ok()); + + // 2. Move up to "styles" + explorer.tree.move_up(1); + + // 3. Add a new folder + explorer.new_folder("sus.sass").unwrap(); + + // 3a. Expect the new folder is added under "styles", although "styles" is not opened + assert_eq!( + render(&mut explorer), + " +[test-explorer/new_folder] + scripts + [styles] +  public +  (sus.sass) + style.css + yoyo + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles/sus.sass")).is_ok()); + + // 4. Add a new folder with non-existent parents + explorer.new_folder("a/b/c").unwrap(); + + // 4a. Expect the non-existent parents are created, + // and the new folder is created, + // and is focused + assert_eq!( + render(&mut explorer), + " + [styles] +  public +  [sus.sass] +  [a] +  [b] +  (c) + style.css + yoyo + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles/sus.sass/a/b/c")).is_ok()); + + // 5. Move to "style.css" + explorer.tree.move_down(1); + + // 6. Add a new folder here + explorer.new_folder("foobar").unwrap(); + + // 6a. Expect the folder is added under "styles", + // because the folder of the current item, "style.css" is "styles/" + assert_eq!( + render(&mut explorer), + " +[test-explorer/new_folder] + scripts + [styles] +  (foobar) +  public +  sus.sass +  a +  b +  c + style.css +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles/foobar")).is_ok()); + } + + #[test] + fn test_new_file() { + let (path, mut explorer) = new_explorer("new_file"); + // 1. Add a new file at the root + explorer.new_file("yoyo").unwrap(); + + // 1a. Expect the new file is added, and is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/new_file] + scripts + styles + .gitignore + index.html + (yoyo) +" + .trim() + ); + + assert!(fs::read_to_string(path.join("yoyo")).is_ok()); + + // 2. Move up to "styles" + explorer.tree.move_up(3); + + // 3. Add a new file + explorer.new_file("sus.sass").unwrap(); + + // 3a. Expect the new file is added under "styles", although "styles" is not opened + assert_eq!( + render(&mut explorer), + " +[test-explorer/new_file] + scripts + [styles] +  public + style.css + (sus.sass) + .gitignore + index.html + yoyo +" + .trim() + ); + + assert!(fs::read_to_string(path.join("styles/sus.sass")).is_ok()); + + // 4. Add a new file with non-existent parents + explorer.new_file("a/b/c").unwrap(); + + // 4a. Expect the non-existent parents are created, + // and the new file is created, + // and is focused + assert_eq!( + render(&mut explorer), + " +[test-explorer/new_file] + scripts + [styles] +  [a] +  [b] + (c) +  public + style.css + sus.sass + .gitignore +" + .trim() + ); + + assert!(fs::read_to_string(path.join("styles/a/b/c")).is_ok()); + + // 5. Move to "style.css" + explorer.tree.move_down(2); + + // 6. Add a new file here + explorer.new_file("foobar").unwrap(); + + // 6a. Expect the file is added under "styles", + // because the folder of the current item, "style.css" is "styles/" + assert_eq!( + render(&mut explorer), + " + [styles] +  a +  b + c +  public + (foobar) + style.css + sus.sass + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_to_string(path.join("styles/foobar")).is_ok()); + } + + #[test] + fn test_remove_file() { + let (path, mut explorer) = new_explorer("remove_file"); + + // 1. Move to ".gitignore" + explorer.reveal_file(path.join(".gitignore")).unwrap(); + + // 1a. Expect the cursor is at ".gitignore" + assert_eq!( + render(&mut explorer), + " +[test-explorer/remove_file] + scripts + styles + (.gitignore) + index.html +" + .trim() + ); + + assert!(fs::read_to_string(path.join(".gitignore")).is_ok()); + + // 2. Remove the current file + explorer.remove_file().unwrap(); + + // 3. Expect ".gitignore" is deleted, and the cursor moved down + assert_eq!( + render(&mut explorer), + " +[test-explorer/remove_file] + scripts + styles + (index.html) +" + .trim() + ); + + assert!(fs::read_to_string(path.join(".gitignore")).is_err()); + + // 3a. Expect "index.html" exists + assert!(fs::read_to_string(path.join("index.html")).is_ok()); + + // 4. Remove the current file + explorer.remove_file().unwrap(); + + // 4a. Expect "index.html" is deleted, at the cursor moved up + assert_eq!( + render(&mut explorer), + " +[test-explorer/remove_file] + scripts + (styles) +" + .trim() + ); + + assert!(fs::read_to_string(path.join("index.html")).is_err()); + } + + #[test] + fn test_remove_folder() { + let (path, mut explorer) = new_explorer("remove_folder"); + + // 1. Move to "styles/" + explorer.reveal_file(path.join("styles")).unwrap(); + + // 1a. Expect the cursor is at "styles" + assert_eq!( + render(&mut explorer), + " +[test-explorer/remove_folder] + scripts + (styles) +  public + style.css + .gitignore + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles")).is_ok()); + + // 2. Remove the current folder + explorer.remove_folder().unwrap(); + + // 3. Expect "styles" is deleted, and the cursor moved down + assert_eq!( + render(&mut explorer), + " +[test-explorer/remove_folder] + scripts + (.gitignore) + index.html +" + .trim() + ); + + assert!(fs::read_dir(path.join("styles")).is_err()); + } + + #[test] + fn test_change_root() { + let (path, mut explorer) = new_explorer("change_root"); + + // 1. Move cursor to "styles" + explorer.reveal_file(path.join("styles")).unwrap(); + + // 2. Change root to current folder, and move cursor down + explorer.change_root_to_current_folder().unwrap(); + explorer.tree.move_down(1); + + // 2a. Expect the current root to be "styles", and the cursor is at "public" + assert_eq!( + render(&mut explorer), + " +[test-explorer/change_root/styles] + (public) + style.css +" + .trim() + ); + + // 3. Change root to the parent of current folder + explorer.change_root_parent_folder().unwrap(); + + // 3a. Expect the current root to be "change_root" + assert_eq!( + render(&mut explorer), + " +(test-explorer/change_root) + scripts + styles + .gitignore + index.html +" + .trim() + ); + + // 4. Go back to previous root + explorer.go_to_previous_root(); + + // 4a. Expect the root te become "styles", and the cursor position is not forgotten + assert_eq!( + render(&mut explorer), + " +[test-explorer/change_root/styles] + (public) + style.css +" + .trim() + ); + + // 5. Go back to previous root again + explorer.go_to_previous_root(); + + // 5a. Expect the current root to be "change_root" again, + // but this time the "styles" folder is opened, + // because it was opened before any change of root + assert_eq!( + render(&mut explorer), + " +[test-explorer/change_root] + scripts + (styles) +  public + style.css + .gitignore + index.html +" + .trim() + ); + } +} diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index d83577d4..486580c0 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -315,8 +315,6 @@ pub struct TreeView { /// For implementing vertical scroll winline: usize, - previous_area: Rect, - /// For implementing horizontal scoll column: usize, @@ -324,10 +322,16 @@ pub struct TreeView { 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>, } @@ -343,8 +347,8 @@ impl TreeView { column: 0, max_len: 0, count: 0, - previous_area: Rect::new(0, 0, 0, 0), tree_symbol_style: "ui.text".into(), + pre_render: None, on_opened_fn: None, on_folded_fn: None, on_next_key: None, @@ -388,10 +392,11 @@ impl TreeView { /// ``` /// vec!["helix-term", "src", "ui", "tree.rs"] /// ``` - pub fn reveal_item(&mut self, segments: Vec<&str>, filter: &String) -> Result<()> { + pub fn reveal_item(&mut self, segments: Vec, filter: &String) -> Result<()> { self.refresh_with_filter(filter)?; // Expand the tree + let root = self.tree.item.name(); segments.iter().fold( Ok(&mut self.tree), |current_tree, segment| match current_tree { @@ -409,9 +414,8 @@ impl TreeView { Ok(tree) } None => Err(anyhow::anyhow!(format!( - "Unable to find path: '{}'. current_segment = {}", + "Unable to find path: '{}'. current_segment = '{segment}'. current_root = '{root}'", segments.join("/"), - segment ))), } } @@ -437,7 +441,9 @@ impl TreeView { } fn align_view_center(&mut self) { - self.winline = self.previous_area.height as usize / 2 + self.pre_render = Some(Box::new(|tree, area| { + tree.winline = area.height as usize / 2 + })) } fn align_view_top(&mut self) { @@ -445,7 +451,7 @@ impl TreeView { } fn align_view_bottom(&mut self) { - self.winline = self.previous_area.height as usize + self.pre_render = Some(Box::new(|tree, area| tree.winline = area.height as usize)) } fn regenerate_index(&mut self) { @@ -492,11 +498,6 @@ impl TreeView { self.move_down(usize::MAX / 2) } - #[cfg(test)] - fn set_previous_area(&mut self, area: Rect) { - self.previous_area = area - } - fn move_leftmost(&mut self) { self.move_left(usize::MAX / 2); } @@ -608,7 +609,7 @@ impl TreeView { self.search_previous(&self.search_str.clone()) } - fn move_down(&mut self, rows: usize) { + pub fn move_down(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { self.set_selected(std::cmp::min(self.selected + rows, len.saturating_sub(1))) @@ -647,7 +648,7 @@ impl TreeView { } } - fn move_up(&mut self, rows: usize) { + pub fn move_up(&mut self, rows: usize) { let len = self.tree.len(); if len > 0 { self.set_selected(self.selected.saturating_sub(rows).max(0)) @@ -659,27 +660,34 @@ impl TreeView { } fn move_right(&mut self, cols: usize) { - let max_scroll = self - .max_len - .saturating_sub(self.previous_area.width as usize) - .saturating_add(1); - self.column = max_scroll.min(self.column + cols); + self.pre_render = Some(Box::new(move |tree, area| { + let max_scroll = tree.max_len.saturating_sub(area.width as usize); + tree.column = max_scroll.min(tree.column + cols); + })); } fn move_down_half_page(&mut self) { - self.move_down(self.previous_area.height as usize / 2) + self.pre_render = Some(Box::new(|tree, area| { + tree.move_down((area.height / 2) as usize); + })); } fn move_up_half_page(&mut self) { - self.move_up(self.previous_area.height as usize / 2); + self.pre_render = Some(Box::new(|tree, area| { + tree.move_up((area.height / 2) as usize); + })); } fn move_down_page(&mut self) { - self.move_down(self.previous_area.height as usize); + self.pre_render = Some(Box::new(|tree, area| { + tree.move_down((area.height) as usize); + })); } fn move_up_page(&mut self) { - self.move_up(self.previous_area.height as usize); + self.pre_render = Some(Box::new(|tree, area| { + tree.move_up((area.height) as usize); + })); } fn save_view(&mut self) { @@ -856,8 +864,7 @@ impl TreeView { } #[cfg(test)] - fn render_to_string(&mut self, filter: &String) -> String { - let area = self.previous_area; + pub fn render_to_string(&mut self, area: Rect, filter: &String) -> String { let lines = self.render_lines(area, filter); lines .into_iter() @@ -876,10 +883,11 @@ impl TreeView { } fn render_lines(&mut self, area: Rect, filter: &String) -> Vec { - self.previous_area = area; - self.winline = self - .winline - .min(self.previous_area.height.saturating_sub(1) as usize); + if let Some(pre_render) = self.pre_render.take() { + pre_render(self, area); + } + + self.winline = self.winline.min(area.height.saturating_sub(1) as usize); let skip = self.selected.saturating_sub(self.winline); let params = RenderTreeParams { tree: &self.tree, @@ -902,7 +910,7 @@ impl TreeView { .max() .unwrap_or(0); - let max_width = self.previous_area.width as usize; + let max_width = area.width as usize; lines .into_iter() @@ -1185,9 +1193,8 @@ mod test_tree_view { } fn dummy_tree_view<'a>() -> TreeView> { - let root = item("who_lives_in_a_pineapple_under_the_sea"); - let mut view = TreeView::new( - root, + TreeView::new( + item("who_lives_in_a_pineapple_under_the_sea"), vec_to_tree(vec![ item("gary_the_snail"), item("krabby_patty"), @@ -1200,11 +1207,7 @@ mod test_tree_view { item("karen"), item("plankton"), ]), - ); - - view.set_previous_area(dummy_area()); - - view + ) } fn dummy_area() -> Rect { @@ -1212,7 +1215,7 @@ mod test_tree_view { } fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(&"".to_string()) + view.render_to_string(dummy_area(), &"".to_string()) } #[test] @@ -1393,7 +1396,6 @@ mod test_tree_view { fn test_move_half() { let mut view = dummy_tree_view(); view.move_down_half_page(); - assert_eq!(view.selected, 2); assert_eq!( render(&mut view), " @@ -1534,7 +1536,10 @@ mod test_tree_view { #[test] fn test_move_left_right() { let mut view = dummy_tree_view(); - view.set_previous_area(dummy_area().with_width(20)); + + fn render<'a>(view: &mut TreeView>) -> String { + view.render_to_string(dummy_area().with_width(20), &"".to_string()) + } assert_eq!( render(&mut view), @@ -1627,7 +1632,7 @@ krabby_patty ); view.move_rightmost(); - assert_eq!(render(&mut view), "(apple_under_the_sea)\n\n\n\n"); + assert_eq!(render(&mut view), "(eapple_under_the_sea)\n\n\n\n"); } #[test] @@ -1921,6 +1926,8 @@ krabby_patty fn test_jump_backward() { let mut view = dummy_tree_view(); view.move_down_half_page(); + render(&mut view); + view.move_down_half_page(); assert_eq!( render(&mut view), -- 2.38.5 From f9ff01dd9cff70e5a69b2e79b70357f884d466ed Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 23 Feb 2023 10:31:52 +0800 Subject: [PATCH 042/191] chore(ui/tree): bind 'o' to Toggle --- changes | 2 +- helix-term/src/ui/tree.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changes b/changes index ccb65caa..d8fd436a 100644 --- a/changes +++ b/changes @@ -42,9 +42,9 @@ New: - [x] fix(explorer/preview): panic if not tall enough - [x] explorer(preview): content not sorted - [x] add integration test for Explorer +- [x] bind "o" to open/close file/folder - [] search highlight matching word - [] Error didn't clear -- [] bind "o" to open/close file/folder - [] should preview be there by default? - [] Fix panic bugs (see github comments) - [] Sticky ancestors diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 486580c0..ee8aab3b 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -509,6 +509,7 @@ impl TreeView { pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { vec![ + ("o, Enter", "Open/Close"), ("j, down", "Down"), ("k, up", "Up"), ("h, left", "Go to parent"), @@ -988,7 +989,7 @@ impl TreeView { }, shift!('H') => self.move_left(1), shift!('L') => self.move_right(1), - key!(Enter) => self.on_enter(cx, params, self.selected, filter), + key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, filter), ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('g') => { -- 2.38.5 From 899491ba254ffc04759e5536e1f0e13446905aa3 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 23 Feb 2023 10:49:10 +0800 Subject: [PATCH 043/191] feat(tree): add C-n/C-p keybinding --- changes | 1 + helix-term/src/ui/tree.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/changes b/changes index d8fd436a..52633356 100644 --- a/changes +++ b/changes @@ -43,6 +43,7 @@ New: - [x] explorer(preview): content not sorted - [x] add integration test for Explorer - [x] bind "o" to open/close file/folder +- [x] bind "C-n/C-p" to up/down - [] search highlight matching word - [] Error didn't clear - [] should preview be there by default? diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index ee8aab3b..1b62fc5a 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -510,8 +510,8 @@ impl TreeView { pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { vec![ ("o, Enter", "Open/Close"), - ("j, down", "Down"), - ("k, up", "Up"), + ("j, down, C-n", "Down"), + ("k, up, C-p", "Up"), ("h, left", "Go to parent"), ("l, right", "Expand"), ("f", "Filter"), @@ -972,8 +972,8 @@ impl TreeView { let count = std::mem::replace(&mut self.count, 0); match key_event { key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10, - key!('k') | key!(Up) => self.move_up(1.max(count)), - key!('j') | key!(Down) => self.move_down(1.max(count)), + key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)), + key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)), key!('z') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { key!('z') => tree.align_view_center(), -- 2.38.5 From 6af9a06e74a658e03bc116c940eb427f5805e01f Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 23 Feb 2023 10:52:18 +0800 Subject: [PATCH 044/191] feat(explorer): bind "="/"_" to "Zoom in"/"Zoom out" --- changes | 2 ++ helix-term/src/ui/explorer.rs | 4 ++-- helix-term/src/ui/tree.rs | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changes b/changes index 52633356..6429aa7f 100644 --- a/changes +++ b/changes @@ -29,6 +29,7 @@ TODO - [x] refactor, add tree.expand_children() method New: +- [x] increase indentation - [x] Change '[' to "go to previous root" - [x] Change 'b' to "go to parent" - [x] Use C-o for jumping to previous position @@ -44,6 +45,7 @@ New: - [x] add integration test for Explorer - [x] bind "o" to open/close file/folder - [x] bind "C-n/C-p" to up/down +- [x] bind "="/"_" to zoom-in/zoom-out - [] search highlight matching word - [] Error didn't clear - [] should preview be there by default? diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index b297c610..e766eee6 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -797,8 +797,8 @@ impl Component for Explorer { key!('[') => self.go_to_previous_root(), key!('d') => self.new_remove_prompt()?, key!('r') => self.new_rename_prompt(cx), - key!('-') => self.decrease_size(), - key!('+') => self.increase_size(), + key!('-') | key!('_') => self.decrease_size(), + key!('+') | key!('=') => self.increase_size(), _ => { self.tree .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 1b62fc5a..b909b272 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -662,7 +662,10 @@ impl TreeView { fn move_right(&mut self, cols: usize) { self.pre_render = Some(Box::new(move |tree, area| { - let max_scroll = tree.max_len.saturating_sub(area.width as usize); + let max_scroll = tree + .max_len + .saturating_sub(area.width as usize) + .saturating_add(1); tree.column = max_scroll.min(tree.column + cols); })); } -- 2.38.5 From 92051175055259fcb0a77736dcdcd6bb880212a4 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 23 Feb 2023 11:12:58 +0800 Subject: [PATCH 045/191] fix: failing tests --- helix-term/src/ui/tree.rs | 111 ++++++++++++++------------------------ 1 file changed, 39 insertions(+), 72 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index b909b272..b68460e2 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -268,25 +268,6 @@ impl Tree { let items = std::mem::take(&mut self.children); self.children = index_elems(0, items); } - - pub fn remove(&mut self, index: usize) { - let children = std::mem::replace(&mut self.children, vec![]); - self.children = children - .into_iter() - .filter_map(|tree| { - if tree.index == index { - None - } else { - Some(tree) - } - }) - .map(|mut tree| { - tree.remove(index); - tree - }) - .collect(); - self.regenerate_index() - } } #[derive(Clone, Debug)] @@ -389,9 +370,9 @@ impl TreeView { /// The name of the root should be excluded. /// /// Example `segments`: - /// ``` - /// vec!["helix-term", "src", "ui", "tree.rs"] - /// ``` + /// + /// vec!["helix-term", "src", "ui", "tree.rs"] + /// pub fn reveal_item(&mut self, segments: Vec, filter: &String) -> Result<()> { self.refresh_with_filter(filter)?; @@ -611,10 +592,7 @@ impl TreeView { } pub fn move_down(&mut self, rows: usize) { - let len = self.tree.len(); - if len > 0 { - self.set_selected(std::cmp::min(self.selected + rows, len.saturating_sub(1))) - } + self.set_selected(self.selected.saturating_add(rows)) } fn set_selected(&mut self, selected: usize) { @@ -650,10 +628,7 @@ impl TreeView { } pub fn move_up(&mut self, rows: usize) { - let len = self.tree.len(); - if len > 0 { - self.set_selected(self.selected.saturating_sub(rows).max(0)) - } + self.set_selected(self.selected.saturating_sub(rows)) } fn move_left(&mut self, cols: usize) { @@ -1120,7 +1095,7 @@ impl TreeView { /// /// For example: /// -/// ``` +/// ```txt /// foo (0) /// bar (1) /// spam (2) @@ -1317,6 +1292,34 @@ mod test_tree_view {  karen  king_neptune  krabby_patty +" + .trim() + ); + + view.move_to_first(); + view.move_up(1); + assert_eq!( + render(&mut view), + " +(who_lives_in_a_pineapple_under_the_sea) + gary_the_snail + karen + king_neptune + krabby_patty +" + .trim() + ); + + view.move_to_last(); + view.move_down(1); + assert_eq!( + render(&mut view), + " + mrs_puff + patrick_star + plankton + sandy_cheeks + (spongebob_squarepants) " .trim() ); @@ -1636,7 +1639,7 @@ krabby_patty ); view.move_rightmost(); - assert_eq!(render(&mut view), "(eapple_under_the_sea)\n\n\n\n"); + assert_eq!(render(&mut view), "(apple_under_the_sea)\n\n\n\n"); } #[test] @@ -2016,8 +2019,7 @@ mod test_tree { assert_eq!(iter.next().map(|tree| tree.item), Some("foo")); assert_eq!(iter.next().map(|tree| tree.item), Some("bar")); - // Expect the iterator to be cyclic, so next() should jump to first item - assert_eq!(iter.next().map(|tree| tree.item), Some("spam")) + assert_eq!(iter.next().map(|tree| tree.item), None) } #[test] @@ -2036,6 +2038,7 @@ mod test_tree { assert_eq!(iter.next_back().map(|tree| tree.item), Some("yo")); assert_eq!(iter.next_back().map(|tree| tree.item), Some("jar")); assert_eq!(iter.next_back().map(|tree| tree.item), Some("spam")); + assert_eq!(iter.next_back().map(|tree| tree.item), None) } #[test] @@ -2088,7 +2091,7 @@ mod test_tree { tree.item.to_lowercase().contains(&"cargo".to_lowercase()) }); - assert_eq!(result, None); + assert_eq!(result, Some(0)); } #[test] @@ -2104,7 +2107,7 @@ mod test_tree { tree.item.to_lowercase().contains(&"cargo".to_lowercase()) }); - assert_eq!(result, None); + assert_eq!(result, Some(3)); let result = tree.find(1, Direction::Backward, |tree| { tree.item.to_lowercase().contains(&"cargo".to_lowercase()) @@ -2130,40 +2133,4 @@ mod test_tree { assert_eq!(result, Some(3)); } - - #[test] - fn test_remove() { - let mut tree = Tree::new( - ".cargo", - vec![ - Tree::new("spam", vec![Tree::new("Cargo.toml", vec![])]), - Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), - Tree::new("hello", vec![]), - ], - ); - - tree.remove(2); - - assert_eq!( - tree, - Tree::new( - ".cargo", - vec![ - Tree::new("spam", vec![]), - Tree::new("Cargo.toml", vec![Tree::new("pam", vec![])]), - Tree::new("hello", vec![]), - ], - ) - ); - - tree.remove(2); - - assert_eq!( - tree, - Tree::new( - ".cargo", - vec![Tree::new("spam", vec![]), Tree::new("hello", vec![]),], - ) - ) - } } -- 2.38.5 From cf9b60a3d1da94889aa9e9b50e4cd4b2f0a324fa Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 12:14:48 +0800 Subject: [PATCH 046/191] feat(tree): sticky ancestors --- changes | 5 +- helix-term/src/ui/explorer.rs | 8 +- helix-term/src/ui/tree.rs | 424 +++++++++++++++++++++++++++++----- 3 files changed, 377 insertions(+), 60 deletions(-) diff --git a/changes b/changes index 6429aa7f..ff97ff45 100644 --- a/changes +++ b/changes @@ -46,12 +46,15 @@ New: - [x] bind "o" to open/close file/folder - [x] bind "C-n/C-p" to up/down - [x] bind "="/"_" to zoom-in/zoom-out +- [x] Sticky ancestors +- [] Toggle preview - [] search highlight matching word - [] Error didn't clear - [] should preview be there by default? - [] Fix panic bugs (see github comments) -- [] Sticky ancestors +- [] explorer(preview): use popup instead of custom components - [] explorer(preview): overflow where bufferline is there - [] explorer(preview): implement scrolling C-j/C-k - [] symlink not showing - [] remove unwrap and expect +- [] bug(tree): zb does not work, because clash with explorer 'b' diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index e766eee6..d0d9aac1 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -593,8 +593,8 @@ impl Explorer { ("b", "Change root to parent folder"), ("]", "Change root to current folder"), ("[", "Go to previous root"), - ("+", "Increase size"), - ("-", "Decrease size"), + ("+, =", "Increase size"), + ("-, _", "Decrease size"), ("q", "Close"), ] .into_iter() @@ -1169,8 +1169,8 @@ mod test_explorer { assert_eq!( render(&mut explorer), " +[test-explorer/new_folder]  [styles] -  public  [sus.sass]  [a]  [b] @@ -1296,8 +1296,8 @@ mod test_explorer { assert_eq!( render(&mut explorer), " +[test-explorer/new_file]  [styles] -  a  b c  public diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index b68460e2..656efa33 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -471,11 +471,11 @@ impl TreeView { Ok(()) } - fn move_to_first(&mut self) { + fn move_to_first_line(&mut self) { self.move_up(usize::MAX / 2) } - fn move_to_last(&mut self) { + fn move_to_last_line(&mut self) { self.move_down(usize::MAX / 2) } @@ -712,11 +712,12 @@ impl TreeView { } } +#[derive(Clone)] struct RenderedLine { indent: String, - name: String, + content: String, selected: bool, - descendant_selected: bool, + is_ancestor_of_current_item: bool, } struct RenderTreeParams<'a, T> { tree: &'a Tree, @@ -753,8 +754,8 @@ fn render_tree( let head = RenderedLine { indent, selected: selected == tree.index, - descendant_selected: selected != tree.index && tree.get(selected).is_some(), - name, + is_ancestor_of_current_item: selected != tree.index && tree.get(selected).is_some(), + content: name, }; let prefix = format!("{}{}", prefix, if level == 0 { "" } else { " " }); vec![head] @@ -828,12 +829,12 @@ impl TreeView { surface.set_stringn( x, area.y, - line.name.clone(), + line.content.clone(), area.width .saturating_sub(indent_len) .saturating_sub(1) .into(), - if line.descendant_selected { + if line.is_ancestor_of_current_item { ancestor_style } else { style @@ -849,11 +850,11 @@ impl TreeView { .into_iter() .map(|line| { let name = if line.selected { - format!("({})", line.name) - } else if line.descendant_selected { - format!("[{}]", line.name) + format!("({})", line.content) + } else if line.is_ancestor_of_current_item { + format!("[{}]", line.content) } else { - line.name + line.content }; format!("{}{}", line.indent, name) }) @@ -884,15 +885,82 @@ impl TreeView { line.indent .chars() .count() - .saturating_add(line.name.chars().count()) + .saturating_add(line.content.chars().count()) }) .max() .unwrap_or(0); let max_width = area.width as usize; - lines + let take = area.height as usize; + + struct RetainAncestorResult { + skipped_ancestors: Vec, + remaining_lines: Vec, + } + fn retain_ancestors(lines: Vec, skip: usize) -> RetainAncestorResult { + if skip == 0 { + return RetainAncestorResult { + skipped_ancestors: vec![], + remaining_lines: lines, + }; + } + if let Some(line) = lines.get(0) { + if line.selected { + return RetainAncestorResult { + skipped_ancestors: vec![], + remaining_lines: lines, + }; + } + } + + let selected_index = lines.iter().position(|line| line.selected); + let skip = match selected_index { + None => skip, + Some(selected_index) => skip.min(selected_index), + }; + let (skipped, remaining) = lines.split_at(skip.min(lines.len().saturating_sub(1))); + + let skipped_ancestors = skipped + .iter() + .cloned() + .filter(|line| line.is_ancestor_of_current_item) + .collect::>(); + + let result = retain_ancestors(remaining.to_vec(), skipped_ancestors.len()); + RetainAncestorResult { + skipped_ancestors: skipped_ancestors + .into_iter() + .chain(result.skipped_ancestors.into_iter()) + .collect(), + remaining_lines: result.remaining_lines, + } + } + + let RetainAncestorResult { + skipped_ancestors, + remaining_lines, + } = retain_ancestors(lines, skip); + + let max_ancestors_len = take.saturating_sub(1); + + // Skip furthest ancestors + let skipped_ancestors = skipped_ancestors .into_iter() + .rev() + .take(max_ancestors_len) + .rev() + .collect::>(); + + let skipped_ancestors_len = skipped_ancestors.len(); + + skipped_ancestors + .into_iter() + .chain( + remaining_lines + .into_iter() + .take(take.saturating_sub(skipped_ancestors_len)), + ) // Horizontal scroll .map(|line| { let skip = self.column; @@ -907,18 +975,15 @@ impl TreeView { .take(max_width) .collect::() }, - name: line - .name + content: line + .content .chars() .skip(skip.saturating_sub(indent_len)) - .take((max_width.saturating_sub(indent_len)).clamp(0, line.name.len())) + .take((max_width.saturating_sub(indent_len)).clamp(0, line.content.len())) .collect::(), ..line } }) - // Vertical scroll - .skip(skip) - .take(area.height as usize) .collect() } @@ -972,8 +1037,8 @@ impl TreeView { ctrl!('u') => self.move_up_half_page(), key!('g') => { self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.move_to_first(), - key!('e') => tree.move_to_last(), + key!('g') => tree.move_to_first_line(), + key!('e') => tree.move_to_last_line(), key!('h') => tree.move_leftmost(), key!('l') => tree.move_rightmost(), _ => {} @@ -1103,7 +1168,7 @@ impl TreeView { /// yo (4) /// ``` fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { - fn index_elems<'a, T>( + fn index_elems( current_index: usize, elems: Vec>, parent_index: usize, @@ -1248,7 +1313,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1261,7 +1326,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  (krabby_patty) @@ -1274,11 +1339,11 @@ mod test_tree_view { assert_eq!( render(&mut view), " +[who_lives_in_a_pineapple_under_the_sea]  (gary_the_snail)  karen  king_neptune  krabby_patty - larry_the_lobster " .trim() ); @@ -1296,7 +1361,7 @@ mod test_tree_view { .trim() ); - view.move_to_first(); + view.move_to_first_line(); view.move_up(1); assert_eq!( render(&mut view), @@ -1310,12 +1375,12 @@ mod test_tree_view { .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.move_down(1); assert_eq!( render(&mut view), " - mrs_puff +[who_lives_in_a_pineapple_under_the_sea]  patrick_star  plankton  sandy_cheeks @@ -1332,7 +1397,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1345,7 +1410,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  (larry_the_lobster)  mrs_puff @@ -1358,7 +1423,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1372,11 +1437,11 @@ mod test_tree_view { fn test_move_to_first_last() { let mut view = dummy_tree_view(); - view.move_to_last(); + view.move_to_last_line(); assert_eq!( render(&mut view), " - mrs_puff +[who_lives_in_a_pineapple_under_the_sea]  patrick_star  plankton  sandy_cheeks @@ -1385,7 +1450,7 @@ mod test_tree_view { .trim() ); - view.move_to_first(); + view.move_to_first_line(); assert_eq!( render(&mut view), " @@ -1432,7 +1497,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - karen +[who_lives_in_a_pineapple_under_the_sea]  king_neptune  krabby_patty  larry_the_lobster @@ -1445,7 +1510,7 @@ mod test_tree_view { assert_eq!( render(&mut view), " - karen +[who_lives_in_a_pineapple_under_the_sea]  king_neptune  (krabby_patty)  larry_the_lobster @@ -1458,11 +1523,11 @@ mod test_tree_view { assert_eq!( render(&mut view), " +[who_lives_in_a_pineapple_under_the_sea]  (karen)  king_neptune  krabby_patty  larry_the_lobster - mrs_puff " .trim() ); @@ -1525,7 +1590,7 @@ mod test_tree_view { .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.move_to_parent(); assert_eq!( render(&mut view), @@ -1747,7 +1812,7 @@ krabby_patty assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1756,7 +1821,7 @@ krabby_patty .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.search_next("who_lives"); assert_eq!( render(&mut view), @@ -1779,7 +1844,7 @@ krabby_patty assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  krabby_patty @@ -1788,12 +1853,12 @@ krabby_patty .trim() ); - view.move_to_last(); + view.move_to_last_line(); view.search_previous("krab"); assert_eq!( render(&mut view), " - gary_the_snail +[who_lives_in_a_pineapple_under_the_sea]  karen  king_neptune  (krabby_patty) @@ -1825,7 +1890,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  larry_the_lobster  mrs_puff @@ -1838,7 +1903,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  (krabby_patty)  larry_the_lobster  mrs_puff @@ -1857,7 +1922,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  larry_the_lobster  mrs_puff @@ -1870,7 +1935,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  (krabby_patty)  larry_the_lobster  mrs_puff @@ -1883,7 +1948,7 @@ krabby_patty assert_eq!( render(&mut view), " - king_neptune +[who_lives_in_a_pineapple_under_the_sea]  krabby_patty  larry_the_lobster  mrs_puff @@ -1898,22 +1963,22 @@ krabby_patty let mut view = dummy_tree_view(); // 1. Move to the last child item on the tree - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); - view.move_to_last(); + view.move_to_last_line(); view.move_to_children(&"".to_string()).unwrap(); // 1a. Expect the current selected item is the last child on the tree assert_eq!( render(&mut view), " -  epants + [spongebob_squarepants] +  [squarepants]  [squar] - sq  [uar] (ar)" .trim_start_matches(|c| c == '\n') @@ -1974,6 +2039,255 @@ krabby_patty .trim() ); } + + #[test] + fn test_sticky_ancestors() { + // The ancestors of the current item should always be visible + // However, if there's not enough space, the current item will take precedence, + // and the nearest ancestor has higher precedence than further ancestors + + #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] + struct Item<'a> { + name: &'a str, + children: Option>>, + } + + fn parent<'a>(name: &'a str, children: Vec>) -> Item<'a> { + Item { + name, + children: Some(children), + } + } + + fn child<'a>(name: &'a str) -> Item<'a> { + Item { + name, + children: None, + } + } + + impl<'a> TreeViewItem for Item<'a> { + type Params = (); + + fn name(&self) -> String { + self.name.to_string() + } + + fn is_parent(&self) -> bool { + self.children.is_some() + } + + fn get_children(&self) -> anyhow::Result> { + match &self.children { + Some(children) => Ok(children.clone()), + None => Ok(vec![]), + } + } + + fn filter(&self, s: &str) -> bool { + self.name().to_lowercase().contains(&s.to_lowercase()) + } + } + + fn render<'a>(view: &mut TreeView>) -> String { + view.render_to_string(dummy_area().with_height(3), &"".to_string()) + } + + let mut view = TreeView::new( + parent("root", vec![]), + vec_to_tree(vec![ + parent("a", vec![child("aa"), child("ab")]), + parent( + "b", + vec![parent( + "ba", + vec![parent("baa", vec![child("baaa"), child("baab")])], + )], + ), + ]), + ); + + assert_eq!( + render(&mut view), + " +(root) + a + b + " + .trim() + ); + + // 1. Move down to "a", and expand it + let filter = "".to_string(); + view.move_down(1); + view.move_to_children(&filter).unwrap(); + + assert_eq!( + render(&mut view), + " +[root] + [a] + (aa) + " + .trim() + ); + + // 2. Move down by 1 + view.move_down(1); + + // 2a. Expect all ancestors (i.e. "root" and "a") are visible, + // and the cursor is at "ab" + assert_eq!( + render(&mut view), + " +[root] + [a] + (ab) + " + .trim() + ); + + // 3. Move down by 1 + view.move_down(1); + + // 3a. Expect "a" is out of view, because it is no longer the ancestor of the current item + assert_eq!( + render(&mut view), + " +[root] + ab + (b) + " + .trim() + ); + + // 4. Move to the children of "b", which is "ba" + view.move_to_children(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + [b] +  (ba) + " + .trim() + ); + + // 5. Move to the children of "ba", which is "baa" + view.move_to_children(&filter).unwrap(); + + // 5a. Expect the furthest ancestor "root" is out of view, + // because when there's no enough space, the nearest ancestor takes precedence + assert_eq!( + render(&mut view), + " + [b] +  [ba] +  (baa) + " + .trim() + ); + + // 5.1 Move to child + view.move_to_children(&filter).unwrap(); + assert_eq!( + render(&mut view), + " +  [ba] +  [baa] + (baaa) +" + .trim_matches('\n') + ); + + // 5.2 Move down + view.move_down(1); + assert_eq!( + render(&mut view), + " +  [ba] +  [baa] + (baab) +" + .trim_matches('\n') + ); + + // 5.3 Move up + view.move_up(1); + assert_eq!(view.current_item().name, "baaa"); + assert_eq!( + render(&mut view), + " +  [ba] +  [baa] + (baaa) +" + .trim_matches('\n') + ); + + // 5.4 Move up + view.move_up(1); + assert_eq!( + render(&mut view), + " + [b] +  [ba] +  (baa) + " + .trim() + ); + + // 6. Move up by one + view.move_up(1); + + // 6a. Expect "root" is visible again, because now there's enough space to render all + // ancestors + assert_eq!( + render(&mut view), + " +[root] + [b] +  (ba) + " + .trim() + ); + + // 7. Move up by one + view.move_up(1); + assert_eq!( + render(&mut view), + " +[root] + (b) +  ba + " + .trim() + ); + + // 8. Move up by one + view.move_up(1); + assert_eq!( + render(&mut view), + " +[root] + [a] + (ab) + " + .trim() + ); + + // 9. Move up by one + view.move_up(1); + assert_eq!( + render(&mut view), + " +[root] + [a] + (aa) + " + .trim() + ); + } } #[cfg(test)] -- 2.38.5 From dffbc15067621ae4e4a613ed11edf2e9398da8fa Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 12:34:33 +0800 Subject: [PATCH 047/191] refactor(explorer,tree): remove unwrap to avoid panics --- changes | 4 +- helix-term/src/ui/explorer.rs | 87 ++++++++--------- helix-term/src/ui/tree.rs | 173 ++++++++++++++++++---------------- 3 files changed, 136 insertions(+), 128 deletions(-) diff --git a/changes b/changes index ff97ff45..c5d82a77 100644 --- a/changes +++ b/changes @@ -47,14 +47,12 @@ New: - [x] bind "C-n/C-p" to up/down - [x] bind "="/"_" to zoom-in/zoom-out - [x] Sticky ancestors +- [x] remove unwrap and expect - [] Toggle preview - [] search highlight matching word - [] Error didn't clear - [] should preview be there by default? - [] Fix panic bugs (see github comments) -- [] explorer(preview): use popup instead of custom components - [] explorer(preview): overflow where bufferline is there -- [] explorer(preview): implement scrolling C-j/C-k - [] symlink not showing -- [] remove unwrap and expect - [] bug(tree): zb does not work, because clash with explorer 'b' diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index d0d9aac1..6fcde7b5 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -279,34 +279,36 @@ impl Explorer { } fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) { - let item = self.tree.current().item(); - let head_area = render_block( - area.clip_bottom(area.height.saturating_sub(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"); - let content = get_preview(&item.path, body_area.height as usize) - .unwrap_or_else(|err| vec![err.to_string()]); - content.into_iter().enumerate().for_each(|(row, line)| { + if let Ok(current) = self.tree.current() { + let item = current.item(); + let head_area = render_block( + area.clip_bottom(area.height.saturating_sub(2)), + surface, + Borders::BOTTOM, + ); + let path_str = format!("{}", item.path.display()); surface.set_stringn( - body_area.x, - body_area.y + row as u16, - line, - body_area.width as usize, - style, + 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"); + let content = get_preview(&item.path, body_area.height as usize) + .unwrap_or_else(|err| vec![err.to_string()]); + 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_create_folder_prompt(&mut self) -> Result<()> { @@ -338,14 +340,14 @@ impl Explorer { } fn nearest_folder(&self) -> Result { - let current = self.tree.current(); - if current.item().is_parent() { - Ok(current.item().path.to_path_buf()) + let current = self.tree.current()?.item(); + if current.is_parent() { + Ok(current.path.to_path_buf()) } else { - let parent_path = current.item().path.parent().ok_or_else(|| { + let parent_path = current.path.parent().ok_or_else(|| { anyhow::anyhow!(format!( "Unable to get parent path of '{}'", - current.item().path.to_string_lossy() + current.path.to_string_lossy() )) })?; Ok(parent_path.to_path_buf()) @@ -353,7 +355,7 @@ impl Explorer { } fn new_remove_prompt(&mut self) -> Result<()> { - let item = self.tree.current().item(); + let item = self.tree.current()?.item(); match item.file_type { FileType::Folder => self.new_remove_folder_prompt(), FileType::File => self.new_remove_file_prompt(), @@ -361,8 +363,8 @@ impl Explorer { } } - fn new_rename_prompt(&mut self, cx: &mut Context) { - let path = self.tree.current_item().path.clone(); + fn new_rename_prompt(&mut self, cx: &mut Context) -> Result<()> { + let path = self.tree.current_item()?.path.clone(); self.prompt = Some(( PromptAction::RenameFile, Prompt::new( @@ -373,10 +375,11 @@ impl Explorer { ) .with_line(path.to_string_lossy().to_string(), cx.editor), )); + Ok(()) } fn new_remove_file_prompt(&mut self) -> Result<()> { - let item = self.tree.current_item(); + let item = self.tree.current_item()?; ensure!( item.path.is_file(), "The path '{}' is not a file", @@ -395,7 +398,7 @@ impl Explorer { } fn new_remove_folder_prompt(&mut self) -> Result<()> { - let item = self.tree.current_item(); + let item = self.tree.current_item()?; ensure!( item.path.is_dir(), "The path '{}' is not a folder", @@ -616,7 +619,7 @@ impl Explorer { }; let line = prompt.line(); - let current_item_path = explorer.tree.current_item().path.clone(); + let current_item_path = explorer.tree.current_item()?.path.clone(); match (&action, event) { (PromptAction::CreateFolder, key!(Enter)) => explorer.new_folder(line)?, (PromptAction::CreateFile, key!(Enter)) => explorer.new_file(line)?, @@ -682,7 +685,7 @@ impl Explorer { } fn change_root_to_current_folder(&mut self) -> Result<()> { - self.change_root(self.tree.current_item().path.clone()) + self.change_root(self.tree.current_item()?.path.clone()) } fn change_root_parent_folder(&mut self) -> Result<()> { @@ -715,7 +718,7 @@ impl Explorer { } fn rename_current(&mut self, line: &String) -> Result<()> { - let item = self.tree.current_item(); + let item = self.tree.current_item()?; let path = PathBuf::from(line); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; @@ -726,13 +729,13 @@ impl Explorer { } fn remove_folder(&mut self) -> Result<()> { - let item = self.tree.current_item(); + let item = self.tree.current_item()?; std::fs::remove_dir_all(&item.path)?; self.tree.refresh() } fn remove_file(&mut self) -> Result<()> { - let item = self.tree.current_item(); + let item = self.tree.current_item()?; std::fs::remove_file(&item.path)?; self.tree.refresh() } @@ -796,7 +799,7 @@ impl Component for Explorer { key!(']') => self.change_root_to_current_folder()?, key!('[') => self.go_to_previous_root(), key!('d') => self.new_remove_prompt()?, - key!('r') => self.new_rename_prompt(cx), + key!('r') => self.new_rename_prompt(cx)?, key!('-') | key!('_') => self.decrease_size(), key!('+') | key!('=') => self.increase_size(), _ => { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 656efa33..2997e5b3 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -439,15 +439,16 @@ impl TreeView { self.tree.regenerate_index(); } - fn move_to_parent(&mut self) { - if let Some(parent) = self.current_parent() { + fn move_to_parent(&mut self) -> Result<()> { + if let Some(parent) = self.current_parent()? { let index = parent.index; self.set_selected(index) } + Ok(()) } fn move_to_children(&mut self, filter: &String) -> Result<()> { - let current = self.current_mut(); + let current = self.current_mut()?; if current.is_opened { self.set_selected(self.selected + 1); Ok(()) @@ -526,17 +527,17 @@ impl TreeView { params: &mut T::Params, selected_index: usize, filter: &String, - ) { - let selected_item = self.get_mut(selected_index); + ) -> Result<()> { + let selected_item = self.get_mut(selected_index)?; if selected_item.is_opened { selected_item.close(); self.regenerate_index(); - return; + return Ok(()); } if let Some(mut on_open_fn) = self.on_opened_fn.take() { - let mut f = || { - let current = self.current_mut(); + let mut f = || -> Result<()> { + let current = self.current_mut()?; match on_open_fn(&mut current.item, cx, params) { TreeOp::GetChildsAndInsert => { if let Err(err) = current.open(filter) { @@ -545,11 +546,13 @@ impl TreeView { } TreeOp::Noop => {} }; + Ok(()) }; - f(); + f()?; self.regenerate_index(); - self.on_opened_fn = Some(on_open_fn) - } + self.on_opened_fn = Some(on_open_fn); + }; + Ok(()) } fn set_search_str(&mut self, s: String) { @@ -675,36 +678,36 @@ impl TreeView { }) } - fn get(&self, index: usize) -> &Tree { - self.tree - .get(index) - .expect(format!("Tree: index {index} is out of bound").as_str()) + fn get(&self, index: usize) -> Result<&Tree> { + self.tree.get(index).ok_or_else(|| { + anyhow::anyhow!("Programming error: TreeView.get: index {index} is out of bound") + }) } - fn get_mut(&mut self, index: usize) -> &mut Tree { - self.tree - .get_mut(index) - .expect(format!("Tree: index {index} is out of bound").as_str()) + fn get_mut(&mut self, index: usize) -> Result<&mut Tree> { + self.tree.get_mut(index).ok_or_else(|| { + anyhow::anyhow!("Programming error: TreeView.get_mut: index {index} is out of bound") + }) } - pub fn current(&self) -> &Tree { + pub fn current(&self) -> Result<&Tree> { self.get(self.selected) } - pub fn current_mut(&mut self) -> &mut Tree { + pub fn current_mut(&mut self) -> Result<&mut Tree> { self.get_mut(self.selected) } - fn current_parent(&self) -> Option<&Tree> { - if let Some(parent_index) = self.current().parent_index { - Some(self.get(parent_index)) + fn current_parent(&self) -> Result>> { + if let Some(parent_index) = self.current()?.parent_index { + Ok(Some(self.get(parent_index)?)) } else { - None + Ok(None) } } - pub fn current_item(&self) -> &T { - &self.current().item + pub fn current_item(&self) -> Result<&T> { + Ok(&self.current()?.item) } pub fn winline(&self) -> usize { @@ -1013,56 +1016,60 @@ impl TreeView { } let count = std::mem::replace(&mut self.count, 0); - match key_event { - key!(i @ '0'..='9') => self.count = i.to_digit(10).unwrap() as usize + count * 10, - key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)), - key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)), - key!('z') => { - self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('z') => tree.align_view_center(), - key!('t') => tree.align_view_top(), - key!('b') => tree.align_view_bottom(), - _ => {} - })); - } - key!('h') | key!(Left) => self.move_to_parent(), - key!('l') | key!(Right) => match self.move_to_children(filter) { - Ok(_) => {} - Err(err) => cx.editor.set_error(err.to_string()), - }, - shift!('H') => self.move_left(1), - shift!('L') => self.move_right(1), - key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, filter), - ctrl!('d') => self.move_down_half_page(), - ctrl!('u') => self.move_up_half_page(), - key!('g') => { - self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.move_to_first_line(), - key!('e') => tree.move_to_last_line(), - key!('h') => tree.move_leftmost(), - key!('l') => tree.move_rightmost(), - _ => {} - })); - } - key!('/') => self.new_search_prompt(Direction::Forward), - key!('n') => self.move_to_next_search_match(), - shift!('N') => self.move_to_previous_next_match(), - key!('f') => self.new_filter_prompt(cx), - key!(PageDown) => self.move_down_page(), - key!(PageUp) => self.move_up_page(), - shift!('R') => { - let filter = self.filter.clone(); - if let Err(error) = self.refresh_with_filter(&filter) { - cx.editor.set_error(error.to_string()) + (|| -> Result { + match key_event { + key!(i @ '0'..='9') => { + self.count = i.to_digit(10).unwrap_or(0) as usize + count * 10 } - } - key!(Home) => self.move_leftmost(), - key!(End) => self.move_rightmost(), - ctrl!('o') => self.jump_backward(), - _ => return EventResult::Ignored(None), - } - - EventResult::Consumed(None) + key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)), + key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)), + key!('z') => { + self.on_next_key = Some(Box::new(|_, tree, event| match event { + key!('z') => tree.align_view_center(), + key!('t') => tree.align_view_top(), + key!('b') => tree.align_view_bottom(), + _ => {} + })); + } + key!('h') | key!(Left) => self.move_to_parent()?, + key!('l') | key!(Right) => self.move_to_children(filter)?, + shift!('H') => self.move_left(1), + shift!('L') => self.move_right(1), + key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, filter)?, + ctrl!('d') => self.move_down_half_page(), + ctrl!('u') => self.move_up_half_page(), + key!('g') => { + self.on_next_key = Some(Box::new(|_, tree, event| match event { + key!('g') => tree.move_to_first_line(), + key!('e') => tree.move_to_last_line(), + key!('h') => tree.move_leftmost(), + key!('l') => tree.move_rightmost(), + _ => {} + })); + } + key!('/') => self.new_search_prompt(Direction::Forward), + key!('n') => self.move_to_next_search_match(), + shift!('N') => self.move_to_previous_next_match(), + key!('f') => self.new_filter_prompt(cx), + key!(PageDown) => self.move_down_page(), + key!(PageUp) => self.move_up_page(), + shift!('R') => { + let filter = self.filter.clone(); + if let Err(error) = self.refresh_with_filter(&filter) { + cx.editor.set_error(error.to_string()) + } + } + key!(Home) => self.move_leftmost(), + key!(End) => self.move_rightmost(), + ctrl!('o') => self.jump_backward(), + _ => return Ok(EventResult::Ignored(None)), + }; + Ok(EventResult::Consumed(None)) + })() + .unwrap_or_else(|err| { + cx.editor.set_error(format!("{err}")); + EventResult::Consumed(None) + }) } fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { @@ -1577,7 +1584,7 @@ mod test_tree_view { .trim() ); - view.move_to_parent(); + view.move_to_parent().unwrap(); assert_eq!( render(&mut view), " @@ -1591,7 +1598,7 @@ mod test_tree_view { ); view.move_to_last_line(); - view.move_to_parent(); + view.move_to_parent().unwrap(); assert_eq!( render(&mut view), " @@ -1751,7 +1758,7 @@ krabby_patty .trim() ); - view.move_to_parent(); + view.move_to_parent().unwrap(); assert_eq!( render(&mut view), " @@ -1764,7 +1771,7 @@ krabby_patty .trim() ); - view.move_to_parent(); + view.move_to_parent().unwrap(); assert_eq!( render(&mut view), " @@ -1777,7 +1784,7 @@ krabby_patty .trim() ); - view.move_to_parent(); + view.move_to_parent().unwrap(); assert_eq!( render(&mut view), " @@ -1988,7 +1995,7 @@ krabby_patty view.refresh_with_filter(&"ar".to_string()).unwrap(); // 3. Get the current item - let item = view.current_item(); + let item = view.current_item().unwrap(); // 3a. Expects no failure assert_eq!(item.name, "who_lives_in_a_pine") @@ -2214,7 +2221,7 @@ krabby_patty // 5.3 Move up view.move_up(1); - assert_eq!(view.current_item().name, "baaa"); + assert_eq!(view.current_item().unwrap().name, "baaa"); assert_eq!( render(&mut view), " -- 2.38.5 From 36769cb3f6d1fe56162a1692d1ef85cea1599235 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 12:47:52 +0800 Subject: [PATCH 048/191] fix(explorer/keymap): change 'b' to 'B' - to not clash with Tree 'zb' --- changes | 2 +- helix-term/src/ui/explorer.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changes b/changes index c5d82a77..cb1b3eeb 100644 --- a/changes +++ b/changes @@ -48,6 +48,7 @@ New: - [x] bind "="/"_" to zoom-in/zoom-out - [x] Sticky ancestors - [x] remove unwrap and expect +- [x] bug(tree): zb does not work, because clash with explorer 'b' - [] Toggle preview - [] search highlight matching word - [] Error didn't clear @@ -55,4 +56,3 @@ New: - [] Fix panic bugs (see github comments) - [] explorer(preview): overflow where bufferline is there - [] symlink not showing -- [] bug(tree): zb does not work, because clash with explorer 'b' diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 6fcde7b5..17b3dcdc 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -593,7 +593,7 @@ impl Explorer { ("A", "Add folder"), ("r", "Rename file/folder"), ("d", "Delete file"), - ("b", "Change root to parent folder"), + ("B", "Change root to parent folder"), ("]", "Change root to current folder"), ("[", "Go to previous root"), ("+, =", "Increase size"), @@ -795,7 +795,7 @@ impl Component for Explorer { key!('?') => self.toggle_help(), key!('a') => self.new_create_file_prompt()?, shift!('A') => self.new_create_folder_prompt()?, - key!('b') => self.change_root_parent_folder()?, + shift!('B') => self.change_root_parent_folder()?, key!(']') => self.change_root_to_current_folder()?, key!('[') => self.go_to_previous_root(), key!('d') => self.new_remove_prompt()?, -- 2.38.5 From b5d92aca450995021db189e634ffc1a00434878a Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 12:56:49 +0800 Subject: [PATCH 049/191] chore: fix clippy warnings --- helix-term/src/ui/editor.rs | 6 ++---- helix-term/src/ui/explorer.rs | 22 +++++++++------------- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/tree.rs | 32 ++++++++++++++++---------------- helix-view/src/editor.rs | 7 ++----- 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index e52f8a7e..766a3cbf 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1385,8 +1385,7 @@ impl Component for EditorView { if let Some(position) = config.explorer.is_embed() { let area = if use_bufferline { area.clip_top(1) - } - else { + } else { area }; explorer.content.render_embed(area, surface, cx, &position); @@ -1478,8 +1477,7 @@ impl Component for EditorView { if let Some(position) = config.explorer.is_embed() { let area = if use_bufferline { area.clip_top(1) - } - else { + } else { area }; explore.content.render_embed(area, surface, cx, &position); diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 17b3dcdc..80b4fb21 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -50,7 +50,7 @@ impl FileInfo { fn get_text(&self) -> Cow<'static, str> { match self.file_type { - FileType::Root => return format!("{}", self.path.display()).into(), + FileType::Root => format!("{}", self.path.display()).into(), FileType::File | FileType::Folder => self .path .file_name() @@ -107,14 +107,11 @@ impl TreeViewItem for FileInfo { } fn is_parent(&self) -> bool { - match self.file_type { - FileType::Folder | FileType::Root => true, - _ => false, - } + matches!(self.file_type, FileType::Folder | FileType::Root) } } -fn dir_entry_to_file_info(entry: DirEntry, path: &PathBuf) -> Option { +fn dir_entry_to_file_info(entry: DirEntry, path: &Path) -> Option { entry.metadata().ok().map(|meta| { let file_type = match meta.is_dir() { true => FileType::Folder, @@ -196,7 +193,7 @@ impl Explorer { } fn new_tree_view(root: PathBuf) -> Result> { - let root = FileInfo::root(root.clone()); + let root = FileInfo::root(root); let children = root.get_children()?; Ok(TreeView::build_tree(root, children).with_enter_fn(Self::toggle_current)) } @@ -368,7 +365,7 @@ impl Explorer { self.prompt = Some(( PromptAction::RenameFile, Prompt::new( - format!(" Rename to ").into(), + " Rename to ".into(), None, ui::completers::none, |_, _, _| {}, @@ -689,7 +686,7 @@ impl Explorer { } fn change_root_parent_folder(&mut self) -> Result<()> { - if let Some(parent) = self.state.current_root.parent().clone() { + if let Some(parent) = self.state.current_root.parent() { let path = parent.to_path_buf(); self.change_root(path) } else { @@ -725,7 +722,7 @@ impl Explorer { } std::fs::rename(&item.path, &path)?; self.tree.refresh()?; - self.reveal_file(path.into()) + self.reveal_file(path) } fn remove_folder(&mut self) -> Result<()> { @@ -752,7 +749,7 @@ fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { .map(|p| p.starts_with(¤t_item_path)) .unwrap_or(false) { - Some(id.clone()) + Some(*id) } else { None } @@ -855,8 +852,7 @@ fn get_preview(p: impl AsRef, max_line: usize) -> Result> { .filter_map(|entry| { entry .ok() - .map(|entry| dir_entry_to_file_info(entry, &p.to_path_buf())) - .flatten() + .and_then(|entry| dir_entry_to_file_info(entry, p)) }) .take(max_line) .collect::>(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 12701f64..ce78e9f9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,7 +29,7 @@ pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; -pub use tree::{TreeViewItem, TreeOp, TreeView}; +pub use tree::{TreeOp, TreeView, TreeViewItem}; 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 index 2997e5b3..8e8ae1f4 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -123,7 +123,7 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} impl Tree { - fn open(&mut self, filter: &String) -> Result<()> { + fn open(&mut self, filter: &str) -> Result<()> { if self.item.is_parent() { self.children = self.get_filtered_children(filter)?; self.is_opened = true; @@ -136,12 +136,12 @@ impl Tree { self.children = vec![]; } - fn refresh(&mut self, filter: &String) -> Result<()> { + fn refresh(&mut self, filter: &str) -> Result<()> { if !self.is_opened { return Ok(()); } let latest_children = self.get_filtered_children(filter)?; - let filtered = std::mem::replace(&mut self.children, vec![]) + let filtered = std::mem::take(&mut self.children) .into_iter() // Remove children that does not exists in latest_children .filter(|tree| { @@ -174,7 +174,7 @@ impl Tree { Ok(()) } - fn get_filtered_children(&self, filter: &String) -> Result>> { + fn get_filtered_children(&self, filter: &str) -> Result>> { Ok(vec_to_tree( self.item .get_children()? @@ -242,7 +242,7 @@ impl Tree { &self.item } - fn get<'a>(&'a self, index: usize) -> Option<&'a Tree> { + fn get(&self, index: usize) -> Option<&Tree> { if self.index == index { Some(self) } else { @@ -261,7 +261,7 @@ impl Tree { } fn len(&self) -> usize { - (1 as usize).saturating_add(self.children.iter().map(|elem| elem.len()).sum()) + (1_usize).saturating_add(self.children.iter().map(|elem| elem.len()).sum()) } fn regenerate_index(&mut self) { @@ -373,7 +373,7 @@ impl TreeView { /// /// vec!["helix-term", "src", "ui", "tree.rs"] /// - pub fn reveal_item(&mut self, segments: Vec, filter: &String) -> Result<()> { + pub fn reveal_item(&mut self, segments: Vec, filter: &str) -> Result<()> { self.refresh_with_filter(filter)?; // Expand the tree @@ -447,7 +447,7 @@ impl TreeView { Ok(()) } - fn move_to_children(&mut self, filter: &String) -> Result<()> { + fn move_to_children(&mut self, filter: &str) -> Result<()> { let current = self.current_mut()?; if current.is_opened { self.set_selected(self.selected + 1); @@ -466,7 +466,7 @@ impl TreeView { self.refresh_with_filter(&self.filter.clone()) } - fn refresh_with_filter(&mut self, filter: &String) -> Result<()> { + fn refresh_with_filter(&mut self, filter: &str) -> Result<()> { self.tree.refresh(filter)?; self.set_selected(self.selected); Ok(()) @@ -526,7 +526,7 @@ impl TreeView { cx: &mut Context, params: &mut T::Params, selected_index: usize, - filter: &String, + filter: &str, ) -> Result<()> { let selected_item = self.get_mut(selected_index)?; if selected_item.is_opened { @@ -561,7 +561,7 @@ impl TreeView { } fn saved_view(&self) -> SavedView { - self.saved_view.clone().unwrap_or_else(|| SavedView { + self.saved_view.clone().unwrap_or(SavedView { selected: self.selected, }) } @@ -776,7 +776,7 @@ fn render_tree( } impl TreeView { - pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &String) { + pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &str) { let style = cx.editor.theme.get(&self.tree_symbol_style); let filter_prompt_area = area.with_height(1); @@ -847,7 +847,7 @@ impl TreeView { } #[cfg(test)] - pub fn render_to_string(&mut self, area: Rect, filter: &String) -> String { + pub fn render_to_string(&mut self, area: Rect, filter: &str) -> String { let lines = self.render_lines(area, filter); lines .into_iter() @@ -865,7 +865,7 @@ impl TreeView { .join("\n") } - fn render_lines(&mut self, area: Rect, filter: &String) -> Vec { + fn render_lines(&mut self, area: Rect, filter: &str) -> Vec { if let Some(pre_render) = self.pre_render.take() { pre_render(self, area); } @@ -995,7 +995,7 @@ impl TreeView { event: &Event, cx: &mut Context, params: &mut T::Params, - filter: &String, + filter: &str, ) -> EventResult { let key_event = match event { Event::Key(event) => event, @@ -1085,7 +1085,7 @@ impl TreeView { } key!(Esc) | ctrl!('c') => { self.filter.clear(); - self.refresh_with_filter(&"".to_string())?; + self.refresh_with_filter("")?; } _ => { if let EventResult::Consumed(_) = diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 055f1870..914159d2 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -241,10 +241,7 @@ impl ExplorerConfig { } pub fn is_overlay(&self) -> bool { - match self.position { - ExplorerPosition::Overlay => true, - _ => false, - } + matches!(self.position, ExplorerPosition::Overlay) } } @@ -257,7 +254,7 @@ impl Default for ExplorerConfig { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. -- 2.38.5 From 38ef079099400684eebaf59294a97fa4e697b9ea Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 13:12:56 +0800 Subject: [PATCH 050/191] feat(tree): jump forward --- changes | 4 +-- helix-term/src/ui/tree.rs | 60 +++++++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/changes b/changes index cb1b3eeb..326a0df6 100644 --- a/changes +++ b/changes @@ -31,8 +31,8 @@ TODO New: - [x] increase indentation - [x] Change '[' to "go to previous root" -- [x] Change 'b' to "go to parent" -- [x] Use C-o for jumping to previous position +- [x] Change 'B' to "go to parent" +- [x] Use C-o/C-i for jump backward/forward - [x] on focus indication - [x] support creating files and folder and the same time (`mkdir -p`) - [x] Ctrl-o should work for 'h', 'gg', 'ge', etc diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 8e8ae1f4..25f322a4 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -289,7 +289,8 @@ pub struct TreeView { /// Selected item idex selected: usize, - history: Vec, + backward_jumps: Vec, + forward_jumps: Vec, saved_view: Option, @@ -322,7 +323,8 @@ impl TreeView { Self { tree: Tree::new(root, items), selected: 0, - history: vec![], + backward_jumps: vec![], + forward_jumps: vec![], saved_view: None, winline: 0, column: 0, @@ -506,6 +508,7 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("Home", "Scroll to the leftmost"), ("End", "Scroll to the rightmost"), ("C-o", "Jump backward"), + ("C-i, Tab", "Jump forward"), ("C-d", "Half page down"), ("C-u", "Half page up"), ("PageUp", "Full page up"), @@ -602,7 +605,7 @@ impl TreeView { let previous_selected = self.selected; self.set_selected_without_history(selected); if previous_selected.abs_diff(selected) > 1 { - self.history.push(previous_selected) + self.backward_jumps.push(previous_selected) } } @@ -625,11 +628,18 @@ impl TreeView { } fn jump_backward(&mut self) { - if let Some(index) = self.history.pop() { + if let Some(index) = self.backward_jumps.pop() { + self.forward_jumps.push(self.selected); self.set_selected_without_history(index); } } + fn jump_forward(&mut self) { + if let Some(index) = self.forward_jumps.pop() { + self.set_selected(index) + } + } + pub fn move_up(&mut self, rows: usize) { self.set_selected(self.selected.saturating_sub(rows)) } @@ -1062,6 +1072,7 @@ impl TreeView { key!(Home) => self.move_leftmost(), key!(End) => self.move_rightmost(), ctrl!('o') => self.jump_backward(), + ctrl!('i') | key!(Tab) => self.jump_forward(), _ => return Ok(EventResult::Ignored(None)), }; Ok(EventResult::Consumed(None)) @@ -2002,7 +2013,7 @@ krabby_patty } #[test] - fn test_jump_backward() { + fn test_jump_backward_forward() { let mut view = dummy_tree_view(); view.move_down_half_page(); render(&mut view); @@ -2041,6 +2052,45 @@ krabby_patty  gary_the_snail  karen  king_neptune + krabby_patty + " + .trim() + ); + + view.jump_forward(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + (karen) + king_neptune + krabby_patty + " + .trim() + ); + + view.jump_forward(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + karen + king_neptune + (krabby_patty) + " + .trim() + ); + + view.jump_backward(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] + gary_the_snail + (karen) + king_neptune  krabby_patty " .trim() -- 2.38.5 From 24b50bb52589b93ef3a74a09207bbf25787dbe85 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 13:26:34 +0800 Subject: [PATCH 051/191] feat(explorer): toggle preview --- changes | 2 +- helix-term/src/ui/explorer.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/changes b/changes index 326a0df6..5c948e0c 100644 --- a/changes +++ b/changes @@ -49,7 +49,7 @@ New: - [x] Sticky ancestors - [x] remove unwrap and expect - [x] bug(tree): zb does not work, because clash with explorer 'b' -- [] Toggle preview +- [x] Toggle preview - [] search highlight matching word - [] Error didn't clear - [] should preview be there by default? diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 80b4fb21..bc62c01f 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -158,6 +158,7 @@ pub struct Explorer { tree: TreeView, history: Vec>, show_help: bool, + show_preview: bool, state: State, prompt: Option<(PromptAction, Prompt)>, #[allow(clippy::type_complexity)] @@ -172,6 +173,7 @@ impl Explorer { tree: Self::new_tree_view(current_root.clone())?, history: vec![], show_help: false, + show_preview: false, state: State::new(true, current_root), prompt: None, on_next_key: None, @@ -184,7 +186,8 @@ impl Explorer { Ok(Self { tree: Self::new_tree_view(root.clone())?, history: vec![], - show_help: true, + show_help: false, + show_preview: false, state: State::new(true, root), prompt: None, on_next_key: None, @@ -540,7 +543,8 @@ impl Explorer { } }; self.render_help(help_area, surface, cx); - } else { + } + if self.show_preview { const PREVIEW_AREA_MAX_WIDTH: u16 = 90; const PREVIEW_AREA_MAX_HEIGHT: u16 = 30; let preview_area_width = @@ -572,6 +576,7 @@ impl Explorer { let area = Rect::new(preview_area.x, y, preview_area_width, preview_area_height); surface.clear_with(area, background); let area = render_block(area, surface, Borders::all()); + self.render_preview(area, surface, cx.editor); } } @@ -595,6 +600,7 @@ impl Explorer { ("[", "Go to previous root"), ("+, =", "Increase size"), ("-, _", "Decrease size"), + ("C-t", "Toggle preview (left/right only)"), ("q", "Close"), ] .into_iter() @@ -736,6 +742,10 @@ impl Explorer { std::fs::remove_file(&item.path)?; self.tree.refresh() } + + fn toggle_preview(&mut self) { + self.show_preview = !self.show_preview + } } fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { @@ -799,6 +809,7 @@ impl Component for Explorer { key!('r') => self.new_rename_prompt(cx)?, key!('-') | key!('_') => self.decrease_size(), key!('+') | key!('=') => self.increase_size(), + ctrl!('t') => self.toggle_preview(), _ => { self.tree .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); -- 2.38.5 From ba00a8003746f242edc2d9d0eeda546863913f98 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 16:38:35 +0800 Subject: [PATCH 052/191] fix(tree): shouldn't use patched font --- helix-term/src/ui/explorer.rs | 136 +++++----- helix-term/src/ui/tree.rs | 462 +++++++++++++++++----------------- 2 files changed, 299 insertions(+), 299 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index bc62c01f..3b08702a 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -950,8 +950,8 @@ mod test_explorer { render(&mut explorer), " (test-explorer/reveal_file) - scripts - styles +⏵ scripts +⏵ styles .gitignore index.html " @@ -966,9 +966,9 @@ mod test_explorer { render(&mut explorer), " [test-explorer/reveal_file] - [scripts] +⏷ [scripts] (main.js) - styles +⏵ styles .gitignore index.html " @@ -1014,8 +1014,8 @@ mod test_explorer { render(&mut explorer), " [test-explorer/rename] - scripts - styles +⏵ scripts +⏵ styles (.gitignore) index.html " @@ -1032,8 +1032,8 @@ mod test_explorer { render(&mut explorer), " [test-explorer/rename] - scripts - styles +⏵ scripts +⏵ styles index.html (who.is) " @@ -1052,9 +1052,9 @@ mod test_explorer { render(&mut explorer), " [test-explorer/rename] - scripts - [styles] -  public +⏵ scripts +⏷ [styles] + ⏵ public (lol) style.css index.html @@ -1076,12 +1076,12 @@ mod test_explorer { render(&mut explorer), " [test-explorer/rename] - [new_folder] -  [sponge] +⏷ [new_folder] + ⏷ [sponge] (bob) - scripts - styles -  public +⏵ scripts +⏷ styles + ⏵ public style.css index.html " @@ -1134,9 +1134,9 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_folder] - scripts - styles - (yoyo) +⏵ scripts +⏵ styles +⏷ (yoyo) .gitignore index.html " @@ -1156,12 +1156,12 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_folder] - scripts - [styles] -  public -  (sus.sass) +⏵ scripts +⏷ [styles] + ⏵ public + ⏷ (sus.sass) style.css - yoyo +⏷ yoyo .gitignore index.html " @@ -1180,13 +1180,13 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_folder] - [styles] -  [sus.sass] -  [a] -  [b] -  (c) +⏷ [styles] + ⏷ [sus.sass] + ⏷ [a] + ⏷ [b] + ⏷ (c) style.css - yoyo +⏷ yoyo .gitignore index.html " @@ -1207,14 +1207,14 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_folder] - scripts - [styles] -  (foobar) -  public -  sus.sass -  a -  b -  c +⏵ scripts +⏷ [styles] + ⏷ (foobar) + ⏵ public + ⏷ sus.sass + ⏷ a + ⏷ b + ⏷ c style.css " .trim() @@ -1234,8 +1234,8 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_file] - scripts - styles +⏵ scripts +⏵ styles .gitignore index.html (yoyo) @@ -1256,9 +1256,9 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_file] - scripts - [styles] -  public +⏵ scripts +⏷ [styles] + ⏵ public style.css (sus.sass) .gitignore @@ -1280,12 +1280,12 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_file] - scripts - [styles] -  [a] -  [b] +⏵ scripts +⏷ [styles] + ⏷ [a] + ⏷ [b] (c) -  public + ⏵ public style.css sus.sass .gitignore @@ -1307,10 +1307,10 @@ mod test_explorer { render(&mut explorer), " [test-explorer/new_file] - [styles] -  b +⏷ [styles] + ⏷ b c -  public + ⏵ public (foobar) style.css sus.sass @@ -1335,8 +1335,8 @@ mod test_explorer { render(&mut explorer), " [test-explorer/remove_file] - scripts - styles +⏵ scripts +⏵ styles (.gitignore) index.html " @@ -1353,8 +1353,8 @@ mod test_explorer { render(&mut explorer), " [test-explorer/remove_file] - scripts - styles +⏵ scripts +⏵ styles (index.html) " .trim() @@ -1373,8 +1373,8 @@ mod test_explorer { render(&mut explorer), " [test-explorer/remove_file] - scripts - (styles) +⏵ scripts +⏵ (styles) " .trim() ); @@ -1394,9 +1394,9 @@ mod test_explorer { render(&mut explorer), " [test-explorer/remove_folder] - scripts - (styles) -  public +⏵ scripts +⏷ (styles) + ⏵ public style.css .gitignore index.html @@ -1414,7 +1414,7 @@ mod test_explorer { render(&mut explorer), " [test-explorer/remove_folder] - scripts +⏵ scripts (.gitignore) index.html " @@ -1440,7 +1440,7 @@ mod test_explorer { render(&mut explorer), " [test-explorer/change_root/styles] - (public) +⏵ (public) style.css " .trim() @@ -1454,8 +1454,8 @@ mod test_explorer { render(&mut explorer), " (test-explorer/change_root) - scripts - styles +⏵ scripts +⏵ styles .gitignore index.html " @@ -1470,7 +1470,7 @@ mod test_explorer { render(&mut explorer), " [test-explorer/change_root/styles] - (public) +⏵ (public) style.css " .trim() @@ -1486,9 +1486,9 @@ mod test_explorer { render(&mut explorer), " [test-explorer/change_root] - scripts - (styles) -  public +⏵ scripts +⏷ (styles) + ⏵ public style.css .gitignore index.html diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 25f322a4..a08578a6 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -752,9 +752,9 @@ fn render_tree( let indent = if level > 0 { let indicator = if tree.item().is_parent() { if tree.is_opened { - "" + "⏷" } else { - "" + "⏵" } } else { " " @@ -1289,10 +1289,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1306,10 +1306,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (gary_the_snail) - karen - king_neptune - krabby_patty +⏵ (gary_the_snail) +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1319,10 +1319,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - karen - king_neptune - (krabby_patty) +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) " .trim() ); @@ -1332,10 +1332,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - krabby_patty - (larry_the_lobster) +⏵ karen +⏵ king_neptune +⏵ krabby_patty +⏵ (larry_the_lobster) " .trim() ); @@ -1345,10 +1345,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - (krabby_patty) - larry_the_lobster +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) +⏵ larry_the_lobster " .trim() ); @@ -1358,10 +1358,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (gary_the_snail) - karen - king_neptune - krabby_patty +⏵ (gary_the_snail) +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1371,10 +1371,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1385,10 +1385,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1399,10 +1399,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - patrick_star - plankton - sandy_cheeks - (spongebob_squarepants) +⏵ patrick_star +⏵ plankton +⏵ sandy_cheeks +⏵ (spongebob_squarepants) " .trim() ); @@ -1416,10 +1416,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - krabby_patty - (larry_the_lobster) +⏵ karen +⏵ king_neptune +⏵ krabby_patty +⏵ (larry_the_lobster) " .trim() ); @@ -1429,10 +1429,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - krabby_patty - (larry_the_lobster) - mrs_puff - patrick_star +⏵ krabby_patty +⏵ (larry_the_lobster) +⏵ mrs_puff +⏵ patrick_star " .trim() ); @@ -1442,10 +1442,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - krabby_patty - (larry_the_lobster) +⏵ karen +⏵ king_neptune +⏵ krabby_patty +⏵ (larry_the_lobster) " .trim() ); @@ -1460,10 +1460,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - patrick_star - plankton - sandy_cheeks - (spongebob_squarepants) +⏵ patrick_star +⏵ plankton +⏵ sandy_cheeks +⏵ (spongebob_squarepants) " .trim() ); @@ -1473,10 +1473,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1490,10 +1490,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - (karen) - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ (karen) +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1503,10 +1503,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - karen - king_neptune - (krabby_patty) +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) " .trim() ); @@ -1516,10 +1516,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - king_neptune - krabby_patty - larry_the_lobster - (mrs_puff) +⏵ king_neptune +⏵ krabby_patty +⏵ larry_the_lobster +⏵ (mrs_puff) " .trim() ); @@ -1529,10 +1529,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - king_neptune - (krabby_patty) - larry_the_lobster - mrs_puff +⏵ king_neptune +⏵ (krabby_patty) +⏵ larry_the_lobster +⏵ mrs_puff " .trim() ); @@ -1542,10 +1542,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (karen) - king_neptune - krabby_patty - larry_the_lobster +⏵ (karen) +⏵ king_neptune +⏵ krabby_patty +⏵ larry_the_lobster " .trim() ); @@ -1555,10 +1555,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1574,10 +1574,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - [gary_the_snail] -  (e_snail) -  gary_th - karen +⏷ [gary_the_snail] + ⏵ (e_snail) + ⏵ gary_th +⏵ karen " .trim() ); @@ -1587,10 +1587,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - [gary_the_snail] -  e_snail -  (gary_th) - karen +⏷ [gary_the_snail] + ⏵ e_snail + ⏵ (gary_th) +⏵ karen " .trim() ); @@ -1600,10 +1600,10 @@ mod test_tree_view { render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (gary_the_snail) -  e_snail -  gary_th - karen +⏷ (gary_the_snail) + ⏵ e_snail + ⏵ gary_th +⏵ karen " .trim() ); @@ -1614,10 +1614,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail -  e_snail -  gary_th - karen +⏷ gary_the_snail + ⏵ e_snail + ⏵ gary_th +⏵ karen " .trim() ); @@ -1635,10 +1635,10 @@ mod test_tree_view { render(&mut view), " (who_lives_in_a_pinea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1700,10 +1700,10 @@ krabby_patty render(&mut view), " (who_lives_in_a_pinea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1713,10 +1713,10 @@ krabby_patty render(&mut view), " (who_lives_in_a_pinea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1735,10 +1735,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (gary_the_snail) - karen - king_neptune - krabby_patty +⏵ (gary_the_snail) +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1748,10 +1748,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - [gary_the_snail] -  (e_snail) -  gary_th - karen +⏷ [gary_the_snail] + ⏵ (e_snail) + ⏵ gary_th +⏵ karen " .trim() ); @@ -1761,10 +1761,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - [gary_the_snail] -  e_snail -  (gary_th) - karen +⏷ [gary_the_snail] + ⏵ e_snail + ⏵ (gary_th) +⏵ karen " .trim() ); @@ -1774,10 +1774,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (gary_the_snail) -  e_snail -  gary_th - karen +⏷ (gary_the_snail) + ⏵ e_snail + ⏵ gary_th +⏵ karen " .trim() ); @@ -1787,10 +1787,10 @@ krabby_patty render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail -  e_snail -  gary_th - karen +⏷ gary_the_snail + ⏵ e_snail + ⏵ gary_th +⏵ karen " .trim() ); @@ -1800,10 +1800,10 @@ krabby_patty render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail -  e_snail -  gary_th - karen +⏷ gary_the_snail + ⏵ e_snail + ⏵ gary_th +⏵ karen " .trim() ) @@ -1818,10 +1818,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - karen - king_neptune - (krabby_patty) +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) " .trim() ); @@ -1831,10 +1831,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - krabby_patty - (larry_the_lobster) +⏵ karen +⏵ king_neptune +⏵ krabby_patty +⏵ (larry_the_lobster) " .trim() ); @@ -1845,10 +1845,10 @@ krabby_patty render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -1863,10 +1863,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - krabby_patty - (larry_the_lobster) +⏵ karen +⏵ king_neptune +⏵ krabby_patty +⏵ (larry_the_lobster) " .trim() ); @@ -1877,10 +1877,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - karen - king_neptune - (krabby_patty) - larry_the_lobster +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) +⏵ larry_the_lobster " .trim() ); @@ -1896,10 +1896,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - karen - king_neptune - (krabby_patty) +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) " .trim() ); @@ -1909,10 +1909,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - krabby_patty - larry_the_lobster - mrs_puff - (patrick_star) +⏵ krabby_patty +⏵ larry_the_lobster +⏵ mrs_puff +⏵ (patrick_star) " .trim() ); @@ -1922,10 +1922,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (krabby_patty) - larry_the_lobster - mrs_puff - patrick_star +⏵ (krabby_patty) +⏵ larry_the_lobster +⏵ mrs_puff +⏵ patrick_star " .trim() ); @@ -1941,10 +1941,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - krabby_patty - larry_the_lobster - mrs_puff - (patrick_star) +⏵ krabby_patty +⏵ larry_the_lobster +⏵ mrs_puff +⏵ (patrick_star) " .trim() ); @@ -1954,10 +1954,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - (krabby_patty) - larry_the_lobster - mrs_puff - patrick_star +⏵ (krabby_patty) +⏵ larry_the_lobster +⏵ mrs_puff +⏵ patrick_star " .trim() ); @@ -1967,10 +1967,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - krabby_patty - larry_the_lobster - mrs_puff - (patrick_star) +⏵ krabby_patty +⏵ larry_the_lobster +⏵ mrs_puff +⏵ (patrick_star) " .trim() ); @@ -1994,10 +1994,10 @@ krabby_patty assert_eq!( render(&mut view), " - [spongebob_squarepants] -  [squarepants] -  [squar] -  [uar] +⏷ [spongebob_squarepants] + ⏷ [squarepants] + ⏷ [squar] + ⏷ [uar] (ar)" .trim_start_matches(|c| c == '\n') ); @@ -2023,10 +2023,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - karen - king_neptune - (krabby_patty) +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) " .trim() ); @@ -2036,10 +2036,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - (karen) - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ (karen) +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -2049,10 +2049,10 @@ krabby_patty render(&mut view), " (who_lives_in_a_pineapple_under_the_sea) - gary_the_snail - karen - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -2062,10 +2062,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - (karen) - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ (karen) +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -2075,10 +2075,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - karen - king_neptune - (krabby_patty) +⏵ gary_the_snail +⏵ karen +⏵ king_neptune +⏵ (krabby_patty) " .trim() ); @@ -2088,10 +2088,10 @@ krabby_patty render(&mut view), " [who_lives_in_a_pineapple_under_the_sea] - gary_the_snail - (karen) - king_neptune - krabby_patty +⏵ gary_the_snail +⏵ (karen) +⏵ king_neptune +⏵ krabby_patty " .trim() ); @@ -2168,8 +2168,8 @@ krabby_patty render(&mut view), " (root) - a - b +⏵ a +⏵ b " .trim() ); @@ -2183,7 +2183,7 @@ krabby_patty render(&mut view), " [root] - [a] +⏷ [a] (aa) " .trim() @@ -2198,7 +2198,7 @@ krabby_patty render(&mut view), " [root] - [a] +⏷ [a] (ab) " .trim() @@ -2213,7 +2213,7 @@ krabby_patty " [root] ab - (b) +⏵ (b) " .trim() ); @@ -2224,8 +2224,8 @@ krabby_patty render(&mut view), " [root] - [b] -  (ba) +⏷ [b] + ⏵ (ba) " .trim() ); @@ -2238,9 +2238,9 @@ krabby_patty assert_eq!( render(&mut view), " - [b] -  [ba] -  (baa) +⏷ [b] + ⏷ [ba] + ⏵ (baa) " .trim() ); @@ -2250,8 +2250,8 @@ krabby_patty assert_eq!( render(&mut view), " -  [ba] -  [baa] + ⏷ [ba] + ⏷ [baa] (baaa) " .trim_matches('\n') @@ -2262,8 +2262,8 @@ krabby_patty assert_eq!( render(&mut view), " -  [ba] -  [baa] + ⏷ [ba] + ⏷ [baa] (baab) " .trim_matches('\n') @@ -2275,8 +2275,8 @@ krabby_patty assert_eq!( render(&mut view), " -  [ba] -  [baa] + ⏷ [ba] + ⏷ [baa] (baaa) " .trim_matches('\n') @@ -2287,9 +2287,9 @@ krabby_patty assert_eq!( render(&mut view), " - [b] -  [ba] -  (baa) +⏷ [b] + ⏷ [ba] + ⏷ (baa) " .trim() ); @@ -2303,8 +2303,8 @@ krabby_patty render(&mut view), " [root] - [b] -  (ba) +⏷ [b] + ⏷ (ba) " .trim() ); @@ -2315,8 +2315,8 @@ krabby_patty render(&mut view), " [root] - (b) -  ba +⏷ (b) + ⏷ ba " .trim() ); @@ -2327,7 +2327,7 @@ krabby_patty render(&mut view), " [root] - [a] +⏷ [a] (ab) " .trim() @@ -2339,7 +2339,7 @@ krabby_patty render(&mut view), " [root] - [a] +⏷ [a] (aa) " .trim() -- 2.38.5 From 601f2c4e5f90e92964c55576cf02fa43edc4b006 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 16:41:00 +0800 Subject: [PATCH 053/191] chore(ui/tree): remove useless comments --- helix-term/src/ui/tree.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index a08578a6..716a819e 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -19,10 +19,8 @@ use super::Prompt; pub trait TreeViewItem: Sized + Ord { type Params; - // fn text(&self, cx: &mut Context, selected: bool, params: &mut Self::Params) -> Spans; fn name(&self) -> String; fn is_parent(&self) -> bool; - // fn cmp(&self, other: &Self) -> Ordering; fn filter(&self, s: &str) -> bool { self.name().to_lowercase().contains(&s.to_lowercase()) @@ -37,7 +35,6 @@ fn tree_item_cmp(item1: &T, item2: &T) -> Ordering { fn vec_to_tree(mut items: Vec) -> Vec> { items.sort(); - // items.sort_by(tree_item_cmp); index_elems( 0, items -- 2.38.5 From ef1850295bdaa5cb415f87cb6f2743e7a74d0488 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 16:41:28 +0800 Subject: [PATCH 054/191] chore: remove temp file --- changes | 58 --------------------------------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 changes diff --git a/changes b/changes deleted file mode 100644 index 5c948e0c..00000000 --- a/changes +++ /dev/null @@ -1,58 +0,0 @@ -- [x] "h" moves to parent instead of scrolling to left -- [x] "l" steps into current folder instead of scrolling to right - - -TODO -- [x] make focus current file works -- [x] test all explorer functionality (e.g. "/" etc) -- [x] Go to parent directory -- [x] add help -- [x] highlight ancestors of current selection -- [x] search "N" will hang -- [x] implement rename -- [x] implement close (refer how overlay works) -- [x] implement refresh -- [x] bug: delete file collapsed whole tree -- [x] update documentation -- [x] fix list view -- [x] -/+ to increase or decrease width -- [x] help page overflow -- [x] preview not showing in small screen -- [x] fix (-/+) overflow -- [x] improve filter UI/UX (should only apply to child not parent) -- [x] bug: "h" does not realign preview -- [x] bug: reveal file does not realign preview -- [] "l" goes back to previous child if any history -- [x] Merge conflicts -- [x] Remove comments -- [x] fix warnings -- [x] refactor, add tree.expand_children() method - -New: -- [x] increase indentation -- [x] Change '[' to "go to previous root" -- [x] Change 'B' to "go to parent" -- [x] Use C-o/C-i for jump backward/forward -- [x] on focus indication -- [x] support creating files and folder and the same time (`mkdir -p`) -- [x] Ctrl-o should work for 'h', 'gg', 'ge', etc -- [x] add unit test for TreeView -- [x] explorer(help): overflow -- [x] n/N wrap around -- [x] fix(filter): crash -- [x] fix(explorer/preview): panic if not tall enough -- [x] explorer(preview): content not sorted -- [x] add integration test for Explorer -- [x] bind "o" to open/close file/folder -- [x] bind "C-n/C-p" to up/down -- [x] bind "="/"_" to zoom-in/zoom-out -- [x] Sticky ancestors -- [x] remove unwrap and expect -- [x] bug(tree): zb does not work, because clash with explorer 'b' -- [x] Toggle preview -- [] search highlight matching word -- [] Error didn't clear -- [] should preview be there by default? -- [] Fix panic bugs (see github comments) -- [] explorer(preview): overflow where bufferline is there -- [] symlink not showing -- 2.38.5 From 5d600fef0fdbf794644044a01ca6157284c1ad0c Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 25 Feb 2023 16:42:12 +0800 Subject: [PATCH 055/191] doc(helix-term/.gitignore): document purpose of test-explorer --- helix-term/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helix-term/.gitignore b/helix-term/.gitignore index 3a070c39..28509b23 100644 --- a/helix-term/.gitignore +++ b/helix-term/.gitignore @@ -1,2 +1,4 @@ /target + +# This folder is used by `test_explorer` to create temporary folders needed for testing test-explorer -- 2.38.5 From d578f8af6165d12de01004cae573f97b3c0854c6 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 26 Feb 2023 20:34:28 +0800 Subject: [PATCH 056/191] chore: fix clippy warning --- helix-term/src/ui/explorer.rs | 10 ++++------ helix-term/src/ui/tree.rs | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 3b08702a..c378d17c 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -907,7 +907,7 @@ mod test_explorer { use pretty_assertions::assert_eq; use std::{fs, path::PathBuf}; - fn dummy_file_tree<'a>(name: &'a str) -> PathBuf { + fn dummy_file_tree(name: &str) -> PathBuf { use build_fs_tree::{dir, file, Build, MergeableFileSystemTree}; let tree = MergeableFileSystemTree::<&str, &str>::from(dir! { "index.html" => file!("") @@ -930,13 +930,11 @@ mod test_explorer { path } - fn render<'a>(explorer: &mut Explorer) -> String { - explorer - .tree - .render_to_string(Rect::new(0, 0, 50, 10), &"".to_string()) + fn render(explorer: &mut Explorer) -> String { + explorer.tree.render_to_string(Rect::new(0, 0, 50, 10), "") } - fn new_explorer<'a>(name: &'a str) -> (PathBuf, Explorer) { + fn new_explorer(name: &str) -> (PathBuf, Explorer) { let path = dummy_file_tree(name); (path.clone(), Explorer::from_path(path, 30).unwrap()) } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 716a819e..24865e10 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1222,7 +1222,7 @@ mod test_tree_view { name: &'a str, } - fn item<'a>(name: &'a str) -> Item<'a> { + fn item(name: &str) -> Item { Item { name } } @@ -1273,8 +1273,8 @@ mod test_tree_view { Rect::new(0, 0, 50, 5) } - fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area(), &"".to_string()) + fn render(view: &mut TreeView) -> String { + view.render_to_string(dummy_area(), "") } #[test] @@ -1624,8 +1624,8 @@ mod test_tree_view { fn test_move_left_right() { let mut view = dummy_tree_view(); - fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area().with_width(20), &"".to_string()) + fn render(view: &mut TreeView) -> String { + view.render_to_string(dummy_area().with_width(20), "") } assert_eq!( @@ -1979,13 +1979,13 @@ krabby_patty // 1. Move to the last child item on the tree view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); // 1a. Expect the current selected item is the last child on the tree assert_eq!( @@ -2000,7 +2000,7 @@ krabby_patty ); // 2. Refreshes the tree with a filter that will remove the last child - view.refresh_with_filter(&"ar".to_string()).unwrap(); + view.refresh_with_filter("ar").unwrap(); // 3. Get the current item let item = view.current_item().unwrap(); @@ -2113,7 +2113,7 @@ krabby_patty } } - fn child<'a>(name: &'a str) -> Item<'a> { + fn child(name: &str) -> Item { Item { name, children: None, @@ -2143,8 +2143,8 @@ krabby_patty } } - fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area().with_height(3), &"".to_string()) + fn render(view: &mut TreeView>) -> String { + view.render_to_string(dummy_area().with_height(3), "") } let mut view = TreeView::new( -- 2.38.5 From c3b8be978e5de70a74d5210afbf74838ae498626 Mon Sep 17 00:00:00 2001 From: WJH Date: Sun, 26 Feb 2023 22:55:15 +0800 Subject: [PATCH 057/191] fix(ci): clippy + failure on Windows --- helix-term/src/ui/explorer.rs | 67 ++++++++++++++++++++--------------- helix-term/src/ui/tree.rs | 31 ++++++++-------- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 3b08702a..6e654f6e 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -49,13 +49,18 @@ impl FileInfo { } fn get_text(&self) -> Cow<'static, str> { - match self.file_type { - FileType::Root => format!("{}", self.path.display()).into(), + let text = match self.file_type { + FileType::Root => format!("{}", self.path.display()), FileType::File | FileType::Folder => self .path .file_name() - .map_or("/".into(), |p| p.to_string_lossy().into_owned().into()), - } + .map_or("/".into(), |p| p.to_string_lossy().into_owned()), + }; + + #[cfg(test)] + let text = text.replace(std::path::MAIN_SEPARATOR, "/"); + + text.into() } } @@ -197,8 +202,7 @@ impl Explorer { fn new_tree_view(root: PathBuf) -> Result> { let root = FileInfo::root(root); - let children = root.get_children()?; - Ok(TreeView::build_tree(root, children).with_enter_fn(Self::toggle_current)) + Ok(TreeView::build_tree(root)?.with_enter_fn(Self::toggle_current)) } fn push_history(&mut self, tree_view: TreeView) { @@ -220,31 +224,38 @@ impl Explorer { fn reveal_file(&mut self, path: PathBuf) -> Result<()> { let current_root = &self.state.current_root; - let current_path = path.as_path().to_string_lossy().to_string(); - let current_root = current_root.as_path().to_string_lossy().to_string() + "/"; + let current_path = &path; + let current_root = format!( + "{}{}", + current_root.as_path().to_string_lossy(), + std::path::MAIN_SEPARATOR + ); let segments = { let stripped = match current_path.strip_prefix(current_root.as_str()) { - Some(stripped) => Ok(stripped), - None => { - let parent = path - .parent() - .ok_or_else(|| anyhow::anyhow!("Failed get parent of '{current_path}'"))?; + Ok(stripped) => Ok(stripped), + Err(_) => { + let parent = path.parent().ok_or_else(|| { + anyhow::anyhow!("Failed get parent of '{}'", current_path.to_string_lossy()) + })?; self.change_root(parent.into())?; current_path - .strip_prefix((parent.to_string_lossy().to_string() + "/").as_str()) - .ok_or_else(|| { + .strip_prefix( + format!("{}{}", parent.to_string_lossy(), std::path::MAIN_SEPARATOR) + .as_str(), + ) + .map_err(|_| { anyhow::anyhow!( "Failed to strip prefix (parent) '{}' from '{}'", parent.to_string_lossy(), - current_path + current_path.to_string_lossy() ) }) } }?; stripped - .split(std::path::MAIN_SEPARATOR) - .map(|s| s.to_string()) + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) .collect::>() }; self.tree.reveal_item(segments, &self.state.filter)?; @@ -907,7 +918,7 @@ mod test_explorer { use pretty_assertions::assert_eq; use std::{fs, path::PathBuf}; - fn dummy_file_tree<'a>(name: &'a str) -> PathBuf { + fn dummy_file_tree(name: &str) -> PathBuf { use build_fs_tree::{dir, file, Build, MergeableFileSystemTree}; let tree = MergeableFileSystemTree::<&str, &str>::from(dir! { "index.html" => file!("") @@ -922,7 +933,7 @@ mod test_explorer { } ".gitignore" => file!("") }); - let path: PathBuf = format!("test-explorer/{}", name).into(); + let path: PathBuf = format!("test-explorer{}{}", std::path::MAIN_SEPARATOR, name).into(); if path.exists() { fs::remove_dir_all(path.clone()).unwrap(); } @@ -930,13 +941,11 @@ mod test_explorer { path } - fn render<'a>(explorer: &mut Explorer) -> String { - explorer - .tree - .render_to_string(Rect::new(0, 0, 50, 10), &"".to_string()) + fn render(explorer: &mut Explorer) -> String { + explorer.tree.render_to_string(Rect::new(0, 0, 50, 10), "") } - fn new_explorer<'a>(name: &'a str) -> (PathBuf, Explorer) { + fn new_explorer(name: &str) -> (PathBuf, Explorer) { let path = dummy_file_tree(name); (path.clone(), Explorer::from_path(path, 30).unwrap()) } @@ -1024,7 +1033,7 @@ mod test_explorer { // 1. Rename the current file to a name that is lexicographically greater than "index.html" explorer - .rename_current(&path.join("who.is").to_string_lossy().into()) + .rename_current(&path.join("who.is").display().to_string()) .unwrap(); // 1a. Expect the file is renamed, and is focused @@ -1044,7 +1053,7 @@ mod test_explorer { // 2. Rename the current file into an existing folder explorer - .rename_current(&path.join("styles/lol").to_string_lossy().into()) + .rename_current(&path.join("styles/lol").display().to_string()) .unwrap(); // 2a. Expect the file is moved to the folder, and is focused @@ -1066,7 +1075,7 @@ mod test_explorer { // 3. Rename the current file into a non-existent folder explorer - .rename_current(&path.join("new_folder/sponge/bob").to_string_lossy().into()) + .rename_current(&path.join("new_folder/sponge/bob").display().to_string()) .unwrap(); // 3a. Expect the non-existent folder to be created, @@ -1107,7 +1116,7 @@ mod test_explorer { // 5. Move cursor to "bob", and move it outside of the current root explorer.tree.move_down(1); explorer - .rename_current(&path.join("scripts/bob").to_string_lossy().into()) + .rename_current(&path.join("scripts/bob").display().to_string()) .unwrap(); // 5a. Expect the current root to be "scripts" diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 716a819e..73542430 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -339,8 +339,9 @@ impl TreeView { } } - pub fn build_tree(root: T, items: Vec) -> Self { - Self::new(root, vec_to_tree(items)) + pub fn build_tree(root: T) -> Result { + let children = root.get_children()?; + Ok(Self::new(root, vec_to_tree(children))) } pub fn with_enter_fn(mut self, f: F) -> Self @@ -1222,7 +1223,7 @@ mod test_tree_view { name: &'a str, } - fn item<'a>(name: &'a str) -> Item<'a> { + fn item(name: &str) -> Item { Item { name } } @@ -1273,8 +1274,8 @@ mod test_tree_view { Rect::new(0, 0, 50, 5) } - fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area(), &"".to_string()) + fn render(view: &mut TreeView) -> String { + view.render_to_string(dummy_area(), "") } #[test] @@ -1624,8 +1625,8 @@ mod test_tree_view { fn test_move_left_right() { let mut view = dummy_tree_view(); - fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area().with_width(20), &"".to_string()) + fn render(view: &mut TreeView) -> String { + view.render_to_string(dummy_area().with_width(20), "") } assert_eq!( @@ -1979,13 +1980,13 @@ krabby_patty // 1. Move to the last child item on the tree view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); view.move_to_last_line(); - view.move_to_children(&"".to_string()).unwrap(); + view.move_to_children("").unwrap(); // 1a. Expect the current selected item is the last child on the tree assert_eq!( @@ -2000,7 +2001,7 @@ krabby_patty ); // 2. Refreshes the tree with a filter that will remove the last child - view.refresh_with_filter(&"ar".to_string()).unwrap(); + view.refresh_with_filter("ar").unwrap(); // 3. Get the current item let item = view.current_item().unwrap(); @@ -2113,7 +2114,7 @@ krabby_patty } } - fn child<'a>(name: &'a str) -> Item<'a> { + fn child(name: &str) -> Item { Item { name, children: None, @@ -2143,8 +2144,8 @@ krabby_patty } } - fn render<'a>(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area().with_height(3), &"".to_string()) + fn render(view: &mut TreeView>) -> String { + view.render_to_string(dummy_area().with_height(3), "") } let mut view = TreeView::new( -- 2.38.5 From 4a0c620b77237e16aa0f85539f981e8016e8a723 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sun, 26 Feb 2023 23:10:39 +0800 Subject: [PATCH 058/191] fix(explorer/filter): not working for newly opened folder --- helix-term/src/ui/explorer.rs | 14 +++++--------- helix-term/src/ui/tree.rs | 31 ++++++++++++++++--------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 6e654f6e..026dd4e3 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -144,7 +144,6 @@ struct State { open: bool, current_root: PathBuf, area_width: u16, - filter: String, } impl State { @@ -154,7 +153,6 @@ impl State { current_root, open: true, area_width: 0, - filter: "".to_string(), } } } @@ -258,7 +256,7 @@ impl Explorer { .map(|c| c.as_os_str().to_string_lossy().to_string()) .collect::>() }; - self.tree.reveal_item(segments, &self.state.filter)?; + self.tree.reveal_item(segments)?; Ok(()) } @@ -489,8 +487,7 @@ impl Explorer { area.width.into(), title_style, ); - self.tree - .render(area.clip_top(1), surface, cx, &self.state.filter); + self.tree.render(area.clip_top(1), surface, cx); } pub fn render_embed( @@ -786,9 +783,8 @@ fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { impl Component for Explorer { /// Process input events, return true if handled. fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - let filter = self.state.filter.clone(); if self.tree.prompting() { - return self.tree.handle_event(event, cx, &mut self.state, &filter); + return self.tree.handle_event(event, cx, &mut self.state); } let key_event = match event { Event::Key(event) => event, @@ -823,7 +819,7 @@ impl Component for Explorer { ctrl!('t') => self.toggle_preview(), _ => { self.tree - .handle_event(&Event::Key(*key_event), cx, &mut self.state, &filter); + .handle_event(&Event::Key(*key_event), cx, &mut self.state); } }; Ok(()) @@ -942,7 +938,7 @@ mod test_explorer { } fn render(explorer: &mut Explorer) -> String { - explorer.tree.render_to_string(Rect::new(0, 0, 50, 10), "") + explorer.tree.render_to_string(Rect::new(0, 0, 50, 10)) } fn new_explorer(name: &str) -> (PathBuf, Explorer) { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 73542430..9892282c 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -373,8 +373,8 @@ impl TreeView { /// /// vec!["helix-term", "src", "ui", "tree.rs"] /// - pub fn reveal_item(&mut self, segments: Vec, filter: &str) -> Result<()> { - self.refresh_with_filter(filter)?; + pub fn reveal_item(&mut self, segments: Vec) -> Result<()> { + self.refresh()?; // Expand the tree let root = self.tree.item.name(); @@ -390,7 +390,7 @@ impl TreeView { { Some(tree) => { if !tree.is_opened { - tree.open(filter)?; + tree.open(&self.filter)?; } Ok(tree) } @@ -784,7 +784,7 @@ fn render_tree( } impl TreeView { - pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context, filter: &str) { + pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let style = cx.editor.theme.get(&self.tree_symbol_style); let filter_prompt_area = area.with_height(1); @@ -818,7 +818,7 @@ impl TreeView { let ancestor_style = cx.editor.theme.get("ui.text.focus"); let area = area.clip_top(2); - let iter = self.render_lines(area, filter).into_iter().enumerate(); + let iter = self.render_lines(area).into_iter().enumerate(); for (index, line) in iter { let area = Rect::new(area.x, area.y.saturating_add(index as u16), area.width, 1); @@ -855,8 +855,8 @@ impl TreeView { } #[cfg(test)] - pub fn render_to_string(&mut self, area: Rect, filter: &str) -> String { - let lines = self.render_lines(area, filter); + pub fn render_to_string(&mut self, area: Rect) -> String { + let lines = self.render_lines(area); lines .into_iter() .map(|line| { @@ -873,7 +873,7 @@ impl TreeView { .join("\n") } - fn render_lines(&mut self, area: Rect, filter: &str) -> Vec { + fn render_lines(&mut self, area: Rect) -> Vec { if let Some(pre_render) = self.pre_render.take() { pre_render(self, area); } @@ -885,7 +885,7 @@ impl TreeView { prefix: &"".to_string(), level: 0, selected: self.selected, - filter, + filter: &self.filter, }; let lines = render_tree(params); @@ -1003,7 +1003,6 @@ impl TreeView { event: &Event, cx: &mut Context, params: &mut T::Params, - filter: &str, ) -> EventResult { let key_event = match event { Event::Key(event) => event, @@ -1024,6 +1023,8 @@ impl TreeView { } let count = std::mem::replace(&mut self.count, 0); + + let filter = self.filter.clone(); (|| -> Result { match key_event { key!(i @ '0'..='9') => { @@ -1040,10 +1041,10 @@ impl TreeView { })); } key!('h') | key!(Left) => self.move_to_parent()?, - key!('l') | key!(Right) => self.move_to_children(filter)?, + key!('l') | key!(Right) => self.move_to_children(&filter)?, shift!('H') => self.move_left(1), shift!('L') => self.move_right(1), - key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, filter)?, + key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, &filter)?, ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('g') => { @@ -1275,7 +1276,7 @@ mod test_tree_view { } fn render(view: &mut TreeView) -> String { - view.render_to_string(dummy_area(), "") + view.render_to_string(dummy_area()) } #[test] @@ -1626,7 +1627,7 @@ mod test_tree_view { let mut view = dummy_tree_view(); fn render(view: &mut TreeView) -> String { - view.render_to_string(dummy_area().with_width(20), "") + view.render_to_string(dummy_area().with_width(20)) } assert_eq!( @@ -2145,7 +2146,7 @@ krabby_patty } fn render(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area().with_height(3), "") + view.render_to_string(dummy_area().with_height(3)) } let mut view = TreeView::new( -- 2.38.5 From 7e4feb02efcfb17faad01a26b1816a8c4c716d36 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 27 Feb 2023 09:57:21 +0800 Subject: [PATCH 059/191] fix(explore): search using previous search word after filter does not work - Also implemented restore_saved_view for filter and search --- helix-term/src/ui/tree.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 9892282c..97ed9a66 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -270,6 +270,7 @@ impl Tree { #[derive(Clone, Debug)] struct SavedView { selected: usize, + winline: usize, } pub struct TreeView { @@ -487,6 +488,16 @@ impl TreeView { fn move_rightmost(&mut self) { self.move_right(usize::MAX / 2) } + + fn restore_saved_view(&mut self) -> Result<()> { + if let Some(saved_view) = self.saved_view.take() { + self.selected = saved_view.selected; + self.winline = saved_view.winline; + self.refresh() + } else { + Ok(()) + } + } } pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { @@ -564,6 +575,7 @@ impl TreeView { fn saved_view(&self) -> SavedView { self.saved_view.clone().unwrap_or(SavedView { selected: self.selected, + winline: self.winline, }) } @@ -683,6 +695,7 @@ impl TreeView { fn save_view(&mut self) { self.saved_view = Some(SavedView { selected: self.selected, + winline: self.winline, }) } @@ -1090,10 +1103,13 @@ impl TreeView { if let EventResult::Consumed(_) = prompt.handle_event(&Event::Key(*event), cx) { + self.saved_view = None; + self.filter = prompt.line().clone(); self.refresh_with_filter(prompt.line())?; } } - key!(Esc) | ctrl!('c') => { + key!(Esc) => self.restore_saved_view()?, + ctrl!('c') => { self.filter.clear(); self.refresh_with_filter("")?; } @@ -1103,7 +1119,6 @@ impl TreeView { { self.refresh_with_filter(prompt.line())?; } - self.filter = prompt.line().clone(); self.filter_prompt = Some(prompt); } }; @@ -1123,7 +1138,12 @@ impl TreeView { self.set_search_str(prompt.line().clone()); EventResult::Consumed(None) } - key!(Esc) => EventResult::Consumed(None), + key!(Esc) => { + if let Err(err) = self.restore_saved_view() { + cx.editor.set_error(format!("{err}")) + } + EventResult::Consumed(None) + } _ => { let event = prompt.handle_event(&Event::Key(*event), cx); let line = prompt.line(); -- 2.38.5 From fae4990444c09c3bbca6632ebf6f0821ecf1f8df Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 28 Feb 2023 11:19:33 +0800 Subject: [PATCH 060/191] test(tree): search prompt and filter prompt --- helix-term/src/compositor.rs | 41 ++++++ helix-term/src/ui/tree.rs | 275 ++++++++++++++++++++++++++++------- 2 files changed, 264 insertions(+), 52 deletions(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index bcb3e449..d29bc1dc 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -34,6 +34,47 @@ impl<'a> Context<'a> { tokio::task::block_in_place(|| helix_lsp::block_on(self.editor.flush_writes()))?; Ok(()) } + + /// Purpose: to test `handle_event` without escalating the test case to integration test + /// Usage: + /// ``` + /// let mut editor = Context::dummy_editor(); + /// let mut jobs = Context::dummy_jobs(); + /// let mut cx = Context::dummy(&mut jobs, &mut editor); + /// ``` + #[cfg(test)] + pub fn dummy(jobs: &'a mut Jobs, editor: &'a mut helix_view::Editor) -> Context<'a> { + Context { + jobs, + scroll: None, + editor, + } + } + + #[cfg(test)] + pub fn dummy_jobs() -> Jobs { + Jobs::new() + } + + #[cfg(test)] + pub fn dummy_editor() -> Editor { + use crate::config::Config; + use arc_swap::{access::Map, ArcSwap}; + use helix_core::syntax::{self, Configuration}; + use helix_view::theme; + use std::sync::Arc; + + let config = Arc::new(ArcSwap::from_pointee(Config::default())); + Editor::new( + Rect::new(0, 0, 60, 120), + Arc::new(theme::Loader::new("", "")), + Arc::new(syntax::Loader::new(Configuration { language: vec![] })), + Arc::new(Arc::new(Map::new( + Arc::clone(&config), + |config: &Config| &config.editor, + ))), + ) + } } pub trait Component: Any + AnyComponent { diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 97ed9a66..c433eec4 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -317,8 +317,10 @@ pub struct TreeView { } impl TreeView { - pub fn new(root: T, items: Vec>) -> Self { - Self { + pub fn build_tree(root: T) -> Result { + let children = root.get_children()?; + let items = vec_to_tree(children); + Ok(Self { tree: Tree::new(root, items), selected: 0, backward_jumps: vec![], @@ -337,12 +339,7 @@ impl TreeView { filter_prompt: None, search_str: "".into(), filter: "".into(), - } - } - - pub fn build_tree(root: T) -> Result { - let children = root.get_children()?; - Ok(Self::new(root, vec_to_tree(children))) + }) } pub fn with_enter_fn(mut self, f: F) -> Self @@ -1011,6 +1008,21 @@ impl TreeView { .collect() } + #[cfg(test)] + pub fn handle_events( + &mut self, + events: &str, + cx: &mut Context, + params: &mut T::Params, + ) -> Result<()> { + use helix_view::input::parse_macro; + + for event in parse_macro(events)? { + self.handle_event(&Event::Key(event), cx, params); + } + Ok(()) + } + pub fn handle_event( &mut self, event: &Event, @@ -1234,21 +1246,26 @@ fn index_elems(parent_index: usize, elems: Vec>) -> Vec> { #[cfg(test)] mod test_tree_view { + use helix_view::graphics::Rect; - use super::{vec_to_tree, TreeView, TreeViewItem}; + use crate::compositor::Context; + + use super::{TreeView, TreeViewItem}; use pretty_assertions::assert_eq; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] - struct Item<'a> { + /// The children of DivisibleItem is the division of itself. + /// This is used to ease the creation of a dummy tree without having to specify so many things. + struct DivisibleItem<'a> { name: &'a str, } - fn item(name: &str) -> Item { - Item { name } + fn item(name: &str) -> DivisibleItem { + DivisibleItem { name } } - impl<'a> TreeViewItem for Item<'a> { + impl<'a> TreeViewItem for DivisibleItem<'a> { type Params = (); fn name(&self) -> String { @@ -1260,7 +1277,20 @@ mod test_tree_view { } fn get_children(&self) -> anyhow::Result> { - if self.is_parent() { + if self.name.eq("who_lives_in_a_pineapple_under_the_sea") { + Ok(vec![ + item("gary_the_snail"), + item("krabby_patty"), + item("larry_the_lobster"), + item("patrick_star"), + item("sandy_cheeks"), + item("spongebob_squarepants"), + item("mrs_puff"), + item("king_neptune"), + item("karen"), + item("plankton"), + ]) + } else if self.is_parent() { let (left, right) = self.name.split_at(self.name.len() / 2); Ok(vec![item(left), item(right)]) } else { @@ -1273,29 +1303,15 @@ mod test_tree_view { } } - fn dummy_tree_view<'a>() -> TreeView> { - TreeView::new( - item("who_lives_in_a_pineapple_under_the_sea"), - vec_to_tree(vec![ - item("gary_the_snail"), - item("krabby_patty"), - item("larry_the_lobster"), - item("patrick_star"), - item("sandy_cheeks"), - item("spongebob_squarepants"), - item("mrs_puff"), - item("king_neptune"), - item("karen"), - item("plankton"), - ]), - ) + fn dummy_tree_view<'a>() -> TreeView> { + TreeView::build_tree(item("who_lives_in_a_pineapple_under_the_sea")).unwrap() } fn dummy_area() -> Rect { Rect::new(0, 0, 50, 5) } - fn render(view: &mut TreeView) -> String { + fn render(view: &mut TreeView) -> String { view.render_to_string(dummy_area()) } @@ -1646,7 +1662,7 @@ mod test_tree_view { fn test_move_left_right() { let mut view = dummy_tree_view(); - fn render(view: &mut TreeView) -> String { + fn render(view: &mut TreeView) -> String { view.render_to_string(dummy_area().with_width(20)) } @@ -2028,7 +2044,7 @@ krabby_patty let item = view.current_item().unwrap(); // 3a. Expects no failure - assert_eq!(item.name, "who_lives_in_a_pine") + assert_eq!(item.name, "ar") } #[test] @@ -2116,33 +2132,33 @@ krabby_patty ); } - #[test] - fn test_sticky_ancestors() { - // The ancestors of the current item should always be visible - // However, if there's not enough space, the current item will take precedence, - // and the nearest ancestor has higher precedence than further ancestors + mod static_tree { + use crate::ui::{TreeView, TreeViewItem}; + + use super::dummy_area; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] - struct Item<'a> { - name: &'a str, - children: Option>>, + /// This is used for test cases where the structure of the tree has to be known upfront + pub struct StaticItem<'a> { + pub name: &'a str, + pub children: Option>>, } - fn parent<'a>(name: &'a str, children: Vec>) -> Item<'a> { - Item { + pub fn parent<'a>(name: &'a str, children: Vec>) -> StaticItem<'a> { + StaticItem { name, children: Some(children), } } - fn child(name: &str) -> Item { - Item { + pub fn child(name: &str) -> StaticItem { + StaticItem { name, children: None, } } - impl<'a> TreeViewItem for Item<'a> { + impl<'a> TreeViewItem for StaticItem<'a> { type Params = (); fn name(&self) -> String { @@ -2165,13 +2181,21 @@ krabby_patty } } - fn render(view: &mut TreeView>) -> String { + pub fn render(view: &mut TreeView>) -> String { view.render_to_string(dummy_area().with_height(3)) } + } + + #[test] + fn test_sticky_ancestors() { + // The ancestors of the current item should always be visible + // However, if there's not enough space, the current item will take precedence, + // and the nearest ancestor has higher precedence than further ancestors + use static_tree::*; - let mut view = TreeView::new( - parent("root", vec![]), - vec_to_tree(vec![ + let mut view = TreeView::build_tree(parent( + "root", + vec![ parent("a", vec![child("aa"), child("ab")]), parent( "b", @@ -2180,8 +2204,9 @@ krabby_patty vec![parent("baa", vec![child("baaa"), child("baab")])], )], ), - ]), - ); + ], + )) + .unwrap(); assert_eq!( render(&mut view), @@ -2364,6 +2389,152 @@ krabby_patty .trim() ); } + + #[tokio::test(flavor = "multi_thread")] + async fn test_search_prompt() { + let mut editor = Context::dummy_editor(); + let mut jobs = Context::dummy_jobs(); + let mut cx = Context::dummy(&mut jobs, &mut editor); + let mut view = dummy_tree_view(); + + view.handle_events("/an", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏵ larry_the_lobster +⏵ mrs_puff +⏵ patrick_star +⏵ (plankton) + " + .trim() + ); + + view.handle_events("t", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏵ patrick_star +⏵ plankton +⏵ sandy_cheeks +⏵ (spongebob_squarepants) + " + .trim() + ); + + view.handle_events("/larry", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏵ karen +⏵ king_neptune +⏵ krabby_patty +⏵ (larry_the_lobster) + " + .trim() + ); + + view.handle_events("", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏵ patrick_star +⏵ plankton +⏵ sandy_cheeks +⏵ (spongebob_squarepants) + " + .trim() + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_filter_prompt() { + use static_tree::*; + let mut editor = Context::dummy_editor(); + let mut jobs = Context::dummy_jobs(); + let mut cx = Context::dummy(&mut jobs, &mut editor); + + let mut view = TreeView::build_tree(parent( + "root", + vec![ + parent("src", vec![child("bar.rs"), child("foo.toml")]), + parent("tests", vec![child("hello.toml"), child("spam.rs")]), + ], + )) + .unwrap(); + + fn render(view: &mut TreeView>) -> String { + view.render_to_string(dummy_area().with_height(5)) + } + + // Open all the children + view.handle_events("lljjl", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + bar.rs + foo.toml +⏷ [tests] + (hello.toml) + " + .trim() + ); + + view.handle_events("frs", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + bar.rs +⏷ [tests] + (spam.rs) + " + .trim() + ); + + view.handle_events("ftoml", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + foo.toml +⏷ [tests] + (hello.toml) + " + .trim() + ); + + // Escape should causes the filter to be reverted + view.handle_events("", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + bar.rs +⏷ [tests] + (spam.rs) + " + .trim() + ); + + // C-c should clear the filter + view.handle_events("f", &mut cx, &mut ()).unwrap(); + assert_eq!( + render(&mut view), + " +[root] + bar.rs + foo.toml +⏷ (tests) + hello.toml + " + .trim() + ); + } } #[cfg(test)] -- 2.38.5 From b18a9746e9a3e589ac40f0057484161ada6cd286 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 28 Feb 2023 18:17:05 +0800 Subject: [PATCH 061/191] fix(explorer): go to previous root does not update state.current_root --- helix-term/src/ui/explorer.rs | 44 ++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 026dd4e3..43d932f2 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -157,9 +157,14 @@ impl State { } } +struct ExplorerHistory { + tree: TreeView, + current_root: PathBuf, +} + pub struct Explorer { tree: TreeView, - history: Vec>, + history: Vec, show_help: bool, show_preview: bool, state: State, @@ -171,7 +176,9 @@ pub struct Explorer { impl Explorer { pub fn new(cx: &mut Context) -> Result { - let current_root = std::env::current_dir().unwrap_or_else(|_| "./".into()); + let current_root = std::env::current_dir() + .unwrap_or_else(|_| "./".into()) + .canonicalize()?; Ok(Self { tree: Self::new_tree_view(current_root.clone())?, history: vec![], @@ -203,8 +210,11 @@ impl Explorer { Ok(TreeView::build_tree(root)?.with_enter_fn(Self::toggle_current)) } - fn push_history(&mut self, tree_view: TreeView) { - self.history.push(tree_view); + fn push_history(&mut self, tree_view: TreeView, current_root: PathBuf) { + self.history.push(ExplorerHistory { + tree: tree_view, + current_root, + }); const MAX_HISTORY_SIZE: usize = 20; Vec::truncate(&mut self.history, MAX_HISTORY_SIZE) } @@ -215,21 +225,16 @@ impl Explorer { } let tree = Self::new_tree_view(root.clone())?; let old_tree = std::mem::replace(&mut self.tree, tree); - self.push_history(old_tree); + self.push_history(old_tree, self.state.current_root.clone()); self.state.current_root = root; Ok(()) } fn reveal_file(&mut self, path: PathBuf) -> Result<()> { - let current_root = &self.state.current_root; - let current_path = &path; - let current_root = format!( - "{}{}", - current_root.as_path().to_string_lossy(), - std::path::MAIN_SEPARATOR - ); + let current_root = &self.state.current_root.canonicalize()?; + let current_path = &path.canonicalize()?; let segments = { - let stripped = match current_path.strip_prefix(current_root.as_str()) { + let stripped = match current_path.strip_prefix(current_root) { Ok(stripped) => Ok(stripped), Err(_) => { let parent = path.parent().ok_or_else(|| { @@ -237,10 +242,7 @@ impl Explorer { })?; self.change_root(parent.into())?; current_path - .strip_prefix( - format!("{}{}", parent.to_string_lossy(), std::path::MAIN_SEPARATOR) - .as_str(), - ) + .strip_prefix(parent.canonicalize()?) .map_err(|_| { anyhow::anyhow!( "Failed to strip prefix (parent) '{}' from '{}'", @@ -690,8 +692,9 @@ impl Explorer { } fn go_to_previous_root(&mut self) { - if let Some(tree) = self.history.pop() { - self.tree = tree + if let Some(history) = self.history.pop() { + self.tree = history.tree; + self.state.current_root = history.current_root } } @@ -1451,6 +1454,8 @@ mod test_explorer { .trim() ); + let current_root = explorer.state.current_root.clone(); + // 3. Change root to the parent of current folder explorer.change_root_parent_folder().unwrap(); @@ -1480,6 +1485,7 @@ mod test_explorer { " .trim() ); + assert_eq!(explorer.state.current_root, current_root); // 5. Go back to previous root again explorer.go_to_previous_root(); -- 2.38.5 From a2cb28d1d1528fdd26d77c7e0c306a3fe7e7ff51 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 1 Mar 2023 09:02:44 +0800 Subject: [PATCH 062/191] chore(keymap): merge with the correct version --- book/src/keymap.md | 59 ++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index ab12c4fd..ea0e7a00 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -130,7 +130,7 @@ | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` | | `J` | Join lines inside selection | `join_selections` | -| `Alt-J` | Join lines inside selection and select space | `join_selections_space` | +| `Alt-J` | Join lines inside selection and select the inserted space | `join_selections_space` | | `K` | Keep selections matching the regex | `keep_selections` | | `Alt-K` | Remove selections matching the regex | `remove_selections` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | @@ -268,30 +268,33 @@ Accessed by typing `Space` in [normal mode](#normal-mode). This layer is a kludge of mappings, mostly pickers. -| Key | Description | Command | -| ----- | ----------- | ------- | -| `f` | Open file picker | `file_picker` | -| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | -| `b` | Open buffer picker | `buffer_picker` | -| `j` | Open jumplist picker | `jumplist_picker` | -| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | -| `s` | Open document symbol picker (**LSP**) | `symbol_picker` | -| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | -| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | -| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | -| `r` | Rename symbol (**LSP**) | `rename_symbol` | -| `a` | Apply code action (**LSP**) | `code_action` | -| `'` | Open last fuzzy picker | `last_picker` | -| `w` | Enter [window mode](#window-mode) | N/A | -| `p` | Paste system clipboard after selections | `paste_clipboard_after` | -| `P` | Paste system clipboard before selections | `paste_clipboard_before` | -| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | -| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | -| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | -| `/` | Global search in workspace folder | `global_search` | -| `?` | Open command palette | `command_palette` | -| `e` | Reveal current file in explorer | `reveal_current_file` | -| `E` | Open or focus explorer | `toggle_or_focus_explorer` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `f` | Open file picker | `file_picker` | +| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | +| `b` | Open buffer picker | `buffer_picker` | +| `j` | Open jumplist picker | `jumplist_picker` | +| `g` | Debug (experimental) | N/A | +| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | +| `s` | Open document symbol picker (**LSP**) | `symbol_picker` | +| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | +| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | +| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | +| `r` | Rename symbol (**LSP**) | `rename_symbol` | +| `a` | Apply code action (**LSP**) | `code_action` | +| `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` | +| `'` | Open last fuzzy picker | `last_picker` | +| `w` | Enter [window mode](#window-mode) | N/A | +| `p` | Paste system clipboard after selections | `paste_clipboard_after` | +| `P` | Paste system clipboard before selections | `paste_clipboard_before` | +| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | +| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | +| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | +| `/` | Global search in workspace folder | `global_search` | +| `?` | Open command palette | `command_palette` | +| `e` | Reveal current file in explorer | `reveal_current_file` | +| `E` | Open or focus explorer | `toggle_or_focus_explorer` | + > TIP: Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. @@ -352,7 +355,7 @@ experience. | `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` | | `Ctrl-u` | Delete to start of line | `kill_to_line_start` | | `Ctrl-k` | Delete to end of line | `kill_to_line_end` | -| `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` | +| `Ctrl-h`, `Backspace`, `Shift-Backspace` | Delete previous char | `delete_char_backward` | | `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` | | `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | @@ -433,7 +436,7 @@ Keys to use within prompt, Remapping currently not supported. | `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word | | `Ctrl-u` | Delete to start of line | | `Ctrl-k` | Delete to end of line | -| `Backspace`, `Ctrl-h` | Delete previous char | +| `Backspace`, `Ctrl-h`, `Shift-Backspace` | Delete previous char | | `Delete`, `Ctrl-d` | Delete next char | | `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later | | `Ctrl-p`, `Up` | Select previous history | @@ -445,4 +448,4 @@ Keys to use within prompt, Remapping currently not supported. # File explorer -Press `?` to see keymaps. Remapping currently not supported. \ No newline at end of file +Press `?` to see keymaps. Remapping currently not supported. -- 2.38.5 From 43b226a2ab20fa8f379d90e83dd0b20da84615b8 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 2 Mar 2023 10:04:01 +0800 Subject: [PATCH 063/191] feat(explorer/keymap): combine 'a' with 'A' Reference: https://github.com/helix-editor/helix/pull/5768#issuecomment-1449536275 --- helix-term/.gitignore | 2 +- helix-term/src/compositor.rs | 15 +++ helix-term/src/ui/explorer.rs | 192 ++++++++++++++++++---------------- helix-term/src/ui/prompt.rs | 4 + helix-term/src/ui/tree.rs | 2 +- 5 files changed, 124 insertions(+), 91 deletions(-) diff --git a/helix-term/.gitignore b/helix-term/.gitignore index 28509b23..3c99a66e 100644 --- a/helix-term/.gitignore +++ b/helix-term/.gitignore @@ -1,4 +1,4 @@ /target # This folder is used by `test_explorer` to create temporary folders needed for testing -test-explorer +test_explorer diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index d29bc1dc..8176bdaf 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -113,6 +113,21 @@ pub trait Component: Any + AnyComponent { fn id(&self) -> Option<&'static str> { None } + + #[cfg(test)] + /// Utility method for testing `handle_event` without using integration test. + fn handle_events(&mut self, events: &str) -> anyhow::Result<()> { + use helix_view::input::parse_macro; + + let mut editor = Context::dummy_editor(); + let mut jobs = Context::dummy_jobs(); + let mut cx = Context::dummy(&mut jobs, &mut editor); + for event in parse_macro(events)? { + println!("Event = {}", event); + self.handle_event(&Event::Key(event), &mut cx); + } + Ok(()) + } } pub struct Compositor { diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 43d932f2..b9e25c2e 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -131,14 +131,13 @@ fn dir_entry_to_file_info(entry: DirEntry, path: &Path) -> Option { #[derive(Clone, Debug)] enum PromptAction { - CreateFolder, - CreateFile, + CreateFileOrFolder, RemoveFolder, RemoveFile, RenameFile, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] struct State { focus: bool, open: bool, @@ -322,30 +321,21 @@ impl Explorer { } } - fn new_create_folder_prompt(&mut self) -> Result<()> { - let folder_path = self.nearest_folder()?; - self.prompt = Some(( - PromptAction::CreateFolder, - Prompt::new( - format!(" New folder: {}/", folder_path.to_string_lossy()).into(), - None, - ui::completers::none, - |_, _, _| {}, - ), - )); - Ok(()) - } - - fn new_create_file_prompt(&mut self) -> Result<()> { + fn new_create_file_or_folder_prompt(&mut self, cx: &mut Context) -> Result<()> { let folder_path = self.nearest_folder()?; self.prompt = Some(( - PromptAction::CreateFile, + PromptAction::CreateFileOrFolder, Prompt::new( - format!(" New file: {}/", folder_path.to_string_lossy()).into(), + format!( + " New file or folder (ends with '{}'): ", + std::path::MAIN_SEPARATOR + ) + .into(), None, ui::completers::none, |_, _, _| {}, - ), + ) + .with_line(format!("{}/", folder_path.to_string_lossy()), cx.editor), )); Ok(()) } @@ -601,8 +591,7 @@ impl Explorer { "Explorer", &[ ("?", "Toggle help"), - ("a", "Add file"), - ("A", "Add folder"), + ("a", "Add file/folder"), ("r", "Rename file/folder"), ("d", "Delete file"), ("B", "Change root to parent folder"), @@ -634,8 +623,13 @@ impl Explorer { let current_item_path = explorer.tree.current_item()?.path.clone(); match (&action, event) { - (PromptAction::CreateFolder, key!(Enter)) => explorer.new_folder(line)?, - (PromptAction::CreateFile, key!(Enter)) => explorer.new_file(line)?, + (PromptAction::CreateFileOrFolder, key!(Enter)) => { + if line.ends_with(std::path::MAIN_SEPARATOR) { + explorer.new_folder(line)? + } else { + explorer.new_file(line)? + } + } (PromptAction::RemoveFolder, key!(Enter)) => { if line == "y" { close_documents(current_item_path, cx)?; @@ -669,9 +663,8 @@ impl Explorer { } } - fn new_file(&mut self, file_name: &str) -> Result<()> { - let current_parent = self.nearest_folder()?; - let path = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); + fn new_file(&mut self, path: &str) -> Result<()> { + let path = helix_core::path::get_normalized_path(&PathBuf::from(path)); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } @@ -680,9 +673,8 @@ impl Explorer { self.reveal_file(path) } - fn new_folder(&mut self, file_name: &str) -> Result<()> { - let current_parent = self.nearest_folder()?; - let path = helix_core::path::get_normalized_path(¤t_parent.join(file_name)); + fn new_folder(&mut self, path: &str) -> Result<()> { + let path = helix_core::path::get_normalized_path(&PathBuf::from(path)); std::fs::create_dir_all(&path)?; self.reveal_file(path) } @@ -810,8 +802,7 @@ impl Component for Explorer { key!(Esc) => self.unfocus(), key!('q') => self.close(), key!('?') => self.toggle_help(), - key!('a') => self.new_create_file_prompt()?, - shift!('A') => self.new_create_folder_prompt()?, + key!('a') => self.new_create_file_or_folder_prompt(cx)?, shift!('B') => self.change_root_parent_folder()?, key!(']') => self.change_root_to_current_folder()?, key!('[') => self.go_to_previous_root(), @@ -912,6 +903,8 @@ fn render_block(area: Rect, surface: &mut Surface, borders: Borders) -> Rect { #[cfg(test)] mod test_explorer { + use crate::compositor::Component; + use super::Explorer; use helix_view::graphics::Rect; use pretty_assertions::assert_eq; @@ -932,7 +925,7 @@ mod test_explorer { } ".gitignore" => file!("") }); - let path: PathBuf = format!("test-explorer{}{}", std::path::MAIN_SEPARATOR, name).into(); + let path: PathBuf = format!("test_explorer{}{}", std::path::MAIN_SEPARATOR, name).into(); if path.exists() { fs::remove_dir_all(path.clone()).unwrap(); } @@ -957,7 +950,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -(test-explorer/reveal_file) +(test_explorer/reveal_file) ⏵ scripts ⏵ styles .gitignore @@ -973,7 +966,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/reveal_file] +[test_explorer/reveal_file] ⏷ [scripts] (main.js) ⏵ styles @@ -991,7 +984,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -(test-explorer/reveal_file/scripts) +(test_explorer/reveal_file/scripts) main.js " .trim() @@ -1006,22 +999,22 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/reveal_file/styles/public] +[test_explorer/reveal_file/styles/public] (file) " .trim() ); } - #[test] - fn test_rename() { + #[tokio::test(flavor = "multi_thread")] + async fn test_rename() { let (path, mut explorer) = new_explorer("rename"); - explorer.tree.move_down(3); + explorer.handle_events("jjj").unwrap(); assert_eq!( render(&mut explorer), " -[test-explorer/rename] +[test_explorer/rename] ⏵ scripts ⏵ styles (.gitignore) @@ -1030,16 +1023,22 @@ mod test_explorer { .trim() ); + // 0. Open the rename file prompt + explorer.handle_events("r").unwrap(); + + // 0.1 Expect the current prompt to be related to file renaming + let prompt = &explorer.prompt.as_ref().unwrap().1; + assert_eq!(prompt.prompt(), " Rename to "); + assert_eq!(prompt.line(), "test_explorer/rename/.gitignore"); + // 1. Rename the current file to a name that is lexicographically greater than "index.html" - explorer - .rename_current(&path.join("who.is").display().to_string()) - .unwrap(); + explorer.handle_events("who.is").unwrap(); // 1a. Expect the file is renamed, and is focused assert_eq!( render(&mut explorer), " -[test-explorer/rename] +[test_explorer/rename] ⏵ scripts ⏵ styles index.html @@ -1052,14 +1051,17 @@ mod test_explorer { // 2. Rename the current file into an existing folder explorer - .rename_current(&path.join("styles/lol").display().to_string()) + .handle_events(&format!( + "rstyles{}lol", + std::path::MAIN_SEPARATOR + )) .unwrap(); // 2a. Expect the file is moved to the folder, and is focused assert_eq!( render(&mut explorer), " -[test-explorer/rename] +[test_explorer/rename] ⏵ scripts ⏷ [styles] ⏵ public @@ -1074,7 +1076,10 @@ mod test_explorer { // 3. Rename the current file into a non-existent folder explorer - .rename_current(&path.join("new_folder/sponge/bob").display().to_string()) + .handle_events(&format!( + "r{}", + path.join("new_folder/sponge/bob").display().to_string() + )) .unwrap(); // 3a. Expect the non-existent folder to be created, @@ -1083,7 +1088,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/rename] +[test_explorer/rename] ⏷ [new_folder] ⏷ [sponge] (bob) @@ -1099,30 +1104,32 @@ mod test_explorer { assert!(path.join("new_folder/sponge/bob").exists()); // 4. Change current root to "new_folder/sponge" - explorer.tree.move_up(1); - explorer.change_root_to_current_folder().unwrap(); + explorer.handle_events("k]").unwrap(); // 4a. Expect the current root to be "sponge" assert_eq!( render(&mut explorer), " -(test-explorer/rename/new_folder/sponge) +(test_explorer/rename/new_folder/sponge) bob " .trim() ); - // 5. Move cursor to "bob", and move it outside of the current root - explorer.tree.move_down(1); + // 5. Move cursor to "bob", and rename it outside of the current root + explorer.handle_events("j").unwrap(); explorer - .rename_current(&path.join("scripts/bob").display().to_string()) + .handle_events(&format!( + "r{}", + path.join("scripts/bob").display().to_string() + )) .unwrap(); // 5a. Expect the current root to be "scripts" assert_eq!( render(&mut explorer), " -[test-explorer/rename/scripts] +[test_explorer/rename/scripts] (bob) main.js " @@ -1130,18 +1137,24 @@ mod test_explorer { ); } - #[test] - fn test_new_folder() { + #[tokio::test(flavor = "multi_thread")] + async fn test_new_folder() { let (path, mut explorer) = new_explorer("new_folder"); + // 0. Open the add file/folder prompt + explorer.handle_events("a").unwrap(); + let prompt = &explorer.prompt.as_ref().unwrap().1; + assert_eq!(prompt.prompt(), " New file or folder (ends with '/'): "); + assert_eq!(prompt.line(), "test_explorer/new_folder/"); + // 1. Add a new folder at the root - explorer.new_folder("yoyo").unwrap(); + explorer.handle_events("yoyo/").unwrap(); // 1a. Expect the new folder is added, and is focused assert_eq!( render(&mut explorer), " -[test-explorer/new_folder] +[test_explorer/new_folder] ⏵ scripts ⏵ styles ⏷ (yoyo) @@ -1154,16 +1167,16 @@ mod test_explorer { assert!(fs::read_dir(path.join("yoyo")).is_ok()); // 2. Move up to "styles" - explorer.tree.move_up(1); + explorer.handle_events("k").unwrap(); // 3. Add a new folder - explorer.new_folder("sus.sass").unwrap(); + explorer.handle_events("asus.sass/").unwrap(); // 3a. Expect the new folder is added under "styles", although "styles" is not opened assert_eq!( render(&mut explorer), " -[test-explorer/new_folder] +[test_explorer/new_folder] ⏵ scripts ⏷ [styles] ⏵ public @@ -1179,7 +1192,7 @@ mod test_explorer { assert!(fs::read_dir(path.join("styles/sus.sass")).is_ok()); // 4. Add a new folder with non-existent parents - explorer.new_folder("a/b/c").unwrap(); + explorer.handle_events("aa/b/c/").unwrap(); // 4a. Expect the non-existent parents are created, // and the new folder is created, @@ -1187,7 +1200,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/new_folder] +[test_explorer/new_folder] ⏷ [styles] ⏷ [sus.sass] ⏷ [a] @@ -1204,17 +1217,17 @@ mod test_explorer { assert!(fs::read_dir(path.join("styles/sus.sass/a/b/c")).is_ok()); // 5. Move to "style.css" - explorer.tree.move_down(1); + explorer.handle_events("j").unwrap(); // 6. Add a new folder here - explorer.new_folder("foobar").unwrap(); + explorer.handle_events("afoobar/").unwrap(); // 6a. Expect the folder is added under "styles", // because the folder of the current item, "style.css" is "styles/" assert_eq!( render(&mut explorer), " -[test-explorer/new_folder] +[test_explorer/new_folder] ⏵ scripts ⏷ [styles] ⏷ (foobar) @@ -1231,17 +1244,18 @@ mod test_explorer { assert!(fs::read_dir(path.join("styles/foobar")).is_ok()); } - #[test] - fn test_new_file() { + #[tokio::test(flavor = "multi_thread")] + async fn test_new_file() { let (path, mut explorer) = new_explorer("new_file"); + // 1. Add a new file at the root - explorer.new_file("yoyo").unwrap(); + explorer.handle_events("ayoyo").unwrap(); // 1a. Expect the new file is added, and is focused assert_eq!( render(&mut explorer), " -[test-explorer/new_file] +[test_explorer/new_file] ⏵ scripts ⏵ styles .gitignore @@ -1257,13 +1271,13 @@ mod test_explorer { explorer.tree.move_up(3); // 3. Add a new file - explorer.new_file("sus.sass").unwrap(); + explorer.handle_events("asus.sass").unwrap(); // 3a. Expect the new file is added under "styles", although "styles" is not opened assert_eq!( render(&mut explorer), " -[test-explorer/new_file] +[test_explorer/new_file] ⏵ scripts ⏷ [styles] ⏵ public @@ -1279,7 +1293,7 @@ mod test_explorer { assert!(fs::read_to_string(path.join("styles/sus.sass")).is_ok()); // 4. Add a new file with non-existent parents - explorer.new_file("a/b/c").unwrap(); + explorer.handle_events("aa/b/c").unwrap(); // 4a. Expect the non-existent parents are created, // and the new file is created, @@ -1287,7 +1301,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/new_file] +[test_explorer/new_file] ⏵ scripts ⏷ [styles] ⏷ [a] @@ -1304,17 +1318,17 @@ mod test_explorer { assert!(fs::read_to_string(path.join("styles/a/b/c")).is_ok()); // 5. Move to "style.css" - explorer.tree.move_down(2); + explorer.handle_events("jj").unwrap(); // 6. Add a new file here - explorer.new_file("foobar").unwrap(); + explorer.handle_events("afoobar").unwrap(); // 6a. Expect the file is added under "styles", // because the folder of the current item, "style.css" is "styles/" assert_eq!( render(&mut explorer), " -[test-explorer/new_file] +[test_explorer/new_file] ⏷ [styles] ⏷ b c @@ -1342,7 +1356,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/remove_file] +[test_explorer/remove_file] ⏵ scripts ⏵ styles (.gitignore) @@ -1360,7 +1374,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/remove_file] +[test_explorer/remove_file] ⏵ scripts ⏵ styles (index.html) @@ -1380,7 +1394,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/remove_file] +[test_explorer/remove_file] ⏵ scripts ⏵ (styles) " @@ -1401,7 +1415,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/remove_folder] +[test_explorer/remove_folder] ⏵ scripts ⏷ (styles) ⏵ public @@ -1421,7 +1435,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/remove_folder] +[test_explorer/remove_folder] ⏵ scripts (.gitignore) index.html @@ -1447,7 +1461,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/change_root/styles] +[test_explorer/change_root/styles] ⏵ (public) style.css " @@ -1463,7 +1477,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -(test-explorer/change_root) +(test_explorer/change_root) ⏵ scripts ⏵ styles .gitignore @@ -1479,7 +1493,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/change_root/styles] +[test_explorer/change_root/styles] ⏵ (public) style.css " @@ -1496,7 +1510,7 @@ mod test_explorer { assert_eq!( render(&mut explorer), " -[test-explorer/change_root] +[test_explorer/change_root] ⏵ scripts ⏷ (styles) ⏵ public diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 35ae8c2a..6303375d 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -94,6 +94,10 @@ impl Prompt { self } + pub fn prompt(&self) -> Cow { + self.prompt.clone() + } + pub fn line(&self) -> &String { &self.line } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index c433eec4..d3897cdc 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -17,7 +17,7 @@ use tui::buffer::Buffer as Surface; use super::Prompt; pub trait TreeViewItem: Sized + Ord { - type Params; + type Params: Default; fn name(&self) -> String; fn is_parent(&self) -> bool; -- 2.38.5 From c2e2f050da1d92ad33b5d4759c9015af960a6e7d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 2 Mar 2023 10:21:36 +0800 Subject: [PATCH 064/191] feat(explorer/delete): no need to press Enter, just press y Reference: https://github.com/helix-editor/helix/pull/5768#issuecomment-1449536275 --- helix-term/src/compositor.rs | 1 + helix-term/src/ui/explorer.rs | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 8176bdaf..2f7282ab 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -116,6 +116,7 @@ pub trait Component: Any + AnyComponent { #[cfg(test)] /// Utility method for testing `handle_event` without using integration test. + /// Especially useful for testing helper components such as `Prompt`, `TreeView` etc fn handle_events(&mut self, events: &str) -> anyhow::Result<()> { use helix_view::input::parse_macro; diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index b9e25c2e..9b0eda76 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -630,14 +630,14 @@ impl Explorer { explorer.new_file(line)? } } - (PromptAction::RemoveFolder, key!(Enter)) => { - if line == "y" { + (PromptAction::RemoveFolder, key) => { + if let key!('y') = key { close_documents(current_item_path, cx)?; explorer.remove_folder()?; } } - (PromptAction::RemoveFile, key!(Enter)) => { - if line == "y" { + (PromptAction::RemoveFile, key) => { + if let key!('y') = key { close_documents(current_item_path, cx)?; explorer.remove_file()?; } @@ -1345,12 +1345,12 @@ mod test_explorer { assert!(fs::read_to_string(path.join("styles/foobar")).is_ok()); } - #[test] - fn test_remove_file() { + #[tokio::test(flavor = "multi_thread")] + async fn test_remove_file() { let (path, mut explorer) = new_explorer("remove_file"); // 1. Move to ".gitignore" - explorer.reveal_file(path.join(".gitignore")).unwrap(); + explorer.handle_events("/.gitignore").unwrap(); // 1a. Expect the cursor is at ".gitignore" assert_eq!( @@ -1368,7 +1368,7 @@ mod test_explorer { assert!(fs::read_to_string(path.join(".gitignore")).is_ok()); // 2. Remove the current file - explorer.remove_file().unwrap(); + explorer.handle_events("dy").unwrap(); // 3. Expect ".gitignore" is deleted, and the cursor moved down assert_eq!( @@ -1388,7 +1388,7 @@ mod test_explorer { assert!(fs::read_to_string(path.join("index.html")).is_ok()); // 4. Remove the current file - explorer.remove_file().unwrap(); + explorer.handle_events("dy").unwrap(); // 4a. Expect "index.html" is deleted, at the cursor moved up assert_eq!( @@ -1404,12 +1404,12 @@ mod test_explorer { assert!(fs::read_to_string(path.join("index.html")).is_err()); } - #[test] - fn test_remove_folder() { + #[tokio::test(flavor = "multi_thread")] + async fn test_remove_folder() { let (path, mut explorer) = new_explorer("remove_folder"); // 1. Move to "styles/" - explorer.reveal_file(path.join("styles")).unwrap(); + explorer.handle_events("/styleso").unwrap(); // 1a. Expect the cursor is at "styles" assert_eq!( @@ -1429,7 +1429,7 @@ mod test_explorer { assert!(fs::read_dir(path.join("styles")).is_ok()); // 2. Remove the current folder - explorer.remove_folder().unwrap(); + explorer.handle_events("dy").unwrap(); // 3. Expect "styles" is deleted, and the cursor moved down assert_eq!( -- 2.38.5 From a4943a722658bb0ca5037ee9ee0db2f9916e9dbd Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 2 Mar 2023 16:56:57 +0800 Subject: [PATCH 065/191] fix(explorer/overlay): prompt overflow - Previously the prompt appears within the float, which has very limited space - Now, the prompt will be rendered at the editor command area --- helix-term/src/commands.rs | 14 ++++++++------ helix-term/src/compositor.rs | 1 - helix-term/src/ui/editor.rs | 19 +++++++++---------- helix-term/src/ui/explorer.rs | 32 ++++++++++++-------------------- helix-term/src/ui/overlay.rs | 21 +-------------------- helix-view/src/editor.rs | 2 +- helix-view/src/graphics.rs | 28 ++++++++++++++++++++++++++++ 7 files changed, 59 insertions(+), 58 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 290374bb..c9b459cc 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2439,9 +2439,9 @@ fn open_or_focus_explorer(cx: &mut Context) { |compositor: &mut Compositor, cx: &mut compositor::Context| { if let Some(editor) = compositor.find::() { match editor.explorer.as_mut() { - Some(explore) => explore.content.focus(), + Some(explore) => explore.focus(), None => match ui::Explorer::new(cx) { - Ok(explore) => editor.explorer = Some(overlayed(explore)), + Ok(explore) => editor.explorer = Some(explore), Err(err) => cx.editor.set_error(format!("{}", err)), }, } @@ -2455,11 +2455,13 @@ fn reveal_current_file(cx: &mut Context) { |compositor: &mut Compositor, cx: &mut compositor::Context| { if let Some(editor) = compositor.find::() { (|| match editor.explorer.as_mut() { - Some(explore) => explore.content.reveal_current_file(cx), + Some(explore) => explore.reveal_current_file(cx), None => { - editor.explorer = Some(overlayed(ui::Explorer::new(cx)?)); - let explorer = editor.explorer.as_mut().unwrap(); - explorer.content.reveal_current_file(cx) + editor.explorer = Some(ui::Explorer::new(cx)?); + if let Some(explorer) = editor.explorer.as_mut() { + explorer.reveal_current_file(cx)?; + } + Ok(()) } })() .unwrap_or_else(|err| cx.editor.set_error(err.to_string())) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 2f7282ab..43c6f2e1 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -124,7 +124,6 @@ pub trait Component: Any + AnyComponent { let mut jobs = Context::dummy_jobs(); let mut cx = Context::dummy(&mut jobs, &mut editor); for event in parse_macro(events)? { - println!("Event = {}", event); self.handle_event(&Event::Key(event), &mut cx); } Ok(()) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 766a3cbf..8845526c 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -6,7 +6,6 @@ use crate::{ keymap::{KeymapResult, Keymaps}, ui::{ document::{render_document, LinePos, TextRenderer, TranslatedPosition}, - overlay::Overlay, Completion, Explorer, ProgressSpinners, }, }; @@ -43,7 +42,7 @@ pub struct EditorView { last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, - pub(crate) explorer: Option>, + pub(crate) explorer: Option, } #[derive(Debug, Clone)] @@ -1360,8 +1359,8 @@ impl Component for EditorView { }; let editor_area = if let Some(explorer) = &self.explorer { - let explorer_column_width = if explorer.content.is_opened() { - explorer.content.column_width().saturating_add(2) + let explorer_column_width = if explorer.is_opened() { + explorer.column_width().saturating_add(2) } else { 0 }; @@ -1381,14 +1380,14 @@ impl Component for EditorView { cx.editor.resize(editor_area); if let Some(explorer) = self.explorer.as_mut() { - if !explorer.content.is_focus() { + if !explorer.is_focus() { if let Some(position) = config.explorer.is_embed() { let area = if use_bufferline { area.clip_top(1) } else { area }; - explorer.content.render_embed(area, surface, cx, &position); + explorer.render_embed(area, surface, cx, &position); } } } @@ -1473,14 +1472,14 @@ impl Component for EditorView { } if let Some(explore) = self.explorer.as_mut() { - if explore.content.is_focus() { + if explore.is_focus() { if let Some(position) = config.explorer.is_embed() { let area = if use_bufferline { area.clip_top(1) } else { area }; - explore.content.render_embed(area, surface, cx, &position); + explore.render_embed(area, surface, cx, &position); } else { explore.render(area, surface, cx); } @@ -1490,11 +1489,11 @@ impl Component for EditorView { fn cursor(&self, _area: Rect, editor: &Editor) -> (Option, CursorKind) { if let Some(explore) = &self.explorer { - if explore.content.is_focus() { + if explore.is_focus() { if editor.config().explorer.is_overlay() { return explore.cursor(_area, editor); } - let cursor = explore.content.cursor(_area, editor); + let cursor = explore.cursor(_area, editor); if cursor.0.is_some() { return cursor; } diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 9b0eda76..58b94c45 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -443,17 +443,14 @@ impl Explorer { } fn render_float(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let float_area_box = area.overlayed(); let background = cx.editor.theme.get("ui.background"); - surface.clear_with(area, background); - let area = render_block(area, surface, Borders::ALL); + surface.clear_with(float_area_box, background); + let float_area = render_block(float_area_box, surface, Borders::ALL); - let mut preview_area = area.clip_left(self.column_width + 1); + let preview_area = float_area.clip_left(self.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; + prompt.render(area, surface, cx); } if self.show_help { self.render_help(preview_area, surface, cx); @@ -461,7 +458,12 @@ impl Explorer { self.render_preview(preview_area, surface, cx.editor); } - let list_area = render_block(area.clip_right(preview_area.width), surface, Borders::RIGHT); + let list_area = render_block( + float_area.clip_right(preview_area.width), + surface, + Borders::RIGHT, + ); + self.render_tree(list_area, surface, cx) } @@ -841,17 +843,7 @@ impl Component for Explorer { Some((_, prompt)) => prompt, None => return (None, CursorKind::Hidden), }; - let config = &editor.config().explorer; - let (x, y) = if config.is_overlay() { - let colw = self.column_width as u16; - if area.width > colw { - (area.x + colw + 2, area.y + area.height.saturating_sub(2)) - } else { - return (None, CursorKind::Hidden); - } - } else { - (area.x, area.y + area.height.saturating_sub(1)) - }; + let (x, y) = (area.x, area.y + area.height.saturating_sub(1)); prompt.cursor(Rect::new(x, y, area.width, 1), editor) } } diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs index 5b2bc806..f5565a69 100644 --- a/helix-term/src/ui/overlay.rs +++ b/helix-term/src/ui/overlay.rs @@ -19,26 +19,7 @@ pub struct Overlay { pub fn overlayed(content: T) -> Overlay { Overlay { content, - calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)), - } -} - -fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect { - fn mul_and_cast(size: u16, factor: u8) -> u16 { - ((size as u32) * (factor as u32) / 100).try_into().unwrap() - } - - let inner_w = mul_and_cast(rect.width, percent_horizontal); - let inner_h = mul_and_cast(rect.height, percent_vertical); - - let offset_x = rect.width.saturating_sub(inner_w) / 2; - let offset_y = rect.height.saturating_sub(inner_h) / 2; - - Rect { - x: rect.x + offset_x, - y: rect.y + offset_y, - width: inner_w, - height: inner_h, + calc_child_size: Box::new(|rect: Rect| rect.overlayed()), } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 914159d2..1155e8f0 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -249,7 +249,7 @@ impl Default for ExplorerConfig { fn default() -> Self { Self { position: ExplorerPosition::Left, - column_width: 30, + column_width: 36, } } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index a0b645fa..802f769b 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -248,6 +248,34 @@ impl Rect { && self.y < other.y + other.height && self.y + self.height > other.y } + + /// Returns a smaller `Rect` with a margin of 5% on each side, and an additional 2 rows at the bottom + pub fn overlayed(self) -> Rect { + self.clip_bottom(2).clip_relative(90, 90) + } + + /// Returns a smaller `Rect` with width and height clipped to the given `percent_horizontal` + /// and `percent_vertical`. + /// + /// Value of `percent_horizontal` and `percent_vertical` is from 0 to 100. + pub fn clip_relative(self, percent_horizontal: u8, percent_vertical: u8) -> Rect { + fn mul_and_cast(size: u16, factor: u8) -> u16 { + ((size as u32) * (factor as u32) / 100).try_into().unwrap() + } + + let inner_w = mul_and_cast(self.width, percent_horizontal); + let inner_h = mul_and_cast(self.height, percent_vertical); + + let offset_x = self.width.saturating_sub(inner_w) / 2; + let offset_y = self.height.saturating_sub(inner_h) / 2; + + Rect { + x: self.x + offset_x, + y: self.y + offset_y, + width: inner_w, + height: inner_h, + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -- 2.38.5 From d3db1b6204c51837609fb0e2288de8c0abb1e7a1 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 6 Mar 2023 10:41:13 +0800 Subject: [PATCH 066/191] style(tree): improve ancestor contrast --- helix-term/src/ui/tree.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index d3897cdc..ccaeb421 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -825,7 +825,14 @@ impl TreeView { ); } - let ancestor_style = cx.editor.theme.get("ui.text.focus"); + let ancestor_style = { + let style = cx.editor.theme.get("ui.selection"); + let fg = cx.editor.theme.get("ui.text").fg; + match (style.fg, fg) { + (None, Some(fg)) => style.fg(fg), + _ => style, + } + }; let area = area.clip_top(2); let iter = self.render_lines(area).into_iter().enumerate(); -- 2.38.5 From 31c0e84461f586e99f3d2a5ea3017940533481b7 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 6 Mar 2023 10:45:17 +0800 Subject: [PATCH 067/191] fix(ci): failing windows test & clippy --- helix-term/src/ui/explorer.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 58b94c45..267bee8b 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -1021,7 +1021,10 @@ mod test_explorer { // 0.1 Expect the current prompt to be related to file renaming let prompt = &explorer.prompt.as_ref().unwrap().1; assert_eq!(prompt.prompt(), " Rename to "); - assert_eq!(prompt.line(), "test_explorer/rename/.gitignore"); + assert_eq!( + prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), + "test_explorer/rename/.gitignore" + ); // 1. Rename the current file to a name that is lexicographically greater than "index.html" explorer.handle_events("who.is").unwrap(); @@ -1070,7 +1073,7 @@ mod test_explorer { explorer .handle_events(&format!( "r{}", - path.join("new_folder/sponge/bob").display().to_string() + path.join("new_folder/sponge/bob").display() )) .unwrap(); @@ -1113,7 +1116,7 @@ mod test_explorer { explorer .handle_events(&format!( "r{}", - path.join("scripts/bob").display().to_string() + path.join("scripts/bob").display() )) .unwrap(); @@ -1137,7 +1140,10 @@ mod test_explorer { explorer.handle_events("a").unwrap(); let prompt = &explorer.prompt.as_ref().unwrap().1; assert_eq!(prompt.prompt(), " New file or folder (ends with '/'): "); - assert_eq!(prompt.line(), "test_explorer/new_folder/"); + assert_eq!( + prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), + "test_explorer/new_folder/" + ); // 1. Add a new folder at the root explorer.handle_events("yoyo/").unwrap(); -- 2.38.5 From bc62b7615d7fe77c6b4e4b6a28eb355305989656 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 6 Mar 2023 10:45:17 +0800 Subject: [PATCH 068/191] fix(ci): failing windows test & clippy --- helix-term/src/ui/explorer.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 58b94c45..267bee8b 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -1021,7 +1021,10 @@ mod test_explorer { // 0.1 Expect the current prompt to be related to file renaming let prompt = &explorer.prompt.as_ref().unwrap().1; assert_eq!(prompt.prompt(), " Rename to "); - assert_eq!(prompt.line(), "test_explorer/rename/.gitignore"); + assert_eq!( + prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), + "test_explorer/rename/.gitignore" + ); // 1. Rename the current file to a name that is lexicographically greater than "index.html" explorer.handle_events("who.is").unwrap(); @@ -1070,7 +1073,7 @@ mod test_explorer { explorer .handle_events(&format!( "r{}", - path.join("new_folder/sponge/bob").display().to_string() + path.join("new_folder/sponge/bob").display() )) .unwrap(); @@ -1113,7 +1116,7 @@ mod test_explorer { explorer .handle_events(&format!( "r{}", - path.join("scripts/bob").display().to_string() + path.join("scripts/bob").display() )) .unwrap(); @@ -1137,7 +1140,10 @@ mod test_explorer { explorer.handle_events("a").unwrap(); let prompt = &explorer.prompt.as_ref().unwrap().1; assert_eq!(prompt.prompt(), " New file or folder (ends with '/'): "); - assert_eq!(prompt.line(), "test_explorer/new_folder/"); + assert_eq!( + prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), + "test_explorer/new_folder/" + ); // 1. Add a new folder at the root explorer.handle_events("yoyo/").unwrap(); -- 2.38.5 From aa6780e1499b3581a82aee6baeccf30e9dff25a7 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 7 Mar 2023 09:36:14 +0800 Subject: [PATCH 069/191] feat(ui/tree): tree-based movements --- helix-term/src/ui/tree.rs | 253 +++++++++++++++++++++++++++++++++++--- 1 file changed, 233 insertions(+), 20 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index ccaeb421..868849e8 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -313,7 +313,7 @@ pub struct TreeView { on_folded_fn: Option>, #[allow(clippy::type_complexity)] - on_next_key: Option>, + on_next_key: Option Result<()>>>, } impl TreeView { @@ -651,6 +651,54 @@ impl TreeView { self.set_selected(self.selected.saturating_sub(rows)) } + fn move_to_next_sibling(&mut self) -> Result<()> { + if let Some(parent) = self.current_parent()? { + if let Some(local_index) = parent + .children + .iter() + .position(|child| child.index == self.selected) + { + if let Some(next_sibling) = parent.children.get(local_index.saturating_add(1)) { + self.set_selected(next_sibling.index) + } + } + } + Ok(()) + } + + fn move_to_previous_sibling(&mut self) -> Result<()> { + if let Some(parent) = self.current_parent()? { + if let Some(local_index) = parent + .children + .iter() + .position(|child| child.index == self.selected) + { + if let Some(next_sibling) = parent.children.get(local_index.saturating_sub(1)) { + self.set_selected(next_sibling.index) + } + } + } + Ok(()) + } + + fn move_to_last_sibling(&mut self) -> Result<()> { + if let Some(parent) = self.current_parent()? { + if let Some(last) = parent.children.last() { + self.set_selected(last.index) + } + } + Ok(()) + } + + fn move_to_first_sibling(&mut self) -> Result<()> { + if let Some(parent) = self.current_parent()? { + if let Some(last) = parent.children.first() { + self.set_selected(last.index) + } + } + Ok(()) + } + fn move_left(&mut self, cols: usize) { self.column = self.column.saturating_sub(cols); } @@ -1041,35 +1089,41 @@ impl TreeView { Event::Resize(..) => return EventResult::Consumed(None), _ => return EventResult::Ignored(None), }; + (|| -> Result { + if let Some(mut on_next_key) = self.on_next_key.take() { - on_next_key(cx, self, key_event); - return EventResult::Consumed(None); + on_next_key(cx, self, key_event)?; + return Ok(EventResult::Consumed(None)); } if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) { - return EventResult::Consumed(c); + return Ok(EventResult::Consumed(c)); } if let EventResult::Consumed(c) = self.handle_filter_event(key_event, cx) { - return EventResult::Consumed(c); + return Ok(EventResult::Consumed(c)); } let count = std::mem::replace(&mut self.count, 0); let filter = self.filter.clone(); - (|| -> Result { match key_event { key!(i @ '0'..='9') => { self.count = i.to_digit(10).unwrap_or(0) as usize + count * 10 } - key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)), - key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)), + shift!('K') => self.move_up(1.max(count)), + shift!('J') => self.move_down(1.max(count)), + key!('j') | key!(Down) | ctrl!('n') => self.move_to_next_sibling()?, + key!('k') | key!(Up) | ctrl!('p') => self.move_to_previous_sibling()?, key!('z') => { - self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('z') => tree.align_view_center(), - key!('t') => tree.align_view_top(), - key!('b') => tree.align_view_bottom(), - _ => {} + self.on_next_key = Some(Box::new(|_, tree, event| { + match event { + key!('z') => tree.align_view_center(), + key!('t') => tree.align_view_top(), + key!('b') => tree.align_view_bottom(), + _ => {} + }; + Ok(()) })); } key!('h') | key!(Left) => self.move_to_parent()?, @@ -1080,12 +1134,17 @@ impl TreeView { ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('g') => { - self.on_next_key = Some(Box::new(|_, tree, event| match event { - key!('g') => tree.move_to_first_line(), - key!('e') => tree.move_to_last_line(), - key!('h') => tree.move_leftmost(), - key!('l') => tree.move_rightmost(), - _ => {} + self.on_next_key = Some(Box::new(|_, tree, event| { + match event { + shift!('G') => tree.move_to_first_line(), + shift!('E') => tree.move_to_last_line(), + key!('g') => tree.move_to_first_sibling()?, + key!('e') => tree.move_to_last_sibling()?, + key!('h') => tree.move_leftmost(), + key!('l') => tree.move_rightmost(), + _ => {} + }; + Ok(()) })); } key!('/') => self.new_search_prompt(Direction::Forward), @@ -1208,7 +1267,7 @@ impl TreeView { } pub fn prompting(&self) -> bool { - self.filter_prompt.is_some() || self.search_prompt.is_some() + self.filter_prompt.is_some() || self.search_prompt.is_some() || self.on_next_key.is_some() } } @@ -1450,6 +1509,160 @@ mod test_tree_view { ); } + #[test] + fn test_move_to_first_last_sibling() { + let mut view = dummy_tree_view(); + view.move_to_children("").unwrap(); + view.move_to_children("").unwrap(); + view.move_to_parent().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ (gary_the_snail) + ⏵ e_snail + ⏵ gary_th +⏵ karen +" + .trim() + ); + + view.move_to_last_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏵ patrick_star +⏵ plankton +⏵ sandy_cheeks +⏵ (spongebob_squarepants) +" + .trim() + ); + + view.move_to_first_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ (gary_the_snail) + ⏵ e_snail + ⏵ gary_th +⏵ karen +" + .trim() + ); + } + + #[test] + fn test_move_to_previous_next_sibling() { + let mut view = dummy_tree_view(); + view.move_to_children("").unwrap(); + view.move_to_children("").unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ [gary_the_snail] + ⏵ (e_snail) + ⏵ gary_th +⏵ karen +" + .trim() + ); + + view.move_to_next_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ [gary_the_snail] + ⏵ e_snail + ⏵ (gary_th) +⏵ karen +" + .trim() + ); + + view.move_to_next_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ [gary_the_snail] + ⏵ e_snail + ⏵ (gary_th) +⏵ karen +" + .trim() + ); + + view.move_to_previous_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ [gary_the_snail] + ⏵ (e_snail) + ⏵ gary_th +⏵ karen +" + .trim() + ); + + view.move_to_previous_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ [gary_the_snail] + ⏵ (e_snail) + ⏵ gary_th +⏵ karen +" + .trim() + ); + + view.move_to_parent().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ (gary_the_snail) + ⏵ e_snail + ⏵ gary_th +⏵ karen +" + .trim() + ); + + view.move_to_next_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ gary_the_snail + ⏵ e_snail + ⏵ gary_th +⏵ (karen) +" + .trim() + ); + + view.move_to_previous_sibling().unwrap(); + assert_eq!( + render(&mut view), + " +[who_lives_in_a_pineapple_under_the_sea] +⏷ (gary_the_snail) + ⏵ e_snail + ⏵ gary_th +⏵ karen +" + .trim() + ); + } + #[test] fn test_align_view() { let mut view = dummy_tree_view(); -- 2.38.5 From d62b487321fbd82e24d4ac3295239f29cd14d806 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 7 Mar 2023 14:57:55 +0800 Subject: [PATCH 070/191] feat(ui/tree): undo breaking changes - bind tree-based movements to other keys, namely J,K,H,L --- helix-term/src/ui/tree.rs | 71 +++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 868849e8..c1aed0c3 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -504,28 +504,28 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("k, up, C-p", "Up"), ("h, left", "Go to parent"), ("l, right", "Expand"), + ("J", "Go to next sibling"), + ("K", "Go to previous sibling"), + ("H", "Go to first child"), + ("L", "Go to last child"), + ("R", "Refresh"), ("f", "Filter"), ("/", "Search"), ("n", "Go to next search match"), ("N", "Go to previous search match"), - ("R", "Refresh"), - ("H", "Scroll left"), - ("L", "Scroll right"), - ("Home", "Scroll to the leftmost"), - ("End", "Scroll to the rightmost"), + ("gh, Home", "Scroll to the leftmost"), + ("gl, End", "Scroll to the rightmost"), ("C-o", "Jump backward"), ("C-i, Tab", "Jump forward"), ("C-d", "Half page down"), ("C-u", "Half page up"), - ("PageUp", "Full page up"), ("PageDown", "Full page down"), - ("zz", "Align view center"), + ("PageUp", "Full page up"), ("zt", "Align view top"), + ("zz", "Align view center"), ("zb", "Align view bottom"), ("gg", "Go to first line"), ("ge", "Go to last line"), - ("gh", "Go to line start"), - ("gl", "Go to line end"), ] } @@ -1090,31 +1090,37 @@ impl TreeView { _ => return EventResult::Ignored(None), }; (|| -> Result { + if let Some(mut on_next_key) = self.on_next_key.take() { + on_next_key(cx, self, key_event)?; + return Ok(EventResult::Consumed(None)); + } - if let Some(mut on_next_key) = self.on_next_key.take() { - on_next_key(cx, self, key_event)?; - return Ok(EventResult::Consumed(None)); - } - - if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) { - return Ok(EventResult::Consumed(c)); - } + if let EventResult::Consumed(c) = self.handle_search_event(key_event, cx) { + return Ok(EventResult::Consumed(c)); + } - if let EventResult::Consumed(c) = self.handle_filter_event(key_event, cx) { - return Ok(EventResult::Consumed(c)); - } + if let EventResult::Consumed(c) = self.handle_filter_event(key_event, cx) { + return Ok(EventResult::Consumed(c)); + } - let count = std::mem::replace(&mut self.count, 0); + let count = std::mem::replace(&mut self.count, 0); - let filter = self.filter.clone(); + let filter = self.filter.clone(); match key_event { key!(i @ '0'..='9') => { self.count = i.to_digit(10).unwrap_or(0) as usize + count * 10 } - shift!('K') => self.move_up(1.max(count)), - shift!('J') => self.move_down(1.max(count)), - key!('j') | key!(Down) | ctrl!('n') => self.move_to_next_sibling()?, - key!('k') | key!(Up) | ctrl!('p') => self.move_to_previous_sibling()?, + shift!('J') => self.move_to_next_sibling()?, + shift!('K') => self.move_to_previous_sibling()?, + shift!('H') => self.move_to_first_sibling()?, + shift!('L') => self.move_to_last_sibling()?, + key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)), + key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)), + key!('h') | key!(Left) => self.move_to_parent()?, + key!('l') | key!(Right) => self.move_to_children(&filter)?, + key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, &filter)?, + ctrl!('d') => self.move_down_half_page(), + ctrl!('u') => self.move_up_half_page(), key!('z') => { self.on_next_key = Some(Box::new(|_, tree, event| { match event { @@ -1126,20 +1132,11 @@ impl TreeView { Ok(()) })); } - key!('h') | key!(Left) => self.move_to_parent()?, - key!('l') | key!(Right) => self.move_to_children(&filter)?, - shift!('H') => self.move_left(1), - shift!('L') => self.move_right(1), - key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, &filter)?, - ctrl!('d') => self.move_down_half_page(), - ctrl!('u') => self.move_up_half_page(), key!('g') => { self.on_next_key = Some(Box::new(|_, tree, event| { match event { - shift!('G') => tree.move_to_first_line(), - shift!('E') => tree.move_to_last_line(), - key!('g') => tree.move_to_first_sibling()?, - key!('e') => tree.move_to_last_sibling()?, + key!('g') => tree.move_to_first_line(), + key!('e') => tree.move_to_last_line(), key!('h') => tree.move_leftmost(), key!('l') => tree.move_rightmost(), _ => {} -- 2.38.5 From e991ed9b17a0da4e89a86c95088e4516ae6f47ce Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 7 Mar 2023 15:13:13 +0800 Subject: [PATCH 071/191] refactor(runtime/themes): revert changes to theme files - This is because explorer specific styling has been abandoned for simplicity --- runtime/themes/autumn.toml | 6 - runtime/themes/base16_default_dark.toml | 6 - runtime/themes/base16_default_light.toml | 6 - runtime/themes/base16_terminal.toml | 6 - runtime/themes/bogster.toml | 6 - runtime/themes/boo_berry.toml | 6 - runtime/themes/catppuccin_mocha.toml | 8 +- runtime/themes/dark_plus.toml | 6 - runtime/themes/dracula.toml | 6 - runtime/themes/dracula_at_night.toml | 6 - runtime/themes/everforest_light.toml | 5 - runtime/themes/fleet_dark.toml | 251 ++++++++++------------ runtime/themes/gruvbox.toml | 6 - runtime/themes/gruvbox_light.toml | 6 - runtime/themes/ingrid.toml | 6 - runtime/themes/kanagawa.toml | 53 +++-- runtime/themes/monokai.toml | 8 +- runtime/themes/monokai_pro.toml | 6 - runtime/themes/monokai_pro_machine.toml | 6 - runtime/themes/monokai_pro_octagon.toml | 6 - runtime/themes/monokai_pro_ristretto.toml | 6 - runtime/themes/monokai_pro_spectrum.toml | 6 - runtime/themes/night_owl.toml | 6 - runtime/themes/nord.toml | 6 - runtime/themes/onedark.toml | 10 +- runtime/themes/papercolor-dark.toml | 1 - runtime/themes/papercolor-light.toml | 1 - runtime/themes/pop-dark.toml | 42 ++-- runtime/themes/rose_pine.toml | 6 - runtime/themes/rose_pine_dawn.toml | 6 - runtime/themes/serika-dark.toml | 6 - runtime/themes/serika-light.toml | 6 - runtime/themes/solarized_dark.toml | 6 - runtime/themes/solarized_light.toml | 6 - runtime/themes/spacebones_light.toml | 6 - runtime/themes/tokyonight.toml | 6 - runtime/themes/tokyonight_storm.toml | 6 - 37 files changed, 156 insertions(+), 391 deletions(-) diff --git a/runtime/themes/autumn.toml b/runtime/themes/autumn.toml index 2a805a5e..51860312 100644 --- a/runtime/themes/autumn.toml +++ b/runtime/themes/autumn.toml @@ -71,12 +71,6 @@ "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 56e2ba0d..1a38a6ae 100644 --- a/runtime/themes/base16_default_dark.toml +++ b/runtime/themes/base16_default_dark.toml @@ -55,12 +55,6 @@ "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" } - "ui.bufferline" = { fg = "base04", bg = "base00" } "ui.bufferline.active" = { fg = "base06", bg = "base01" } diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml index f0448917..84dab530 100644 --- a/runtime/themes/base16_default_light.toml +++ b/runtime/themes/base16_default_light.toml @@ -58,12 +58,6 @@ "ui.bufferline" = { fg = "base04", bg = "base01" } "ui.bufferline.active" = { fg = "base07", bg = "base00" } -"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 b5a31304..f3975861 100644 --- a/runtime/themes/base16_terminal.toml +++ b/runtime/themes/base16_terminal.toml @@ -51,9 +51,3 @@ "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 e6801aad..1ea13f1f 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -98,9 +98,3 @@ bogster-base5 = "#abb2bf" bogster-fg0 = "#c6b8ad" bogster-fg1 = "#e5ded6" - -"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 51b2c216..62e3b372 100644 --- a/runtime/themes/boo_berry.toml +++ b/runtime/themes/boo_berry.toml @@ -67,12 +67,6 @@ "diagnostic.info" = { underline = { color = "lilac", style = "curl"} } "diagnostic.hint" = { underline = { color = "lilac", style = "curl"} } -"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_fade = "#5A3D6E" diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 59fa430f..2504fec8 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -103,10 +103,10 @@ "ui.menu" = { fg = "overlay2", bg = "surface0" } "ui.menu.selected" = { fg = "text", bg = "surface1", modifiers = ["bold"] } -"diagnostic.error" = { underline = { color = "red", style = "curl" } } -"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } -"diagnostic.info" = { underline = { color = "sky", style = "curl" } } -"diagnostic.hint" = { underline = { color = "teal", style = "curl" } } +"diagnostic.error" = { fg = "red", underline = { color = "red", style = "curl" } } +"diagnostic.warning" = { fg = "yellow", underline = { color = "yellow", style = "curl" } } +"diagnostic.info" = { fg = "sky", underline = { color = "sky", style = "curl" } } +"diagnostic.hint" = { fg = "teal", underline = { color = "teal", style = "curl" } } error = "red" warning = "yellow" diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index b793aa7c..be8245c4 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -98,12 +98,6 @@ "diagnostic.error".underline = { color = "red", style = "curl" } "diagnostic".underline = { color = "gold", style = "curl" } -"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 f61cb1e2..51464824 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -59,12 +59,6 @@ "diagnostic".underline = { color = "orange", style = "curl" } "diagnostic.error".underline = { color = "red", style = "curl" } -"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 2e419cf5..9f10ec90 100644 --- a/runtime/themes/dracula_at_night.toml +++ b/runtime/themes/dracula_at_night.toml @@ -56,12 +56,6 @@ "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_light.toml b/runtime/themes/everforest_light.toml index 1a804a07..f9a55b0a 100644 --- a/runtime/themes/everforest_light.toml +++ b/runtime/themes/everforest_light.toml @@ -94,11 +94,6 @@ "diagnostic.info" = { underline = { color = "aqua", style = "curl" } } "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } -"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/fleet_dark.toml b/runtime/themes/fleet_dark.toml index 83caf8f3..0fc9e812 100644 --- a/runtime/themes/fleet_dark.toml +++ b/runtime/themes/fleet_dark.toml @@ -4,161 +4,128 @@ # Original author: @krfl # Contributors: # @matoous -# @kirawi -"attribute" = "Lime" - -"type" = "Blue" -"type.return" = "Blue Light" -"type.parameter" = "Blue Light" -"constructor" = "Yellow" - -"constant" = "Violet" +"attribute" = "green" +"type" = "light_blue" +"type.enum.variant" = "purple" +"constructor" = "yellow" +"constant" = "cyan" # "constant.builtin" = {} # .boolean -"constant.builtin.boolean" = "Cyan" -"constant.character" = "Yellow" -"constant.character.escape" = "Cyan" -"constant.numeric" = "Yellow" -"string" = "Pink" -"string.regexp" = "Cyan" -"string.special" = { fg = "Yellow", modifiers = ["underlined"] } #.path / .url / .symbol - -"comment" = "Gray 90" # .line +"constant.builtin.boolean" = "yellow" +"constant.character" = "yellow" +"constant.characted.escape" = "light" +"constant.numeric" = "yellow" +"string" = "pink" +"string.regexp" = "light" +"string.special" = { fg = "yellow", modifiers = ["underlined"] } #.path / .url / .symbol +"comment" = "light_gray" # .line # "comment.block" = {} # .documentation -"variable" = "Gray 120" # .builtin -"variable.builtin" = { fg = "Coral" } +"variable" = "light" # .builtin +"variable.builtin" = { fg = "red", modifiers = ["underlined"] } +"variable.parameter" = "light" # "variable.other" = {} # .member -"variable.other.member" = "Violet" -"label" = "Yellow" -"keyword" = "Cyan" # .operator / .directive / .function -"function" = "Yellow" -"function.declaration" = "#EFEFEF" -"function.macro" = "Lime" -"function.builtin" = "Lime" -"function.special" = "Lime" +"variable.other.member" = "purple" +"label" = "yellow" +"punctuation" = "light" # .delimiter / .bracket +"keyword" = "cyan" # .operator / .directive / .function +# "keyword.control" = "cyan" # .conditional / .repeat / .import / .return / .exception +"keyword.control.exception" = "purple" +"operator" = "light" +"function" = "yellow" +"function.macro" = "green" +"function.builtin" = "green" +"function.special" = "green" +"function.method" = "light" #"function.declaration.method" = { fg = "lightest", modifiers = ["bold"] } #depends on #4892 -"tag" = "Blue" -"special" = "Lime" -"namespace" = "Blue" +"tag" = "light_blue" +"special" = "green" +"namespace" = "light" # used in theming # "markup" = {} # .normal / .quote / .raw # "markup.normal" = {} # .completion / .hover -"markup.bold" = { modifiers = ["bold"] } +"markup.bold" = { fg = "lightest", modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.heading" = { fg = "Cyan", modifiers = ["bold"] } # .marker / .1 / .2 / .3 / .4 / .5 / .6 -"markup.list" = "Pink" # .unnumbered / .numbered -"markup.list.numbered" = "Cyan" -"markup.list.unnumbered" = "Cyan" +"markup.heading" = { fg = "cyan", modifiers = ["bold"] } # .marker / .1 / .2 / .3 / .4 / .5 / .6 +"markup.list" = "pink" # .unnumbered / .numbered +"markup.list.numbered" = "cyan" +"markup.list.unnumbered" = "cyan" # "markup.link" = "green" -"markup.link.url" = { fg = "Pink", modifiers = ['italic', 'underlined'] } -"markup.link.text" = "Cyan" -"markup.link.label" = "Purple 20" -"markup.quote" = "Pink" -"markup.raw" = "Pink" -"markup.raw.inline" = "Cyan" # .completion / .hover -"markup.raw.block" = "#EB83E2" - -"diff.plus" = "Green 50" -"diff.minus" = "Red 50" -"diff.delta" = "Blue 80" +"markup.link.url" = { fg = "pink", modifiers = ['italic', 'underlined'] } +"markup.link.text" = "cyan" +"markup.link.label" = "purple" +"markup.quote" = "pink" +"markup.raw" = "pink" +"markup.raw.inline" = "cyan" # .completion / .hover +"markup.raw.block" = "pink" + +"diff.plus" = "diff_plus" +"diff.minus" = "red_accent" +"diff.delta" = "blue_accent" # ui specific -"ui.background" = { bg = "Gray 10" } # .separator -"ui.statusline" = { fg = "Gray 120", bg = "Gray 10" } # .inactive / .normal / .insert / .select -# "ui.statusline.normal" = { fg = "lightest", bg = "darker"} -# "ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" } -# "ui.statusline.select" = { fg = "lightest", bg = "orange_accent" } - -"ui.cursor" = { modifiers = ["reversed"] } # .insert / .select / .match / .primary -"ui.cursor.match" = { bg = "Blue 30" } # .insert / .select / .match / .primary -"ui.selection" = { bg = "Gray 50" } # .primary -"ui.selection.primary" = { bg = "Blue 40" } - -"ui.cursorline" = { bg = "Gray 20" } -"ui.linenr" = "Gray 70" -"ui.linenr.selected" = "Gray 110" - -"ui.popup" = { fg = "Gray 120", bg = "Gray 20" } # .info -"ui.window" = { fg = "Gray 50" } -"ui.help" = { fg = "Gray 120", bg = "Gray 20" } -"ui.menu" = { fg = "Gray 120", bg = "Gray 20" } # .selected -"ui.menu.selected" = { fg = "White", bg = "Blue 40" } # .selected -# Calculated as #ffffff with 30% opacity -"ui.menu.scroll" = { fg = "#dfdfdf" } - -"ui.text" = "Gray 120" # .focus / .info -"ui.text.focus" = { fg = "White", bg = "Blue 40" } - -"ui.virtual" = "Gray 80" # .whitespace -# "ui.virtual.ruler" = { bg = "darker"} - -"hint" = "Gray 80" -"info" = "#A366C4" -"warning" = "#FACb66" -"error" = "#FF5269" - -"diagnostic.hint" = { underline = { color = "Gray 80", style = "line" } } -"diagnostic.info" = { underline = { color = "#A366C4", style = "line" } } -"diagnostic.warning" = { underline = { color = "#FACB66", style = "line" } } -"diagnostic.error" = { underline = { color = "#FF5269", style = "line" } } +"ui.background" = { bg = "background" } # .separator +"ui.cursor" = { bg = "dark_gray", modifiers = ["reversed"] } # .insert / .select / .match / .primary +"ui.cursor.match" = { fg = "light", bg = "selection" } # .insert / .select / .match / .primary +"ui.cursorline" = { bg = "darker" } +"ui.linenr" = "dark_gray" +"ui.linenr.selected" = { fg = "light", bg = "darker" } +"ui.statusline" = { fg = "light", bg = "darker" } # .inactive / .normal / .insert / .select +"ui.statusline.inactive" = { fg = "dark", bg = "darker" } +"ui.statusline.normal" = { fg = "lightest", bg = "darker"} +"ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" } +"ui.statusline.select" = { fg = "lightest", bg = "orange_accent" } +"ui.popup" = { fg = "light", bg = "darkest" } # .info +"ui.window" = { fg = "dark", bg = "darkest" } +"ui.help" = { fg = "light", bg = "darkest" } +"ui.text" = "light" # .focus / .info +"ui.text.focus" = { fg = "lightest", bg = "focus" } +"ui.virtual" = "dark" # .whitespace +"ui.virtual.ruler" = { bg = "darker"} +"ui.menu" = { fg = "light", bg = "darkest" } # .selected +"ui.menu.selected" = { fg = "lightest", bg = "focus" } # .selected +"ui.selection" = { bg = "darker" } # .primary +"ui.selection.primary" = { bg = "selection" } +"hint" = "blue" +"info" = "yellow_accent" +"warning" = "orange_accent" +"error" = "red_error" +"diagnostic" = { modifiers = [] } +"diagnostic.hint" = { underline = { color = "light", style = "curl" } } +"diagnostic.info" = { underline = { color = "blue_accent", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow_accent", style = "curl" } } +"diagnostic.error" = { underline = { color = "red_error", style = "curl" } } [palette] -"White" = "#ffffff" -"Gray 120" = "#d1d1d1" -"Gray 110" = "#c2c2c2" -"Gray 100" = "#a0a0a0" -"Gray 90" = "#898989" -"Gray 80" = "#767676" -"Gray 70" = "#5d5d5d" -"Gray 60" = "#484848" -"Gray 50" = "#383838" -"Gray 40" = "#333333" -"Gray 30" = "#2d2d2d" -"Gray 20" = "#292929" -"Gray 10" = "#181818" -"Black" = "#000000" -"Blue 110" = "#6daaf7" -"Blue 100" = "#4d9bf8" -"Blue 90" = "#3691f9" -"Blue 80" = "#1a85f6" -"Blue 70" = "#0273eb" -"Blue 60" = "#0c6ddd" -"Blue 50" = "#195eb5" -"Blue 40" = "#194176" -"Blue 30" = "#163764" -"Blue 20" = "#132c4f" -"Blue 10" = "#0b1b32" -"Red 80" = "#ec7388" -"Red 70" = "#ea4b67" -"Red 60" = "#d93953" -"Red 50" = "#ce364d" -"Red 40" = "#c03248" -"Red 30" = "#a72a3f" -"Red 20" = "#761b2d" -"Red 10" = "#390813" -"Green 50" = "#4ca988" -"Green 40" = "#3ea17f" -"Green 30" = "#028764" -"Green 20" = "#134939" -"Green 10" = "#081f19" -"Yellow 60" = "#f8ab17" -"Yellow 50" = "#e1971b" -"Yellow 40" = "#b5791f" -"Yellow 30" = "#7c511a" -"Yellow 20" = "#5a3a14" -"Yellow 10" = "#281806" -"Purple 20" = "#c07bf3" -"Purple 10" = "#b35def" - -"Blue" = "#87C3FF" -"Blue Light" = "#ADD1DE" -"Coral" = "#CC7C8A" -"Cyan" = "#82D2CE" -"Cyan Dark" = "#779E9E" -"Lime" = "#A8CC7C" -"Orange" = "#E09B70" -"Pink" = "#E394DC" -"Violet" = "#AF9CFF" -"Yellow" = "#EBC88D" +background = "#181818" +darkest = "#1e1e1e" +darker = "#292929" +dark = "#898989" + +light = "#d6d6dd" +lightest = "#ffffff" + +dark_gray = "#535353" +light_gray = "#6d6d6d" +purple = "#a390f0" +light_blue = "#7dbeff" +blue = "#52a7f6" +pink = "#d898d8" +green = "#afcb85" +cyan = "#78d0bd" +orange = "#efb080" +yellow = "#e5c995" +red = "#CC7C8A" + +blue_accent = "#2197F3" +pink_accent = "#E44C7A" +green_accent = "#00AF99" +orange_accent = "#EE7F25" +yellow_accent = "#DEA407" +red_accent = "#F44747" + +red_error = "#EB5F6A" +selection = "#1F3661" +diff_plus = "#5A9F81" +focus = "#204474" diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index d9a42478..b88becd1 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -74,12 +74,6 @@ "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 e2506fa5..bd1a5ef2 100644 --- a/runtime/themes/gruvbox_light.toml +++ b/runtime/themes/gruvbox_light.toml @@ -75,12 +75,6 @@ "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 deb46354..8942c3ca 100644 --- a/runtime/themes/ingrid.toml +++ b/runtime/themes/ingrid.toml @@ -73,9 +73,3 @@ "diagnostic.error" = { underline = { color = "#D74E50", style = "curl" } } "diagnostic.info" = { underline = { color = "#839A53", style = "curl" } } "diagnostic.hint" = { underline = { color = "#A6B6CE", style = "curl" } } - -"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/kanagawa.toml b/runtime/themes/kanagawa.toml index a7d33f3e..16e27362 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -14,8 +14,9 @@ "ui.linenr" = { fg = "sumiInk4" } "ui.linenr.selected" = { fg = "roninYellow" } -"ui.virtual" = "sumiInk4" "ui.virtual.ruler" = { bg = "sumiInk2" } +"ui.virtual.whitespace" = "waveBlue1" +"ui.virtual.indent-guide" = "sumiInk4" "ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" } "ui.statusline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } @@ -23,9 +24,8 @@ "ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen", modifiers = ["bold"] } "ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet", modifiers = ["bold"] } -"ui.bufferline" = { fg = "fujiGray", bg = "sumiInk0" } -"ui.bufferline.active" = { fg = "oldWhite", bg = "sumiInk0" } -"ui.bufferline.background" = { bg = "sumiInk0" } +"ui.bufferline" = { fg = "oldWhite", bg = "sumiInk0" } +"ui.bufferline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } "ui.popup" = { fg = "fujiWhite", bg = "sumiInk0" } "ui.window" = { fg = "sumiInk0" } @@ -33,16 +33,14 @@ "ui.text" = "fujiWhite" "ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } -"ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2"} -"ui.cursor.primary" = { fg = "waveBlue1", bg = "fujiWhite" } -"ui.cursor.match" = { fg = "waveRed", modifiers = ["bold"] } +"ui.cursor" = { fg = "waveBlue1", bg = "fujiWhite"} +"ui.cursor.primary" = { fg = "waveBlue1", bg = "seaFoam" } +"ui.cursor.match" = { fg = "seaFoam", modifiers = ["bold"] } "ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" } -"ui.menu" = { fg = "fujiWhite", bg = "waveBlue1" } -"ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue2", modifiers = ["bold"] } -"ui.menu.scroll" = { fg = "oldWhite", bg = "waveBlue1" } +"ui.menu" = { fg = "fujiWhite", bg = "sumiInk0" } +"ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } "ui.cursorline.primary" = { bg = "sumiInk3"} -"ui.cursorcolumn.primary" = { bg = "sumiInk3" } "diagnostic.error" = { underline = { color = "samuraiRed", style = "curl" } } "diagnostic.warning" = { underline = { color = "roninYellow", style = "curl" } } @@ -60,16 +58,12 @@ hint = "dragonBlue" "diff.delta" = "autumnYellow" ## Syntax highlighting -"attribute" = "waveRed" "type" = "waveAqua2" -"constructor" = "springBlue" "constant" = "surimiOrange" "constant.numeric" = "sakuraPink" "constant.character.escape" = "springBlue" "string" = "springGreen" "string.regexp" = "boatYellow2" -"string.special.url" = "springBlue" -"string.special.symbol" = "oniViolet" "comment" = "fujiGray" "variable" = "fujiWhite" "variable.builtin" = "waveRed" @@ -77,36 +71,37 @@ hint = "dragonBlue" "variable.other.member" = "carpYellow" "label" = "springBlue" "punctuation" = "springViolet2" +"punctuation.delimiter" = "springViolet2" +"punctuation.bracket" = "springViolet2" "keyword" = "oniViolet" -"keyword.control.return" = "peachRed" -"keyword.control.exception" = "peachRed" "keyword.directive" = "peachRed" "operator" = "boatYellow2" "function" = "crystalBlue" -"function.builtin" = "springBlue" +"function.builtin" = "peachRed" "function.macro" = "waveRed" -"tag" = "waveAqua2" +"tag" = "springBlue" "namespace" = "surimiOrange" +"attribute" = "peachRed" +"constructor" = "springBlue" +"module" = "waveAqua2" "special" = "peachRed" ## Markup modifiers -"markup.heading.marker" = "springViolet2" +"markup.heading.marker" = "fujiGray" "markup.heading.1" = { fg = "surimiOrange", modifiers = ["bold"] } "markup.heading.2" = { fg = "carpYellow", modifiers = ["bold"] } "markup.heading.3" = { fg = "waveAqua2", modifiers = ["bold"] } -"markup.heading.4" = { fg = "lightBlue", modifiers = ["bold"] } -"markup.heading.5" = { fg = "oniViolet", modifiers = ["bold"] } -"markup.heading.6" = { fg = "springViolet1", modifiers = ["bold"] } -"markup.list.numbered" = "sakuraPink" -"markup.list.unnumbered" = "waveRed" +"markup.heading.4" = { fg = "springGreen", modifiers = ["bold"] } +"markup.heading.5" = { fg = "waveRed", modifiers = ["bold"] } +"markup.heading.6" = { fg = "autumnRed", modifiers = ["bold"] } +"markup.list" = "oniViolet" "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.url" = { fg = "springBlue", modifiers = ["underlined"] } "markup.link.text" = "crystalBlue" -"markup.link.url" = { fg = "springBlue", underline.style = "line" } -"markup.link.label" = "surimiOrange" -"markup.quote" = "springViolet1" -"markup.raw" = "springGreen" +"markup.quote" = "seaFoam" +"markup.raw" = "seaFoam" [palette] seaFoam = "#C7CCD1" # custom lighter foreground diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index a26adc6a..b75912f7 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -78,7 +78,7 @@ "ui.statusline" = { fg = "active_text", bg = "#414339" } "ui.statusline.inactive" = { fg = "active_text", bg = "#75715e" } -"ui.text" = { fg = "text" } +"ui.text" = { fg = "text", bg = "background" } "ui.text.focus" = { fg = "active_text" } "warning" = { fg = "#cca700" } @@ -91,12 +91,6 @@ "diagnostic.info" = { underline = { color = "#75beff", style = "curl" } } "diagnostic.hint" = { underline = { color = "#eeeeeb3", style = "curl" } } -"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 9401cd46..57bede94 100644 --- a/runtime/themes/monokai_pro.toml +++ b/runtime/themes/monokai_pro.toml @@ -104,12 +104,6 @@ "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 8b43fd26..b292e6b0 100644 --- a/runtime/themes/monokai_pro_machine.toml +++ b/runtime/themes/monokai_pro_machine.toml @@ -101,12 +101,6 @@ "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 3f0b6690..3236fc16 100644 --- a/runtime/themes/monokai_pro_octagon.toml +++ b/runtime/themes/monokai_pro_octagon.toml @@ -104,12 +104,6 @@ "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 81632c87..f897bddb 100644 --- a/runtime/themes/monokai_pro_ristretto.toml +++ b/runtime/themes/monokai_pro_ristretto.toml @@ -101,12 +101,6 @@ "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 301784ce..74533404 100644 --- a/runtime/themes/monokai_pro_spectrum.toml +++ b/runtime/themes/monokai_pro_spectrum.toml @@ -101,12 +101,6 @@ "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 3a0a865b..fc27dc7c 100644 --- a/runtime/themes/night_owl.toml +++ b/runtime/themes/night_owl.toml @@ -92,12 +92,6 @@ '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 2e95f068..c653f539 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -110,12 +110,6 @@ "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 a56708fc..9cab0901 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -59,12 +59,10 @@ "ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } "ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]} -"ui.selection" = { bg = "faint-gray" } +"ui.selection" = { bg = "light-gray" } "ui.selection.primary" = { bg = "gray" } "ui.cursorline.primary" = { bg = "light-black" } -"ui.highlight" = { bg = "gray" } - "ui.linenr" = { fg = "linenr" } "ui.linenr.selected" = { fg = "white" } @@ -84,12 +82,6 @@ "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/papercolor-dark.toml b/runtime/themes/papercolor-dark.toml index eaaa36dc..088658e9 100644 --- a/runtime/themes/papercolor-dark.toml +++ b/runtime/themes/papercolor-dark.toml @@ -7,7 +7,6 @@ "ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]} "ui.selection" = {bg="selection_background", fg="selection_foreground"} "ui.cursorline" = {bg="cursorline_background"} -"ui.highlight" = {bg="cursorline_background"} "ui.statusline" = {bg="paper_bar_bg", fg="regular0"} "ui.statusline.select" = {bg="background", fg="bright7"} "ui.statusline.normal" = {bg="background", fg="bright3"} diff --git a/runtime/themes/papercolor-light.toml b/runtime/themes/papercolor-light.toml index 63671e1b..c44c6709 100644 --- a/runtime/themes/papercolor-light.toml +++ b/runtime/themes/papercolor-light.toml @@ -6,7 +6,6 @@ "ui.text" = "foreground" "ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]} "ui.selection" = {bg="selection_background", fg="selection_foreground"} -"ui.highlight" = {bg="cursorline_background"} "ui.cursorline" = {bg="cursorline_background"} "ui.statusline" = {bg="paper_bar_bg", fg="regular0"} "ui.statusline.select" = {bg="background", fg="bright7"} diff --git a/runtime/themes/pop-dark.toml b/runtime/themes/pop-dark.toml index 540d7074..2ee55903 100644 --- a/runtime/themes/pop-dark.toml +++ b/runtime/themes/pop-dark.toml @@ -8,13 +8,13 @@ info = { fg = 'yellowH', bg = 'brownD' } hint = { fg = 'brownD', bg = 'yellowH', modifiers = ['bold'] } warning = { fg = 'brownD', bg = 'orangeW', modifiers = ['bold'] } error = { fg = 'brownD', bg = 'redE', modifiers = ['bold'] } -'diagnostic.info'.underline = { color = 'yellowH', style = 'curl' } -'diagnostic.hint'.underline = { color = 'yellowH', style = 'curl' } -'diagnostic.warning'.underline = { color = 'orangeW', style = 'curl' } -'diagnostic.error'.underline = { color = 'redE', style = 'curl' } +"diagnostic.info".underline = { color = "yellowH", style = "curl" } +"diagnostic.hint".underline = { color = "yellowH", style = "curl" } +"diagnostic.warning".underline = { color = "orangeW", style = "curl" } +"diagnostic.error".underline = { color = "redE", style = "curl" } 'ui.background' = { bg = 'brownN' } -'ui.window' = { bg = 'brownH', fg = 'brownD' } -'ui.gutter' = { bg = 'brownU' } +'ui.window' = { bg = 'brownH', fg = "brownD" } +'ui.gutter' = { bg = 'brownH' } 'ui.text' = { fg = 'greyT' } 'ui.text.focus' = { fg = 'orangeN' } 'ui.text.info' = { fg = 'orangeH', bg = 'brownH' } @@ -25,20 +25,18 @@ error = { fg = 'brownD', bg = 'redE', modifiers = ['bold'] } 'ui.cursor.primary' = { fg = 'black', bg = 'orangeN' } 'ui.selection' = { bg = 'blueH', fg = 'white' } 'ui.selection.primary' = { bg = 'blueD', fg = 'white' } -'ui.linenr' = { bg = 'brownU', fg = 'greyL' } -'ui.linenr.selected' = { fg = 'orangeH' } -'ui.cursorline' = { bg = 'brownH' } -'ui.statusline.inactive' = { fg = 'greyT', bg = 'brownN' } -'ui.statusline.normal' = { fg = 'greyT', bg = 'brownD', modifiers = ['bold'] } -'ui.statusline.select' = { bg = 'blueL', fg = 'brownD', modifiers = ['bold'] } -'ui.statusline.insert' = { bg = 'orangeL', fg = 'brownD', modifiers = ['bold'] } -'ui.help' = { fg = 'greyT', bg = 'brownD' } +'ui.linenr' = { bg = "brownN", fg = 'greyL' } +'ui.linenr.selected' = { bg = 'brownH', fg = 'orangeH' } +'ui.cursorline' = { bg = 'brownD' } +'ui.statusline' = { fg = "greyT", bg = 'brownH' } +'ui.statusline.inactive' = { fg = "greyT", bg = 'brownN' } +'ui.help' = { fg = "greyT", bg = 'brownD' } 'ui.highlight' = { bg = 'brownH' } 'ui.virtual' = { fg = 'brownV' } 'ui.virtual.ruler' = { bg = 'brownR' } 'ui.virtual.whitespace' = { fg = 'brownV' } 'ui.virtual.indent-guide' = { fg = 'brownR' } -'ui.menu' = { fg = 'greyT', bg = 'brownD' } +'ui.menu' = { fg = "greyT", bg = 'brownD' } 'ui.menu.selected' = { fg = 'orangeH', bg = 'brownH' } 'ui.popup' = { bg = 'brownD' } 'ui.popup.info' = { bg = 'brownH', fg = 'greyT' } @@ -105,7 +103,7 @@ namespace = { fg = 'orangeL' } 'markup.list.unnumbered' = { fg = 'greenN' } 'markup.bold' = { modifiers = ['bold'] } 'markup.italic' = { modifiers = ['italic'] } -'markup.strikethrough' = { modifiers = ['crossed_out'] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } 'markup.link' = { fg = 'blueD' } 'markup.link.url' = { fg = 'blueL' } 'markup.link.label' = { fg = 'blueH' } @@ -126,12 +124,6 @@ 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' @@ -159,8 +151,8 @@ blueL = '#6dd2fa' blueN = '#39B7C7' blueD = '#4AAAD6' brownV = '#67634F' -brownH = '#4b4845' -brownN = '#3E3B39' +brownH = '#56524E' +brownN = '#3F3B39' brownR = '#35312f' brownD = '#2B2928' -brownU = '#4C4643' + diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index a8e360fc..06e89306 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -164,12 +164,6 @@ "diff.delta" = "highlight_high" # "diff.delta.moved" = "" -"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 02dff083..030ddccf 100644 --- a/runtime/themes/rose_pine_dawn.toml +++ b/runtime/themes/rose_pine_dawn.toml @@ -6,12 +6,6 @@ inherits = "rose_pine" -"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 341a42c2..ba9bd3b3 100644 --- a/runtime/themes/serika-dark.toml +++ b/runtime/themes/serika-dark.toml @@ -77,12 +77,6 @@ "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 dd5975e7..bac8b240 100644 --- a/runtime/themes/serika-light.toml +++ b/runtime/themes/serika-light.toml @@ -77,12 +77,6 @@ "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 = "#e1e1e3" diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml index 1272d7f7..6e5b790b 100644 --- a/runtime/themes/solarized_dark.toml +++ b/runtime/themes/solarized_dark.toml @@ -108,12 +108,6 @@ "diagnostic.info" = { underline = { style = "curl", color = "blue" } } "diagnostic.hint" = { underline = { style = "curl", color = "base01" } } -"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 40d4fc4e..ba21b90e 100644 --- a/runtime/themes/solarized_light.toml +++ b/runtime/themes/solarized_light.toml @@ -126,12 +126,6 @@ "diagnostic.hint" = { underline = { style = "curl", color = "base01" } } -"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 9ab33222..a62c7c82 100644 --- a/runtime/themes/spacebones_light.toml +++ b/runtime/themes/spacebones_light.toml @@ -78,12 +78,6 @@ "diagnostic.info" = { underline = { style = "curl", color = "theme_yellow" } } "diagnostic.hint" = { underline = { style = "curl", color = "bg2" } } -"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 52756ae3..4b082dc7 100644 --- a/runtime/themes/tokyonight.toml +++ b/runtime/themes/tokyonight.toml @@ -70,12 +70,6 @@ "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 8df690fb..e82c4340 100644 --- a/runtime/themes/tokyonight_storm.toml +++ b/runtime/themes/tokyonight_storm.toml @@ -2,12 +2,6 @@ inherits = "tokyonight" -"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] background = "#24283b" background_highlight = "#373d5a" -- 2.38.5 From 9726ae7dbba4c1b88bba1199b49198fa2dc4dd2d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 7 Mar 2023 15:16:53 +0800 Subject: [PATCH 072/191] fix(ci/test): failing on Windows --- helix-term/src/ui/explorer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 267bee8b..51cf0e69 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -1139,7 +1139,10 @@ mod test_explorer { // 0. Open the add file/folder prompt explorer.handle_events("a").unwrap(); let prompt = &explorer.prompt.as_ref().unwrap().1; - assert_eq!(prompt.prompt(), " New file or folder (ends with '/'): "); + assert_eq!( + prompt.prompt().replace(std::path::MAIN_SEPARATOR, "/"), + " New file or folder (ends with '/'): " + ); assert_eq!( prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), "test_explorer/new_folder/" -- 2.38.5 From 7ccee10297d27597c2498c84d85e52aa0d4e1d76 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 7 Mar 2023 15:21:45 +0800 Subject: [PATCH 073/191] chore: correction of e991ed9 --- runtime/themes/catppuccin_mocha.toml | 8 +- runtime/themes/everforest_dark.toml | 5 - runtime/themes/fleet_dark.toml | 251 +++++++++++++++------------ runtime/themes/kanagawa.toml | 53 +++--- runtime/themes/monokai.toml | 2 +- runtime/themes/onedark.toml | 4 +- runtime/themes/onelight.toml | 211 +++++++++++++--------- runtime/themes/papercolor-dark.toml | 1 + runtime/themes/papercolor-light.toml | 1 + runtime/themes/pop-dark.toml | 36 ++-- 10 files changed, 333 insertions(+), 239 deletions(-) diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 2504fec8..59fa430f 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -103,10 +103,10 @@ "ui.menu" = { fg = "overlay2", bg = "surface0" } "ui.menu.selected" = { fg = "text", bg = "surface1", modifiers = ["bold"] } -"diagnostic.error" = { fg = "red", underline = { color = "red", style = "curl" } } -"diagnostic.warning" = { fg = "yellow", underline = { color = "yellow", style = "curl" } } -"diagnostic.info" = { fg = "sky", underline = { color = "sky", style = "curl" } } -"diagnostic.hint" = { fg = "teal", underline = { color = "teal", style = "curl" } } +"diagnostic.error" = { underline = { color = "red", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.info" = { underline = { color = "sky", style = "curl" } } +"diagnostic.hint" = { underline = { color = "teal", style = "curl" } } error = "red" warning = "yellow" diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml index d6079971..4947e4f3 100644 --- a/runtime/themes/everforest_dark.toml +++ b/runtime/themes/everforest_dark.toml @@ -95,11 +95,6 @@ "diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } "diagnostic.error" = { underline = { color = "red", style = "curl" } } -"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/fleet_dark.toml b/runtime/themes/fleet_dark.toml index 0fc9e812..83caf8f3 100644 --- a/runtime/themes/fleet_dark.toml +++ b/runtime/themes/fleet_dark.toml @@ -4,128 +4,161 @@ # Original author: @krfl # Contributors: # @matoous +# @kirawi -"attribute" = "green" -"type" = "light_blue" -"type.enum.variant" = "purple" -"constructor" = "yellow" -"constant" = "cyan" +"attribute" = "Lime" + +"type" = "Blue" +"type.return" = "Blue Light" +"type.parameter" = "Blue Light" +"constructor" = "Yellow" + +"constant" = "Violet" # "constant.builtin" = {} # .boolean -"constant.builtin.boolean" = "yellow" -"constant.character" = "yellow" -"constant.characted.escape" = "light" -"constant.numeric" = "yellow" -"string" = "pink" -"string.regexp" = "light" -"string.special" = { fg = "yellow", modifiers = ["underlined"] } #.path / .url / .symbol -"comment" = "light_gray" # .line +"constant.builtin.boolean" = "Cyan" +"constant.character" = "Yellow" +"constant.character.escape" = "Cyan" +"constant.numeric" = "Yellow" +"string" = "Pink" +"string.regexp" = "Cyan" +"string.special" = { fg = "Yellow", modifiers = ["underlined"] } #.path / .url / .symbol + +"comment" = "Gray 90" # .line # "comment.block" = {} # .documentation -"variable" = "light" # .builtin -"variable.builtin" = { fg = "red", modifiers = ["underlined"] } -"variable.parameter" = "light" +"variable" = "Gray 120" # .builtin +"variable.builtin" = { fg = "Coral" } # "variable.other" = {} # .member -"variable.other.member" = "purple" -"label" = "yellow" -"punctuation" = "light" # .delimiter / .bracket -"keyword" = "cyan" # .operator / .directive / .function -# "keyword.control" = "cyan" # .conditional / .repeat / .import / .return / .exception -"keyword.control.exception" = "purple" -"operator" = "light" -"function" = "yellow" -"function.macro" = "green" -"function.builtin" = "green" -"function.special" = "green" -"function.method" = "light" +"variable.other.member" = "Violet" +"label" = "Yellow" +"keyword" = "Cyan" # .operator / .directive / .function +"function" = "Yellow" +"function.declaration" = "#EFEFEF" +"function.macro" = "Lime" +"function.builtin" = "Lime" +"function.special" = "Lime" #"function.declaration.method" = { fg = "lightest", modifiers = ["bold"] } #depends on #4892 -"tag" = "light_blue" -"special" = "green" -"namespace" = "light" +"tag" = "Blue" +"special" = "Lime" +"namespace" = "Blue" # used in theming # "markup" = {} # .normal / .quote / .raw # "markup.normal" = {} # .completion / .hover -"markup.bold" = { fg = "lightest", modifiers = ["bold"] } +"markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.heading" = { fg = "cyan", modifiers = ["bold"] } # .marker / .1 / .2 / .3 / .4 / .5 / .6 -"markup.list" = "pink" # .unnumbered / .numbered -"markup.list.numbered" = "cyan" -"markup.list.unnumbered" = "cyan" +"markup.heading" = { fg = "Cyan", modifiers = ["bold"] } # .marker / .1 / .2 / .3 / .4 / .5 / .6 +"markup.list" = "Pink" # .unnumbered / .numbered +"markup.list.numbered" = "Cyan" +"markup.list.unnumbered" = "Cyan" # "markup.link" = "green" -"markup.link.url" = { fg = "pink", modifiers = ['italic', 'underlined'] } -"markup.link.text" = "cyan" -"markup.link.label" = "purple" -"markup.quote" = "pink" -"markup.raw" = "pink" -"markup.raw.inline" = "cyan" # .completion / .hover -"markup.raw.block" = "pink" - -"diff.plus" = "diff_plus" -"diff.minus" = "red_accent" -"diff.delta" = "blue_accent" +"markup.link.url" = { fg = "Pink", modifiers = ['italic', 'underlined'] } +"markup.link.text" = "Cyan" +"markup.link.label" = "Purple 20" +"markup.quote" = "Pink" +"markup.raw" = "Pink" +"markup.raw.inline" = "Cyan" # .completion / .hover +"markup.raw.block" = "#EB83E2" + +"diff.plus" = "Green 50" +"diff.minus" = "Red 50" +"diff.delta" = "Blue 80" # ui specific -"ui.background" = { bg = "background" } # .separator -"ui.cursor" = { bg = "dark_gray", modifiers = ["reversed"] } # .insert / .select / .match / .primary -"ui.cursor.match" = { fg = "light", bg = "selection" } # .insert / .select / .match / .primary -"ui.cursorline" = { bg = "darker" } -"ui.linenr" = "dark_gray" -"ui.linenr.selected" = { fg = "light", bg = "darker" } -"ui.statusline" = { fg = "light", bg = "darker" } # .inactive / .normal / .insert / .select -"ui.statusline.inactive" = { fg = "dark", bg = "darker" } -"ui.statusline.normal" = { fg = "lightest", bg = "darker"} -"ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" } -"ui.statusline.select" = { fg = "lightest", bg = "orange_accent" } -"ui.popup" = { fg = "light", bg = "darkest" } # .info -"ui.window" = { fg = "dark", bg = "darkest" } -"ui.help" = { fg = "light", bg = "darkest" } -"ui.text" = "light" # .focus / .info -"ui.text.focus" = { fg = "lightest", bg = "focus" } -"ui.virtual" = "dark" # .whitespace -"ui.virtual.ruler" = { bg = "darker"} -"ui.menu" = { fg = "light", bg = "darkest" } # .selected -"ui.menu.selected" = { fg = "lightest", bg = "focus" } # .selected -"ui.selection" = { bg = "darker" } # .primary -"ui.selection.primary" = { bg = "selection" } -"hint" = "blue" -"info" = "yellow_accent" -"warning" = "orange_accent" -"error" = "red_error" -"diagnostic" = { modifiers = [] } -"diagnostic.hint" = { underline = { color = "light", style = "curl" } } -"diagnostic.info" = { underline = { color = "blue_accent", style = "curl" } } -"diagnostic.warning" = { underline = { color = "yellow_accent", style = "curl" } } -"diagnostic.error" = { underline = { color = "red_error", style = "curl" } } +"ui.background" = { bg = "Gray 10" } # .separator +"ui.statusline" = { fg = "Gray 120", bg = "Gray 10" } # .inactive / .normal / .insert / .select +# "ui.statusline.normal" = { fg = "lightest", bg = "darker"} +# "ui.statusline.insert" = { fg = "lightest", bg = "blue_accent" } +# "ui.statusline.select" = { fg = "lightest", bg = "orange_accent" } + +"ui.cursor" = { modifiers = ["reversed"] } # .insert / .select / .match / .primary +"ui.cursor.match" = { bg = "Blue 30" } # .insert / .select / .match / .primary +"ui.selection" = { bg = "Gray 50" } # .primary +"ui.selection.primary" = { bg = "Blue 40" } + +"ui.cursorline" = { bg = "Gray 20" } +"ui.linenr" = "Gray 70" +"ui.linenr.selected" = "Gray 110" + +"ui.popup" = { fg = "Gray 120", bg = "Gray 20" } # .info +"ui.window" = { fg = "Gray 50" } +"ui.help" = { fg = "Gray 120", bg = "Gray 20" } +"ui.menu" = { fg = "Gray 120", bg = "Gray 20" } # .selected +"ui.menu.selected" = { fg = "White", bg = "Blue 40" } # .selected +# Calculated as #ffffff with 30% opacity +"ui.menu.scroll" = { fg = "#dfdfdf" } + +"ui.text" = "Gray 120" # .focus / .info +"ui.text.focus" = { fg = "White", bg = "Blue 40" } + +"ui.virtual" = "Gray 80" # .whitespace +# "ui.virtual.ruler" = { bg = "darker"} + +"hint" = "Gray 80" +"info" = "#A366C4" +"warning" = "#FACb66" +"error" = "#FF5269" + +"diagnostic.hint" = { underline = { color = "Gray 80", style = "line" } } +"diagnostic.info" = { underline = { color = "#A366C4", style = "line" } } +"diagnostic.warning" = { underline = { color = "#FACB66", style = "line" } } +"diagnostic.error" = { underline = { color = "#FF5269", style = "line" } } [palette] -background = "#181818" -darkest = "#1e1e1e" -darker = "#292929" -dark = "#898989" - -light = "#d6d6dd" -lightest = "#ffffff" - -dark_gray = "#535353" -light_gray = "#6d6d6d" -purple = "#a390f0" -light_blue = "#7dbeff" -blue = "#52a7f6" -pink = "#d898d8" -green = "#afcb85" -cyan = "#78d0bd" -orange = "#efb080" -yellow = "#e5c995" -red = "#CC7C8A" - -blue_accent = "#2197F3" -pink_accent = "#E44C7A" -green_accent = "#00AF99" -orange_accent = "#EE7F25" -yellow_accent = "#DEA407" -red_accent = "#F44747" - -red_error = "#EB5F6A" -selection = "#1F3661" -diff_plus = "#5A9F81" -focus = "#204474" +"White" = "#ffffff" +"Gray 120" = "#d1d1d1" +"Gray 110" = "#c2c2c2" +"Gray 100" = "#a0a0a0" +"Gray 90" = "#898989" +"Gray 80" = "#767676" +"Gray 70" = "#5d5d5d" +"Gray 60" = "#484848" +"Gray 50" = "#383838" +"Gray 40" = "#333333" +"Gray 30" = "#2d2d2d" +"Gray 20" = "#292929" +"Gray 10" = "#181818" +"Black" = "#000000" +"Blue 110" = "#6daaf7" +"Blue 100" = "#4d9bf8" +"Blue 90" = "#3691f9" +"Blue 80" = "#1a85f6" +"Blue 70" = "#0273eb" +"Blue 60" = "#0c6ddd" +"Blue 50" = "#195eb5" +"Blue 40" = "#194176" +"Blue 30" = "#163764" +"Blue 20" = "#132c4f" +"Blue 10" = "#0b1b32" +"Red 80" = "#ec7388" +"Red 70" = "#ea4b67" +"Red 60" = "#d93953" +"Red 50" = "#ce364d" +"Red 40" = "#c03248" +"Red 30" = "#a72a3f" +"Red 20" = "#761b2d" +"Red 10" = "#390813" +"Green 50" = "#4ca988" +"Green 40" = "#3ea17f" +"Green 30" = "#028764" +"Green 20" = "#134939" +"Green 10" = "#081f19" +"Yellow 60" = "#f8ab17" +"Yellow 50" = "#e1971b" +"Yellow 40" = "#b5791f" +"Yellow 30" = "#7c511a" +"Yellow 20" = "#5a3a14" +"Yellow 10" = "#281806" +"Purple 20" = "#c07bf3" +"Purple 10" = "#b35def" + +"Blue" = "#87C3FF" +"Blue Light" = "#ADD1DE" +"Coral" = "#CC7C8A" +"Cyan" = "#82D2CE" +"Cyan Dark" = "#779E9E" +"Lime" = "#A8CC7C" +"Orange" = "#E09B70" +"Pink" = "#E394DC" +"Violet" = "#AF9CFF" +"Yellow" = "#EBC88D" diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml index 16e27362..a7d33f3e 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -14,9 +14,8 @@ "ui.linenr" = { fg = "sumiInk4" } "ui.linenr.selected" = { fg = "roninYellow" } +"ui.virtual" = "sumiInk4" "ui.virtual.ruler" = { bg = "sumiInk2" } -"ui.virtual.whitespace" = "waveBlue1" -"ui.virtual.indent-guide" = "sumiInk4" "ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" } "ui.statusline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } @@ -24,8 +23,9 @@ "ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen", modifiers = ["bold"] } "ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet", modifiers = ["bold"] } -"ui.bufferline" = { fg = "oldWhite", bg = "sumiInk0" } -"ui.bufferline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } +"ui.bufferline" = { fg = "fujiGray", bg = "sumiInk0" } +"ui.bufferline.active" = { fg = "oldWhite", bg = "sumiInk0" } +"ui.bufferline.background" = { bg = "sumiInk0" } "ui.popup" = { fg = "fujiWhite", bg = "sumiInk0" } "ui.window" = { fg = "sumiInk0" } @@ -33,14 +33,16 @@ "ui.text" = "fujiWhite" "ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } -"ui.cursor" = { fg = "waveBlue1", bg = "fujiWhite"} -"ui.cursor.primary" = { fg = "waveBlue1", bg = "seaFoam" } -"ui.cursor.match" = { fg = "seaFoam", modifiers = ["bold"] } +"ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2"} +"ui.cursor.primary" = { fg = "waveBlue1", bg = "fujiWhite" } +"ui.cursor.match" = { fg = "waveRed", modifiers = ["bold"] } "ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" } -"ui.menu" = { fg = "fujiWhite", bg = "sumiInk0" } -"ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } +"ui.menu" = { fg = "fujiWhite", bg = "waveBlue1" } +"ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue2", modifiers = ["bold"] } +"ui.menu.scroll" = { fg = "oldWhite", bg = "waveBlue1" } "ui.cursorline.primary" = { bg = "sumiInk3"} +"ui.cursorcolumn.primary" = { bg = "sumiInk3" } "diagnostic.error" = { underline = { color = "samuraiRed", style = "curl" } } "diagnostic.warning" = { underline = { color = "roninYellow", style = "curl" } } @@ -58,12 +60,16 @@ hint = "dragonBlue" "diff.delta" = "autumnYellow" ## Syntax highlighting +"attribute" = "waveRed" "type" = "waveAqua2" +"constructor" = "springBlue" "constant" = "surimiOrange" "constant.numeric" = "sakuraPink" "constant.character.escape" = "springBlue" "string" = "springGreen" "string.regexp" = "boatYellow2" +"string.special.url" = "springBlue" +"string.special.symbol" = "oniViolet" "comment" = "fujiGray" "variable" = "fujiWhite" "variable.builtin" = "waveRed" @@ -71,37 +77,36 @@ hint = "dragonBlue" "variable.other.member" = "carpYellow" "label" = "springBlue" "punctuation" = "springViolet2" -"punctuation.delimiter" = "springViolet2" -"punctuation.bracket" = "springViolet2" "keyword" = "oniViolet" +"keyword.control.return" = "peachRed" +"keyword.control.exception" = "peachRed" "keyword.directive" = "peachRed" "operator" = "boatYellow2" "function" = "crystalBlue" -"function.builtin" = "peachRed" +"function.builtin" = "springBlue" "function.macro" = "waveRed" -"tag" = "springBlue" +"tag" = "waveAqua2" "namespace" = "surimiOrange" -"attribute" = "peachRed" -"constructor" = "springBlue" -"module" = "waveAqua2" "special" = "peachRed" ## Markup modifiers -"markup.heading.marker" = "fujiGray" +"markup.heading.marker" = "springViolet2" "markup.heading.1" = { fg = "surimiOrange", modifiers = ["bold"] } "markup.heading.2" = { fg = "carpYellow", modifiers = ["bold"] } "markup.heading.3" = { fg = "waveAqua2", modifiers = ["bold"] } -"markup.heading.4" = { fg = "springGreen", modifiers = ["bold"] } -"markup.heading.5" = { fg = "waveRed", modifiers = ["bold"] } -"markup.heading.6" = { fg = "autumnRed", modifiers = ["bold"] } -"markup.list" = "oniViolet" +"markup.heading.4" = { fg = "lightBlue", modifiers = ["bold"] } +"markup.heading.5" = { fg = "oniViolet", modifiers = ["bold"] } +"markup.heading.6" = { fg = "springViolet1", modifiers = ["bold"] } +"markup.list.numbered" = "sakuraPink" +"markup.list.unnumbered" = "waveRed" "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "springBlue", modifiers = ["underlined"] } "markup.link.text" = "crystalBlue" -"markup.quote" = "seaFoam" -"markup.raw" = "seaFoam" +"markup.link.url" = { fg = "springBlue", underline.style = "line" } +"markup.link.label" = "surimiOrange" +"markup.quote" = "springViolet1" +"markup.raw" = "springGreen" [palette] seaFoam = "#C7CCD1" # custom lighter foreground diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index b75912f7..a4917cc1 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -78,7 +78,7 @@ "ui.statusline" = { fg = "active_text", bg = "#414339" } "ui.statusline.inactive" = { fg = "active_text", bg = "#75715e" } -"ui.text" = { fg = "text", bg = "background" } +"ui.text" = { fg = "text" } "ui.text.focus" = { fg = "active_text" } "warning" = { fg = "#cca700" } diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 9cab0901..81ca0463 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -59,10 +59,12 @@ "ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } "ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]} -"ui.selection" = { bg = "light-gray" } +"ui.selection" = { bg = "faint-gray" } "ui.selection.primary" = { bg = "gray" } "ui.cursorline.primary" = { bg = "light-black" } +"ui.highlight" = { bg = "gray" } + "ui.linenr" = { fg = "linenr" } "ui.linenr.selected" = { fg = "white" } diff --git a/runtime/themes/onelight.toml b/runtime/themes/onelight.toml index 7c266979..c2d4e16e 100644 --- a/runtime/themes/onelight.toml +++ b/runtime/themes/onelight.toml @@ -1,129 +1,184 @@ -# One Light # Author : erasin "attribute" = { fg = "yellow" } -"comment" = { fg = "gray", modifiers = ["italic"] } +"constructor" = { fg = "brown" } +"label" = { fg = "cyan" } +"operator" = { fg = "red" } +"tag" = { fg = "cyan" } +"namespace" = { fg = "blue" } +"special" = { fg = "deep-purple" } +"property" = { fg = "purple" } +"module" = { fg = "cyan" } -"constant" = { fg = "cyan" } +"type" = { fg = "gold" } +"type.builtin" = { fg = "light-blue" } +"type.enum" = { fg = "cyan" } +"type.enum.variant" = { fg = "cyan" } + +"constant" = { fg = "cyan", modifiers = ["bold"] } +"constant.builtin" = { fg = "deep-purple" } +"constant.builtin.boolean" = { fg = "deep-purple" } +"constant.character" = { fg = "green" } +"constant.character.escape" = { fg = "brown" } "constant.numeric" = { fg = "gold" } -"constant.builtin" = { fg = "gold" } -"constant.character.escape" = { fg = "gold" } +"constant.numeric.integer" = { fg = "gold" } +"constant.numeric.float" = { fg = "gold" } -"constructor" = { fg = "yellow" } +"string" = { fg = "green" } +"string.regexp" = { fg = "purple" } +"string.special" = { fg = "green" } +"string.special.path" = { fg = "blue" } +"string.special.url" = { fg = "light-blue" } +"string.special.symbol" = { fg = "pink" } + +"comment" = { fg = "grey", modifiers = ["italic"] } +"comment.line" = { fg = "grey", modifiers = ["italic"] } +"comment.block" = { fg = "grey", modifiers = ["italic"] } +"comment.block.documentation" = { fg = "grey", modifiers = ["italic"] } + +# "variable" = { fg = "black" } +"variable.builtin" = { fg = "light-blue" } +"variable.parameter" = { fg = "red" } +"variable.other" = { fg = "pink" } +"variable.other.member" = { fg = "pink" } -"function" = { fg = "blue" } -"function.builtin" = { fg = "cyan" } -"function.macro" = { fg = "red" } +"punctuation" = { fg = "black" } +"punctuation.delimiter" = { fg = "purple" } +"punctuation.bracket" = { fg = "brown" } +"punctuation.special" = { fg = "brown" } "keyword" = { fg = "purple" } -"keyword.function" = { fg = "purple" } "keyword.control" = { fg = "purple" } -"keyword.control.import" = { fg = "purple" } +"keyword.control.conditional" = { fg = "red", modifiers = ["bold"] } +"keyword.control.repeat" = { fg = "pink", modifiers = ["bold"] } +"keyword.control.import" = { fg = "red" } +"keyword.control.return" = { fg = "deep-purple", modifiers = ["bold"] } +"keyword.control.exception" = { fg = "purple" } +"keyword.operator" = { fg = "red" } "keyword.directive" = { fg = "purple" } -"keyword.operator" = { fg = "purple" } +"keyword.function" = { fg = "purple" } +"keyword.storage" = { fg = "purple" } "keyword.storage.type" = { fg = "purple" } +"keyword.storage.modifier" = { fg = "purple", modifiers = ["bold"] } -"tag" = "cyan" -"label" = { fg = "cyan" } -"namespace" = { fg = "red" } -"operator" = { fg = "red" } -"special" = { fg = "purple" } -"string" = { fg = "green" } -"module" = { fg = "cyan" } - -"type" = { fg = "yellow" } -"type.builtin" = { fg = "purple" } - -"punctuation" = { fg = "gray" } -"punctuation.delimiter" = { fg = "black" } -"punctuation.bracket" = { fg = "gray" } - -"variable" = { fg = "black" } -"variable.builtin" = { fg = "light-blue" } -"variable.parameter" = { fg = "red" } -"variable.other.member" = { fg = "red" } +"function" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } +"function.method" = { fg = "light-blue" } +"function.macro" = { fg = "pink", modifiers = ["bold"] } +"function.special" = { fg = "cyan" } "markup.heading" = { fg = "red" } -"markup.raw" = { fg = "gray" } -"markup.raw.inline" = { fg = "green", bg = "grey-200" } -"markup.bold" = { fg = "yellow", modifiers = ["bold"] } -"markup.italic" = { fg = "purple", modifiers = ["italic"] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.list" = { fg = "light-blue" } -"markup.quote" = { fg = "gray" } -"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] } -"markup.link.text" = { fg = "light-blue" } +"markup.heading.marker" = { fg = "red" } "markup.heading.1" = { fg = "red", modifiers = ["bold"] } -"markup.heading.2" = { fg = "gold", modifiers = ["bold"] } +"markup.heading.2" = { fg = "gold", modifiers = [ + "bold", +], underline = { style = "line" } } "markup.heading.3" = { fg = "yellow", modifiers = ["bold"] } "markup.heading.4" = { fg = "green", modifiers = ["bold"] } "markup.heading.5" = { fg = "blue", modifiers = ["bold"] } "markup.heading.6" = { fg = "purple", modifiers = ["bold"] } +"markup.list" = { fg = "light-blue" } +"markup.list.unnumbered" = { fg = "light-blue" } +"markup.list.numbered" = { fg = "light-blue" } +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "purple", modifiers = ["italic"] } +"markup.link" = { fg = "light-blue" } +"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] } +"markup.link.text" = { fg = "light-blue" } +"markup.quote" = { fg = "grey" } +"markup.raw" = { fg = "brown" } +"markup.raw.inline" = { fg = "green" } +"markup.raw.block" = { fg = "grey" } -"diff.plus" = "green" -"diff.delta" = "gold" -"diff.minus" = "red" - -"diagnostic.info".underline = { color = "blue", style = "curl" } -"diagnostic.hint".underline = { color = "green", style = "curl" } -"diagnostic.warning".underline = { color = "yellow", style = "curl" } -"diagnostic.error".underline = { color = "red", style = "curl" } - -"info" = { fg = "blue", modifiers = ["bold"] } -"hint" = { fg = "green", modifiers = ["bold"] } -"warning" = { fg = "yellow", modifiers = ["bold"] } -"error" = { fg = "red", modifiers = ["bold"] } +"diff" = { fg = "red" } +"diff.plus" = { fg = "green" } +"diff.minus" = { fg = "red" } +"diff.delta" = { fg = "cyan" } +"diff.delta.moved" = { fg = "cyan" } "ui.background" = { bg = "white" } +"ui.background.separator" = { bg = "white" } -"ui.cursor" = { fg = "white", bg = "gray" } +"ui.cursor" = { fg = "white", bg = "grey" } +"ui.cursor.normal" = { fg = "white", bg = "grey" } +"ui.cursor.insert" = { fg = "white", bg = "grey" } +"ui.cursor.select" = { fg = "white", bg = "grey" } +"ui.cursor.match" = { bg = "light-white", modifiers = ["bold"] } "ui.cursor.primary" = { fg = "white", bg = "black" } -"ui.cursor.match" = { bg = "light-gray" } +"ui.cursor.primary.normal" = { fg = "white", bg = "black" } +"ui.cursor.primary.insert" = { fg = "red", bg = "black" } +"ui.cursor.primary.select" = { fg = "white", bg = "black" } -"ui.cursorline.primary" = { fg = "white", bg = "grey-100" } -# "ui.cursorline.secondary" = { fg = "white", bg = "grey-200" } - -"ui.highlight" = { bg = "light-white" } - -"ui.selection" = { bg = "light-white", modifiers = ["dim"] } -"ui.selection.primary" = { bg = "light-white" } - -"ui.virtual" = { fg = "light-white" } -"ui.virtual.indent-guide" = { fg = "grey-500" } -"ui.virtual.ruler" = { bg = "light-white" } -"ui.virtual.whitespace" = { fg = "light-white" } +"ui.gutter" = { fg = "grey-500" } +"ui.gutter.selected" = { fg = "black" } "ui.linenr" = { fg = "grey-500" } -"ui.linenr.selected" = { fg = "black", modifiers = ["dim"] } +"ui.linenr.selected" = { fg = "black", modifiers = ["bold"] } "ui.statusline" = { fg = "black", bg = "light-white" } -"ui.statusline.inactive" = { fg = "gray", bg = "light-white" } +"ui.statusline.inactive" = { fg = "grey", bg = "grey-200" } "ui.statusline.normal" = { fg = "light-white", bg = "light-blue" } "ui.statusline.insert" = { fg = "light-white", bg = "green" } "ui.statusline.select" = { fg = "light-white", bg = "purple" } +"ui.popup" = { fg = "black", bg = "grey-200" } +"ui.popup.info" = { fg = "black", bg = "grey-200" } +"ui.window" = { fg = "grey-500", bg = "grey-100" } +"ui.help" = { fg = "black", bg = "grey-200" } + "ui.text" = { fg = "black" } "ui.text.focus" = { fg = "red", bg = "light-white", modifiers = ["bold"] } +"ui.text.inactive" = { fg = "grey" } +"ui.text.info" = { fg = "black" } + +"ui.virtual" = { fg = "light-white" } +"ui.virtual.ruler" = { bg = "light-white" } +"ui.virtual.wrap" = { bg = "light-white" } +"ui.virtual.whitespace" = { fg = "light-white" } +"ui.virtual.indent-guide" = { fg = "grey-500" } +"ui.virtual.inlay-hint" = { fg = "grey" } -"ui.help" = { fg = "black", bg = "grey-200" } -"ui.popup" = { fg = "black", bg = "grey-200" } -"ui.window" = { fg = "black", bg = "light-white" } "ui.menu" = { fg = "black", bg = "light-white" } "ui.menu.selected" = { fg = "white", bg = "light-blue" } +"ui.menu.scroll" = { fg = "light-blue", bg = "white" } + +"ui.selection" = { bg = "light-white", modifiers = ["dim"] } +"ui.selection.primary" = { bg = "light-white" } + +"ui.cursorline.primary" = { fg = "white", bg = "grey-100" } +"ui.cursorline.secondary" = { fg = "white", bg = "grey-200" } + +"ui.cursorcolumn.primary" = { fg = "white", bg = "grey-100" } +"ui.cursorcolumn.secondary" = { fg = "white", bg = "grey-200" } + +"ui.highlight" = { bg = "light-white" } + +"diagnostic.info" = { underline = { color = "blue", style = "dotted" } } +"diagnostic.hint" = { underline = { color = "green", style = "dashed" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.error" = { underline = { color = "red", style = "curl" } } + +"info" = { fg = "blue", modifiers = ["bold"] } +"hint" = { fg = "green", modifiers = ["bold"] } +"warning" = { fg = "yellow", modifiers = ["bold"] } +"error" = { fg = "red", modifiers = ["bold"] } [palette] white = "#FAFAFA" -yellow = "#A06600" +yellow = "#FF6F00" +gold = "#D35400" +brown = "#4E342E" blue = "#0061FF" -light-blue = "#1877F2" -red = "#DC003F" +light-blue = "#0091EA" +red = "#D50000" +pink = "#C2185B" purple = "#B500A9" +deep-purple = "#651FFF" green = "#24A443" -gold = "#D35400" cyan = "#0086C1" black = "#282C34" light-white = "#E3E3E3" -gray = "#5C6370" +grey = "#5C6370" grey-100 = "#F3F3F3" grey-200 = "#EDEDED" grey-500 = "#9E9E9E" diff --git a/runtime/themes/papercolor-dark.toml b/runtime/themes/papercolor-dark.toml index 088658e9..eaaa36dc 100644 --- a/runtime/themes/papercolor-dark.toml +++ b/runtime/themes/papercolor-dark.toml @@ -7,6 +7,7 @@ "ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]} "ui.selection" = {bg="selection_background", fg="selection_foreground"} "ui.cursorline" = {bg="cursorline_background"} +"ui.highlight" = {bg="cursorline_background"} "ui.statusline" = {bg="paper_bar_bg", fg="regular0"} "ui.statusline.select" = {bg="background", fg="bright7"} "ui.statusline.normal" = {bg="background", fg="bright3"} diff --git a/runtime/themes/papercolor-light.toml b/runtime/themes/papercolor-light.toml index c44c6709..63671e1b 100644 --- a/runtime/themes/papercolor-light.toml +++ b/runtime/themes/papercolor-light.toml @@ -6,6 +6,7 @@ "ui.text" = "foreground" "ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]} "ui.selection" = {bg="selection_background", fg="selection_foreground"} +"ui.highlight" = {bg="cursorline_background"} "ui.cursorline" = {bg="cursorline_background"} "ui.statusline" = {bg="paper_bar_bg", fg="regular0"} "ui.statusline.select" = {bg="background", fg="bright7"} diff --git a/runtime/themes/pop-dark.toml b/runtime/themes/pop-dark.toml index 2ee55903..d2010ed6 100644 --- a/runtime/themes/pop-dark.toml +++ b/runtime/themes/pop-dark.toml @@ -8,13 +8,13 @@ info = { fg = 'yellowH', bg = 'brownD' } hint = { fg = 'brownD', bg = 'yellowH', modifiers = ['bold'] } warning = { fg = 'brownD', bg = 'orangeW', modifiers = ['bold'] } error = { fg = 'brownD', bg = 'redE', modifiers = ['bold'] } -"diagnostic.info".underline = { color = "yellowH", style = "curl" } -"diagnostic.hint".underline = { color = "yellowH", style = "curl" } -"diagnostic.warning".underline = { color = "orangeW", style = "curl" } -"diagnostic.error".underline = { color = "redE", style = "curl" } +'diagnostic.info'.underline = { color = 'yellowH', style = 'curl' } +'diagnostic.hint'.underline = { color = 'yellowH', style = 'curl' } +'diagnostic.warning'.underline = { color = 'orangeW', style = 'curl' } +'diagnostic.error'.underline = { color = 'redE', style = 'curl' } 'ui.background' = { bg = 'brownN' } -'ui.window' = { bg = 'brownH', fg = "brownD" } -'ui.gutter' = { bg = 'brownH' } +'ui.window' = { bg = 'brownH', fg = 'brownD' } +'ui.gutter' = { bg = 'brownU' } 'ui.text' = { fg = 'greyT' } 'ui.text.focus' = { fg = 'orangeN' } 'ui.text.info' = { fg = 'orangeH', bg = 'brownH' } @@ -25,18 +25,20 @@ error = { fg = 'brownD', bg = 'redE', modifiers = ['bold'] } 'ui.cursor.primary' = { fg = 'black', bg = 'orangeN' } 'ui.selection' = { bg = 'blueH', fg = 'white' } 'ui.selection.primary' = { bg = 'blueD', fg = 'white' } -'ui.linenr' = { bg = "brownN", fg = 'greyL' } -'ui.linenr.selected' = { bg = 'brownH', fg = 'orangeH' } -'ui.cursorline' = { bg = 'brownD' } -'ui.statusline' = { fg = "greyT", bg = 'brownH' } -'ui.statusline.inactive' = { fg = "greyT", bg = 'brownN' } -'ui.help' = { fg = "greyT", bg = 'brownD' } +'ui.linenr' = { bg = 'brownU', fg = 'greyL' } +'ui.linenr.selected' = { fg = 'orangeH' } +'ui.cursorline' = { bg = 'brownH' } +'ui.statusline.inactive' = { fg = 'greyT', bg = 'brownN' } +'ui.statusline.normal' = { fg = 'greyT', bg = 'brownD', modifiers = ['bold'] } +'ui.statusline.select' = { bg = 'blueL', fg = 'brownD', modifiers = ['bold'] } +'ui.statusline.insert' = { bg = 'orangeL', fg = 'brownD', modifiers = ['bold'] } +'ui.help' = { fg = 'greyT', bg = 'brownD' } 'ui.highlight' = { bg = 'brownH' } 'ui.virtual' = { fg = 'brownV' } 'ui.virtual.ruler' = { bg = 'brownR' } 'ui.virtual.whitespace' = { fg = 'brownV' } 'ui.virtual.indent-guide' = { fg = 'brownR' } -'ui.menu' = { fg = "greyT", bg = 'brownD' } +'ui.menu' = { fg = 'greyT', bg = 'brownD' } 'ui.menu.selected' = { fg = 'orangeH', bg = 'brownH' } 'ui.popup' = { bg = 'brownD' } 'ui.popup.info' = { bg = 'brownH', fg = 'greyT' } @@ -103,7 +105,7 @@ namespace = { fg = 'orangeL' } 'markup.list.unnumbered' = { fg = 'greenN' } 'markup.bold' = { modifiers = ['bold'] } 'markup.italic' = { modifiers = ['italic'] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } +'markup.strikethrough' = { modifiers = ['crossed_out'] } 'markup.link' = { fg = 'blueD' } 'markup.link.url' = { fg = 'blueL' } 'markup.link.label' = { fg = 'blueH' } @@ -151,8 +153,8 @@ blueL = '#6dd2fa' blueN = '#39B7C7' blueD = '#4AAAD6' brownV = '#67634F' -brownH = '#56524E' -brownN = '#3F3B39' +brownH = '#4b4845' +brownN = '#3E3B39' brownR = '#35312f' brownD = '#2B2928' - +brownU = '#4C4643' -- 2.38.5 From 10032eb156e7dafd6a477cbe86801b69139252f5 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 8 Mar 2023 11:09:59 +0800 Subject: [PATCH 074/191] fix(ci): cargo fmt --- helix-term/src/commands.rs | 32 ++++++++++++++++++++------------ helix-term/src/ui/explorer.rs | 4 +++- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 22bd32d9..d7d4348c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -459,7 +459,6 @@ impl MappableCommand { command_palette, "Open command pallete", open_or_focus_explorer, "Open or focus explorer", reveal_current_file, "Reveal current file in explorer", - close_explorer, "close explorer", ); } @@ -1140,10 +1139,20 @@ fn goto_file_impl(cx: &mut Context, action: Action) { } for sel in paths { let p = sel.trim(); - if !p.is_empty() { - if let Err(e) = cx.editor.open(&PathBuf::from(p), action) { - cx.editor.set_error(format!("Open file failed: {:?}", e)); + if p.is_empty() { + continue; + } + + let path = &PathBuf::from(p); + if let Err(err) = (|| -> anyhow::Result<()> { + if path.is_file() { + cx.editor.open(path, action)?; + } else { + reveal_file(cx, Some(path.clone())); } + Ok(()) + })() { + cx.editor.set_error(format!("Open file failed: {:?}", err)); } } } @@ -2451,12 +2460,15 @@ fn open_or_focus_explorer(cx: &mut Context) { )); } -fn reveal_current_file(cx: &mut Context) { +fn reveal_file(cx: &mut Context, path: Option) { 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.reveal_current_file(cx), + Some(explorer) => match path { + Some(path) => explorer.reveal_file(path), + None => explorer.reveal_current_file(cx), + }, None => { editor.explorer = Some(ui::Explorer::new(cx)?); if let Some(explorer) = editor.explorer.as_mut() { @@ -2471,12 +2483,8 @@ fn reveal_current_file(cx: &mut Context) { )); } -fn close_explorer(cx: &mut Context) { - cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { - if let Some(editor) = compositor.find::() { - editor.explorer.take(); - } - })); +fn reveal_current_file(cx: &mut Context) { + reveal_file(cx, None) } fn buffer_picker(cx: &mut Context) { diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 51cf0e69..aea7e1cd 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -229,9 +229,11 @@ impl Explorer { Ok(()) } - fn reveal_file(&mut self, path: PathBuf) -> Result<()> { + pub fn reveal_file(&mut self, path: PathBuf) -> Result<()> { + log::error!("Reveal file = {}", path.display()); let current_root = &self.state.current_root.canonicalize()?; let current_path = &path.canonicalize()?; + log::error!("current_path = {}", current_path.display()); let segments = { let stripped = match current_path.strip_prefix(current_root) { Ok(stripped) => Ok(stripped), -- 2.38.5 From eb9287d8169841158791acf4c9de2c2bbe4ea1e6 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 9 Mar 2023 18:21:12 +0800 Subject: [PATCH 075/191] fix(ci): cargo fmt and windows test --- helix-term/src/ui/editor.rs | 2 +- helix-term/src/ui/explorer.rs | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 1ac6e29a..87333155 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -21,8 +21,8 @@ use helix_core::{ visual_offset_from_block, Position, Range, Selection, Transaction, }; use helix_view::{ - editor::{CompleteAction, CursorShapeConfig, ExplorerPosition}, document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, + editor::{CompleteAction, CursorShapeConfig, ExplorerPosition}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index aea7e1cd..f7ed358b 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -230,10 +230,8 @@ impl Explorer { } pub fn reveal_file(&mut self, path: PathBuf) -> Result<()> { - log::error!("Reveal file = {}", path.display()); let current_root = &self.state.current_root.canonicalize()?; let current_path = &path.canonicalize()?; - log::error!("current_path = {}", current_path.display()); let segments = { let stripped = match current_path.strip_prefix(current_root) { Ok(stripped) => Ok(stripped), @@ -1141,17 +1139,17 @@ mod test_explorer { // 0. Open the add file/folder prompt explorer.handle_events("a").unwrap(); let prompt = &explorer.prompt.as_ref().unwrap().1; + fn sanitize(s: &str) -> String { + s.replace(std::path::MAIN_SEPARATOR, "/") + } assert_eq!( - prompt.prompt().replace(std::path::MAIN_SEPARATOR, "/"), + sanitize(&prompt.prompt()), " New file or folder (ends with '/'): " ); - assert_eq!( - prompt.line().replace(std::path::MAIN_SEPARATOR, "/"), - "test_explorer/new_folder/" - ); + assert_eq!(sanitize(prompt.line()), "test_explorer/new_folder/"); // 1. Add a new folder at the root - explorer.handle_events("yoyo/").unwrap(); + explorer.handle_events(&sanitize("yoyo/")).unwrap(); // 1a. Expect the new folder is added, and is focused assert_eq!( @@ -1173,7 +1171,9 @@ mod test_explorer { explorer.handle_events("k").unwrap(); // 3. Add a new folder - explorer.handle_events("asus.sass/").unwrap(); + explorer + .handle_events(&sanitize("asus.sass/")) + .unwrap(); // 3a. Expect the new folder is added under "styles", although "styles" is not opened assert_eq!( @@ -1195,7 +1195,7 @@ mod test_explorer { assert!(fs::read_dir(path.join("styles/sus.sass")).is_ok()); // 4. Add a new folder with non-existent parents - explorer.handle_events("aa/b/c/").unwrap(); + explorer.handle_events(&sanitize("aa/b/c/")).unwrap(); // 4a. Expect the non-existent parents are created, // and the new folder is created, @@ -1223,7 +1223,7 @@ mod test_explorer { explorer.handle_events("j").unwrap(); // 6. Add a new folder here - explorer.handle_events("afoobar/").unwrap(); + explorer.handle_events(&sanitize("afoobar/")).unwrap(); // 6a. Expect the folder is added under "styles", // because the folder of the current item, "style.css" is "styles/" -- 2.38.5 From c4c3e8075e8770c4d9c1e7acb0fa12d0b2ca190f Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Mar 2023 12:21:22 +0800 Subject: [PATCH 076/191] style(explorer/delete): capitalize default choice Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133064678 --- helix-term/src/ui/explorer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index f7ed358b..e311c9f1 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -389,7 +389,7 @@ impl Explorer { self.prompt = Some(( PromptAction::RemoveFile, Prompt::new( - format!(" Delete file: '{}'? y/n: ", item.path.display()).into(), + format!(" Delete file: '{}'? y/N: ", item.path.display()).into(), None, ui::completers::none, |_, _, _| {}, @@ -409,7 +409,7 @@ impl Explorer { self.prompt = Some(( PromptAction::RemoveFolder, Prompt::new( - format!(" Delete folder: '{}'? y/n: ", item.path.display()).into(), + format!(" Delete folder: '{}'? y/N: ", item.path.display()).into(), None, ui::completers::none, |_, _, _| {}, -- 2.38.5 From 178086767f6f22408906f3c1120276bdf3ed3d21 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Mar 2023 12:25:02 +0800 Subject: [PATCH 077/191] refactor(ui/explorer/handle_prompt_event): remove unnecessary function Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133064955 --- helix-term/src/ui/explorer.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index e311c9f1..24c37ed6 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -612,51 +612,47 @@ impl Explorer { } fn handle_prompt_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { - fn handle_prompt_event( - explorer: &mut Explorer, - event: &KeyEvent, - cx: &mut Context, - ) -> Result { - let (action, mut prompt) = match explorer.prompt.take() { + let result = (|| -> Result { + let (action, mut prompt) = match self.prompt.take() { Some((action, p)) => (action, p), _ => return Ok(EventResult::Ignored(None)), }; let line = prompt.line(); - let current_item_path = explorer.tree.current_item()?.path.clone(); + let current_item_path = self.tree.current_item()?.path.clone(); match (&action, event) { (PromptAction::CreateFileOrFolder, key!(Enter)) => { if line.ends_with(std::path::MAIN_SEPARATOR) { - explorer.new_folder(line)? + self.new_folder(line)? } else { - explorer.new_file(line)? + self.new_file(line)? } } (PromptAction::RemoveFolder, key) => { if let key!('y') = key { close_documents(current_item_path, cx)?; - explorer.remove_folder()?; + self.remove_folder()?; } } (PromptAction::RemoveFile, key) => { if let key!('y') = key { close_documents(current_item_path, cx)?; - explorer.remove_file()?; + self.remove_file()?; } } (PromptAction::RenameFile, key!(Enter)) => { close_documents(current_item_path, cx)?; - explorer.rename_current(line)?; + self.rename_current(line)?; } (_, key!(Esc) | ctrl!('c')) => {} _ => { prompt.handle_event(&Event::Key(*event), cx); - explorer.prompt = Some((action, prompt)); + self.prompt = Some((action, prompt)); } } Ok(EventResult::Consumed(None)) - } - match handle_prompt_event(self, event, cx) { + })(); + match result { Ok(event_result) => event_result, Err(err) => { cx.editor.set_error(err.to_string()); -- 2.38.5 From 9a1aff25bd11c10d44b6a47dfdbd231f34c4b9eb Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Mar 2023 12:33:45 +0800 Subject: [PATCH 078/191] refactor(ui/explorer/close_documents): concise code Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133065427 --- helix-term/src/ui/explorer.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 24c37ed6..a8ea640a 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -755,11 +755,7 @@ fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { .documents .iter() .filter_map(|(id, doc)| { - if doc - .path() - .map(|p| p.starts_with(¤t_item_path)) - .unwrap_or(false) - { + if doc.path()?.starts_with(¤t_item_path) { Some(*id) } else { None -- 2.38.5 From 8b561e2e88f3c39428779816ea5adaeeb1ae1310 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Mar 2023 12:37:41 +0800 Subject: [PATCH 079/191] fix: type error --- helix-term/src/compositor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 43c6f2e1..ea6510c2 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -67,7 +67,7 @@ impl<'a> Context<'a> { let config = Arc::new(ArcSwap::from_pointee(Config::default())); Editor::new( Rect::new(0, 0, 60, 120), - Arc::new(theme::Loader::new("", "")), + Arc::new(theme::Loader::new(&[])), Arc::new(syntax::Loader::new(Configuration { language: vec![] })), Arc::new(Arc::new(Map::new( Arc::clone(&config), -- 2.38.5 From 41ebc30ea69969fec80d5c1d2cce97dbed466102 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Tue, 14 Mar 2023 12:38:19 +0800 Subject: [PATCH 080/191] fix(ui/tree/clone): `is_openend` should not be false Resolve https://github.com/helix-editor/helix/pull/5768#discussion_r1133066209 --- helix-term/src/ui/tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index c1aed0c3..3f4eeb5e 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -74,7 +74,7 @@ impl Clone for Tree { item: self.item.clone(), index: self.index, children: self.children.clone(), - is_opened: false, + is_opened: self.is_opened, parent_index: self.parent_index, } } -- 2.38.5 From 52be2e0c43a03a06688080c6be431f045a3bc6f7 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 16 Mar 2023 09:38:45 +0800 Subject: [PATCH 081/191] refactor(ui/tree): remove filter --- helix-term/src/ui/tree.rs | 278 ++++---------------------------------- 1 file changed, 29 insertions(+), 249 deletions(-) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 3f4eeb5e..f8ce3de7 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -120,9 +120,9 @@ impl<'a, T> DoubleEndedIterator for TreeIter<'a, T> { impl<'a, T> ExactSizeIterator for TreeIter<'a, T> {} impl Tree { - fn open(&mut self, filter: &str) -> Result<()> { + fn open(&mut self) -> Result<()> { if self.item.is_parent() { - self.children = self.get_filtered_children(filter)?; + self.children = self.get_children()?; self.is_opened = true; } Ok(()) @@ -133,11 +133,11 @@ impl Tree { self.children = vec![]; } - fn refresh(&mut self, filter: &str) -> Result<()> { + fn refresh(&mut self) -> Result<()> { if !self.is_opened { return Ok(()); } - let latest_children = self.get_filtered_children(filter)?; + let latest_children = self.get_children()?; let filtered = std::mem::take(&mut self.children) .into_iter() // Remove children that does not exists in latest_children @@ -147,7 +147,7 @@ impl Tree { .any(|child| tree.item.name().eq(&child.item.name())) }) .map(|mut tree| { - tree.refresh(filter)?; + tree.refresh()?; Ok(tree) }) .collect::>>()?; @@ -171,15 +171,10 @@ impl Tree { Ok(()) } - fn get_filtered_children(&self, filter: &str) -> Result>> { + fn get_children(&self) -> Result>> { Ok(vec_to_tree( self.item .get_children()? - .into_iter() - .filter(|item| { - item.is_parent() || item.name().to_lowercase().contains(&filter.to_lowercase()) - }) - .collect(), )) } @@ -278,12 +273,8 @@ pub struct TreeView { search_prompt: Option<(Direction, Prompt)>, - filter_prompt: Option, - search_str: String, - filter: String, - /// Selected item idex selected: usize, @@ -336,9 +327,7 @@ impl TreeView { on_folded_fn: None, on_next_key: None, search_prompt: None, - filter_prompt: None, search_str: "".into(), - filter: "".into(), }) } @@ -388,7 +377,7 @@ impl TreeView { { Some(tree) => { if !tree.is_opened { - tree.open(&self.filter)?; + tree.open()?; } Ok(tree) } @@ -445,13 +434,13 @@ impl TreeView { Ok(()) } - fn move_to_children(&mut self, filter: &str) -> Result<()> { + fn move_to_children(&mut self) -> Result<()> { let current = self.current_mut()?; if current.is_opened { self.set_selected(self.selected + 1); Ok(()) } else { - current.open(filter)?; + current.open()?; if !current.children.is_empty() { self.set_selected(self.selected + 1); self.regenerate_index(); @@ -461,15 +450,12 @@ impl TreeView { } pub fn refresh(&mut self) -> Result<()> { - self.refresh_with_filter(&self.filter.clone()) - } - - fn refresh_with_filter(&mut self, filter: &str) -> Result<()> { - self.tree.refresh(filter)?; + self.tree.refresh()?; self.set_selected(self.selected); Ok(()) } + fn move_to_first_line(&mut self) { self.move_up(usize::MAX / 2) } @@ -509,7 +495,6 @@ pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { ("H", "Go to first child"), ("L", "Go to last child"), ("R", "Refresh"), - ("f", "Filter"), ("/", "Search"), ("n", "Go to next search match"), ("N", "Go to previous search match"), @@ -535,7 +520,6 @@ impl TreeView { cx: &mut Context, params: &mut T::Params, selected_index: usize, - filter: &str, ) -> Result<()> { let selected_item = self.get_mut(selected_index)?; if selected_item.is_opened { @@ -549,7 +533,7 @@ impl TreeView { let current = self.current_mut()?; match on_open_fn(&mut current.item, cx, params) { TreeOp::GetChildsAndInsert => { - if let Err(err) = current.open(filter) { + if let Err(err) = current.open() { cx.editor.set_error(format!("{err}")) } } @@ -793,7 +777,6 @@ struct RenderTreeParams<'a, T> { prefix: &'a String, level: usize, selected: usize, - filter: &'a str, } fn render_tree( @@ -802,7 +785,6 @@ fn render_tree( prefix, level, selected, - filter, }: RenderTreeParams, ) -> Vec { let indent = if level > 0 { @@ -835,7 +817,6 @@ fn render_tree( prefix: &prefix, level: level + 1, selected, - filter, }) })) .collect() @@ -844,22 +825,7 @@ fn render_tree( impl TreeView { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { let style = cx.editor.theme.get(&self.tree_symbol_style); - - let filter_prompt_area = area.with_height(1); - if let Some(prompt) = self.filter_prompt.as_mut() { - surface.set_style(filter_prompt_area, style.add_modifier(Modifier::REVERSED)); - prompt.render_prompt(filter_prompt_area, surface, cx) - } else { - surface.set_stringn( - filter_prompt_area.x, - filter_prompt_area.y, - format!("[FILTER]: {}", self.filter.clone()), - filter_prompt_area.width as usize, - style, - ); - } - - let search_prompt_area = area.clip_top(1).with_height(1); + let search_prompt_area = area; if let Some((_, prompt)) = self.search_prompt.as_mut() { surface.set_style(search_prompt_area, style.add_modifier(Modifier::REVERSED)); prompt.render_prompt(search_prompt_area, surface, cx) @@ -882,7 +848,7 @@ impl TreeView { } }; - let area = area.clip_top(2); + let area = area.clip_top(1); let iter = self.render_lines(area).into_iter().enumerate(); for (index, line) in iter { @@ -950,7 +916,6 @@ impl TreeView { prefix: &"".to_string(), level: 0, selected: self.selected, - filter: &self.filter, }; let lines = render_tree(params); @@ -1099,13 +1064,8 @@ impl TreeView { return Ok(EventResult::Consumed(c)); } - if let EventResult::Consumed(c) = self.handle_filter_event(key_event, cx) { - return Ok(EventResult::Consumed(c)); - } - let count = std::mem::replace(&mut self.count, 0); - let filter = self.filter.clone(); match key_event { key!(i @ '0'..='9') => { self.count = i.to_digit(10).unwrap_or(0) as usize + count * 10 @@ -1117,8 +1077,8 @@ impl TreeView { key!('j') | key!(Down) | ctrl!('n') => self.move_down(1.max(count)), key!('k') | key!(Up) | ctrl!('p') => self.move_up(1.max(count)), key!('h') | key!(Left) => self.move_to_parent()?, - key!('l') | key!(Right) => self.move_to_children(&filter)?, - key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected, &filter)?, + key!('l') | key!(Right) => self.move_to_children()?, + key!(Enter) | key!('o') => self.on_enter(cx, params, self.selected)?, ctrl!('d') => self.move_down_half_page(), ctrl!('u') => self.move_up_half_page(), key!('z') => { @@ -1147,12 +1107,10 @@ impl TreeView { key!('/') => self.new_search_prompt(Direction::Forward), key!('n') => self.move_to_next_search_match(), shift!('N') => self.move_to_previous_next_match(), - key!('f') => self.new_filter_prompt(cx), key!(PageDown) => self.move_down_page(), key!(PageUp) => self.move_up_page(), shift!('R') => { - let filter = self.filter.clone(); - if let Err(error) = self.refresh_with_filter(&filter) { + if let Err(error) = self.refresh() { cx.editor.set_error(error.to_string()) } } @@ -1170,42 +1128,6 @@ impl TreeView { }) } - fn handle_filter_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { - if let Some(mut prompt) = self.filter_prompt.take() { - (|| -> Result<()> { - match event { - key!(Enter) => { - if let EventResult::Consumed(_) = - prompt.handle_event(&Event::Key(*event), cx) - { - self.saved_view = None; - self.filter = prompt.line().clone(); - self.refresh_with_filter(prompt.line())?; - } - } - key!(Esc) => self.restore_saved_view()?, - ctrl!('c') => { - self.filter.clear(); - self.refresh_with_filter("")?; - } - _ => { - if let EventResult::Consumed(_) = - prompt.handle_event(&Event::Key(*event), cx) - { - self.refresh_with_filter(prompt.line())?; - } - self.filter_prompt = Some(prompt); - } - }; - Ok(()) - })() - .unwrap_or_else(|err| cx.editor.set_error(format!("{err}"))); - EventResult::Consumed(None) - } else { - EventResult::Ignored(None) - } - } - fn handle_search_event(&mut self, event: &KeyEvent, cx: &mut Context) -> EventResult { if let Some((direction, mut prompt)) = self.search_prompt.take() { match event { @@ -1250,21 +1172,8 @@ impl TreeView { )) } - fn new_filter_prompt(&mut self, cx: &mut Context) { - self.save_view(); - self.filter_prompt = Some( - Prompt::new( - "[FILTER]: ".into(), - None, - ui::completers::none, - |_, _, _| {}, - ) - .with_line(self.filter.clone(), cx.editor), - ) - } - pub fn prompting(&self) -> bool { - self.filter_prompt.is_some() || self.search_prompt.is_some() || self.on_next_key.is_some() + self.search_prompt.is_some() || self.on_next_key.is_some() } } @@ -1361,9 +1270,6 @@ mod test_tree_view { } } - fn filter(&self, s: &str) -> bool { - self.name().to_lowercase().contains(&s.to_lowercase()) - } } fn dummy_tree_view<'a>() -> TreeView> { @@ -1509,8 +1415,8 @@ mod test_tree_view { #[test] fn test_move_to_first_last_sibling() { let mut view = dummy_tree_view(); - view.move_to_children("").unwrap(); - view.move_to_children("").unwrap(); + view.move_to_children().unwrap(); + view.move_to_children().unwrap(); view.move_to_parent().unwrap(); assert_eq!( render(&mut view), @@ -1554,8 +1460,8 @@ mod test_tree_view { #[test] fn test_move_to_previous_next_sibling() { let mut view = dummy_tree_view(); - view.move_to_children("").unwrap(); - view.move_to_children("").unwrap(); + view.move_to_children().unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), " @@ -1818,10 +1724,9 @@ mod test_tree_view { #[test] fn move_to_children_parent() { - let filter = "".to_string(); let mut view = dummy_tree_view(); view.move_down(1); - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), " @@ -1980,9 +1885,8 @@ krabby_patty #[test] fn test_move_to_parent_child() { let mut view = dummy_tree_view(); - let filter = "".to_string(); - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), " @@ -1995,7 +1899,7 @@ krabby_patty .trim() ); - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), " @@ -2228,41 +2132,6 @@ krabby_patty ); } - #[test] - fn test_refresh() { - let mut view = dummy_tree_view(); - - // 1. Move to the last child item on the tree - view.move_to_last_line(); - view.move_to_children("").unwrap(); - view.move_to_last_line(); - view.move_to_children("").unwrap(); - view.move_to_last_line(); - view.move_to_children("").unwrap(); - view.move_to_last_line(); - view.move_to_children("").unwrap(); - - // 1a. Expect the current selected item is the last child on the tree - assert_eq!( - render(&mut view), - " -⏷ [spongebob_squarepants] - ⏷ [squarepants] - ⏷ [squar] - ⏷ [uar] - (ar)" - .trim_start_matches(|c| c == '\n') - ); - - // 2. Refreshes the tree with a filter that will remove the last child - view.refresh_with_filter("ar").unwrap(); - - // 3. Get the current item - let item = view.current_item().unwrap(); - - // 3a. Expects no failure - assert_eq!(item.name, "ar") - } #[test] fn test_jump_backward_forward() { @@ -2393,9 +2262,6 @@ krabby_patty } } - fn filter(&self, s: &str) -> bool { - self.name().to_lowercase().contains(&s.to_lowercase()) - } } pub fn render(view: &mut TreeView>) -> String { @@ -2436,9 +2302,8 @@ krabby_patty ); // 1. Move down to "a", and expand it - let filter = "".to_string(); view.move_down(1); - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), @@ -2480,7 +2345,7 @@ krabby_patty ); // 4. Move to the children of "b", which is "ba" - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), " @@ -2492,7 +2357,7 @@ krabby_patty ); // 5. Move to the children of "ba", which is "baa" - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); // 5a. Expect the furthest ancestor "root" is out of view, // because when there's no enough space, the nearest ancestor takes precedence @@ -2507,7 +2372,7 @@ krabby_patty ); // 5.1 Move to child - view.move_to_children(&filter).unwrap(); + view.move_to_children().unwrap(); assert_eq!( render(&mut view), " @@ -2667,91 +2532,6 @@ krabby_patty ); } - #[tokio::test(flavor = "multi_thread")] - async fn test_filter_prompt() { - use static_tree::*; - let mut editor = Context::dummy_editor(); - let mut jobs = Context::dummy_jobs(); - let mut cx = Context::dummy(&mut jobs, &mut editor); - - let mut view = TreeView::build_tree(parent( - "root", - vec![ - parent("src", vec![child("bar.rs"), child("foo.toml")]), - parent("tests", vec![child("hello.toml"), child("spam.rs")]), - ], - )) - .unwrap(); - - fn render(view: &mut TreeView>) -> String { - view.render_to_string(dummy_area().with_height(5)) - } - - // Open all the children - view.handle_events("lljjl", &mut cx, &mut ()).unwrap(); - assert_eq!( - render(&mut view), - " -[root] - bar.rs - foo.toml -⏷ [tests] - (hello.toml) - " - .trim() - ); - - view.handle_events("frs", &mut cx, &mut ()).unwrap(); - assert_eq!( - render(&mut view), - " -[root] - bar.rs -⏷ [tests] - (spam.rs) - " - .trim() - ); - - view.handle_events("ftoml", &mut cx, &mut ()).unwrap(); - assert_eq!( - render(&mut view), - " -[root] - foo.toml -⏷ [tests] - (hello.toml) - " - .trim() - ); - - // Escape should causes the filter to be reverted - view.handle_events("", &mut cx, &mut ()).unwrap(); - assert_eq!( - render(&mut view), - " -[root] - bar.rs -⏷ [tests] - (spam.rs) - " - .trim() - ); - - // C-c should clear the filter - view.handle_events("f", &mut cx, &mut ()).unwrap(); - assert_eq!( - render(&mut view), - " -[root] - bar.rs - foo.toml -⏷ (tests) - hello.toml - " - .trim() - ); - } } #[cfg(test)] -- 2.38.5 From f5af209f09b54617ab735d5591ba32393a6cde9c Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Fri, 17 Mar 2023 09:19:45 +0800 Subject: [PATCH 082/191] refactor(explorer): remove preview - Also moved Tree search prompt to bottom --- helix-term/src/ui/explorer.rs | 153 +++------------------------------- helix-term/src/ui/tree.rs | 19 +---- 2 files changed, 17 insertions(+), 155 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index a8ea640a..37b446cc 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -21,12 +21,6 @@ use tui::{ 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(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] enum FileType { File, @@ -165,7 +159,6 @@ pub struct Explorer { tree: TreeView, history: Vec, show_help: bool, - show_preview: bool, state: State, prompt: Option<(PromptAction, Prompt)>, #[allow(clippy::type_complexity)] @@ -182,7 +175,6 @@ impl Explorer { tree: Self::new_tree_view(current_root.clone())?, history: vec![], show_help: false, - show_preview: false, state: State::new(true, current_root), prompt: None, on_next_key: None, @@ -196,7 +188,6 @@ impl Explorer { tree: Self::new_tree_view(root.clone())?, history: vec![], show_help: false, - show_preview: false, state: State::new(true, root), prompt: None, on_next_key: None, @@ -288,39 +279,6 @@ impl Explorer { self.state.focus } - fn render_preview(&mut self, area: Rect, surface: &mut Surface, editor: &Editor) { - if let Ok(current) = self.tree.current() { - let item = current.item(); - let head_area = render_block( - area.clip_bottom(area.height.saturating_sub(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"); - let content = get_preview(&item.path, body_area.height as usize) - .unwrap_or_else(|err| vec![err.to_string()]); - 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_create_file_or_folder_prompt(&mut self, cx: &mut Context) -> Result<()> { let folder_path = self.nearest_folder()?; self.prompt = Some(( @@ -448,26 +406,24 @@ impl Explorer { surface.clear_with(float_area_box, background); let float_area = render_block(float_area_box, surface, Borders::ALL); - let preview_area = float_area.clip_left(self.column_width + 1); + let help_area = float_area.clip_left(self.column_width + 1); if let Some((_, prompt)) = self.prompt.as_mut() { prompt.render(area, surface, cx); } if self.show_help { - self.render_help(preview_area, surface, cx); - } else { - self.render_preview(preview_area, surface, cx.editor); + self.render_help(help_area, surface, cx); } let list_area = render_block( - float_area.clip_right(preview_area.width), + float_area.clip_right(help_area.width), surface, Borders::RIGHT, ); - self.render_tree(list_area, surface, cx) + self.render_tree(list_area, area, surface, cx) } - fn render_tree(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render_tree(&mut self, area: Rect, prompt_area: Rect, surface: &mut Surface, cx: &mut Context) { let title_style = cx.editor.theme.get("ui.text"); let title_style = if self.is_focus() { title_style.add_modifier(Modifier::BOLD) @@ -481,7 +437,7 @@ impl Explorer { area.width.into(), title_style, ); - self.tree.render(area.clip_top(1), surface, cx); + self.tree.render(area.clip_top(1), prompt_area, surface, cx); } pub fn render_embed( @@ -520,7 +476,7 @@ impl Explorer { render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1) } }; - self.render_tree(list_area, surface, cx); + self.render_tree(list_area, prompt_area, surface, cx); { let statusline = if self.is_focus() { @@ -536,51 +492,12 @@ impl Explorer { surface.clear_with(area, statusline); } - if self.is_focus() { - if self.show_help { - let help_area = match position { - ExplorerPositionEmbed::Left => area, - ExplorerPositionEmbed::Right => { - area.clip_right(list_area.width.saturating_add(2)) - } - }; - self.render_help(help_area, surface, cx); - } - if self.show_preview { - const PREVIEW_AREA_MAX_WIDTH: u16 = 90; - const PREVIEW_AREA_MAX_HEIGHT: u16 = 30; - let preview_area_width = - (area.width.saturating_sub(side_area.width)).min(PREVIEW_AREA_MAX_WIDTH); - let preview_area_height = area.height.min(PREVIEW_AREA_MAX_HEIGHT); - - let preview_area = match position { - ExplorerPositionEmbed::Left => area.clip_left(side_area.width), - ExplorerPositionEmbed::Right => (Rect { - x: area - .width - .saturating_sub(side_area.width) - .saturating_sub(preview_area_width), - ..area - }) - .clip_right(side_area.width), - } - .clip_bottom(2); - if preview_area.width < 30 || preview_area.height < 3 { - return; - } - let y = self.tree.winline() as u16; - let y = if (preview_area_height + y) > preview_area.height { - preview_area.height.saturating_sub(preview_area_height) - } else { - y - } - .saturating_add(1); - let area = Rect::new(preview_area.x, y, preview_area_width, preview_area_height); - surface.clear_with(area, background); - let area = render_block(area, surface, Borders::all()); - - self.render_preview(area, surface, cx.editor); - } + if self.is_focus() && self.show_help { + let help_area = match position { + ExplorerPositionEmbed::Left => area, + ExplorerPositionEmbed::Right => area.clip_right(list_area.width.saturating_add(2)), + }; + self.render_help(help_area, surface, cx); } if let Some((_, prompt)) = self.prompt.as_mut() { @@ -601,7 +518,6 @@ impl Explorer { ("[", "Go to previous root"), ("+, =", "Increase size"), ("-, _", "Decrease size"), - ("C-t", "Toggle preview (left/right only)"), ("q", "Close"), ] .into_iter() @@ -743,10 +659,6 @@ impl Explorer { std::fs::remove_file(&item.path)?; self.tree.refresh() } - - fn toggle_preview(&mut self) { - self.show_preview = !self.show_preview - } } fn close_documents(current_item_path: PathBuf, cx: &mut Context) -> Result<()> { @@ -804,7 +716,6 @@ impl Component for Explorer { key!('r') => self.new_rename_prompt(cx)?, key!('-') | key!('_') => self.decrease_size(), key!('+') | key!('=') => self.increase_size(), - ctrl!('t') => self.toggle_preview(), _ => { self.tree .handle_event(&Event::Key(*key_event), cx, &mut self.state); @@ -840,44 +751,6 @@ impl Component for Explorer { } } -fn get_preview(p: impl AsRef, max_line: usize) -> Result> { - let p = p.as_ref(); - if p.is_dir() { - let mut entries = p - .read_dir()? - .filter_map(|entry| { - entry - .ok() - .and_then(|entry| dir_entry_to_file_info(entry, p)) - }) - .take(max_line) - .collect::>(); - - entries.sort(); - - return Ok(entries - .into_iter() - .map(|entry| match entry.file_type { - FileType::Folder => format!("{}/", entry.name()), - _ => entry.name(), - }) - .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); diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index f8ce3de7..fe533c07 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -823,21 +823,11 @@ fn render_tree( } impl TreeView { - pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + pub fn render(&mut self, area: Rect, prompt_area: Rect, surface: &mut Surface, cx: &mut Context) { let style = cx.editor.theme.get(&self.tree_symbol_style); - let search_prompt_area = area; if let Some((_, prompt)) = self.search_prompt.as_mut() { - surface.set_style(search_prompt_area, style.add_modifier(Modifier::REVERSED)); - prompt.render_prompt(search_prompt_area, surface, cx) - } else { - surface.set_stringn( - search_prompt_area.x, - search_prompt_area.y, - format!("[SEARCH]: {}", self.search_str.clone()), - search_prompt_area.width as usize, - style, - ); - } + prompt.render_prompt(prompt_area, surface, cx) + } let ancestor_style = { let style = cx.editor.theme.get("ui.selection"); @@ -848,7 +838,6 @@ impl TreeView { } }; - let area = area.clip_top(1); let iter = self.render_lines(area).into_iter().enumerate(); for (index, line) in iter { @@ -1164,7 +1153,7 @@ impl TreeView { self.search_prompt = Some(( direction, Prompt::new( - "[SEARCH]: ".into(), + "search: ".into(), None, ui::completers::none, |_, _, _| {}, -- 2.38.5 From afda68a11d68d260a3864b32181686b7d2d5ebbd Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Fri, 17 Mar 2023 09:47:40 +0800 Subject: [PATCH 083/191] chore: cargo fmt --- helix-term/src/ui/explorer.rs | 8 +++++++- helix-term/src/ui/tree.rs | 27 ++++++++++----------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 37b446cc..5f1bbeb8 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -423,7 +423,13 @@ impl Explorer { self.render_tree(list_area, area, surface, cx) } - fn render_tree(&mut self, area: Rect, prompt_area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render_tree( + &mut self, + area: Rect, + prompt_area: Rect, + surface: &mut Surface, + cx: &mut Context, + ) { let title_style = cx.editor.theme.get("ui.text"); let title_style = if self.is_focus() { title_style.add_modifier(Modifier::BOLD) diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index fe533c07..516e9459 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -172,10 +172,7 @@ impl Tree { } fn get_children(&self) -> Result>> { - Ok(vec_to_tree( - self.item - .get_children()? - )) + Ok(vec_to_tree(self.item.get_children()?)) } fn sort(&mut self) { @@ -455,7 +452,6 @@ impl TreeView { Ok(()) } - fn move_to_first_line(&mut self) { self.move_up(usize::MAX / 2) } @@ -823,11 +819,17 @@ fn render_tree( } impl TreeView { - pub fn render(&mut self, area: Rect, prompt_area: Rect, surface: &mut Surface, cx: &mut Context) { + pub fn render( + &mut self, + area: Rect, + prompt_area: Rect, + surface: &mut Surface, + cx: &mut Context, + ) { let style = cx.editor.theme.get(&self.tree_symbol_style); if let Some((_, prompt)) = self.search_prompt.as_mut() { prompt.render_prompt(prompt_area, surface, cx) - } + } let ancestor_style = { let style = cx.editor.theme.get("ui.selection"); @@ -1152,12 +1154,7 @@ impl TreeView { self.save_view(); self.search_prompt = Some(( direction, - Prompt::new( - "search: ".into(), - None, - ui::completers::none, - |_, _, _| {}, - ), + Prompt::new("search: ".into(), None, ui::completers::none, |_, _, _| {}), )) } @@ -1258,7 +1255,6 @@ mod test_tree_view { Ok(vec![]) } } - } fn dummy_tree_view<'a>() -> TreeView> { @@ -2121,7 +2117,6 @@ krabby_patty ); } - #[test] fn test_jump_backward_forward() { let mut view = dummy_tree_view(); @@ -2250,7 +2245,6 @@ krabby_patty None => Ok(vec![]), } } - } pub fn render(view: &mut TreeView>) -> String { @@ -2520,7 +2514,6 @@ krabby_patty .trim() ); } - } #[cfg(test)] -- 2.38.5 From e5dfde2a9b08bfa1fc108745622b0ee4eca46890 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Fri, 17 Mar 2023 17:53:06 +0800 Subject: [PATCH 084/191] refactor(explorer): remove overlay option --- helix-term/src/ui/editor.rs | 32 ++++++++------------- helix-term/src/ui/explorer.rs | 52 +++++++++-------------------------- helix-view/src/editor.rs | 20 -------------- 3 files changed, 24 insertions(+), 80 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index bf5a999c..03522194 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1386,7 +1386,6 @@ impl Component for EditorView { // We should have a Dock trait that allows a component to dock to the top/left/bottom/right // of another component. match config.explorer.position { - ExplorerPosition::Overlay => editor_area, ExplorerPosition::Left => editor_area.clip_left(explorer_column_width), ExplorerPosition::Right => editor_area.clip_right(explorer_column_width), } @@ -1399,14 +1398,12 @@ impl Component for EditorView { if let Some(explorer) = self.explorer.as_mut() { if !explorer.is_focus() { - if let Some(position) = config.explorer.is_embed() { - let area = if use_bufferline { - area.clip_top(1) - } else { - area - }; - explorer.render_embed(area, surface, cx, &position); - } + let area = if use_bufferline { + area.clip_top(1) + } else { + area + }; + explorer.render(area, surface, cx); } } @@ -1491,16 +1488,12 @@ impl Component for EditorView { if let Some(explore) = self.explorer.as_mut() { if explore.is_focus() { - if let Some(position) = config.explorer.is_embed() { - let area = if use_bufferline { - area.clip_top(1) - } else { - area - }; - explore.render_embed(area, surface, cx, &position); + let area = if use_bufferline { + area.clip_top(1) } else { - explore.render(area, surface, cx); - } + area + }; + explore.render(area, surface, cx); } } } @@ -1508,9 +1501,6 @@ impl Component for EditorView { fn cursor(&self, _area: Rect, editor: &Editor) -> (Option, CursorKind) { if let Some(explore) = &self.explorer { if explore.is_focus() { - if editor.config().explorer.is_overlay() { - return explore.cursor(_area, editor); - } let cursor = explore.cursor(_area, editor); if cursor.0.is_some() { return cursor; diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 5f1bbeb8..22020c91 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{bail, ensure, Result}; use helix_core::Position; use helix_view::{ - editor::{Action, ExplorerPositionEmbed}, + editor::{Action, ExplorerPosition}, graphics::{CursorKind, Rect}, info::Info, input::{Event, KeyEvent}, @@ -400,29 +400,6 @@ impl Explorer { }) } - fn render_float(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let float_area_box = area.overlayed(); - let background = cx.editor.theme.get("ui.background"); - surface.clear_with(float_area_box, background); - let float_area = render_block(float_area_box, surface, Borders::ALL); - - let help_area = float_area.clip_left(self.column_width + 1); - if let Some((_, prompt)) = self.prompt.as_mut() { - prompt.render(area, surface, cx); - } - if self.show_help { - self.render_help(help_area, surface, cx); - } - - let list_area = render_block( - float_area.clip_right(help_area.width), - surface, - Borders::RIGHT, - ); - - self.render_tree(list_area, area, surface, cx) - } - fn render_tree( &mut self, area: Rect, @@ -446,12 +423,12 @@ impl Explorer { self.tree.render(area.clip_top(1), prompt_area, surface, cx); } - pub fn render_embed( + fn render_embed( &mut self, area: Rect, surface: &mut Surface, cx: &mut Context, - position: &ExplorerPositionEmbed, + position: &ExplorerPosition, ) { if !self.state.open { return; @@ -461,8 +438,8 @@ impl Explorer { self.state.area_width = area.width; let side_area = match position { - ExplorerPositionEmbed::Left => Rect { width, ..area }, - ExplorerPositionEmbed::Right => Rect { + ExplorerPosition::Left => Rect { width, ..area }, + ExplorerPosition::Right => Rect { x: area.width - width, width, ..area @@ -475,10 +452,10 @@ impl Explorer { let prompt_area = area.clip_top(side_area.height); let list_area = match position { - ExplorerPositionEmbed::Left => { + ExplorerPosition::Left => { render_block(side_area.clip_left(1), surface, Borders::RIGHT).clip_bottom(1) } - ExplorerPositionEmbed::Right => { + ExplorerPosition::Right => { render_block(side_area.clip_right(1), surface, Borders::LEFT).clip_bottom(1) } }; @@ -492,16 +469,16 @@ impl Explorer { }; let area = side_area.clip_top(list_area.height); let area = match position { - ExplorerPositionEmbed::Left => area.clip_right(1), - ExplorerPositionEmbed::Right => area.clip_left(1), + ExplorerPosition::Left => area.clip_right(1), + ExplorerPosition::Right => area.clip_left(1), }; surface.clear_with(area, statusline); } if self.is_focus() && self.show_help { let help_area = match position { - ExplorerPositionEmbed::Left => area, - ExplorerPositionEmbed::Right => area.clip_right(list_area.width.saturating_add(2)), + ExplorerPosition::Left => area, + ExplorerPosition::Right => area.clip_right(list_area.width.saturating_add(2)), }; self.render_help(help_area, surface, cx); } @@ -740,11 +717,8 @@ impl Component for Explorer { return; } let config = &cx.editor.config().explorer; - if let Some(position) = config.is_embed() { - self.render_embed(area, surface, cx, &position); - } else { - self.render_float(area, surface, cx); - } + let position = config.position; + self.render_embed(area, surface, cx, &position); } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 8be7af92..03e4a8e6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -221,30 +221,10 @@ pub struct ExplorerConfig { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ExplorerPosition { - Overlay, Left, Right, } -pub enum ExplorerPositionEmbed { - Left, - Right, -} - -impl ExplorerConfig { - pub fn is_embed(&self) -> Option { - match self.position { - ExplorerPosition::Overlay => None, - ExplorerPosition::Left => Some(ExplorerPositionEmbed::Left), - ExplorerPosition::Right => Some(ExplorerPositionEmbed::Right), - } - } - - pub fn is_overlay(&self) -> bool { - matches!(self.position, ExplorerPosition::Overlay) - } -} - impl Default for ExplorerConfig { fn default() -> Self { Self { -- 2.38.5 From 1be2ac286bfb549954e99f40a14394ce765d444d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 18 Mar 2023 13:21:54 +0800 Subject: [PATCH 085/191] fix(ui/explorer): tree search cursor not rendered --- helix-term/src/ui/explorer.rs | 17 +++++++++++------ helix-term/src/ui/tree.rs | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 22020c91..5563d1c0 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -722,12 +722,17 @@ impl Component for Explorer { } 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 (x, y) = (area.x, area.y + area.height.saturating_sub(1)); - prompt.cursor(Rect::new(x, y, area.width, 1), editor) + if let Some(prompt) = self + .prompt + .as_ref() + .map(|(_, prompt)| prompt) + .or_else(|| self.tree.prompt()) + { + let (x, y) = (area.x, area.y + area.height.saturating_sub(1)); + prompt.cursor(Rect::new(x, y, area.width, 1), editor) + } else { + (None, CursorKind::Hidden) + } } } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 516e9459..7199570e 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -477,6 +477,14 @@ impl TreeView { Ok(()) } } + + pub fn prompt(&self) -> Option<&Prompt> { + if let Some((_, prompt)) = self.search_prompt.as_ref() { + Some(prompt) + } else { + None + } + } } pub fn tree_view_help() -> Vec<(&'static str, &'static str)> { -- 2.38.5 From ee34720a31bb4e89802973f22e6c9e9a766e5416 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Sat, 18 Mar 2023 13:30:56 +0800 Subject: [PATCH 086/191] style(explorer): move title to statusline - so that the UI is more consistent with other component of the editor - also it may improve the focus indication --- helix-term/src/ui/explorer.rs | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 5563d1c0..5d6bc1ce 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -407,20 +407,7 @@ impl Explorer { surface: &mut Surface, cx: &mut Context, ) { - let title_style = cx.editor.theme.get("ui.text"); - let title_style = if self.is_focus() { - title_style.add_modifier(Modifier::BOLD) - } else { - title_style - }; - surface.set_stringn( - area.x, - area.y, - "Explorer: press ? for help", - area.width.into(), - title_style, - ); - self.tree.render(area.clip_top(1), prompt_area, surface, cx); + self.tree.render(area, prompt_area, surface, cx); } fn render_embed( @@ -473,6 +460,24 @@ impl Explorer { ExplorerPosition::Right => area.clip_left(1), }; surface.clear_with(area, statusline); + + let title_style = cx.editor.theme.get("ui.text"); + let title_style = if self.is_focus() { + title_style.add_modifier(Modifier::BOLD) + } else { + title_style + }; + surface.set_stringn( + area.x, + area.y, + if self.is_focus() { + " EXPLORER: press ? for help" + } else { + " EXPLORER" + }, + area.width.into(), + title_style, + ); } if self.is_focus() && self.show_help { -- 2.38.5 From 404f950b09c65ff49c4997cc4b646a24cb8ca651 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Mar 2023 09:08:51 +0800 Subject: [PATCH 087/191] fix(tests/explorer/new_folder): failing on Windows Co-authored-by: LEI Reference: https://github.com/helix-editor/helix/pull/5768#discussion_r1143991188 --- helix-term/src/ui/explorer.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 5d6bc1ce..5dc53d90 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -994,17 +994,22 @@ mod test_explorer { // 0. Open the add file/folder prompt explorer.handle_events("a").unwrap(); let prompt = &explorer.prompt.as_ref().unwrap().1; - fn sanitize(s: &str) -> String { + fn to_forward_slash(s: &str) -> String { s.replace(std::path::MAIN_SEPARATOR, "/") } + fn to_os_main_separator(s: &str) -> String { + s.replace('/', format!("{}", std::path::MAIN_SEPARATOR).as_str()) + } assert_eq!( - sanitize(&prompt.prompt()), + to_forward_slash(&prompt.prompt()), " New file or folder (ends with '/'): " ); - assert_eq!(sanitize(prompt.line()), "test_explorer/new_folder/"); + assert_eq!(to_forward_slash(prompt.line()), "test_explorer/new_folder/"); // 1. Add a new folder at the root - explorer.handle_events(&sanitize("yoyo/")).unwrap(); + explorer + .handle_events(&to_os_main_separator("yoyo/")) + .unwrap(); // 1a. Expect the new folder is added, and is focused assert_eq!( @@ -1027,7 +1032,7 @@ mod test_explorer { // 3. Add a new folder explorer - .handle_events(&sanitize("asus.sass/")) + .handle_events(&to_os_main_separator("asus.sass/")) .unwrap(); // 3a. Expect the new folder is added under "styles", although "styles" is not opened @@ -1050,7 +1055,9 @@ mod test_explorer { assert!(fs::read_dir(path.join("styles/sus.sass")).is_ok()); // 4. Add a new folder with non-existent parents - explorer.handle_events(&sanitize("aa/b/c/")).unwrap(); + explorer + .handle_events(&to_os_main_separator("aa/b/c/")) + .unwrap(); // 4a. Expect the non-existent parents are created, // and the new folder is created, @@ -1078,7 +1085,9 @@ mod test_explorer { explorer.handle_events("j").unwrap(); // 6. Add a new folder here - explorer.handle_events(&sanitize("afoobar/")).unwrap(); + explorer + .handle_events(&to_os_main_separator("afoobar/")) + .unwrap(); // 6a. Expect the folder is added under "styles", // because the folder of the current item, "style.css" is "styles/" -- 2.38.5 From 898c1670d141bf61e3aa1b6e325fd589b71d93bf Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Mar 2023 10:54:22 +0800 Subject: [PATCH 088/191] fix(integration-test/test_goto_file_impl): failing due to untested changes --- helix-term/src/commands.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2d8f6446..eb271199 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1154,20 +1154,10 @@ fn goto_file_impl(cx: &mut Context, action: Action) { } for sel in paths { let p = sel.trim(); - if p.is_empty() { - continue; - } - - let path = &PathBuf::from(p); - if let Err(err) = (|| -> anyhow::Result<()> { - if path.is_file() { - cx.editor.open(path, action)?; - } else { - reveal_file(cx, Some(path.clone())); + if !p.is_empty() { + if let Err(e) = cx.editor.open(&PathBuf::from(p), action) { + cx.editor.set_error(format!("Open file failed: {:?}", e)); } - Ok(()) - })() { - cx.editor.set_error(format!("Open file failed: {:?}", err)); } } } -- 2.38.5 From 33542e9ddbd5776727531eb19d2eda83449a592e Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Mar 2023 11:34:59 +0800 Subject: [PATCH 089/191] refactor: remove unnecessary dev-dependencies - Resolve https://github.com/helix-editor/helix/pull/5768/files#r1126720143 --- Cargo.lock | 124 ---------------------------------- helix-term/Cargo.toml | 2 - helix-term/src/ui/explorer.rs | 41 ++++++----- helix-term/src/ui/tree.rs | 1 - 4 files changed, 25 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cd6a37c..af0858ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,20 +94,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "build-fs-tree" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85199b032e7d08f84570a62dc4b59d4ef37e094939d634e9dddd161515ec3ba9" -dependencies = [ - "derive_more", - "pipe-trait", - "serde", - "serde_yaml", - "text-block-macros", - "thiserror", -] - [[package]] name = "bumpalo" version = "3.11.1" @@ -203,12 +189,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -250,16 +230,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ctor" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "cxx" version = "1.0.82" @@ -304,25 +274,6 @@ dependencies = [ "syn", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "dirs" version = "4.0.0" @@ -1209,7 +1160,6 @@ version = "0.6.0" dependencies = [ "anyhow", "arc-swap", - "build-fs-tree", "chrono", "content_inspector", "crossterm", @@ -1230,7 +1180,6 @@ dependencies = [ "libc", "log", "once_cell", - "pretty_assertions", "pulldown-cmark", "serde", "serde_json", @@ -1627,15 +1576,6 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" -[[package]] -name = "output_vt100" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" -dependencies = [ - "winapi", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -1677,24 +1617,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pipe-trait" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1be1ec9e59f0360aefe84efa6f699198b685ab0d5718081e9f72aa2344289e2" - -[[package]] -name = "pretty_assertions" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" -dependencies = [ - "ctor", - "diff", - "output_vt100", - "yansi", -] - [[package]] name = "proc-macro2" version = "1.0.47" @@ -1810,15 +1732,6 @@ dependencies = [ "str_indices", ] -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.36.7" @@ -1860,12 +1773,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - [[package]] name = "serde" version = "1.0.155" @@ -1917,19 +1824,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha1_smol" version = "1.0.0" @@ -2089,12 +1983,6 @@ dependencies = [ "dirs-next", ] -[[package]] -name = "text-block-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8b59b4da1c1717deaf1de80f0179a9d8b4ac91c986d5fd9f4a8ff177b84049" - [[package]] name = "textwrap" version = "0.16.0" @@ -2337,12 +2225,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unsafe-libyaml" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" - [[package]] name = "url" version = "2.3.1" @@ -2589,9 +2471,3 @@ dependencies = [ "helix-view", "toml", ] - -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 9311d410..5222ddaa 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -77,5 +77,3 @@ helix-loader = { version = "0.6", path = "../helix-loader" } smallvec = "1.10" indoc = "2.0.1" tempfile = "3.4.0" -pretty_assertions = "1.3.0" -build-fs-tree = "0.4.1" diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 5dc53d90..22d41192 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -754,29 +754,38 @@ mod test_explorer { use super::Explorer; use helix_view::graphics::Rect; - use pretty_assertions::assert_eq; use std::{fs, path::PathBuf}; + /// This code should create the following file tree: + /// + /// test_explorer/ + /// ├── index.html + /// ├── .gitignore + /// ├── scripts + /// │ └── main.js + /// └── styles + /// ├── style.css + /// └── public + /// └── file + /// fn dummy_file_tree(name: &str) -> PathBuf { - use build_fs_tree::{dir, file, Build, MergeableFileSystemTree}; - let tree = MergeableFileSystemTree::<&str, &str>::from(dir! { - "index.html" => file!("") - "scripts" => dir! { - "main.js" => file!("") - } - "styles" => dir! { - "style.css" => file!("") - "public" => dir! { - "file" => file!("") - } - } - ".gitignore" => file!("") - }); let path: PathBuf = format!("test_explorer{}{}", std::path::MAIN_SEPARATOR, name).into(); if path.exists() { fs::remove_dir_all(path.clone()).unwrap(); } - tree.build(&path).unwrap(); + fs::create_dir_all(path.clone()).unwrap(); + fs::write(path.join("index.html"), "").unwrap(); + fs::write(path.join(".gitignore"), "").unwrap(); + + fs::create_dir_all(path.join("scripts")).unwrap(); + fs::write(path.join("scripts").join("main.js"), "").unwrap(); + + fs::create_dir_all(path.join("styles")).unwrap(); + fs::write(path.join("styles").join("style.css"), "").unwrap(); + + fs::create_dir_all(path.join("styles").join("public")).unwrap(); + fs::write(path.join("styles").join("public").join("file"), "").unwrap(); + path } diff --git a/helix-term/src/ui/tree.rs b/helix-term/src/ui/tree.rs index 7199570e..c97c21c8 100644 --- a/helix-term/src/ui/tree.rs +++ b/helix-term/src/ui/tree.rs @@ -1218,7 +1218,6 @@ mod test_tree_view { use crate::compositor::Context; use super::{TreeView, TreeViewItem}; - use pretty_assertions::assert_eq; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] /// The children of DivisibleItem is the division of itself. -- 2.38.5 From f5aec54fe2b20305a9288f6fba0bd376d3fa491b Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Mar 2023 11:38:30 +0800 Subject: [PATCH 090/191] chore(commands): revert accidental typo - Resolve https://github.com/helix-editor/helix/pull/5768/files#r1143859919 --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index eb271199..4224f480 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -471,7 +471,7 @@ impl MappableCommand { decrement, "Decrement item under cursor", record_macro, "Record macro", replay_macro, "Replay macro", - command_palette, "Open command pallete", + command_palette, "Open command palette", open_or_focus_explorer, "Open or focus explorer", reveal_current_file, "Reveal current file in explorer", ); -- 2.38.5 From a331e52971b07910cbb92cb56d4817966d37af58 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Mar 2023 11:40:16 +0800 Subject: [PATCH 091/191] chore(keymap): remove "E" - Personally, I never uses this shortcut - Secondly, we are running out of keys for mappings, so I would like to reserve "E" for other more useful mappings --- book/src/keymap.md | 1 - helix-term/src/keymap/default.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 272edbdc..7d5c4c79 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -292,7 +292,6 @@ This layer is a kludge of mappings, mostly pickers. | `/` | Global search in workspace folder | `global_search` | | `?` | Open command palette | `command_palette` | | `e` | Reveal current file in explorer | `reveal_current_file` | -| `E` | Open or focus explorer | `toggle_or_focus_explorer` | > 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index a5f82b93..c8a0aa68 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -275,7 +275,6 @@ pub fn default() -> HashMap { "h" => select_references_to_symbol_under_cursor, "?" => command_palette, "e" => reveal_current_file, - "E" => open_or_focus_explorer, }, "z" => { "View" "z" | "c" => align_view_center, -- 2.38.5 From eebff622de41884a8e76ee8e164b26e866235ed9 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Wed, 22 Mar 2023 11:46:18 +0800 Subject: [PATCH 092/191] chore(doc/configuration/explorer/position): remove `overlay` option --- book/src/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 26311e98..4514fc7e 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -347,4 +347,4 @@ Sets explorer side width and style. | Key | Description | Default | | --- | ----------- | ------- | | `column-width` | explorer side width | 30 | - | `position` | explorer widget position, `overlay`, `right`, or `left` | `left` | + | `position` | explorer widget position, `left` or `right` | `left` | \ No newline at end of file -- 2.38.5 From f37c795c96dc84deec65255eb1cb5c964b343c4d Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Thu, 23 Mar 2023 08:52:13 +0800 Subject: [PATCH 093/191] chore(ui/prompt): use &str instead of Cow - Resolve https://github.com/helix-editor/helix/pull/5768/files#r1140994104 --- helix-term/src/ui/prompt.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 6303375d..3be74ebf 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -94,8 +94,8 @@ impl Prompt { self } - pub fn prompt(&self) -> Cow { - self.prompt.clone() + pub fn prompt(&self) -> &str { + self.prompt.as_ref() } pub fn line(&self) -> &String { -- 2.38.5 From e399bbc379fd40a7310f12173727f7b0fe1217b5 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Thu, 23 Mar 2023 17:36:10 +0100 Subject: [PATCH 094/191] dracula theme: style wrap indicator like whitespace (#6414) --- runtime/themes/dracula.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index 51464824..8bde4708 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -41,6 +41,7 @@ "ui.text.focus" = { fg = "cyan" } "ui.window" = { fg = "foreground" } "ui.virtual.whitespace" = { fg = "subtle" } +"ui.virtual.wrap" = { fg = "subtle" } "ui.virtual.ruler" = { bg = "background_dark"} "error" = { fg = "red" } -- 2.38.5 From 6acd2000284095ac6ec06aaa5c34d843a833800a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20C=2E=20M=C3=BCller?= Date: Thu, 23 Mar 2023 22:26:41 -0400 Subject: [PATCH 095/191] Fix spelling of diagnostics (#6418) --- book/src/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index bf314993..e2dfc89e 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -305,7 +305,7 @@ Example: min-width = 1 ``` -#### `[editor.gutters.diagnotics]` Section +#### `[editor.gutters.diagnostics]` Section Currently unused -- 2.38.5 From 9a651188998bbc1584519ebf658cb5c2c31f4b21 Mon Sep 17 00:00:00 2001 From: Alexis-Lapierre <128792625+Alexis-Lapierre@users.noreply.github.com> Date: Fri, 24 Mar 2023 19:13:01 +0100 Subject: [PATCH 096/191] Recognize .cts and .mts file type as TypeScript (#6424) TypeScript can use three type of file extensions: - .ts for regular TypeScript - .cts for CommonJS modules - .mts for ES modules Official documentation on supported file extensions: https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 2bf9218e..670f3adf 100644 --- a/languages.toml +++ b/languages.toml @@ -453,7 +453,7 @@ includeInlayVariableTypeHints = true name = "typescript" scope = "source.ts" injection-regex = "(ts|typescript)" -file-types = ["ts"] +file-types = ["ts", "mts", "cts"] shebangs = [] roots = [] # TODO: highlights-params -- 2.38.5 From 2f64c768dff1e6b3784d88a5604bd2f1b96b2b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Sat, 25 Mar 2023 14:40:19 +0100 Subject: [PATCH 097/191] feat(languages): highlight .svg as xml (#6431) Add "svg" as a file type for xml. Fixes: https://github.com/helix-editor/helix/issues/6337 --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 670f3adf..a56de1f4 100644 --- a/languages.toml +++ b/languages.toml @@ -2068,7 +2068,7 @@ source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d1 name = "xml" scope = "source.xml" injection-regex = "xml" -file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard"] +file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg"] indent = { tab-width = 2, unit = " " } roots = [] -- 2.38.5 From 851ac6cdd3c5a865d43968ea81d98b5b7c859728 Mon Sep 17 00:00:00 2001 From: Erasin Wang Date: Sun, 26 Mar 2023 00:41:31 +0800 Subject: [PATCH 098/191] Add theme keys for (un)checked markup list items (#6434) --- book/src/themes.md | 2 ++ runtime/queries/markdown/highlights.scm | 3 +++ runtime/themes/onelight.toml | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/book/src/themes.md b/book/src/themes.md index 994542c5..7accb67f 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -228,6 +228,8 @@ We use a similar set of scopes as - `list` - `unnumbered` - `numbered` + - `checked` + - `unchecked` - `bold` - `italic` - `strikethrough` diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm index 80c9f958..72bb68dc 100644 --- a/runtime/queries/markdown/highlights.scm +++ b/runtime/queries/markdown/highlights.scm @@ -39,6 +39,9 @@ (list_marker_parenthesis) ] @markup.list.numbered +(task_list_marker_checked) @markup.list.checked +(task_list_marker_unchecked) @markup.list.unchecked + (thematic_break) @punctuation.special [ diff --git a/runtime/themes/onelight.toml b/runtime/themes/onelight.toml index a9d7a0d3..e35abdb3 100644 --- a/runtime/themes/onelight.toml +++ b/runtime/themes/onelight.toml @@ -80,9 +80,11 @@ "markup.list" = { fg = "light-blue" } "markup.list.unnumbered" = { fg = "light-blue" } "markup.list.numbered" = { fg = "light-blue" } +"markup.list.checked" = { fg = "green" } +"markup.list.unchecked" = { fg = "blue" } "markup.bold" = { fg = "yellow", modifiers = ["bold"] } "markup.italic" = { fg = "purple", modifiers = ["italic"] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.strikethrough" = { fg = "red", modifiers = ["crossed_out"] } "markup.link" = { fg = "light-blue" } "markup.link.url" = { fg = "cyan", modifiers = ["underlined"] } "markup.link.text" = { fg = "light-blue" } -- 2.38.5 From 685ae2365a89346ef276dceb52c3cae40260d1fd Mon Sep 17 00:00:00 2001 From: Tom Burdick Date: Sat, 25 Mar 2023 12:10:54 -0500 Subject: [PATCH 099/191] Add vhdl language support (#5826) Simple highlight query file with keywords and builtin types matching. Many VHDL types however are defined in std libraries which do not currently get matched on. This is because the grammar doesn't consider them builtin types. --- book/src/generated/lang-support.md | 1 + languages.toml | 14 ++ runtime/queries/vhdl/highlights.scm | 338 ++++++++++++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 runtime/queries/vhdl/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 3c18956a..524c2adf 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -145,6 +145,7 @@ | v | ✓ | ✓ | ✓ | `v` | | vala | ✓ | | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | +| vhdl | ✓ | | | `vhdl_ls` | | vhs | ✓ | | | | | vue | ✓ | | | `vls` | | wast | ✓ | | | | diff --git a/languages.toml b/languages.toml index a56de1f4..f6a54e87 100644 --- a/languages.toml +++ b/languages.toml @@ -2369,3 +2369,17 @@ language-server = { command = "cs", args = ["launch", "com.disneystreaming.smith [[grammar]] name = "smithy" source = { git = "https://github.com/indoorvivants/tree-sitter-smithy", rev = "cf8c7eb9faf7c7049839585eac19c94af231e6a0" } + +[[language]] +name = "vhdl" +scope = "source.vhdl" +file-types = ["vhd", "vhdl"] +roots = [] +comment-token = "--" +language-server = { command = "vhdl_ls", args = [] } +indent = { tab-width = 2, unit = " " } +injection-regex = "vhdl" + +[[grammar]] +name = "vhdl" +source = { git = "https://github.com/teburd/tree-sitter-vhdl", rev = "c57313adee2231100db0a7880033f6865deeadb2" } diff --git a/runtime/queries/vhdl/highlights.scm b/runtime/queries/vhdl/highlights.scm new file mode 100644 index 00000000..59cef41c --- /dev/null +++ b/runtime/queries/vhdl/highlights.scm @@ -0,0 +1,338 @@ +(comment) @comment + +; Keywords +[ + ; vhdl 08 + "abs" + "access" + "after" + "alias" + "all" + "and" + "architecture" + "array" + "assert" + "attribute" + "begin" + "block" + "body" + "buffer" + "bus" + "case" + "component" + "configuration" + "constant" + "disconnect" + "downto" + "else" + "elsif" + "end" + "entity" + "exit" + "file" + "for" + "function" + "generic" + "group" + "guarded" + "if" + "impure" + "in" + "inertial" + "inout" + "is" + "label" + "library" + "linkage" + "literal" + "loop" + "map" + "mod" + "nand" + "new" + "next" + "nor" + "not" + "null" + "of" + "on" + "open" + "or" + "others" + "out" + "package" + "port" + "postponed" + "procedure" + "process" + "protected" + "pure" + "range" + "record" + "register" + "reject" + "rem" + "report" + "return" + "rol" + "ror" + "select" + "severity" + "shared" + "signal" + "sla" + "sll" + "sra" + "srl" + "subtype" + "then" + "to" + "transport" + "type" + "unaffected" + "units" + "until" + "use" + "variable" + "wait" + "when" + "while" + "with" + "xnor" + "xor" + ; vhdl 08 + "context" + "force" + "property" + "release" + "sequence" +] @keyword + +[ + ; vhdl 02 + "boolean" + "bit" + "bit_vector" + ;"character" + ;"severity_level" + ;"integer" + ;"real" + ;"time" + ;"natural" + ;"positive" + "string" + ;"line" + ;"text" + ;"side" + ;"unsigned" + ;"signed" + ;"delay_length" + ;"file_open_kind" + ;"file_open_status" + ;"std_logic" + ;"std_logic_vector" + ;"std_ulogic" + ;"std_ulogic_vector" + ; vhdl 08 + ;"boolean_vector" + ;"integer_vector" + ;"real_vector" + ;"time_vector" + ; math types + ;"complex" + ;"complex_polar" + ;"positive_real" + ;"principal_value" +] @type.builtin + +[ + ; vhdl 02 + "base" + "left" + "right" + "high" + "low" + "pos" + "val" + "succ" + "pred" + "leftof" + "rightof" + "range" + "reverse_range" + "length" + "delayed" + "stable" + "quiet" + "transaction" + "event" + "active" + "last_event" + "last_active" + "last_value" + "driving" + "driving_value" + "ascending" + "value" + "image" + "simple_name" + "instance_name" + "path_name" + ;"foreign" + ; vhdl 08 + "instance_name" + "path_name" +] @attribute + +;[ + ; vhdl 02 + ;"now" + ;"resolved" + ;"rising_edge" + ;"falling_edge" + ;"read" + ;"readline" + ;"hread" + ;"oread" + ;"write" + ;"writeline" + ;"hwrite" + ;"owrite" + ;"endfile" + ;"resize" + ;"is_X" + ;"std_match" + ;"shift_left" + ;"shift_right" + ;"rotate_left" + ;"rotate_right" + ;"to_unsigned" + ;"to_signed" + ;"to_integer" + ;"to_stdLogicVector" + ;"to_stdULogic" + ;"to_stdULogicVector" + ;"to_bit" + ;"to_bitVector" + ;"to_X01" + ;"to_X01Z" + ;"to_UX01" + ;"to_01" + ;"conv_unsigned" + ;"conv_signed" + ;"conv_integer" + ;"conv_std_logic_vector" + ;"shl" + ;"shr" + ;"ext" + ;"sxt" + ;"deallocate" + ; vhdl 08 + ;"finish" + ;"flush" + ;"justify" + ;"maximum" + ;"minimum" + ;"resolution_limit" + ;"stop" + ;"swrite" + ;"tee" + ;"to_binarystring" + ;"to_bstring" + ;"to_hexstring" + ;"to_hstring" + ;"to_octalstring" + ;"to_ostring" + ;"to_string" + ; vhdl math + ;"arccos" + ;"arccosh" + ;"arcsin" + ;"arcsinh" + ;"arctan" + ;"arctanh" + ;"arg" + ;"cbrt" + ;"ceil" + ;"cmplx" + ;"complex_to_polar" + ;"conj" + ;"cos" + ;"cosh" + ;"exp" + ;"floor" + ;"get_principal_value" + ;"log" + ;"log10" + ;"log2" + ;"polar_to_complex" + ;"realmax" + ;"realmin" + ;"round" + ;"sign" + ;"sin" + ;"sinh" + ;"sqrt" + ;"tan" + ;"tanh" + ;"trunc" + ;"uniform" +;] @function.builtin + +; Operators +[ + "+" + "-" + "*" + "/" + "**" + "abs" + "not" + "mod" + "rem" + "&" + "sll" + "srl" + "sla" + "sra" + "rol" + "ror" + "=" + "/=" + "?=" + "?/=" + "?<" + "?<=" + "?>" + "?>=" + "<" + "<=" + ">" + ">=" + "and" + "or" + "nand" + "nor" + "xor" + "xnor" + ":=" + "<=" + "??" +] @operator + +[ + ";" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "'" +] @punctuation.bracket + +(full_type_declaration "type" name: (identifier) @type) +(signal_declaration "signal" (identifier_list) @variable) +(variable_declaration "variable" (identifier_list) @variable) +(constant_declaration "constant" (identifier_list) @variable) + -- 2.38.5 From abef92a9b341209aeae8802d30fc8c1f971a43df Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 26 Mar 2023 11:44:07 +0200 Subject: [PATCH 100/191] log failures in the git integration (#6441) --- Cargo.lock | 1 + helix-vcs/Cargo.toml | 1 + helix-vcs/src/git.rs | 55 ++++++++++++++++++++++++--------------- helix-vcs/src/git/test.rs | 12 ++++----- helix-vcs/src/lib.rs | 31 ++++++++++++++++------ 5 files changed, 65 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c86b5aac..0bef317e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,6 +1220,7 @@ dependencies = [ name = "helix-vcs" version = "0.6.0" dependencies = [ + "anyhow", "arc-swap", "gix", "helix-core", diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 5ad6c91b..b32c028b 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -19,6 +19,7 @@ arc-swap = { version = "1.6.0" } gix = { version = "0.41.0", default-features = false , optional = true } imara-diff = "0.1.5" +anyhow = "1" log = "0.4" diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 1732bdd0..00a2c596 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -1,3 +1,4 @@ +use anyhow::{bail, Context, Result}; use arc_swap::ArcSwap; use std::path::Path; use std::sync::Arc; @@ -14,7 +15,7 @@ mod test; pub struct Git; impl Git { - fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option { + fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Result { // custom open options let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); @@ -45,26 +46,31 @@ impl Git { open_options.ceiling_dirs = vec![ceiling_dir.to_owned()]; } - ThreadSafeRepository::discover_with_environment_overrides_opts( + let res = ThreadSafeRepository::discover_with_environment_overrides_opts( path, open_options, git_open_opts_map, - ) - .ok() + )?; + + Ok(res) } } impl DiffProvider for Git { - fn get_diff_base(&self, file: &Path) -> Option> { + fn get_diff_base(&self, file: &Path) -> Result> { debug_assert!(!file.exists() || file.is_file()); debug_assert!(file.is_absolute()); // TODO cache repository lookup - let repo = Git::open_repo(file.parent()?, None)?.to_thread_local(); - let head = repo.head_commit().ok()?; + + let repo_dir = file.parent().context("file has no parent directory")?; + let repo = Git::open_repo(repo_dir, None) + .context("failed to open git repo")? + .to_thread_local(); + let head = repo.head_commit()?; let file_oid = find_file_in_commit(&repo, &head, file)?; - let file_object = repo.find_object(file_oid).ok()?; + let file_object = repo.find_object(file_oid)?; let mut data = file_object.detach().data; // convert LF to CRLF if configured to avoid showing every line as changed if repo @@ -87,35 +93,42 @@ impl DiffProvider for Git { } data = normalized_file } - Some(data) + Ok(data) } - fn get_current_head_name(&self, file: &Path) -> Option>>> { + fn get_current_head_name(&self, file: &Path) -> Result>>> { debug_assert!(!file.exists() || file.is_file()); debug_assert!(file.is_absolute()); - let repo = Git::open_repo(file.parent()?, None)?.to_thread_local(); - let head_ref = repo.head_ref().ok()?; - let head_commit = repo.head_commit().ok()?; + let repo_dir = file.parent().context("file has no parent directory")?; + let repo = Git::open_repo(repo_dir, None) + .context("failed to open git repo")? + .to_thread_local(); + let head_ref = repo.head_ref()?; + let head_commit = repo.head_commit()?; let name = match head_ref { Some(reference) => reference.name().shorten().to_string(), None => head_commit.id.to_hex_with_len(8).to_string(), }; - Some(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) + Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str()))) } } /// Finds the object that contains the contents of a file at a specific commit. -fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Option { - let repo_dir = repo.work_dir()?; - let rel_path = file.strip_prefix(repo_dir).ok()?; - let tree = commit.tree().ok()?; - let tree_entry = tree.lookup_entry_by_path(rel_path).ok()??; +fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Result { + let repo_dir = repo.work_dir().context("repo has no worktree")?; + let rel_path = file.strip_prefix(repo_dir)?; + let tree = commit.tree()?; + let tree_entry = tree + .lookup_entry_by_path(rel_path)? + .context("file is untracked")?; match tree_entry.mode() { // not a file, everything is new, do not show diff - EntryMode::Tree | EntryMode::Commit | EntryMode::Link => None, + mode @ (EntryMode::Tree | EntryMode::Commit | EntryMode::Link) => { + bail!("entry at {} is not a file but a {mode:?}", file.display()) + } // found a file - EntryMode::Blob | EntryMode::BlobExecutable => Some(tree_entry.object_id()), + EntryMode::Blob | EntryMode::BlobExecutable => Ok(tree_entry.object_id()), } } diff --git a/helix-vcs/src/git/test.rs b/helix-vcs/src/git/test.rs index 6b1aba7f..9c67d2c3 100644 --- a/helix-vcs/src/git/test.rs +++ b/helix-vcs/src/git/test.rs @@ -54,7 +54,7 @@ fn missing_file() { let file = temp_git.path().join("file.txt"); File::create(&file).unwrap().write_all(b"foo").unwrap(); - assert_eq!(Git.get_diff_base(&file), None); + assert!(Git.get_diff_base(&file).is_err()); } #[test] @@ -64,7 +64,7 @@ fn unmodified_file() { let contents = b"foo".as_slice(); File::create(&file).unwrap().write_all(contents).unwrap(); create_commit(temp_git.path(), true); - assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents))); + assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); } #[test] @@ -76,7 +76,7 @@ fn modified_file() { create_commit(temp_git.path(), true); File::create(&file).unwrap().write_all(b"bar").unwrap(); - assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents))); + assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); } /// Test that `get_file_head` does not return content for a directory. @@ -95,7 +95,7 @@ fn directory() { std::fs::remove_dir_all(&dir).unwrap(); File::create(&dir).unwrap().write_all(b"bar").unwrap(); - assert_eq!(Git.get_diff_base(&dir), None); + assert!(Git.get_diff_base(&dir).is_err()); } /// Test that `get_file_head` does not return content for a symlink. @@ -116,6 +116,6 @@ fn symlink() { symlink("file.txt", &file_link).unwrap(); create_commit(temp_git.path(), true); - assert_eq!(Git.get_diff_base(&file_link), None); - assert_eq!(Git.get_diff_base(&file), Some(Vec::from(contents))); + assert!(Git.get_diff_base(&file_link).is_err()); + assert_eq!(Git.get_diff_base(&file).unwrap(), Vec::from(contents)); } diff --git a/helix-vcs/src/lib.rs b/helix-vcs/src/lib.rs index 6f5e40d0..4d3a3623 100644 --- a/helix-vcs/src/lib.rs +++ b/helix-vcs/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::{bail, Result}; use arc_swap::ArcSwap; use std::{path::Path, sync::Arc}; @@ -18,19 +19,19 @@ pub trait DiffProvider { /// if this provider is used. /// The data is returned as raw byte without any decoding or encoding performed /// to ensure all file encodings are handled correctly. - fn get_diff_base(&self, file: &Path) -> Option>; - fn get_current_head_name(&self, file: &Path) -> Option>>>; + fn get_diff_base(&self, file: &Path) -> Result>; + fn get_current_head_name(&self, file: &Path) -> Result>>>; } #[doc(hidden)] pub struct Dummy; impl DiffProvider for Dummy { - fn get_diff_base(&self, _file: &Path) -> Option> { - None + fn get_diff_base(&self, _file: &Path) -> Result> { + bail!("helix was compiled without git support") } - fn get_current_head_name(&self, _file: &Path) -> Option>>> { - None + fn get_current_head_name(&self, _file: &Path) -> Result>>> { + bail!("helix was compiled without git support") } } @@ -42,13 +43,27 @@ impl DiffProviderRegistry { pub fn get_diff_base(&self, file: &Path) -> Option> { self.providers .iter() - .find_map(|provider| provider.get_diff_base(file)) + .find_map(|provider| match provider.get_diff_base(file) { + Ok(res) => Some(res), + Err(err) => { + log::error!("{err:#?}"); + log::error!("failed to open diff base for {}", file.display()); + None + } + }) } pub fn get_current_head_name(&self, file: &Path) -> Option>>> { self.providers .iter() - .find_map(|provider| provider.get_current_head_name(file)) + .find_map(|provider| match provider.get_current_head_name(file) { + Ok(res) => Some(res), + Err(err) => { + log::error!("{err:#?}"); + log::error!("failed to obtain current head name for {}", file.display()); + None + } + }) } } -- 2.38.5 From 5d7c90c5cfd6887fc0a43ee695d91f0166955a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20C=2E=20M=C3=BCller?= Date: Sun, 26 Mar 2023 17:49:37 -0400 Subject: [PATCH 101/191] Add language support for rego (OpenPolicyAgent) (#6415) --- book/src/generated/lang-support.md | 1 + languages.toml | 15 +++++++ runtime/queries/rego/highlights.scm | 68 +++++++++++++++++++++++++++++ runtime/queries/rego/injections.scm | 2 + 4 files changed, 86 insertions(+) create mode 100644 runtime/queries/rego/highlights.scm create mode 100644 runtime/queries/rego/injections.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 524c2adf..996bdb43 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -112,6 +112,7 @@ | r | ✓ | | | `R` | | racket | ✓ | | | `racket` | | regex | ✓ | | | | +| rego | ✓ | | | `regols` | | rescript | ✓ | ✓ | | `rescript-language-server` | | rmarkdown | ✓ | | ✓ | `R` | | ron | ✓ | | ✓ | | diff --git a/languages.toml b/languages.toml index f6a54e87..fe94d699 100644 --- a/languages.toml +++ b/languages.toml @@ -2383,3 +2383,18 @@ injection-regex = "vhdl" [[grammar]] name = "vhdl" source = { git = "https://github.com/teburd/tree-sitter-vhdl", rev = "c57313adee2231100db0a7880033f6865deeadb2" } + +[[language]] +name = "rego" +roots = [] +scope = "source.rego" +injection-regex = "rego" +file-types = ["rego"] +auto-format = true +comment-token = "#" +language-server = { command = "regols" } +grammar = "rego" + +[[grammar]] +name = "rego" +source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "b2667c975f07b33be3ceb83bea5cfbad88095866" } diff --git a/runtime/queries/rego/highlights.scm b/runtime/queries/rego/highlights.scm new file mode 100644 index 00000000..0ff6de55 --- /dev/null +++ b/runtime/queries/rego/highlights.scm @@ -0,0 +1,68 @@ +[ + (import) +] @keyword.control.import + +[ + (package) +] @namespace + +[ + (with) + (as) + (every) + (some) + (in) + (default) + "null" +] @keyword.control + +[ + (not) + (if) + (contains) + (else) +] @keyword.control.conditional + +[ + (boolean) +] @constant.builtin.boolean + +[ + (assignment_operator) + (bool_operator) + (arith_operator) + (bin_operator) +] @operator + +[ + (string) + (raw_string) +] @string + +(term (ref (var))) @variable + +(comment) @comment.line + +(number) @constant.numeric.integer + +(expr_call func_name: (fn_name (var) @function .)) + +(expr_call func_arguments: (fn_args (expr) @variable.parameter)) + +(rule_args (term) @variable.parameter) + +[ + (open_paren) + (close_paren) + (open_bracket) + (close_bracket) + (open_curly) + (close_curly) +] @punctuation.bracket + +(rule (rule_head (var) @function.method)) + +(rule + (rule_head (term (ref (var) @namespace))) + (rule_body (query (literal (expr (expr_infix (expr (term (ref (var)) @_output)))))) (#eq? @_output @namespace)) +) diff --git a/runtime/queries/rego/injections.scm b/runtime/queries/rego/injections.scm new file mode 100644 index 00000000..321c90ad --- /dev/null +++ b/runtime/queries/rego/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) -- 2.38.5 From c8fde8b6f99b07faca7e6f93162d887ee132e0f6 Mon Sep 17 00:00:00 2001 From: JJ Date: Sun, 26 Mar 2023 15:06:48 -0700 Subject: [PATCH 102/191] Initial Nim language support (#6123) --- book/src/generated/lang-support.md | 1 + languages.toml | 23 ++ runtime/queries/nim/highlights.scm | 315 ++++++++++++++++++++++++++++ runtime/queries/nim/indents.scm | 54 +++++ runtime/queries/nim/textobjects.scm | 19 ++ 5 files changed, 412 insertions(+) create mode 100644 runtime/queries/nim/highlights.scm create mode 100644 runtime/queries/nim/indents.scm create mode 100644 runtime/queries/nim/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 996bdb43..5cd0c8c1 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -88,6 +88,7 @@ | msbuild | ✓ | | ✓ | | | nasm | ✓ | ✓ | | | | nickel | ✓ | | ✓ | `nls` | +| nim | ✓ | ✓ | ✓ | `nimlangserver` | | nix | ✓ | | | `nil` | | nu | ✓ | | | | | ocaml | ✓ | | ✓ | `ocamllsp` | diff --git a/languages.toml b/languages.toml index fe94d699..ae4135f0 100644 --- a/languages.toml +++ b/languages.toml @@ -2398,3 +2398,26 @@ grammar = "rego" [[grammar]] name = "rego" source = { git = "https://github.com/FallenAngel97/tree-sitter-rego", rev = "b2667c975f07b33be3ceb83bea5cfbad88095866" } + +[[language]] +name = "nim" +scope = "source.nim" +injection-regex = "nim" +file-types = ["nim", "nims", "nimble"] +shebangs = [] +roots = [] +comment-token = "#" +indent = { tab-width = 2, unit = " " } +language-server = { command = "nimlangserver" } + +[language.auto-pairs] +'(' = ')' +'[' = ']' +'"' = '"' +"'" = "'" +'{' = '}' + +# Nim's tree-sitter grammar is in heavy development. +[[grammar]] +name = "nim" +source = { git = "https://github.com/aMOPel/tree-sitter-nim", rev = "240239b232550e431d67de250d1b5856209e7f06" } diff --git a/runtime/queries/nim/highlights.scm b/runtime/queries/nim/highlights.scm new file mode 100644 index 00000000..1d325685 --- /dev/null +++ b/runtime/queries/nim/highlights.scm @@ -0,0 +1,315 @@ +;; Constants, Comments, and Literals + +(comment) @comment.line +(multilineComment) @comment.block +(docComment) @comment.block.documentation +(multilineDocComment) @comment.block.documentation +; comments + +[(literal) (generalizedLit)] @constant +[(nil_lit)] @constant.builtin +[(bool_lit)] @constant.builtin.boolean +[(char_lit)] @constant.character +[(char_esc_seq) (str_esc_seq)] @constant.character.escape +[(custom_numeric_lit)] @constant.numeric +[(int_lit) (int_suffix)] @constant.numeric.integer +[(float_lit) (float_suffix)] @constant.numeric.float +; literals +; note: somewhat irritatingly for testing, lits have the same syntax highlighting as types + +[ + (str_lit) + (triplestr_lit) + (rstr_lit) + (generalized_str_lit) + (generalized_triplestr_lit) + (interpolated_str_lit) + (interpolated_triplestr_lit) +] @string +; [] @string.regexp +; string literals + +[ + "." + "," + ";" + ":" +] @punctuation.delimiter +[ + "(" + ")" + "[" + "]" + "{" + "}" + "{." + ".}" + "#[" + "]#" +] @punctuation.bracket +(interpolated_str_lit "&" @punctuation.special) +(interpolated_str_lit "{" @punctuation.special) +(interpolated_str_lit "}" @punctuation.special) +; punctuation + +[ + "and" + "or" + "xor" + "not" + "in" + "notin" + "is" + "isnot" + "div" + "mod" + "shl" + "shr" +] @keyword.operator +; operators: we list them explicitly to deliminate them from symbolic operators + +[(operator) (opr) "="] @operator +; all operators (must come after @keyword.operator) + +(pragma) @attribute +; pragmas + + +;; Imports and Exports + +(importStmt + (keyw) @keyword.control.import + (expr (primary (symbol) @namespace))? + (expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?) +(exportStmt + (keyw) @keyword.control.import + (expr (primary (symbol) @namespace))? + (expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?) +(fromStmt + (keyw) @keyword.control.import + (expr (primary (symbol) @namespace))? + (expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?) +(includeStmt + (keyw) @keyword.control.import + (expr (primary (symbol) @namespace))? + (expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?) +(importExceptStmt + (keyw) @keyword.control.import + (expr (primary (symbol) @namespace))? + (expr (primary (arrayConstr (exprColonExprList (exprColonExpr (expr (primary (symbol) @namespace)))))))?) +; import statements +; yeah, this is a bit gross. + + +;; Control Flow + +(ifStmt (keyw) @keyword.control.conditional) +(whenStmt (keyw) @keyword.control.conditional) +(elifStmt (keyw) @keyword.control.conditional) +(elseStmt (keyw) @keyword.control.conditional) +(caseStmt (keyw) @keyword.control.conditional) +(ofBranch (keyw) @keyword.control.conditional) +(inlineIfStmt (keyw) @keyword.control.conditional) +(inlineWhenStmt (keyw) @keyword.control.conditional) +; conditional statements +; todo: do block + +(forStmt + . (keyw) @keyword.control.repeat + . (symbol) @variable + . (keyw) @keyword.control.repeat) +(whileStmt (keyw) @keyword.control.repeat) +; loop statements + +(returnStmt (keyw) @keyword.control.repeat) +(yieldStmt (keyw) @keyword.control.repeat) +(discardStmt (keyw) @keyword.control.repeat) +(breakStmt (keyw) @keyword.control.repeat) +(continueStmt (keyw) @keyword.control.repeat) +; control flow statements + +(raiseStmt (keyw) @keyword.control.exception) +(tryStmt (keyw) @keyword.control.exception) +(tryExceptStmt (keyw) @keyword.control.exception) +(tryFinallyStmt (keyw) @keyword.control.exception) +(inlineTryStmt (keyw) @keyword.control.exception) +; (inlineTryExceptStmt (keyw) @keyword.control.exception) +; (inlineTryFinallyStmt (keyw) @keyword.control.exception) +; exception handling statements + +(staticStmt (keyw) @keyword) +(deferStmt (keyw) @keyword) +(asmStmt (keyw) @keyword) +(bindStmt (keyw) @keyword) +(mixinStmt (keyw) @keyword) +; miscellaneous blocks + +(blockStmt + (keyw) @keyword.control + (symbol) @label) +; block statements + + +;; Types and Type Declarations + +(typeDef + (keyw) @keyword.storage.type + (symbol) @type) +; names of new types type declarations + +(exprColonEqExpr + . (expr (primary (symbol) @variable)) + . (expr (primary (symbol) @type))) +; variables in inline tuple declarations + +(primarySuffix + (indexSuffix + (exprColonEqExprList + (exprColonEqExpr + (expr + (primary + (symbol) @type)))))) +; nested types in brackets, i.e. seq[string] + +(primaryTypeDef (symbol) @type) +; primary types of type declarations (NOT nested types) + +(primaryTypeDef (primaryPrefix (keyw) @type)) +; for consistency + +(primaryTypeDesc (symbol) @type) +; type annotations, on declarations or in objects + +(primaryTypeDesc (primaryPrefix (keyw) @type)) +; var types etc + +(genericParamList (genericParam (symbol) @type)) +; types in generic blocks + +(enumDecl (keyw) @keyword.storage.type) +(enumElement (symbol) @type.enum.variant) +; enum declarations and elements + +(tupleDecl (keyw) @keyword.storage.type) +; tuple declarations + +(objectDecl (keyw) @keyword.storage.type) +(objectPart (symbol) @variable.other.member) +; object declarations and fields + +(objectCase + (keyw) @keyword.control.conditional + (symbol) @variable.other.member) +(objectBranch (keyw) @keyword.control.conditional) +(objectElif (keyw) @keyword.control.conditional) +(objectElse (keyw) @keyword.control.conditional) +(objectWhen (keyw) @keyword.control.conditional) +; variant objects + +(conceptDecl (keyw) @keyword.storage.type) +(conceptParam (keyw) @type) +(conceptParam (symbol) @variable) +; concept declarations, parameters, and qualifiers on those parameters + +((expr + (primary (symbol)) + (operator) @operator + (primary (symbol) @type)) + (#match? @operator "is")) +((exprStmt + (primary (symbol)) + (operator) @operator + (primary (symbol) @type)) + (#match? @operator "is")) +; symbols likely to be types: "x is t" means t is either a type or a type variable + +; distinct? + + +;; Functions + +(routine + . (keyw) @keyword.function + . (symbol) @function) +; function declarations + +(routineExpr (keyw) @keyword.function) +; discarded function + +(routineExprTypeDesc (keyw) @keyword.function) +; function declarations as types + +(primary + . (symbol) @function.call + . (primarySuffix (functionCall))) +; regular function calls + +(primary + . (symbol) @function.call + . (primarySuffix (cmdCall))) +; function calls without parenthesis + +(primary + (primarySuffix (qualifiedSuffix (symbol) @function.call)) + . (primarySuffix (functionCall))) +; uniform function call syntax calls + +(primary + (primarySuffix (qualifiedSuffix (symbol) @function.call)) + . (primarySuffix (cmdCall))) +; just in case + +(primary + (symbol) @constructor + (primarySuffix (objectConstr))) +; object constructor + +; does not appear to be a way to distinguish these without verbatium matching +; [] @function.builtin +; [] @function.method +; [] @function.macro +; [] @function.special + + +;; Variables + +(paramList (paramColonEquals (symbol) @variable.parameter)) +; parameter identifiers + +(identColon (ident) @variable.other.member) +; named parts of tuples + +(symbolColonExpr (symbol) @variable) +; object constructor parameters + +(symbolEqExpr (symbol) @variable) +; named parameters + +(variable + (keyw) @keyword.storage.type + (declColonEquals (symbol) @variable)) +; let, var, const expressions + +((primary (symbol) @variable.builtin) + (#match? @variable.builtin "result")) +; `result` is an implicit builtin variable inside function scopes + +((primary (symbol) @type) + (#match? @type "^[A-Z]")) +; assume PascalCase identifiers to be types + +((primary + (primarySuffix + (qualifiedSuffix + (symbol) @type))) + (#match? @type "^[A-Z]")) +; assume PascalCase member variables to be enum entries + +(primary (symbol) @variable) +; overzealous, matches variables + +(primary (primarySuffix (qualifiedSuffix (symbol) @variable.other.member))) +; overzealous, matches member variables: i.e. x in foo.x + +(keyw) @keyword +; more specific matches are done above whenever possible diff --git a/runtime/queries/nim/indents.scm b/runtime/queries/nim/indents.scm new file mode 100644 index 00000000..67743540 --- /dev/null +++ b/runtime/queries/nim/indents.scm @@ -0,0 +1,54 @@ +[ + (typeDef) + (ifStmt) + (whenStmt) + (elifStmt) + (elseStmt) + (ofBranch) ; note: not caseStmt + (whileStmt) + (tryStmt) + (tryExceptStmt) + (tryFinallyStmt) + (forStmt) + (blockStmt) + (staticStmt) + (deferStmt) + (asmStmt) + ; exprStmt? +] @indent +;; increase the indentation level + +[ + (ifStmt) + (whenStmt) + (elifStmt) + (elseStmt) + (ofBranch) ; note: not caseStmt + (whileStmt) + (tryStmt) + (tryExceptStmt) + (tryFinallyStmt) + (forStmt) + (blockStmt) + (staticStmt) + (deferStmt) + (asmStmt) + ; exprStmt? +] @extend +;; ??? + +[ + (returnStmt) + (raiseStmt) + (yieldStmt) + (breakStmt) + (continueStmt) +] @extend.prevent-once +;; end a level of indentation while staying indented + +[ + ")" ; tuples + "]" ; arrays, seqs + "}" ; sets +] @outdent +;; end a level of indentation and unindent the line diff --git a/runtime/queries/nim/textobjects.scm b/runtime/queries/nim/textobjects.scm new file mode 100644 index 00000000..943aa7f0 --- /dev/null +++ b/runtime/queries/nim/textobjects.scm @@ -0,0 +1,19 @@ +(routine + (block) @function.inside) @function.around + +; @class.inside (types?) +; @class.around + +; paramListSuffix is strange and i do not understand it +(paramList + (paramColonEquals) @parameter.inside) @parameter.around + +(comment) @comment.inside +(multilineComment) @comment.inside +(docComment) @comment.inside +(multilineDocComment) @comment.inside + +(comment)+ @comment.around +(multilineComment) @comment.around +(docComment)+ @comment.around +(multilineDocComment) @comment.around -- 2.38.5 From cf9669f276dc223323d526d3dcf7c1bbc67a3922 Mon Sep 17 00:00:00 2001 From: wongjiahau Date: Mon, 27 Mar 2023 08:54:03 +0800 Subject: [PATCH 103/191] fix(ci): clippy error --- helix-term/src/ui/explorer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 22d41192..76c43751 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -1010,7 +1010,7 @@ mod test_explorer { s.replace('/', format!("{}", std::path::MAIN_SEPARATOR).as_str()) } assert_eq!( - to_forward_slash(&prompt.prompt()), + to_forward_slash(prompt.prompt()), " New file or folder (ends with '/'): " ); assert_eq!(to_forward_slash(prompt.line()), "test_explorer/new_folder/"); -- 2.38.5 From 0ab96cc2576cf8d78d54bcc42e0e7f5285321030 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 22 Mar 2023 18:41:56 +0100 Subject: [PATCH 104/191] remove incorrect assert This assert was added during early development of #5420 and makes no sense with the current code. We simply forgot to remove it. --- helix-term/src/ui/document.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index d4176264..39c20950 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -175,7 +175,6 @@ pub fn render_text<'t>( text_annotations, ); row_off += offset.vertical_offset; - assert_eq!(0, offset.vertical_offset); let (mut formatter, mut first_visible_char_idx) = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, offset.anchor); -- 2.38.5 From 72b93116784ec944f49c7f6a335b0aa663f1430e Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 6 Mar 2023 16:25:41 +0100 Subject: [PATCH 105/191] fix view anchors not at start of a visual line The top of a view is marked by a char idx anchor. That char idx is usually the first character of the visual line it's on. We use a char index instead of a line index because the view may start in the middle of a line with soft wrapping. However, it's possible to temporarily endup in a state where this anchor is not the first character of the first visual line. This is pretty rare because edits usually happen inside/after the view. In most cases we handle this case correctly. However, if the cursor is before the anchor (but still in view) there can be crashes or visual artifacts. This is caused by the fact that visual_offset_from_anchor (and the positioning code in view.rs) incorrectly assumed that the (cursor) position is always after the view anchor if the cursor is in view. But if the anchor is not the first character of the first visual line this is not the case anymore. In that case crashes and visual artifacts are possible. This commit fixes that problem by changing `visual_offset_from_anchor` (and callsites) to properly consider that case. --- helix-core/src/lib.rs | 2 +- helix-core/src/position.rs | 40 ++++++++++++++++---- helix-view/src/view.rs | 76 +++++++++++++++++++------------------- 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index e3f862a6..4d50e48b 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -98,7 +98,7 @@ pub use {regex, tree_sitter}; pub use graphemes::RopeGraphemes; pub use position::{ char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, - visual_offset_from_block, Position, + visual_offset_from_block, Position, VisualOffsetError, }; #[allow(deprecated)] pub use position::{pos_at_visual_coords, visual_coords_at_pos}; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 7b8dc326..c3233a34 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -137,6 +137,12 @@ pub fn visual_offset_from_block( (last_pos, block_start) } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum VisualOffsetError { + PosBeforeAnchorRow, + PosAfterMaxRow, +} + /// Returns the visual offset from the start of the visual line /// that contains anchor. pub fn visual_offset_from_anchor( @@ -146,28 +152,46 @@ pub fn visual_offset_from_anchor( text_fmt: &TextFormat, annotations: &TextAnnotations, max_rows: usize, -) -> Option<(Position, usize)> { +) -> Result<(Position, usize), VisualOffsetError> { let (formatter, block_start) = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); let mut char_pos = block_start; let mut anchor_line = None; + let mut found_pos = None; let mut last_pos = Position::default(); + if pos < block_start { + return Err(VisualOffsetError::PosBeforeAnchorRow); + } + for (grapheme, vpos) in formatter { last_pos = vpos; char_pos += grapheme.doc_chars(); - if char_pos > anchor && anchor_line.is_none() { - anchor_line = Some(last_pos.row); - } if char_pos > pos { - last_pos.row -= anchor_line.unwrap(); - return Some((last_pos, block_start)); + if let Some(anchor_line) = anchor_line { + last_pos.row -= anchor_line; + return Ok((last_pos, block_start)); + } else { + found_pos = Some(last_pos); + } + } + if char_pos > anchor && anchor_line.is_none() { + if let Some(mut found_pos) = found_pos { + return if found_pos.row == last_pos.row { + found_pos.row = 0; + Ok((found_pos, block_start)) + } else { + Err(VisualOffsetError::PosBeforeAnchorRow) + }; + } else { + anchor_line = Some(last_pos.row); + } } if let Some(anchor_line) = anchor_line { if vpos.row >= anchor_line + max_rows { - return None; + return Err(VisualOffsetError::PosAfterMaxRow); } } } @@ -175,7 +199,7 @@ pub fn visual_offset_from_anchor( let anchor_line = anchor_line.unwrap_or(last_pos.row); last_pos.row -= anchor_line; - Some((last_pos, block_start)) + Ok((last_pos, block_start)) } /// Convert (line, column) coordinates to a character index. diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 0ac7ca3b..ee6fc127 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -7,9 +7,13 @@ use crate::{ }; use helix_core::{ - char_idx_at_visual_offset, doc_formatter::TextFormat, syntax::Highlight, - text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block, - Position, RopeSlice, Selection, Transaction, + char_idx_at_visual_offset, + doc_formatter::TextFormat, + syntax::Highlight, + text_annotations::TextAnnotations, + visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection, + Transaction, + VisualOffsetError::{PosAfterMaxRow, PosBeforeAnchorRow}, }; use std::{ @@ -213,46 +217,38 @@ impl View { // - 1 so we have at least one gap in the middle. // a height of 6 with padding of 3 on each side will keep shifting the view back and forth // as we type - let scrolloff = scrolloff.min(viewport.height.saturating_sub(1) as usize / 2); + let scrolloff = if CENTERING { + 0 + } else { + scrolloff.min(viewport.height.saturating_sub(1) as usize / 2) + }; let cursor = doc.selection(self.id).primary().cursor(doc_text); let mut offset = self.offset; + let off = visual_offset_from_anchor( + doc_text, + offset.anchor, + cursor, + &text_fmt, + &annotations, + vertical_viewport_end, + ); - let (visual_off, mut at_top) = if cursor >= offset.anchor { - let off = visual_offset_from_anchor( - doc_text, - offset.anchor, - cursor, - &text_fmt, - &annotations, - vertical_viewport_end, - ); - (off, false) - } else if CENTERING { - // cursor out of view - return None; - } else { - (None, true) - }; - - let new_anchor = match visual_off { - Some((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { - if CENTERING && visual_pos.row < offset.vertical_offset { + let (new_anchor, at_top) = match off { + Ok((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { + if CENTERING { // cursor out of view return None; } - at_top = true; - true + (true, true) } - Some((visual_pos, _)) if visual_pos.row + scrolloff + 1 >= vertical_viewport_end => { - if CENTERING && visual_pos.row >= vertical_viewport_end { - // cursor out of view - return None; - } - true + Ok((visual_pos, _)) if visual_pos.row + scrolloff >= vertical_viewport_end => { + (true, false) } - Some(_) => false, - None => true, + Ok((_, _)) => (false, false), + Err(_) if CENTERING => return None, + Err(PosBeforeAnchorRow) => (true, true), + Err(PosAfterMaxRow) => (true, false), }; if new_anchor { @@ -269,8 +265,8 @@ impl View { offset.horizontal_offset = 0; } else { // determine the current visual column of the text - let col = visual_off - .unwrap_or_else(|| { + let col = off + .unwrap_or_else(|_| { visual_offset_from_block( doc_text, offset.anchor, @@ -360,8 +356,9 @@ impl View { ); match pos { - Some((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), - None => visual_height.saturating_sub(1), + Ok((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), + Err(PosAfterMaxRow) => visual_height.saturating_sub(1), + Err(PosBeforeAnchorRow) => 0, } } @@ -390,7 +387,8 @@ impl View { &text_fmt, &annotations, viewport.height as usize, - )? + ) + .ok()? .0; if pos.row < self.offset.vertical_offset { return None; -- 2.38.5 From 7cf448eb5bfd604e1342a1d0f5685e3231f6c1a9 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 22 Mar 2023 18:38:32 +0100 Subject: [PATCH 106/191] use partition_point instead of binary_search_by Using `partition_point` ensures we always find the first entry. With binary search it is "random" (deterministic but implementation specific) which index is retruned if there are multiple equal elements. `partition_point` was added to the standard library to cover extactly the usecase here. --- helix-core/src/text_annotations.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index 3e48de4d..e6093184 100644 --- a/helix-core/src/text_annotations.rs +++ b/helix-core/src/text_annotations.rs @@ -1,5 +1,4 @@ use std::cell::Cell; -use std::convert::identity; use std::ops::Range; use std::rc::Rc; @@ -113,9 +112,7 @@ impl Layer { pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) { let new_index = self .annotations - .binary_search_by_key(&char_idx, get_char_idx) - .unwrap_or_else(identity); - + .partition_point(|annot| get_char_idx(annot) < char_idx); self.current_index.set(new_index); } -- 2.38.5 From d6c8e0c946d768abdb2d688cb7cd67683ac51240 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 22 Mar 2023 16:30:51 +0100 Subject: [PATCH 107/191] allow scrolling past virtual text line Virtual text lines (either caused by softwrapped inlay hints that take multiple or line annotations) currently block scrolling downwards. if the visual offset passed to char_idx_at_visual_offset or visual_offset_from_block is within a virtual text line then the char position before the virtual text and a visual offset are returned. We previously ignored that visual offset and as a result the cursor would be stuck at the start of the virtual text. This commit fixes that by simply moving the cursor to the next char (so past the virtual text) if this visual offset is non-zero --- helix-term/src/commands.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1c1edece..d53df831 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1489,18 +1489,19 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { &annotations, ); - let head; + let mut head; match direction { Forward => { - head = char_idx_at_visual_offset( + let off; + (head, off) = char_idx_at_visual_offset( doc_text, view.offset.anchor, (view.offset.vertical_offset + scrolloff) as isize, 0, &text_fmt, &annotations, - ) - .0; + ); + head += (off != 0) as usize; if head <= cursor { return; } -- 2.38.5 From 15e751b9a291b8732468235af95142bfbd0c9be2 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 22 Mar 2023 16:45:22 +0100 Subject: [PATCH 108/191] make scrolloff calculation consistent While scrolling (with the `scroll`) command scrolloff was calculated slightly differently than in `ensure_cursor_in_view` which could cause the cursor to get stuck while scrolling --- helix-term/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d53df831..4a7b7883 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1470,7 +1470,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { let cursor = range.cursor(text); let height = view.inner_height(); - let scrolloff = config.scrolloff.min(height / 2); + let scrolloff = config.scrolloff.min(height.saturating_sub(1) as usize / 2); let offset = match direction { Forward => offset as isize, Backward => -(offset as isize), @@ -1510,7 +1510,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { head = char_idx_at_visual_offset( doc_text, view.offset.anchor, - (view.offset.vertical_offset + height - scrolloff) as isize, + (view.offset.vertical_offset + height - scrolloff - 1) as isize, 0, &text_fmt, &annotations, -- 2.38.5 From 9fac574178bb6b66675ffc72819a79dee25112df Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 6 Mar 2023 16:34:54 +0100 Subject: [PATCH 109/191] do not ignore mouse scrolling when on top of virtual text --- helix-term/src/ui/editor.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 0b6ab046..4cac0fa8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1038,10 +1038,15 @@ impl EditorView { .. } = *event; - let pos_and_view = |editor: &Editor, row, column| { + let pos_and_view = |editor: &Editor, row, column, ignore_virtual_text| { editor.tree.views().find_map(|(view, _focus)| { - view.pos_at_screen_coords(&editor.documents[&view.doc], row, column, true) - .map(|pos| (pos, view.id)) + view.pos_at_screen_coords( + &editor.documents[&view.doc], + row, + column, + ignore_virtual_text, + ) + .map(|pos| (pos, view.id)) }) }; @@ -1056,7 +1061,7 @@ impl EditorView { MouseEventKind::Down(MouseButton::Left) => { let editor = &mut cxt.editor; - if let Some((pos, view_id)) = pos_and_view(editor, row, column) { + if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { let doc = doc_mut!(editor, &view!(editor, view_id).doc); if modifiers == KeyModifiers::ALT { @@ -1120,7 +1125,7 @@ impl EditorView { _ => unreachable!(), }; - match pos_and_view(cxt.editor, row, column) { + match pos_and_view(cxt.editor, row, column, false) { Some((_, view_id)) => cxt.editor.tree.focus = view_id, None => return EventResult::Ignored(None), } @@ -1191,7 +1196,7 @@ impl EditorView { return EventResult::Consumed(None); } - if let Some((pos, view_id)) = pos_and_view(editor, row, column) { + if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { let doc = doc_mut!(editor, &view!(editor, view_id).doc); doc.set_selection(view_id, Selection::point(pos)); cxt.editor.focus(view_id); -- 2.38.5 From 2af14a24abbd8de510f69e6569c18601533dc912 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 22 Mar 2023 16:23:29 +0100 Subject: [PATCH 110/191] respect line annotations in char_idx_at_visual_row_offset char_idx_at_visual_row_offset asssumed that a single line/block break always corresponded to a vertical offset of 1. However conceal can hide the line break (in which case the certical offset would be 0) and line annotations (or softwrapped inlay hints at the end of the line) can insert addtional vertical lines. To correctly account for these cases we simply compute the visual offset of the start of the next block from the previous block instead of the visual offset of the block end. This means that the line breaks at the end of the block (however many there may be) are automatically included and we don't need to manually add 1 to the `row_offset` anymore. --- helix-core/src/position.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index c3233a34..3902b4d4 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -317,10 +317,11 @@ pub fn char_idx_at_visual_offset<'a>( text_fmt: &TextFormat, annotations: &TextAnnotations, ) -> (usize, usize) { + let mut pos = anchor; // convert row relative to visual line containing anchor to row relative to a block containing anchor (anchor may change) loop { let (visual_pos_in_block, block_char_offset) = - visual_offset_from_block(text, anchor, anchor, text_fmt, annotations); + visual_offset_from_block(text, anchor, pos, text_fmt, annotations); row_offset += visual_pos_in_block.row as isize; anchor = block_char_offset; if row_offset >= 0 { @@ -332,10 +333,10 @@ pub fn char_idx_at_visual_offset<'a>( break; } // the row_offset is negative so we need to look at the previous block - // set the anchor to the last char before the current block - // this char index is also always a line earlier so increase the row_offset by 1 + // set the anchor to the last char before the current block so that we can compute + // the distance of this block from the start of the previous block + pos = anchor; anchor -= 1; - row_offset += 1; } char_idx_at_visual_block_offset( -- 2.38.5 From d7431db55cd076dbacfde2cebaed315509923df5 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 27 Mar 2023 01:27:38 -0500 Subject: [PATCH 111/191] Update tree-sitter-git-commit, add comment textobject (#6439) The update includes a fix for comments in commit messages where there was no space separating the '#' and the comment text. The comment textobject can be useful occasionally to jump to the summary part of the commit edit message. --- book/src/generated/lang-support.md | 2 +- languages.toml | 2 +- runtime/queries/git-commit/textobjects.scm | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 runtime/queries/git-commit/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 5cd0c8c1..003ed4a4 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -41,7 +41,7 @@ | fortran | ✓ | | ✓ | `fortls` | | gdscript | ✓ | ✓ | ✓ | | | git-attributes | ✓ | | | | -| git-commit | ✓ | | | | +| git-commit | ✓ | ✓ | | | | git-config | ✓ | | | | | git-ignore | ✓ | | | | | git-rebase | ✓ | | | | diff --git a/languages.toml b/languages.toml index ae4135f0..7c6c278e 100644 --- a/languages.toml +++ b/languages.toml @@ -1194,7 +1194,7 @@ text-width = 72 [[grammar]] name = "git-commit" -source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "318dd72abfaa7b8044c1d1fbeabcd06deaaf038f" } +source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "7421fd81840950c0ff4191733cee3b6ac06cb295" } [[language]] name = "diff" diff --git a/runtime/queries/git-commit/textobjects.scm b/runtime/queries/git-commit/textobjects.scm new file mode 100644 index 00000000..4465c876 --- /dev/null +++ b/runtime/queries/git-commit/textobjects.scm @@ -0,0 +1,2 @@ +(comment) @comment.inside +(comment)+ @comment.around -- 2.38.5 From 5323020c3f02b178f2b6807f13d89bf7f40d3cce Mon Sep 17 00:00:00 2001 From: Philip Giuliani Date: Mon, 27 Mar 2023 19:24:17 +0200 Subject: [PATCH 112/191] Add .arb as a supported extension with json highlighting (#6452) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 7c6c278e..0a0e29ba 100644 --- a/languages.toml +++ b/languages.toml @@ -160,7 +160,7 @@ indent = { tab-width = 2, unit = " " } name = "json" scope = "source.json" injection-regex = "json" -file-types = ["json", "jsonc"] +file-types = ["json", "jsonc", "arb"] roots = [] language-server = { command = "vscode-json-language-server", args = ["--stdio"] } auto-format = true -- 2.38.5 From 198ff2c3f9c56afe4649cc0ecbb09ded5fd4a7c7 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Tue, 28 Mar 2023 01:33:55 +0200 Subject: [PATCH 113/191] Fix clippy lints (#6454) --- helix-core/src/position.rs | 4 ++-- helix-term/src/commands.rs | 4 ++-- helix-tui/src/widgets/block.rs | 9 ++------- helix-view/src/editor.rs | 9 ++------- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 3902b4d4..04bf8c31 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -309,8 +309,8 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) /// on the visual line is returned if the visual line contains any text: /// If the visual line at the specified offset is a virtual line generated by a `LineAnnotation` /// the previous char_index is returned, together with the remaining vertical offset (`virtual_lines`) -pub fn char_idx_at_visual_offset<'a>( - text: RopeSlice<'a>, +pub fn char_idx_at_visual_offset( + text: RopeSlice, mut anchor: usize, mut row_offset: isize, column: usize, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4a7b7883..2f41a2dc 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -501,7 +501,7 @@ impl std::str::FromStr for MappableCommand { fn from_str(s: &str) -> Result { if let Some(suffix) = s.strip_prefix(':') { - let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim()); + let mut typable_command = suffix.split(' ').map(|arg| arg.trim()); let name = typable_command .next() .ok_or_else(|| anyhow!("Expected typable command name"))?; @@ -1470,7 +1470,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { let cursor = range.cursor(text); let height = view.inner_height(); - let scrolloff = config.scrolloff.min(height.saturating_sub(1) as usize / 2); + let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2); let offset = match direction { Forward => offset as isize, Backward => -(offset as isize), diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs index 98f84abe..a6fdde4c 100644 --- a/helix-tui/src/widgets/block.rs +++ b/helix-tui/src/widgets/block.rs @@ -7,8 +7,9 @@ use crate::{ use helix_view::graphics::{Rect, Style}; /// Border render type. Defaults to [`BorderType::Plain`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum BorderType { + #[default] Plain, Rounded, Double, @@ -26,12 +27,6 @@ impl BorderType { } } -impl Default for BorderType { - fn default() -> BorderType { - BorderType::Plain - } -} - /// Base widget to be used with all upper level ones. It may be used to display a box border around /// the widget and/or add a title. /// diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 7207baf3..ee535b5c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -532,10 +532,11 @@ impl Default for CursorShapeConfig { } /// bufferline render modes -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum BufferLine { /// Don't render bufferline + #[default] Never, /// Always render Always, @@ -543,12 +544,6 @@ pub enum BufferLine { Multiple, } -impl Default for BufferLine { - fn default() -> Self { - BufferLine::Never - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum LineNumber { -- 2.38.5 From fc22ed4ac566a4c9d2b058ad56ea87773bd46915 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 19:14:00 -0500 Subject: [PATCH 114/191] build(deps): bump regex from 1.7.1 to 1.7.3 (#6458) Bumps [regex](https://github.com/rust-lang/regex) from 1.7.1 to 1.7.3. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.7.1...1.7.3) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bef317e..c8cc225f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1718,9 +1718,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -1735,9 +1735,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "ropey" -- 2.38.5 From fadccd64c04ca87915e03122747969e2a3257db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 19:15:14 -0500 Subject: [PATCH 115/191] build(deps): bump chrono from 0.4.23 to 0.4.24 (#6460) Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.23 to 0.4.24. - [Release notes](https://github.com/chronotope/chrono/releases) - [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md) - [Commits](https://github.com/chronotope/chrono/compare/v0.4.23...v0.4.24) --- updated-dependencies: - dependency-name: chrono dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8cc225f..5270e76f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "num-integer", -- 2.38.5 From 038d7727cec0f5a414f409bb4971b4afeb6a6718 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 19:19:55 -0500 Subject: [PATCH 116/191] build(deps): bump toml from 0.7.2 to 0.7.3 (#6459) Bumps [toml](https://github.com/toml-rs/toml) from 0.7.2 to 0.7.3. - [Release notes](https://github.com/toml-rs/toml/releases) - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.2...toml-v0.7.3) --- updated-dependencies: - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5270e76f..13ae863a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1540,15 +1540,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -2148,9 +2139,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ "serde", "serde_spanned", @@ -2169,15 +2160,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.3" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", - "nom8", "serde", "serde_spanned", "toml_datetime", + "winnow", ] [[package]] @@ -2480,6 +2471,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "xtask" version = "0.6.0" -- 2.38.5 From 03087882f37bac4d0a9e6d73a6e9faf1be08fbe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 19:26:41 -0500 Subject: [PATCH 117/191] build(deps): bump anyhow from 1.0.69 to 1.0.70 (#6462) Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.69 to 1.0.70. - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.69...1.0.70) --- updated-dependencies: - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13ae863a..b6a775e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "arc-swap" -- 2.38.5 From 67b7b5b10912da7aca105cd13221191f68574b48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 19:27:35 -0500 Subject: [PATCH 118/191] build(deps): bump tokio from 1.26.0 to 1.27.0 (#6461) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.26.0 to 1.27.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.26.0...tokio-1.27.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 15 +++++++-------- helix-lsp/Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6a775e7..8b2765d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,9 +1923,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -2097,14 +2097,13 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.26.0" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", @@ -2117,13 +2116,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.4", ] [[package]] diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 9d76822d..33ec5f30 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -24,6 +24,6 @@ lsp-types = { version = "0.94" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.26", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.12" which = "4.4" -- 2.38.5 From cefc9986d8de3c6d626f18d5d58b1530b8dff30f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 19:27:54 -0500 Subject: [PATCH 119/191] build(deps): bump thiserror from 1.0.39 to 1.0.40 (#6463) Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.39 to 1.0.40. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.39...1.0.40) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b2765d7..98563fed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2015,22 +2015,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.4", ] [[package]] -- 2.38.5 From 6a323c0b1b8fd2491dcbca38b5a1f62bf9581da4 Mon Sep 17 00:00:00 2001 From: Ivan <116971836+seshotake@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:54:17 +0300 Subject: [PATCH 120/191] Update catppuccin theme (#6464) --- runtime/themes/catppuccin_mocha.toml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 87cfe41d..126613bc 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -13,7 +13,7 @@ "string.regexp" = "peach" "string.special" = "blue" -"comment" = { fg = "surface2", modifiers = ["italic"] } +"comment" = { fg = "overlay1", modifiers = ["italic"] } "variable" = "text" "variable.parameter" = { fg = "maroon", modifiers = ["italic"] } @@ -26,15 +26,16 @@ "punctuation.special" = "sky" "keyword" = "mauve" +"keyword.storage.modifier.ref" = "teal" "keyword.control.conditional" = { fg = "mauve", modifiers = ["italic"] } "operator" = "sky" "function" = "blue" -"function.builtin" = "peach" "function.macro" = "mauve" "tag" = "mauve" +"attribute" = "blue" "namespace" = { fg = "blue", modifiers = ["italic"] } @@ -51,7 +52,7 @@ "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "rosewater", modifiers = ["italic", "underlined"] } +"markup.link.url" = { fg = "rosewater", modifiers = ["underlined"] } "markup.link.text" = "blue" "markup.raw" = "flamingo" @@ -66,7 +67,7 @@ "ui.linenr" = { fg = "surface1" } "ui.linenr.selected" = { fg = "lavender" } -"ui.statusline" = { fg = "text", bg = "mantle" } +"ui.statusline" = { fg = "subtext1", bg = "mantle" } "ui.statusline.inactive" = { fg = "surface2", bg = "mantle" } "ui.statusline.normal" = { fg = "base", bg = "lavender", modifiers = ["bold"] } "ui.statusline.insert" = { fg = "base", bg = "green", modifiers = ["bold"] } @@ -76,12 +77,9 @@ "ui.window" = { fg = "crust" } "ui.help" = { fg = "overlay2", bg = "surface0" } -"ui.bufferline" = { fg = "surface1", bg = "mantle" } -"ui.bufferline.active" = { fg = "text", bg = "base", modifiers = [ - "bold", - "italic", -] } -"ui.bufferline.background" = { bg = "surface0" } +"ui.bufferline" = { fg = "subtext0", bg = "mantle" } +"ui.bufferline.active" = { fg = "mauve", bg = "base", underline = { color = "mauve", style = "line" } } +"ui.bufferline.background" = { bg = "crust" } "ui.text" = "text" "ui.text.focus" = { fg = "text", bg = "surface0", modifiers = ["bold"] } -- 2.38.5 From bbcdcd04a5f6c02c14d73d6bd0f53099b1fcb765 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 28 Mar 2023 22:51:11 -0500 Subject: [PATCH 121/191] tui: Handle keyboard enhancement check failure (#6438) If the terminal doesn't send the primary device attributes response to the query, the `terminal::supports_keyboard_enhancement` function from crossterm may timeout and return an Err. We should interpret this error to mean that the terminal doesn't support the keyboard enhancement protocol rather than an error in claiming the terminal. --- helix-tui/src/backend/crossterm.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 4b230f53..4d44f187 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -78,21 +78,20 @@ where } #[inline] - fn supports_keyboard_enhancement_protocol(&self) -> io::Result { - self.supports_keyboard_enhancement_protocol - .get_or_try_init(|| { + fn supports_keyboard_enhancement_protocol(&self) -> bool { + *self.supports_keyboard_enhancement_protocol + .get_or_init(|| { use std::time::Instant; let now = Instant::now(); - let support = terminal::supports_keyboard_enhancement(); + let supported = matches!(terminal::supports_keyboard_enhancement(), Ok(true)); log::debug!( "The keyboard enhancement protocol is {}supported in this terminal (checked in {:?})", - if matches!(support, Ok(true)) { "" } else { "not " }, + if supported { "" } else { "not " }, Instant::now().duration_since(now) ); - support + supported }) - .copied() } } @@ -125,7 +124,7 @@ where if config.enable_mouse_capture { execute!(self.buffer, EnableMouseCapture)?; } - if self.supports_keyboard_enhancement_protocol()? { + if self.supports_keyboard_enhancement_protocol() { execute!( self.buffer, PushKeyboardEnhancementFlags( @@ -143,7 +142,7 @@ where if config.enable_mouse_capture { execute!(self.buffer, DisableMouseCapture)?; } - if self.supports_keyboard_enhancement_protocol()? { + if self.supports_keyboard_enhancement_protocol() { execute!(self.buffer, PopKeyboardEnhancementFlags)?; } execute!( -- 2.38.5 From d59b80514e15d26f280a9b0dbd18afac08578638 Mon Sep 17 00:00:00 2001 From: Filip Dutescu Date: Wed, 29 Mar 2023 06:52:19 +0300 Subject: [PATCH 122/191] feat(debug): highlight current line (#5957) Add new theme highlight keys, for setting the colour of the breakpoint character and the current line at which execution has been paused at. The two new keys are `ui.highlight.frameline` and `ui.debug.breakpoint`. Highlight according to those keys, both the line at which debugging is paused at and the breakpoint indicator. Add an indicator for the current line at which execution is paused at, themed by the `ui.debug.active` theme scope. Update various themes to showcase how the new functionality works. Better icons are dependent on #2869, and as such will be handled in the future, once it lands. Closes: #5952 Signed-off-by: Filip Dutescu --- book/src/themes.md | 3 ++ helix-dap/src/client.rs | 6 +++ helix-term/src/ui/editor.rs | 52 +++++++++--------------- helix-view/src/editor.rs | 7 ++++ helix-view/src/gutter.rs | 60 +++++++++++++++++++--------- runtime/themes/acme.toml | 2 + runtime/themes/autumn.toml | 2 + runtime/themes/ayu_dark.toml | 2 + runtime/themes/ayu_light.toml | 2 + runtime/themes/ayu_mirage.toml | 2 + runtime/themes/dracula.toml | 2 + runtime/themes/dracula_at_night.toml | 2 + runtime/themes/onedark.toml | 3 ++ runtime/themes/onedarker.toml | 2 + theme.toml | 4 +- 15 files changed, 98 insertions(+), 53 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 7accb67f..56d0372c 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -278,8 +278,11 @@ These scopes are used for theming the editor interface: | `ui.cursor.primary.normal` | | | `ui.cursor.primary.insert` | | | `ui.cursor.primary.select` | | +| `ui.debug.breakpoint` | Breakpoint indicator, found in the gutter | +| `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter | | `ui.gutter` | Gutter | | `ui.gutter.selected` | Gutter for the line the cursor is on | +| `ui.highlight.frameline` | Line at which debugging execution is paused at | | `ui.linenr` | Line numbers | | `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.statusline` | Statusline | diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index ff727d00..7efb72d8 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -512,4 +512,10 @@ impl Client { self.call::(args) } + + pub fn current_stack_frame(&self) -> Option<&StackFrame> { + self.stack_frames + .get(&self.thread_id?)? + .get(self.active_frame?) + } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 4cac0fa8..d4b141a0 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -93,40 +93,6 @@ impl EditorView { let mut line_decorations: Vec> = Vec::new(); let mut translated_positions: Vec = Vec::new(); - // DAP: Highlight current stack frame position - let stack_frame = editor.debugger.as_ref().and_then(|debugger| { - if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) { - debugger - .stack_frames - .get(&thread_id) - .and_then(|bt| bt.get(frame)) - } else { - None - } - }); - if let Some(frame) = stack_frame { - if doc.path().is_some() - && frame - .source - .as_ref() - .and_then(|source| source.path.as_ref()) - == doc.path() - { - let line = frame.line - 1; // convert to 0-indexing - let style = theme.get("ui.highlight"); - let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { - if pos.doc_line != line { - return; - } - renderer - .surface - .set_style(Rect::new(area.x, pos.visual_line, area.width, 1), style); - }; - - line_decorations.push(Box::new(line_decoration)); - } - } - if is_focused && config.cursorline { line_decorations.push(Self::cursorline_decorator(doc, view, theme)) } @@ -135,6 +101,23 @@ impl EditorView { Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations); } + // Set DAP highlights, if needed. + if let Some(frame) = editor.current_stack_frame() { + let dap_line = frame.line.saturating_sub(1) as usize; + let style = theme.get("ui.highlight.frameline"); + let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { + if pos.doc_line != dap_line { + return; + } + renderer.surface.set_style( + Rect::new(inner.x, inner.y + pos.visual_line, inner.width, 1), + style, + ); + }; + + line_decorations.push(Box::new(line_decoration)); + } + let mut highlights = Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); let overlay_highlights = Self::overlay_syntax_highlights( @@ -422,6 +405,7 @@ impl EditorView { let primary_selection_scope = theme .find_scope_index_exact("ui.selection.primary") .unwrap_or(selection_scope); + let base_cursor_scope = theme .find_scope_index_exact("ui.cursor") .unwrap_or(selection_scope); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ee535b5c..c939aa5c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -10,6 +10,7 @@ use crate::{ view::ViewPosition, Align, Document, DocumentId, View, ViewId, }; +use dap::StackFrame; use helix_vcs::DiffProviderRegistry; use futures_util::stream::select_all::SelectAll; @@ -1652,6 +1653,12 @@ impl Editor { doc.restore_cursor = false; } } + + pub fn current_stack_frame(&self) -> Option<&StackFrame> { + self.debugger + .as_ref() + .and_then(|debugger| debugger.current_stack_frame()) + } } fn try_restore_indent(doc: &mut Document, view: &mut View) { diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 36e8e16a..3ecae919 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use crate::{ editor::GutterType, - graphics::{Color, Style, UnderlineStyle}, + graphics::{Style, UnderlineStyle}, Document, Editor, Theme, View, }; @@ -245,9 +245,9 @@ pub fn breakpoints<'doc>( theme: &Theme, _is_focused: bool, ) -> GutterFn<'doc> { - let warning = theme.get("warning"); let error = theme.get("error"); let info = theme.get("info"); + let breakpoint_style = theme.get("ui.debug.breakpoint"); let breakpoints = doc.path().and_then(|path| editor.breakpoints.get(path)); @@ -265,30 +265,52 @@ pub fn breakpoints<'doc>( .iter() .find(|breakpoint| breakpoint.line == line)?; - let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { + let style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { error.underline_style(UnderlineStyle::Line) } else if breakpoint.condition.is_some() { error } else if breakpoint.log_message.is_some() { info } else { - warning + breakpoint_style }; - if !breakpoint.verified { - // Faded colors - style = if let Some(Color::Rgb(r, g, b)) = style.fg { - style.fg(Color::Rgb( - ((r as f32) * 0.4).floor() as u8, - ((g as f32) * 0.4).floor() as u8, - ((b as f32) * 0.4).floor() as u8, - )) - } else { - style.fg(Color::Gray) - } - }; + let sym = if breakpoint.verified { "●" } else { "◯" }; + write!(out, "{}", sym).unwrap(); + Some(style) + }, + ) +} + +fn execution_pause_indicator<'doc>( + editor: &'doc Editor, + doc: &'doc Document, + theme: &Theme, + is_focused: bool, +) -> GutterFn<'doc> { + let style = theme.get("ui.debug.active"); + let current_stack_frame = editor.current_stack_frame(); + let frame_line = current_stack_frame.map(|frame| frame.line - 1); + let frame_source_path = current_stack_frame.map(|frame| { + frame + .source + .as_ref() + .and_then(|source| source.path.as_ref()) + }); + let should_display_for_current_doc = + doc.path().is_some() && frame_source_path.unwrap_or(None) == doc.path(); + + Box::new( + move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { + if !first_visual_line + || !is_focused + || line != frame_line? + || !should_display_for_current_doc + { + return None; + } - let sym = if breakpoint.verified { "▲" } else { "⊚" }; + let sym = "▶"; write!(out, "{}", sym).unwrap(); Some(style) }, @@ -304,9 +326,11 @@ pub fn diagnostics_or_breakpoints<'doc>( ) -> GutterFn<'doc> { let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused); let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused); + let mut execution_pause_indicator = execution_pause_indicator(editor, doc, theme, is_focused); Box::new(move |line, selected, first_visual_line: bool, out| { - breakpoints(line, selected, first_visual_line, out) + execution_pause_indicator(line, selected, first_visual_line, out) + .or_else(|| breakpoints(line, selected, first_visual_line, out)) .or_else(|| diagnostics(line, selected, first_visual_line, out)) }) } diff --git a/runtime/themes/acme.toml b/runtime/themes/acme.toml index e1d66ff8..65092474 100644 --- a/runtime/themes/acme.toml +++ b/runtime/themes/acme.toml @@ -12,6 +12,8 @@ "ui.virtual.ruler" = { bg = "acme_bar_bg" } "ui.cursor.match" = {bg="acme_bar_bg"} "ui.cursor" = {bg="cursor", fg="white"} +"ui.debug" = {fg="orange"} +"ui.highlight.frameline" = {bg="#da8581"} "string" = "red" "comment" = "green" "ui.help" = {fg="black", bg="acme_bg"} diff --git a/runtime/themes/autumn.toml b/runtime/themes/autumn.toml index 1430e0a8..4474b0d4 100644 --- a/runtime/themes/autumn.toml +++ b/runtime/themes/autumn.toml @@ -26,6 +26,8 @@ "ui.cursor.primary" = { fg = "my_white", modifiers = ["reversed"] } "ui.cursorline.primary" = { bg = "my_black" } "ui.cursorline.secondary" = { bg = "my_black" } +"ui.highlight.frameline" = { bg = "#8b6904" } +"ui.debug" = { fg = "my_yellow1", bg = "my_gray0" } "ui.text" = "my_white" "operator" = "my_white" "ui.text.focus" = "my_white" diff --git a/runtime/themes/ayu_dark.toml b/runtime/themes/ayu_dark.toml index 37060a24..211d423f 100644 --- a/runtime/themes/ayu_dark.toml +++ b/runtime/themes/ayu_dark.toml @@ -61,6 +61,8 @@ "diagnostic.error"= { underline = { color = "red", style="curl"} } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } +"ui.debug" = { fg = "orange", bg = "background" } +"ui.highlight.frameline" = { bg = "#0067a3" } "special" = "orange" diff --git a/runtime/themes/ayu_light.toml b/runtime/themes/ayu_light.toml index 58b25484..4b0ba1db 100644 --- a/runtime/themes/ayu_light.toml +++ b/runtime/themes/ayu_light.toml @@ -61,6 +61,8 @@ "diagnostic.error"= { underline = { color = "red", style = "curl" } } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } +"ui.debug" = { fg = "orange", bg = "background" } +"ui.highlight.frameline" = { bg = "#cfe0f2" } "special" = "orange" diff --git a/runtime/themes/ayu_mirage.toml b/runtime/themes/ayu_mirage.toml index 4c1f8fa6..5afe0acd 100644 --- a/runtime/themes/ayu_mirage.toml +++ b/runtime/themes/ayu_mirage.toml @@ -61,6 +61,8 @@ "diagnostic.error"= { underline = { color = "red", style = "curl" } } "ui.bufferline" = { fg = "gray", bg = "background" } "ui.bufferline.active" = { fg = "foreground", bg = "dark_gray" } +"ui.debug" = { fg = "orange", bg = "background" } +"ui.highlight.frameline" = { bg = "#0067a3" } "special" = "orange" diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index 8bde4708..b08357db 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -25,6 +25,8 @@ "ui.cursor.primary" = { fg = "background", bg = "cyan", modifiers = ["dim"] } "ui.cursorline.primary" = { bg = "background_dark" } "ui.help" = { fg = "foreground", bg = "background_dark" } +"ui.debug" = { fg = "red" } +"ui.highlight.frameline" = { fg = "black", bg = "red" } "ui.linenr" = { fg = "comment" } "ui.linenr.selected" = { fg = "foreground" } "ui.menu" = { fg = "foreground", bg = "background_dark" } diff --git a/runtime/themes/dracula_at_night.toml b/runtime/themes/dracula_at_night.toml index 9f10ec90..b2e3b9a9 100644 --- a/runtime/themes/dracula_at_night.toml +++ b/runtime/themes/dracula_at_night.toml @@ -25,6 +25,8 @@ "ui.cursor.match" = { fg = "green", modifiers = ["underlined"] } "ui.cursor.primary" = { fg = "background", bg = "cyan", modifiers = ["dim"] } "ui.help" = { fg = "foreground", bg = "background_dark" } +"ui.debug" = { fg = "red" } +"ui.highlight.frameline" = { fg = "black", bg = "red" } "ui.linenr" = { fg = "comment" } "ui.linenr.selected" = { fg = "foreground" } "ui.menu" = { fg = "foreground", bg = "background_dark" } diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 81ca0463..6df5f797 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -64,6 +64,7 @@ "ui.cursorline.primary" = { bg = "light-black" } "ui.highlight" = { bg = "gray" } +"ui.highlight.frameline" = { bg = "#97202a" } "ui.linenr" = { fg = "linenr" } "ui.linenr.selected" = { fg = "white" } @@ -84,6 +85,8 @@ "ui.menu.selected" = { fg = "black", bg = "blue" } "ui.menu.scroll" = { fg = "white", bg = "light-gray" } +"ui.debug" = { fg = "red" } + [palette] yellow = "#E5C07B" diff --git a/runtime/themes/onedarker.toml b/runtime/themes/onedarker.toml index 33f900cc..7169fd02 100644 --- a/runtime/themes/onedarker.toml +++ b/runtime/themes/onedarker.toml @@ -78,6 +78,8 @@ "ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] } "ui.help" = { fg = "white", bg = "gray" } +"ui.debug" = { fg = "red" } +"ui.highlight.frameline" = { bg = "#97202a" } "ui.popup" = { bg = "gray" } "ui.window" = { fg = "gray" } "ui.menu" = { fg = "white", bg = "gray" } diff --git a/theme.toml b/theme.toml index b67eaecc..dd1a5d88 100644 --- a/theme.toml +++ b/theme.toml @@ -69,7 +69,9 @@ label = "honey" "ui.cursor" = { modifiers = ["reversed"] } "ui.cursorline.primary" = { bg = "bossanova" } "ui.highlight" = { bg = "bossanova" } - +"ui.highlight.frameline" = { bg = "#634450" } +"ui.debug" = { fg = "#634450" } +"ui.debug.breakpoint" = { fg = "apricot" } "ui.menu" = { fg = "lavender", bg = "revolver" } "ui.menu.selected" = { fg = "revolver", bg = "white" } "ui.menu.scroll" = { fg = "lavender", bg = "comet" } -- 2.38.5 From 2d10a429ebf7abe5af184b6227346377dc0523e8 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 31 Jan 2023 00:31:21 +0100 Subject: [PATCH 123/191] add workspace config and manual LSP root management fixup documentation Co-authored-by: LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com> fixup typo Co-authored-by: LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com> --- book/src/configuration.md | 4 + book/src/generated/typable-cmd.md | 1 + book/src/languages.md | 1 + helix-core/src/lib.rs | 47 +---------- helix-core/src/syntax.rs | 6 +- helix-loader/src/config.rs | 8 +- helix-loader/src/lib.rs | 44 ++++------ helix-lsp/src/client.rs | 12 ++- helix-lsp/src/lib.rs | 56 +++++++++++- helix-term/src/commands.rs | 6 +- helix-term/src/commands/typed.rs | 26 +++++- helix-term/src/config.rs | 136 ++++++++++++++++++++++++------ helix-term/src/keymap.rs | 79 ++++++++--------- helix-term/src/main.rs | 27 +++--- helix-term/tests/test/helpers.rs | 12 ++- helix-view/src/editor.rs | 12 +-- 16 files changed, 295 insertions(+), 182 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index e2dfc89e..2af0e632 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -30,6 +30,9 @@ You can use a custom configuration file by specifying it with the `-c` or Additionally, you can reload the configuration file by sending the USR1 signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`. +Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository. +Its settings will be merged with the configuration directory `config.toml` and the built-in configuration. + ## Editor ### `[editor]` Section @@ -58,6 +61,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` | +| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | ### `[editor.statusline]` Section diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 8b367aad..9d15b83c 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -70,6 +70,7 @@ | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:config-reload` | Refresh user config. | | `:config-open` | Open the user config.toml file. | +| `:config-open-workspace` | Open the workspace config.toml file. | | `:log-open` | Open the helix log file. | | `:insert-output` | Run shell command, inserting output before each selection. | | `:append-output` | Run shell command, appending output after each selection. | diff --git a/book/src/languages.md b/book/src/languages.md index 5ed69505..a7fa35a6 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -64,6 +64,7 @@ These configuration keys are available: | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` | +| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | `` | ### File-type detection and the `file-types` key diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 4d50e48b..b67e2c8a 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -36,55 +36,12 @@ pub mod unicode { pub use unicode_width as width; } +pub use helix_loader::find_workspace; + pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option { line.chars().position(|ch| !ch.is_whitespace()) } -/// Find project root. -/// -/// Order of detection: -/// * Top-most folder containing a root marker in current git repository -/// * Git repository root if no marker detected -/// * Top-most folder containing a root marker if not git repository detected -/// * Current working directory as fallback -pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::PathBuf { - let current_dir = std::env::current_dir().expect("unable to determine current directory"); - - let root = match root { - Some(root) => { - let root = std::path::Path::new(root); - if root.is_absolute() { - root.to_path_buf() - } else { - current_dir.join(root) - } - } - None => current_dir.clone(), - }; - - let mut top_marker = None; - for ancestor in root.ancestors() { - if root_markers - .iter() - .any(|marker| ancestor.join(marker).exists()) - { - top_marker = Some(ancestor); - } - - if ancestor.join(".git").exists() { - // Top marker is repo root if not root marker was detected yet - if top_marker.is_none() { - top_marker = Some(ancestor); - } - // Don't go higher than repo if we're in one - break; - } - } - - // Return the found top marker or the current_dir as fallback - top_marker.map_or(current_dir, |a| a.to_path_buf()) -} - pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice}; // pub use tendril::StrTendril as Tendril; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index e494ee9b..40846967 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -20,7 +20,7 @@ use std::{ fmt, hash::{Hash, Hasher}, mem::{replace, transmute}, - path::Path, + path::{Path, PathBuf}, str::FromStr, sync::Arc, }; @@ -127,6 +127,10 @@ pub struct LanguageConfiguration { pub auto_pairs: Option, pub rulers: Option>, // if set, override editor's rulers + + /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`. + /// Falling back to the current working directory if none are configured. + pub workspace_lsp_roots: Option>, } #[derive(Debug, PartialEq, Eq, Hash)] diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 0f329d21..8924c8fb 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -9,9 +9,8 @@ pub fn default_lang_config() -> toml::Value { /// User configured languages.toml file, merged with the default config. pub fn user_lang_config() -> Result { - let config = crate::local_config_dirs() + let config = [crate::config_dir(), crate::find_workspace().join(".helix")] .into_iter() - .chain([crate::config_dir()].into_iter()) .map(|path| path.join("languages.toml")) .filter_map(|file| { std::fs::read_to_string(file) @@ -20,8 +19,7 @@ pub fn user_lang_config() -> Result { }) .collect::, _>>()? .into_iter() - .chain([default_lang_config()].into_iter()) - .fold(toml::Value::Table(toml::value::Table::default()), |a, b| { + .fold(default_lang_config(), |a, b| { // combines for example // b: // [[language]] @@ -38,7 +36,7 @@ pub fn user_lang_config() -> Result { // language-server = { command = "/usr/bin/taplo" } // // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values - crate::merge_toml_values(b, a, 3) + crate::merge_toml_values(a, b, 3) }); Ok(config) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 04b44b5a..51bde716 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -42,7 +42,7 @@ fn prioritize_runtime_dirs() -> Vec { let mut rt_dirs = Vec::new(); if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent - let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); + let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR); log::debug!("runtime dir: {}", path.to_string_lossy()); rt_dirs.push(path); } @@ -113,15 +113,6 @@ pub fn config_dir() -> PathBuf { path } -pub fn local_config_dirs() -> Vec { - let directories = find_local_config_dirs() - .into_iter() - .map(|path| path.join(".helix")) - .collect(); - log::debug!("Located configuration folders: {:?}", directories); - directories -} - pub fn cache_dir() -> PathBuf { // TODO: allow env var override let strategy = choose_base_strategy().expect("Unable to find the config directory!"); @@ -137,6 +128,10 @@ pub fn config_file() -> PathBuf { .unwrap_or_else(|| config_dir().join("config.toml")) } +pub fn workspace_config_file() -> PathBuf { + find_workspace().join(".helix").join("config.toml") +} + pub fn lang_config_file() -> PathBuf { config_dir().join("languages.toml") } @@ -145,22 +140,6 @@ pub fn log_file() -> PathBuf { cache_dir().join("helix.log") } -pub fn find_local_config_dirs() -> Vec { - let current_dir = std::env::current_dir().expect("unable to determine current directory"); - let mut directories = Vec::new(); - - for ancestor in current_dir.ancestors() { - if ancestor.join(".git").exists() { - directories.push(ancestor.to_path_buf()); - // Don't go higher than repo if we're in one - break; - } else if ancestor.join(".helix").is_dir() { - directories.push(ancestor.to_path_buf()); - } - } - directories -} - /// Merge two TOML documents, merging values from `right` onto `left` /// /// When an array exists in both `left` and `right`, `right`'s array is @@ -302,3 +281,16 @@ mod merge_toml_tests { ) } } + +/// Finds the current workspace folder. +/// Used as a ceiling dir for root resolve, for the filepicker and other related +pub fn find_workspace() -> PathBuf { + let current_dir = std::env::current_dir().expect("unable to determine current directory"); + for ancestor in current_dir.ancestors() { + if ancestor.join(".git").exists() || ancestor.join(".helix").exists() { + return ancestor.to_owned(); + } + } + + current_dir +} diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f93e5826..34e4c346 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1,22 +1,22 @@ use crate::{ - jsonrpc, + find_root, jsonrpc, transport::{Payload, Transport}, Call, Error, OffsetEncoding, Result, }; -use helix_core::{find_root, ChangeSet, Rope}; +use helix_core::{ChangeSet, Rope}; use helix_loader::{self, VERSION_AND_GIT_HASH}; use lsp::PositionEncodingKind; use lsp_types as lsp; use serde::Deserialize; use serde_json::Value; -use std::collections::HashMap; use std::future::Future; use std::process::Stdio; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; +use std::{collections::HashMap, path::PathBuf}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, @@ -49,6 +49,7 @@ impl Client { config: Option, server_environment: HashMap, root_markers: &[String], + manual_roots: &[PathBuf], id: usize, req_timeout: u64, doc_path: Option<&std::path::PathBuf>, @@ -77,8 +78,11 @@ impl Client { Transport::start(reader, writer, stderr, id); let root_path = find_root( - doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())), + doc_path + .and_then(|x| x.parent().and_then(|x| x.to_str())) + .unwrap_or("."), root_markers, + manual_roots, ); let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 5609a624..e4b00946 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -10,11 +10,15 @@ pub use lsp::{Position, Url}; pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; +use helix_core::{ + find_workspace, + syntax::{LanguageConfiguration, LanguageServerConfiguration}, +}; use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, + path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -641,6 +645,7 @@ impl Registry { &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -656,7 +661,7 @@ impl Registry { let id = self.counter.fetch_add(1, Ordering::Relaxed); let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path)?; + start_client(id, language_config, config, doc_path, root_dirs)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); let (_, old_client) = entry.insert((id, client.clone())); @@ -684,6 +689,7 @@ impl Registry { &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -697,7 +703,7 @@ impl Registry { let id = self.counter.fetch_add(1, Ordering::Relaxed); let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path)?; + start_client(id, language_config, config, doc_path, root_dirs)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); entry.insert((id, client.clone())); @@ -798,6 +804,7 @@ fn start_client( config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], ) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, @@ -805,6 +812,7 @@ fn start_client( config.config.clone(), ls_config.environment.clone(), &config.roots, + config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), id, ls_config.timeout, doc_path, @@ -842,6 +850,48 @@ fn start_client( Ok(NewClientResult(client, incoming)) } +/// Find an LSP root of a file using the following mechansim: +/// * start at `file` (either an absolute path or relative to CWD) +/// * find the top most directory containing a root_marker +/// * inside the current workspace +/// * stop the search at the first root_dir that contains `file` or the workspace (obtained from `helix_core::find_workspace`) +/// * root_dirs only apply inside the workspace. For files outside of the workspace they are ignored +/// * outside the current workspace: keep searching to the top of the file hiearchy +pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf { + let file = std::path::Path::new(file); + let workspace = find_workspace(); + let file = if file.is_absolute() { + file.to_path_buf() + } else { + let current_dir = std::env::current_dir().expect("unable to determine current directory"); + current_dir.join(file) + }; + + let inside_workspace = file.strip_prefix(&workspace).is_ok(); + + let mut top_marker = None; + for ancestor in file.ancestors() { + if root_markers + .iter() + .any(|marker| ancestor.join(marker).exists()) + { + top_marker = Some(ancestor); + } + + if inside_workspace + && (ancestor == workspace + || root_dirs + .iter() + .any(|root_dir| root_dir == ancestor.strip_prefix(&workspace).unwrap())) + { + return top_marker.unwrap_or(ancestor).to_owned(); + } + } + + // If no root was found use the workspace as a fallback + workspace +} + #[cfg(test)] mod tests { use super::{lsp, util::*, OffsetEncoding}; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2f41a2dc..e4d0d753 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -12,7 +12,7 @@ pub use typed::*; use helix_core::{ char_idx_at_visual_offset, comment, doc_formatter::TextFormat, - encoding, find_first_non_whitespace_char, find_root, graphemes, + encoding, find_first_non_whitespace_char, find_workspace, graphemes, history::UndoKind, increment, indent, indent::IndentStyle, @@ -2419,9 +2419,7 @@ fn append_mode(cx: &mut Context) { } fn file_picker(cx: &mut Context) { - // We don't specify language markers, root will be the root of the current - // git repo or the current dir if we're not in a repo - let root = find_root(None, &[]); + let root = find_workspace(); let picker = ui::file_picker(root, &cx.editor.config()); cx.push_layer(Box::new(overlayed(picker))); } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2c72686d..ca55151a 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1371,13 +1371,16 @@ fn lsp_restart( return Ok(()); } + let editor_config = cx.editor.config.load(); let (_view, doc) = current!(cx.editor); let config = doc .language_config() .context("LSP not defined for the current document")?; let scope = config.scope.clone(); - cx.editor.language_servers.restart(config, doc.path())?; + cx.editor + .language_servers + .restart(config, doc.path(), &editor_config.workspace_lsp_roots)?; // This collect is needed because refresh_language_server would need to re-borrow editor. let document_ids_to_refresh: Vec = cx @@ -1970,6 +1973,20 @@ fn open_config( Ok(()) } +fn open_workspace_config( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + cx.editor + .open(&helix_loader::workspace_config_file(), Action::Replace)?; + Ok(()) +} + fn open_log( cx: &mut compositor::Context, _args: &[Cow], @@ -2646,6 +2663,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: open_config, signature: CommandSignature::none(), }, + TypableCommand { + name: "config-open-workspace", + aliases: &[], + doc: "Open the workspace config.toml file.", + fun: open_workspace_config, + signature: CommandSignature::none(), + }, TypableCommand { name: "log-open", aliases: &[], diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882..9776ef7a 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,27 +1,34 @@ -use crate::keymap::{default::default, merge_keys, Keymap}; +use crate::keymap; +use crate::keymap::{merge_keys, Keymap}; +use helix_loader::merge_toml_values; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; use std::fmt::Display; +use std::fs; use std::io::Error as IOError; -use std::path::PathBuf; use toml::de::Error as TomlError; -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone, PartialEq)] pub struct Config { pub theme: Option, - #[serde(default = "default")] pub keys: HashMap, - #[serde(default)] pub editor: helix_view::editor::Config, } +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigRaw { + pub theme: Option, + pub keys: Option>, + pub editor: Option, +} + impl Default for Config { fn default() -> Config { Config { theme: None, - keys: default(), + keys: keymap::default(), editor: helix_view::editor::Config::default(), } } @@ -33,6 +40,12 @@ pub enum ConfigLoadError { Error(IOError), } +impl Default for ConfigLoadError { + fn default() -> Self { + ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder")) + } +} + impl Display for ConfigLoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -43,17 +56,72 @@ impl Display for ConfigLoadError { } impl Config { - pub fn load(config_path: PathBuf) -> Result { - match std::fs::read_to_string(config_path) { - Ok(config) => toml::from_str(&config) - .map(merge_keys) - .map_err(ConfigLoadError::BadConfig), - Err(err) => Err(ConfigLoadError::Error(err)), - } + pub fn load( + global: Result, + local: Result, + ) -> Result { + let global_config: Result = + global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let local_config: Result = + local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let res = match (global_config, local_config) { + (Ok(global), Ok(local)) => { + let mut keys = keymap::default(); + if let Some(global_keys) = global.keys { + merge_keys(&mut keys, global_keys) + } + if let Some(local_keys) = local.keys { + merge_keys(&mut keys, local_keys) + } + + let editor = match (global.editor, local.editor) { + (None, None) => helix_view::editor::Config::default(), + (None, Some(val)) | (Some(val), None) => { + val.try_into().map_err(ConfigLoadError::BadConfig)? + } + (Some(global), Some(local)) => merge_toml_values(global, local, 3) + .try_into() + .map_err(ConfigLoadError::BadConfig)?, + }; + + Config { + theme: local.theme.or(global.theme), + keys, + editor, + } + } + // if any configs are invalid return that first + (_, Err(ConfigLoadError::BadConfig(err))) + | (Err(ConfigLoadError::BadConfig(err)), _) => { + return Err(ConfigLoadError::BadConfig(err)) + } + (Ok(config), Err(_)) | (Err(_), Ok(config)) => { + let mut keys = keymap::default(); + if let Some(keymap) = config.keys { + merge_keys(&mut keys, keymap); + } + Config { + theme: config.theme, + keys, + editor: config.editor.map_or_else( + || Ok(helix_view::editor::Config::default()), + |val| val.try_into().map_err(ConfigLoadError::BadConfig), + )?, + } + } + // these are just two io errors return the one for the global config + (Err(err), Err(_)) => return Err(err), + }; + + Ok(res) } pub fn load_default() -> Result { - Config::load(helix_loader::config_file()) + let global_config = + fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error); + let local_config = fs::read_to_string(helix_loader::workspace_config_file()) + .map_err(ConfigLoadError::Error); + Config::load(global_config, local_config) } } @@ -61,6 +129,12 @@ impl Config { mod tests { use super::*; + impl Config { + fn load_test(config: &str) -> Config { + Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() + } + } + #[test] fn parsing_keymaps_config_file() { use crate::keymap; @@ -77,18 +151,24 @@ mod tests { A-F12 = "move_next_word_end" "#; + let mut keys = keymap::default(); + merge_keys( + &mut keys, + hashmap! { + Mode::Insert => Keymap::new(keymap!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + })), + Mode::Normal => Keymap::new(keymap!({ "Normal mode" + "A-F12" => move_next_word_end, + })), + }, + ); + assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), + Config::load_test(sample_keymaps), Config { - keys: hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" - "A-F12" => move_next_word_end, - })), - }, + keys, ..Default::default() } ); @@ -97,11 +177,11 @@ mod tests { #[test] fn keys_resolve_to_correct_defaults() { // From serde default - let default_keys = toml::from_str::("").unwrap().keys; - assert_eq!(default_keys, default()); + let default_keys = Config::load_test("").keys; + assert_eq!(default_keys, keymap::default()); // From the Default trait let default_keys = Config::default().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, keymap::default()); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e94a5f66..3033c6a4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -2,7 +2,6 @@ pub mod default; pub mod macros; pub use crate::commands::MappableCommand; -use crate::config::Config; use arc_swap::{ access::{DynAccess, DynGuard}, ArcSwap, @@ -16,7 +15,7 @@ use std::{ sync::Arc, }; -use default::default; +pub use default::default; use macros::key; #[derive(Debug, Clone)] @@ -417,12 +416,10 @@ impl Default for Keymaps { } /// Merge default config keys with user overwritten keys for custom user config. -pub fn merge_keys(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default()); - for (mode, keys) in &mut config.keys { +pub fn merge_keys(dst: &mut HashMap, mut delta: HashMap) { + for (mode, keys) in dst { keys.merge(delta.remove(mode).unwrap_or_default()) } - config } #[cfg(test)] @@ -449,26 +446,24 @@ mod tests { #[test] fn merge_partial_keys() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "i" => normal_mode, - "无" => insert_mode, - "z" => jump_backward, - "g" => { "Merge into goto mode" - "$" => goto_line_end, - "g" => delete_char_forward, - }, - }) - ) - }, - ..Default::default() + let keymap = hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "i" => normal_mode, + "无" => insert_mode, + "z" => jump_backward, + "g" => { "Merge into goto mode" + "$" => goto_line_end, + "g" => delete_char_forward, + }, + }) + ) }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); + let mut merged_keyamp = default(); + merge_keys(&mut merged_keyamp, keymap.clone()); + assert_ne!(keymap, merged_keyamp); - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone()))); assert_eq!( keymap.get(Mode::Normal, key!('i')), KeymapResult::Matched(MappableCommand::normal_mode), @@ -486,7 +481,7 @@ mod tests { "Leaf should replace node" ); - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Assumes that `g` is a node in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('$')]).unwrap(), @@ -506,30 +501,28 @@ mod tests { "Old leaves in subnode should be present in merged node" ); - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0); } #[test] fn order_should_be_set() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, + let keymap = hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, }, - }) - ) - }, - ..Default::default() + }, + }) + ) }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + let mut merged_keyamp = default(); + merge_keys(&mut merged_keyamp, keymap.clone()); + assert_ne!(keymap, merged_keyamp); + let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( keymap diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c537..e0c3f6e7 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -3,7 +3,7 @@ use crossterm::event::EventStream; use helix_loader::VERSION_AND_GIT_HASH; use helix_term::application::Application; use helix_term::args::Args; -use helix_term::config::Config; +use helix_term::config::{Config, ConfigLoadError}; use std::path::PathBuf; fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { @@ -126,18 +126,19 @@ FLAGS: helix_loader::initialize_config_file(args.config_file.clone()); - let config = match std::fs::read_to_string(helix_loader::config_file()) { - Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::merge_keys) - .unwrap_or_else(|err| { - eprintln!("Bad config: {}", err); - eprintln!("Press to continue with default config"); - use std::io::Read; - let _ = std::io::stdin().read(&mut []); - Config::default() - }), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), - Err(err) => return Err(Error::new(err)), + let config = match Config::load_default() { + Ok(config) => config, + Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => { + Config::default() + } + Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)), + Err(ConfigLoadError::BadConfig(err)) => { + eprintln!("Bad config: {}", err); + eprintln!("Press to continue with default config"); + use std::io::Read; + let _ = std::io::stdin().read(&mut []); + Config::default() + } }; let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index ccd07bfa..30fe7d0e 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -1,6 +1,7 @@ use std::{ fs::File, io::{Read, Write}, + mem::replace, path::PathBuf, time::Duration, }; @@ -222,10 +223,11 @@ pub fn temp_file_with_contents>( /// Generates a config with defaults more suitable for integration tests pub fn test_config() -> Config { - merge_keys(Config { + Config { editor: test_editor_config(), + keys: helix_term::keymap::default(), ..Default::default() - }) + } } pub fn test_editor_config() -> helix_view::editor::Config { @@ -300,8 +302,10 @@ impl AppBuilder { // Remove this attribute once `with_config` is used in a test: #[allow(dead_code)] - pub fn with_config(mut self, config: Config) -> Self { - self.config = helix_term::keymap::merge_keys(config); + pub fn with_config(mut self, mut config: Config) -> Self { + let keys = replace(&mut config.keys, helix_term::keymap::default()); + merge_keys(&mut config.keys, keys); + self.config = config; self } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c939aa5c..727e1261 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -282,6 +282,8 @@ pub struct Config { /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, pub soft_wrap: SoftWrap, + /// Workspace specific lsp ceiling dirs + pub workspace_lsp_roots: Vec, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -747,6 +749,7 @@ impl Default for Config { soft_wrap: SoftWrap::default(), text_width: 80, completion_replace: false, + workspace_lsp_roots: Vec::new(), } } } @@ -1087,15 +1090,14 @@ impl Editor { } // if doc doesn't have a URL it's a scratch buffer, ignore it - let (lang, path) = { - let doc = self.document(doc_id)?; - (doc.language.clone(), doc.path().cloned()) - }; + let doc = self.document(doc_id)?; + let (lang, path) = (doc.language.clone(), doc.path().cloned()); + let root_dirs = &doc.config.load().workspace_lsp_roots; // try to find a language server based on the language name let language_server = lang.as_ref().and_then(|language| { self.language_servers - .get(language, path.as_ref()) + .get(language, path.as_ref(), root_dirs) .map_err(|e| { log::error!( "Failed to initialize the LSP for `{}` {{ {} }}", -- 2.38.5 From 5b3dd6a678ba138ea21d7d5dd8d3c8a53c7a6d3b Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Tue, 7 Feb 2023 15:59:04 +0100 Subject: [PATCH 124/191] implement proper lsp-workspace support fix typo Co-authored-by: LeoniePhiline <22329650+LeoniePhiline@users.noreply.github.com> --- Cargo.lock | 1 + book/src/languages.md | 2 +- helix-loader/src/config.rs | 61 ++++++------ helix-loader/src/lib.rs | 15 ++- helix-lsp/Cargo.toml | 1 + helix-lsp/src/client.rs | 177 +++++++++++++++++++++++++++++----- helix-lsp/src/lib.rs | 116 +++++++++++++--------- helix-term/src/application.rs | 2 +- helix-term/src/commands.rs | 2 +- 9 files changed, 270 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98563fed..e6ee9d54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1149,6 +1149,7 @@ dependencies = [ "helix-parsec", "log", "lsp-types", + "parking_lot", "serde", "serde_json", "thiserror", diff --git a/book/src/languages.md b/book/src/languages.md index a7fa35a6..f44509fc 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -64,7 +64,7 @@ These configuration keys are available: | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` | -| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | `` | +| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | ### File-type detection and the `file-types` key diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 8924c8fb..d092d20f 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -9,35 +9,38 @@ pub fn default_lang_config() -> toml::Value { /// User configured languages.toml file, merged with the default config. pub fn user_lang_config() -> Result { - let config = [crate::config_dir(), crate::find_workspace().join(".helix")] - .into_iter() - .map(|path| path.join("languages.toml")) - .filter_map(|file| { - std::fs::read_to_string(file) - .map(|config| toml::from_str(&config)) - .ok() - }) - .collect::, _>>()? - .into_iter() - .fold(default_lang_config(), |a, b| { - // combines for example - // b: - // [[language]] - // name = "toml" - // language-server = { command = "taplo", args = ["lsp", "stdio"] } - // - // a: - // [[language]] - // language-server = { command = "/usr/bin/taplo" } - // - // into: - // [[language]] - // name = "toml" - // language-server = { command = "/usr/bin/taplo" } - // - // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values - crate::merge_toml_values(a, b, 3) - }); + let config = [ + crate::config_dir(), + crate::find_workspace().0.join(".helix"), + ] + .into_iter() + .map(|path| path.join("languages.toml")) + .filter_map(|file| { + std::fs::read_to_string(file) + .map(|config| toml::from_str(&config)) + .ok() + }) + .collect::, _>>()? + .into_iter() + .fold(default_lang_config(), |a, b| { + // combines for example + // b: + // [[language]] + // name = "toml" + // language-server = { command = "taplo", args = ["lsp", "stdio"] } + // + // a: + // [[language]] + // language-server = { command = "/usr/bin/taplo" } + // + // into: + // [[language]] + // name = "toml" + // language-server = { command = "/usr/bin/taplo" } + // + // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values + crate::merge_toml_values(a, b, 3) + }); Ok(config) } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 51bde716..6c716975 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -129,7 +129,7 @@ pub fn config_file() -> PathBuf { } pub fn workspace_config_file() -> PathBuf { - find_workspace().join(".helix").join("config.toml") + find_workspace().0.join(".helix").join("config.toml") } pub fn lang_config_file() -> PathBuf { @@ -283,14 +283,19 @@ mod merge_toml_tests { } /// Finds the current workspace folder. -/// Used as a ceiling dir for root resolve, for the filepicker and other related -pub fn find_workspace() -> PathBuf { +/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root +/// +/// This function starts searching the FS upward from the CWD +/// and returns the first directory that contains either `.git` or `.helix`. +/// If no workspace was found returns (CWD, true). +/// Otherwise (workspace, false) is returned +pub fn find_workspace() -> (PathBuf, bool) { let current_dir = std::env::current_dir().expect("unable to determine current directory"); for ancestor in current_dir.ancestors() { if ancestor.join(".git").exists() || ancestor.join(".helix").exists() { - return ancestor.to_owned(); + return (ancestor.to_owned(), false); } } - current_dir + (current_dir, true) } diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 33ec5f30..f8526515 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -27,3 +27,4 @@ thiserror = "1.0" tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.12" which = "4.4" +parking_lot = "0.12.1" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 34e4c346..3dab6bc5 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1,13 +1,17 @@ use crate::{ - find_root, jsonrpc, + find_lsp_workspace, jsonrpc, transport::{Payload, Transport}, Call, Error, OffsetEncoding, Result, }; -use helix_core::{ChangeSet, Rope}; +use helix_core::{find_workspace, ChangeSet, Rope}; use helix_loader::{self, VERSION_AND_GIT_HASH}; -use lsp::PositionEncodingKind; +use lsp::{ + notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, + PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent, +}; use lsp_types as lsp; +use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; use std::future::Future; @@ -26,6 +30,17 @@ use tokio::{ }, }; +fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { + lsp::WorkspaceFolder { + name: uri + .path_segments() + .and_then(|segments| segments.last()) + .map(|basename| basename.to_string()) + .unwrap_or_default(), + uri, + } +} + #[derive(Debug)] pub struct Client { id: usize, @@ -36,11 +51,120 @@ pub struct Client { config: Option, root_path: std::path::PathBuf, root_uri: Option, - workspace_folders: Vec, + workspace_folders: Mutex>, + initalize_notify: Arc, + /// workspace folders added while the server is still initalizing req_timeout: u64, } impl Client { + pub fn try_add_doc( + self: &Arc, + root_markers: &[String], + manual_roots: &[PathBuf], + doc_path: Option<&std::path::PathBuf>, + may_support_workspace: bool, + ) -> bool { + let (workspace, workspace_is_cwd) = find_workspace(); + let root = find_lsp_workspace( + doc_path + .and_then(|x| x.parent().and_then(|x| x.to_str())) + .unwrap_or("."), + root_markers, + manual_roots, + &workspace, + workspace_is_cwd, + ); + let root_uri = root + .as_ref() + .and_then(|root| lsp::Url::from_file_path(root).ok()); + + if self.root_path == root.unwrap_or(workspace) + || root_uri.as_ref().map_or(false, |root_uri| { + self.workspace_folders + .lock() + .iter() + .any(|workspace| &workspace.uri == root_uri) + }) + { + // workspace URI is already registered so we can use this client + return true; + } + + // this server definitly doesn't support multiple workspace, no need to check capabilities + if !may_support_workspace { + return false; + } + + let Some(capabilities) = self.capabilities.get() else { + let client = Arc::clone(self); + // initalization hasn't finished yet, deal with this new root later + // TODO: In the edgecase that a **new root** is added + // for an LSP that **doesn't support workspace_folders** before initaliation is finished + // the new roots are ignored. + // That particular edgecase would require retroactively spawning new LSP + // clients and therefore also require us to retroactively update the corresponding + // documents LSP client handle. It's doable but a pretty weird edgecase so let's + // wait and see if anyone ever runs into it. + tokio::spawn(async move { + client.initalize_notify.notified().await; + if let Some(workspace_folders_caps) = client + .capabilities() + .workspace + .as_ref() + .and_then(|cap| cap.workspace_folders.as_ref()) + .filter(|cap| cap.supported.unwrap_or(false)) + { + client.add_workspace_folder( + root_uri, + &workspace_folders_caps.change_notifications, + ); + } + }); + return true; + }; + + if let Some(workspace_folders_caps) = capabilities + .workspace + .as_ref() + .and_then(|cap| cap.workspace_folders.as_ref()) + .filter(|cap| cap.supported.unwrap_or(false)) + { + self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications); + true + } else { + // the server doesn't support multi workspaces, we need a new client + false + } + } + + fn add_workspace_folder( + &self, + root_uri: Option, + change_notifications: &Option>, + ) { + // root_uri is None just means that there isn't really any LSP workspace + // associated with this file. For servers that support multiple workspaces + // there is just one server so we can always just use that shared instance. + // No need to add a new workspace root here as there is no logical root for this file + // let the server deal with this + let Some(root_uri) = root_uri else { + return; + }; + + // server supports workspace folders, let's add the new root to the list + self.workspace_folders + .lock() + .push(workspace_for_uri(root_uri.clone())); + if &Some(OneOf::Left(false)) == change_notifications { + // server specifically opted out of DidWorkspaceChange notifications + // let's assume the server will request the workspace folders itself + // and that we can therefore reuse the client (but are done now) + return; + } + tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())); + } + #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] pub fn start( @@ -76,30 +200,25 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id); - - let root_path = find_root( + let (workspace, workspace_is_cwd) = find_workspace(); + let root = find_lsp_workspace( doc_path .and_then(|x| x.parent().and_then(|x| x.to_str())) .unwrap_or("."), root_markers, manual_roots, + &workspace, + workspace_is_cwd, ); - let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); + // `root_uri` and `workspace_folder` can be empty in case there is no workspace + // `root_url` can not, use `workspace` as a fallback + let root_path = root.clone().unwrap_or_else(|| workspace.clone()); + let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); - // TODO: support multiple workspace folders let workspace_folders = root_uri .clone() - .map(|root| { - vec![lsp::WorkspaceFolder { - name: root - .path_segments() - .and_then(|segments| segments.last()) - .map(|basename| basename.to_string()) - .unwrap_or_default(), - uri: root, - }] - }) + .map(|root| vec![workspace_for_uri(root)]) .unwrap_or_default(); let client = Self { @@ -110,10 +229,10 @@ impl Client { capabilities: OnceCell::new(), config, req_timeout, - root_path, root_uri, - workspace_folders, + workspace_folders: Mutex::new(workspace_folders), + initalize_notify: initialize_notify.clone(), }; Ok((client, server_rx, initialize_notify)) @@ -169,8 +288,10 @@ impl Client { self.config.as_ref() } - pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] { - &self.workspace_folders + pub async fn workspace_folders( + &self, + ) -> parking_lot::MutexGuard<'_, Vec> { + self.workspace_folders.lock() } /// Execute a RPC request on the language server. @@ -298,7 +419,7 @@ impl Client { #[allow(deprecated)] let params = lsp::InitializeParams { process_id: Some(std::process::id()), - workspace_folders: Some(self.workspace_folders.clone()), + workspace_folders: Some(self.workspace_folders.lock().clone()), // root_path is obsolete, but some clients like pyright still use it so we specify both. // clients will prefer _uri if possible root_path: self.root_path.to_str().map(|path| path.to_owned()), @@ -469,6 +590,16 @@ impl Client { ) } + pub fn did_change_workspace( + &self, + added: Vec, + removed: Vec, + ) -> impl Future> { + self.notify::(DidChangeWorkspaceFoldersParams { + event: WorkspaceFoldersChangeEvent { added, removed }, + }) + } + // ------------------------------------------------------------------------------------------- // Text document // ------------------------------------------------------------------------------------------- diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index e4b00946..d56148a4 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -10,15 +10,12 @@ pub use lsp::{Position, Url}; pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::{ - find_workspace, - syntax::{LanguageConfiguration, LanguageServerConfiguration}, -}; +use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, - path::PathBuf, + path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -609,7 +606,7 @@ impl Notification { #[derive(Debug)] pub struct Registry { - inner: HashMap)>, + inner: HashMap)>>, counter: AtomicUsize, pub incoming: SelectAll>, @@ -633,12 +630,16 @@ impl Registry { pub fn get_by_id(&self, id: usize) -> Option<&Client> { self.inner .values() + .flatten() .find(|(client_id, _)| client_id == &id) .map(|(_, client)| client.as_ref()) } pub fn remove_by_id(&mut self, id: usize) { - self.inner.retain(|_, (client_id, _)| client_id != &id) + self.inner.retain(|_, clients| { + clients.retain(|&(client_id, _)| client_id != id); + !clients.is_empty() + }) } pub fn restart( @@ -664,11 +665,13 @@ impl Registry { start_client(id, language_config, config, doc_path, root_dirs)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); - let (_, old_client) = entry.insert((id, client.clone())); + let old_clients = entry.insert(vec![(id, client.clone())]); - tokio::spawn(async move { - let _ = old_client.force_shutdown().await; - }); + for (_, old_client) in old_clients { + tokio::spawn(async move { + let _ = old_client.force_shutdown().await; + }); + } Ok(Some(client)) } @@ -678,10 +681,12 @@ impl Registry { pub fn stop(&mut self, language_config: &LanguageConfiguration) { let scope = language_config.scope.clone(); - if let Some((_, client)) = self.inner.remove(&scope) { - tokio::spawn(async move { - let _ = client.force_shutdown().await; - }); + if let Some(clients) = self.inner.remove(&scope) { + for (_, client) in clients { + tokio::spawn(async move { + let _ = client.force_shutdown().await; + }); + } } } @@ -696,24 +701,25 @@ impl Registry { None => return Ok(None), }; - match self.inner.entry(language_config.scope.clone()) { - Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())), - Entry::Vacant(entry) => { - // initialize a new client - let id = self.counter.fetch_add(1, Ordering::Relaxed); - - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path, root_dirs)?; - self.incoming.push(UnboundedReceiverStream::new(incoming)); - - entry.insert((id, client.clone())); - Ok(Some(client)) - } + let clients = self.inner.entry(language_config.scope.clone()).or_default(); + // check if we already have a client for this documents root that we can reuse + if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| { + client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) + }) { + return Ok(Some(client.1.clone())); } + // initialize a new client + let id = self.counter.fetch_add(1, Ordering::Relaxed); + + let NewClientResult(client, incoming) = + start_client(id, language_config, config, doc_path, root_dirs)?; + clients.push((id, client.clone())); + self.incoming.push(UnboundedReceiverStream::new(incoming)); + Ok(Some(client)) } pub fn iter_clients(&self) -> impl Iterator> { - self.inner.values().map(|(_, client)| client) + self.inner.values().flatten().map(|(_, client)| client) } } @@ -850,16 +856,23 @@ fn start_client( Ok(NewClientResult(client, incoming)) } -/// Find an LSP root of a file using the following mechansim: -/// * start at `file` (either an absolute path or relative to CWD) -/// * find the top most directory containing a root_marker -/// * inside the current workspace -/// * stop the search at the first root_dir that contains `file` or the workspace (obtained from `helix_core::find_workspace`) -/// * root_dirs only apply inside the workspace. For files outside of the workspace they are ignored -/// * outside the current workspace: keep searching to the top of the file hiearchy -pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf { +/// Find an LSP workspace of a file using the following mechanism: +/// * if the file is outside `workspace` return `None` +/// * start at `file` and search the file tree upward +/// * stop the search at the first `root_dirs` entry that contains `file` +/// * if no `root_dirs` matchs `file` stop at workspace +/// * Returns the top most directory that contains a `root_marker` +/// * If no root marker and we stopped at a `root_dirs` entry, return the directory we stopped at +/// * If we stopped at `workspace` instead and `workspace_is_cwd == false` return `None` +/// * If we stopped at `workspace` instead and `workspace_is_cwd == true` return `workspace` +pub fn find_lsp_workspace( + file: &str, + root_markers: &[String], + root_dirs: &[PathBuf], + workspace: &Path, + workspace_is_cwd: bool, +) -> Option { let file = std::path::Path::new(file); - let workspace = find_workspace(); let file = if file.is_absolute() { file.to_path_buf() } else { @@ -867,7 +880,9 @@ pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> current_dir.join(file) }; - let inside_workspace = file.strip_prefix(&workspace).is_ok(); + if !file.starts_with(workspace) { + return None; + } let mut top_marker = None; for ancestor in file.ancestors() { @@ -878,18 +893,25 @@ pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> top_marker = Some(ancestor); } - if inside_workspace - && (ancestor == workspace - || root_dirs - .iter() - .any(|root_dir| root_dir == ancestor.strip_prefix(&workspace).unwrap())) + if root_dirs + .iter() + .any(|root_dir| root_dir == ancestor.strip_prefix(workspace).unwrap()) { - return top_marker.unwrap_or(ancestor).to_owned(); + // if the worskapce is the cwd do not search any higher for workspaces + // but specify + return Some(top_marker.unwrap_or(workspace).to_owned()); + } + if ancestor == workspace { + // if the workspace is the CWD, let the LSP decide what the workspace + // is + return top_marker + .or_else(|| (!workspace_is_cwd).then_some(workspace)) + .map(Path::to_owned); } } - // If no root was found use the workspace as a fallback - workspace + debug_assert!(false, "workspace must be an ancestor of "); + None } #[cfg(test)] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 95faa01b..4d903eec 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1018,7 +1018,7 @@ impl Application { let language_server = self.editor.language_servers.get_by_id(server_id).unwrap(); - Ok(json!(language_server.workspace_folders())) + Ok(json!(&*language_server.workspace_folders().await)) } Ok(MethodCall::WorkspaceConfiguration(params)) => { let result: Vec<_> = params diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e4d0d753..0f53fdc9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2419,7 +2419,7 @@ fn append_mode(cx: &mut Context) { } fn file_picker(cx: &mut Context) { - let root = find_workspace(); + let root = find_workspace().0; let picker = ui::file_picker(root, &cx.editor.config()); cx.push_layer(Box::new(overlayed(picker))); } -- 2.38.5 From d04288e0f3d292ce47fc0246bcbdc50a9d57ad5e Mon Sep 17 00:00:00 2001 From: jazzfool <56189047+jazzfool@users.noreply.github.com> Date: Fri, 31 Mar 2023 03:21:40 +1100 Subject: [PATCH 125/191] Canonicalize paths before stripping current dir as prefix (#6290) Co-authored-by: jazzfool --- Cargo.lock | 1 + helix-core/Cargo.toml | 1 + helix-core/src/path.rs | 29 +++++++++++++++++++++---- helix-term/tests/integration.rs | 2 -- helix-term/tests/test/commands/write.rs | 10 ++++----- helix-term/tests/test/splits.rs | 8 ++++--- 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6ee9d54..278ab535 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,7 @@ dependencies = [ "arc-swap", "bitflags 2.0.2", "chrono", + "dunce", "encoding_rs", "etcetera", "hashbrown 0.13.2", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 9dfef9ae..e5c5f8f1 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -32,6 +32,7 @@ regex = "1" bitflags = "2.0" ahash = "0.8.3" hashbrown = { version = "0.13.2", features = ["raw"] } +dunce = "1.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-core/src/path.rs b/helix-core/src/path.rs index d59a6baa..efa46c46 100644 --- a/helix-core/src/path.rs +++ b/helix-core/src/path.rs @@ -40,6 +40,21 @@ pub fn expand_tilde(path: &Path) -> PathBuf { /// needs to improve on. /// Copied from cargo: pub fn get_normalized_path(path: &Path) -> PathBuf { + // normalization strategy is to canonicalize first ancestor path that exists (i.e., canonicalize as much as possible), + // then run handrolled normalization on the non-existent remainder + let (base, path) = path + .ancestors() + .find_map(|base| { + let canonicalized_base = dunce::canonicalize(base).ok()?; + let remainder = path.strip_prefix(base).ok()?.into(); + Some((canonicalized_base, remainder)) + }) + .unwrap_or_else(|| (PathBuf::new(), PathBuf::from(path))); + + if path.as_os_str().is_empty() { + return base; + } + let mut components = path.components().peekable(); let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { components.next(); @@ -63,7 +78,7 @@ pub fn get_normalized_path(path: &Path) -> PathBuf { } } } - ret + base.join(ret) } /// Returns the canonical, absolute form of a path with all intermediate components normalized. @@ -82,13 +97,19 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result { } pub fn get_relative_path(path: &Path) -> PathBuf { + let path = PathBuf::from(path); let path = if path.is_absolute() { - let cwdir = std::env::current_dir().expect("couldn't determine current directory"); - path.strip_prefix(cwdir).unwrap_or(path) + let cwdir = std::env::current_dir() + .map(|path| get_normalized_path(&path)) + .expect("couldn't determine current directory"); + get_normalized_path(&path) + .strip_prefix(cwdir) + .map(PathBuf::from) + .unwrap_or(path) } else { path }; - fold_home_dir(path) + fold_home_dir(&path) } /// Returns a truncated filepath where the basepart of the path is reduced to the first diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index cec374af..d77eefed 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -2,8 +2,6 @@ mod test { mod helpers; - use std::path::PathBuf; - use helix_core::{syntax::AutoPairConfig, Selection}; use helix_term::config::Config; diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index 0ea66a12..26515b7a 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -3,7 +3,7 @@ use std::{ ops::RangeInclusive, }; -use helix_core::diagnostic::Severity; +use helix_core::{diagnostic::Severity, path::get_normalized_path}; use helix_view::doc; use super::*; @@ -23,7 +23,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { assert_eq!(1, docs.len()); let doc = docs.pop().unwrap(); - assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path)); + assert_eq!(Some(&get_normalized_path(file.path())), doc.path()); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), false, @@ -269,7 +269,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { assert_eq!(1, docs.len()); let doc = docs.pop().unwrap(); - assert_eq!(Some(&file.path().to_path_buf()), doc.path()); + assert_eq!(Some(&get_normalized_path(file.path())), doc.path()); }), false, ) @@ -341,7 +341,7 @@ async fn test_write_new_path() -> anyhow::Result<()> { Some(&|app| { let doc = doc!(app.editor); assert!(!app.editor.is_err()); - assert_eq!(file1.path(), doc.path().unwrap()); + assert_eq!(&get_normalized_path(file1.path()), doc.path().unwrap()); }), ), ( @@ -349,7 +349,7 @@ async fn test_write_new_path() -> anyhow::Result<()> { Some(&|app| { let doc = doc!(app.editor); assert!(!app.editor.is_err()); - assert_eq!(file2.path(), doc.path().unwrap()); + assert_eq!(&get_normalized_path(file2.path()), doc.path().unwrap()); assert!(app.editor.document_by_path(file1.path()).is_none()); }), ), diff --git a/helix-term/tests/test/splits.rs b/helix-term/tests/test/splits.rs index 96ced21a..1d70f24a 100644 --- a/helix-term/tests/test/splits.rs +++ b/helix-term/tests/test/splits.rs @@ -1,5 +1,7 @@ use super::*; +use helix_core::path::get_normalized_path; + #[tokio::test(flavor = "multi_thread")] async fn test_split_write_quit_all() -> anyhow::Result<()> { let mut file1 = tempfile::NamedTempFile::new()?; @@ -25,21 +27,21 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> { let doc1 = docs .iter() - .find(|doc| doc.path().unwrap() == file1.path()) + .find(|doc| doc.path().unwrap() == &get_normalized_path(file1.path())) .unwrap(); assert_eq!("hello1", doc1.text().to_string()); let doc2 = docs .iter() - .find(|doc| doc.path().unwrap() == file2.path()) + .find(|doc| doc.path().unwrap() == &get_normalized_path(file2.path())) .unwrap(); assert_eq!("hello2", doc2.text().to_string()); let doc3 = docs .iter() - .find(|doc| doc.path().unwrap() == file3.path()) + .find(|doc| doc.path().unwrap() == &get_normalized_path(file3.path())) .unwrap(); assert_eq!("hello3", doc3.text().to_string()); -- 2.38.5 From fc9229c8468d408c427281c093b703bbf1db352f Mon Sep 17 00:00:00 2001 From: Clara Hobbs Date: Thu, 30 Mar 2023 21:32:27 -0400 Subject: [PATCH 126/191] Add injection for markdown strings (#6489) --- runtime/queries/julia/injections.scm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtime/queries/julia/injections.scm b/runtime/queries/julia/injections.scm index ce4011f2..fd174a4a 100644 --- a/runtime/queries/julia/injections.scm +++ b/runtime/queries/julia/injections.scm @@ -26,3 +26,9 @@ prefix: (identifier) @function.macro) @injection.content (#eq? @function.macro "re") (#set! injection.language "regex")) + +( + (prefixed_string_literal + prefix: (identifier) @function.macro) @injection.content + (#eq? @function.macro "md") + (#set! injection.language "markdown")) -- 2.38.5 From d284444eb402d76090ef0743d21fa51b9eee058d Mon Sep 17 00:00:00 2001 From: Clara Hobbs Date: Thu, 30 Mar 2023 21:32:36 -0400 Subject: [PATCH 127/191] Add indents.scm for Julia (#6490) * Add indents.scm for Julia * Update documentation for new indent support --- book/src/generated/lang-support.md | 2 +- runtime/queries/julia/indents.scm | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 runtime/queries/julia/indents.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 003ed4a4..48667f2e 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -68,7 +68,7 @@ | json | ✓ | | ✓ | `vscode-json-language-server` | | jsonnet | ✓ | | | `jsonnet-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` | -| julia | ✓ | | | `julia` | +| julia | ✓ | | ✓ | `julia` | | kdl | ✓ | | | | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | diff --git a/runtime/queries/julia/indents.scm b/runtime/queries/julia/indents.scm new file mode 100644 index 00000000..08f55aa7 --- /dev/null +++ b/runtime/queries/julia/indents.scm @@ -0,0 +1,16 @@ +[ + (struct_definition) + (macro_definition) + (function_definition) + (compound_expression) + (let_statement) + (if_statement) + (for_statement) + (while_statement) + (do_clause) + (parameter_list) +] @indent + +[ + "end" +] @outdent -- 2.38.5 From 565445be6058fc07dd7397a4e32db0c3bf9ba545 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 30 Mar 2023 20:32:49 -0500 Subject: [PATCH 128/191] Update tree-sitter-git-commit (#6493) The last update introduced a bug with comments where a comment would be recognized as a message if there were multiple newlines between the last message or subject and the comment, causing a noticeable change in highlighting. This change fixes that behavior. --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 0a0e29ba..a8fdc4a8 100644 --- a/languages.toml +++ b/languages.toml @@ -1194,7 +1194,7 @@ text-width = 72 [[grammar]] name = "git-commit" -source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "7421fd81840950c0ff4191733cee3b6ac06cb295" } +source = { git = "https://github.com/the-mikedavis/tree-sitter-git-commit", rev = "bd0ca5a6065f2cada3ac6a82a66db3ceff55fa6b" } [[language]] name = "diff" -- 2.38.5 From a863fd89e149ae698f1c7d1d493cc4197abd430a Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 30 Mar 2023 15:51:32 +0200 Subject: [PATCH 129/191] update dependencies --- Cargo.lock | 478 ++++++++++++++++++++++++------------------- helix-vcs/Cargo.toml | 2 +- 2 files changed, 274 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 278ab535..b7ed2c9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,9 @@ checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" [[package]] name = "bstr" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", "once_cell", @@ -102,9 +102,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytecount" @@ -114,9 +114,9 @@ checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cassowary" @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", @@ -260,24 +260,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] @@ -329,9 +329,9 @@ checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encoding_rs" @@ -353,13 +353,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys", ] [[package]] @@ -395,32 +395,32 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fern" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" dependencies = [ "log", ] [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" dependencies = [ "cfg-if", "libc", - "redox_syscall", - "windows-sys 0.42.0", + "redox_syscall 0.2.16", + "windows-sys", ] [[package]] @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.41.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1853c840375a04e02315cb225b6d802291abfabbf08e500727c98a8670f1bf1" +checksum = "c256ea71cc1967faaefdaad15f334146b7c806f12460dcafd3afed845c8c78dd" dependencies = [ "gix-actor", "gix-attributes", @@ -604,9 +604,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aa7d7dd60256b7a0c0506a1d708ec92767c2662ee57b3301b538eaa3e064f8a" +checksum = "7fbad5ce54a8fc997acc50febd89ec80fa6e97cb7f8d0654cb229936407489d8" dependencies = [ "bstr", "gix-config-value", @@ -615,6 +615,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", + "log", "memchr", "nom", "once_cell", @@ -625,9 +626,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d4a4ba0531e46fe558459557a5b29fb86c3e4b2666c1c0861d93c7c678331" +checksum = "d09154c0c8677e4da0ec35e896f56ee3e338e741b9599fae06075edd83a4081c" dependencies = [ "bitflags 1.3.2", "bstr", @@ -666,9 +667,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585b0834d4b6791a848637c4e109545fda9b0f29b591ba55edb33ceda6e7856b" +checksum = "103a0fa79b0d438f5ecb662502f052e530ace4fe1fe8e1c83c0c6da76d728e67" dependencies = [ "gix-hash", "gix-object", @@ -678,9 +679,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.16.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b58931ab475a977deff03417e041a66e4bcb76c4e5797e7ec2fcb272ebce01c" +checksum = "6eba8ba458cb8f4a6c33409b0fe650b1258655175a7ffd1d24fafd3ed31d880b" dependencies = [ "bstr", "dunce", @@ -693,9 +694,9 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a9dfa7b3c1a99315203e8b97f8f99f3bd95731590607abeaa5ca31bc41fe3" +checksum = "0b76f9a80f6dd7be66442ae86e1f534effad9546676a392acc95e269d0c21c22" dependencies = [ "crc32fast", "flate2", @@ -741,9 +742,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "546ee7855d5d8731288f05a63c07ab41b59cb406660a825ed3fe89d7223823df" +checksum = "717ab601ece7921f59fe86849dbe27d44a46ebb883b5885732c4f30df4996177" dependencies = [ "bitflags 1.3.2", "bstr", @@ -804,9 +805,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa63fce01e5bce663bb24ad01fa2b77266e91b1d1982aab3f67cb0aed8af8169" +checksum = "e83af2e3e36005bfe010927f0dff41fb5acc3e3d89c6f1174135b3a34086bda2" dependencies = [ "arc-swap", "gix-features", @@ -822,9 +823,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bc9d22f0d0620d013003ab05ead5c60124b6e1101bc245be9be4fd7e2330cb" +checksum = "9401911c7fe032ad7b31c6a6b5be59cb283d1d6c999417a8215056efe6d635f3" dependencies = [ "clru", "gix-chunk", @@ -844,9 +845,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c104a66dec149cb8f7aaafc6ab797654cf82d67f050fd0cb7e7294e328354b" +checksum = "32370dce200bb951df013e03dff35b4233fc7a89458642b047629b91734a7e19" dependencies = [ "bstr", "thiserror", @@ -854,9 +855,9 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20cebf73229debaa82574c4fd20dcaf00fa8d4bfce823a862c4e990d7a0b5b4" +checksum = "0f3034d4d935aef2c7bf719aaa54b88c520e82413118d886ae880a31d5bdee57" dependencies = [ "gix-command", "gix-config-value", @@ -878,9 +879,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.27.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de7cd1050fa82be4240994defc1f1f2fd9def5d8815dcd005f5ddc5e3dc7511" +checksum = "e4e909396ed3b176823991ccc391c276ae2a015e54edaafa3566d35123cfac9d" dependencies = [ "gix-actor", "gix-features", @@ -911,9 +912,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed98e4a0254953c64bc913bd23146a1de662067d5cf974cbdde396958b39e5b0" +checksum = "b12fc4bbc3161a5b2d68079fce93432cef8771ff88ca017abb01187fddfc41a1" dependencies = [ "bstr", "gix-date", @@ -933,14 +934,14 @@ dependencies = [ "dirs", "gix-path", "libc", - "windows", + "windows 0.43.0", ] [[package]] name = "gix-tempfile" -version = "5.0.1" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aed73ef9642f779d609fd19acc332ac1597b978ee87ec11743a68eefaed65bfa" +checksum = "c2ceb30a610e3f5f2d5f9a5114689fde507ba9417705a8cf3429604275b2153c" dependencies = [ "libc", "once_cell", @@ -978,9 +979,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69ddb780ea1465255e66818d75b7098371c58dbc9560da4488a44b9f5c7e443" +checksum = "7bd629d3680773e1785e585d76fd4295b740b559cad9141517300d99a0c8c049" dependencies = [ "bstr", "thiserror", @@ -988,9 +989,9 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "992b8fdade33e079dc61c29f2388ab8e049965ebf7be40efa7f8b80e3c4543fe" +checksum = "54ec9a000b4f24af706c3cc680c7cda235656cbe3216336522f5692773b8a301" dependencies = [ "bstr", "gix-attributes", @@ -1266,13 +1267,19 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -1290,16 +1297,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "716f12fbcfac6ffab0a5e9ec51d0a0ff70503742bb2dc7b99396394c9dc323f0" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows 0.47.0", ] [[package]] @@ -1351,9 +1358,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -1386,25 +1393,26 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -1433,18 +1441,18 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" [[package]] name = "lock_api" @@ -1486,9 +1494,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] @@ -1510,21 +1518,21 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "nix" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1534,9 +1542,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -1563,11 +1571,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -1598,15 +1606,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -1629,18 +1637,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" -version = "23.1.1" +version = "23.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73c6b64cb5b99eb63ca97d378685712617ec0172ff5c04cd47a489d3e2c51f8" +checksum = "9516b775656bc3e8985e19cd4b8c0c0de045095074e453d2c0a513b5f978392d" [[package]] name = "pulldown-cmark" @@ -1698,6 +1706,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -1705,7 +1722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -1744,23 +1761,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.7" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +checksum = "0e78cc525325c06b4a7ff02db283472f3c042b7ff0c391f96c6d5ac6f4f91b75" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" @@ -1779,35 +1796,35 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -1816,13 +1833,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 2.0.11", ] [[package]] @@ -1863,9 +1880,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -1884,9 +1901,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -1947,15 +1964,15 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "str_indices" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0" +checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" [[package]] name = "syn" -version = "1.0.104" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1964,9 +1981,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" dependencies = [ "proc-macro2", "quote", @@ -1975,22 +1992,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -2032,15 +2049,16 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] @@ -2055,9 +2073,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", "libc", @@ -2075,9 +2093,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" dependencies = [ "time-core", ] @@ -2093,9 +2111,9 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" @@ -2113,7 +2131,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -2124,7 +2142,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.11", ] [[package]] @@ -2192,9 +2210,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-bom" @@ -2210,9 +2228,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-linebreak" @@ -2265,12 +2283,11 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] @@ -2282,9 +2299,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2292,24 +2309,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2317,22 +2334,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn 1.0.104", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "which" @@ -2382,28 +2399,22 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "2649ff315bee4c98757f15dac226efe3d81927adbb6e882084bb1ee3e0c330a7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets 0.47.0", ] [[package]] @@ -2412,65 +2423,122 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "2f8996d3f43b4b2d44327cd71b7b0efd1284ab60e6e9d0e8b630e18555d87d3e" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.47.0", + "windows_aarch64_msvc 0.47.0", + "windows_i686_gnu 0.47.0", + "windows_i686_msvc 0.47.0", + "windows_x86_64_gnu 0.47.0", + "windows_x86_64_gnullvm 0.47.0", + "windows_x86_64_msvc 0.47.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "831d567d53d4f3cb1db332b68e6e2b6260228eb4d99a777d8b2e8ed794027c90" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a42d54a417c60ce4f0e31661eed628f0fa5aca73448c093ec4d45fab4c51cdf" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1925beafdbb22201a53a483db861a5644123157c1c3cee83323a2ed565d71e3" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "3a8ef8f2f1711b223947d9b69b596cf5a4e452c930fb58b6fc3fdae7d0ec6b31" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acaa0c2cf0d2ef99b61c308a0c3dbae430a51b7345dedec470bd8f53f5a3642" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a0628f71be1d11e17ca4a0e9e15b3a5180f6fbf1c2d55e3ba3f850378052c1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9d6e62c256dc6d40b8c8707df17df8d774e60e39db723675241e7c15e910bce7" [[package]] name = "winnow" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index b32c028b..8a226a0b 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -17,7 +17,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p parking_lot = "0.12" arc-swap = { version = "1.6.0" } -gix = { version = "0.41.0", default-features = false , optional = true } +gix = { version = "0.43.0", default-features = false , optional = true } imara-diff = "0.1.5" anyhow = "1" -- 2.38.5 From e72be529968071abcd5fce1d9a06e8e2e2bbaacf Mon Sep 17 00:00:00 2001 From: mWalrus Date: Wed, 22 Mar 2023 15:38:34 +0100 Subject: [PATCH 130/191] Truncate paths in the file picker (#6410) --- helix-term/src/ui/menu.rs | 1 + helix-term/src/ui/picker.rs | 1 + helix-tui/src/buffer.rs | 25 +++++++++++++++++++++++++ helix-tui/src/widgets/table.rs | 20 ++++++++++++++++---- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 30625ace..bdad2e40 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -347,6 +347,7 @@ impl Component for Menu { offset: scroll, selected: self.cursor, }, + false, ); if let Some(cursor) = self.cursor { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3294a2a1..e73088e5 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -885,6 +885,7 @@ impl Component for Picker { offset: 0, selected: Some(cursor), }, + self.truncate_start, ); } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index b1fd4478..2c212b12 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -433,6 +433,31 @@ impl Buffer { (x_offset as u16, y) } + pub fn set_spans_truncated(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { + let mut remaining_width = width; + let mut alt_x = x; + let (text, styles) = + spans + .0 + .iter() + .fold((String::new(), vec![]), |(mut s, mut h), span| { + s.push_str(span.content.as_ref()); + let mut styles = span + .styled_graphemes(span.style) + .map(|grapheme| grapheme.style) + .collect(); + h.append(&mut styles); + + let w = span.width() as u16; + alt_x = alt_x + w; + remaining_width = remaining_width.saturating_sub(w); + + (s, h) + }); + self.set_string_truncated(x, y, &text, width.into(), |idx| styles[idx], true, true); + (x, y) + } + pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { let mut remaining_width = width; let mut x = x; diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index 400f65e0..97762167 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -354,7 +354,13 @@ impl TableState { impl<'a> Table<'a> { // type State = TableState; - pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) { + pub fn render_table( + mut self, + area: Rect, + buf: &mut Buffer, + state: &mut TableState, + truncate: bool, + ) { if area.area() == 0 { return; } @@ -401,6 +407,7 @@ impl<'a> Table<'a> { width: *width, height: max_header_height, }, + truncate, ); col += *width + self.column_spacing; } @@ -457,6 +464,7 @@ impl<'a> Table<'a> { width: *width, height: table_row.height, }, + truncate, ); col += *width + self.column_spacing; } @@ -464,20 +472,24 @@ impl<'a> Table<'a> { } } -fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) { +fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect, truncate: bool) { buf.set_style(area, cell.style); for (i, spans) in cell.content.lines.iter().enumerate() { if i as u16 >= area.height { break; } - buf.set_spans(area.x, area.y + i as u16, spans, area.width); + if truncate { + buf.set_spans_truncated(area.x, area.y + i as u16, spans, area.width); + } else { + buf.set_spans(area.x, area.y + i as u16, spans, area.width); + } } } impl<'a> Widget for Table<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = TableState::default(); - Table::render_table(self, area, buf, &mut state); + Table::render_table(self, area, buf, &mut state, false); } } -- 2.38.5 From 67783ddfd4fc7f06bd6addaa6d65d49759934ace Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 30 Mar 2023 17:31:54 +0200 Subject: [PATCH 131/191] Performant and correct set_spans_truncated the previous implementation used set_string_truncated. This is not only awkward with this kind of "streaming" string (and therefore lead to an inefficient and incorrect initial implementation) but that function also truncates strings of width 1 when there is only a single char available. The implementation here is performant, correct and also handles the single width case correctly. --- helix-tui/src/buffer.rs | 60 ++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 2c212b12..8e0b0adf 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -434,28 +434,44 @@ impl Buffer { } pub fn set_spans_truncated(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { - let mut remaining_width = width; - let mut alt_x = x; - let (text, styles) = - spans - .0 - .iter() - .fold((String::new(), vec![]), |(mut s, mut h), span| { - s.push_str(span.content.as_ref()); - let mut styles = span - .styled_graphemes(span.style) - .map(|grapheme| grapheme.style) - .collect(); - h.append(&mut styles); - - let w = span.width() as u16; - alt_x = alt_x + w; - remaining_width = remaining_width.saturating_sub(w); - - (s, h) - }); - self.set_string_truncated(x, y, &text, width.into(), |idx| styles[idx], true, true); - (x, y) + // prevent panic if out of range + if !self.in_bounds(x, y) || width == 0 { + return (x, y); + } + + let mut x_offset = x as usize; + let max_offset = min(self.area.right(), width.saturating_add(x)); + let mut start_index = self.index_of(x, y); + let mut index = self.index_of(max_offset as u16, y); + + let content_width = spans.width(); + let truncated = content_width > width as usize; + if truncated { + self.content[start_index].set_symbol("…"); + start_index += 1; + } else { + index -= width as usize - content_width; + } + for span in spans.0.iter().rev() { + for s in span.content.graphemes(true).rev() { + let width = s.width(); + if width == 0 { + continue; + } + let start = index - width; + if start < start_index { + break; + } + self.content[start].set_symbol(s); + self.content[start].set_style(span.style); + for i in start + 1..index { + self.content[i].reset(); + } + index -= width; + x_offset += width; + } + } + (x_offset as u16, y) } pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) { -- 2.38.5 From ab819d80f1391667f8ff6b149fa4fbe977f4607a Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 30 Mar 2023 17:56:10 +0200 Subject: [PATCH 132/191] Correctly reload theme on :config-reload The current implementation didn't reload the theme if no no theme was explicitly configured (so the default theme was used). This commit brings `refresh_theme` in line with the initialization code. --- helix-term/src/application.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 4d903eec..130a74af 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -393,20 +393,23 @@ impl Application { /// Refresh theme after config change fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> { - if let Some(theme) = config.theme.clone() { - let true_color = self.true_color(); - let theme = self - .theme_loader - .load(&theme) - .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; - - if true_color || theme.is_16_color() { - self.editor.set_theme(theme); - } else { - anyhow::bail!("theme requires truecolor support, which is not available") - } - } + let true_color = config.editor.true_color || crate::true_color(); + let theme = config + .theme + .as_ref() + .and_then(|theme| { + self.theme_loader + .load(theme) + .map_err(|e| { + log::warn!("failed to load theme `{}` - {}", theme, e); + e + }) + .ok() + .filter(|theme| (true_color || theme.is_16_color())) + }) + .unwrap_or_else(|| self.theme_loader.default_theme(true_color)); + self.editor.set_theme(theme); Ok(()) } @@ -431,10 +434,6 @@ impl Application { } } - fn true_color(&self) -> bool { - self.config.load().editor.true_color || crate::true_color() - } - #[cfg(windows)] // no signal handling available on windows pub async fn handle_signals(&mut self, _signal: ()) {} -- 2.38.5 From 7a69c40524833f93c3df32ba457a1a658472bb4b Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 30 Mar 2023 18:22:51 +0200 Subject: [PATCH 133/191] Hide signature help if it overlays completion menu (#5523) --- helix-term/src/commands.rs | 15 ++++++++++++--- helix-term/src/commands/lsp.rs | 17 ++++++++++++++++- helix-term/src/ui/completion.rs | 6 +++++- helix-term/src/ui/editor.rs | 26 ++++++++++++++++++-------- helix-term/src/ui/popup.rs | 28 +++++++++++++++++----------- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0f53fdc9..b55f1ab7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -54,8 +54,8 @@ use crate::{ job::Callback, keymap::ReverseKeymap, ui::{ - self, editor::InsertEvent, overlay::overlayed, FilePicker, Picker, Popup, Prompt, - PromptEvent, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlayed, FilePicker, Picker, + Popup, Prompt, PromptEvent, }, }; @@ -4261,7 +4261,7 @@ pub fn completion(cx: &mut Context) { } let size = compositor.size(); let ui = compositor.find::().unwrap(); - ui.set_completion( + let completion_area = ui.set_completion( editor, savepoint, items, @@ -4270,6 +4270,15 @@ pub fn completion(cx: &mut Context) { trigger_offset, size, ); + let size = compositor.size(); + let signature_help_area = compositor + .find_id::>(SignatureHelp::ID) + .map(|signature_help| signature_help.area(size, editor)); + // Delete the signature help popup if they intersect. + if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) + { + compositor.remove(SignatureHelp::ID); + } }, ); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 0b0d1db4..f8e83a46 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1221,10 +1221,25 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { contents.set_active_param_range(active_param_range()); let old_popup = compositor.find_id::>(SignatureHelp::ID); - let popup = Popup::new(SignatureHelp::ID, contents) + let mut popup = Popup::new(SignatureHelp::ID, contents) .position(old_popup.and_then(|p| p.get_position())) .position_bias(Open::Above) .ignore_escape_key(true); + + // Don't create a popup if it intersects the auto-complete menu. + let size = compositor.size(); + if compositor + .find::() + .unwrap() + .completion + .as_mut() + .map(|completion| completion.area(size, editor)) + .filter(|area| area.intersects(popup.area(size, editor))) + .is_some() + { + return; + } + compositor.replace_or_push(SignatureHelp::ID, popup); }, ); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index da6b5ddc..e0b1419c 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -414,6 +414,10 @@ impl Completion { true } + + pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { + self.popup.area(viewport, editor) + } } impl Component for Completion { @@ -481,7 +485,7 @@ impl Component for Completion { }; let popup_area = { - let (popup_x, popup_y) = self.popup.get_rel_position(area, cx); + let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor); let (popup_width, popup_height) = self.popup.get_size(); Rect::new(popup_x, popup_y, popup_width, popup_height) }; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index d4b141a0..fd8e8fb2 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -952,7 +952,7 @@ impl EditorView { start_offset: usize, trigger_offset: usize, size: Rect, - ) { + ) -> Option { let mut completion = Completion::new( editor, savepoint, @@ -964,15 +964,17 @@ impl EditorView { if completion.is_empty() { // skip if we got no completion results - return; + return None; } + let area = completion.area(size, editor); editor.last_completion = None; self.last_insert.1.push(InsertEvent::TriggerCompletion); // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); + Some(area) } pub fn clear_completion(&mut self, editor: &mut Editor) { @@ -1256,13 +1258,15 @@ impl Component for EditorView { // let completion swallow the event if necessary let mut consumed = false; if let Some(completion) = &mut self.completion { - // use a fake context here - let mut cx = Context { - editor: cx.editor, - jobs: cx.jobs, - scroll: None, + let res = { + // use a fake context here + let mut cx = Context { + editor: cx.editor, + jobs: cx.jobs, + scroll: None, + }; + completion.handle_event(event, &mut cx) }; - let res = completion.handle_event(event, &mut cx); if let EventResult::Consumed(callback) = res { consumed = true; @@ -1270,6 +1274,12 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn self.clear_completion(cx.editor); + + // In case the popup was deleted because of an intersection w/ the auto-complete menu. + commands::signature_help_impl( + &mut cx, + commands::SignatureHelpInvoked::Automatic, + ); } } } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 5a95c1bb..dff7b231 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -6,7 +6,10 @@ use crate::{ use tui::buffer::Buffer as Surface; use helix_core::Position; -use helix_view::graphics::{Margin, Rect}; +use helix_view::{ + graphics::{Margin, Rect}, + Editor, +}; // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box) @@ -88,10 +91,10 @@ impl Popup { /// Calculate the position where the popup should be rendered and return the coordinates of the /// top left corner. - pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { + pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) { let position = self .position - .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); + .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); let (width, height) = self.size; @@ -155,6 +158,16 @@ impl Popup { pub fn contents_mut(&mut self) -> &mut T { &mut self.contents } + + pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { + // trigger required_size so we recalculate if the child changed + self.required_size((viewport.width, viewport.height)); + + let (rel_x, rel_y) = self.get_rel_position(viewport, editor); + + // clip to viewport + viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)) + } } impl Component for Popup { @@ -232,16 +245,9 @@ impl Component for Popup { } fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - // trigger required_size so we recalculate if the child changed - self.required_size((viewport.width, viewport.height)); - + let area = self.area(viewport, cx.editor); cx.scroll = Some(self.scroll); - let (rel_x, rel_y) = self.get_rel_position(viewport, cx); - - // clip to viewport - let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)); - // clear area let background = cx.editor.theme.get("ui.popup"); surface.clear_with(area, background); -- 2.38.5 From a48d1a4abc0d23d6bc3cfda714d87b9fa4484da8 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Thu, 30 Mar 2023 18:27:00 +0200 Subject: [PATCH 134/191] Prefer utf-8 over utf-32 Utf-8 support has been around for a while as an unstable feature but utf-32 is fairly new. A bunch of LS (like rust-analyzer) added this in a pinch, but it's pretty broken right now. The performance overhead is not very large (still a lot better than utf-16). We can switch back once the ecosystem has matured. --- helix-lsp/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 3dab6bc5..f9496338 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -538,8 +538,8 @@ impl Client { }), general: Some(lsp::GeneralClientCapabilities { position_encodings: Some(vec![ - PositionEncodingKind::UTF32, PositionEncodingKind::UTF8, + PositionEncodingKind::UTF32, PositionEncodingKind::UTF16, ]), ..Default::default() -- 2.38.5 From 9fe3adcff9866ef18953a067fbc0b84e7eb968b5 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 31 Mar 2023 04:26:20 +0200 Subject: [PATCH 135/191] add option to enable/disable lsp snippets --- book/src/configuration.md | 1 + helix-lsp/src/client.rs | 4 ++-- helix-lsp/src/lib.rs | 25 ++++++++++++++++++++----- helix-term/src/commands/typed.rs | 9 ++++++--- helix-view/src/editor.rs | 8 ++++++-- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 2af0e632..4c8ff064 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -127,6 +127,7 @@ The following statusline elements can be configured: | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-signature-help-docs` | Display docs under signature help popup | `true` | +| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` | [^1]: By default, a progress spinner is shown in the statusline beside the file path. [^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f9496338..94e99489 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -411,7 +411,7 @@ impl Client { // General messages // ------------------------------------------------------------------------------------------- - pub(crate) async fn initialize(&self) -> Result { + pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result { if let Some(config) = &self.config { log::info!("Using custom LSP config: {}", config); } @@ -459,7 +459,7 @@ impl Client { text_document: Some(lsp::TextDocumentClientCapabilities { completion: Some(lsp::CompletionClientCapabilities { completion_item: Some(lsp::CompletionItemCapability { - snippet_support: Some(true), + snippet_support: Some(enable_snippets), resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport { properties: vec![ String::from("documentation"), diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index d56148a4..c3a5d816 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -647,6 +647,7 @@ impl Registry { language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -661,8 +662,14 @@ impl Registry { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path, root_dirs)?; + let NewClientResult(client, incoming) = start_client( + id, + language_config, + config, + doc_path, + root_dirs, + enable_snippets, + )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); let old_clients = entry.insert(vec![(id, client.clone())]); @@ -695,6 +702,7 @@ impl Registry { language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -711,8 +719,14 @@ impl Registry { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path, root_dirs)?; + let NewClientResult(client, incoming) = start_client( + id, + language_config, + config, + doc_path, + root_dirs, + enable_snippets, + )?; clients.push((id, client.clone())); self.incoming.push(UnboundedReceiverStream::new(incoming)); Ok(Some(client)) @@ -811,6 +825,7 @@ fn start_client( ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, @@ -834,7 +849,7 @@ fn start_client( .capabilities .get_or_try_init(|| { _client - .initialize() + .initialize(enable_snippets) .map_ok(|response| response.capabilities) }) .await; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ca55151a..0255bbea 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1378,9 +1378,12 @@ fn lsp_restart( .context("LSP not defined for the current document")?; let scope = config.scope.clone(); - cx.editor - .language_servers - .restart(config, doc.path(), &editor_config.workspace_lsp_roots)?; + cx.editor.language_servers.restart( + config, + doc.path(), + &editor_config.workspace_lsp_roots, + editor_config.lsp.snippets, + )?; // This collect is needed because refresh_language_server would need to re-borrow editor. let document_ids_to_refresh: Vec = cx diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 727e1261..34c59b9b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -352,6 +352,8 @@ pub struct LspConfig { pub display_signature_help_docs: bool, /// Display inlay hints pub display_inlay_hints: bool, + /// Whether to enable snippet support + pub snippets: bool, } impl Default for LspConfig { @@ -362,6 +364,7 @@ impl Default for LspConfig { auto_signature_help: true, display_signature_help_docs: true, display_inlay_hints: false, + snippets: true, } } } @@ -1092,12 +1095,13 @@ impl Editor { // if doc doesn't have a URL it's a scratch buffer, ignore it let doc = self.document(doc_id)?; let (lang, path) = (doc.language.clone(), doc.path().cloned()); - let root_dirs = &doc.config.load().workspace_lsp_roots; + let config = doc.config.load(); + let root_dirs = &config.workspace_lsp_roots; // try to find a language server based on the language name let language_server = lang.as_ref().and_then(|language| { self.language_servers - .get(language, path.as_ref(), root_dirs) + .get(language, path.as_ref(), root_dirs, config.lsp.snippets) .map_err(|e| { log::error!( "Failed to initialize the LSP for `{}` {{ {} }}", -- 2.38.5 From ec55b4d5afcfc827a933697baaf19f44984cc72b Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 31 Mar 2023 01:21:05 -0500 Subject: [PATCH 136/191] Add changelog notes for the 23.03 release (#6455) * changelog: Checkpoint 2023-01-10 commit: 927fa112ec049e5f40309ffdd57c314897e18bbc * changelog: Checkpoint 2023-02-05 commit: 9c98043c1cd6a8b92f35214007a90bb0f287beda * changelog: Checkpoint 2023-03-17 commit: bd473928ae049dfe956f8966bfde19859c148e81 * changelog: Checkpoint 2023-03-27 commit: 5323020c3f02b178f2b6807f13d89bf7f40d3cce * Set a tentative release date for 2023-03-31 * Update CHANGELOG.md * Mention virtual text PR in inlayed type hints feature links * Fix description for 5097 * Rebalance features, usability improvements and fixes * Reorganize some out-of-place changes to the proper sections * Eliminate the LSP configurations section This has a lot of overlap with the 'new languages' section with newly supported LSP configurations. Smaller changes to LSP configurations are not so common so I folded those into the 'updated languages and queries' section. --- CHANGELOG.md | 262 ++++++++++++++++++++++++++++++++++++++ VERSION | 2 +- contrib/Helix.appdata.xml | 3 + 3 files changed, 266 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc91c9ff..0778fef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,265 @@ +# 23.03 (2023-03-31) + +> Checkpoint: `5323020c3f02b178f2b6807f13d89bf7f40d3cce` + +For the full log, check out the [git log](https://github.com/helix-editor/helix/compare/22.12..23.03). + +Breaking changes: + +- Select diagnostic range in `goto_*_diag` commands (#4713, #5164, #6193) +- Remove jump behavior from `increment`/`decrement` (#4123, #5929) +- Select change range in `goto_*_change` commands (#5206) +- Split file modification indicator from filename statusline elements (#4731, #6036) +- Jump to symbol ranges in LSP goto commands (#5986) + +Features: + +- Dynamic workspace symbol picker (#5055) +- Soft-wrap (#5420, #5786, #5893, #6142, #6440) +- Initial support for LSP snippet completions (#5864, b1f7528, #6263, bbf4800, 90348b8, f87299f, #6371) +- Add a statusline element for showing the current version control HEAD (#5682) +- Display LSP type hints (#5420, #5934, #6312) +- Enable the Kitty keyboard protocol on terminals with support (#4939, #6170, #6194) +- Add a statusline element for the basename of the current file (#5318) +- Add substring matching syntax for the picker (#5658) +- Support LSP `textDocument/prepareRename` (#6103) +- Allow multiple runtime directories with priorities (#5411) +- Allow configuring whether to insert or replace completions (#5728) + +Commands: + +- `:pipe-to` which pipes selections into a shell command and ignores output (#4931) +- `merge_consecutive_selections` (`A-_`) combines all consecutive selections (#5047) +- `rotate_view_reverse` which focuses the previous view (#5356) +- `goto_declaration` (`gD`, requires LSP) which jumps to a symbol's declaration (#5646) +- `file_picker_in_current_buffer_directory` (#4666) +- `:character-info` which shows information about the character under the cursor (#4000) +- `:toggle-option` for toggling config options at runtime (#4085) +- `dap_restart` for restarting a debug session in DAP (#5651) +- `:lsp-stop` to stop the language server of the current buffer (#5964) +- `:reset-diff-change` for resetting a diff hunk to its original text (#4974) + +Usability improvements: + +- Remove empty detail section in completion menu when LSP doesn't send details (#4902) +- Pass client information on LSP initialization (#4904) +- Allow specifying environment variables for language servers in language config (#4004) +- Allow detached git worktrees to be recognized as root paths (#5097) +- Improve error message handling for theme loading failures (#5073) +- Print the names of binaries required for LSP/DAP in health-check (#5195) +- Improve sorting in the picker in cases of ties (#5169) +- Add theming for prompt suggestions (#5104) +- Open a file picker when using `:open` on directories (#2707, #5278) +- Reload language config with `:config-reload` (#5239, #5381, #5431) +- Improve indent queries for python when the tree is errored (#5332) +- Picker: Open files without closing the picker with `A-ret` (#4435) +- Allow theming cursors by primary/secondary and by mode (#5130) +- Allow configuration of the minimum width for the line-numbers gutter (#4724, #5696) +- Use filename completer for `:run-shell-command` command (#5729) +- Surround with line-endings with `ms` (#4571) +- Hide duplicate symlinks in file pickers (#5658) +- Tabulate buffer picker contents (#5777) +- Add an option to disable LSP (#4425) +- Short-circuit tree-sitter and word object motions (#5851) +- Add exit code to failed command message (#5898) +- Make `m` textobject look for pairs enclosing selections (#3344) +- Negotiate LSP position encoding (#5894) +- Display deprecated LSP completions with strikethrough (#5932) +- Add JSONRPC request ID to failed LSP/DAP request log messages (#6010, #6018) +- Ignore case when filtering LSP completions (#6008) +- Show current language when no arguments are passed to `:set-language` (#5895) +- Refactor and rewrite all book documentation (#5534) +- Separate diagnostic picker message and code (#6095) +- Add a config option to bypass undercurl detection (#6253) +- Only complete appropriate arguments for typed commands (#5966) +- Discard outdated LSP diagnostics (3c9d5d0) +- Discard outdated LSP workspace edits (b6a4927) +- Run shell commands asynchronously (#6373) +- Show diagnostic codes in LSP diagnostic messages (#6378) + +Fixes: + +- Fix behavior of `auto-completion` flag for completion-on-trigger (#5042) +- Reset editor mode when changing buffers (#5072) +- Respect scrolloff settings in mouse movements (#5255) +- Avoid trailing `s` when only one file is opened (#5189) +- Fix erroneous indent between closers of auto-pairs (#5330) +- Expand `~` when parsing file paths in `:open` (#5329) +- Fix theme inheritance for default themes (#5218) +- Fix `extend_line` with a count when the current line(s) are selected (#5288) +- Prompt: Fix autocompletion for paths containing periods (#5175) +- Skip serializing JSONRPC params if params is null (#5471) +- Fix interaction with the `xclip` clipboard provider (#5426) +- Fix undo/redo execution from the command palette (#5294) +- Fix highlighting of non-block cursors (#5575) +- Fix panic when nooping in `join_selections` and `join_selections_space` (#5423) +- Fix selecting a changed file in global search (#5639) +- Fix initial syntax highlight layer sort order (#5196) +- Fix UTF-8 length handling for shellwords (#5738) +- Remove C-j and C-k bindings from the completion menu (#5070) +- Always commit to history when pasting (#5790) +- Properly handle LSP position encoding (#5711) +- Fix infinite loop in `copy_selection_on_prev_line` (#5888) +- Fix completion popup positioning (#5842) +- Fix a panic when uncommenting a line with only a comment token (#5933) +- Fix panic in `goto_window_center` at EOF (#5987) +- Ignore invalid file URIs sent by a language server (#6000) +- Decode LSP URIs for the workspace diagnostics picker (#6016) +- Fix incorrect usages of `tab_width` with `indent_width` (#5918) +- DAP: Send Disconnect if the Terminated event is received (#5532) +- DAP: Validate key and index exist when requesting variables (#5628) +- Check LSP renaming support before prompting for rename text (#6257) +- Fix indent guide rendering (#6136) +- Fix division by zero panic (#6155) +- Fix lacking space panic (#6109) +- Send error replies for malformed and unhandled LSP requests (#6058) +- Fix table column calculations for dynamic pickers (#5920) +- Skip adding jumplist entries for `:` line number previews (#5751) +- Fix completion race conditions (#6173) +- Fix `shrink_selection` with multiple cursors (#6093) +- Fix indentation calculation for lines with mixed tabs/spaces (#6278) +- No-op `client/registerCapability` LSP requests (#6258) +- Send the STOP signal to all processes in the process group (#3546) +- Fix workspace edit client capabilities declaration (7bf168d) +- Fix highlighting in picker results with multiple columns (#6333) + +Themes: + +- Update `serika` (#5038, #6344) +- Update `flatwhite` (#5036, #6323) +- Update `autumn` (#5051, #5397, #6280, #6316) +- Update `acme` (#5019, #5486, #5488) +- Update `gruvbox` themes (#5066, #5333, #5540, #6285, #6295) +- Update `base16_transparent` (#5105) +- Update `dark_high_contrast` (#5105) +- Update `dracula` (#5236, #5627, #6414) +- Update `monokai_pro_spectrum` (#5250, #5602) +- Update `rose_pine` (#5267, #5489, #6384) +- Update `kanagawa` (#5273, #5571, #6085) +- Update `emacs` (#5334) +- Add `github` themes (#5353, efeec12) + - Dark themes: `github_dark`, `github_dark_colorblind`, `github_dark_dimmed`, `github_dark_high_contrast`, `github_dark_tritanopia` + - Light themes: `github_light`, `github_light_colorblind`, `github_light_dimmed`, `github_light_high_contrast`, `github_light_tritanopia` +- Update `solarized` variants (#5445, #6327) +- Update `catppuccin` variants (#5404, #6107, #6269) +- Use curly underlines in built-in themes (#5419) +- Update `zenburn` (#5573) +- Rewrite `snazzy` (#3971) +- Add `monokai_aqua` (#5578) +- Add `markup.strikethrough` to existing themes (#5619) +- Update `sonokai` (#5440) +- Update `onedark` (#5755) +- Add `ayu_evolve` (#5638, #6028, #6225) +- Add `jellybeans` (#5719) +- Update `fleet_dark` (#5605, #6266, #6324, #6375) +- Add `darcula-solid` (#5778) +- Remove text background from monokai themes (#6009) +- Update `pop_dark` (#5992, #6208, #6227, #6292) +- Add `everblush` (#6086) +- Add `adwaita-dark` (#6042, #6342) +- Update `papercolor` (#6162) +- Update `onelight` (#6192, #6276) +- Add `molokai` (#6260) +- Update `ayu` variants (#6329) +- Update `tokyonight` variants (#6349) +- Update `nord` variants (#6376) + +New languages: + +- BibTeX (#5064) +- Mermaid.js (#5147) +- Crystal (#4993, #5205) +- MATLAB/Octave (#5192) +- `tfvars` (uses HCL) (#5396) +- Ponylang (#5416) +- DHall (1f6809c) +- Sagemath (#5649) +- MSBuild (#5793) +- pem (#5797) +- passwd (#4959) +- hosts (#4950, #5914) +- uxntal (#6047) +- Yuck (#6064, #6242) +- GNU gettext PO (#5996) +- Sway (#6023) +- NASM (#6068) +- PRQL (#6126) +- reStructuredText (#6180) +- Smithy (#6370) +- VHDL (#5826) +- Rego (OpenPolicy Agent) (#6415) +- Nim (#6123) + +Updated languages and queries: + +- Use diff syntax for patch files (#5085) +- Add Haskell textobjects (#5061) +- Fix commonlisp configuration (#5091) +- Update Scheme (bae890d) +- Add indent queries for Bash (#5149) +- Recognize `c++` as a C++ extension (#5183) +- Enable HTTP server in `metals` (Scala) config (#5551) +- Change V-lang language server to `v ls` from `vls` (#5677) +- Inject comment grammar into Nix (#5208) +- Update Rust highlights (#5238, #5349) +- Fix HTML injection within Markdown (#5265) +- Fix comment token for godot (#5276) +- Expand injections for Vue (#5268) +- Add `.bash_aliases` as a Bash file-type (#5347) +- Fix comment token for sshclientconfig (#5351) +- Update Prisma (#5417) +- Update C++ (#5457) +- Add more file-types for Python (#5593) +- Update tree-sitter-scala (#5576) +- Add an injection regex for Lua (#5606) +- Add `build.gradle` to java roots configuration (#5641) +- Add Hub PR files to markdown file-types (#5634) +- Add an external formatter configuration for Cue (#5679) +- Add injections for builders and writers to Nix (#5629) +- Update tree-sitter-xml to fix whitespace parsing (#5685) +- Add `Justfile` to the make file-types configuration (#5687) +- Update tree-sitter-sql and highlight queries (#5683, #5772) +- Use the bash grammar and queries for env language (#5720) +- Add podspec files to ruby file-types (#5811) +- Recognize `.C` and `.H` file-types as C++ (#5808) +- Recognize plist and mobileconfig files as XML (#5863) +- Fix `select` indentation in Go (#5713) +- Check for external file modifications when writing (#5805) +- Recognize containerfiles as dockerfile syntax (#5873) +- Update godot grammar and queries (#5944, #6186) +- Improve DHall highlights (#5959) +- Recognize `.env.dist` and `source.env` as env language (#6003) +- Update tree-sitter-git-rebase (#6030, #6094) +- Improve SQL highlights (#6041) +- Improve markdown highlights and inject LaTeX (#6100) +- Add textobject queries for Elm (#6084) +- Recognize graphql schema file type (#6159) +- Improve highlighting in comments (#6143) +- Improve highlighting for JavaScript/TypeScript/ECMAScript languages (#6205) +- Improve PHP highlights (#6203, #6250, #6299) +- Improve Go highlights (#6204) +- Highlight unchecked sqlx functions as SQL in Rust (#6256) +- Improve Erlang highlights (cdd6c8d) +- Improve Nix highlights (fb4d703) +- Improve gdscript highlights (#6311) +- Improve Vlang highlights (#6279) +- Improve Makefile highlights (#6339) +- Remove auto-pair for `'` in OCaml (#6381) +- Fix indents in switch statements in ECMA languages (#6369) +- Recognize xlb and storyboard file-types as XML (#6407) +- Recognize cts and mts file-types as TypeScript (#6424) +- Recognize SVG file-type as XML (#6431) +- Add theme scopes for (un)checked list item markup scopes (#6434) +- Update git commit grammar and add the comment textobject (#6439) +- Recognize ARB file-type as JSON (#6452) + +Packaging: + +- Fix Nix flake devShell for darwin hosts (#5368) +- Add Appstream metadata file to `contrib/` (#5643) +- Increase the MSRV to 1.65 (#5570, #6185) +- Expose the Nix flake's `wrapper` (#5994) + # 22.12 (2022-12-06) This is a great big release filled with changes from a 99 contributors. A big _thank you_ to you all! diff --git a/VERSION b/VERSION index e70b3aeb..35371314 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -22.12 \ No newline at end of file +23.03 \ No newline at end of file diff --git a/contrib/Helix.appdata.xml b/contrib/Helix.appdata.xml index a2428497..b99738a1 100644 --- a/contrib/Helix.appdata.xml +++ b/contrib/Helix.appdata.xml @@ -36,6 +36,9 @@ + + https://helix-editor.com/news/release-23-03-highlights/ + https://helix-editor.com/news/release-22-12-highlights/ -- 2.38.5 From e59cb19892c7b3987d9e27ae8505a5a42bc4e07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 31 Mar 2023 17:13:51 +0900 Subject: [PATCH 137/191] Disable aarch64-macos build for now (build issues) --- .github/workflows/release.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9518a537..bc153359 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,12 +85,13 @@ jobs: rust: stable target: x86_64-pc-windows-msvc cross: false - - build: aarch64-macos - os: macos-latest - rust: stable - target: aarch64-apple-darwin - cross: false - skip_tests: true # x86_64 host can't run aarch64 code + # 23.03: build issues + # - build: aarch64-macos + # os: macos-latest + # rust: stable + # target: aarch64-apple-darwin + # cross: false + # skip_tests: true # x86_64 host can't run aarch64 code # - build: x86_64-win-gnu # os: windows-2019 # rust: stable-x86_64-gnu -- 2.38.5 From 3cf037237f1d080fdcb7990250955701389ae072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 31 Mar 2023 17:14:01 +0900 Subject: [PATCH 138/191] Fix AppImage build problems --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc153359..2fa34e1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -156,6 +156,10 @@ jobs: shell: bash if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux' run: | + # Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE + sudo add-apt-repository universe + sudo apt install libfuse2 + mkdir dist name=dev -- 2.38.5 From 406c5c38a1bb0475579c7d4cd1fd7a90f09d543c Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 31 Mar 2023 22:10:41 -0500 Subject: [PATCH 139/191] changelog: Add remaining notes for 23.03 (#6498) * changelog: Add remaining notes for 23.03 * changelog: Convert PR/commit names to links * Split out 5748 into multiple bullets --- CHANGELOG.md | 467 ++++++++++++++++++++++++++------------------------- 1 file changed, 239 insertions(+), 228 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0778fef1..01184571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,264 +1,275 @@ # 23.03 (2023-03-31) -> Checkpoint: `5323020c3f02b178f2b6807f13d89bf7f40d3cce` +23.03 brings some long-awaited and exciting features. Thank you to everyone involved! This release saw changes from 102 contributors. For the full log, check out the [git log](https://github.com/helix-editor/helix/compare/22.12..23.03). +Also check out the [release notes](https://helix-editor.com/news/release-23-03-highlights/) for more commentary on larger features. Breaking changes: -- Select diagnostic range in `goto_*_diag` commands (#4713, #5164, #6193) -- Remove jump behavior from `increment`/`decrement` (#4123, #5929) -- Select change range in `goto_*_change` commands (#5206) -- Split file modification indicator from filename statusline elements (#4731, #6036) -- Jump to symbol ranges in LSP goto commands (#5986) +- Select diagnostic range in `goto_*_diag` commands ([#4713](https://github.com/helix-editor/helix/pull/4713), [#5164](https://github.com/helix-editor/helix/pull/5164), [#6193](https://github.com/helix-editor/helix/pull/6193)) +- Remove jump behavior from `increment`/`decrement` ([#4123](https://github.com/helix-editor/helix/pull/4123), [#5929](https://github.com/helix-editor/helix/pull/5929)) +- Select change range in `goto_*_change` commands ([#5206](https://github.com/helix-editor/helix/pull/5206)) +- Split file modification indicator from filename statusline elements ([#4731](https://github.com/helix-editor/helix/pull/4731), [#6036](https://github.com/helix-editor/helix/pull/6036)) +- Jump to symbol ranges in LSP goto commands ([#5986](https://github.com/helix-editor/helix/pull/5986)) +- Workspace detection now stops at the first `.helix/` directory (merging multiple `.helix/languages.toml` configurations is no longer supported) ([#5748](https://github.com/helix-editor/helix/pull/5748)) Features: -- Dynamic workspace symbol picker (#5055) -- Soft-wrap (#5420, #5786, #5893, #6142, #6440) -- Initial support for LSP snippet completions (#5864, b1f7528, #6263, bbf4800, 90348b8, f87299f, #6371) -- Add a statusline element for showing the current version control HEAD (#5682) -- Display LSP type hints (#5420, #5934, #6312) -- Enable the Kitty keyboard protocol on terminals with support (#4939, #6170, #6194) -- Add a statusline element for the basename of the current file (#5318) -- Add substring matching syntax for the picker (#5658) -- Support LSP `textDocument/prepareRename` (#6103) -- Allow multiple runtime directories with priorities (#5411) -- Allow configuring whether to insert or replace completions (#5728) +- Dynamic workspace symbol picker ([#5055](https://github.com/helix-editor/helix/pull/5055)) +- Soft-wrap ([#5420](https://github.com/helix-editor/helix/pull/5420), [#5786](https://github.com/helix-editor/helix/pull/5786), [#5893](https://github.com/helix-editor/helix/pull/5893), [#6142](https://github.com/helix-editor/helix/pull/6142), [#6440](https://github.com/helix-editor/helix/pull/6440)) +- Initial support for LSP snippet completions ([#5864](https://github.com/helix-editor/helix/pull/5864), [b1f7528](https://github.com/helix-editor/helix/commit/b1f7528), [#6263](https://github.com/helix-editor/helix/pull/6263), [bbf4800](https://github.com/helix-editor/helix/commit/bbf4800), [90348b8](https://github.com/helix-editor/helix/commit/90348b8), [f87299f](https://github.com/helix-editor/helix/commit/f87299f), [#6371](https://github.com/helix-editor/helix/pull/6371), [9fe3adc](https://github.com/helix-editor/helix/commit/9fe3adc)) +- Add a statusline element for showing the current version control HEAD ([#5682](https://github.com/helix-editor/helix/pull/5682)) +- Display LSP type hints ([#5420](https://github.com/helix-editor/helix/pull/5420), [#5934](https://github.com/helix-editor/helix/pull/5934), [#6312](https://github.com/helix-editor/helix/pull/6312)) +- Enable the Kitty keyboard protocol on terminals with support ([#4939](https://github.com/helix-editor/helix/pull/4939), [#6170](https://github.com/helix-editor/helix/pull/6170), [#6194](https://github.com/helix-editor/helix/pull/6194), [#6438](https://github.com/helix-editor/helix/pull/6438)) +- Add a statusline element for the basename of the current file ([#5318](https://github.com/helix-editor/helix/pull/5318)) +- Add substring matching syntax for the picker ([#5658](https://github.com/helix-editor/helix/pull/5658)) +- Support LSP `textDocument/prepareRename` ([#6103](https://github.com/helix-editor/helix/pull/6103)) +- Allow multiple runtime directories with priorities ([#5411](https://github.com/helix-editor/helix/pull/5411)) +- Allow configuring whether to insert or replace completions ([#5728](https://github.com/helix-editor/helix/pull/5728)) +- Allow per-workspace config file `.helix/config.toml` ([#5748](https://github.com/helix-editor/helix/pull/5748)) +- Add `workspace-lsp-roots` config option to support multiple LSP roots for use with monorepos ([#5748](https://github.com/helix-editor/helix/pull/5748)) Commands: -- `:pipe-to` which pipes selections into a shell command and ignores output (#4931) -- `merge_consecutive_selections` (`A-_`) combines all consecutive selections (#5047) -- `rotate_view_reverse` which focuses the previous view (#5356) -- `goto_declaration` (`gD`, requires LSP) which jumps to a symbol's declaration (#5646) -- `file_picker_in_current_buffer_directory` (#4666) -- `:character-info` which shows information about the character under the cursor (#4000) -- `:toggle-option` for toggling config options at runtime (#4085) -- `dap_restart` for restarting a debug session in DAP (#5651) -- `:lsp-stop` to stop the language server of the current buffer (#5964) -- `:reset-diff-change` for resetting a diff hunk to its original text (#4974) +- `:pipe-to` which pipes selections into a shell command and ignores output ([#4931](https://github.com/helix-editor/helix/pull/4931)) +- `merge_consecutive_selections` (`A-_`) combines all consecutive selections ([#5047](https://github.com/helix-editor/helix/pull/5047)) +- `rotate_view_reverse` which focuses the previous view ([#5356](https://github.com/helix-editor/helix/pull/5356)) +- `goto_declaration` (`gD`, requires LSP) which jumps to a symbol's declaration ([#5646](https://github.com/helix-editor/helix/pull/5646)) +- `file_picker_in_current_buffer_directory` ([#4666](https://github.com/helix-editor/helix/pull/4666)) +- `:character-info` which shows information about the character under the cursor ([#4000](https://github.com/helix-editor/helix/pull/4000)) +- `:toggle-option` for toggling config options at runtime ([#4085](https://github.com/helix-editor/helix/pull/4085)) +- `dap_restart` for restarting a debug session in DAP ([#5651](https://github.com/helix-editor/helix/pull/5651)) +- `:lsp-stop` to stop the language server of the current buffer ([#5964](https://github.com/helix-editor/helix/pull/5964)) +- `:reset-diff-change` for resetting a diff hunk to its original text ([#4974](https://github.com/helix-editor/helix/pull/4974)) +- `:config-open-workspace` for opening the config file local to the current workspace ([#5748](https://github.com/helix-editor/helix/pull/5748)) Usability improvements: -- Remove empty detail section in completion menu when LSP doesn't send details (#4902) -- Pass client information on LSP initialization (#4904) -- Allow specifying environment variables for language servers in language config (#4004) -- Allow detached git worktrees to be recognized as root paths (#5097) -- Improve error message handling for theme loading failures (#5073) -- Print the names of binaries required for LSP/DAP in health-check (#5195) -- Improve sorting in the picker in cases of ties (#5169) -- Add theming for prompt suggestions (#5104) -- Open a file picker when using `:open` on directories (#2707, #5278) -- Reload language config with `:config-reload` (#5239, #5381, #5431) -- Improve indent queries for python when the tree is errored (#5332) -- Picker: Open files without closing the picker with `A-ret` (#4435) -- Allow theming cursors by primary/secondary and by mode (#5130) -- Allow configuration of the minimum width for the line-numbers gutter (#4724, #5696) -- Use filename completer for `:run-shell-command` command (#5729) -- Surround with line-endings with `ms` (#4571) -- Hide duplicate symlinks in file pickers (#5658) -- Tabulate buffer picker contents (#5777) -- Add an option to disable LSP (#4425) -- Short-circuit tree-sitter and word object motions (#5851) -- Add exit code to failed command message (#5898) -- Make `m` textobject look for pairs enclosing selections (#3344) -- Negotiate LSP position encoding (#5894) -- Display deprecated LSP completions with strikethrough (#5932) -- Add JSONRPC request ID to failed LSP/DAP request log messages (#6010, #6018) -- Ignore case when filtering LSP completions (#6008) -- Show current language when no arguments are passed to `:set-language` (#5895) -- Refactor and rewrite all book documentation (#5534) -- Separate diagnostic picker message and code (#6095) -- Add a config option to bypass undercurl detection (#6253) -- Only complete appropriate arguments for typed commands (#5966) -- Discard outdated LSP diagnostics (3c9d5d0) -- Discard outdated LSP workspace edits (b6a4927) -- Run shell commands asynchronously (#6373) -- Show diagnostic codes in LSP diagnostic messages (#6378) +- Remove empty detail section in completion menu when LSP doesn't send details ([#4902](https://github.com/helix-editor/helix/pull/4902)) +- Pass client information on LSP initialization ([#4904](https://github.com/helix-editor/helix/pull/4904)) +- Allow specifying environment variables for language servers in language config ([#4004](https://github.com/helix-editor/helix/pull/4004)) +- Allow detached git worktrees to be recognized as root paths ([#5097](https://github.com/helix-editor/helix/pull/5097)) +- Improve error message handling for theme loading failures ([#5073](https://github.com/helix-editor/helix/pull/5073)) +- Print the names of binaries required for LSP/DAP in health-check ([#5195](https://github.com/helix-editor/helix/pull/5195)) +- Improve sorting in the picker in cases of ties ([#5169](https://github.com/helix-editor/helix/pull/5169)) +- Add theming for prompt suggestions ([#5104](https://github.com/helix-editor/helix/pull/5104)) +- Open a file picker when using `:open` on directories ([#2707](https://github.com/helix-editor/helix/pull/2707), [#5278](https://github.com/helix-editor/helix/pull/5278)) +- Reload language config with `:config-reload` ([#5239](https://github.com/helix-editor/helix/pull/5239), [#5381](https://github.com/helix-editor/helix/pull/5381), [#5431](https://github.com/helix-editor/helix/pull/5431)) +- Improve indent queries for python when the tree is errored ([#5332](https://github.com/helix-editor/helix/pull/5332)) +- Picker: Open files without closing the picker with `A-ret` ([#4435](https://github.com/helix-editor/helix/pull/4435)) +- Allow theming cursors by primary/secondary and by mode ([#5130](https://github.com/helix-editor/helix/pull/5130)) +- Allow configuration of the minimum width for the line-numbers gutter ([#4724](https://github.com/helix-editor/helix/pull/4724), [#5696](https://github.com/helix-editor/helix/pull/5696)) +- Use filename completer for `:run-shell-command` command ([#5729](https://github.com/helix-editor/helix/pull/5729)) +- Surround with line-endings with `ms` ([#4571](https://github.com/helix-editor/helix/pull/4571)) +- Hide duplicate symlinks in file pickers ([#5658](https://github.com/helix-editor/helix/pull/5658)) +- Tabulate buffer picker contents ([#5777](https://github.com/helix-editor/helix/pull/5777)) +- Add an option to disable LSP ([#4425](https://github.com/helix-editor/helix/pull/4425)) +- Short-circuit tree-sitter and word object motions ([#5851](https://github.com/helix-editor/helix/pull/5851)) +- Add exit code to failed command message ([#5898](https://github.com/helix-editor/helix/pull/5898)) +- Make `m` textobject look for pairs enclosing selections ([#3344](https://github.com/helix-editor/helix/pull/3344)) +- Negotiate LSP position encoding ([#5894](https://github.com/helix-editor/helix/pull/5894), [a48d1a4](https://github.com/helix-editor/helix/commit/a48d1a4)) +- Display deprecated LSP completions with strikethrough ([#5932](https://github.com/helix-editor/helix/pull/5932)) +- Add JSONRPC request ID to failed LSP/DAP request log messages ([#6010](https://github.com/helix-editor/helix/pull/6010), [#6018](https://github.com/helix-editor/helix/pull/6018)) +- Ignore case when filtering LSP completions ([#6008](https://github.com/helix-editor/helix/pull/6008)) +- Show current language when no arguments are passed to `:set-language` ([#5895](https://github.com/helix-editor/helix/pull/5895)) +- Refactor and rewrite all book documentation ([#5534](https://github.com/helix-editor/helix/pull/5534)) +- Separate diagnostic picker message and code ([#6095](https://github.com/helix-editor/helix/pull/6095)) +- Add a config option to bypass undercurl detection ([#6253](https://github.com/helix-editor/helix/pull/6253)) +- Only complete appropriate arguments for typed commands ([#5966](https://github.com/helix-editor/helix/pull/5966)) +- Discard outdated LSP diagnostics ([3c9d5d0](https://github.com/helix-editor/helix/commit/3c9d5d0)) +- Discard outdated LSP workspace edits ([b6a4927](https://github.com/helix-editor/helix/commit/b6a4927)) +- Run shell commands asynchronously ([#6373](https://github.com/helix-editor/helix/pull/6373)) +- Show diagnostic codes in LSP diagnostic messages ([#6378](https://github.com/helix-editor/helix/pull/6378)) +- Highlight the current line in a DAP debug session ([#5957](https://github.com/helix-editor/helix/pull/5957)) +- Hide signature help if it overlaps with the completion menu ([#5523](https://github.com/helix-editor/helix/pull/5523), [7a69c40](https://github.com/helix-editor/helix/commit/7a69c40)) Fixes: -- Fix behavior of `auto-completion` flag for completion-on-trigger (#5042) -- Reset editor mode when changing buffers (#5072) -- Respect scrolloff settings in mouse movements (#5255) -- Avoid trailing `s` when only one file is opened (#5189) -- Fix erroneous indent between closers of auto-pairs (#5330) -- Expand `~` when parsing file paths in `:open` (#5329) -- Fix theme inheritance for default themes (#5218) -- Fix `extend_line` with a count when the current line(s) are selected (#5288) -- Prompt: Fix autocompletion for paths containing periods (#5175) -- Skip serializing JSONRPC params if params is null (#5471) -- Fix interaction with the `xclip` clipboard provider (#5426) -- Fix undo/redo execution from the command palette (#5294) -- Fix highlighting of non-block cursors (#5575) -- Fix panic when nooping in `join_selections` and `join_selections_space` (#5423) -- Fix selecting a changed file in global search (#5639) -- Fix initial syntax highlight layer sort order (#5196) -- Fix UTF-8 length handling for shellwords (#5738) -- Remove C-j and C-k bindings from the completion menu (#5070) -- Always commit to history when pasting (#5790) -- Properly handle LSP position encoding (#5711) -- Fix infinite loop in `copy_selection_on_prev_line` (#5888) -- Fix completion popup positioning (#5842) -- Fix a panic when uncommenting a line with only a comment token (#5933) -- Fix panic in `goto_window_center` at EOF (#5987) -- Ignore invalid file URIs sent by a language server (#6000) -- Decode LSP URIs for the workspace diagnostics picker (#6016) -- Fix incorrect usages of `tab_width` with `indent_width` (#5918) -- DAP: Send Disconnect if the Terminated event is received (#5532) -- DAP: Validate key and index exist when requesting variables (#5628) -- Check LSP renaming support before prompting for rename text (#6257) -- Fix indent guide rendering (#6136) -- Fix division by zero panic (#6155) -- Fix lacking space panic (#6109) -- Send error replies for malformed and unhandled LSP requests (#6058) -- Fix table column calculations for dynamic pickers (#5920) -- Skip adding jumplist entries for `:` line number previews (#5751) -- Fix completion race conditions (#6173) -- Fix `shrink_selection` with multiple cursors (#6093) -- Fix indentation calculation for lines with mixed tabs/spaces (#6278) -- No-op `client/registerCapability` LSP requests (#6258) -- Send the STOP signal to all processes in the process group (#3546) -- Fix workspace edit client capabilities declaration (7bf168d) -- Fix highlighting in picker results with multiple columns (#6333) +- Fix behavior of `auto-completion` flag for completion-on-trigger ([#5042](https://github.com/helix-editor/helix/pull/5042)) +- Reset editor mode when changing buffers ([#5072](https://github.com/helix-editor/helix/pull/5072)) +- Respect scrolloff settings in mouse movements ([#5255](https://github.com/helix-editor/helix/pull/5255)) +- Avoid trailing `s` when only one file is opened ([#5189](https://github.com/helix-editor/helix/pull/5189)) +- Fix erroneous indent between closers of auto-pairs ([#5330](https://github.com/helix-editor/helix/pull/5330)) +- Expand `~` when parsing file paths in `:open` ([#5329](https://github.com/helix-editor/helix/pull/5329)) +- Fix theme inheritance for default themes ([#5218](https://github.com/helix-editor/helix/pull/5218)) +- Fix `extend_line` with a count when the current line(s) are selected ([#5288](https://github.com/helix-editor/helix/pull/5288)) +- Prompt: Fix autocompletion for paths containing periods ([#5175](https://github.com/helix-editor/helix/pull/5175)) +- Skip serializing JSONRPC params if params is null ([#5471](https://github.com/helix-editor/helix/pull/5471)) +- Fix interaction with the `xclip` clipboard provider ([#5426](https://github.com/helix-editor/helix/pull/5426)) +- Fix undo/redo execution from the command palette ([#5294](https://github.com/helix-editor/helix/pull/5294)) +- Fix highlighting of non-block cursors ([#5575](https://github.com/helix-editor/helix/pull/5575)) +- Fix panic when nooping in `join_selections` and `join_selections_space` ([#5423](https://github.com/helix-editor/helix/pull/5423)) +- Fix selecting a changed file in global search ([#5639](https://github.com/helix-editor/helix/pull/5639)) +- Fix initial syntax highlight layer sort order ([#5196](https://github.com/helix-editor/helix/pull/5196)) +- Fix UTF-8 length handling for shellwords ([#5738](https://github.com/helix-editor/helix/pull/5738)) +- Remove C-j and C-k bindings from the completion menu ([#5070](https://github.com/helix-editor/helix/pull/5070)) +- Always commit to history when pasting ([#5790](https://github.com/helix-editor/helix/pull/5790)) +- Properly handle LSP position encoding ([#5711](https://github.com/helix-editor/helix/pull/5711)) +- Fix infinite loop in `copy_selection_on_prev_line` ([#5888](https://github.com/helix-editor/helix/pull/5888)) +- Fix completion popup positioning ([#5842](https://github.com/helix-editor/helix/pull/5842)) +- Fix a panic when uncommenting a line with only a comment token ([#5933](https://github.com/helix-editor/helix/pull/5933)) +- Fix panic in `goto_window_center` at EOF ([#5987](https://github.com/helix-editor/helix/pull/5987)) +- Ignore invalid file URIs sent by a language server ([#6000](https://github.com/helix-editor/helix/pull/6000)) +- Decode LSP URIs for the workspace diagnostics picker ([#6016](https://github.com/helix-editor/helix/pull/6016)) +- Fix incorrect usages of `tab_width` with `indent_width` ([#5918](https://github.com/helix-editor/helix/pull/5918)) +- DAP: Send Disconnect if the Terminated event is received ([#5532](https://github.com/helix-editor/helix/pull/5532)) +- DAP: Validate key and index exist when requesting variables ([#5628](https://github.com/helix-editor/helix/pull/5628)) +- Check LSP renaming support before prompting for rename text ([#6257](https://github.com/helix-editor/helix/pull/6257)) +- Fix indent guide rendering ([#6136](https://github.com/helix-editor/helix/pull/6136)) +- Fix division by zero panic ([#6155](https://github.com/helix-editor/helix/pull/6155)) +- Fix lacking space panic ([#6109](https://github.com/helix-editor/helix/pull/6109)) +- Send error replies for malformed and unhandled LSP requests ([#6058](https://github.com/helix-editor/helix/pull/6058)) +- Fix table column calculations for dynamic pickers ([#5920](https://github.com/helix-editor/helix/pull/5920)) +- Skip adding jumplist entries for `:` line number previews ([#5751](https://github.com/helix-editor/helix/pull/5751)) +- Fix completion race conditions ([#6173](https://github.com/helix-editor/helix/pull/6173)) +- Fix `shrink_selection` with multiple cursors ([#6093](https://github.com/helix-editor/helix/pull/6093)) +- Fix indentation calculation for lines with mixed tabs/spaces ([#6278](https://github.com/helix-editor/helix/pull/6278)) +- No-op `client/registerCapability` LSP requests ([#6258](https://github.com/helix-editor/helix/pull/6258)) +- Send the STOP signal to all processes in the process group ([#3546](https://github.com/helix-editor/helix/pull/3546)) +- Fix workspace edit client capabilities declaration ([7bf168d](https://github.com/helix-editor/helix/commit/7bf168d)) +- Fix highlighting in picker results with multiple columns ([#6333](https://github.com/helix-editor/helix/pull/6333)) +- Canonicalize paths before stripping the current dir as a prefix ([#6290](https://github.com/helix-editor/helix/pull/6290)) +- Fix truncation behavior for long path names in the file picker ([#6410](https://github.com/helix-editor/helix/pull/6410), [67783dd](https://github.com/helix-editor/helix/commit/67783dd)) +- Fix theme reloading behavior in `:config-reload` ([ab819d8](https://github.com/helix-editor/helix/commit/ab819d8)) Themes: -- Update `serika` (#5038, #6344) -- Update `flatwhite` (#5036, #6323) -- Update `autumn` (#5051, #5397, #6280, #6316) -- Update `acme` (#5019, #5486, #5488) -- Update `gruvbox` themes (#5066, #5333, #5540, #6285, #6295) -- Update `base16_transparent` (#5105) -- Update `dark_high_contrast` (#5105) -- Update `dracula` (#5236, #5627, #6414) -- Update `monokai_pro_spectrum` (#5250, #5602) -- Update `rose_pine` (#5267, #5489, #6384) -- Update `kanagawa` (#5273, #5571, #6085) -- Update `emacs` (#5334) -- Add `github` themes (#5353, efeec12) +- Update `serika` ([#5038](https://github.com/helix-editor/helix/pull/5038), [#6344](https://github.com/helix-editor/helix/pull/6344)) +- Update `flatwhite` ([#5036](https://github.com/helix-editor/helix/pull/5036), [#6323](https://github.com/helix-editor/helix/pull/6323)) +- Update `autumn` ([#5051](https://github.com/helix-editor/helix/pull/5051), [#5397](https://github.com/helix-editor/helix/pull/5397), [#6280](https://github.com/helix-editor/helix/pull/6280), [#6316](https://github.com/helix-editor/helix/pull/6316)) +- Update `acme` ([#5019](https://github.com/helix-editor/helix/pull/5019), [#5486](https://github.com/helix-editor/helix/pull/5486), [#5488](https://github.com/helix-editor/helix/pull/5488)) +- Update `gruvbox` themes ([#5066](https://github.com/helix-editor/helix/pull/5066), [#5333](https://github.com/helix-editor/helix/pull/5333), [#5540](https://github.com/helix-editor/helix/pull/5540), [#6285](https://github.com/helix-editor/helix/pull/6285), [#6295](https://github.com/helix-editor/helix/pull/6295)) +- Update `base16_transparent` ([#5105](https://github.com/helix-editor/helix/pull/5105)) +- Update `dark_high_contrast` ([#5105](https://github.com/helix-editor/helix/pull/5105)) +- Update `dracula` ([#5236](https://github.com/helix-editor/helix/pull/5236), [#5627](https://github.com/helix-editor/helix/pull/5627), [#6414](https://github.com/helix-editor/helix/pull/6414)) +- Update `monokai_pro_spectrum` ([#5250](https://github.com/helix-editor/helix/pull/5250), [#5602](https://github.com/helix-editor/helix/pull/5602)) +- Update `rose_pine` ([#5267](https://github.com/helix-editor/helix/pull/5267), [#5489](https://github.com/helix-editor/helix/pull/5489), [#6384](https://github.com/helix-editor/helix/pull/6384)) +- Update `kanagawa` ([#5273](https://github.com/helix-editor/helix/pull/5273), [#5571](https://github.com/helix-editor/helix/pull/5571), [#6085](https://github.com/helix-editor/helix/pull/6085)) +- Update `emacs` ([#5334](https://github.com/helix-editor/helix/pull/5334)) +- Add `github` themes ([#5353](https://github.com/helix-editor/helix/pull/5353), [efeec12](https://github.com/helix-editor/helix/commit/efeec12)) - Dark themes: `github_dark`, `github_dark_colorblind`, `github_dark_dimmed`, `github_dark_high_contrast`, `github_dark_tritanopia` - Light themes: `github_light`, `github_light_colorblind`, `github_light_dimmed`, `github_light_high_contrast`, `github_light_tritanopia` -- Update `solarized` variants (#5445, #6327) -- Update `catppuccin` variants (#5404, #6107, #6269) -- Use curly underlines in built-in themes (#5419) -- Update `zenburn` (#5573) -- Rewrite `snazzy` (#3971) -- Add `monokai_aqua` (#5578) -- Add `markup.strikethrough` to existing themes (#5619) -- Update `sonokai` (#5440) -- Update `onedark` (#5755) -- Add `ayu_evolve` (#5638, #6028, #6225) -- Add `jellybeans` (#5719) -- Update `fleet_dark` (#5605, #6266, #6324, #6375) -- Add `darcula-solid` (#5778) -- Remove text background from monokai themes (#6009) -- Update `pop_dark` (#5992, #6208, #6227, #6292) -- Add `everblush` (#6086) -- Add `adwaita-dark` (#6042, #6342) -- Update `papercolor` (#6162) -- Update `onelight` (#6192, #6276) -- Add `molokai` (#6260) -- Update `ayu` variants (#6329) -- Update `tokyonight` variants (#6349) -- Update `nord` variants (#6376) +- Update `solarized` variants ([#5445](https://github.com/helix-editor/helix/pull/5445), [#6327](https://github.com/helix-editor/helix/pull/6327)) +- Update `catppuccin` variants ([#5404](https://github.com/helix-editor/helix/pull/5404), [#6107](https://github.com/helix-editor/helix/pull/6107), [#6269](https://github.com/helix-editor/helix/pull/6269), [#6464](https://github.com/helix-editor/helix/pull/6464)) +- Use curly underlines in built-in themes ([#5419](https://github.com/helix-editor/helix/pull/5419)) +- Update `zenburn` ([#5573](https://github.com/helix-editor/helix/pull/5573)) +- Rewrite `snazzy` ([#3971](https://github.com/helix-editor/helix/pull/3971)) +- Add `monokai_aqua` ([#5578](https://github.com/helix-editor/helix/pull/5578)) +- Add `markup.strikethrough` to existing themes ([#5619](https://github.com/helix-editor/helix/pull/5619)) +- Update `sonokai` ([#5440](https://github.com/helix-editor/helix/pull/5440)) +- Update `onedark` ([#5755](https://github.com/helix-editor/helix/pull/5755)) +- Add `ayu_evolve` ([#5638](https://github.com/helix-editor/helix/pull/5638), [#6028](https://github.com/helix-editor/helix/pull/6028), [#6225](https://github.com/helix-editor/helix/pull/6225)) +- Add `jellybeans` ([#5719](https://github.com/helix-editor/helix/pull/5719)) +- Update `fleet_dark` ([#5605](https://github.com/helix-editor/helix/pull/5605), [#6266](https://github.com/helix-editor/helix/pull/6266), [#6324](https://github.com/helix-editor/helix/pull/6324), [#6375](https://github.com/helix-editor/helix/pull/6375)) +- Add `darcula-solid` ([#5778](https://github.com/helix-editor/helix/pull/5778)) +- Remove text background from monokai themes ([#6009](https://github.com/helix-editor/helix/pull/6009)) +- Update `pop_dark` ([#5992](https://github.com/helix-editor/helix/pull/5992), [#6208](https://github.com/helix-editor/helix/pull/6208), [#6227](https://github.com/helix-editor/helix/pull/6227), [#6292](https://github.com/helix-editor/helix/pull/6292)) +- Add `everblush` ([#6086](https://github.com/helix-editor/helix/pull/6086)) +- Add `adwaita-dark` ([#6042](https://github.com/helix-editor/helix/pull/6042), [#6342](https://github.com/helix-editor/helix/pull/6342)) +- Update `papercolor` ([#6162](https://github.com/helix-editor/helix/pull/6162)) +- Update `onelight` ([#6192](https://github.com/helix-editor/helix/pull/6192), [#6276](https://github.com/helix-editor/helix/pull/6276)) +- Add `molokai` ([#6260](https://github.com/helix-editor/helix/pull/6260)) +- Update `ayu` variants ([#6329](https://github.com/helix-editor/helix/pull/6329)) +- Update `tokyonight` variants ([#6349](https://github.com/helix-editor/helix/pull/6349)) +- Update `nord` variants ([#6376](https://github.com/helix-editor/helix/pull/6376)) New languages: -- BibTeX (#5064) -- Mermaid.js (#5147) -- Crystal (#4993, #5205) -- MATLAB/Octave (#5192) -- `tfvars` (uses HCL) (#5396) -- Ponylang (#5416) -- DHall (1f6809c) -- Sagemath (#5649) -- MSBuild (#5793) -- pem (#5797) -- passwd (#4959) -- hosts (#4950, #5914) -- uxntal (#6047) -- Yuck (#6064, #6242) -- GNU gettext PO (#5996) -- Sway (#6023) -- NASM (#6068) -- PRQL (#6126) -- reStructuredText (#6180) -- Smithy (#6370) -- VHDL (#5826) -- Rego (OpenPolicy Agent) (#6415) -- Nim (#6123) +- BibTeX ([#5064](https://github.com/helix-editor/helix/pull/5064)) +- Mermaid.js ([#5147](https://github.com/helix-editor/helix/pull/5147)) +- Crystal ([#4993](https://github.com/helix-editor/helix/pull/4993), [#5205](https://github.com/helix-editor/helix/pull/5205)) +- MATLAB/Octave ([#5192](https://github.com/helix-editor/helix/pull/5192)) +- `tfvars` (uses HCL) ([#5396](https://github.com/helix-editor/helix/pull/5396)) +- Ponylang ([#5416](https://github.com/helix-editor/helix/pull/5416)) +- DHall ([1f6809c](https://github.com/helix-editor/helix/commit/1f6809c)) +- Sagemath ([#5649](https://github.com/helix-editor/helix/pull/5649)) +- MSBuild ([#5793](https://github.com/helix-editor/helix/pull/5793)) +- pem ([#5797](https://github.com/helix-editor/helix/pull/5797)) +- passwd ([#4959](https://github.com/helix-editor/helix/pull/4959)) +- hosts ([#4950](https://github.com/helix-editor/helix/pull/4950), [#5914](https://github.com/helix-editor/helix/pull/5914)) +- uxntal ([#6047](https://github.com/helix-editor/helix/pull/6047)) +- Yuck ([#6064](https://github.com/helix-editor/helix/pull/6064), [#6242](https://github.com/helix-editor/helix/pull/6242)) +- GNU gettext PO ([#5996](https://github.com/helix-editor/helix/pull/5996)) +- Sway ([#6023](https://github.com/helix-editor/helix/pull/6023)) +- NASM ([#6068](https://github.com/helix-editor/helix/pull/6068)) +- PRQL ([#6126](https://github.com/helix-editor/helix/pull/6126)) +- reStructuredText ([#6180](https://github.com/helix-editor/helix/pull/6180)) +- Smithy ([#6370](https://github.com/helix-editor/helix/pull/6370)) +- VHDL ([#5826](https://github.com/helix-editor/helix/pull/5826)) +- Rego (OpenPolicy Agent) ([#6415](https://github.com/helix-editor/helix/pull/6415)) +- Nim ([#6123](https://github.com/helix-editor/helix/pull/6123)) Updated languages and queries: -- Use diff syntax for patch files (#5085) -- Add Haskell textobjects (#5061) -- Fix commonlisp configuration (#5091) -- Update Scheme (bae890d) -- Add indent queries for Bash (#5149) -- Recognize `c++` as a C++ extension (#5183) -- Enable HTTP server in `metals` (Scala) config (#5551) -- Change V-lang language server to `v ls` from `vls` (#5677) -- Inject comment grammar into Nix (#5208) -- Update Rust highlights (#5238, #5349) -- Fix HTML injection within Markdown (#5265) -- Fix comment token for godot (#5276) -- Expand injections for Vue (#5268) -- Add `.bash_aliases` as a Bash file-type (#5347) -- Fix comment token for sshclientconfig (#5351) -- Update Prisma (#5417) -- Update C++ (#5457) -- Add more file-types for Python (#5593) -- Update tree-sitter-scala (#5576) -- Add an injection regex for Lua (#5606) -- Add `build.gradle` to java roots configuration (#5641) -- Add Hub PR files to markdown file-types (#5634) -- Add an external formatter configuration for Cue (#5679) -- Add injections for builders and writers to Nix (#5629) -- Update tree-sitter-xml to fix whitespace parsing (#5685) -- Add `Justfile` to the make file-types configuration (#5687) -- Update tree-sitter-sql and highlight queries (#5683, #5772) -- Use the bash grammar and queries for env language (#5720) -- Add podspec files to ruby file-types (#5811) -- Recognize `.C` and `.H` file-types as C++ (#5808) -- Recognize plist and mobileconfig files as XML (#5863) -- Fix `select` indentation in Go (#5713) -- Check for external file modifications when writing (#5805) -- Recognize containerfiles as dockerfile syntax (#5873) -- Update godot grammar and queries (#5944, #6186) -- Improve DHall highlights (#5959) -- Recognize `.env.dist` and `source.env` as env language (#6003) -- Update tree-sitter-git-rebase (#6030, #6094) -- Improve SQL highlights (#6041) -- Improve markdown highlights and inject LaTeX (#6100) -- Add textobject queries for Elm (#6084) -- Recognize graphql schema file type (#6159) -- Improve highlighting in comments (#6143) -- Improve highlighting for JavaScript/TypeScript/ECMAScript languages (#6205) -- Improve PHP highlights (#6203, #6250, #6299) -- Improve Go highlights (#6204) -- Highlight unchecked sqlx functions as SQL in Rust (#6256) -- Improve Erlang highlights (cdd6c8d) -- Improve Nix highlights (fb4d703) -- Improve gdscript highlights (#6311) -- Improve Vlang highlights (#6279) -- Improve Makefile highlights (#6339) -- Remove auto-pair for `'` in OCaml (#6381) -- Fix indents in switch statements in ECMA languages (#6369) -- Recognize xlb and storyboard file-types as XML (#6407) -- Recognize cts and mts file-types as TypeScript (#6424) -- Recognize SVG file-type as XML (#6431) -- Add theme scopes for (un)checked list item markup scopes (#6434) -- Update git commit grammar and add the comment textobject (#6439) -- Recognize ARB file-type as JSON (#6452) +- Use diff syntax for patch files ([#5085](https://github.com/helix-editor/helix/pull/5085)) +- Add Haskell textobjects ([#5061](https://github.com/helix-editor/helix/pull/5061)) +- Fix commonlisp configuration ([#5091](https://github.com/helix-editor/helix/pull/5091)) +- Update Scheme ([bae890d](https://github.com/helix-editor/helix/commit/bae890d)) +- Add indent queries for Bash ([#5149](https://github.com/helix-editor/helix/pull/5149)) +- Recognize `c++` as a C++ extension ([#5183](https://github.com/helix-editor/helix/pull/5183)) +- Enable HTTP server in `metals` (Scala) config ([#5551](https://github.com/helix-editor/helix/pull/5551)) +- Change V-lang language server to `v ls` from `vls` ([#5677](https://github.com/helix-editor/helix/pull/5677)) +- Inject comment grammar into Nix ([#5208](https://github.com/helix-editor/helix/pull/5208)) +- Update Rust highlights ([#5238](https://github.com/helix-editor/helix/pull/5238), [#5349](https://github.com/helix-editor/helix/pull/5349)) +- Fix HTML injection within Markdown ([#5265](https://github.com/helix-editor/helix/pull/5265)) +- Fix comment token for godot ([#5276](https://github.com/helix-editor/helix/pull/5276)) +- Expand injections for Vue ([#5268](https://github.com/helix-editor/helix/pull/5268)) +- Add `.bash_aliases` as a Bash file-type ([#5347](https://github.com/helix-editor/helix/pull/5347)) +- Fix comment token for sshclientconfig ([#5351](https://github.com/helix-editor/helix/pull/5351)) +- Update Prisma ([#5417](https://github.com/helix-editor/helix/pull/5417)) +- Update C++ ([#5457](https://github.com/helix-editor/helix/pull/5457)) +- Add more file-types for Python ([#5593](https://github.com/helix-editor/helix/pull/5593)) +- Update tree-sitter-scala ([#5576](https://github.com/helix-editor/helix/pull/5576)) +- Add an injection regex for Lua ([#5606](https://github.com/helix-editor/helix/pull/5606)) +- Add `build.gradle` to java roots configuration ([#5641](https://github.com/helix-editor/helix/pull/5641)) +- Add Hub PR files to markdown file-types ([#5634](https://github.com/helix-editor/helix/pull/5634)) +- Add an external formatter configuration for Cue ([#5679](https://github.com/helix-editor/helix/pull/5679)) +- Add injections for builders and writers to Nix ([#5629](https://github.com/helix-editor/helix/pull/5629)) +- Update tree-sitter-xml to fix whitespace parsing ([#5685](https://github.com/helix-editor/helix/pull/5685)) +- Add `Justfile` to the make file-types configuration ([#5687](https://github.com/helix-editor/helix/pull/5687)) +- Update tree-sitter-sql and highlight queries ([#5683](https://github.com/helix-editor/helix/pull/5683), [#5772](https://github.com/helix-editor/helix/pull/5772)) +- Use the bash grammar and queries for env language ([#5720](https://github.com/helix-editor/helix/pull/5720)) +- Add podspec files to ruby file-types ([#5811](https://github.com/helix-editor/helix/pull/5811)) +- Recognize `.C` and `.H` file-types as C++ ([#5808](https://github.com/helix-editor/helix/pull/5808)) +- Recognize plist and mobileconfig files as XML ([#5863](https://github.com/helix-editor/helix/pull/5863)) +- Fix `select` indentation in Go ([#5713](https://github.com/helix-editor/helix/pull/5713)) +- Check for external file modifications when writing ([#5805](https://github.com/helix-editor/helix/pull/5805)) +- Recognize containerfiles as dockerfile syntax ([#5873](https://github.com/helix-editor/helix/pull/5873)) +- Update godot grammar and queries ([#5944](https://github.com/helix-editor/helix/pull/5944), [#6186](https://github.com/helix-editor/helix/pull/6186)) +- Improve DHall highlights ([#5959](https://github.com/helix-editor/helix/pull/5959)) +- Recognize `.env.dist` and `source.env` as env language ([#6003](https://github.com/helix-editor/helix/pull/6003)) +- Update tree-sitter-git-rebase ([#6030](https://github.com/helix-editor/helix/pull/6030), [#6094](https://github.com/helix-editor/helix/pull/6094)) +- Improve SQL highlights ([#6041](https://github.com/helix-editor/helix/pull/6041)) +- Improve markdown highlights and inject LaTeX ([#6100](https://github.com/helix-editor/helix/pull/6100)) +- Add textobject queries for Elm ([#6084](https://github.com/helix-editor/helix/pull/6084)) +- Recognize graphql schema file type ([#6159](https://github.com/helix-editor/helix/pull/6159)) +- Improve highlighting in comments ([#6143](https://github.com/helix-editor/helix/pull/6143)) +- Improve highlighting for JavaScript/TypeScript/ECMAScript languages ([#6205](https://github.com/helix-editor/helix/pull/6205)) +- Improve PHP highlights ([#6203](https://github.com/helix-editor/helix/pull/6203), [#6250](https://github.com/helix-editor/helix/pull/6250), [#6299](https://github.com/helix-editor/helix/pull/6299)) +- Improve Go highlights ([#6204](https://github.com/helix-editor/helix/pull/6204)) +- Highlight unchecked sqlx functions as SQL in Rust ([#6256](https://github.com/helix-editor/helix/pull/6256)) +- Improve Erlang highlights ([cdd6c8d](https://github.com/helix-editor/helix/commit/cdd6c8d)) +- Improve Nix highlights ([fb4d703](https://github.com/helix-editor/helix/commit/fb4d703)) +- Improve gdscript highlights ([#6311](https://github.com/helix-editor/helix/pull/6311)) +- Improve Vlang highlights ([#6279](https://github.com/helix-editor/helix/pull/6279)) +- Improve Makefile highlights ([#6339](https://github.com/helix-editor/helix/pull/6339)) +- Remove auto-pair for `'` in OCaml ([#6381](https://github.com/helix-editor/helix/pull/6381)) +- Fix indents in switch statements in ECMA languages ([#6369](https://github.com/helix-editor/helix/pull/6369)) +- Recognize xlb and storyboard file-types as XML ([#6407](https://github.com/helix-editor/helix/pull/6407)) +- Recognize cts and mts file-types as TypeScript ([#6424](https://github.com/helix-editor/helix/pull/6424)) +- Recognize SVG file-type as XML ([#6431](https://github.com/helix-editor/helix/pull/6431)) +- Add theme scopes for (un)checked list item markup scopes ([#6434](https://github.com/helix-editor/helix/pull/6434)) +- Update git commit grammar and add the comment textobject ([#6439](https://github.com/helix-editor/helix/pull/6439), [#6493](https://github.com/helix-editor/helix/pull/6493)) +- Recognize ARB file-type as JSON ([#6452](https://github.com/helix-editor/helix/pull/6452)) +- Inject markdown into markdown strings in Julia ([#6489](https://github.com/helix-editor/helix/pull/6489)) Packaging: -- Fix Nix flake devShell for darwin hosts (#5368) -- Add Appstream metadata file to `contrib/` (#5643) -- Increase the MSRV to 1.65 (#5570, #6185) -- Expose the Nix flake's `wrapper` (#5994) +- Fix Nix flake devShell for darwin hosts ([#5368](https://github.com/helix-editor/helix/pull/5368)) +- Add Appstream metadata file to `contrib/` ([#5643](https://github.com/helix-editor/helix/pull/5643)) +- Increase the MSRV to 1.65 ([#5570](https://github.com/helix-editor/helix/pull/5570), [#6185](https://github.com/helix-editor/helix/pull/6185)) +- Expose the Nix flake's `wrapper` ([#5994](https://github.com/helix-editor/helix/pull/5994)) # 22.12 (2022-12-06) -- 2.38.5 From 2f169b172fc16827cc1a2c95865d5c3ab17708f3 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 1 Apr 2023 11:43:56 +0200 Subject: [PATCH 140/191] Add `rm` command to delete the current file --- helix-term/src/commands/typed.rs | 31 +++++++++++++++++++++++++++++++ helix-view/src/document.rs | 15 +++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 0255bbea..a8c85303 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -298,6 +298,30 @@ fn force_buffer_close_all( buffer_close_by_ids_impl(cx, &document_ids, true) } +fn delete( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + cx.block_try_flush_writes()?; + let doc = doc_mut!(cx.editor); + + if doc.path().is_none() { + bail!("cannot delete a buffer with no associated file on the disk"); + } + + let doc_id = view!(cx.editor).doc; + + let future = doc.delete(); + cx.jobs.add(Job::new(future)); + + buffer_close_by_ids_impl(cx, &[doc_id], true) +} + fn buffer_next( cx: &mut compositor::Context, _args: &[Cow], @@ -2233,6 +2257,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: force_buffer_close_all, signature: CommandSignature::none(), }, + TypableCommand { + name: "delete", + aliases: &["remove", "rm", "del"], + doc: "Deletes the file associated with the current buffer", + fun: delete, + signature: CommandSignature::none(), + }, TypableCommand { name: "buffer-next", aliases: &["bn", "bnext"], diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index eca60026..bcc334ef 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -544,6 +544,21 @@ impl Document { } } + /// Deletes the file associated with this document + pub fn delete(&mut self) -> impl Future> { + let path = self + .path() + .expect("Cannot delete with no path set!") + .clone(); + + async move { + use tokio::fs; + fs::remove_file(path).await?; + + Ok(()) + } + } + /// If supported, returns the changes that should be applied to this document in order /// to format it nicely. // We can't use anyhow::Result here since the output of the future has to be -- 2.38.5 From 1d86a9bdcc7234f838046723d5be8bfb9a52c2d4 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 1 Apr 2023 11:47:36 +0200 Subject: [PATCH 141/191] Add dracula-purple theme --- runtime/themes/dracula-purple.toml | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 runtime/themes/dracula-purple.toml diff --git a/runtime/themes/dracula-purple.toml b/runtime/themes/dracula-purple.toml new file mode 100644 index 00000000..b28f9610 --- /dev/null +++ b/runtime/themes/dracula-purple.toml @@ -0,0 +1,82 @@ +# Author : Sam Sartor , Trivernis +# A port of https://github.com/bceskavich/dracula-at-night +"comment" = { fg = "comment" } +"constant" = { fg = "purple" } +"constant.character.escape" = { fg = "pink" } +"function" = { fg = "green" } +"keyword" = { fg = "pink" } +"operator" = { fg = "pink" } +"special" = { fg = "yellow" } +"punctuation" = { fg = "foreground" } +"string" = { fg = "yellow" } +"string.regexp" = { fg = "red" } +"tag" = { fg = "pink" } +"attribute" = { fg = "cyan" } +"type" = { fg = "cyan", modifiers = ["italic"] } +"type.enum.variant" = { fg = "foreground", modifiers = ["italic"] } +"variable" = { fg = "foreground" } +"variable.builtin" = { fg = "cyan", modifiers = ["italic"] } +"variable.parameter" = { fg ="orange", modifiers = ["italic"] } + +"diff.plus" = { fg = "green" } +"diff.delta" = { fg = "orange" } +"diff.minus" = { fg = "red" } + +"ui.background" = { fg = "foreground", bg = "background" } +"ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] } +"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] } +"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] } +"ui.cursorline" = {bg = "background_dark"} +"ui.help" = { fg = "foreground", bg = "background_dark" } +"ui.linenr" = { fg = "comment" } +"ui.linenr.selected" = { fg = "foreground" } +"ui.menu" = { fg = "foreground", bg = "background_dark" } +"ui.menu.selected" = { fg = "cyan", bg = "background_dark" } +"ui.popup" = { fg = "foreground", bg = "background_dark" } +"ui.selection" = { fg = "background", bg = "purple", modifiers = ["dim"] } +"ui.selection.primary" = { fg = "background", bg = "pink" } +"ui.text" = { fg = "foreground" } +"ui.text.focus" = { fg = "cyan" } +"ui.window" = { fg = "foreground" } +"ui.virtual.ruler" = { bg = "ruler" } +"ui.virtual.indent-guide" = { fg = "ruler" } + +"ui.statusline" = { fg = "foreground", bg = "background_dark" } +"ui.statusline.inactive" = { fg = "comment", bg = "background_dark" } +"ui.statusline.normal" = { fg = "background_dark", bg = "purple"} +"ui.statusline.insert" = { fg = "background_dark", bg = "pink"} +"ui.statusline.select" = { fg = "background_dark", bg = "cyan"} + +"error" = { fg = "red" } +"warning" = { fg = "cyan" } + +"markup.heading" = { fg = "purple", modifiers = ["bold"] } +"markup.list" = "cyan" +"markup.bold" = { fg = "orange", modifiers = ["bold"] } +"markup.italic" = { fg = "yellow", modifiers = ["italic"] } +"markup.link.url" = "cyan" +"markup.link.text" = "pink" +"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" } + +rainbow = ["#7c5ea3", "#9c5b95", "#9c5e80", "#6b4466"] + +[palette] +background = "#3A2A4D" +background_dark = "#2B1C3D" +foreground = "#f8f8f2" +ruler = "#453254" +comment = "#886C9C" +red = "#ff5555" +orange = "#ffb86c" +yellow = "#f1fa8c" +green = "#50fa7b" +purple = "#bd93f9" +cyan = "#8be9fd" +pink = "#ff79c6" \ No newline at end of file -- 2.38.5 From 432522e72407a8dc9cab78fc8f6cf5decb71ef10 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 1 Apr 2023 12:21:47 +0200 Subject: [PATCH 142/191] Add update of selected file in explorer when switching buffers --- helix-term/src/ui/editor.rs | 21 ++++++++++++++++++++- helix-term/src/ui/explorer.rs | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ab6d3430..d6f4fce5 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1496,7 +1496,26 @@ impl Component for EditorView { } if let Some(explore) = self.explorer.as_mut() { - if explore.is_focus() { + let needs_update = explore.is_focus() || { + if let Some(current_document_path) = doc!(cx.editor).path().cloned() { + if let Some(current_explore_path) = explore.current_file() { + if *current_explore_path != current_document_path { + let _ = explore.reveal_file(current_document_path); + + true + } else { + false + } + } else { + let _ = explore.reveal_file(current_document_path); + true + } + } else { + false + } + }; + + if needs_update { let area = if use_bufferline { area.clip_top(1) } else { diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index 76c43751..d527ee58 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -605,6 +605,11 @@ impl Explorer { } } + /// Returns the current file in the tree view + pub fn current_file(&self) -> Option<&PathBuf> { + self.tree.current_item().ok().map(|c| &c.path) + } + pub fn is_opened(&self) -> bool { self.state.open } -- 2.38.5 From bd32cb31144806d9849972490be8e4d077e228b7 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 1 Apr 2023 12:31:32 +0200 Subject: [PATCH 143/191] Add --show-explorer cli arg --- helix-term/src/application.rs | 22 ++++++++++++++++++---- helix-term/src/args.rs | 2 ++ helix-term/src/ui/explorer.rs | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 95faa01b..1a3e1c6d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -21,11 +21,11 @@ use tui::backend::Backend; use crate::{ args::Args, commands::apply_workspace_edit, - compositor::{Compositor, Event}, + compositor::{self, Compositor, Event}, config::Config, job::Jobs, keymap::Keymaps, - ui::{self, overlay::overlayed}, + ui::{self, overlay::overlayed, Explorer}, }; use log::{debug, error, warn}; @@ -155,7 +155,21 @@ impl Application { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + let mut editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + + let mut jobs = Jobs::new(); + + if args.show_explorer { + let mut context = compositor::Context { + editor: &mut editor, + scroll: None, + jobs: &mut jobs, + }; + let mut explorer = Explorer::new(&mut context)?; + explorer.unfocus(); + editor_view.explorer = Some(explorer); + } + compositor.push(editor_view); if args.load_tutor { @@ -244,7 +258,7 @@ impl Application { syn_loader, signals, - jobs: Jobs::new(), + jobs, lsp_progress: LspProgressMap::new(), last_render: Instant::now(), }; diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index dd787f1f..53920e29 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -10,6 +10,7 @@ pub struct Args { pub health: bool, pub health_arg: Option, pub load_tutor: bool, + pub show_explorer: bool, pub fetch_grammars: bool, pub build_grammars: bool, pub split: Option, @@ -32,6 +33,7 @@ impl Args { "--version" => args.display_version = true, "--help" => args.display_help = true, "--tutor" => args.load_tutor = true, + "--show-explorer" => args.show_explorer = true, "--vsplit" => match args.split { Some(_) => anyhow::bail!("can only set a split once of a specific type"), None => args.split = Some(Layout::Vertical), diff --git a/helix-term/src/ui/explorer.rs b/helix-term/src/ui/explorer.rs index d527ee58..91b18684 100644 --- a/helix-term/src/ui/explorer.rs +++ b/helix-term/src/ui/explorer.rs @@ -266,7 +266,7 @@ impl Explorer { self.state.open = true; } - fn unfocus(&mut self) { + pub fn unfocus(&mut self) { self.state.focus = false; } -- 2.38.5 From c3c87741d95c893980e70719dfb5f620bb6fde3e Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Sun, 2 Apr 2023 01:58:10 +0300 Subject: [PATCH 144/191] build(nix): update flake dependencies, remove deprecated code from flake --- flake.lock | 111 +++++++++++++++++++++++++++++++++++++++++------------ flake.nix | 2 - 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/flake.lock b/flake.lock index fa292273..d33c404e 100644 --- a/flake.lock +++ b/flake.lock @@ -18,9 +18,6 @@ }, "dream2nix": { "inputs": { - "alejandra": [ - "nci" - ], "all-cabal-json": [ "nci" ], @@ -28,6 +25,8 @@ "devshell": [ "nci" ], + "drv-parts": "drv-parts", + "flake-compat": "flake-compat", "flake-parts": [ "nci", "parts" @@ -51,6 +50,7 @@ "nci", "nixpkgs" ], + "nixpkgsV1": "nixpkgsV1", "poetry2nix": [ "nci" ], @@ -62,11 +62,11 @@ ] }, "locked": { - "lastModified": 1677289985, - "narHash": "sha256-lUp06cTTlWubeBGMZqPl9jODM99LpWMcwxRiscFAUJg=", + "lastModified": 1680258209, + "narHash": "sha256-lEo50RXI/17/a9aCIun8Hz62ZJ5JM5RGeTgclIP+Lgc=", "owner": "nix-community", "repo": "dream2nix", - "rev": "28b973a8d4c30cc1cbb3377ea2023a76bc3fb889", + "rev": "6f512b5a220fdb26bd3c659f7b55e4f052ec8b35", "type": "github" }, "original": { @@ -75,6 +75,54 @@ "type": "github" } }, + "drv-parts": { + "inputs": { + "flake-compat": [ + "nci", + "dream2nix", + "flake-compat" + ], + "flake-parts": [ + "nci", + "dream2nix", + "flake-parts" + ], + "nixpkgs": [ + "nci", + "dream2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1680172861, + "narHash": "sha256-QMyI338xRxaHFDlCXdLCtgelGQX2PdlagZALky4ZXJ8=", + "owner": "davhau", + "repo": "drv-parts", + "rev": "ced8a52f62b0a94244713df2225c05c85b416110", + "type": "github" + }, + "original": { + "owner": "davhau", + "repo": "drv-parts", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "locked": { "lastModified": 1659877975, @@ -119,11 +167,11 @@ ] }, "locked": { - "lastModified": 1677297103, - "narHash": "sha256-ArlJIbp9NGV9yvhZdV0SOUFfRlI/kHeKoCk30NbSiLc=", + "lastModified": 1680329418, + "narHash": "sha256-+KN0eQLSZvL1J0kDO8/fxv0UCHTyZCADLmpIfeeiSGo=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "a79272a2cb0942392bb3a5bf9a3ec6bc568795b2", + "rev": "98c1d2ff5155f0fee5d290f6b982cb990839d540", "type": "github" }, "original": { @@ -134,11 +182,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1677063315, - "narHash": "sha256-qiB4ajTeAOVnVSAwCNEEkoybrAlA+cpeiBxLobHndE8=", + "lastModified": 1680213900, + "narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "988cc958c57ce4350ec248d2d53087777f9e1949", + "rev": "e3652e0735fbec227f342712f180f4f21f0594f2", "type": "github" }, "original": { @@ -151,11 +199,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1675183161, - "narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=", + "lastModified": 1678375444, + "narHash": "sha256-XIgHfGvjFvZQ8hrkfocanCDxMefc/77rXeHvYdzBMc8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e", + "rev": "130fa0baaa2b93ec45523fdcde942f6844ee9f6e", "type": "github" }, "original": { @@ -166,6 +214,21 @@ "type": "github" } }, + "nixpkgsV1": { + "locked": { + "lastModified": 1678500271, + "narHash": "sha256-tRBLElf6f02HJGG0ZR7znMNFv/Uf7b2fFInpTHiHaSE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5eb98948b66de29f899c7fe27ae112a47964baf8", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.11", + "type": "indirect" + } + }, "parts": { "inputs": { "nixpkgs-lib": [ @@ -174,11 +237,11 @@ ] }, "locked": { - "lastModified": 1675933616, - "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=", + "lastModified": 1679737941, + "narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "47478a4a003e745402acf63be7f9a092d51b83d7", + "rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", "type": "github" }, "original": { @@ -192,11 +255,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1675933616, - "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=", + "lastModified": 1679737941, + "narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "47478a4a003e745402acf63be7f9a092d51b83d7", + "rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c", "type": "github" }, "original": { @@ -221,11 +284,11 @@ ] }, "locked": { - "lastModified": 1677292251, - "narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=", + "lastModified": 1680315536, + "narHash": "sha256-0AsBuKssJMbcRcw4HJQwJsUHhZxR5+gaf6xPQayhR44=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc", + "rev": "5c8c151bdd639074a0051325c16df1a64ee23497", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2ac76488..6dcaf6cc 100644 --- a/flake.nix +++ b/flake.nix @@ -123,8 +123,6 @@ then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment'' else "$RUSTFLAGS"; in { - # by default NCI adds rust-analyzer component, but helix toolchain doesn't have rust-analyzer - nci.toolchains.shell.components = ["rust-src" "rustfmt" "clippy"]; nci.projects."helix-project".relPath = ""; nci.crates."helix-term" = { overrides = { -- 2.38.5 From fc5e515b306ed71369b7cd0f5420afeaef23b7c5 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 31 Mar 2023 10:02:05 -0500 Subject: [PATCH 145/191] Enable aarch64-macos releases --- .github/workflows/release.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fa34e1f..74ab8be0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,12 +86,12 @@ jobs: target: x86_64-pc-windows-msvc cross: false # 23.03: build issues - # - build: aarch64-macos - # os: macos-latest - # rust: stable - # target: aarch64-apple-darwin - # cross: false - # skip_tests: true # x86_64 host can't run aarch64 code + - build: aarch64-macos + os: macos-latest + rust: stable + target: aarch64-apple-darwin + cross: false + skip_tests: true # x86_64 host can't run aarch64 code # - build: x86_64-win-gnu # os: windows-2019 # rust: stable-x86_64-gnu -- 2.38.5 From 6bfc3097412e54563f7bd6a6ff5fce5b4d9577e1 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 31 Mar 2023 10:05:52 -0500 Subject: [PATCH 146/191] Remove the rust-toolchain.toml file before building the release The 'dtolnay/rust-toolchain' action ignores the rust-toolchain.toml file, but the installed 'cargo' respects it. This can create a version mismatch if the MSRV is different from the stable rust version. Any additional targets installed by rustup like aarch64-darwin might not be installed for the correct version. To fix this, we remove the rust-toolchain.toml file before calling 'cargo'. --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74ab8be0..b26daca8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,6 +114,12 @@ jobs: mkdir -p runtime/grammars/sources tar xJf grammars/grammars.tar.xz -C runtime/grammars/sources + # The rust-toolchain action ignores rust-toolchain.toml files. + # Removing this before building with cargo ensures that the rust-toolchain + # is considered the same between installation and usage. + - name: Remove the rust-toolchain.toml file + run: rm rust-toolchain.toml + - name: Install ${{ matrix.rust }} toolchain uses: dtolnay/rust-toolchain@master with: -- 2.38.5 From 38b9bdf871d406df8e9a3f1ed8c34dae992c184e Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Fri, 31 Mar 2023 14:02:53 -0500 Subject: [PATCH 147/191] Recursive create the pkgname directory when creating a release tarball This step without the '-p' works fine for regular releases but it can fail if the CI is running when this file changes or on a branch matching 'patch/ci-release-*'. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b26daca8..b509ff9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -255,7 +255,7 @@ jobs: exe=".exe" fi pkgname=helix-$GITHUB_REF_NAME-$platform - mkdir $pkgname + mkdir -p $pkgname cp $source/LICENSE $source/README.md $pkgname mkdir $pkgname/contrib cp -r $source/contrib/completion $pkgname/contrib -- 2.38.5 From bfe8d267fec4964c6981ae38d9e4f46cdebb61b7 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 3 Apr 2023 03:56:48 +0200 Subject: [PATCH 148/191] normalize LSP workspaces (#6517) --- helix-lsp/src/client.rs | 4 +++- helix-lsp/src/lib.rs | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 94e99489..29a67988 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -4,7 +4,7 @@ use crate::{ Call, Error, OffsetEncoding, Result, }; -use helix_core::{find_workspace, ChangeSet, Rope}; +use helix_core::{find_workspace, path, ChangeSet, Rope}; use helix_loader::{self, VERSION_AND_GIT_HASH}; use lsp::{ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, @@ -66,6 +66,7 @@ impl Client { may_support_workspace: bool, ) -> bool { let (workspace, workspace_is_cwd) = find_workspace(); + let workspace = path::get_normalized_path(&workspace); let root = find_lsp_workspace( doc_path .and_then(|x| x.parent().and_then(|x| x.to_str())) @@ -201,6 +202,7 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id); let (workspace, workspace_is_cwd) = find_workspace(); + let workspace = path::get_normalized_path(&workspace); let root = find_lsp_workspace( doc_path .and_then(|x| x.parent().and_then(|x| x.to_str())) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index c3a5d816..c206ac1d 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -10,7 +10,10 @@ pub use lsp::{Position, Url}; pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; -use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration}; +use helix_core::{ + path, + syntax::{LanguageConfiguration, LanguageServerConfiguration}, +}; use tokio::sync::mpsc::UnboundedReceiver; use std::{ @@ -888,12 +891,13 @@ pub fn find_lsp_workspace( workspace_is_cwd: bool, ) -> Option { let file = std::path::Path::new(file); - let file = if file.is_absolute() { + let mut file = if file.is_absolute() { file.to_path_buf() } else { let current_dir = std::env::current_dir().expect("unable to determine current directory"); current_dir.join(file) }; + file = path::get_normalized_path(&file); if !file.starts_with(workspace) { return None; @@ -910,7 +914,7 @@ pub fn find_lsp_workspace( if root_dirs .iter() - .any(|root_dir| root_dir == ancestor.strip_prefix(workspace).unwrap()) + .any(|root_dir| path::get_normalized_path(&workspace.join(root_dir)) == ancestor) { // if the worskapce is the cwd do not search any higher for workspaces // but specify -- 2.38.5 From 1073dd632932d0c9131f6413a5ced69ee7096e60 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Mon, 3 Apr 2023 03:58:50 +0200 Subject: [PATCH 149/191] robustly handle invalid LSP ranges (#6512) --- helix-lsp/src/lib.rs | 25 ++++++++++++++++++++----- helix-term/src/ui/completion.rs | 16 ++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index c206ac1d..a59fa31e 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -132,7 +132,11 @@ pub mod util { ) -> Option { let pos_line = pos.line as usize; if pos_line > doc.len_lines() - 1 { - return None; + // If it extends past the end, truncate it to the end. This is because the + // way the LSP describes the range including the last newline is by + // specifying a line number after what we would call the last line. + log::warn!("LSP position {pos:?} out of range assuming EOF"); + return Some(doc.len_chars()); } // We need to be careful here to fully comply ith the LSP spec. @@ -242,9 +246,20 @@ pub mod util { pub fn lsp_range_to_range( doc: &Rope, - range: lsp::Range, + mut range: lsp::Range, offset_encoding: OffsetEncoding, ) -> Option { + // This is sort of an edgecase. It's not clear from the spec how to deal with + // ranges where end < start. They don't make much sense but vscode simply caps start to end + // and because it's not specified quite a few LS rely on this as a result (for example the TS server) + if range.start > range.end { + log::error!( + "Invalid LSP range start {:?} > end {:?}, using an empty range at the end instead", + range.start, + range.end + ); + range.start = range.end; + } let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?; let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?; @@ -951,16 +966,16 @@ mod tests { test_case!("", (0, 0) => Some(0)); test_case!("", (0, 1) => Some(0)); - test_case!("", (1, 0) => None); + test_case!("", (1, 0) => Some(0)); test_case!("\n\n", (0, 0) => Some(0)); test_case!("\n\n", (1, 0) => Some(1)); test_case!("\n\n", (1, 1) => Some(1)); test_case!("\n\n", (2, 0) => Some(2)); - test_case!("\n\n", (3, 0) => None); + test_case!("\n\n", (3, 0) => Some(2)); test_case!("test\n\n\n\ncase", (4, 3) => Some(11)); test_case!("test\n\n\n\ncase", (4, 4) => Some(12)); test_case!("test\n\n\n\ncase", (4, 5) => Some(12)); - test_case!("", (u32::MAX, u32::MAX) => None); + test_case!("", (u32::MAX, u32::MAX) => Some(0)); } #[test] diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index e0b1419c..bc216509 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -141,16 +141,12 @@ impl Completion { } }; - let start_offset = - match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) { - Some(start) => start as i128 - primary_cursor as i128, - None => return Transaction::new(doc.text()), - }; - let end_offset = - match util::lsp_pos_to_pos(doc.text(), edit.range.end, offset_encoding) { - Some(end) => end as i128 - primary_cursor as i128, - None => return Transaction::new(doc.text()), - }; + let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{ + return Transaction::new(doc.text()); + }; + + let start_offset = range.anchor as i128 - primary_cursor as i128; + let end_offset = range.head as i128 - primary_cursor as i128; (Some((start_offset, end_offset)), edit.new_text) } else { -- 2.38.5 From 9420ba7484b14d10d24edf7236852cc18d985dfb Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Mon, 3 Apr 2023 06:41:41 +0200 Subject: [PATCH 150/191] Let..else refactor (#6562) --- helix-term/src/commands/typed.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 0255bbea..afc3d706 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2125,20 +2125,16 @@ fn reset_diff_change( let scrolloff = editor.config().scrolloff; let (view, doc) = current!(editor); - // TODO refactor to use let..else once MSRV is raised to 1.65 - let handle = match doc.diff_handle() { - Some(handle) => handle, - None => bail!("Diff is not available in the current buffer"), + let Some(handle) = doc.diff_handle() else { + bail!("Diff is not available in the current buffer") }; let diff = handle.load(); let doc_text = doc.text().slice(..); let line = doc.selection(view.id).primary().cursor_line(doc_text); - // TODO refactor to use let..else once MSRV is raised to 1.65 - let hunk_idx = match diff.hunk_at(line as u32, true) { - Some(hunk_idx) => hunk_idx, - None => bail!("There is no change at the cursor"), + let Some(hunk_idx) = diff.hunk_at(line as u32, true) else { + bail!("There is no change at the cursor") }; let hunk = diff.nth_hunk(hunk_idx); let diff_base = diff.diff_base(); -- 2.38.5 From d63c717b82c690c6ecd3a9fae687b849eb533e91 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Mon, 3 Apr 2023 10:26:31 +0200 Subject: [PATCH 151/191] dracula theme: style inlay hints as comments (#6515) --- runtime/themes/dracula.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index b08357db..6935b487 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -45,6 +45,9 @@ "ui.virtual.whitespace" = { fg = "subtle" } "ui.virtual.wrap" = { fg = "subtle" } "ui.virtual.ruler" = { bg = "background_dark"} +"ui.virtual.inlay-hint" = { fg = "comment" } +"ui.virtual.inlay-hint.parameter" = { fg = "comment", modifiers = ["italic"] } +"ui.virtual.inlay-hint.type" = { fg = "comment", modifiers = ["italic"] } "error" = { fg = "red" } "warning" = { fg = "cyan" } -- 2.38.5 From 1fcfef12be3d9041092111523e6c9cb13d2f519c Mon Sep 17 00:00:00 2001 From: Jack Allison Date: Mon, 3 Apr 2023 04:34:19 -0400 Subject: [PATCH 152/191] Update OneDark theme to use light-gray for inlay hints. (#6503) Co-authored-by: Jack Allison --- runtime/themes/onedark.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 6df5f797..21101ea7 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -54,6 +54,7 @@ "ui.virtual.indent-guide" = { fg = "faint-gray" } "ui.virtual.whitespace" = { fg = "light-gray" } "ui.virtual.ruler" = { bg = "gray" } +"ui.virtual.inlay-hint" = { fg = "light-gray" } "ui.cursor" = { fg = "white", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } -- 2.38.5 From dd6e0cce3bcc6a4e57c5869f6a5ba36c101a17b3 Mon Sep 17 00:00:00 2001 From: Dmitry Ulyanov Date: Mon, 3 Apr 2023 17:22:43 +0300 Subject: [PATCH 153/191] Fix line number display for LSP goto pickers (#6559) Line numbers are 0-indexed in the LSP spec but 1-indexed for display and jumping purposes in Helix. --- helix-term/src/commands/lsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index f8e83a46..78dbc0be 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -81,7 +81,7 @@ impl ui::menu::Item for lsp::Location { // Most commonly, this will not allocate, especially on Unix systems where the root prefix // is a simple `/` and not `C:\` (with whatever drive letter) - write!(&mut res, ":{}", self.range.start.line) + write!(&mut res, ":{}", self.range.start.line + 1) .expect("Will only failed if allocating fail"); res.into() } -- 2.38.5 From d0c9f38b6836e6337a1a5c7c4c55c612b1e45e90 Mon Sep 17 00:00:00 2001 From: Bertrand Bousquet Date: Mon, 3 Apr 2023 15:56:21 +0000 Subject: [PATCH 154/191] Update Varua theme for soft wrap (#6568) --- runtime/themes/varua.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/varua.toml b/runtime/themes/varua.toml index c1afaca0..20ee235f 100644 --- a/runtime/themes/varua.toml +++ b/runtime/themes/varua.toml @@ -65,6 +65,7 @@ "ui.virtual.whitespace" = "grey0" "ui.statusline.insert" = { bg = "green", fg = "bg2" } "ui.statusline.select" = { bg = "blue", fg = "bg2" } +"ui.virtual.wrap" = { fg = "grey0" } "hint" = "blue" "info" = "aqua" -- 2.38.5 From 43072f78760777978da5c7a6426c66c20d7c4568 Mon Sep 17 00:00:00 2001 From: Yevgnen Date: Mon, 3 Apr 2023 23:57:38 +0800 Subject: [PATCH 155/191] Update colors for inlay hints for emacs theme (#6569) --- runtime/themes/emacs.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/emacs.toml b/runtime/themes/emacs.toml index bb33e2d8..513af60e 100644 --- a/runtime/themes/emacs.toml +++ b/runtime/themes/emacs.toml @@ -70,6 +70,7 @@ "ui.selection.primary" = { bg = "lightgoldenrod2" } "ui.virtual.whitespace" = "highlight" "ui.virtual.ruler" = { bg = "gray95" } +"ui.virtual.inlay-hint" = { fg = "gray75" } "ui.cursorline.primary" = { bg = "darkseagreen2" } "ui.cursorline.secondary" = { bg = "darkseagreen2" } -- 2.38.5 From 789833c9953f1639ff4c580de326d3bb7b9210f9 Mon Sep 17 00:00:00 2001 From: Rowan Shi <37481598+rowanxshi@users.noreply.github.com> Date: Mon, 3 Apr 2023 12:36:54 -0400 Subject: [PATCH 156/191] minor: R lang config update --slave to --no-echo (#6570) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index a8fdc4a8..38de820b 100644 --- a/languages.toml +++ b/languages.toml @@ -1446,7 +1446,7 @@ shebangs = ["r", "R"] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } -language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] } +language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] } [[grammar]] name = "r" -- 2.38.5 From 9d883873054f9fd4f9289dd80f06d66898524fa6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 08:59:13 +0800 Subject: [PATCH 157/191] build(deps): bump futures-executor from 0.3.27 to 0.3.28 (#6576) Bumps [futures-executor](https://github.com/rust-lang/futures-rs) from 0.3.27 to 0.3.28. - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.27...0.3.28) --- updated-dependencies: - dependency-name: futures-executor dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7ed2c9c..c3421fbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,15 +450,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -467,15 +467,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-task", -- 2.38.5 From dbafe756fa592bd684d5024bd993927159527251 Mon Sep 17 00:00:00 2001 From: Slug <106496265+GreasySlug@users.noreply.github.com> Date: Tue, 4 Apr 2023 22:59:12 +0900 Subject: [PATCH 159/191] Update base16_transparent and dark_high_contrast themes (#6577) * Update inlay-hint and wrap for base16_transparent * Update inlay-hint and wrap for dark_high_contrast * Tune dark_high_contrast cursor match theming --- runtime/themes/base16_transparent.toml | 4 ++++ runtime/themes/dark_high_contrast.toml | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/runtime/themes/base16_transparent.toml b/runtime/themes/base16_transparent.toml index fd07cb28..8019c6fa 100644 --- a/runtime/themes/base16_transparent.toml +++ b/runtime/themes/base16_transparent.toml @@ -25,6 +25,10 @@ "ui.virtual.ruler" = { bg = "gray" } "ui.virtual.whitespace" = "gray" "ui.virtual.indent-guide" = "gray" +"ui.virtual.inlay-hint" = { fg = "white", bg = "gray" } +"ui.virtual.inlay-hint.parameter" = { fg = "white", bg = "gray"} +"ui.virtual.inlay-hint.type" = { fg = "white", bg = "gray"} +"ui.virtual.wrap" = "gray" "variable" = "light-red" "constant.numeric" = "yellow" diff --git a/runtime/themes/dark_high_contrast.toml b/runtime/themes/dark_high_contrast.toml index 897c31e2..51701cfc 100644 --- a/runtime/themes/dark_high_contrast.toml +++ b/runtime/themes/dark_high_contrast.toml @@ -8,9 +8,14 @@ "ui.text" = "white" "ui.text.focus" = { modifiers = ["reversed"] } # file picker selected +"ui.virtual" = "gray" "ui.virtual.whitespace" = "gray" "ui.virtual.ruler" = { fg = "white", bg = "gray" } "ui.virtual.indent-guide" = "white" +"ui.virtual.inlay-hint" = { fg = "black", bg = "orange" } +"ui.virtual.inlay-hint.parameter" = { fg = "black", bg = "orange" } +"ui.virtual.inlay-hint.type" = { fg = "black", bg = "orange" } +"ui.virtual.wrap" = "gray" "ui.statusline" = { fg = "white", bg = "deep_blue" } "ui.statusline.inactive" = { fg = "gray", bg = "deep_blue" } @@ -22,7 +27,7 @@ "ui.cursor" = { fg = "black", bg = "white" } "ui.cursor.insert" = { fg = "black", bg = "white" } "ui.cursor.select" = { fg = "black", bg = "white" } -"ui.cursor.match" = { bg = "white", modifiers = ["dim"] } +"ui.cursor.match" = { modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "black", bg = "white", modifiers = ["slow_blink"] } "ui.cursor.secondary" = "white" "ui.cursorline.primary" = { bg = "deep_blue", underline = { color = "orange", style = "double_line" } } -- 2.38.5 From 480784d2cf56dbb86a194f42c6ae50a88f0f6ffb Mon Sep 17 00:00:00 2001 From: Rohit K Viswanath Date: Tue, 4 Apr 2023 19:35:13 +0530 Subject: [PATCH 160/191] Update inlay-hint color for mellow & rasmus themes (#6583) --- runtime/themes/mellow.toml | 1 + runtime/themes/rasmus.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/runtime/themes/mellow.toml b/runtime/themes/mellow.toml index 279fd5c2..16d7b608 100644 --- a/runtime/themes/mellow.toml +++ b/runtime/themes/mellow.toml @@ -80,6 +80,7 @@ "ui.virtual" = { fg = "gray02" } "ui.virtual.indent-guide" = { fg = "gray02" } +"ui.virtual.inlay-hint" = { fg = "gray04" } "ui.selection" = { bg = "gray03" } "ui.selection.primary" = { bg = "gray03" } diff --git a/runtime/themes/rasmus.toml b/runtime/themes/rasmus.toml index 3158a6a4..bcfb0c66 100644 --- a/runtime/themes/rasmus.toml +++ b/runtime/themes/rasmus.toml @@ -85,6 +85,7 @@ "ui.virtual" = { fg = "gray03" } "ui.virtual.indent-guide" = { fg = "gray04" } +"ui.virtual.inlay-hint" = { fg = "gray05" } "ui.selection" = { bg = "gray03" } "ui.selection.primary" = { bg = "gray03" } -- 2.38.5 From 2f4b9a47f36c04eae7c2a67445150337065aaeff Mon Sep 17 00:00:00 2001 From: Bertrand Bousquet Date: Tue, 4 Apr 2023 17:34:25 +0000 Subject: [PATCH 161/191] Update Varua theme for inlay hints (#6589) --- runtime/themes/varua.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/varua.toml b/runtime/themes/varua.toml index 20ee235f..b07ab08d 100644 --- a/runtime/themes/varua.toml +++ b/runtime/themes/varua.toml @@ -66,6 +66,7 @@ "ui.statusline.insert" = { bg = "green", fg = "bg2" } "ui.statusline.select" = { bg = "blue", fg = "bg2" } "ui.virtual.wrap" = { fg = "grey0" } +"ui.virtual.inlay-hint" = { fg = "grey1" } "hint" = "blue" "info" = "aqua" -- 2.38.5 From 577aded04a01801cbd61c7953a24b24d499a7e83 Mon Sep 17 00:00:00 2001 From: Jack Wolfard <31635014+JackWolfard@users.noreply.github.com> Date: Tue, 4 Apr 2023 12:25:06 -0700 Subject: [PATCH 162/191] Recognize CUDA files as C++ (#6521) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 38de820b..0d5b3222 100644 --- a/languages.toml +++ b/languages.toml @@ -212,7 +212,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-c", rev = "7175a6dd name = "cpp" scope = "source.cpp" injection-regex = "cpp" -file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"] +file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"] roots = [] comment-token = "//" language-server = { command = "clangd" } -- 2.38.5 From 01b70762fda6a7cd004ed623844398d620c44eda Mon Sep 17 00:00:00 2001 From: Constantin Angheloiu Date: Tue, 4 Apr 2023 22:47:23 +0300 Subject: [PATCH 163/191] Dim pane divider color in base16_transparent theme (#6534) --- runtime/themes/base16_transparent.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/themes/base16_transparent.toml b/runtime/themes/base16_transparent.toml index 8019c6fa..63aa1f86 100644 --- a/runtime/themes/base16_transparent.toml +++ b/runtime/themes/base16_transparent.toml @@ -9,7 +9,7 @@ "ui.linenr" = { fg = "light-gray" } "ui.linenr.selected" = { fg = "white", modifiers = ["bold"] } "ui.popup" = { fg = "white" } -"ui.window" = { fg = "white" } +"ui.window" = { fg = "gray" } "ui.selection" = { bg = "gray" } "comment" = "light-gray" "ui.statusline" = { fg = "white" } -- 2.38.5 From 531b745c54e5d316603601cc05ade50ab02066ab Mon Sep 17 00:00:00 2001 From: Anton Romanov Date: Tue, 4 Apr 2023 15:11:09 -0700 Subject: [PATCH 164/191] [theme][zenburn] set inlay hint to comment style (#6593) --- runtime/themes/zenburn.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/themes/zenburn.toml b/runtime/themes/zenburn.toml index 4c019a2c..8518e78f 100644 --- a/runtime/themes/zenburn.toml +++ b/runtime/themes/zenburn.toml @@ -8,7 +8,8 @@ "ui.popup" = { bg = "uibg" } "ui.selection" = { bg = "#304a3d" } "ui.selection.primary" = { bg = "#2f2f2f" } -"comment" = { fg = "#7f9f7f" } +"comment" = { fg = "comment" } +"ui.virtual.inlay-hint" = { fg = "comment" } "comment.block.documentation" = { fg = "black", modifiers = ["bold"] } "ui.statusline" = { bg = "statusbg", fg = "#ccdc90" } "ui.statusline.inactive" = { fg = '#2e3330', bg = '#88b090' } @@ -50,6 +51,7 @@ "error" = "errorfg" [palette] +comment = "#7f9f7f" bg = "#3f3f3f" uibg = "#2c2e2e" constant = "#dca3a3" -- 2.38.5 From b6909bc41ad998dac9a0a8198535ba0f21a32c22 Mon Sep 17 00:00:00 2001 From: Ivan Ermakov Date: Thu, 6 Apr 2023 10:28:08 +0700 Subject: [PATCH 165/191] Add gdformat support (#6614) --- languages.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/languages.toml b/languages.toml index 0d5b3222..48c1345c 100644 --- a/languages.toml +++ b/languages.toml @@ -1545,6 +1545,7 @@ file-types = ["gd"] shebangs = [] roots = ["project.godot"] auto-format = true +formatter = { command = "gdformat", args = ["-"] } comment-token = "#" indent = { tab-width = 4, unit = "\t" } -- 2.38.5 From fc4ca96c2986bc875f1fc6e2cb164dcaf4f4a09c Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Wed, 5 Apr 2023 22:29:17 -0500 Subject: [PATCH 166/191] Update tree-sitter to v0.20.10 (#6608) We used a git dependency to take advantage of the latest fixes in master but a new release is now available: https://crates.io/crates/tree-sitter/0.20.10 --- Cargo.lock | 5 +++-- Cargo.toml | 3 --- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3421fbb..26448c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2192,8 +2192,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.9" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" dependencies = [ "cc", "regex", diff --git a/Cargo.toml b/Cargo.toml index aaa21659..c6351889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,3 @@ inherits = "test" package.helix-core.opt-level = 2 package.helix-tui.opt-level = 2 package.helix-term.opt-level = 2 - -[patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" } -- 2.38.5 From 951e8686e8182ca9a914ea8ea864c7234349d391 Mon Sep 17 00:00:00 2001 From: Gyeongwan Koh Date: Fri, 7 Apr 2023 01:18:39 +0900 Subject: [PATCH 167/191] Colorize inlay hints in the boo_berry theme (#6625) --- runtime/themes/boo_berry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/boo_berry.toml b/runtime/themes/boo_berry.toml index 62e3b372..d3a3e223 100644 --- a/runtime/themes/boo_berry.toml +++ b/runtime/themes/boo_berry.toml @@ -52,6 +52,7 @@ "ui.virtual.whitespace" = { fg = "berry_desaturated" } "ui.virtual.ruler" = { bg = "berry_dim" } "ui.virtual.indent-guide" = { fg = "berry_fade" } +"ui.virtual.inlay-hint" = { fg = "berry_desaturated" } "diff.plus" = { fg = "mint" } "diff.delta" = { fg = "gold" } -- 2.38.5 From c22ebfe62ed0404a0f7328e3a5100c971f864004 Mon Sep 17 00:00:00 2001 From: Erasin Wang Date: Fri, 7 Apr 2023 00:26:41 +0800 Subject: [PATCH 168/191] Add Hurl Support (#6450) * Add http Support It's like [vscode-restclient](https://github.com/Huachao/vscode-restclient) - https://github.com/erasin/tree-sitter-http/tree/main/tests * Add Hurl Support --- book/src/generated/lang-support.md | 1 + languages.toml | 14 +++ runtime/queries/hurl/highlights.scm | 127 ++++++++++++++++++++++++++++ runtime/queries/hurl/indents.scm | 11 +++ runtime/queries/hurl/injections.scm | 14 +++ 5 files changed, 167 insertions(+) create mode 100644 runtime/queries/hurl/highlights.scm create mode 100644 runtime/queries/hurl/indents.scm create mode 100644 runtime/queries/hurl/injections.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 48667f2e..d71acc56 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -59,6 +59,7 @@ | heex | ✓ | ✓ | | `elixir-ls` | | hosts | ✓ | | | | | html | ✓ | | | `vscode-html-language-server` | +| hurl | ✓ | | ✓ | | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | | ini | ✓ | | | | diff --git a/languages.toml b/languages.toml index 48c1345c..c5c69b9b 100644 --- a/languages.toml +++ b/languages.toml @@ -2422,3 +2422,17 @@ language-server = { command = "nimlangserver" } [[grammar]] name = "nim" source = { git = "https://github.com/aMOPel/tree-sitter-nim", rev = "240239b232550e431d67de250d1b5856209e7f06" } + +[[language]] +name = "hurl" +scope = "source.hurl" +injection-regex = "hurl" +file-types = ["hurl"] +roots = [] +comment-token = "#" +indent = { tab-width = 2, unit = " " } + +[[grammar]] +name = "hurl" +source = { git = "https://github.com/pfeiferj/tree-sitter-hurl", rev = "264c42064b61ee21abe88d0061f29a0523352e22" } + diff --git a/runtime/queries/hurl/highlights.scm b/runtime/queries/hurl/highlights.scm new file mode 100644 index 00000000..c066b284 --- /dev/null +++ b/runtime/queries/hurl/highlights.scm @@ -0,0 +1,127 @@ +[ + "[QueryStringParams]" + "[FormParams]" + "[MultipartFormData]" + "[Cookies]" + "[Captures]" + "[Asserts]" + "[Options]" + "[BasicAuth]" +] @attribute + +(comment) @comment + +[ + (key_string) + (json_key_string) +] @variable.other.member + +(value_string) @string +(quoted_string) @string +(json_string) @string +(file_value) @string.special.path +(regex) @string.regex + +[ + "\\" + (regex_escaped_char) + (quoted_string_escaped_char) + (key_string_escaped_char) + (value_string_escaped_char) + (oneline_string_escaped_char) + (multiline_string_escaped_char) + (filename_escaped_char) + (json_string_escaped_char) +] @constant.character.escape + +(method) @type.builtin +(multiline_string_type) @type + +[ + "status" + "url" + "header" + "cookie" + "body" + "xpath" + "jsonpath" + "regex" + "variable" + "duration" + "sha256" + "md5" + "bytes" +] @function.builtin + +(filter) @attribute + +(version) @string.special +[ + "null" + "cacert" + "location" + "insecure" + "max-redirs" + "retry" + "retry-interval" + "retry-max-count" + (variable_option "variable") + "verbose" + "very-verbose" +] @constant.builtin + +(boolean) @constant.builtin.boolean + +(variable_name) @variable + +[ + "not" + "equals" + "==" + "notEquals" + "!=" + "greaterThan" + ">" + "greaterThanOrEquals" + ">=" + "lessThan" + "<" + "lessThanOrEquals" + "<=" + "startsWith" + "endsWith" + "contains" + "matches" + "exists" + "includes" + "isInteger" + "isFloat" + "isBoolean" + "isString" + "isCollection" +] @keyword.operator + +(integer) @constant.numeric.integer +(float) @constant.numeric.float +(status) @constant.numeric +(json_number) @constant.numeric.float + +[ + ":" + "," +] @punctuation.delimiter + +[ + "[" + "]" + "{" + "}" + "{{" + "}}" +] @punctuation.special + +[ + "base64," + "file," + "hex," +] @string.special \ No newline at end of file diff --git a/runtime/queries/hurl/indents.scm b/runtime/queries/hurl/indents.scm new file mode 100644 index 00000000..d436f76f --- /dev/null +++ b/runtime/queries/hurl/indents.scm @@ -0,0 +1,11 @@ +[ + (json_object) + (json_array) + (xml_tag) +] @indent + +[ + "}" + "]" + (xml_close_tag) +] @outdent diff --git a/runtime/queries/hurl/injections.scm b/runtime/queries/hurl/injections.scm new file mode 100644 index 00000000..a0d23817 --- /dev/null +++ b/runtime/queries/hurl/injections.scm @@ -0,0 +1,14 @@ +((comment) @injection.content + (#set! injection.language "comment")) + +((json_value) @injection.content + (#set! injection.language "json")) + +((xml) @injection.content + (#set! injection.language "xml")) + +((multiline_string + (multiline_string_type) @injection.language + (multiline_string_content) @injection.content) + (#set! injection.include-children) + (#set! injection.combined)) -- 2.38.5 From 7ce52e5b2c5fb1d8c9c5c783ec2072b6b9f97663 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Thu, 6 Apr 2023 18:30:47 +0200 Subject: [PATCH 169/191] Added `ferra` theme (#6619) * Added ferra theme * Updated with author information * Conform to themelint --- runtime/themes/ferra.toml | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 runtime/themes/ferra.toml diff --git a/runtime/themes/ferra.toml b/runtime/themes/ferra.toml new file mode 100644 index 00000000..da5339e2 --- /dev/null +++ b/runtime/themes/ferra.toml @@ -0,0 +1,84 @@ +# Author : Casper Rogild Storm + +"comment" = { fg = "ferra_bark", modifiers = ["italic"] } +"constant" = { fg = "ferra_sage" } +"function" = { fg = "ferra_coral" } +"function.macro" = { fg = "ferra_mist" } +"keyword" = { fg = "ferra_mist" } +"operator" = { fg = "ferra_mist" } +"punctuation" = { fg = "ferra_blush" } +"string" = { fg = "ferra_sage" } +"type" = { fg = "ferra_rose" } +"variable" = { fg = "ferra_blush" } +"variable.builtin" = { fg = "ferra_rose" } +"tag" = { fg = "ferra_sage" } +"label" = { fg = "ferra_sage" } +"attribute" = { fg = "ferra_blush" } +"namespace" = { fg = "ferra_blush" } +"module" = { fg = "ferra_blush" } + +"markup.heading" = { fg = "ferra_sage", modifiers = ["bold"] } +"markup.heading.marker" = { fg = "ferra_bark" } +"markup.list" = { fg = "ferra_mist" } +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.url" = { fg = "ferra_rose", modifiers = ["underlined"] } +"markup.link.text" = { fg = "ferra_rose" } +"markup.quote" = { fg = "ferra_bark" } +"markup.raw" = { fg = "ferra_coral" } + +"ui.background" = { bg = "ferra_night" } +"ui.cursor" = { fg = "ferra_night", bg = "ferra_blush" } +"ui.cursor.match" = { fg = "ferra_night", bg = "ferra_bark" } +"ui.cursor.select" = { fg = "ferra_night", bg = "ferra_rose" } +"ui.cursor.insert" = { fg = "ferra_night", bg = "ferra_coral" } +"ui.linenr" = { fg = "ferra_bark" } +"ui.linenr.selected" = { fg = "ferra_blush" } +"ui.cursorline" = { fg = "ferra_blush", bg = "ferra_ash" } +"ui.statusline" = { fg = "ferra_blush", bg = "ferra_ash" } +"ui.statusline.inactive" = { fg = "ferra_bark", bg = "ferra_ash" } +"ui.statusline.normal" = { fg = "ferra_ash", bg = "ferra_blush" } +"ui.statusline.insert" = { fg = "ferra_ash", bg = "ferra_coral" } +"ui.statusline.select" = { fg = "ferra_ash", bg = "ferra_rose" } +"ui.popup" = { fg = "ferra_blush", bg = "ferra_ash" } +"ui.window" = { fg = "ferra_bark", bg = "ferra_night" } +"ui.help" = { fg = "ferra_blush", bg = "ferra_ash" } +"ui.text" = { fg = "ferra_blush" } +"ui.text.focus" = { fg = "ferra_coral" } +"ui.menu" = { fg = "ferra_blush", bg = "ferra_ash" } +"ui.menu.selected" = { fg = "ferra_coral", bg = "ferra_ash" } +"ui.selection" = { bg = "ferra_umber", fg = "ferra_night" } +"ui.selection.primary" = { bg = "ferra_night", fg = "ferra_umber" } +"ui.virtual" = { fg = "ferra_bark" } +"ui.virtual.whitespace" = { fg = "ferra_bark" } +"ui.virtual.ruler" = { fg = "ferra_night", bg = "ferra_ash" } +"ui.virtual.indent-guide" = { fg = "ferra_ash" } +"ui.virtual.inlay-hint" = { fg = "ferra_bark" } + +"diff.plus" = { fg = "ferra_sage" } +"diff.delta" = { fg = "ferra_blush" } +"diff.minus" = { fg = "ferra_ember" } + +"error" = { fg = "ferra_ember" } +"warning" = { fg = "ferra_honey" } +"info" = { fg = "ferra_blush" } +"hint" = { fg = "ferra_blush" } + +"diagnostic.warning" = { underline = { color = "ferra_honey", style = "curl" } } +"diagnostic.error" = { underline = { color = "ferra_ember", style = "curl" } } +"diagnostic.info" = { underline = { color = "ferra_blush", style = "curl" } } +"diagnostic.hint" = { underline = { color = "ferra_blush", style = "curl" } } + +[palette] +ferra_night = "#2b292d" +ferra_ash = "#383539" +ferra_umber = "#4d424b" +ferra_bark = "#6F5D63" +ferra_mist = "#D1D1E0" +ferra_sage = "#B1B695" +ferra_blush = "#fecdb2" +ferra_coral = "#ffa07a" +ferra_rose = "#F6B6C9" +ferra_ember = "#e06b75" +ferra_honey = "#F5D76E" -- 2.38.5 From 4b32b544fc57c53e1406fa7705068f0222498019 Mon Sep 17 00:00:00 2001 From: Clara Hobbs Date: Thu, 6 Apr 2023 12:35:05 -0400 Subject: [PATCH 170/191] Add textobject queries for Julia (#6588) * Add textobjects queries for Julia * Update docs for Julia textobject queries --- book/src/generated/lang-support.md | 2 +- runtime/queries/julia/textobjects.scm | 46 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 runtime/queries/julia/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index d71acc56..12cd2bab 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -69,7 +69,7 @@ | json | ✓ | | ✓ | `vscode-json-language-server` | | jsonnet | ✓ | | | `jsonnet-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` | -| julia | ✓ | | ✓ | `julia` | +| julia | ✓ | ✓ | ✓ | `julia` | | kdl | ✓ | | | | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | diff --git a/runtime/queries/julia/textobjects.scm b/runtime/queries/julia/textobjects.scm new file mode 100644 index 00000000..1927c2b1 --- /dev/null +++ b/runtime/queries/julia/textobjects.scm @@ -0,0 +1,46 @@ +(function_definition (_)? @function.inside) @function.around + +(short_function_definition (_)? @function.inside) @function.around + +(macro_definition (_)? @function.inside) @function.around + +(struct_definition (_)? @class.inside) @class.around + +(abstract_definition (_)? @class.inside) @class.around + +(primitive_definition (_)? @class.inside) @class.around + +(parameter_list + ; Match all children of parameter_list *except* keyword_parameters + ([(identifier) + (slurp_parameter) + (optional_parameter) + (typed_parameter) + (tuple_expression) + (interpolation_expression) + (call_expression)] + @parameter.inside . ","? @parameter.around) @parameter.around) + +(keyword_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_parameter_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(line_comment) @comment.inside + +(line_comment)+ @comment.around + +(block_comment) @comment.inside + +(block_comment)+ @comment.around + +(_expression (macro_identifier + (identifier) @_name + (#match? @_name "^(test|test_throws|test_logs|inferred|test_deprecated|test_warn|test_nowarn|test_broken|test_skip)$") + ) + . + (macro_argument_list) @test.inside) @test.around -- 2.38.5 From 3dd715a115880831e3a0f75a3c00f0b6e1a8364f Mon Sep 17 00:00:00 2001 From: Danillo Melo Date: Thu, 6 Apr 2023 13:37:45 -0300 Subject: [PATCH 171/191] Update Ruby Highlights (#6587) * update ruby highlights * Updated SQL injection.scm * Move private, public, protected to builtin methods --- languages.toml | 2 +- runtime/queries/ruby/highlights.scm | 120 +++++++++++++++++++--------- runtime/queries/ruby/injections.scm | 6 ++ 3 files changed, 89 insertions(+), 39 deletions(-) diff --git a/languages.toml b/languages.toml index c5c69b9b..a1a2f0a1 100644 --- a/languages.toml +++ b/languages.toml @@ -606,7 +606,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "ruby" -source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "4c600a463d97e36a0ca5ac57e11f3ac8c297a0fa" } +source = { git = "https://github.com/tree-sitter/tree-sitter-ruby", rev = "206c7077164372c596ffa8eaadb9435c28941364" } [[language]] name = "bash" diff --git a/runtime/queries/ruby/highlights.scm b/runtime/queries/ruby/highlights.scm index 898f8f79..7c69276b 100644 --- a/runtime/queries/ruby/highlights.scm +++ b/runtime/queries/ruby/highlights.scm @@ -1,44 +1,67 @@ ; Keywords [ + "BEGIN" + "END" "alias" - "and" "begin" - "break" - "case" "class" - "def" "do" - "else" - "elsif" "end" - "ensure" - "for" - "if" - "in" "module" - "next" - "or" + "in" "rescue" - "retry" - "return" - "then" - "unless" - "until" + "ensure" +] @keyword + +[ + "if" + "else" + "elsif" "when" + "case" + "unless" + "then" +] @keyword.control.conditional + +[ + "for" "while" + "retry" + "until" + "redo" +] @keyword.control.repeat + +[ "yield" -] @keyword + "return" + "next" + "break" +] @keyword.control.return + +[ + "def" + "undef" +] @keyword.function + +((identifier) @keyword.control.import + (#match? @keyword.control.import "^(require|require_relative|load|autoload)$")) + +[ + "or" + "and" + "not" +] @keyword.operator -((identifier) @keyword - (#match? @keyword "^(private|protected|public)$")) +((identifier) @keyword.control.exception + (#match? @keyword.control.exception "^(raise|fail)$")) ; Function calls -((identifier) @function.method.builtin - (#eq? @function.method.builtin "require")) +((identifier) @function.builtin + (#match? @function.builtin "^(attr|attr_accessor|attr_reader|attr_writer|include|prepend|refine|private|protected|public)$")) -"defined?" @function.method.builtin +"defined?" @function.builtin (call method: [(identifier) (constant)] @function.method) @@ -58,7 +81,10 @@ ] @variable.other.member ((identifier) @constant.builtin - (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) + (#match? @constant.builtin "^(__FILE__|__LINE__|__ENCODING__)$")) + +((constant) @constant.builtin + (#match? @constant.builtin "^(ENV|ARGV|ARGF|RUBY_PLATFORM|RUBY_RELEASE_DATE|RUBY_VERSION|STDERR|STDIN|STDOUT|TOPLEVEL_BINDING)$")) ((constant) @constant (#match? @constant "^[A-Z\\d_]+$")) @@ -66,22 +92,23 @@ (constant) @constructor (self) @variable.builtin -(super) @variable.builtin +(super) @function.builtin +[(forward_parameter)(forward_argument)] @variable.parameter +(keyword_parameter name:((_)":" @variable.parameter) @variable.parameter) +(optional_parameter name:((_)"=" @operator) @variable.parameter) +(optional_parameter name: (identifier) @variable.parameter) +(splat_parameter name: (identifier) @variable.parameter) @variable.parameter +(hash_splat_parameter name: (identifier) @variable.parameter) @variable.parameter +(method_parameters (identifier) @variable.parameter) (block_parameter (identifier) @variable.parameter) (block_parameters (identifier) @variable.parameter) -(destructured_parameter (identifier) @variable.parameter) -(hash_splat_parameter (identifier) @variable.parameter) -(lambda_parameters (identifier) @variable.parameter) -(method_parameters (identifier) @variable.parameter) -(splat_parameter (identifier) @variable.parameter) - -(keyword_parameter name: (identifier) @variable.parameter) -(optional_parameter name: (identifier) @variable.parameter) ((identifier) @function.method (#is-not? local)) -(identifier) @variable +[ + (identifier) +] @variable ; Literals @@ -96,10 +123,11 @@ [ (simple_symbol) (delimited_symbol) - (hash_key_symbol) (bare_symbol) ] @string.special.symbol +(pair key: ((_)":" @string.special.symbol) @string.special.symbol) + (regex) @string.regexp (escape_sequence) @constant.character.escape @@ -112,7 +140,7 @@ (nil) (true) (false) -]@constant.builtin +] @constant.builtin (interpolation "#{" @punctuation.special @@ -121,20 +149,36 @@ (comment) @comment ; Operators - [ -"=" +":" +"?" +"~" "=>" "->" +"!" ] @operator +(assignment + "=" @operator) + +(operator_assignment + operator: ["+=" "-=" "*=" "**=" "/=" "||=" "|=" "&&=" "&=" "%=" ">>=" "<<=" "^="] @operator) + +(binary + operator: ["/" "|" "==" "===" "||" "&&" ">>" "<<" "<" ">" "<=" ">=" "&" "^" "!~" "=~" "<=>" "**" "*" "!=" "%" "-" "+"] @operator) + +(range + operator: [".." "..."] @operator) + [ "," ";" "." + "&." ] @punctuation.delimiter [ + "|" "(" ")" "[" diff --git a/runtime/queries/ruby/injections.scm b/runtime/queries/ruby/injections.scm index 321c90ad..1a865df1 100644 --- a/runtime/queries/ruby/injections.scm +++ b/runtime/queries/ruby/injections.scm @@ -1,2 +1,8 @@ ((comment) @injection.content (#set! injection.language "comment")) + +((heredoc_body + (heredoc_content) @injection.content + (heredoc_end) @name + (#set! injection.language "sql")) + (#eq? @name "SQL")) -- 2.38.5 From b663b89529b40d00d4db580901cccacdd35c63cf Mon Sep 17 00:00:00 2001 From: Michael <20937441+KMikeeU@users.noreply.github.com> Date: Thu, 6 Apr 2023 21:48:10 +0200 Subject: [PATCH 172/191] xml: highlight .xsd as XML files (#6631) xsd or "XML Schema Definition" files are in XML format and should therefore be highlighted as such --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index a1a2f0a1..51ef3bcf 100644 --- a/languages.toml +++ b/languages.toml @@ -2069,7 +2069,7 @@ source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d1 name = "xml" scope = "source.xml" injection-regex = "xml" -file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg"] +file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd"] indent = { tab-width = 2, unit = " " } roots = [] -- 2.38.5 From 1148ce1fd9941e00bd416bce1f06a987d0e7b5f2 Mon Sep 17 00:00:00 2001 From: karei <31644842+kareigu@users.noreply.github.com> Date: Fri, 7 Apr 2023 03:19:48 +0300 Subject: [PATCH 173/191] Add support for Robot Framework files (#6611) * Add support for Robot Framework files * Run docgen --- book/src/generated/lang-support.md | 1 + languages.toml | 14 ++++++++++++++ runtime/queries/robot/highlights.scm | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 runtime/queries/robot/highlights.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 12cd2bab..3f56dd60 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -117,6 +117,7 @@ | rego | ✓ | | | `regols` | | rescript | ✓ | ✓ | | `rescript-language-server` | | rmarkdown | ✓ | | ✓ | `R` | +| robot | ✓ | | | `robotframework_ls` | | ron | ✓ | | ✓ | | | rst | ✓ | | | | | ruby | ✓ | ✓ | ✓ | `solargraph` | diff --git a/languages.toml b/languages.toml index 51ef3bcf..c643beab 100644 --- a/languages.toml +++ b/languages.toml @@ -1437,6 +1437,20 @@ comment-token = "//" indent = { tab-width = 4, unit = " " } grammar = "rust" +[[language]] +name = "robot" +scope = "source.robot" +injection-regex = "robot" +file-types = ["robot", "resource"] +comment-token = "#" +roots = [] +indent = { tab-width = 4, unit = " " } +language-server = { command = "robotframework_ls" } + +[[grammar]] +name = "robot" +source = { git = "https://github.com/Hubro/tree-sitter-robot", rev = "f1142bfaa6acfce95e25d2c6d18d218f4f533927" } + [[language]] name = "r" scope = "source.r" diff --git a/runtime/queries/robot/highlights.scm b/runtime/queries/robot/highlights.scm new file mode 100644 index 00000000..60f416b7 --- /dev/null +++ b/runtime/queries/robot/highlights.scm @@ -0,0 +1,21 @@ +(comment) @comment +(ellipses) @punctuation.delimiter + +(section_header) @keyword +(extra_text) @comment + +(setting_statement) @keyword + +(variable_definition (variable_name) @variable) + +(keyword_definition (name) @function) +(keyword_definition (body (keyword_setting) @keyword)) + +(test_case_definition (name) @property) + +(keyword_invocation (keyword) @function) + +(argument (text_chunk) @string) +(argument (scalar_variable) @string.special) +(argument (list_variable) @string.special) +(argument (dictionary_variable) @string.special) -- 2.38.5 From e856906f766aa6d58aba6f6bca9e2e1879b1629d Mon Sep 17 00:00:00 2001 From: Daniel Sedlak Date: Fri, 7 Apr 2023 17:10:38 +0200 Subject: [PATCH 174/191] Fix typos (#6643) --- helix-core/src/position.rs | 4 ++-- helix-core/src/selection.rs | 2 +- helix-core/src/shellwords.rs | 4 ++-- helix-core/src/text_annotations.rs | 4 ++-- helix-lsp/src/client.rs | 14 +++++++------- helix-lsp/src/lib.rs | 6 +++--- helix-lsp/src/snippet.rs | 12 ++++++------ helix-term/src/application.rs | 4 ++-- helix-term/src/commands.rs | 22 +++++++++++----------- helix-term/src/commands/dap.rs | 4 ++-- helix-term/src/commands/lsp.rs | 26 +++++++++++++------------- helix-term/src/commands/typed.rs | 4 ++-- helix-term/src/ui/document.rs | 8 ++++---- helix-term/src/ui/fuzzy_match.rs | 6 +++--- helix-term/src/ui/fuzzy_match/test.rs | 4 ++-- helix-term/src/ui/overlay.rs | 2 +- helix-term/src/ui/picker.rs | 2 +- helix-term/tests/test/movement.rs | 2 +- helix-tui/src/backend/crossterm.rs | 6 +++--- helix-view/src/editor.rs | 2 +- helix-view/src/theme.rs | 2 +- 21 files changed, 70 insertions(+), 70 deletions(-) diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 04bf8c31..ee764bc6 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -109,7 +109,7 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po /// softwrapping positions are estimated with an O(1) algorithm /// to ensure consistent performance for large lines (currently unimplemented) /// -/// Usualy you want to use `visual_offset_from_anchor` instead but this function +/// Usually you want to use `visual_offset_from_anchor` instead but this function /// can be useful (and faster) if /// * You already know the visual position of the block /// * You only care about the horizontal offset (column) and not the vertical offset (row) @@ -291,7 +291,7 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) /// /// If no (text) grapheme starts at exactly at the specified column the /// start of the grapheme to the left is returned. If there is no grapheme -/// to the left (for example if the line starts with virtual text) then the positiong +/// to the left (for example if the line starts with virtual text) then the positioning /// of the next grapheme to the right is returned. /// /// If the `line` coordinate is beyond the end of the file, the EOF diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 8e93c633..259b131a 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -38,7 +38,7 @@ use std::borrow::Cow; /// Ranges are considered to be inclusive on the left and /// exclusive on the right, regardless of anchor-head ordering. /// This means, for example, that non-zero-width ranges that -/// are directly adjecent, sharing an edge, do not overlap. +/// are directly adjacent, sharing an edge, do not overlap. /// However, a zero-width range will overlap with the shared /// left-edge of another range. /// diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 0883eb91..9d873c36 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -294,14 +294,14 @@ mod test { #[test] fn test_lists() { let input = - r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#; + r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "quotes"]'"#; let shellwords = Shellwords::from(input); let result = shellwords.words().to_vec(); let expected = vec![ Cow::from(":set"), Cow::from("statusline.center"), Cow::from(r#"["file-type","file-encoding"]"#), - Cow::from(r#"["list", "in", "qoutes"]"#), + Cow::from(r#"["list", "in", "quotes"]"#), ]; assert_eq!(expected, result); } diff --git a/helix-core/src/text_annotations.rs b/helix-core/src/text_annotations.rs index e6093184..11d19d48 100644 --- a/helix-core/src/text_annotations.rs +++ b/helix-core/src/text_annotations.rs @@ -172,7 +172,7 @@ impl TextAnnotations { for char_idx in char_range { if let Some((_, Some(highlight))) = self.overlay_at(char_idx) { // we don't know the number of chars the original grapheme takes - // however it doesn't matter as highlight bounderies are automatically + // however it doesn't matter as highlight boundaries are automatically // aligned to grapheme boundaries in the rendering code highlights.push((highlight.0, char_idx..char_idx + 1)) } @@ -203,7 +203,7 @@ impl TextAnnotations { /// Add new grapheme overlays. /// - /// The overlayed grapheme will be rendered with `highlight` + /// The overlaid grapheme will be rendered with `highlight` /// patched on top of `ui.text`. /// /// The overlays **must be sorted** by their `char_idx`. diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 29a67988..93e822c4 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -52,8 +52,8 @@ pub struct Client { root_path: std::path::PathBuf, root_uri: Option, workspace_folders: Mutex>, - initalize_notify: Arc, - /// workspace folders added while the server is still initalizing + initialize_notify: Arc, + /// workspace folders added while the server is still initializing req_timeout: u64, } @@ -92,14 +92,14 @@ impl Client { return true; } - // this server definitly doesn't support multiple workspace, no need to check capabilities + // this server definitely doesn't support multiple workspace, no need to check capabilities if !may_support_workspace { return false; } let Some(capabilities) = self.capabilities.get() else { let client = Arc::clone(self); - // initalization hasn't finished yet, deal with this new root later + // initialization hasn't finished yet, deal with this new root later // TODO: In the edgecase that a **new root** is added // for an LSP that **doesn't support workspace_folders** before initaliation is finished // the new roots are ignored. @@ -108,7 +108,7 @@ impl Client { // documents LSP client handle. It's doable but a pretty weird edgecase so let's // wait and see if anyone ever runs into it. tokio::spawn(async move { - client.initalize_notify.notified().await; + client.initialize_notify.notified().await; if let Some(workspace_folders_caps) = client .capabilities() .workspace @@ -234,7 +234,7 @@ impl Client { root_path, root_uri, workspace_folders: Mutex::new(workspace_folders), - initalize_notify: initialize_notify.clone(), + initialize_notify: initialize_notify.clone(), }; Ok((client, server_rx, initialize_notify)) @@ -279,7 +279,7 @@ impl Client { "utf-16" => Some(OffsetEncoding::Utf16), "utf-32" => Some(OffsetEncoding::Utf32), encoding => { - log::error!("Server provided invalid position encording {encoding}, defaulting to utf-16"); + log::error!("Server provided invalid position encoding {encoding}, defaulting to utf-16"); None }, }) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index a59fa31e..31ee1d75 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -152,10 +152,10 @@ pub mod util { // > ‘\n’, ‘\r\n’ and ‘\r’. Positions are line end character agnostic. // > So you can not specify a position that denotes \r|\n or \n| where | represents the character offset. // - // This means that while the line must be in bounds the `charater` + // This means that while the line must be in bounds the `character` // must be capped to the end of the line. // Note that the end of the line here is **before** the line terminator - // so we must use `line_end_char_index` istead of `doc.line_to_char(pos_line + 1)` + // so we must use `line_end_char_index` instead of `doc.line_to_char(pos_line + 1)` // // FIXME: Helix does not fully comply with the LSP spec for line terminators. // The LSP standard requires that line terminators are ['\n', '\r\n', '\r']. @@ -893,7 +893,7 @@ fn start_client( /// * if the file is outside `workspace` return `None` /// * start at `file` and search the file tree upward /// * stop the search at the first `root_dirs` entry that contains `file` -/// * if no `root_dirs` matchs `file` stop at workspace +/// * if no `root_dirs` matches `file` stop at workspace /// * Returns the top most directory that contains a `root_marker` /// * If no root marker and we stopped at a `root_dirs` entry, return the directory we stopped at /// * If we stopped at `workspace` instead and `workspace_is_cwd == false` return `None` diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index a4f049e8..ebf3da24 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -61,7 +61,7 @@ fn render_elements( offset: &mut usize, tabstops: &mut Vec<(usize, (usize, usize))>, newline_with_offset: &str, - include_placeholer: bool, + include_placeholder: bool, ) { use SnippetElement::*; @@ -89,7 +89,7 @@ fn render_elements( offset, tabstops, newline_with_offset, - include_placeholer, + include_placeholder, ); } &Tabstop { tabstop } => { @@ -100,14 +100,14 @@ fn render_elements( value: inner_snippet_elements, } => { let start_offset = *offset; - if include_placeholer { + if include_placeholder { render_elements( inner_snippet_elements, insert, offset, tabstops, newline_with_offset, - include_placeholer, + include_placeholder, ); } tabstops.push((*tabstop, (start_offset, *offset))); @@ -127,7 +127,7 @@ fn render_elements( pub fn render( snippet: &Snippet<'_>, newline_with_offset: &str, - include_placeholer: bool, + include_placeholder: bool, ) -> (Tendril, Vec>) { let mut insert = Tendril::new(); let mut tabstops = Vec::new(); @@ -139,7 +139,7 @@ pub fn render( &mut offset, &mut tabstops, newline_with_offset, - include_placeholer, + include_placeholder, ); // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 130a74af..f7d7fa63 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -25,7 +25,7 @@ use crate::{ config::Config, job::Jobs, keymap::Keymaps, - ui::{self, overlay::overlayed}, + ui::{self, overlay::overlaid}, }; use log::{debug, error, warn}; @@ -169,7 +169,7 @@ impl Application { std::env::set_current_dir(first).context("set current dir")?; editor.new_file(Action::VerticalSplit); let picker = ui::file_picker(".".into(), &config.load().editor); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); } else { let nr_of_files = args.files.len(); for (i, (file, pos)) in args.files.into_iter().enumerate() { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b55f1ab7..17669924 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -54,7 +54,7 @@ use crate::{ job::Callback, keymap::ReverseKeymap, ui::{ - self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlayed, FilePicker, Picker, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, }, }; @@ -1561,7 +1561,7 @@ fn half_page_down(cx: &mut Context) { } #[allow(deprecated)] -// currently uses the deprected `visual_coords_at_pos`/`pos_at_visual_coords` functions +// currently uses the deprecated `visual_coords_at_pos`/`pos_at_visual_coords` functions // as this function ignores softwrapping (and virtual text) and instead only cares // about "text visual position" // @@ -2147,7 +2147,7 @@ fn global_search(cx: &mut Context) { Some((path.clone().into(), Some((*line_num, *line_num)))) }, ); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); }, )); Ok(call) @@ -2421,7 +2421,7 @@ fn append_mode(cx: &mut Context) { fn file_picker(cx: &mut Context) { let root = find_workspace().0; let picker = ui::file_picker(root, &cx.editor.config()); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } fn file_picker_in_current_buffer_directory(cx: &mut Context) { @@ -2438,12 +2438,12 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) { }; let picker = ui::file_picker(path, &cx.editor.config()); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } fn file_picker_in_current_directory(cx: &mut Context) { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")); let picker = ui::file_picker(cwd, &cx.editor.config()); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } fn buffer_picker(cx: &mut Context) { @@ -2508,7 +2508,7 @@ fn buffer_picker(cx: &mut Context) { Some((meta.id.into(), Some((line, line)))) }, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } fn jumplist_picker(cx: &mut Context) { @@ -2590,7 +2590,7 @@ fn jumplist_picker(cx: &mut Context) { Some((meta.path.clone()?.into(), Some((line, line)))) }, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } impl ui::menu::Item for MappableCommand { @@ -2664,7 +2664,7 @@ pub fn command_palette(cx: &mut Context) { } } }); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); }, )); } @@ -4185,7 +4185,7 @@ pub fn completion(cx: &mut Context) { None => return, }; - // setup a chanel that allows the request to be canceled + // setup a channel that allows the request to be canceled let (tx, rx) = oneshot::channel(); // set completion_request so that this request can be canceled // by setting completion_request, the old channel stored there is dropped @@ -4238,7 +4238,7 @@ pub fn completion(cx: &mut Context) { let (view, doc) = current_ref!(editor); // check if the completion request is stale. // - // Completions are completed asynchrounsly and therefore the user could + // Completions are completed asynchronously and therefore the user could //switch document/view or leave insert mode. In all of thoise cases the // completion should be discarded if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc { diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index dac1e9d5..8efdc9cf 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -2,7 +2,7 @@ use super::{Context, Editor}; use crate::{ compositor::{self, Compositor}, job::{Callback, Jobs}, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, + ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, }; use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; @@ -270,7 +270,7 @@ pub fn dap_launch(cx: &mut Context) { let templates = config.templates.clone(); - cx.push_layer(Box::new(overlayed(Picker::new( + cx.push_layer(Box::new(overlaid(Picker::new( templates, (), |cx, template, _action| { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 78dbc0be..7a26b3cf 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -26,7 +26,7 @@ use helix_view::{ use crate::{ compositor::{self, Compositor}, ui::{ - self, lsp::SignatureHelp, overlay::overlayed, DynamicPicker, FileLocation, FilePicker, + self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker, Popup, PromptEvent, }, }; @@ -372,7 +372,7 @@ pub fn symbol_picker(cx: &mut Context) { }; let picker = sym_picker(symbols, current_url, offset_encoding); - compositor.push(Box::new(overlayed(picker))) + compositor.push(Box::new(overlaid(picker))) } }, ) @@ -431,7 +431,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { future.boxed() }; let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols)); - compositor.push(Box::new(overlayed(dyn_picker))) + compositor.push(Box::new(overlaid(dyn_picker))) }, ) } @@ -454,7 +454,7 @@ pub fn diagnostics_picker(cx: &mut Context) { DiagnosticsFormat::HideSourcePath, offset_encoding, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } } @@ -471,7 +471,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { DiagnosticsFormat::ShowSourcePath, offset_encoding, ); - cx.push_layer(Box::new(overlayed(picker))); + cx.push_layer(Box::new(overlaid(picker))); } impl ui::menu::Item for lsp::CodeActionOrCommand { @@ -491,7 +491,7 @@ impl ui::menu::Item for lsp::CodeActionOrCommand { /// /// While the `kind` field is defined as open ended in the LSP spec (any value may be used) /// in practice a closed set of common values (mostly suggested in the LSP spec) are used. -/// VSCode displays each of these categories seperatly (seperated by a heading in the codeactions picker) +/// VSCode displays each of these categories separately (separated by a heading in the codeactions picker) /// to make them easier to navigate. Helix does not display these headings to the user. /// However it does sort code actions by their categories to achieve the same order as the VScode picker, /// just without the headings. @@ -521,7 +521,7 @@ fn action_category(action: &CodeActionOrCommand) -> u32 { } } -fn action_prefered(action: &CodeActionOrCommand) -> bool { +fn action_preferred(action: &CodeActionOrCommand) -> bool { matches!( action, CodeActionOrCommand::CodeAction(CodeAction { @@ -600,12 +600,12 @@ pub fn code_action(cx: &mut Context) { } // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec. - // Many details are modeled after vscode because langauge servers are usually tested against it. + // Many details are modeled after vscode because language servers are usually tested against it. // VScode sorts the codeaction two times: // // First the codeactions that fix some diagnostics are moved to the front. // If both codeactions fix some diagnostics (or both fix none) the codeaction - // that is marked with `is_preffered` is shown first. The codeactions are then shown in seperate + // that is marked with `is_preferred` is shown first. The codeactions are then shown in separate // submenus that only contain a certain category (see `action_category`) of actions. // // Below this done in in a single sorting step @@ -627,10 +627,10 @@ pub fn code_action(cx: &mut Context) { return order; } - // if one of the codeactions is marked as prefered show it first + // if one of the codeactions is marked as preferred show it first // otherwise keep the original LSP sorting - action_prefered(action1) - .cmp(&action_prefered(action2)) + action_preferred(action1) + .cmp(&action_preferred(action2)) .reverse() }); @@ -955,7 +955,7 @@ fn goto_impl( }, move |_editor, location| Some(location_to_file_location(location)), ); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); } } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index afc3d706..3c954d20 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -116,7 +116,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> let call: job::Callback = job::Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { let picker = ui::file_picker(path, &editor.config()); - compositor.push(Box::new(overlayed(picker))); + compositor.push(Box::new(overlaid(picker))); }, )); Ok(call) @@ -1335,7 +1335,7 @@ fn lsp_workspace_command( let picker = ui::Picker::new(commands, (), |cx, command, _action| { execute_lsp_command(cx.editor, command.clone()); }); - compositor.push(Box::new(overlayed(picker))) + compositor.push(Box::new(overlaid(picker))) }, )); Ok(call) diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index 39c20950..80da1c54 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -118,7 +118,7 @@ pub fn render_document( fn translate_positions( char_pos: usize, - first_visisble_char_idx: usize, + first_visible_char_idx: usize, translated_positions: &mut [TranslatedPosition], text_fmt: &TextFormat, renderer: &mut TextRenderer, @@ -126,7 +126,7 @@ fn translate_positions( ) { // check if any positions translated on the fly (like cursor) has been reached for (char_idx, callback) in &mut *translated_positions { - if *char_idx < char_pos && *char_idx >= first_visisble_char_idx { + if *char_idx < char_pos && *char_idx >= first_visible_char_idx { // by replacing the char_index with usize::MAX large number we ensure // that the same position is only translated once // text will never reach usize::MAX as rust memory allocations are limited @@ -259,7 +259,7 @@ pub fn render_text<'t>( } } - // aquire the correct grapheme style + // acquire the correct grapheme style if char_pos >= style_span.1 { style_span = styles.next().unwrap_or((Style::default(), usize::MAX)); } @@ -404,7 +404,7 @@ impl<'a> TextRenderer<'a> { let cut_off_start = self.col_offset.saturating_sub(position.col); let is_whitespace = grapheme.is_whitespace(); - // TODO is it correct to apply the whitspace style to all unicode white spaces? + // TODO is it correct to apply the whitespace style to all unicode white spaces? if is_whitespace { style = style.patch(self.whitespace_style); } diff --git a/helix-term/src/ui/fuzzy_match.rs b/helix-term/src/ui/fuzzy_match.rs index b406702f..22dc3a7f 100644 --- a/helix-term/src/ui/fuzzy_match.rs +++ b/helix-term/src/ui/fuzzy_match.rs @@ -54,7 +54,7 @@ impl QueryAtom { } fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec) -> bool { - // for inverse there are no indicies to return + // for inverse there are no indices to return // just return whether we matched if self.inverse { return self.matches(matcher, item); @@ -120,7 +120,7 @@ enum QueryAtomKind { /// /// Usage: `foo` Fuzzy, - /// Item contains query atom as a continous substring + /// Item contains query atom as a continuous substring /// /// Usage `'foo` Substring, @@ -213,7 +213,7 @@ impl FuzzyQuery { Some(score) } - pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec)> { + pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec)> { let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else( || Some((0, Vec::new())), |atom| matcher.fuzzy_indices(item, atom), diff --git a/helix-term/src/ui/fuzzy_match/test.rs b/helix-term/src/ui/fuzzy_match/test.rs index 3f90ef68..5df79eeb 100644 --- a/helix-term/src/ui/fuzzy_match/test.rs +++ b/helix-term/src/ui/fuzzy_match/test.rs @@ -7,8 +7,8 @@ fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec { items .iter() .filter_map(|item| { - let (_, indicies) = query.fuzzy_indicies(item, &matcher)?; - let matched_string = indicies + let (_, indices) = query.fuzzy_indices(item, &matcher)?; + let matched_string = indices .iter() .map(|&pos| item.chars().nth(pos).unwrap()) .collect(); diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs index 5b2bc806..ff184d40 100644 --- a/helix-term/src/ui/overlay.rs +++ b/helix-term/src/ui/overlay.rs @@ -16,7 +16,7 @@ pub struct Overlay { } /// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom -pub fn overlayed(content: T) -> Overlay { +pub fn overlaid(content: T) -> Overlay { Overlay { content, calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)), diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e73088e5..e7a7de90 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -794,7 +794,7 @@ impl Component for Picker { // might be inconsistencies. This is the best we can do since only the // text in Row is displayed to the end user. let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) - .fuzzy_indicies(&line, &self.matcher) + .fuzzy_indices(&line, &self.matcher) .unwrap_or_default(); let highlight_byte_ranges: Vec<_> = line diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index e10ec6f5..9a48cdbc 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -391,7 +391,7 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn cursor_position_append_eof() -> anyhow::Result<()> { - // Selection is fowards + // Selection is forwards test(( "#[foo|]#", "abar", diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 4d44f187..9d70a9fb 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -344,9 +344,9 @@ impl ModifierDiff { } } -/// Crossterm uses semicolon as a seperator for colors -/// this is actually not spec compliant (altough commonly supported) -/// However the correct approach is to use colons as a seperator. +/// Crossterm uses semicolon as a separator for colors +/// this is actually not spec compliant (although commonly supported) +/// However the correct approach is to use colons as a separator. /// This usually doesn't make a difference for emulators that do support colored underlines. /// However terminals that do not support colored underlines will ignore underlines colors with colons /// while escape sequences with semicolons are always processed which leads to weird visual artifacts. diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 34c59b9b..80c47ed0 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -851,7 +851,7 @@ pub struct Editor { pub config_events: (UnboundedSender, UnboundedReceiver), /// Allows asynchronous tasks to control the rendering /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw - /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired + /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be acquired pub redraw_handle: RedrawHandle, pub needs_redraw: bool, /// Cached position of the cursor calculated during rendering. diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 5d79ff26..a8cc5926 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -128,7 +128,7 @@ impl Loader { let parent_palette = parent_theme_toml.get("palette"); let palette = theme_toml.get("palette"); - // handle the table seperately since it needs a `merge_depth` of 2 + // handle the table separately since it needs a `merge_depth` of 2 // this would conflict with the rest of the theme merge strategy let palette_values = match (parent_palette, palette) { (Some(parent_palette), Some(palette)) => { -- 2.38.5 From af88a3c15cc4ddbb1e2f2ea6492868ffb8aba40b Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 8 Apr 2023 14:18:11 +0200 Subject: [PATCH 175/191] Fix #6605: Remove soft-wrap.enable option wrapping. (#6656) Co-authored-by: gibbz00 --- helix-core/src/syntax.rs | 2 +- helix-view/src/document.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 40846967..0e6696db 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -555,7 +555,7 @@ impl LanguageConfiguration { #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SoftWrap { /// Soft wrap lines that exceed viewport width. Default to off - pub enable: Option, + pub enable: bool, /// Maximum space left free at the end of the line. /// This space is used to wrap text at word boundaries. If that is not possible within this limit /// the word is simply split at the end of the line. diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index eca60026..65a5a6e2 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1444,9 +1444,8 @@ impl Document { .as_ref() .and_then(|config| config.soft_wrap.as_ref()); let enable_soft_wrap = language_soft_wrap - .and_then(|soft_wrap| soft_wrap.enable) - .or(editor_soft_wrap.enable) - .unwrap_or(false); + .map(|soft_wrap| soft_wrap.enable) + .unwrap_or_else(|| editor_soft_wrap.enable); let max_wrap = language_soft_wrap .and_then(|soft_wrap| soft_wrap.max_wrap) .or(config.soft_wrap.max_wrap) -- 2.38.5 From 25858ec2e3265a2cfc562e39136beb76df77953b Mon Sep 17 00:00:00 2001 From: Jan Scheer Date: Sat, 8 Apr 2023 14:18:33 +0200 Subject: [PATCH 176/191] themes: add inlay-hint to nightfox (#6655) --- runtime/themes/nightfox.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/themes/nightfox.toml b/runtime/themes/nightfox.toml index 8cddbbae..fad56d18 100644 --- a/runtime/themes/nightfox.toml +++ b/runtime/themes/nightfox.toml @@ -33,6 +33,7 @@ "ui.virtual.ruler" = { bg = "bg3" } # Vertical rulers (colored columns in editing area). "ui.virtual.whitespace" = { fg = "bg3" } # Whitespace markers in editing area. "ui.virtual.indent-guide" = { fg = "black" } # Vertical indent width guides +"ui.virtual.inlay-hint" = { fg = "comment", bg = "bg2" } # Default style for inlay hints of all kinds "ui.statusline" = { fg = "fg2", bg = "bg0" } # Status line. "ui.statusline.inactive" = { fg = "fg3", bg = "bg0" } # Status line in unfocused windows. -- 2.38.5 From 58e457a4e1037caf43c17dea93c5321c1ae74103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sat, 8 Apr 2023 21:50:43 +0900 Subject: [PATCH 177/191] Revert "Fix #6605: Remove soft-wrap.enable option wrapping. (#6656)" This caused a bug that would ignore the global config. This reverts commit af88a3c15cc4ddbb1e2f2ea6492868ffb8aba40b. --- helix-core/src/syntax.rs | 4 +++- helix-view/src/document.rs | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 0e6696db..c34ea81a 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -555,7 +555,9 @@ impl LanguageConfiguration { #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SoftWrap { /// Soft wrap lines that exceed viewport width. Default to off - pub enable: bool, + // NOTE: Option on purpose because the struct is shared between language config and global config. + // By default the option is None so that the language config falls back to the global config unless explicitly set. + pub enable: Option, /// Maximum space left free at the end of the line. /// This space is used to wrap text at word boundaries. If that is not possible within this limit /// the word is simply split at the end of the line. diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 65a5a6e2..eca60026 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1444,8 +1444,9 @@ impl Document { .as_ref() .and_then(|config| config.soft_wrap.as_ref()); let enable_soft_wrap = language_soft_wrap - .map(|soft_wrap| soft_wrap.enable) - .unwrap_or_else(|| editor_soft_wrap.enable); + .and_then(|soft_wrap| soft_wrap.enable) + .or(editor_soft_wrap.enable) + .unwrap_or(false); let max_wrap = language_soft_wrap .and_then(|soft_wrap| soft_wrap.max_wrap) .or(config.soft_wrap.max_wrap) -- 2.38.5 From d4c3609c439b108cbd879c9d302e8597958607d1 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 16:45:05 +0100 Subject: [PATCH 178/191] wip: generalised `load_theme` into `load_inheritable_toml` --- helix-view/src/theme.rs | 89 +++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index a8cc5926..327389a1 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -61,7 +61,19 @@ impl Loader { } let mut visited_paths = HashSet::new(); - let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?; + let default_themes = HashMap::from([ + ("default", &DEFAULT_THEME_DATA), + ("base16_default", &BASE16_DEFAULT_THEME_DATA), + ]); + let theme = self + .load_inheritable_toml( + name, + &self.theme_dirs, + &mut visited_paths, + &default_themes, + Self::merge_themes, + ) + .map(Theme::from)?; Ok(Theme { name: name.into(), @@ -69,43 +81,55 @@ impl Loader { }) } - /// Recursively load a theme, merging with any inherited parent themes. + /// Recursively load a TOML document, merging with any inherited parent files. /// /// The paths that have been visited in the inheritance hierarchy are tracked /// to detect and avoid cycling. /// /// It is possible for one file to inherit from another file with the same name - /// so long as the second file is in a themes directory with lower priority. + /// so long as the second file is in a search directory with lower priority. /// However, it is not recommended that users do this as it will make tracing /// errors more difficult. - fn load_theme(&self, name: &str, visited_paths: &mut HashSet) -> Result { - let path = self.path(name, visited_paths)?; - - let theme_toml = self.load_toml(path)?; - - let inherits = theme_toml.get("inherits"); - - let theme_toml = if let Some(parent_theme_name) = inherits { - let parent_theme_name = parent_theme_name.as_str().ok_or_else(|| { + fn load_inheritable_toml( + &self, + name: &str, + search_directories: &[PathBuf], + visited_paths: &mut HashSet, + default_toml_data: &HashMap<&str, &Lazy>, + merge_toml_docs: fn(Value, Value) -> Value, + ) -> Result { + let path = self.path(name, search_directories, visited_paths)?; + + let toml_doc = self.load_toml(&path)?; + + let inherits = toml_doc.get("inherits"); + + let toml_doc = if let Some(parent_toml_name) = inherits { + let parent_toml_name = parent_toml_name.as_str().ok_or_else(|| { anyhow!( - "Theme: expected 'inherits' to be a string: {}", - parent_theme_name + "{:?}: expected 'inherits' to be a string: {}", + path, + parent_toml_name ) })?; - let parent_theme_toml = match parent_theme_name { - // load default themes's toml from const. - "default" => DEFAULT_THEME_DATA.clone(), - "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(), - _ => self.load_theme(parent_theme_name, visited_paths)?, + let parent_toml_doc = match default_toml_data.get(parent_toml_name) { + Some(p) => (**p).clone(), + None => self.load_inheritable_toml( + parent_toml_name, + search_directories, + visited_paths, + default_toml_data, + merge_toml_docs, + )?, }; - self.merge_themes(parent_theme_toml, theme_toml) + merge_toml_docs(parent_toml_doc, toml_doc) } else { - theme_toml + toml_doc }; - Ok(theme_toml) + Ok(toml_doc) } pub fn read_names(path: &Path) -> Vec { @@ -124,7 +148,7 @@ impl Loader { } // merge one theme into the parent theme - fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value { + fn merge_themes(parent_theme_toml: Value, theme_toml: Value) -> Value { let parent_palette = parent_theme_toml.get("palette"); let palette = theme_toml.get("palette"); @@ -149,22 +173,27 @@ impl Loader { merge_toml_values(theme, palette.into(), 1) } - // Loads the theme data as `toml::Value` - fn load_toml(&self, path: PathBuf) -> Result { + // Loads the TOML data as `toml::Value` + fn load_toml(&self, path: &Path) -> Result { let data = std::fs::read_to_string(path)?; let value = toml::from_str(&data)?; Ok(value) } - /// Returns the path to the theme with the given name + /// Returns the path to the TOML document with the given name /// /// Ignores paths already visited and follows directory priority order. - fn path(&self, name: &str, visited_paths: &mut HashSet) -> Result { + fn path( + &self, + name: &str, + search_directories: &[PathBuf], + visited_paths: &mut HashSet, + ) -> Result { let filename = format!("{}.toml", name); let mut cycle_found = false; // track if there was a path, but it was in a cycle - self.theme_dirs + search_directories .iter() .find_map(|dir| { let path = dir.join(&filename); @@ -181,9 +210,9 @@ impl Loader { }) .ok_or_else(|| { if cycle_found { - anyhow!("Theme: cycle found in inheriting: {}", name) + anyhow!("Toml: cycle found in inheriting: {}", name) } else { - anyhow!("Theme: file not found for: {}", name) + anyhow!("Toml: file not found for: {}", name) } }) } -- 2.38.5 From 7d6b2cbbf68922511934eb8edf7b7e71d4545d04 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 17:17:11 +0100 Subject: [PATCH 179/191] =?UTF-8?q?wip:=20moved=20`load=5Finheritable=5Fto?= =?UTF-8?q?ml`,=20`path`=20(=E2=86=92=20`get=5Ftoml=5Fpath`),=20and=20`loa?= =?UTF-8?q?d=5Ftoml`=20from=20theme.rs=20to=20the=20`helix-loader`=20modul?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helix-loader/src/lib.rs | 103 ++++++++++++++++++++++++++++++++++-- helix-view/src/theme.rs | 114 ++++------------------------------------ 2 files changed, 109 insertions(+), 108 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 6c716975..2abc7377 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,8 +1,14 @@ pub mod config; pub mod grammar; +use anyhow::{anyhow, Result}; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; -use std::path::{Path, PathBuf}; +use once_cell::sync::Lazy; +use std::{ + collections::{HashMap, HashSet}, + path::{Path, PathBuf}, +}; +use toml::Value; pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); @@ -154,8 +160,6 @@ pub fn log_file() -> PathBuf { /// where one usually wants to override or add to the array instead of /// replacing it altogether. pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { - use toml::Value; - fn get_name(v: &Value) -> Option<&str> { v.get("name").and_then(Value::as_str) } @@ -209,6 +213,99 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi } } +/// Recursively load a TOML document, merging with any inherited parent files. +/// +/// The paths that have been visited in the inheritance hierarchy are tracked +/// to detect and avoid cycling. +/// +/// It is possible for one file to inherit from another file with the same name +/// so long as the second file is in a search directory with lower priority. +/// However, it is not recommended that users do this as it will make tracing +/// errors more difficult. +pub fn load_inheritable_toml( + name: &str, + search_directories: &[PathBuf], + visited_paths: &mut HashSet, + default_toml_data: &HashMap<&str, &Lazy>, + merge_toml_docs: fn(Value, Value) -> Value, +) -> Result { + let path = get_toml_path(name, search_directories, visited_paths)?; + + let toml_doc = load_toml(&path)?; + + let inherits = toml_doc.get("inherits"); + + let toml_doc = if let Some(parent_toml_name) = inherits { + let parent_toml_name = parent_toml_name.as_str().ok_or_else(|| { + anyhow!( + "{:?}: expected 'inherits' to be a string: {}", + path, + parent_toml_name + ) + })?; + + let parent_toml_doc = match default_toml_data.get(parent_toml_name) { + Some(p) => (**p).clone(), + None => load_inheritable_toml( + parent_toml_name, + search_directories, + visited_paths, + default_toml_data, + merge_toml_docs, + )?, + }; + + merge_toml_docs(parent_toml_doc, toml_doc) + } else { + toml_doc + }; + + Ok(toml_doc) +} + +/// Returns the path to the TOML document with the given name +/// +/// Ignores paths already visited and follows directory priority order. +fn get_toml_path( + name: &str, + search_directories: &[PathBuf], + visited_paths: &mut HashSet, +) -> Result { + let filename = format!("{}.toml", name); + + let mut cycle_found = false; // track if there was a path, but it was in a cycle + search_directories + .iter() + .find_map(|dir| { + let path = dir.join(&filename); + if !path.exists() { + None + } else if visited_paths.contains(&path) { + // Avoiding cycle, continuing to look in lower priority directories + cycle_found = true; + None + } else { + visited_paths.insert(path.clone()); + Some(path) + } + }) + .ok_or_else(|| { + if cycle_found { + anyhow!("Toml: cycle found in inheriting: {}", name) + } else { + anyhow!("Toml: file not found for: {}", name) + } + }) +} + +// Loads the TOML data as `toml::Value` +fn load_toml(path: &Path) -> Result { + let data = std::fs::read_to_string(path)?; + let value = toml::from_str(&data)?; + + Ok(value) +} + #[cfg(test)] mod merge_toml_tests { use std::str; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 327389a1..47bcc4b8 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -4,7 +4,7 @@ use std::{ str, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use helix_core::hashmap; use helix_loader::merge_toml_values; use log::warn; @@ -65,15 +65,14 @@ impl Loader { ("default", &DEFAULT_THEME_DATA), ("base16_default", &BASE16_DEFAULT_THEME_DATA), ]); - let theme = self - .load_inheritable_toml( - name, - &self.theme_dirs, - &mut visited_paths, - &default_themes, - Self::merge_themes, - ) - .map(Theme::from)?; + let theme = helix_loader::load_inheritable_toml( + name, + &self.theme_dirs, + &mut visited_paths, + &default_themes, + Self::merge_themes, + ) + .map(Theme::from)?; Ok(Theme { name: name.into(), @@ -81,57 +80,6 @@ impl Loader { }) } - /// Recursively load a TOML document, merging with any inherited parent files. - /// - /// The paths that have been visited in the inheritance hierarchy are tracked - /// to detect and avoid cycling. - /// - /// It is possible for one file to inherit from another file with the same name - /// so long as the second file is in a search directory with lower priority. - /// However, it is not recommended that users do this as it will make tracing - /// errors more difficult. - fn load_inheritable_toml( - &self, - name: &str, - search_directories: &[PathBuf], - visited_paths: &mut HashSet, - default_toml_data: &HashMap<&str, &Lazy>, - merge_toml_docs: fn(Value, Value) -> Value, - ) -> Result { - let path = self.path(name, search_directories, visited_paths)?; - - let toml_doc = self.load_toml(&path)?; - - let inherits = toml_doc.get("inherits"); - - let toml_doc = if let Some(parent_toml_name) = inherits { - let parent_toml_name = parent_toml_name.as_str().ok_or_else(|| { - anyhow!( - "{:?}: expected 'inherits' to be a string: {}", - path, - parent_toml_name - ) - })?; - - let parent_toml_doc = match default_toml_data.get(parent_toml_name) { - Some(p) => (**p).clone(), - None => self.load_inheritable_toml( - parent_toml_name, - search_directories, - visited_paths, - default_toml_data, - merge_toml_docs, - )?, - }; - - merge_toml_docs(parent_toml_doc, toml_doc) - } else { - toml_doc - }; - - Ok(toml_doc) - } - pub fn read_names(path: &Path) -> Vec { std::fs::read_dir(path) .map(|entries| { @@ -173,50 +121,6 @@ impl Loader { merge_toml_values(theme, palette.into(), 1) } - // Loads the TOML data as `toml::Value` - fn load_toml(&self, path: &Path) -> Result { - let data = std::fs::read_to_string(path)?; - let value = toml::from_str(&data)?; - - Ok(value) - } - - /// Returns the path to the TOML document with the given name - /// - /// Ignores paths already visited and follows directory priority order. - fn path( - &self, - name: &str, - search_directories: &[PathBuf], - visited_paths: &mut HashSet, - ) -> Result { - let filename = format!("{}.toml", name); - - let mut cycle_found = false; // track if there was a path, but it was in a cycle - search_directories - .iter() - .find_map(|dir| { - let path = dir.join(&filename); - if !path.exists() { - None - } else if visited_paths.contains(&path) { - // Avoiding cycle, continuing to look in lower priority directories - cycle_found = true; - None - } else { - visited_paths.insert(path.clone()); - Some(path) - } - }) - .ok_or_else(|| { - if cycle_found { - anyhow!("Toml: cycle found in inheriting: {}", name) - } else { - anyhow!("Toml: file not found for: {}", name) - } - }) - } - pub fn default_theme(&self, true_color: bool) -> Theme { if true_color { self.default() -- 2.38.5 From 55de40768157448d1787400599da3dbd5c99445c Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 17:23:06 +0100 Subject: [PATCH 180/191] wip: documented and moved `theme::Loader::read_names` to `helix_loader::read_toml_names` --- helix-loader/src/lib.rs | 16 ++++++++++++++++ helix-term/src/ui/mod.rs | 5 ++--- helix-view/src/theme.rs | 17 +---------------- xtask/src/themelint.rs | 6 ++---- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 2abc7377..62d55f14 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -306,6 +306,22 @@ fn load_toml(path: &Path) -> Result { Ok(value) } +/// Returns the names of the TOML documents within a directory +pub fn read_toml_names(path: &Path) -> Vec { + std::fs::read_dir(path) + .map(|entries| { + entries + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "toml") + .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) + }) + .collect() + }) + .unwrap_or_default() +} + #[cfg(test)] mod merge_toml_tests { use std::str; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3e9a14b0..bd8b888d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -239,7 +239,6 @@ pub mod completers { use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; use helix_view::document::SCRATCH_BUFFER_NAME; - use helix_view::theme; use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; @@ -280,9 +279,9 @@ pub mod completers { } pub fn theme(_editor: &Editor, input: &str) -> Vec { - let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); + let mut names = helix_loader::read_toml_names(&helix_loader::config_dir().join("themes")); for rt_dir in helix_loader::runtime_dirs() { - names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); + names.extend(helix_loader::read_toml_names(&rt_dir.join("themes"))); } names.push("default".into()); names.push("base16_default".into()); diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 47bcc4b8..78e81435 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,6 +1,6 @@ use std::{ collections::{HashMap, HashSet}, - path::{Path, PathBuf}, + path::PathBuf, str, }; @@ -80,21 +80,6 @@ impl Loader { }) } - pub fn read_names(path: &Path) -> Vec { - std::fs::read_dir(path) - .map(|entries| { - entries - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - (path.extension()? == "toml") - .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) - }) - .collect() - }) - .unwrap_or_default() - } - // merge one theme into the parent theme fn merge_themes(parent_theme_toml: Value, theme_toml: Value) -> Value { let parent_palette = parent_theme_toml.get("palette"); diff --git a/xtask/src/themelint.rs b/xtask/src/themelint.rs index f7efb7d9..16a6c841 100644 --- a/xtask/src/themelint.rs +++ b/xtask/src/themelint.rs @@ -1,8 +1,6 @@ use crate::path; use crate::DynError; -use helix_view::theme::Loader; -use helix_view::theme::Modifier; -use helix_view::Theme; +use helix_view::{theme::Modifier, Theme}; struct Rule { fg: Option<&'static str>, @@ -180,7 +178,7 @@ pub fn lint(file: String) -> Result<(), DynError> { } pub fn lint_all() -> Result<(), DynError> { - let files = Loader::read_names(path::themes().as_path()); + let files = helix_loader::read_toml_names(path::themes().as_path()); let files_count = files.len(); let ok_files_count = files .into_iter() -- 2.38.5 From 63051a7163c0e106bc30e785ab0edc017f86b52c Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 17:46:17 +0100 Subject: [PATCH 181/191] wip: add the `icons` module as well as default and nerdfonts flavors --- helix-view/src/icons.rs | 299 +++++++++++++++++++++++++++++++++++ helix-view/src/lib.rs | 1 + icons.toml | 18 +++ runtime/icons/nerdfonts.toml | 284 +++++++++++++++++++++++++++++++++ 4 files changed, 602 insertions(+) create mode 100644 helix-view/src/icons.rs create mode 100644 icons.toml create mode 100644 runtime/icons/nerdfonts.toml diff --git a/helix-view/src/icons.rs b/helix-view/src/icons.rs new file mode 100644 index 00000000..3e1ebb4d --- /dev/null +++ b/helix-view/src/icons.rs @@ -0,0 +1,299 @@ +use helix_loader::merge_toml_values; +use log::warn; +use once_cell::sync::Lazy; +use serde::Deserialize; +use std::collections::{HashMap, HashSet}; +use std::{ + path::{Path, PathBuf}, + str, +}; +use toml::Value; + +use crate::graphics::{Color, Style}; +use crate::Theme; + +pub static BLANK_ICON: Icon = Icon { + icon_char: ' ', + style: None, +}; + +/// The style of an icon can either be defined by the TOML file, or by the theme. +/// We need to remember that in order to reload the icons colors when the theme changes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IconStyle { + Custom(Style), + Default(Style), +} + +impl Default for IconStyle { + fn default() -> Self { + IconStyle::Default(Style::default()) + } +} + +impl From for Style { + fn from(icon_style: IconStyle) -> Self { + match icon_style { + IconStyle::Custom(style) => style, + IconStyle::Default(style) => style, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Icon { + #[serde(rename = "icon")] + pub icon_char: char, + #[serde(default)] + #[serde(deserialize_with = "icon_color_to_style", rename = "color")] + pub style: Option, +} + +impl Icon { + /// Loads a given style if the icon style is undefined or based on a default value + pub fn with_default_style(&mut self, style: Style) { + if self.style.is_none() || matches!(self.style, Some(IconStyle::Default(_))) { + self.style = Some(IconStyle::Default(style)); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Icons { + pub name: String, + pub mime_type: Option>, + pub diagnostic: Diagnostic, + pub symbol_kind: Option>, + pub breakpoint: Breakpoint, + pub diff: Diff, + pub ui: Option>, +} + +impl Icons { + pub fn name(&self) -> &str { + &self.name + } + + /// Set theme defined styles to diagnostic icons + pub fn set_diagnostic_icons_base_style(&mut self, theme: &Theme) { + self.diagnostic.error.with_default_style(theme.get("error")); + self.diagnostic.info.with_default_style(theme.get("info")); + self.diagnostic.hint.with_default_style(theme.get("hint")); + self.diagnostic + .warning + .with_default_style(theme.get("warning")); + } + + /// Set theme defined styles to symbol-kind icons + pub fn set_symbolkind_icons_base_style(&mut self, theme: &Theme) { + let style = theme + .try_get("symbolkind") + .unwrap_or_else(|| theme.get("keyword")); + if let Some(symbol_kind_icons) = &mut self.symbol_kind { + for (_, icon) in symbol_kind_icons.iter_mut() { + icon.with_default_style(style); + } + } + } + + /// Set the default style for all icons + pub fn reset_styles(&mut self) { + if let Some(mime_type_icons) = &mut self.mime_type { + for (_, icon) in mime_type_icons.iter_mut() { + icon.style = Some(IconStyle::Default(Style::default())); + } + } + if let Some(symbol_kind_icons) = &mut self.symbol_kind { + for (_, icon) in symbol_kind_icons.iter_mut() { + icon.style = Some(IconStyle::Default(Style::default())); + } + } + if let Some(ui_icons) = &mut self.ui { + for (_, icon) in ui_icons.iter_mut() { + icon.style = Some(IconStyle::Default(Style::default())); + } + } + self.diagnostic.error.style = Some(IconStyle::Default(Style::default())); + self.diagnostic.warning.style = Some(IconStyle::Default(Style::default())); + self.diagnostic.hint.style = Some(IconStyle::Default(Style::default())); + self.diagnostic.info.style = Some(IconStyle::Default(Style::default())); + } + + pub fn icon_from_filetype<'a>(&'a self, filetype: &str) -> Option<&'a Icon> { + if let Some(mime_type_icons) = &self.mime_type { + mime_type_icons.get(filetype) + } else { + None + } + } + + /// Try to return a reference to an appropriate icon for the specified file path, with a default "file" icon if none is found. + /// If no such "file" icon is available, return `None`. + pub fn icon_from_path<'a>(&'a self, filepath: Option<&PathBuf>) -> Option<&'a Icon> { + self.mime_type + .as_ref() + .and_then(|mime_type_icons| { + filepath? + .extension() + .or(filepath?.file_name()) + .map(|extension_or_filename| extension_or_filename.to_str())? + .and_then(|extension_or_filename| mime_type_icons.get(extension_or_filename)) + }) + .or_else(|| self.ui.as_ref().and_then(|ui_icons| ui_icons.get("file"))) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Diagnostic { + pub error: Icon, + pub warning: Icon, + pub info: Icon, + pub hint: Icon, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Breakpoint { + pub verified: Icon, + pub unverified: Icon, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Diff { + pub added: Icon, + pub deleted: Icon, + pub modified: Icon, +} + +fn icon_color_to_style<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let mut style = Style::default(); + if !s.is_empty() { + match hex_string_to_rgb(&s) { + Ok(c) => { + style = style.fg(c); + } + Err(e) => { + log::error!("{}", e); + } + }; + Ok(Some(IconStyle::Custom(style))) + } else { + Ok(None) + } +} + +pub fn hex_string_to_rgb(s: &str) -> Result { + if s.starts_with('#') && s.len() >= 7 { + if let (Ok(red), Ok(green), Ok(blue)) = ( + u8::from_str_radix(&s[1..3], 16), + u8::from_str_radix(&s[3..5], 16), + u8::from_str_radix(&s[5..7], 16), + ) { + return Ok(Color::Rgb(red, green, blue)); + } + } + Err(format!("Icon color: malformed hexcode: {}", s)) +} + +pub struct Loader { + /// Icons directories to search from highest to lowest priority + icons_dirs: Vec, +} + +pub static DEFAULT_ICONS_DATA: Lazy = Lazy::new(|| { + let bytes = include_bytes!("../../icons.toml"); + toml::from_str(str::from_utf8(bytes).unwrap()).expect("Failed to parse base 16 default theme") +}); + +pub static DEFAULT_ICONS: Lazy = Lazy::new(|| Icons { + name: "default".into(), + ..Icons::from(DEFAULT_ICONS_DATA.clone()) +}); + +impl Loader { + /// Creates a new loader that can load icons flavors from two directories. + pub fn new>(dirs: &[PathBuf]) -> Self { + Self { + icons_dirs: dirs.iter().map(|p| p.join("icons")).collect(), + } + } + + /// Loads icons flavors first looking in the `user_dir` then in `default_dir`. + /// The `theme` is needed in order to load default styles for diagnostic icons. + pub fn load( + &self, + name: &str, + theme: &Theme, + true_color: bool, + ) -> Result { + if name == "default" { + return Ok(self.default(theme)); + } + + let mut visited_paths = HashSet::new(); + let default_icons = HashMap::from([("default", &DEFAULT_ICONS_DATA)]); + let mut icons = helix_loader::load_inheritable_toml( + name, + &self.icons_dirs, + &mut visited_paths, + &default_icons, + Self::merge_icons, + ) + .map(Icons::from)?; + + // Remove all styles when there is no truecolor support. + // Not classy, but less cumbersome than trying to pass a parameter to a deserializer. + if !true_color { + icons.reset_styles(); + } else { + icons.set_diagnostic_icons_base_style(theme); + icons.set_symbolkind_icons_base_style(theme); + } + + Ok(Icons { + name: name.into(), + ..icons + }) + } + + fn merge_icons(parent: Value, child: Value) -> Value { + merge_toml_values(parent, child, 3) + } + + /// Returns the default icon flavor. + /// The `theme` is needed in order to load default styles for diagnostic icons. + pub fn default(&self, theme: &Theme) -> Icons { + let mut icons = DEFAULT_ICONS.clone(); + icons.set_diagnostic_icons_base_style(theme); + icons.set_symbolkind_icons_base_style(theme); + icons + } +} + +impl From for Icons { + fn from(value: Value) -> Self { + if let Value::Table(mut table) = value { + // remove inherits from value to prevent errors + table.remove("inherits"); + let toml_str = table.to_string(); + match toml::from_str(&toml_str) { + Ok(icons) => icons, + Err(e) => { + log::error!("Failed to load icons, falling back to default: {}\n", e); + DEFAULT_ICONS.clone() + } + } + } else { + warn!("Expected icons TOML value to be a table, found {:?}", value); + DEFAULT_ICONS.clone() + } + } +} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index c3f67345..60c6efd5 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -12,6 +12,7 @@ pub mod handlers { pub mod lsp; } pub mod base64; +pub mod icons; pub mod info; pub mod input; pub mod keyboard; diff --git a/icons.toml b/icons.toml new file mode 100644 index 00000000..9c3bc8db --- /dev/null +++ b/icons.toml @@ -0,0 +1,18 @@ +name = "default" + +# All icons here must be available as [default Unicode characters](https://en.wikipedia.org/wiki/List_of_Unicode_characters) + +[diagnostic] +error = {icon = "●"} +warning = {icon = "●"} +info = {icon = "●"} +hint = {icon = "●"} + +[breakpoint] +verified = {icon = "▲"} +unverified = {icon = "⊚"} + +[diff] +added = {icon = "▍"} +deleted = {icon = "▔"} +modified = {icon = "▍"} diff --git a/runtime/icons/nerdfonts.toml b/runtime/icons/nerdfonts.toml new file mode 100644 index 00000000..598879c2 --- /dev/null +++ b/runtime/icons/nerdfonts.toml @@ -0,0 +1,284 @@ +name = "nerdfonts" + +[diagnostic] +error = {icon = ""} +warning = {icon = ""} +info = {icon = ""} +hint = {icon = ""} + +[breakpoint] +verified = {icon = "▲"} +unverified = {icon = "⊚"} + +[diff] +added = {icon = "▍"} +deleted = {icon = "▔"} +modified = {icon = "▍"} + +[symbol-kind] +file = {icon = ""} +module = {icon = ""} +namespace = {icon = ""} +package = {icon = ""} +class = {icon = "ﴯ"} +method = {icon = ""} +property = {icon = ""} +field = {icon = ""} +constructor = {icon = ""} +enumeration = {icon = ""} +interface = {icon = ""} +variable = {icon = ""} +function = {icon = ""} +constant = {icon = ""} +string = {icon = ""} +number = {icon = ""} +boolean = {icon = ""} +array = {icon = ""} +object = {icon = ""} +key = {icon = ""} +null = {icon = "ﳠ"} +enum-member = {icon = ""} +structure = {icon = "פּ"} +event = {icon = ""} +operator = {icon = ""} +type-parameter = {icon = ""} + +[ui] +file = {icon = ""} +folder = {icon = ""} +folder_opened = {icon = ""} +git_branch = {icon = ""} + +[mime-type] +# This is heavily based on https://github.com/nvim-tree/nvim-web-devicons +".babelrc" = { icon = "ﬥ", color = "#cbcb41" } +".bash_profile" = { icon = "", color = "#89e051" } +".bashrc" = { icon = "", color = "#89e051" } +".DS_Store" = { icon = "", color = "#41535b" } +".gitattributes" = { icon = "", color = "#41535b" } +".gitconfig" = { icon = "", color = "#41535b" } +".gitignore" = { icon = "", color = "#41535b" } +".gitlab-ci.yml" = { icon = "", color = "#e24329" } +".gitmodules" = { icon = "", color = "#41535b" } +".gvimrc" = { icon = "", color = "#019833" } +".npmignore" = { icon = "", color = "#E8274B" } +".npmrc" = { icon = "", color = "#E8274B" } +".settings.json" = { icon = "", color = "#854CC7" } +".vimrc" = { icon = "", color = "#019833" } +".zprofile" = { icon = "", color = "#89e051" } +".zshenv" = { icon = "", color = "#89e051" } +".zshrc" = { icon = "", color = "#89e051" } +"Brewfile" = { icon = "", color = "#701516" } +"CMakeLists.txt" = { icon = "", color = "#6d8086" } +"COMMIT_EDITMSG" = { icon = "", color = "#41535b" } +"COPYING" = { icon = "", color = "#cbcb41" } +"COPYING.LESSER" = { icon = "", color = "#cbcb41" } +"Dockerfile" = { icon = "", color = "#384d54" } +"Gemfile$" = { icon = "", color = "#701516" } +"LICENSE" = { icon = "", color = "#d0bf41" } +"R" = { icon = "ﳒ", color = "#358a5b" } +"Rmd" = { icon = "", color = "#519aba" } +"Vagrantfile$" = { icon = "", color = "#1563FF" } +"_gvimrc" = { icon = "", color = "#019833" } +"_vimrc" = { icon = "", color = "#019833" } +"ai" = { icon = "", color = "#cbcb41" } +"awk" = { icon = "", color = "#4d5a5e" } +"bash" = { icon = "", color = "#89e051" } +"bat" = { icon = "", color = "#C1F12E" } +"bmp" = { icon = "", color = "#a074c4" } +"c" = { icon = "", color = "#599eff" } +"c++" = { icon = "", color = "#f34b7d" } +"cbl" = { icon = "⚙", color = "#005ca5" } +"cc" = { icon = "", color = "#f34b7d" } +"cfg" = { icon = "", color = "#ECECEC" } +"clj" = { icon = "", color = "#8dc149" } +"cljc" = { icon = "", color = "#8dc149" } +"cljs" = { icon = "", color = "#519aba" } +"cljd" = { icon = "", color = "#519aba" } +"cmake" = { icon = "", color = "#6d8086" } +"cob" = { icon = "⚙", color = "#005ca5" } +"cobol" = { icon = "⚙", color = "#005ca5" } +"coffee" = { icon = "", color = "#cbcb41" } +"conf" = { icon = "", color = "#6d8086" } +"config.ru" = { icon = "", color = "#701516" } +"cp" = { icon = "", color = "#519aba" } +"cpp" = { icon = "", color = "#519aba" } +"cpy" = { icon = "⚙", color = "#005ca5" } +"cr" = { icon = "" } +"cs" = { icon = "", color = "#596706" } +"csh" = { icon = "", color = "#4d5a5e" } +"cson" = { icon = "", color = "#cbcb41" } +"css" = { icon = "", color = "#42a5f5" } +"csv" = { icon = "", color = "#89e051" } +"cxx" = { icon = "", color = "#519aba" } +"d" = { icon = "", color = "#427819" } +"dart" = { icon = "", color = "#03589C" } +"db" = { icon = "", color = "#dad8d8" } +"desktop" = { icon = "", color = "#563d7c" } +"diff" = { icon = "", color = "#41535b" } +"doc" = { icon = "", color = "#185abd" } +"dockerfile" = { icon = "", color = "#384d54" } +"drl" = { icon = "", color = "#ffafaf" } +"dropbox" = { icon = "", color = "#0061FE" } +"dump" = { icon = "", color = "#dad8d8" } +"edn" = { icon = "", color = "#519aba" } +"eex" = { icon = "", color = "#a074c4" } +"ejs" = { icon = "", color = "#cbcb41" } +"elm" = { icon = "", color = "#519aba" } +"epp" = { icon = "", color = "#FFA61A" } +"erb" = { icon = "", color = "#701516" } +"erl" = { icon = "", color = "#B83998" } +"ex" = { icon = "", color = "#a074c4" } +"exs" = { icon = "", color = "#a074c4" } +"f#" = { icon = "", color = "#519aba" } +"favicon.ico" = { icon = "", color = "#cbcb41" } +"fnl" = { icon = "🌜", color = "#fff3d7" } +"fish" = { icon = "", color = "#4d5a5e" } +"fs" = { icon = "", color = "#519aba" } +"fsi" = { icon = "", color = "#519aba" } +"fsscript" = { icon = "", color = "#519aba" } +"fsx" = { icon = "", color = "#519aba" } +"gd" = { icon = "", color = "#6d8086" } +"gemspec" = { icon = "", color = "#701516" } +"gif" = { icon = "", color = "#a074c4" } +"git" = { icon = "", color = "#F14C28" } +"glb" = { icon = "", color = "#FFB13B" } +"go" = { icon = "", color = "#519aba" } +"godot" = { icon = "", color = "#6d8086" } +"graphql" = { icon = "", color = "#e535ab" } +"gruntfile" = { icon = "", color = "#e37933" } +"gulpfile" = { icon = "", color = "#cc3e44" } +"h" = { icon = "", color = "#a074c4" } +"haml" = { icon = "", color = "#eaeae1" } +"hbs" = { icon = "", color = "#f0772b" } +"heex" = { icon = "", color = "#a074c4" } +"hh" = { icon = "", color = "#a074c4" } +"hpp" = { icon = "", color = "#a074c4" } +"hrl" = { icon = "", color = "#B83998" } +"hs" = { icon = "", color = "#a074c4" } +"htm" = { icon = "", color = "#e34c26" } +"html" = { icon = "", color = "#e44d26" } +"hxx" = { icon = "", color = "#a074c4" } +"ico" = { icon = "", color = "#cbcb41" } +"import" = { icon = "", color = "#ECECEC" } +"ini" = { icon = "", color = "#6d8086" } +"java" = { icon = "", color = "#cc3e44" } +"jl" = { icon = "", color = "#a270ba" } +"jpeg" = { icon = "", color = "#a074c4" } +"jpg" = { icon = "", color = "#a074c4" } +"js" = { icon = "", color = "#cbcb41" } +"json" = { icon = "", color = "#cbcb41" } +"json5" = { icon = "ﬥ", color = "#cbcb41" } +"jsx" = { icon = "", color = "#519aba" } +"ksh" = { icon = "", color = "#4d5a5e" } +"kt" = { icon = "", color = "#F88A02" } +"kts" = { icon = "", color = "#F88A02" } +"leex" = { icon = "", color = "#a074c4" } +"less" = { icon = "", color = "#563d7c" } +"lhs" = { icon = "", color = "#a074c4" } +"license" = { icon = "", color = "#cbcb41" } +"lua" = { icon = "", color = "#51a0cf" } +"luau" = { icon = "", color = "#51a0cf" } +"makefile" = { icon = "", color = "#6d8086" } +"markdown" = { icon = "", color = "#d74c4c" } +"material" = { icon = "", color = "#B83998" } +"md" = { icon = "", color = "#d74c4c" } +"mdx" = { icon = "", color = "#d74c4c" } +"mint" = { icon = "", color = "#87c095" } +"mix.lock" = { icon = "", color = "#a074c4" } +"mjs" = { icon = "", color = "#f1e05a" } +"ml" = { icon = "λ", color = "#e37933" } +"mli" = { icon = "λ", color = "#e37933" } +"mo" = { icon = "∞", color = "#9772FB" } +"mustache" = { icon = "", color = "#e37933" } +"nim" = { icon = "👑", color = "#f3d400" } +"nix" = { icon = "", color = "#7ebae4" } +"node_modules" = { icon = "", color = "#E8274B" } +"opus" = { icon = "", color = "#F88A02" } +"otf" = { icon = "", color = "#ECECEC" } +"package.json" = { icon = "", color = "#e8274b" } +"package-lock.json" = { icon = "", color = "#7a0d21" } +"pck" = { icon = "", color = "#6d8086" } +"pdf" = { icon = "", color = "#b30b00" } +"php" = { icon = "", color = "#a074c4" } +"pl" = { icon = "", color = "#519aba" } +"pm" = { icon = "", color = "#519aba" } +"png" = { icon = "", color = "#a074c4" } +"pp" = { icon = "", color = "#FFA61A" } +"ppt" = { icon = "", color = "#cb4a32" } +"pro" = { icon = "", color = "#e4b854" } +"Procfile" = { icon = "", color = "#a074c4" } +"ps1" = { icon = "", color = "#4d5a5e" } +"psb" = { icon = "", color = "#519aba" } +"psd" = { icon = "", color = "#519aba" } +"py" = { icon = "", color = "#ffbc03" } +"pyc" = { icon = "", color = "#ffe291" } +"pyd" = { icon = "", color = "#ffe291" } +"pyo" = { icon = "", color = "#ffe291" } +"query" = { icon = "", color = "#90a850" } +"r" = { icon = "ﳒ", color = "#358a5b" } +"rake" = { icon = "", color = "#701516" } +"rakefile" = { icon = "", color = "#701516" } +"rb" = { icon = "", color = "#701516" } +"rlib" = { icon = "", color = "#dea584" } +"rmd" = { icon = "", color = "#519aba" } +"rproj" = { icon = "鉶", color = "#358a5b" } +"rs" = { icon = "", color = "#dea584" } +"rss" = { icon = "", color = "#FB9D3B" } +"sass" = { icon = "", color = "#f55385" } +"sbt" = { icon = "", color = "#cc3e44" } +"scala" = { icon = "", color = "#cc3e44" } +"scm" = { icon = "ﬦ" } +"scss" = { icon = "", color = "#f55385" } +"sh" = { icon = "", color = "#4d5a5e" } +"sig" = { icon = "λ", color = "#e37933" } +"slim" = { icon = "", color = "#e34c26" } +"sln" = { icon = "", color = "#854CC7" } +"sml" = { icon = "λ", color = "#e37933" } +"sql" = { icon = "", color = "#dad8d8" } +"sqlite" = { icon = "", color = "#dad8d8" } +"sqlite3" = { icon = "", color = "#dad8d8" } +"styl" = { icon = "", color = "#8dc149" } +"sublime" = { icon = "", color = "#e37933" } +"suo" = { icon = "", color = "#854CC7" } +"sv" = { icon = "", color = "#019833" } +"svelte" = { icon = "", color = "#ff3e00" } +"svh" = { icon = "", color = "#019833" } +"svg" = { icon = "ﰟ", color = "#FFB13B" } +"swift" = { icon = "", color = "#e37933" } +"t" = { icon = "", color = "#519aba" } +"tbc" = { icon = "﯑", color = "#1e5cb3" } +"tcl" = { icon = "﯑", color = "#1e5cb3" } +"terminal" = { icon = "", color = "#31B53E" } +"tex" = { icon = "ﭨ", color = "#3D6117" } +"tf" = { icon = "", color = "#5F43E9" } +"tfvars" = { icon = "", color = "#5F43E9" } +"toml" = { icon = "", color = "#6d8086" } +"tres" = { icon = "", color = "#cbcb41" } +"ts" = { icon = "", color = "#519aba" } +"tscn" = { icon = "", color = "#a074c4" } +"tsx" = { icon = "", color = "#519aba" } +"twig" = { icon = "", color = "#8dc149" } +"txt" = { icon = "", color = "#89e051" } +"v" = { icon = "", color = "#019833" } +"vh" = { icon = "", color = "#019833" } +"vhd" = { icon = "", color = "#019833" } +"vhdl" = { icon = "", color = "#019833" } +"vim" = { icon = "", color = "#019833" } +"vue" = { icon = "﵂", color = "#8dc149" } +"webmanifest" = { icon = "", color = "#f1e05a" } +"webp" = { icon = "", color = "#a074c4" } +"webpack" = { icon = "ﰩ", color = "#519aba" } +"xcplayground" = { icon = "", color = "#e37933" } +"xls" = { icon = "", color = "#207245" } +"xml" = { icon = "謹", color = "#e37933" } +"xul" = { icon = "", color = "#e37933" } +"yaml" = { icon = "", color = "#6d8086" } +"yml" = { icon = "", color = "#6d8086" } +"zig" = { icon = "", color = "#f69a1b" } +"zsh" = { icon = "", color = "#89e051" } +"sol" = { icon = "ﲹ", color = "#519aba" } +".env" = { icon = "", color = "#faf743" } +"prisma" = { icon = "卑" } +"lock" = { icon = "", color = "#bbbbbb" } +"log" = { icon = "" } -- 2.38.5 From cfcf2ff4ffba0716d087fb639b8e5b85a5495c18 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:06:03 +0100 Subject: [PATCH 182/191] feat: add icons launch and runtime loading --- helix-term/src/application.rs | 46 +++++++++++++++++++++++++++----- helix-term/src/commands/typed.rs | 31 +++++++++++++++++++++ helix-term/src/config.rs | 5 ++++ helix-term/src/ui/mod.rs | 31 +++++++++++++++++++++ helix-view/src/editor.rs | 40 +++++++++++++++++++++++++++ helix-view/src/icons.rs | 7 ++--- 6 files changed, 149 insertions(+), 11 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index f7d7fa63..3da88c85 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -11,7 +11,7 @@ use helix_view::{ document::DocumentSavedEventResult, editor::{ConfigEvent, EditorEvent}, graphics::Rect, - theme, + icons, theme, tree::Layout, Align, Editor, }; @@ -69,6 +69,7 @@ pub struct Application { #[allow(dead_code)] theme_loader: Arc, + icons_loader: Arc, #[allow(dead_code)] syn_loader: Arc, @@ -111,9 +112,9 @@ impl Application { use helix_view::editor::Action; - let mut theme_parent_dirs = vec![helix_loader::config_dir()]; - theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); - let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); + let mut theme_and_icons_parent_dirs = vec![helix_loader::config_dir()]; + theme_and_icons_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); + let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_and_icons_parent_dirs)); let true_color = config.editor.true_color || crate::true_color(); let theme = config @@ -131,6 +132,21 @@ impl Application { }) .unwrap_or_else(|| theme_loader.default_theme(true_color)); + let icons_loader = std::sync::Arc::new(icons::Loader::new(&theme_and_icons_parent_dirs)); + let icons = config + .icons + .as_ref() + .and_then(|icons| { + icons_loader + .load(icons, &theme, true_color) + .map_err(|e| { + log::warn!("failed to load icons `{}` - {}", icons, e); + e + }) + .ok() + }) + .unwrap_or_else(|| icons_loader.default(&theme)); + let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); #[cfg(not(feature = "integration"))] @@ -146,12 +162,16 @@ impl Application { let mut editor = Editor::new( area, theme_loader.clone(), + icons_loader.clone(), syn_loader.clone(), Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor })), ); + editor.set_theme(theme); + editor.set_icons(icons); + let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); @@ -225,8 +245,6 @@ impl Application { .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } - editor.set_theme(theme); - #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] @@ -241,6 +259,7 @@ impl Application { config, theme_loader, + icons_loader, syn_loader, signals, @@ -413,12 +432,27 @@ impl Application { Ok(()) } + /// Refresh icons after config change + fn refresh_icons(&mut self, config: &Config) -> Result<(), Error> { + if let Some(icons) = config.icons.clone() { + let true_color = config.editor.true_color || crate::true_color(); + let icons = self + .icons_loader + .load(&icons, &self.editor.theme, true_color) + .map_err(|err| anyhow::anyhow!("Failed to load icons `{}`: {}", icons, err))?; + self.editor.set_icons(icons); + } + + Ok(()) + } + fn refresh_config(&mut self) { let mut refresh_config = || -> Result<(), Error> { let default_config = Config::load_default() .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; self.refresh_language_config()?; self.refresh_theme(&default_config)?; + self.refresh_icons(&default_config)?; // Store new config self.config.store(Arc::new(default_config)); Ok(()) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 3c954d20..1d4b6285 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -853,6 +853,30 @@ fn theme( Ok(()) } +fn icons( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + let true_color = cx.editor.config.load().true_color || crate::true_color(); + if let PromptEvent::Validate = event { + if let Some(flavor_name) = args.first() { + let icons = cx + .editor + .icons_loader + .load(flavor_name, &cx.editor.theme, true_color) + .map_err(|err| anyhow!("Could not load icon flavor: {}", err))?; + cx.editor.set_icons(icons); + } else { + let name = cx.editor.icons.name().to_string(); + + cx.editor.set_status(name); + } + }; + + Ok(()) +} + fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, _args: &[Cow], @@ -2374,6 +2398,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: theme, signature: CommandSignature::positional(&[completers::theme]), }, + TypableCommand { + name: "icons", + aliases: &[], + doc: "Change the editor icon flavor (show current flavor if no name specified).", + fun: icons, + signature: CommandSignature::positional(&[completers::icons]), + }, TypableCommand { name: "clipboard-yank", aliases: &[], diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 9776ef7a..e36b4851 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -12,6 +12,7 @@ use toml::de::Error as TomlError; #[derive(Debug, Clone, PartialEq)] pub struct Config { pub theme: Option, + pub icons: Option, pub keys: HashMap, pub editor: helix_view::editor::Config, } @@ -20,6 +21,7 @@ pub struct Config { #[serde(deny_unknown_fields)] pub struct ConfigRaw { pub theme: Option, + pub icons: Option, pub keys: Option>, pub editor: Option, } @@ -28,6 +30,7 @@ impl Default for Config { fn default() -> Config { Config { theme: None, + icons: None, keys: keymap::default(), editor: helix_view::editor::Config::default(), } @@ -86,6 +89,7 @@ impl Config { Config { theme: local.theme.or(global.theme), + icons: local.icons.or(global.icons), keys, editor, } @@ -102,6 +106,7 @@ impl Config { } Config { theme: config.theme, + icons: config.icons, keys, editor: config.editor.map_or_else( || Ok(helix_view::editor::Config::default()), diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index bd8b888d..80454c0f 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -310,6 +310,37 @@ pub mod completers { names } + pub fn icons(_editor: &Editor, input: &str) -> Vec { + let mut names = helix_loader::read_toml_names(&helix_loader::config_dir().join("icons")); + for rt_dir in helix_loader::runtime_dirs() { + names.extend(helix_loader::read_toml_names(&rt_dir.join("icons"))); + } + names.push("default".into()); + names.sort(); + names.dedup(); + + let mut names: Vec<_> = names + .into_iter() + .map(|name| ((0..), Cow::from(name))) + .collect(); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = names + .into_iter() + .filter_map(|(_range, name)| { + matcher.fuzzy_match(&name, input).map(|score| (name, score)) + }) + .collect(); + + matches.sort_unstable_by(|(name1, score1), (name2, score2)| { + (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2)) + }); + names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + + names + } + /// Recursive function to get all keys from this value and add them to vec fn get_keys(value: &serde_json::Value, vec: &mut Vec, scope: Option<&str>) { if let Some(map) = value.as_object() { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 80c47ed0..7c3fe4bb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -3,6 +3,7 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, graphics::{CursorKind, Rect}, + icons::{self, Icons}, info::Info, input::KeyEvent, theme::{self, Theme}, @@ -211,6 +212,27 @@ impl Default for FilePickerConfig { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct IconsConfig { + /// Enables icons in front of buffer names in bufferline. Defaults to `true` + pub bufferline: bool, + /// Enables icons in front of items in the picker. Defaults to `true` + pub picker: bool, + /// Enables icons in front of items in the statusline. Defaults to `true` + pub statusline: bool, +} + +impl Default for IconsConfig { + fn default() -> Self { + Self { + bufferline: true, + picker: true, + statusline: true, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { @@ -284,6 +306,8 @@ pub struct Config { pub soft_wrap: SoftWrap, /// Workspace specific lsp ceiling dirs pub workspace_lsp_roots: Vec, + /// Icons configuration + pub icons: IconsConfig, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -753,6 +777,7 @@ impl Default for Config { text_width: 80, completion_replace: false, workspace_lsp_roots: Vec::new(), + icons: IconsConfig::default(), } } } @@ -829,6 +854,8 @@ pub struct Editor { /// The currently applied editor theme. While previewing a theme, the previewed theme /// is set here. pub theme: Theme, + pub icons: Icons, + pub icons_loader: Arc, /// The primary Selection prior to starting a goto_line_number preview. This is /// restored when the preview is aborted, or added to the jumplist when it is @@ -927,11 +954,14 @@ impl Editor { pub fn new( mut area: Rect, theme_loader: Arc, + icons_loader: Arc, syn_loader: Arc, config: Arc>, ) -> Self { let conf = config.load(); let auto_pairs = (&conf.auto_pairs).into(); + let theme = theme_loader.default(); + let icons = icons_loader.default(&theme); // HAXX: offset the render area height by 1 to account for prompt/commandline area.height -= 1; @@ -974,6 +1004,8 @@ impl Editor { needs_redraw: false, cursor_cache: Cell::new(None), completion_request_handle: None, + icons, + icons_loader, } } @@ -1074,6 +1106,9 @@ impl Editor { } ThemeAction::Set => { self.last_theme = None; + // Reload the icons to apply default colors based on theme + self.icons.set_diagnostic_icons_base_style(&theme); + self.icons.set_symbolkind_icons_base_style(&theme); self.theme = theme; } } @@ -1081,6 +1116,11 @@ impl Editor { self._refresh(); } + pub fn set_icons(&mut self, icons: Icons) { + self.icons = icons; + self._refresh(); + } + /// Refreshes the language server for a given document pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { self.launch_language_server(doc_id) diff --git a/helix-view/src/icons.rs b/helix-view/src/icons.rs index 3e1ebb4d..5921d778 100644 --- a/helix-view/src/icons.rs +++ b/helix-view/src/icons.rs @@ -3,10 +3,7 @@ use log::warn; use once_cell::sync::Lazy; use serde::Deserialize; use std::collections::{HashMap, HashSet}; -use std::{ - path::{Path, PathBuf}, - str, -}; +use std::{path::PathBuf, str}; use toml::Value; use crate::graphics::{Color, Style}; @@ -220,7 +217,7 @@ pub static DEFAULT_ICONS: Lazy = Lazy::new(|| Icons { impl Loader { /// Creates a new loader that can load icons flavors from two directories. - pub fn new>(dirs: &[PathBuf]) -> Self { + pub fn new(dirs: &[PathBuf]) -> Self { Self { icons_dirs: dirs.iter().map(|p| p.join("icons")).collect(), } -- 2.38.5 From 18945587ffb5970cb856d689a426d12d55525cb4 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:37:22 +0100 Subject: [PATCH 183/191] feat: add icons to pickers --- helix-term/src/application.rs | 2 +- helix-term/src/commands.rs | 57 +++++++++++++---- helix-term/src/commands/dap.rs | 11 ++-- helix-term/src/commands/lsp.rs | 105 ++++++++++++++++++++++++------- helix-term/src/commands/typed.rs | 4 +- helix-term/src/ui/completion.rs | 4 +- helix-term/src/ui/menu.rs | 28 +++++---- helix-term/src/ui/mod.rs | 8 ++- helix-term/src/ui/picker.rs | 28 ++++++--- helix-tui/src/text.rs | 10 +++ 10 files changed, 194 insertions(+), 63 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3da88c85..a0c4b552 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -188,7 +188,7 @@ impl Application { if first.is_dir() { std::env::set_current_dir(first).context("set current dir")?; editor.new_file(Action::VerticalSplit); - let picker = ui::file_picker(".".into(), &config.load().editor); + let picker = ui::file_picker(".".into(), &config.load().editor, &editor.icons); compositor.push(Box::new(overlaid(picker))); } else { let nr_of_files = args.files.len(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 17669924..45158a27 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6,7 +6,7 @@ pub use dap::*; use helix_vcs::Hunk; pub use lsp::*; use tokio::sync::oneshot; -use tui::widgets::Row; +use tui::{text::Span, widgets::Row}; pub use typed::*; use helix_core::{ @@ -34,6 +34,7 @@ use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, editor::{Action, Motion}, + icons::Icons, info::Info, input::KeyEvent, keyboard::KeyCode, @@ -1989,11 +1990,12 @@ fn global_search(cx: &mut Context) { impl ui::menu::Item for FileResult { type Data = Option; - fn format(&self, current_path: &Self::Data) -> Row { + fn format<'a>(&self, current_path: &Self::Data, icons: Option<&'a Icons>) -> Row { + let icon = icons.and_then(|icons| icons.icon_from_path(Some(&self.path))); let relative_path = helix_core::path::get_relative_path(&self.path) .to_string_lossy() .into_owned(); - if current_path + let path_span: Span = if current_path .as_ref() .map(|p| p == &self.path) .unwrap_or(false) @@ -2001,6 +2003,12 @@ fn global_search(cx: &mut Context) { format!("{} (*)", relative_path).into() } else { relative_path.into() + }; + + if let Some(icon) = icon { + Row::new([icon.into(), path_span]) + } else { + path_span.into() } } } @@ -2117,6 +2125,7 @@ fn global_search(cx: &mut Context) { let picker = FilePicker::new( all_matches, current_path, + editor.config().icons.picker.then_some(&editor.icons), move |cx, FileResult { path, line_num }, action| { match cx.editor.open(path, action) { Ok(_) => {} @@ -2420,7 +2429,7 @@ fn append_mode(cx: &mut Context) { fn file_picker(cx: &mut Context) { let root = find_workspace().0; - let picker = ui::file_picker(root, &cx.editor.config()); + let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons); cx.push_layer(Box::new(overlaid(picker))); } @@ -2437,12 +2446,12 @@ fn file_picker_in_current_buffer_directory(cx: &mut Context) { } }; - let picker = ui::file_picker(path, &cx.editor.config()); + let picker = ui::file_picker(path, &cx.editor.config(), &cx.editor.icons); cx.push_layer(Box::new(overlaid(picker))); } fn file_picker_in_current_directory(cx: &mut Context) { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")); - let picker = ui::file_picker(cwd, &cx.editor.config()); + let picker = ui::file_picker(cwd, &cx.editor.config(), &cx.editor.icons); cx.push_layer(Box::new(overlaid(picker))); } @@ -2459,7 +2468,7 @@ fn buffer_picker(cx: &mut Context) { impl ui::menu::Item for BufferMeta { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row { let path = self .path .as_deref() @@ -2469,6 +2478,9 @@ fn buffer_picker(cx: &mut Context) { None => SCRATCH_BUFFER_NAME, }; + // Get the filetype icon, or a "file" icon for scratch buffers + let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref())); + let mut flags = String::new(); if self.is_modified { flags.push('+'); @@ -2477,7 +2489,17 @@ fn buffer_picker(cx: &mut Context) { flags.push('*'); } - Row::new([self.id.to_string(), flags, path.to_string()]) + if let Some(icon) = icon { + let icon_span = Span::from(icon); + Row::new(vec![ + icon_span, + self.id.to_string().into(), + flags.into(), + path.to_string().into(), + ]) + } else { + Row::new([self.id.to_string(), flags, path.to_string()]) + } } } @@ -2495,6 +2517,7 @@ fn buffer_picker(cx: &mut Context) { .map(|doc| new_meta(doc)) .collect(), (), + cx.editor.config().icons.picker.then_some(&cx.editor.icons), |cx, meta, action| { cx.editor.switch(meta.id, action); }, @@ -2523,7 +2546,10 @@ fn jumplist_picker(cx: &mut Context) { impl ui::menu::Item for JumpMeta { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, icons: Option<&'a Icons>) -> Row { + // Get the filetype icon, or a "file" icon for scratch buffers + let icon = icons.and_then(|icons| icons.icon_from_path(self.path.as_ref())); + let path = self .path .as_deref() @@ -2543,7 +2569,13 @@ fn jumplist_picker(cx: &mut Context) { } else { format!(" ({})", flags.join("")) }; - format!("{} {}{} {}", self.id, path, flag, self.text).into() + + let path_span: Span = format!("{} {}{} {}", self.id, path, flag, self.text).into(); + if let Some(icon) = icon { + Row::new(vec![icon.into(), path_span]) + } else { + path_span.into() + } } } @@ -2577,6 +2609,7 @@ fn jumplist_picker(cx: &mut Context) { }) .collect(), (), + cx.editor.config().icons.picker.then_some(&cx.editor.icons), |cx, meta, action| { cx.editor.switch(meta.id, action); let config = cx.editor.config(); @@ -2596,7 +2629,7 @@ fn jumplist_picker(cx: &mut Context) { impl ui::menu::Item for MappableCommand { type Data = ReverseKeymap; - fn format(&self, keymap: &Self::Data) -> Row { + fn format<'a>(&self, keymap: &Self::Data, _icons: Option<&'a Icons>) -> Row { let fmt_binding = |bindings: &Vec>| -> String { bindings.iter().fold(String::new(), |mut acc, bind| { if !acc.is_empty() { @@ -2638,7 +2671,7 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let picker = Picker::new(commands, keymap, None, move |cx, command, _action| { let mut ctx = Context { register: None, count: std::num::NonZeroUsize::new(1), diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 8efdc9cf..3c0214b0 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_dap::{self as dap, Client}; use helix_lsp::block_on; -use helix_view::editor::Breakpoint; +use helix_view::{editor::Breakpoint, icons::Icons}; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -25,7 +25,7 @@ use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select impl ui::menu::Item for StackFrame { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { self.name.as_str().into() // TODO: include thread_states in the label } } @@ -33,7 +33,7 @@ impl ui::menu::Item for StackFrame { impl ui::menu::Item for DebugTemplate { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { self.name.as_str().into() } } @@ -41,7 +41,7 @@ impl ui::menu::Item for DebugTemplate { impl ui::menu::Item for Thread { type Data = ThreadStates; - fn format(&self, thread_states: &Self::Data) -> Row { + fn format<'a>(&self, thread_states: &Self::Data, _icons: Option<&'a Icons>) -> Row { format!( "{} ({})", self.name, @@ -76,6 +76,7 @@ fn thread_picker( let picker = FilePicker::new( threads, thread_states, + None, move |cx, thread, _action| callback_fn(cx.editor, thread), move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; @@ -273,6 +274,7 @@ pub fn dap_launch(cx: &mut Context) { cx.push_layer(Box::new(overlaid(Picker::new( templates, (), + None, |cx, template, _action| { let completions = template.completion.clone(); let name = template.name.clone(); @@ -731,6 +733,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let picker = FilePicker::new( frames, (), + None, move |cx, frame, _action| { let debugger = debugger!(cx.editor); // TODO: this should be simpler to find diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 7a26b3cf..7b6499d4 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -3,15 +3,12 @@ use helix_lsp::{ block_on, lsp::{ self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, - NumberOrString, + NumberOrString, SymbolKind, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; -use tui::{ - text::{Span, Spans}, - widgets::Row, -}; +use tui::{text::Span, widgets::Row}; use super::{align_view, push_jump, Align, Context, Editor, Open}; @@ -19,6 +16,7 @@ use helix_core::{path, text_annotations::InlineAnnotation, Selection}; use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, editor::Action, + icons::{self, Icon, Icons}, theme::Style, Document, View, }; @@ -57,7 +55,7 @@ impl ui::menu::Item for lsp::Location { /// Current working directory. type Data = PathBuf; - fn format(&self, cwdir: &Self::Data) -> Row { + fn format<'a>(&self, cwdir: &Self::Data, _icons: Option<&'a Icons>) -> Row { // The preallocation here will overallocate a few characters since it will account for the // URL's scheme, which is not used most of the time since that scheme will be "file://". // Those extra chars will be used to avoid allocating when writing the line number (in the @@ -91,16 +89,58 @@ impl ui::menu::Item for lsp::SymbolInformation { /// Path to currently focussed document type Data = Option; - fn format(&self, current_doc_path: &Self::Data) -> Row { + fn format<'a>(&self, current_doc_path: &Self::Data, icons: Option<&'a Icons>) -> Row { + let icon = + icons + .and_then(|icons| icons.symbol_kind.as_ref()) + .and_then(|symbol_kind_icons| match self.kind { + SymbolKind::FILE => symbol_kind_icons.get("file"), + SymbolKind::MODULE => symbol_kind_icons.get("module"), + SymbolKind::NAMESPACE => symbol_kind_icons.get("namespace"), + SymbolKind::PACKAGE => symbol_kind_icons.get("package"), + SymbolKind::CLASS => symbol_kind_icons.get("class"), + SymbolKind::METHOD => symbol_kind_icons.get("method"), + SymbolKind::PROPERTY => symbol_kind_icons.get("property"), + SymbolKind::FIELD => symbol_kind_icons.get("field"), + SymbolKind::CONSTRUCTOR => symbol_kind_icons.get("constructor"), + SymbolKind::ENUM => symbol_kind_icons.get("enumeration"), + SymbolKind::INTERFACE => symbol_kind_icons.get("interface"), + SymbolKind::FUNCTION => symbol_kind_icons.get("function"), + SymbolKind::VARIABLE => symbol_kind_icons.get("variable"), + SymbolKind::CONSTANT => symbol_kind_icons.get("constant"), + SymbolKind::STRING => symbol_kind_icons.get("string"), + SymbolKind::NUMBER => symbol_kind_icons.get("number"), + SymbolKind::BOOLEAN => symbol_kind_icons.get("boolean"), + SymbolKind::ARRAY => symbol_kind_icons.get("array"), + SymbolKind::OBJECT => symbol_kind_icons.get("object"), + SymbolKind::KEY => symbol_kind_icons.get("key"), + SymbolKind::NULL => symbol_kind_icons.get("null"), + SymbolKind::ENUM_MEMBER => symbol_kind_icons.get("enum-member"), + SymbolKind::STRUCT => symbol_kind_icons.get("structure"), + SymbolKind::EVENT => symbol_kind_icons.get("event"), + SymbolKind::OPERATOR => symbol_kind_icons.get("operator"), + SymbolKind::TYPE_PARAMETER => symbol_kind_icons.get("type-parameter"), + _ => Some(&icons::BLANK_ICON), + }); + if current_doc_path.as_ref() == Some(&self.location.uri) { - self.name.as_str().into() + if let Some(icon) = icon { + Row::new([Span::from(icon), self.name.as_str().into()]) + } else { + self.name.as_str().into() + } } else { - match self.location.uri.to_file_path() { + let symbol_span: Span = match self.location.uri.to_file_path() { Ok(path) => { let get_relative_path = path::get_relative_path(path.as_path()); format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into() } Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(), + }; + if let Some(icon) = icon { + Row::new([Span::from(icon), symbol_span]) + } else { + Row::from(symbol_span) } } } @@ -121,7 +161,18 @@ struct PickerDiagnostic { impl ui::menu::Item for PickerDiagnostic { type Data = (DiagnosticStyles, DiagnosticsFormat); - fn format(&self, (styles, format): &Self::Data) -> Row { + fn format<'a>(&self, (styles, format): &Self::Data, icons: Option<&'a Icons>) -> Row { + let icon: Option<&'a Icon> = + icons + .zip(self.diag.severity) + .map(|(icons, severity)| match severity { + DiagnosticSeverity::ERROR => &icons.diagnostic.error, + DiagnosticSeverity::WARNING => &icons.diagnostic.warning, + DiagnosticSeverity::HINT => &icons.diagnostic.hint, + DiagnosticSeverity::INFORMATION => &icons.diagnostic.info, + _ => &icons::BLANK_ICON, + }); + let mut style = self .diag .severity @@ -152,12 +203,20 @@ impl ui::menu::Item for PickerDiagnostic { } }; - Spans::from(vec![ - Span::raw(path), - Span::styled(&self.diag.message, style), - Span::styled(code, style), - ]) - .into() + if let Some(icon) = icon { + Row::new(vec![ + icon.into(), + Span::raw(path), + Span::styled(&self.diag.message, style), + Span::styled(code, style), + ]) + } else { + Row::new(vec![ + Span::raw(path), + Span::styled(&self.diag.message, style), + Span::styled(code, style), + ]) + } } } @@ -213,11 +272,13 @@ fn sym_picker( symbols: Vec, current_path: Option, offset_encoding: OffsetEncoding, + editor: &Editor, ) -> FilePicker { // TODO: drop current_path comparison and instead use workspace: bool flag? FilePicker::new( symbols, current_path.clone(), + editor.config().icons.picker.then_some(&editor.icons), move |cx, symbol, action| { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -293,6 +354,7 @@ fn diag_picker( FilePicker::new( flat_diag, (styles, format), + cx.editor.config().icons.picker.then_some(&cx.editor.icons), move |cx, PickerDiagnostic { url, diag }, action| { if current_path.as_ref() == Some(url) { let (view, doc) = current!(cx.editor); @@ -371,7 +433,7 @@ pub fn symbol_picker(cx: &mut Context) { } }; - let picker = sym_picker(symbols, current_url, offset_encoding); + let picker = sym_picker(symbols, current_url, offset_encoding, editor); compositor.push(Box::new(overlaid(picker))) } }, @@ -394,9 +456,9 @@ pub fn workspace_symbol_picker(cx: &mut Context) { cx.callback( future, - move |_editor, compositor, response: Option>| { + move |editor, compositor, response: Option>| { let symbols = response.unwrap_or_default(); - let picker = sym_picker(symbols, current_url, offset_encoding); + let picker = sym_picker(symbols, current_url, offset_encoding, editor); let get_symbols = |query: String, editor: &mut Editor| { let doc = doc!(editor); let language_server = match doc.language_server() { @@ -476,7 +538,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { impl ui::menu::Item for lsp::CodeActionOrCommand { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { match self { lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(), lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(), @@ -672,7 +734,7 @@ pub fn code_action(cx: &mut Context) { impl ui::menu::Item for lsp::Command { type Data = (); - fn format(&self, _data: &Self::Data) -> Row { + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> Row { self.title.as_str().into() } } @@ -950,6 +1012,7 @@ fn goto_impl( let picker = FilePicker::new( locations, cwdir, + None, move |cx, location, action| { jump_to_location(cx.editor, location, offset_encoding, action) }, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1d4b6285..e95aaec4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -115,7 +115,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> let callback = async move { let call: job::Callback = job::Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::file_picker(path, &editor.config()); + let picker = ui::file_picker(path, &editor.config(), &editor.icons); compositor.push(Box::new(overlaid(picker))); }, )); @@ -1356,7 +1356,7 @@ fn lsp_workspace_command( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::Picker::new(commands, (), |cx, command, _action| { + let picker = ui::Picker::new(commands, (), None, |cx, command, _action| { execute_lsp_command(cx.editor, command.clone()); }); compositor.push(Box::new(overlaid(picker))) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index bc216509..efd41ef1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -2,6 +2,7 @@ use crate::compositor::{Component, Context, Event, EventResult}; use helix_view::{ document::SavePoint, editor::CompleteAction, + icons::Icons, theme::{Modifier, Style}, ViewId, }; @@ -33,7 +34,8 @@ impl menu::Item for CompletionItem { .into() } - fn format(&self, _data: &Self::Data) -> menu::Row { + // Before implementing icons for the `CompletionItemKind`s, something must be done to `Menu::required_size` and `Menu::recalculate_size` in order to have correct sizes even with icons. + fn format<'a>(&self, _data: &Self::Data, _icons: Option<&'a Icons>) -> menu::Row { let deprecated = self.deprecated.unwrap_or_default() || self.tags.as_ref().map_or(false, |tags| { tags.contains(&lsp::CompletionItemTag::DEPRECATED) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index bdad2e40..be94eeee 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -4,29 +4,29 @@ use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; -use tui::{buffer::Buffer as Surface, widgets::Table}; +use tui::{buffer::Buffer as Surface, text::Span, widgets::Table}; pub use tui::widgets::{Cell, Row}; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; -use helix_view::{graphics::Rect, Editor}; +use helix_view::{graphics::Rect, icons::Icons, Editor}; use tui::layout::Constraint; pub trait Item { /// Additional editor state that is used for label calculation. type Data; - fn format(&self, data: &Self::Data) -> Row; + fn format<'a>(&self, data: &Self::Data, icons: Option<&'a Icons>) -> Row; fn sort_text(&self, data: &Self::Data) -> Cow { - let label: String = self.format(data).cell_text().collect(); + let label: String = self.format(data, None).cell_text().collect(); label.into() } fn filter_text(&self, data: &Self::Data) -> Cow { - let label: String = self.format(data).cell_text().collect(); + let label: String = self.format(data, None).cell_text().collect(); label.into() } } @@ -35,11 +35,15 @@ impl Item for PathBuf { /// Root prefix to strip. type Data = PathBuf; - fn format(&self, root_path: &Self::Data) -> Row { - self.strip_prefix(root_path) + fn format<'a>(&self, root_path: &Self::Data, icons: Option<&'a Icons>) -> Row { + let path_str = self + .strip_prefix(root_path) .unwrap_or(self) - .to_string_lossy() - .into() + .to_string_lossy(); + match icons.and_then(|icons| icons.icon_from_path(Some(self))) { + Some(icon) => Row::new([icon.into(), Span::raw(path_str)]), + None => path_str.into(), + } } } @@ -142,10 +146,10 @@ impl Menu { let n = self .options .first() - .map(|option| option.format(&self.editor_data).cells.len()) + .map(|option| option.format(&self.editor_data, None).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&self.editor_data); + let row = option.format(&self.editor_data, None); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -331,7 +335,7 @@ impl Component for Menu { let rows = options .iter() - .map(|option| option.format(&self.editor_data)); + .map(|option| option.format(&self.editor_data, None)); let table = Table::new(rows) .style(style) .highlight_style(selected) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 80454c0f..abe0c112 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -19,6 +19,7 @@ use crate::filter_picker_entry; use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; +use helix_view::icons::Icons; pub use markdown::Markdown; pub use menu::Menu; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; @@ -158,7 +159,11 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker( + root: PathBuf, + config: &helix_view::editor::Config, + icons: &Icons, +) -> FilePicker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -220,6 +225,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi FilePicker::new( files, root, + config.icons.picker.then_some(icons), move |cx, path: &PathBuf, action| { if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e7a7de90..18ff7313 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -31,6 +31,7 @@ use helix_core::{ use helix_view::{ editor::Action, graphics::{CursorKind, Margin, Modifier, Rect}, + icons::Icons, theme::Style, view::ViewPosition, Document, DocumentId, Editor, @@ -126,11 +127,12 @@ impl FilePicker { pub fn new( options: Vec, editor_data: T::Data, + icons: Option<&'_ Icons>, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { let truncate_start = true; - let mut picker = Picker::new(options, editor_data, callback_fn); + let mut picker = Picker::new(options, editor_data, icons, callback_fn); picker.truncate_start = truncate_start; Self { @@ -424,12 +426,14 @@ pub struct Picker { widths: Vec, callback_fn: PickerCallback, + has_icons: bool, } impl Picker { pub fn new( options: Vec, editor_data: T::Data, + icons: Option<&'_ Icons>, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( @@ -452,9 +456,10 @@ impl Picker { callback_fn: Box::new(callback_fn), completion_height: 0, widths: Vec::new(), + has_icons: icons.is_some(), }; - picker.calculate_column_widths(); + picker.calculate_column_widths(icons); // scoring on empty input // TODO: just reuse score() @@ -472,23 +477,23 @@ impl Picker { picker } - pub fn set_options(&mut self, new_options: Vec) { + pub fn set_options(&mut self, new_options: Vec, icons: &'_ Icons) { self.options = new_options; self.cursor = 0; self.force_score(); - self.calculate_column_widths(); + self.calculate_column_widths(self.has_icons.then_some(icons)); } /// Calculate the width constraints using the maximum widths of each column /// for the current options. - fn calculate_column_widths(&mut self) { + fn calculate_column_widths(&mut self, icons: Option<&'_ Icons>) { let n = self .options .first() - .map(|option| option.format(&self.editor_data).cells.len()) + .map(|option| option.format(&self.editor_data, icons).cells.len()) .unwrap_or_default(); let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&self.editor_data); + let row = option.format(&self.editor_data, icons); // maintain max for each column for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { let width = cell.content.width(); @@ -779,7 +784,12 @@ impl Component for Picker { .skip(offset) .take(rows as usize) .map(|pmatch| &self.options[pmatch.index]) - .map(|option| option.format(&self.editor_data)) + .map(|option| { + option.format( + &self.editor_data, + cx.editor.config().icons.picker.then_some(&cx.editor.icons), + ) + }) .map(|mut row| { const TEMP_CELL_SEP: &str = " "; @@ -953,7 +963,7 @@ impl Component for DynamicPicker { Some(overlay) => &mut overlay.content.file_picker.picker, None => return, }; - picker.set_options(new_options); + picker.set_options(new_options, &editor.icons); editor.reset_idle_timer(); })); anyhow::Ok(callback) diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 076766dd..b9836b3a 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -49,6 +49,7 @@ use helix_core::line_ending::str_is_line_ending; use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::Style; +use helix_view::icons::Icon; use std::borrow::Cow; use unicode_segmentation::UnicodeSegmentation; @@ -208,6 +209,15 @@ impl<'a> From> for Span<'a> { } } +impl<'a, 'b> From<&'b Icon> for Span<'a> { + fn from(icon: &'b Icon) -> Self { + Span { + content: format!("{}", icon.icon_char).into(), + style: icon.style.unwrap_or_default().into(), + } + } +} + /// A string composed of clusters of graphemes, each with their own style. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Spans<'a>(pub Vec>); -- 2.38.5 From d9e342796e0d40e824c1d65c36855f00b454e8c5 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:09:56 +0100 Subject: [PATCH 184/191] feat: handle icons in statusline widget, bufferline and gutter --- helix-term/src/ui/editor.rs | 32 +++++++++++++ helix-term/src/ui/statusline.rs | 81 +++++++++++++++++++++++++++++---- helix-view/src/editor.rs | 3 ++ helix-view/src/gutter.rs | 37 +++++++++------ helix-view/src/icons.rs | 1 + icons.toml | 5 +- runtime/icons/nerdfonts.toml | 7 +-- 7 files changed, 138 insertions(+), 28 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index fd8e8fb2..09050ca8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -531,8 +531,24 @@ impl EditorView { let mut x = viewport.x; let current_doc = view!(editor).doc; + let config = editor.config(); + let icons_enabled = config.icons.bufferline; for doc in editor.documents() { + let filetype_icon = doc + .language_config() + .and_then(|config| { + config + .file_types + .iter() + .map(|filetype| match filetype { + helix_core::syntax::FileType::Extension(s) => s, + helix_core::syntax::FileType::Suffix(s) => s, + }) + .find_map(|filetype| editor.icons.icon_from_filetype(filetype)) + }) + .or_else(|| editor.icons.icon_from_path(doc.path())); + let fname = doc .path() .unwrap_or(&scratch) @@ -551,6 +567,22 @@ impl EditorView { let used_width = viewport.x.saturating_sub(x); let rem_width = surface.area.width.saturating_sub(used_width); + if icons_enabled { + if let Some(icon) = filetype_icon { + x = surface + .set_stringn( + x, + viewport.y, + format!(" {}", icon.icon_char), + rem_width as usize, + match icon.style { + Some(s) => style.patch(s.into()), + None => style, + }, + ) + .0; + } + } x = surface .set_stringn(x, viewport.y, text, rem_width as usize, style) .0; diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88786351..72f716dc 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -4,6 +4,7 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::{ document::{Mode, SCRATCH_BUFFER_NAME}, graphics::Rect, + icons::Icon, theme::Style, Document, Editor, View, }; @@ -21,6 +22,7 @@ pub struct RenderContext<'a> { pub focused: bool, pub spinners: &'a ProgressSpinners, pub parts: RenderBuffer<'a>, + pub icons: RenderContextIcons<'a>, } impl<'a> RenderContext<'a> { @@ -31,6 +33,25 @@ impl<'a> RenderContext<'a> { focused: bool, spinners: &'a ProgressSpinners, ) -> Self { + // Determine icon based on language name if possible + let mut filetype_icon = None; + if let Some(language_config) = doc.language_config() { + for filetype in &language_config.file_types { + let filetype_str = match filetype { + helix_core::syntax::FileType::Extension(s) => s, + helix_core::syntax::FileType::Suffix(s) => s, + }; + filetype_icon = editor.icons.icon_from_filetype(filetype_str); + if filetype_icon.is_some() { + break; + } + } + } + // Otherwise based on filetype + if filetype_icon.is_none() { + filetype_icon = editor.icons.icon_from_path(doc.path()) + } + RenderContext { editor, doc, @@ -38,10 +59,21 @@ impl<'a> RenderContext<'a> { focused, spinners, parts: RenderBuffer::default(), + icons: RenderContextIcons { + enabled: editor.config().icons.statusline, + filetype_icon, + vcs_icon: editor.icons.ui.as_ref().and_then(|ui| ui.get("vcs_branch")), + }, } } } +pub struct RenderContextIcons<'a> { + pub enabled: bool, + pub filetype_icon: Option<&'a Icon>, + pub vcs_icon: Option<&'a Icon>, +} + #[derive(Default)] pub struct RenderBuffer<'a> { pub left: Spans<'a>, @@ -148,6 +180,7 @@ where helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding, helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending, helix_view::editor::StatusLineElement::FileType => render_file_type, + helix_view::editor::StatusLineElement::FileTypeIcon => render_file_type_icon, helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics, helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics, helix_view::editor::StatusLineElement::Selections => render_selections, @@ -240,7 +273,13 @@ where if warnings > 0 { write( context, - "●".to_string(), + context + .editor + .icons + .diagnostic + .warning + .icon_char + .to_string(), Some(context.editor.theme.get("warning")), ); write(context, format!(" {} ", warnings), None); @@ -249,7 +288,7 @@ where if errors > 0 { write( context, - "●".to_string(), + context.editor.icons.diagnostic.error.icon_char.to_string(), Some(context.editor.theme.get("error")), ); write(context, format!(" {} ", errors), None); @@ -282,7 +321,13 @@ where if warnings > 0 { write( context, - "●".to_string(), + context + .editor + .icons + .diagnostic + .warning + .icon_char + .to_string(), Some(context.editor.theme.get("warning")), ); write(context, format!(" {} ", warnings), None); @@ -291,7 +336,7 @@ where if errors > 0 { write( context, - "●".to_string(), + context.editor.icons.diagnostic.error.icon_char.to_string(), Some(context.editor.theme.get("error")), ); write(context, format!(" {} ", errors), None); @@ -412,6 +457,21 @@ where write(context, format!(" {} ", file_type), None); } +fn render_file_type_icon(context: &mut RenderContext, write: F) +where + F: Fn(&mut RenderContext, String, Option