diff --git a/src/app.rs b/src/app.rs index e9e52dd..5a4b494 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,5 @@ use std::{ fs, - io::Read, path::PathBuf, sync::{Arc, mpsc}, thread::JoinHandle, @@ -8,11 +7,12 @@ use std::{ }; use crate::{ + command_palette::{self, CommandPalette}, file_editor::{FileEditor, SaveStatus}, folder::Folder, preferences::Preferences, text_styles::{H1, H1_MONO, H2, H2_MONO, H3, H3_MONO, H4, H4_MONO, H5, H5_MONO, H6, H6_MONO}, - util::{GuiSender, file_mtime, log_error}, + util::{GuiSender, log_error}, }; use egui::{ Align, Button, Context, FontData, FontDefinitions, FontFamily, FontId, Frame, Image, Key, @@ -40,6 +40,9 @@ pub struct App { open_tab_index: Option, next_tab_id: TabId, + + #[serde(skip)] + command_palette: Option, } pub struct Jobs { @@ -129,6 +132,7 @@ impl Default for App { next_tab_id: 2, show_folders: false, folders: vec![], + command_palette: None, } } } @@ -292,6 +296,10 @@ impl eframe::App for App { if input.consume_key(Modifiers::CTRL, Key::S) { self.save_active_tab(ctx); } + + if input.consume_key(Modifiers::CTRL, Key::K) { + self.command_palette = Some(Default::default()); + } }); egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { @@ -310,19 +318,10 @@ impl eframe::App for App { self.jobs.start(ui.ctx(), move || { let file_path = rfd::FileDialog::new().pick_file()?; - let mut file = fs::File::open(&file_path) - .inspect_err(|e| log::error!("Failed to open {file_path:?}: {e}")) - .ok()?; - - let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?; - - let mut text = String::new(); - file.read_to_string(&mut text) - .inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}")) - .ok()?; - - let editor = FileEditor::from_file(file_path, &text, mtime); - Some(Action::OpenFile(editor)) + log_error(eyre!("Failed to open file {file_path:?}"), || { + FileEditor::open_file(file_path) + }) + .map(Action::OpenFile) }); } @@ -445,23 +444,10 @@ impl eframe::App for App { if let Some(file_path) = response.open_file { let file_path = file_path.to_owned(); self.jobs.start(ui.ctx(), move || { - let mut file = fs::File::open(&file_path) - .inspect_err(|e| { - log::error!("Failed to open {file_path:?}: {e}") - }) - .ok()?; - - let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?; - - let mut text = String::new(); - file.read_to_string(&mut text) - .inspect_err(|e| { - log::error!("Failed to read {file_path:?}: {e}") - }) - .ok()?; - - let editor = FileEditor::from_file(file_path, &text, mtime); - Some(Action::OpenFile(editor)) + log_error(eyre!("Failed to open file {file_path:?}"), || { + FileEditor::open_file(file_path) + }) + .map(Action::OpenFile) }); } @@ -489,6 +475,16 @@ impl eframe::App for App { egui::warn_if_debug_build(ui); }); }); + + if let Some(command_palette) = &mut self.command_palette { + match command_palette.show(ctx, &mut self.jobs, &mut self.folders) { + command_palette::Response::None => {} + command_palette::Response::Close => { + self.command_palette = None; + ctx.request_repaint(); + } + }; + } } } diff --git a/src/command_palette.rs b/src/command_palette.rs new file mode 100644 index 0000000..f154402 --- /dev/null +++ b/src/command_palette.rs @@ -0,0 +1,146 @@ +use std::{ + cmp::min, + path::{Path, PathBuf}, +}; + +use egui::{Key, Modifiers, RichText, Ui, Window}; +use eyre::eyre; + +use crate::{ + app::{Action, Jobs}, + file_editor::FileEditor, + folder::Folder, + util::log_error, +}; + +pub struct CommandPalette { + first_frame: bool, + input: String, + results: Vec, + list_i: usize, +} + +impl Default for CommandPalette { + fn default() -> Self { + Self { + first_frame: true, + input: Default::default(), + results: Default::default(), + list_i: 0, + } + } +} + +struct OpenFileCommand { + pretty: String, + path: PathBuf, +} + +pub enum Response { + None, + Close, +} + +impl CommandPalette { + pub fn show( + &mut self, + ctx: &egui::Context, + jobs: &mut Jobs, + folders: &mut [Folder], + ) -> Response { + Window::new("Command Palette") + .collapsible(false) + .show(ctx, |ui| { + let text_edit = ui.text_edit_singleline(&mut self.input); + let mut response = Response::None; + if self.first_frame { + self.first_frame = false; + text_edit.request_focus(); + } + + if text_edit.has_focus() || text_edit.lost_focus() { + ui.input_mut(|input| { + if input.consume_key(Modifiers::NONE, Key::ArrowUp) { + self.list_i = self.list_i.saturating_sub(1); + }; + if input.consume_key(Modifiers::NONE, Key::ArrowDown) { + self.list_i = + min(self.list_i + 1, self.results.len().saturating_sub(1)); + }; + if input.consume_key(Modifiers::NONE, Key::Enter) + && let Some(result) = self.results.get(self.list_i) + { + let path = result.path.clone(); + log::info!("open {}", result.pretty); + jobs.start(ui.ctx(), move || { + log_error(eyre!("Failed to open file {path:?}"), move || { + FileEditor::open_file(path) + }) + .map(Action::OpenFile) + }); + }; + }); + } + + if !text_edit.has_focus() { + log::info!("sad face"); + response = Response::Close; + } + + if text_edit.changed() { + self.results.clear(); + + if !self.input.is_empty() { + self.input = self.input.to_lowercase(); + search_files(ui, &self.input, "".as_ref(), folders, &mut self.results); + } + + self.list_i = min(self.list_i, self.results.len().saturating_sub(1)); + } + + for (i, result) in self.results.iter().enumerate() { + let highlight = i == self.list_i; + ui.horizontal(|ui| { + let mut label = RichText::new("> open"); + if highlight { + label = label.strong().underline(); + } + ui.label(label); + ui.monospace(&result.pretty); + }); + } + + response + }) + .expect("Window is always open") + .inner + .expect("Windows is never collapsed") + } +} + +/// Search the tree of [Folder]s for files matching `query`. +fn search_files( + ui: &mut Ui, + query: &str, + path: &Path, + folders: &mut [Folder], + out: &mut Vec, +) { + for folder in folders { + let Some(folder) = folder.load(ui) else { + continue; + }; + + let path = path.join(&folder.name); + + for file in &folder.child_files { + if file.name.to_lowercase().contains(query) { + let pretty = path.join(&file.name).to_string_lossy().to_string(); + let path = file.path.clone(); + out.push(OpenFileCommand { pretty, path }); + } + } + + search_files(ui, query, &path, &mut folder.child_folders, out); + } +} diff --git a/src/file_editor.rs b/src/file_editor.rs index c25d2b5..c62f461 100644 --- a/src/file_editor.rs +++ b/src/file_editor.rs @@ -2,7 +2,7 @@ use std::{ cmp::Ordering, fmt::{self, Display}, fs::{self, File}, - io::Write, + io::{Read as _, Write}, ops::{Div as _, Sub as _}, path::{Path, PathBuf}, str::FromStr, @@ -13,7 +13,7 @@ use chrono::{DateTime, Local}; use egui::{ Align, Button, Context, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, vec2, }; -use eyre::eyre; +use eyre::{Context as _, eyre}; use notify::{EventKind, Watcher}; use crate::{ @@ -100,7 +100,7 @@ impl FileEditor { } } - pub fn from_file(file_path: PathBuf, contents: &str, mtime: DateTime) -> Self { + pub fn from_path(file_path: PathBuf, contents: &str, mtime: DateTime) -> Self { let file_title = file_path .file_name() .map(|name| name.to_string_lossy().to_string()) @@ -115,6 +115,19 @@ impl FileEditor { } } + pub fn open_file(file_path: PathBuf) -> eyre::Result { + let mut file = + fs::File::open(&file_path).wrap_err_with(|| eyre!("Failed to open {file_path:?}"))?; + + let mtime = file_mtime(&file)?; + + let mut text = String::new(); + file.read_to_string(&mut text) + .wrap_err_with(|| eyre!("Failed to read {file_path:?}"))?; + + Ok(FileEditor::from_path(file_path, &text, mtime)) + } + pub fn title(&self) -> &str { &self.title } diff --git a/src/folder.rs b/src/folder.rs index 5169a33..95ee7b8 100644 --- a/src/folder.rs +++ b/src/folder.rs @@ -158,7 +158,7 @@ impl LoadedFolder { } impl Folder { - fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> { + pub fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> { if let Folder::NotLoaded { name, path } = self { let (tx, rx) = mpsc::channel(); diff --git a/src/lib.rs b/src/lib.rs index 38641e6..4c5de30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod command_palette; pub mod constants; pub mod custom_code_block; pub mod file_editor;