diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2e80c..2f4a216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,14 @@ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [#49](https://github.com/zee-editor/zee/pull/49) - Add [Haskell](https://github.com/tree-sitter/tree-sitter-haskell) syntax highlighting [#62](https://github.com/zee-editor/zee/pull/62) +- Add a custom input component, rather than relying on zi's input. This change + enables reusing zee's text editing functions and makes the editing prompt + input behave similar to text editing in a buffer. + [#62](https://github.com/zee-editor/zee/pull/62) +- Refactor file pickers to use a newly added custom input component. New text + editing bindings were introduced to match buffers. A number of edge cases and + bugs were fixed, e.g. when editing an empty path. + [#62](https://github.com/zee-editor/zee/pull/62) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 763b5ec..bb088a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1501,6 +1501,12 @@ dependencies = [ "time-macros", ] +[[package]] +name = "time-humanize" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e32d019b4f7c100bcd5494e40a27119d45b71fba2b07a4684153129279a4647" + [[package]] name = "time-macros" version = "0.2.4" @@ -1769,6 +1775,7 @@ dependencies = [ "smallstr", "smallvec", "thiserror", + "time-humanize", "tree-sitter", "zee-edit", "zee-grammar", diff --git a/zee-edit/src/lib.rs b/zee-edit/src/lib.rs index db17b06..927121f 100644 --- a/zee-edit/src/lib.rs +++ b/zee-edit/src/lib.rs @@ -96,6 +96,10 @@ impl Cursor { } } + pub fn is_selecting(&self) -> bool { + self.selection.is_some() + } + pub fn column_offset(&self, tab_width: usize, text: &Rope) -> usize { let char_line_start = text.line_to_char(text.cursor_to_line(self)); graphemes::width(tab_width, &text.slice(char_line_start..self.range.start)) diff --git a/zee/Cargo.toml b/zee/Cargo.toml index 8eba078..89f4e30 100644 --- a/zee/Cargo.toml +++ b/zee/Cargo.toml @@ -38,6 +38,7 @@ size_format = "1.0.2" smallstr = "0.3.0" smallvec = "1.9.0" thiserror = "1.0.31" +time-humanize = "0.1.3" tree-sitter = "0.20.8" zi = "0.3.2" zi-term = "0.3.2" diff --git a/zee/src/components/input.rs b/zee/src/components/input.rs new file mode 100644 index 0000000..0a38d82 --- /dev/null +++ b/zee/src/components/input.rs @@ -0,0 +1,297 @@ +use ropey::Rope; + +use zi::{ + unicode_width::UnicodeWidthStr, AnyCharacter, Bindings, Callback, Canvas, Component, + ComponentLink, Key, Layout, Rect, ShouldRender, Style, +}; + +use crate::editor::ContextHandle; +use zee_edit::{graphemes::RopeGraphemes, movement, Cursor, Direction}; + +#[derive(Clone, PartialEq)] +pub struct InputProperties { + pub context: ContextHandle, + pub style: InputStyle, + pub content: Rope, + pub cursor: Cursor, + pub on_change: Option>, + pub focused: bool, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct InputStyle { + pub content: Style, + pub cursor: Style, +} + +#[derive(Clone, Debug)] +pub struct InputChange { + pub content: Option, + pub cursor: Cursor, +} + +pub struct Input { + properties: InputProperties, + frame: Rect, +} + +impl Component for Input { + type Message = Message; + type Properties = InputProperties; + + fn create(properties: Self::Properties, frame: Rect, _link: ComponentLink) -> Self { + Self { properties, frame } + } + + fn change(&mut self, properties: Self::Properties) -> ShouldRender { + let should_render = (self.properties != properties).into(); + self.properties = properties; + should_render + } + + fn resize(&mut self, frame: Rect) -> ShouldRender { + self.frame = frame; + ShouldRender::Yes + } + + fn update(&mut self, message: Self::Message) -> ShouldRender { + let mut cursor = self.properties.cursor.clone(); + let mut content_change = None; + let content = &self.properties.content; + match message { + // Movement + Message::Move(direction, count) => { + movement::move_horizontally(content, &mut cursor, direction, count); + } + Message::MoveWord(direction, count) => { + movement::move_word(content, &mut cursor, direction, count) + } + Message::StartOfLine => { + movement::move_to_start_of_line(content, &mut cursor); + } + Message::EndOfLine => { + movement::move_to_end_of_line(content, &mut cursor); + } + + // Insertion + Message::InsertChar { character } => { + let mut new_content = content.clone(); + cursor.insert_char(&mut new_content, character); + movement::move_horizontally(&new_content, &mut cursor, Direction::Forward, 1); + content_change = Some(new_content); + } + + // Deletion + Message::DeleteBackward => { + let mut new_content = content.clone(); + cursor.delete_backward(&mut new_content); + content_change = Some(new_content); + } + Message::DeleteForward => { + let mut new_content = content.clone(); + cursor.delete_forward(&mut new_content); + content_change = Some(new_content); + } + Message::DeleteLine => { + let mut new_content = content.clone(); + cursor.delete_line(&mut new_content); + content_change = Some(new_content); + } + + // Selection + Message::BeginSelection => { + if cursor.is_selecting() { + cursor.clear_selection(); + } else { + cursor.begin_selection(); + } + } + Message::SelectAll => { + cursor.select_all(content); + } + + Message::Yank => { + let clipboard_str = self.properties.context.clipboard.get_contents().unwrap(); + if !clipboard_str.is_empty() { + let mut new_content = content.clone(); + cursor.insert_chars(&mut new_content, clipboard_str.chars()); + movement::move_horizontally( + &new_content, + &mut cursor, + Direction::Forward, + clipboard_str.chars().count(), + ); + content_change = Some(new_content); + } + } + Message::CopySelection => { + let selection = cursor.selection(); + self.properties + .context + .clipboard + .set_contents(content.slice(selection.start..selection.end).into()) + .unwrap(); + cursor.clear_selection(); + } + Message::CutSelection => { + let mut new_content = content.clone(); + let operation = cursor.delete_selection(&mut new_content); + self.properties + .context + .clipboard + .set_contents(operation.deleted.into()) + .unwrap(); + content_change = Some(new_content); + } + } + + if let Some(on_change) = self.properties.on_change.as_ref() { + on_change.emit(InputChange { + cursor, + content: content_change, + }); + } + + ShouldRender::Yes + } + + fn view(&self) -> Layout { + let Self { + properties: + InputProperties { + ref content, + ref cursor, + ref style, + .. + }, + .. + } = *self; + + let mut canvas = Canvas::new(self.frame.size); + canvas.clear(style.content); + + let mut char_offset = 0; + let mut visual_offset = 0; + for grapheme in RopeGraphemes::new(&content.slice(..)) { + let len_chars = grapheme.len_chars(); + // TODO: don't unwrap (need to be able to create a smallstring from a rope slice) + let grapheme = grapheme.as_str().unwrap(); + let grapheme_width = UnicodeWidthStr::width(grapheme); + + canvas.draw_str( + visual_offset, + 0, + if cursor.selection().contains(&char_offset) { + style.cursor + } else { + style.content + }, + if grapheme_width > 0 { grapheme } else { " " }, + ); + visual_offset += grapheme_width; + char_offset += len_chars; + } + + if cursor.range().start == char_offset { + canvas.draw_str(visual_offset, 0, style.cursor, " "); + } + + canvas.into() + } + + fn bindings(&self, bindings: &mut Bindings) { + use Key::*; + + bindings.set_focus(self.properties.focused); + if !bindings.is_empty() { + return; + } + + // Movement + bindings + .command("move-backward", || Message::Move(Direction::Backward, 1)) + .with([Ctrl('b')]) + .with([Left]); + bindings + .command("move-forward", || Message::Move(Direction::Forward, 1)) + .with([Ctrl('f')]) + .with([Right]); + bindings + .command("move-backward-word", || { + Message::MoveWord(Direction::Backward, 1) + }) + .with([Alt('b')]); + bindings + .command("move-forward-word", || { + Message::MoveWord(Direction::Forward, 1) + }) + .with([Alt('f')]); + bindings + .command("start-of-line", || Message::StartOfLine) + .with([Ctrl('a')]) + .with([Home]); + bindings + .command("end-of-line", || Message::EndOfLine) + .with([Ctrl('e')]) + .with([End]); + + // Selection + // + // Begin selection + bindings + .command("begin-selection", || Message::BeginSelection) + .with([Null]) + .with([Ctrl(' ')]); + + // Select all + bindings.add("select-all", [Ctrl('x'), Char('h')], || Message::SelectAll); + // Copy selection to clipboard + bindings.add("copy-selection", [Alt('w')], || Message::CopySelection); + // Cut selection to clipboard + bindings.add("cut-selection", [Ctrl('w')], || Message::CutSelection); + // Paste from clipboard + bindings.add("paste-clipboard", [Ctrl('y')], || Message::Yank); + + // Editing + bindings + .command("delete-forward", || Message::DeleteForward) + .with([Ctrl('d')]) + .with([Delete]); + bindings.add("delete-backward", [Backspace], || Message::DeleteBackward); + bindings.add("delete-line", [Ctrl('k')], || Message::DeleteLine); + + bindings.add( + "insert-character", + AnyCharacter, + |keys: &[Key]| match keys { + &[Char(character)] + if character != '\n' && character != '\r' && character != '\t' => + { + Some(Message::InsertChar { character }) + } + _ => None, + }, + ); + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Message { + // Movement + Move(Direction, usize), + MoveWord(Direction, usize), + StartOfLine, + EndOfLine, + + // Editing + BeginSelection, + SelectAll, + Yank, + CopySelection, + CutSelection, + + DeleteForward, + DeleteBackward, + DeleteLine, + InsertChar { character: char }, +} diff --git a/zee/src/components/mod.rs b/zee/src/components/mod.rs index 3db0a74..fe41beb 100644 --- a/zee/src/components/mod.rs +++ b/zee/src/components/mod.rs @@ -1,5 +1,6 @@ pub mod buffer; pub mod edit_tree_viewer; +pub mod input; pub mod prompt; pub mod splash; pub mod theme; diff --git a/zee/src/components/prompt/mod.rs b/zee/src/components/prompt/mod.rs index a2a53ce..00126d4 100644 --- a/zee/src/components/prompt/mod.rs +++ b/zee/src/components/prompt/mod.rs @@ -130,7 +130,7 @@ impl Component for Prompt { BufferPicker::with(BufferPickerProperties { message: message.clone(), - context: self.properties.context.clone(), + context: self.properties.context, theme: self.properties.theme.clone(), entries: entries.clone(), on_select: on_select.clone(), @@ -142,7 +142,7 @@ impl Component for Prompt { on_change_height, on_open, } => FilePicker::with(FilePickerProperties { - context: self.properties.context.clone(), + context: self.properties.context, theme: self.properties.theme.clone(), source: *source, on_open: on_open.clone(), diff --git a/zee/src/components/prompt/picker.rs b/zee/src/components/prompt/picker.rs index 01190af..dfe8e8f 100644 --- a/zee/src/components/prompt/picker.rs +++ b/zee/src/components/prompt/picker.rs @@ -1,31 +1,36 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use ignore::WalkBuilder; use ropey::Rope; +use size_format::{SizeFormatterBinary, SizeFormatterSI}; use std::{ borrow::Cow, cmp, fmt, fs, - path::{Path, PathBuf}, + ops::Deref, + path::{Path, PathBuf, MAIN_SEPARATOR}, rc::Rc, + time::SystemTime, }; +use time_humanize::HumanTime; use zi::{ components::{ - input::{Cursor, Input, InputChange, InputProperties, InputStyle}, select::{Select, SelectProperties}, - text::{Text, TextProperties}, + text::{Text, TextAlign, TextProperties}, }, prelude::*, Callback, }; +use zee_edit::{movement, Cursor, Direction}; + use super::{ status::{Status, StatusProperties}, Theme, PROMPT_MAX_HEIGHT, }; use crate::{ + components::input::{Input, InputChange, InputProperties, InputStyle}, editor::ContextHandle, error::{Context as _Context, Result}, task::TaskId, - utils::ensure_trailing_newline_with_content, }; #[derive(Debug)] @@ -44,7 +49,7 @@ impl FileSource { fn status_name(&self) -> Cow<'static, str> { match self { Self::Directory => "open", - Self::Repository => "repo", + Self::Repository => "find", } .into() } @@ -82,19 +87,23 @@ pub struct FilePicker { } impl FilePicker { - fn list_files(&mut self, source: FileSource) { - let link = self.link.clone(); - let input = self.input.clone(); - let mut listing = (*self.listing).clone(); - self.current_task_id = Some(self.properties.context.task_pool.spawn(move |task_id| { - let path_str = input.to_string(); - link.send(Message::FileListingDone(match source { - FileSource::Directory => pick_from_directory(&mut listing, path_str) - .map(|_| FileListingDone { task_id, listing }), - FileSource::Repository => pick_from_repository(&mut listing, path_str) + fn list_files(&mut self) { + let task = { + let link = self.link.clone(); + let path_str = self.input.to_string(); + let mut listing = (*self.listing).clone(); + let source = self.properties.source; + move |task_id| { + link.send(Message::FileListingDone( + match source { + FileSource::Directory => pick_from_directory(&mut listing, path_str), + FileSource::Repository => pick_from_repository(&mut listing, path_str), + } .map(|_| FileListingDone { task_id, listing }), - })) - })) + )) + } + }; + self.current_task_id = Some(self.properties.context.task_pool.spawn(task)); } fn height(&self) -> usize { @@ -107,16 +116,14 @@ impl Component for FilePicker { type Properties = Properties; fn create(properties: Self::Properties, _frame: Rect, link: ComponentLink) -> Self { + let input = { + let mut current_working_dir = + Rope::from(properties.context.current_working_dir.to_string_lossy()); + current_working_dir.insert_char(current_working_dir.len_chars(), MAIN_SEPARATOR); + current_working_dir + }; let mut cursor = Cursor::new(); - let mut current_working_dir: String = properties - .context - .current_working_dir - .to_string_lossy() - .into(); - current_working_dir.push('/'); - current_working_dir.push('\n'); - let input = current_working_dir.into(); - cursor.move_to_end_of_line(&input); + movement::move_to_end_of_line(&input, &mut cursor); let mut picker = Self { properties, @@ -127,18 +134,18 @@ impl Component for FilePicker { selected_index: 0, current_task_id: None, }; - picker.list_files(picker.properties.source); + picker.list_files(); picker.properties.on_change_height.emit(picker.height()); picker } fn change(&mut self, properties: Self::Properties) -> ShouldRender { - if self.properties.source != properties.source { - self.list_files(properties.source); - } - let should_render = (self.properties.theme != properties.theme).into(); + let should_relist_files = self.properties.source != properties.source; self.properties = properties; - should_render + if should_relist_files { + self.list_files(); + } + ShouldRender::Yes } fn update(&mut self, message: Message) -> ShouldRender { @@ -151,26 +158,50 @@ impl Component for FilePicker { false } Message::SelectParentDirectory => { - let path_str: String = self.input.slice(..).into(); - self.input = Path::new(&path_str.trim()) - .parent() - .map(|parent| parent.to_string_lossy()) - .unwrap_or_else(|| "".into()) - .into(); - ensure_trailing_newline_with_content(&mut self.input); - self.cursor.move_to_end_of_line(&self.input); - self.cursor.insert_char(&mut self.input, '/'); - self.cursor.move_right(&self.input); + let new_input = { + let path_str: Cow = self.input.slice(..).into(); + let new_input = Path::new(path_str.deref()); + new_input + .parent() + .unwrap_or(new_input) + .to_str() + .expect("utf-8 path as it's constructed from a utf-8 str") + .into() + }; + + self.input = new_input; + movement::move_to_end_of_buffer(&self.input, &mut self.cursor); + + let ends_with_separator = self + .input + .chars_at(self.input.len_chars()) + .reversed() + .next() + .map(|character| character == MAIN_SEPARATOR) + .unwrap_or(false); + if !ends_with_separator { + self.cursor.insert_char(&mut self.input, MAIN_SEPARATOR); + movement::move_horizontally( + &self.input, + &mut self.cursor, + Direction::Forward, + 1, + ); + } true } Message::AutocompletePath => { if let Some(path) = self.listing.selected(self.selected_index) { self.input = path.to_string_lossy().into(); - ensure_trailing_newline_with_content(&mut self.input); - self.cursor.move_to_end_of_line(&self.input); + movement::move_to_end_of_line(&self.input, &mut self.cursor); if path.is_dir() { - self.cursor.insert_char(&mut self.input, '/'); - self.cursor.move_right(&self.input); + self.cursor.insert_char(&mut self.input, MAIN_SEPARATOR); + movement::move_horizontally( + &self.input, + &mut self.cursor, + Direction::Forward, + 1, + ); } self.selected_index = 0; true @@ -210,7 +241,7 @@ impl Component for FilePicker { }; if input_changed { - self.list_files(self.properties.source); + self.list_files(); } if initial_height != self.height() { @@ -222,6 +253,7 @@ impl Component for FilePicker { fn view(&self) -> Layout { let input = Input::with(InputProperties { + context: self.properties.context, style: InputStyle { content: self.properties.theme.input, cursor: self.properties.theme.cursor, @@ -235,28 +267,78 @@ impl Component for FilePicker { let listing = self.listing.clone(); let selected_index = self.selected_index; let theme = self.properties.theme.clone(); + let now = SystemTime::now(); let item_at = move |index| { - let path = listing.selected(index).unwrap(); + let path = listing.selected(index).unwrap_or_else(|| Path::new("")); let background = if index == selected_index { theme.item_focused_background } else { theme.item_unfocused_background }; - let style = if path.is_dir() { - Style::bold(background, theme.item_directory_foreground) - } else { - Style::normal(background, theme.item_file_foreground) + let (is_dir, formatted_size, formatted_last_modified) = match path.metadata().ok() { + Some(metadata) => ( + metadata.is_dir(), + format!(" {} ", SizeFormatterBinary::new(metadata.len())), + metadata + .modified() + .ok() + .and_then(|last_modified| now.duration_since(last_modified).ok()) + .map(HumanTime::from) + .map(|last_modified| { + last_modified.to_text_en( + time_humanize::Accuracy::Rough, + time_humanize::Tense::Past, + ) + }) + .unwrap_or_else(String::new), + ), + None => (false, String::new(), String::new()), }; - let content = &path.to_string_lossy()[listing - .prefix() + let name = &path.to_string_lossy()[listing + .search_dir() .to_str() - .map(|prefix| prefix.len() + 1) + .map(|prefix| prefix.len()) .unwrap_or(0)..]; - Item::fixed(1)(Text::with_key( - content, - TextProperties::new().content(content).style(style), - )) + + Item::fixed(1)(Container::row([ + Text::item_with_key( + FlexBasis::Auto, + format!("{}-name", name).as_str(), + TextProperties::new().content(name).style(if is_dir { + Style::bold(background, theme.item_directory_foreground) + } else { + Style::normal(background, theme.item_file_foreground) + }), + ), + Text::item_with_key( + FlexBasis::Fixed(16), + format!("{}-size", name).as_str(), + TextProperties::new() + .content(formatted_size) + .style(Style::normal(background, theme.file_size)) + .align(TextAlign::Right), + ), + Text::item_with_key( + FlexBasis::Fixed(40), + format!("{}-last-modified", name).as_str(), + TextProperties::new() + .content(formatted_last_modified) + .style(Style::normal(background, theme.mode)), + ), + ])) }; + + let formatted_num_results = format!( + "{} of {}{}", + SizeFormatterSI::new(u64::try_from(self.listing.num_filtered()).unwrap()), + if self.listing.paths.len() >= MAX_FILES_IN_PICKER { + "≥" + } else { + "" + }, + SizeFormatterSI::new(u64::try_from(self.listing.paths.len()).unwrap()) + ); + Layout::column([ Item::auto(Select::with(SelectProperties { background: Style::normal( @@ -281,6 +363,14 @@ impl Component for FilePicker { TextProperties::new().style(self.properties.theme.input), )), Item::auto(input), + Text::item_with_key( + FlexBasis::Fixed(16), + "num-results", + TextProperties::new() + .content(formatted_num_results) + .style(self.properties.theme.action.invert()) + .align(TextAlign::Right), + ), ])), ]) } @@ -302,11 +392,12 @@ impl Component for FilePicker { } } +/// A list of potentially filtered paths at a given location struct FileListing { paths: Vec, filtered: Vec<(usize, i64)>, // (index, score) matcher: Box, // Boxed as it's big and we store a FileListing in an enum variant - prefix: PathBuf, + search_dir: PathBuf, } impl fmt::Debug for FileListing { @@ -316,7 +407,7 @@ impl fmt::Debug for FileListing { .field("paths", &self.paths) .field("filtered", &self.filtered) .field("matcher", &"SkimMatcherV2(...)") - .field("prefix", &self.prefix) + .field("search_dir", &self.search_dir) .finish() } } @@ -327,7 +418,7 @@ impl Clone for FileListing { paths: self.paths.clone(), filtered: self.filtered.clone(), matcher: Default::default(), - prefix: self.prefix.clone(), + search_dir: self.search_dir.clone(), } } } @@ -338,12 +429,12 @@ impl FileListing { paths: Vec::new(), filtered: Vec::new(), matcher: Default::default(), - prefix: PathBuf::new(), + search_dir: PathBuf::new(), } } - pub fn prefix(&self) -> &Path { - self.prefix.as_path() + pub fn search_dir(&self) -> &Path { + self.search_dir.as_path() } pub fn num_filtered(&self) -> usize { @@ -369,19 +460,14 @@ impl FileListing { pub fn reset( &mut self, paths_iter: impl Iterator, - filter: &str, - prefix_path: impl AsRef, + search_path: &str, + search_dir: impl AsRef, ) { - let Self { - ref mut paths, - ref mut prefix, - .. - } = *self; - paths.clear(); - paths.extend(paths_iter); - prefix.clear(); - prefix.push(prefix_path); - self.set_filter(filter); + self.paths.clear(); + self.paths.extend(paths_iter); + self.search_dir.clear(); + self.search_dir.push(search_dir); + self.set_filter(search_path); } pub fn selected(&self, filtered_index: usize) -> Option<&Path> { @@ -389,71 +475,74 @@ impl FileListing { .get(filtered_index) .map(|(index, _)| self.paths[*index].as_path()) } -} -fn update_listing( - listing: &mut FileListing, - path_str: String, - files_iter: impl FnOnce(String) -> Result, -) -> Result<()> -where - FilesIterT: Iterator, -{ - let prefix = Path::new(&path_str).parent().unwrap(); - if listing.prefix() != prefix { - listing.reset( - files_iter(path_str.clone())?.take(MAX_FILES_IN_PICKER), - &path_str, - &prefix, - ); - } else { - listing.set_filter(&path_str) + fn update( + &mut self, + path_str: String, + files_iter: impl FnOnce(&Path) -> Result, + ) -> Result<()> + where + FilesIterT: Iterator, + { + let search_dir = { + let path = Path::new(&path_str); + if path_str.ends_with(MAIN_SEPARATOR) { + path + } else { + path.parent().unwrap_or(path) + } + }; + + if self.search_dir != search_dir { + self.reset( + files_iter(search_dir)?.take(MAX_FILES_IN_PICKER), + &path_str, + &search_dir, + ); + } else { + self.set_filter(&path_str) + } + Ok(()) } - Ok(()) } -fn pick_from_directory(listing: &mut FileListing, path_str: String) -> Result<()> { - update_listing(listing, path_str, |path| { - Ok(directory_files_iter(path)?.filter_map(|result_path| result_path.ok())) +fn pick_from_directory(listing: &mut FileListing, search_path: String) -> Result<()> { + listing.update(search_path, |search_dir| { + Ok(directory_files_iter(search_dir)?.filter_map(|result_path| result_path.ok())) }) } -fn pick_from_repository(listing: &mut FileListing, path_str: String) -> Result<()> { - update_listing(listing, path_str, |path| { - Ok(repository_files_iter(path).filter_map(|result_path| result_path.ok())) +fn pick_from_repository(listing: &mut FileListing, search_path: String) -> Result<()> { + listing.update(search_path, |search_dir| { + Ok(repository_files_iter(search_dir).filter_map(|result_path| result_path.ok())) }) } -fn directory_files_iter(path: impl AsRef) -> Result>> { - Ok( - fs::read_dir(path.as_ref().parent().unwrap_or_else(|| path.as_ref())).map(|walk| { - walk.map(|entry| { - entry - .map(|entry| entry.path()) - .context("Cannot read entry while walking directory") - }) - })?, - ) +fn directory_files_iter(search_dir: &Path) -> Result>> { + let walk = fs::read_dir(search_dir).context("Cannot read entry while walking directory")?; + Ok(walk.map(|entry| { + entry + .map(|entry| entry.path()) + .context("Cannot read entry while walking directory") + })) } -fn repository_files_iter(path: impl AsRef) -> impl Iterator> { - WalkBuilder::new(path.as_ref().parent().unwrap_or_else(|| path.as_ref())) - .build() - .filter_map(|entry| { - let is_dir = entry - .as_ref() - .map(|entry| entry.path().is_dir()) - .unwrap_or(false); - if entry.is_ok() && !is_dir { - Some( - entry - .map(|entry| entry.path().to_path_buf()) - .context("Cannot read entry while walking directory"), - ) - } else { - None - } - }) +fn repository_files_iter(search_dir: &Path) -> impl Iterator> { + WalkBuilder::new(search_dir).build().filter_map(|entry| { + let is_dir = entry + .as_ref() + .map(|entry| entry.path().is_dir()) + .unwrap_or(false); + if entry.is_ok() && !is_dir { + Some( + entry + .map(|entry| entry.path().to_path_buf()) + .context("Cannot read entry while walking directory"), + ) + } else { + None + } + }) } const MAX_FILES_IN_PICKER: usize = 16384; diff --git a/zee/src/editor/buffer.rs b/zee/src/editor/buffer.rs index 0542ab9..04cbe6d 100644 --- a/zee/src/editor/buffer.rs +++ b/zee/src/editor/buffer.rs @@ -79,13 +79,8 @@ impl Buffers { // Generate a new buffer id let buffer_id = BufferId(self.next_buffer_id); self.next_buffer_id += 1; - self.buffers.push(Buffer::new( - self.context.clone(), - buffer_id, - text, - file_path, - repo, - )); + self.buffers + .push(Buffer::new(self.context, buffer_id, text, file_path, repo)); buffer_id } @@ -353,11 +348,17 @@ impl Buffer { movement::move_paragraph(content, cursor, direction, count) } - CursorMessage::BeginSelection => cursor.begin_selection(), + CursorMessage::BeginSelection => { + self.context.log("Begin selection"); + cursor.begin_selection(); + } CursorMessage::ClearSelection => { cursor.clear_selection(); } - CursorMessage::SelectAll => cursor.select_all(content), + CursorMessage::SelectAll => { + self.context.log("Select all"); + cursor.select_all(content); + } _ => {} } diff --git a/zee/src/editor/mod.rs b/zee/src/editor/mod.rs index fdc1121..c88f4c5 100644 --- a/zee/src/editor/mod.rs +++ b/zee/src/editor/mod.rs @@ -103,7 +103,7 @@ impl Context { } } -#[derive(Clone)] +#[derive(Clone, Copy)] pub struct ContextHandle(pub &'static Context); impl std::ops::Deref for ContextHandle { @@ -114,6 +114,12 @@ impl std::ops::Deref for ContextHandle { } } +impl PartialEq for ContextHandle { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.0, other.0) + } +} + impl Context { pub fn log(&self, message: impl Into) { self.link.send(Message::Log(Some(message.into()))); @@ -264,7 +270,7 @@ impl Component for Editor { theme_index, prompt_action: PromptAction::None, prompt_height: PROMPT_INACTIVE_HEIGHT, - buffers: Buffers::new(context.clone()), + buffers: Buffers::new(context), context, windows: WindowTree::new(), } @@ -409,6 +415,7 @@ impl Component for Editor { } fn view(&self) -> Layout { + let context = self.context; let buffers = if self.windows.is_empty() { Splash::item_with_key( FlexBasis::Auto, @@ -423,7 +430,7 @@ impl Component for Editor { BufferView::with_key( format!("{}.{}", index, id).as_str(), BufferViewProperties { - context: self.context.clone(), + context, theme: Cow::Borrowed(&self.themes[self.theme_index].0.buffer), focused: focused && !self.prompt_action.is_interactive(), frame_id: index.one_based_index(), @@ -454,7 +461,7 @@ impl Component for Editor { }), "prompt", PromptProperties { - context: self.context.clone(), + context, theme: Cow::Borrowed(&self.themes[self.theme_index].0.prompt), action: self.prompt_action.clone(), }, diff --git a/zee/src/utils.rs b/zee/src/utils.rs index 5283823..7a06d33 100644 --- a/zee/src/utils.rs +++ b/zee/src/utils.rs @@ -1,5 +1,3 @@ -use ropey::Rope; - #[derive(Copy)] pub struct StaticRefEq(&'static T); @@ -28,9 +26,3 @@ impl From<&'static T> for StaticRefEq { Self(other) } } - -pub fn ensure_trailing_newline_with_content(text: &mut Rope) { - if text.len_chars() == 0 || text.char(text.len_chars() - 1) != '\n' { - text.insert_char(text.len_chars(), '\n'); - } -} diff --git a/zee/src/versioned.rs b/zee/src/versioned.rs index 6a5350f..6a1355d 100644 --- a/zee/src/versioned.rs +++ b/zee/src/versioned.rs @@ -61,3 +61,16 @@ impl WeakHandle { self.version } } + +impl PartialEq for WeakHandle { + fn eq(&self, other: &Self) -> bool { + // `WeakHandle`s use case is for sharing expensive-to-copy fields with + // children components. Typically, `eq` would be used as part of testing + // whether the `Properties` of a component have changed. + // + // Hence, `PartialEq` tests for referential equality -- i.e. it's the + // same Rc and the same version. Otherwise, they're deemed not equal + // even if `::eq` is true. + std::ptr::eq(self.value.as_ptr(), other.value.as_ptr()) && self.version == other.version + } +}