diff --git a/Cargo.lock b/Cargo.lock index a504240..9938dd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,24 +115,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "document-features" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" -dependencies = [ - "litrs", -] - -[[package]] -name = "dyn-fmt" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c992f591dfce792a9bc2d1880ab67ffd4acc04551f8e551ca3b6233efb322f00" -dependencies = [ - "document-features", -] - [[package]] name = "either" version = "1.13.0" @@ -151,20 +133,19 @@ dependencies = [ [[package]] name = "error_set" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8a70e1c5e3557e22af5af1e78f546303c9953638e60aee2c547322076cfabf" +checksum = "7f6b480d31460f72eda2955b10b81146f68e1e8ef5136d35e559939e2fde07f0" dependencies = [ "error_set_impl", ] [[package]] name = "error_set_impl" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f8c9888cc6d3349076683c776fc48d3b0b8685aa2ca05107cfa1df72445157" +checksum = "d449cd125912cacf5bb583db2a4f8730bea5d625e010ec5f84a939a6740d5cec" dependencies = [ - "dyn-fmt", "indices", "proc-macro2", "quote", @@ -249,12 +230,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - [[package]] name = "lock_api" version = "0.4.12" @@ -311,14 +286,15 @@ dependencies = [ [[package]] name = "mlua" -version = "0.9.9" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" +checksum = "0f6ddbd668297c46be4bdea6c599dcc1f001a129586272d53170b7ac0a62961e" dependencies = [ "bstr", + "either", "mlua-sys", "num-traits", - "once_cell", + "parking_lot", "rustc-hash", ] @@ -344,12 +320,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - [[package]] name = "option-ext" version = "0.2.0" @@ -358,7 +328,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.6.10" +version = "0.7.0" dependencies = [ "alinio", "base64", @@ -637,9 +607,9 @@ dependencies = [ [[package]] name = "synoptic" -version = "2.2.1" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0ed930df773aaac411d90bbf1e20b1538514c981f0dc1231f5feea1ef2910f0" +checksum = "4550857be213a870db94e3cb806b2698de900412ef555515f84772191c063fc9" dependencies = [ "if_chain", "regex", diff --git a/Cargo.toml b/Cargo.toml index 3f2c12f..d1b509d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["cactus"] [package] name = "ox" -version = "0.6.10" +version = "0.7.0" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A Rust powered text editor." @@ -28,6 +28,7 @@ assets = [ ] #[profile.release] +#debug = true #lto = true #panic = "abort" #codegen-units = 1 @@ -38,7 +39,7 @@ base64 = "0.22.1" crossterm = "0.28.1" jargon-args = "0.2.7" kaolinite = { path = "./kaolinite" } -mlua = { version = "0.9.9", features = ["lua54", "vendored"] } -error_set = "0.6" +mlua = { version = "0.10", features = ["lua54", "vendored"] } +error_set = "0.7" shellexpand = "3.1.0" -synoptic = "2.2.1" +synoptic = "2.2.6" diff --git a/build.sh b/build.sh index 47a9510..350e6ba 100644 --- a/build.sh +++ b/build.sh @@ -16,7 +16,7 @@ cargo deb cp target/debian/*.deb target/pkgs/ # Build for macOS (binary) -export SDKROOT=../../make/MacOSX13.3.sdk/ +export SDKROOT=/home/luke/dev/make/MacOSX13.3.sdk/ export PATH=$PATH:~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/ export CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=rust-lld cargo zigbuild --release --target x86_64-apple-darwin diff --git a/config/.oxrc b/config/.oxrc index e4f4365..bc3fe9c 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -271,6 +271,11 @@ event_mapping = { ["ctrl_w"] = function() editor:remove_word() end, + -- Macros + ["ctrl_esc"] = function() + editor:macro_record_stop() + editor:display_info("Macro recorded") + end, } -- Define user-defined commands @@ -303,6 +308,22 @@ commands = { editor:reload_config() editor:display_info("Configuration file reloaded") end, + ["macro"] = function(arguments) + if arguments[1] == "record" then + editor:macro_record_start() + editor:display_info("Recording macro, press ctrl+esc to stop") + elseif arguments[1] == "play" then + local reps + if arguments[2] == nil then + reps = 1 + else + reps = tonumber(arguments[2]) + end + editor:macro_play(reps) + else + editor:display_error(tostring(arguments[1]) .. " is not a valid macro command") + end + end, } -- Configure Documents -- diff --git a/kaolinite/Cargo.toml b/kaolinite/Cargo.toml index 15a8c3a..0511c46 100644 --- a/kaolinite/Cargo.toml +++ b/kaolinite/Cargo.toml @@ -12,8 +12,8 @@ keywords = ["unicode", "text-processing"] categories = ["text-processing"] [dependencies] -error_set = "0.6" -regex = "1.10.6" +error_set = "0.7" +regex = "1" ropey = "1.6.1" unicode-width = "0.2" diff --git a/kaolinite/src/document/cursor.rs b/kaolinite/src/document/cursor.rs index 8e04cf5..b993ea9 100644 --- a/kaolinite/src/document/cursor.rs +++ b/kaolinite/src/document/cursor.rs @@ -186,6 +186,7 @@ impl Document { /// Move up by 1 page pub fn move_page_up(&mut self) { + self.clear_cursors(); // Set x to 0 self.cursor.loc.x = 0; self.char_ptr = 0; @@ -201,6 +202,7 @@ impl Document { /// Move down by 1 page pub fn move_page_down(&mut self) { + self.clear_cursors(); // Set x to 0 self.cursor.loc.x = 0; self.char_ptr = 0; @@ -399,4 +401,24 @@ impl Document { pub fn cancel_selection(&mut self) { self.cursor.selection_end = self.cursor.loc; } + + /// Create a new alternative cursor + pub fn new_cursor(&mut self, loc: Loc) { + if let Some(idx) = self.has_cursor(loc) { + self.secondary_cursors.remove(idx); + } else if self.out_of_range(loc.x, loc.y).is_ok() { + self.secondary_cursors.push(loc); + } + } + + /// Clear all secondary cursors + pub fn clear_cursors(&mut self) { + self.secondary_cursors.clear(); + } + + /// Determine if there is a secondary cursor at a certain position + #[must_use] + pub fn has_cursor(&self, loc: Loc) -> Option { + self.secondary_cursors.iter().position(|c| *c == loc) + } } diff --git a/kaolinite/src/document/disk.rs b/kaolinite/src/document/disk.rs index 3e56f08..79659b5 100644 --- a/kaolinite/src/document/disk.rs +++ b/kaolinite/src/document/disk.rs @@ -42,6 +42,7 @@ impl Document { eol: false, read_only: false, }, + secondary_cursors: vec![], } } @@ -77,6 +78,7 @@ impl Document { tab_width: 4, old_cursor: 0, in_redo: false, + secondary_cursors: vec![], }) } diff --git a/kaolinite/src/document/mod.rs b/kaolinite/src/document/mod.rs index 84c468f..ce73e0e 100644 --- a/kaolinite/src/document/mod.rs +++ b/kaolinite/src/document/mod.rs @@ -50,6 +50,8 @@ pub struct Document { pub in_redo: bool, /// The number of spaces a tab should be rendered as pub tab_width: usize, + /// Secondary cursor (for multi-cursors) + pub secondary_cursors: Vec, } impl Document { @@ -113,6 +115,7 @@ impl Document { /// # Errors /// Returns an error if there is a problem with the specified operation. pub fn forth(&mut self, ev: Event) -> Result<()> { + // Perform the event match ev { Event::Insert(loc, ch) => self.insert(&loc, &ch), Event::Delete(loc, st) => self.delete_with_tab(&loc, &st), @@ -256,9 +259,15 @@ impl Document { // Account for double width characters idx = idx.saturating_sub(self.dbl_map.count(loc, true).unwrap_or(0)); // Account for tab characters - idx = idx.saturating_sub( - self.tab_map.count(loc, true).unwrap_or(0) * self.tab_width.saturating_sub(1), - ); + let tabs_behind = self.tab_map.count(loc, true).unwrap_or(0); + idx = if let Some(inner_idx) = self.tab_map.inside(self.tab_width, loc.x, loc.y) { + // Display index is within a tab, account for it properly + let existing_tabs = tabs_behind.saturating_sub(1) * self.tab_width.saturating_sub(1); + idx.saturating_sub(existing_tabs + inner_idx) + } else { + // Display index isn't in a tab + idx.saturating_sub(tabs_behind * self.tab_width.saturating_sub(1)) + }; idx } diff --git a/kaolinite/src/map.rs b/kaolinite/src/map.rs index 476e126..a7578f8 100644 --- a/kaolinite/src/map.rs +++ b/kaolinite/src/map.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use unicode_width::UnicodeWidthChar; /// This is a type for making a note of the location of different characters +/// `HashMap`<`y_pos`, Vec<(display, character)>> type CharHashMap = HashMap>; /// Keeps notes of specific characters within a document for the purposes of double width and @@ -166,6 +167,18 @@ impl CharMap { } Some(ctr) } + + /// If all character maps are of size n, then determine if x would be within one, + /// and return their index inside the mapped char + #[must_use] + pub fn inside(&self, n: usize, x: usize, y: usize) -> Option { + for (disp, _) in self.get(y)? { + if ((disp + 1)..(disp + n)).contains(&x) { + return Some(x.saturating_sub(*disp)); + } + } + None + } } /// Vector that takes two usize values diff --git a/src/config/assistant.rs b/src/config/assistant.rs index 049fb5f..87df0dd 100644 --- a/src/config/assistant.rs +++ b/src/config/assistant.rs @@ -8,9 +8,7 @@ use crossterm::execute; use crossterm::style::{SetBackgroundColor as Bg, SetForegroundColor as Fg}; use crossterm::terminal::{Clear, ClearType}; use mlua::prelude::*; -use std::cell::RefCell; use std::io::{stdout, Write}; -use std::rc::Rc; pub const TROPICAL: &str = include_str!("../../plugins/themes/tropical.lua"); pub const GALAXY: &str = include_str!("../../plugins/themes/galaxy.lua"); @@ -571,14 +569,14 @@ impl Assistant { pub fn demonstrate_theme(name: &str, code: &str) -> Result { // Create an environment to capture all the values let lua = Lua::new(); - let colors = Rc::new(RefCell::new(Colors::default())); - let syntax_highlighting = Rc::new(RefCell::new(SyntaxHighlighting::default())); + let colors = lua.create_userdata(Colors::default())?; + let syntax_highlighting = lua.create_userdata(SyntaxHighlighting::default())?; lua.globals().set("syntax", syntax_highlighting.clone())?; lua.globals().set("colors", colors.clone())?; // Access all the values lua.load(code).exec()?; // Gather the editor colours - let col = colors.borrow(); + let col: LuaUserDataRef = colors.borrow()?; let editor = format!( "{}{}", Fg(col.editor_fg.to_color()?), @@ -625,28 +623,29 @@ impl Assistant { Bg(col.info_bg.to_color()?) ); // Gather syntax highlighting colours - let string = Fg(syntax_highlighting.borrow().get_theme("string")?); - let comment = Fg(syntax_highlighting.borrow().get_theme("comment")?); - let digit = Fg(syntax_highlighting.borrow().get_theme("digit")?); - let keyword = Fg(syntax_highlighting.borrow().get_theme("keyword")?); - let character = Fg(syntax_highlighting.borrow().get_theme("character")?); - let type_syn = Fg(syntax_highlighting.borrow().get_theme("type")?); - let function = Fg(syntax_highlighting.borrow().get_theme("function")?); - let macro_syn = Fg(syntax_highlighting.borrow().get_theme("macro")?); - let block = Fg(syntax_highlighting.borrow().get_theme("block")?); - let namespace = Fg(syntax_highlighting.borrow().get_theme("namespace")?); - let header = Fg(syntax_highlighting.borrow().get_theme("header")?); - let struct_syn = Fg(syntax_highlighting.borrow().get_theme("struct")?); - let operator = Fg(syntax_highlighting.borrow().get_theme("operator")?); - let boolean = Fg(syntax_highlighting.borrow().get_theme("boolean")?); - let reference = Fg(syntax_highlighting.borrow().get_theme("reference")?); - let tag = Fg(syntax_highlighting.borrow().get_theme("tag")?); - let heading = Fg(syntax_highlighting.borrow().get_theme("heading")?); - let link = Fg(syntax_highlighting.borrow().get_theme("link")?); - let bold = Fg(syntax_highlighting.borrow().get_theme("bold")?); - let italic = Fg(syntax_highlighting.borrow().get_theme("italic")?); - let insertion = Fg(syntax_highlighting.borrow().get_theme("insertion")?); - let deletion = Fg(syntax_highlighting.borrow().get_theme("deletion")?); + let syn: LuaUserDataRef = syntax_highlighting.borrow()?; + let string = Fg(syn.get_theme("string")?); + let comment = Fg(syn.get_theme("comment")?); + let digit = Fg(syn.get_theme("digit")?); + let keyword = Fg(syn.get_theme("keyword")?); + let character = Fg(syn.get_theme("character")?); + let type_syn = Fg(syn.get_theme("type")?); + let function = Fg(syn.get_theme("function")?); + let macro_syn = Fg(syn.get_theme("macro")?); + let block = Fg(syn.get_theme("block")?); + let namespace = Fg(syn.get_theme("namespace")?); + let header = Fg(syn.get_theme("header")?); + let struct_syn = Fg(syn.get_theme("struct")?); + let operator = Fg(syn.get_theme("operator")?); + let boolean = Fg(syn.get_theme("boolean")?); + let reference = Fg(syn.get_theme("reference")?); + let tag = Fg(syn.get_theme("tag")?); + let heading = Fg(syn.get_theme("heading")?); + let link = Fg(syn.get_theme("link")?); + let bold = Fg(syn.get_theme("bold")?); + let italic = Fg(syn.get_theme("italic")?); + let insertion = Fg(syn.get_theme("insertion")?); + let deletion = Fg(syn.get_theme("deletion")?); // Render the preview let name = format!(" {name} "); Ok(format!("{name:─^47} diff --git a/src/config/colors.rs b/src/config/colors.rs index 0eb648d..8d81b23 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -1,5 +1,5 @@ -use crate::error::{OxError, Result}; /// For dealing with colours in the configuration file +use crate::error::{OxError, Result}; use crate::ui::{rgb_to_xterm256, supports_true_color}; use crossterm::style::Color as CColor; use mlua::prelude::*; @@ -69,7 +69,7 @@ impl Default for Colors { impl LuaUserData for Colors { #[allow(clippy::too_many_lines)] - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("editor_bg", |env, this| Ok(this.editor_bg.to_lua(env))); fields.add_field_method_get("editor_fg", |env, this| Ok(this.editor_fg.to_lua(env))); fields.add_field_method_get("status_bg", |env, this| Ok(this.status_bg.to_lua(env))); @@ -210,9 +210,9 @@ pub enum Color { impl Color { /// Converts from a lua value into a colour - pub fn from_lua(value: LuaValue<'_>) -> Self { + pub fn from_lua(value: LuaValue) -> Self { match value { - LuaValue::String(string) => match string.to_str().unwrap_or("transparent") { + LuaValue::String(string) => match string.to_string_lossy().as_str() { "black" => Self::Black, "darkgrey" => Self::DarkGrey, "red" => Self::Red, @@ -267,7 +267,7 @@ impl Color { } /// Converts from a colour into a lua value - pub fn to_lua<'a>(&self, env: &'a Lua) -> LuaValue<'a> { + pub fn to_lua(&self, env: &Lua) -> LuaValue { let msg = "Failed to create lua string"; match self { Color::Hex(hex) => { diff --git a/src/config/editor.rs b/src/config/editor.rs index 55415e7..84c06f4 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -2,13 +2,13 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; -use crate::{fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; +use crate::{config, fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; use kaolinite::{Loc, Size}; use mlua::prelude::*; impl LuaUserData for Editor { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("cursor", |_, editor| { if let Some(doc) = editor.try_doc() { let loc = doc.char_loc(); @@ -82,10 +82,14 @@ impl LuaUserData for Editor { } }); fields.add_field_method_get("cwd", |_, _| Ok(get_cwd())); + fields.add_field_method_get("macro_recording", |_, editor| { + Ok(editor.macro_man.recording) + }); + fields.add_field_method_get("macro_playing", |_, editor| Ok(editor.macro_man.playing)); } #[allow(clippy::too_many_lines)] - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + fn add_methods>(methods: &mut M) { // Debugging methods methods.add_method_mut("panic", |_, _, msg: String| { fatal_error(&msg); @@ -561,7 +565,8 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("set_file_type", |_, editor, name: String| { - if let Some(file_type) = editor.config.document.borrow().file_types.get_name(&name) { + let doc = config!(editor.config, document); + if let Some(file_type) = doc.file_types.get_name(&name) { let mut highlighter = file_type.get_highlighter(&editor.config, 4); highlighter.run(&editor.doc().lines); editor.files[editor.ptr].highlighter = highlighter; @@ -625,6 +630,19 @@ impl LuaUserData for Editor { } Ok(()) }); + methods.add_method_mut("macro_record_start", |_, editor, ()| { + editor.macro_man.record(); + Ok(()) + }); + methods.add_method_mut("macro_record_stop", |_, editor, ()| { + editor.macro_man.finish(); + Ok(()) + }); + methods.add_method_mut("macro_play", |_, editor, times: usize| { + editor.macro_man.finish(); + editor.macro_man.play(times); + Ok(()) + }); } } @@ -634,9 +652,9 @@ pub struct LuaLoc { y: usize, } -impl IntoLua<'_> for LuaLoc { +impl IntoLua for LuaLoc { /// Convert this rust struct so the plug-in and configuration system can use it - fn into_lua(self, lua: &Lua) -> std::result::Result, LuaError> { + fn into_lua(self, lua: &Lua) -> std::result::Result { let table = lua.create_table()?; table.set("x", self.x)?; table.set("y", self.y)?; diff --git a/src/config/highlighting.rs b/src/config/highlighting.rs index bb36955..b08cf40 100644 --- a/src/config/highlighting.rs +++ b/src/config/highlighting.rs @@ -68,7 +68,7 @@ impl SyntaxHighlighting { } impl LuaUserData for SyntaxHighlighting { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + fn add_methods>(methods: &mut M) { methods.add_method_mut( "keywords", |lua, _, (name, pattern): (String, Vec)| { @@ -120,7 +120,7 @@ impl LuaUserData for SyntaxHighlighting { // Add rules one by one for rule_idx in 1..=(rules.len()?) { // Get rule - let rule = rules.get::>(rule_idx)?; + let rule = rules.get::>(rule_idx)?; // Find type of rule and attatch it to the highlighter match rule["kind"].as_str() { "keyword" => { diff --git a/src/config/interface.rs b/src/config/interface.rs index de6e38c..31768c4 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -28,7 +28,7 @@ impl Default for Terminal { } impl LuaUserData for Terminal { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("mouse_enabled", |_, this| Ok(this.mouse_enabled)); fields.add_field_method_set("mouse_enabled", |_, this, value| { this.mouse_enabled = value; @@ -61,7 +61,7 @@ impl Default for LineNumbers { } impl LuaUserData for LineNumbers { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); fields.add_field_method_set("enabled", |_, this, value| { this.enabled = value; @@ -114,9 +114,9 @@ impl GreetingMessage { .skip(1) .take(m.text.chars().count().saturating_sub(2)) .collect::(); - if let Ok(func) = lua.globals().get::(name) { - if let Ok(r) = func.call::<(), LuaString>(()) { - result = result.replace(&m.text, r.to_str().unwrap_or("")); + if let Ok(func) = lua.globals().get::(name) { + if let Ok(r) = func.call::(()) { + result = result.replace(&m.text, r.to_string_lossy().as_str()); } else { break; } @@ -129,7 +129,7 @@ impl GreetingMessage { } impl LuaUserData for GreetingMessage { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); fields.add_field_method_set("enabled", |_, this, value| { this.enabled = value; @@ -175,9 +175,9 @@ impl HelpMessage { .skip(1) .take(m.text.chars().count().saturating_sub(2)) .collect::(); - if let Ok(func) = lua.globals().get::(name) { - if let Ok(r) = func.call::<(), LuaString>(()) { - message = message.replace(&m.text, r.to_str().unwrap_or("")); + if let Ok(func) = lua.globals().get::(name) { + if let Ok(r) = func.call::(()) { + message = message.replace(&m.text, r.to_string_lossy().as_str()); } else { break; } @@ -203,7 +203,7 @@ impl HelpMessage { } impl LuaUserData for HelpMessage { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); fields.add_field_method_set("enabled", |_, this, value| { this.enabled = value; @@ -270,10 +270,10 @@ impl TabLine { .skip(1) .take(m.text.chars().count().saturating_sub(2)) .collect::(); - if let Ok(func) = lua.globals().get::(name) { - match func.call::(absolute_path.clone()) { + if let Ok(func) = lua.globals().get::(name) { + match func.call::(absolute_path.clone()) { Ok(r) => { - result = result.replace(&m.text, r.to_str().unwrap_or("")); + result = result.replace(&m.text, r.to_string_lossy().as_str()); } Err(e) => { *feedback = Feedback::Error(format!("Error occured in tab line: {e:?}")); @@ -289,7 +289,7 @@ impl TabLine { } impl LuaUserData for TabLine { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); fields.add_field_method_set("enabled", |_, this, value| { this.enabled = value; @@ -373,9 +373,9 @@ impl StatusLine { .skip(1) .take(m.text.chars().count().saturating_sub(2)) .collect::(); - if let Ok(func) = lua.globals().get::(name) { - let r = func.call::(absolute_path.clone())?; - part = part.replace(&m.text, r.to_str().unwrap_or("")); + if let Ok(func) = lua.globals().get::(name) { + let r = func.call::(absolute_path.clone())?; + part = part.replace(&m.text, r.to_string_lossy().as_str()); } else { break; } @@ -392,7 +392,7 @@ impl StatusLine { } impl LuaUserData for StatusLine { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("parts", |lua, this| { let parts = lua.create_table()?; for (i, part) in this.parts.iter().enumerate() { diff --git a/src/config/keys.rs b/src/config/keys.rs index 8a29fb9..894d861 100644 --- a/src/config/keys.rs +++ b/src/config/keys.rs @@ -42,7 +42,7 @@ pub fn run_key_before(mut key: &str) -> String { } /// This contains code for getting event listeners -pub fn get_listeners<'a>(name: &'a str, lua: &'a Lua) -> Result>, OxError> { +pub fn get_listeners(name: &str, lua: &Lua) -> Result, OxError> { let mut result = vec![]; let listeners: LuaTable = lua .load(format!("(global_event_mapping[\"{name}\"] or {{}})")) diff --git a/src/config/mod.rs b/src/config/mod.rs index 93a3b10..380d831 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,12 +2,8 @@ use crate::editor::{FileType, FileTypes}; use crate::error::{OxError, Result}; use mlua::prelude::*; +use std::fmt::{Display, Error, Formatter}; use std::sync::{Arc, Mutex}; -use std::{ - cell::RefCell, - fmt::{Display, Error, Formatter}, - rc::Rc, -}; mod assistant; mod colors; @@ -49,18 +45,60 @@ pub const PLUGIN_NETWORKING: &str = include_str!("../plugin/networking.lua"); /// This contains the code for running the plugins pub const PLUGIN_MANAGER: &str = include_str!("../plugin/plugin_manager.lua"); +/// A nice macro to quickly interpret configuration +#[macro_export] +macro_rules! config { + ($cfg:expr, document) => { + $cfg.document.borrow::<$crate::config::Document>().unwrap() + }; + ($cfg:expr, colors) => { + $cfg.colors.borrow::<$crate::config::Colors>().unwrap() + }; + ($cfg:expr, syntax) => { + $cfg.syntax_highlighting + .borrow::<$crate::config::SyntaxHighlighting>() + .unwrap() + }; + ($cfg:expr, line_numbers) => { + $cfg.line_numbers + .borrow::<$crate::config::LineNumbers>() + .unwrap() + }; + ($cfg:expr, status_line) => { + $cfg.status_line + .borrow::<$crate::config::StatusLine>() + .unwrap() + }; + ($cfg:expr, tab_line) => { + $cfg.tab_line.borrow::<$crate::config::TabLine>().unwrap() + }; + ($cfg:expr, greeting_message) => { + $cfg.greeting_message + .borrow::<$crate::config::GreetingMessage>() + .unwrap() + }; + ($cfg:expr, help_message) => { + $cfg.help_message + .borrow::<$crate::config::HelpMessage>() + .unwrap() + }; + ($cfg:expr, terminal) => { + $cfg.terminal.borrow::<$crate::config::Terminal>().unwrap() + }; +} + /// The struct that holds all the configuration information #[derive(Debug)] pub struct Config { - pub syntax_highlighting: Rc>, - pub line_numbers: Rc>, - pub colors: Rc>, - pub status_line: Rc>, - pub tab_line: Rc>, - pub greeting_message: Rc>, - pub help_message: Rc>, - pub terminal: Rc>, - pub document: Rc>, + pub syntax_highlighting: LuaAnyUserData, + pub line_numbers: LuaAnyUserData, + pub colors: LuaAnyUserData, + pub status_line: LuaAnyUserData, + pub tab_line: LuaAnyUserData, + pub greeting_message: LuaAnyUserData, + pub help_message: LuaAnyUserData, + pub terminal: LuaAnyUserData, + pub document: LuaAnyUserData, pub task_manager: Arc>, } @@ -68,15 +106,15 @@ impl Config { /// Take a lua instance, inject all the configuration tables and return a default config struct pub fn new(lua: &Lua) -> Result { // Set up structs to populate (the default values will be thrown away) - let syntax_highlighting = Rc::new(RefCell::new(SyntaxHighlighting::default())); - let line_numbers = Rc::new(RefCell::new(LineNumbers::default())); - let greeting_message = Rc::new(RefCell::new(GreetingMessage::default())); - let help_message = Rc::new(RefCell::new(HelpMessage::default())); - let colors = Rc::new(RefCell::new(Colors::default())); - let status_line = Rc::new(RefCell::new(StatusLine::default())); - let tab_line = Rc::new(RefCell::new(TabLine::default())); - let terminal = Rc::new(RefCell::new(Terminal::default())); - let document = Rc::new(RefCell::new(Document::default())); + let syntax_highlighting = lua.create_userdata(SyntaxHighlighting::default())?; + let line_numbers = lua.create_userdata(LineNumbers::default())?; + let greeting_message = lua.create_userdata(GreetingMessage::default())?; + let help_message = lua.create_userdata(HelpMessage::default())?; + let colors = lua.create_userdata(Colors::default())?; + let status_line = lua.create_userdata(StatusLine::default())?; + let tab_line = lua.create_userdata(TabLine::default())?; + let terminal = lua.create_userdata(Terminal::default())?; + let document = lua.create_userdata(Document::default())?; // Set up the task manager let task_manager = Arc::new(Mutex::new(TaskManager::default())); @@ -200,7 +238,7 @@ impl Config { // Get list of requested built-in plugins let plugins: Vec = lua .globals() - .get::<_, LuaTable>("builtins") + .get::("builtins") .unwrap() .sequence_values() .filter_map(std::result::Result::ok) @@ -218,7 +256,7 @@ impl Config { } else { // User hasn't provided configuration file, check for local copy !lua.globals() - .get::<_, LuaTable>("plugins") + .get::("plugins") .unwrap() .sequence_values() .filter_map(std::result::Result::ok) @@ -278,7 +316,7 @@ impl Default for Document { } impl LuaUserData for Document { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fn add_fields>(fields: &mut F) { fields.add_field_method_get("tab_width", |_, document| Ok(document.tab_width)); fields.add_field_method_set("tab_width", |_, this, value| { this.tab_width = value; @@ -304,27 +342,27 @@ impl LuaUserData for Document { } } -impl FromLua<'_> for FileTypes { - fn from_lua(value: LuaValue<'_>, lua: &Lua) -> std::result::Result { +impl FromLua for FileTypes { + fn from_lua(value: LuaValue, lua: &Lua) -> std::result::Result { let mut result = vec![]; if let LuaValue::Table(table) = value { for i in table.pairs::() { let (name, info) = i?; - let icon = info.get::<_, String>("icon")?; + let icon = info.get::("icon")?; let extensions = info - .get::<_, LuaTable>("extensions") + .get::("extensions") .unwrap_or(lua.create_table()?) .pairs::() .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) .collect::>(); let files = info - .get::<_, LuaTable>("files") + .get::("files") .unwrap_or(lua.create_table()?) .pairs::() .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) .collect::>(); let modelines = info - .get::<_, LuaTable>("modelines") + .get::("modelines") .unwrap_or(lua.create_table()?) .pairs::() .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) diff --git a/src/editor/cursor.rs b/src/editor/cursor.rs index ac49a9b..246279f 100644 --- a/src/editor/cursor.rs +++ b/src/editor/cursor.rs @@ -1,5 +1,8 @@ /// Functions for moving the cursor around +use crate::{config, ged, handle_event, CEvent, Loc, Result}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use kaolinite::event::Status; +use mlua::{AnyUserData, Lua}; use super::Editor; @@ -18,7 +21,7 @@ impl Editor { pub fn select_left(&mut self) { let status = self.doc_mut().select_left(); // Cursor wrapping if cursor hits the start of the line - let wrapping = self.config.document.borrow().wrap_cursor; + let wrapping = config!(self.config, document).wrap_cursor; if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { self.doc_mut().select_up(); self.doc_mut().select_end(); @@ -29,7 +32,7 @@ impl Editor { pub fn select_right(&mut self) { let status = self.doc_mut().select_right(); // Cursor wrapping if cursor hits the end of a line - let wrapping = self.config.document.borrow().wrap_cursor; + let wrapping = config!(self.config, document).wrap_cursor; if status == Status::EndOfLine && wrapping { self.doc_mut().select_down(); self.doc_mut().select_home(); @@ -56,7 +59,7 @@ impl Editor { pub fn left(&mut self) { let status = self.doc_mut().move_left(); // Cursor wrapping if cursor hits the start of the line - let wrapping = self.config.document.borrow().wrap_cursor; + let wrapping = config!(self.config, document).wrap_cursor; if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { self.doc_mut().move_up(); self.doc_mut().move_end(); @@ -67,7 +70,7 @@ impl Editor { pub fn right(&mut self) { let status = self.doc_mut().move_right(); // Cursor wrapping if cursor hits the end of a line - let wrapping = self.config.document.borrow().wrap_cursor; + let wrapping = config!(self.config, document).wrap_cursor; if status == Status::EndOfLine && wrapping { self.doc_mut().move_down(); self.doc_mut().move_home(); @@ -77,7 +80,7 @@ impl Editor { /// Move the cursor to the previous word in the line pub fn prev_word(&mut self) { let status = self.doc_mut().move_prev_word(); - let wrapping = self.config.document.borrow().wrap_cursor; + let wrapping = config!(self.config, document).wrap_cursor; if status == Status::StartOfLine && wrapping { self.doc_mut().move_up(); self.doc_mut().move_end(); @@ -87,10 +90,155 @@ impl Editor { /// Move the cursor to the next word in the line pub fn next_word(&mut self) { let status = self.doc_mut().move_next_word(); - let wrapping = self.config.document.borrow().wrap_cursor; + let wrapping = config!(self.config, document).wrap_cursor; if status == Status::EndOfLine && wrapping { self.doc_mut().move_down(); self.doc_mut().move_home(); } } } + +/// Handle multiple cursors (replay a key event for each of them) +pub fn handle_multiple_cursors( + editor: &AnyUserData, + event: &CEvent, + lua: &Lua, + original_loc: &Loc, +) -> Result<()> { + let mut original_loc = *original_loc; + // Cache the state of the document + let mut cursor = ged!(&editor).doc().cursor; + // For each secondary cursor, replay the key event + ged!(mut &editor).macro_man.playing = true; + let mut secondary_cursors = ged!(&editor).doc().secondary_cursors.clone(); + // Prevent interference + adjust_other_cursors( + &mut secondary_cursors, + &original_loc.clone(), + &cursor.loc, + event, + &mut original_loc, + ); + // Update each secondary cursor + let mut ptr = 0; + while ptr < secondary_cursors.len() { + // Move to the secondary cursor position + let sec_cursor = secondary_cursors[ptr]; + ged!(mut &editor).doc_mut().move_to(&sec_cursor); + // Replay the event + let old_loc = ged!(&editor).doc().char_loc(); + handle_event(editor, event, lua)?; + // Prevent any interference + let char_loc = ged!(&editor).doc().char_loc(); + cursor.loc = adjust_other_cursors( + &mut secondary_cursors, + &old_loc, + &char_loc, + event, + &mut cursor.loc, + ); + // Update the secondary cursor + *secondary_cursors.get_mut(ptr).unwrap() = char_loc; + // Move to the next secondary cursor + ptr += 1; + } + ged!(mut &editor).doc_mut().secondary_cursors = secondary_cursors; + ged!(mut &editor).macro_man.playing = false; + // Restore back to the state of the document beforehand + // TODO: calculate char_ptr and old_cursor too + ged!(mut &editor).doc_mut().cursor = cursor; + let char_ptr = ged!(&editor).doc().character_idx(&cursor.loc); + ged!(mut &editor).doc_mut().char_ptr = char_ptr; + ged!(mut &editor).doc_mut().old_cursor = cursor.loc.x; + ged!(mut &editor).doc_mut().cancel_selection(); + Ok(()) +} + +/// Adjust other secondary cursors based of a change in one +fn adjust_other_cursors( + cursors: &mut Vec, + old_pos: &Loc, + new_pos: &Loc, + event: &CEvent, + primary: &mut Loc, +) -> Loc { + cursors.push(*primary); + match event { + CEvent::Key(KeyEvent { + code: KeyCode::Enter, + .. + }) => { + // Enter key, push all cursors below this line downwards + for c in cursors.iter_mut() { + if c == old_pos { + continue; + } + let mut new_loc = *c; + // Adjust x position + if old_pos.y == c.y && old_pos.x < c.x { + new_loc.x -= old_pos.x; + } + // If this cursor is after the currently moved cursor, shift down + if c.y > old_pos.y || (c.y == old_pos.y && c.x > old_pos.x) { + new_loc.y += 1; + } + // Update the secondary cursor + *c = new_loc; + } + } + CEvent::Key(KeyEvent { + code: KeyCode::Backspace, + .. + }) => { + // Backspace key, push all cursors below this line upwards + for c in cursors.iter_mut() { + if c == old_pos { + continue; + } + let mut new_loc = *c; + let at_line_start = old_pos.x == 0 && old_pos.y != 0; + // Adjust x position + if old_pos.y == c.y && old_pos.x < c.x && at_line_start { + new_loc.x += new_pos.x; + } + // If this cursor is after the currently moved cursor, shift up + if (c.y > old_pos.y || (c.y == old_pos.y && c.x > old_pos.x)) && at_line_start { + new_loc.y -= 1; + } + // Update the secondary cursor + *c = new_loc; + } + } + _ => (), + } + cursors.pop().unwrap() +} + +// Determine whether an event should be acted on by the multi cursor +#[allow(clippy::module_name_repetitions)] +pub fn allowed_by_multi_cursor(event: &CEvent) -> bool { + matches!( + event, + CEvent::Key( + KeyEvent { + code: KeyCode::Tab + | KeyCode::Backspace + | KeyCode::Enter + | KeyCode::Up + | KeyCode::Down + | KeyCode::Left + | KeyCode::Right, + modifiers: KeyModifiers::NONE, + .. + } | KeyEvent { + code: KeyCode::Char(_), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + .. + } | KeyEvent { + code: KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right, + modifiers: KeyModifiers::CONTROL, + .. + } + ) + ) +} diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 7e66ff7..6bc5413 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -135,7 +135,7 @@ impl Editor { if self.doc().loc().y == self.doc().len_lines() { self.exe(Event::InsertLine(self.doc().loc().y, String::new()))?; if !self.doc().info.read_only { - self.highlighter().append(&String::new()); + self.highlighter().append(""); } } Ok(()) diff --git a/src/editor/filetypes.rs b/src/editor/filetypes.rs index b84c092..98ca401 100644 --- a/src/editor/filetypes.rs +++ b/src/editor/filetypes.rs @@ -1,3 +1,4 @@ +use crate::config; /// Tools for managing and identifying file types use crate::editor::Config; use kaolinite::utils::get_file_name; @@ -81,12 +82,7 @@ impl FileType { /// Identify the correct highlighter to use pub fn get_highlighter(&self, config: &Config, tab_width: usize) -> Highlighter { - if let Some(highlighter) = config - .syntax_highlighting - .borrow() - .user_rules - .get(&self.name) - { + if let Some(highlighter) = config!(config, syntax).user_rules.get(&self.name) { // The user has defined their own syntax highlighter for this file type highlighter.clone() } else { diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 99b0872..a5d1d57 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,9 +1,11 @@ +/// Functions for rendering the UI +use crate::config; use crate::error::{OxError, Result}; +use crate::events::wait_for_event_hog; use crate::ui::{key_event, size, Feedback}; -/// Functions for rendering the UI use crate::{display, handle_lua_error}; use crossterm::{ - event::{read, KeyCode as KCode, KeyModifiers as KMod}, + event::{KeyCode as KCode, KeyModifiers as KMod}, queue, style::{ Attribute, Color, Print, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg, @@ -29,8 +31,7 @@ impl Editor { let max = self.dent(); self.doc_mut().size.w = w.saturating_sub(max); // Render the tab line - let tab_enabled = self.config.tab_line.borrow().enabled; - if tab_enabled { + if config!(self.config, tab_line).enabled { self.render_tab_line(lua, w)?; } // Run through each line of the terminal, rendering the correct line @@ -56,9 +57,9 @@ impl Editor { #[allow(clippy::similar_names, clippy::too_many_lines)] pub fn render_document(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { // Get some details about the help message - let colors = self.config.colors.borrow().highlight.to_color()?; - let tab_width = self.config.document.borrow().tab_width; - let message = self.config.help_message.borrow().render(lua); + let colors = config!(self.config, colors).highlight.to_color()?; + let tab_width = config!(self.config, document).tab_width; + let message = config!(self.config, help_message).render(lua); let max_width = message .iter() .map(|(_, line)| width(line, tab_width)) @@ -82,7 +83,7 @@ impl Editor { for y in 0..u16::try_from(h).unwrap_or(0) { // Work out how long the line should be (accounting for help message if necessary) let required_width = - if self.config.help_message.borrow().enabled && (start..=end).contains(&y) { + if config!(self.config, help_message).enabled && (start..=end).contains(&y) { w.saturating_sub(self.dent()).saturating_sub(max_width) } else { w.saturating_sub(self.dent()) @@ -90,18 +91,18 @@ impl Editor { // Go to the right location self.terminal.goto(0, y as usize + self.push_down)?; // Start colours - let editor_bg = Bg(self.config.colors.borrow().editor_bg.to_color()?); - let editor_fg = Fg(self.config.colors.borrow().editor_fg.to_color()?); - let line_number_bg = Bg(self.config.colors.borrow().line_number_bg.to_color()?); - let line_number_fg = Fg(self.config.colors.borrow().line_number_fg.to_color()?); - let selection_bg = Bg(self.config.colors.borrow().selection_bg.to_color()?); - let selection_fg = Fg(self.config.colors.borrow().selection_fg.to_color()?); + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + let line_number_bg = Bg(config!(self.config, colors).line_number_bg.to_color()?); + let line_number_fg = Fg(config!(self.config, colors).line_number_fg.to_color()?); + let selection_bg = Bg(config!(self.config, colors).selection_bg.to_color()?); + let selection_fg = Fg(config!(self.config, colors).selection_fg.to_color()?); display!(self, editor_bg, editor_fg); // Write line number of document - if self.config.line_numbers.borrow().enabled { + if config!(self.config, line_numbers).enabled { let num = self.doc().line_number(y as usize + self.doc().offset.y); - let padding_left = " ".repeat(self.config.line_numbers.borrow().padding_left); - let padding_right = " ".repeat(self.config.line_numbers.borrow().padding_right); + let padding_left = " ".repeat(config!(self.config, line_numbers).padding_left); + let padding_right = " ".repeat(config!(self.config, line_numbers).padding_right); display!( self, line_number_bg, @@ -129,7 +130,7 @@ impl Editor { let (text, colour) = match token { // Non-highlighted text TokOpt::Some(text, kind) => { - let colour = self.config.syntax_highlighting.borrow().get_theme(&kind); + let colour = config!(self.config, syntax).get_theme(&kind); let colour = match colour { // Success, write token Ok(col) => Fg(col), @@ -148,16 +149,40 @@ impl Editor { for c in text.chars() { let at_x = self.doc().character_idx(&Loc { y: idx, x: x_pos }); let is_selected = self.doc().is_loc_selected(Loc { y: idx, x: at_x }); - if is_selected && (cache_bg != selection_bg || cache_fg != selection_fg) { - display!(self, selection_bg, selection_fg); - cache_bg = selection_bg; - cache_fg = selection_fg; - } else if !is_selected && (cache_bg != editor_bg || cache_fg != colour) { - display!(self, editor_bg, colour); - cache_bg = editor_bg; - cache_fg = colour; + // Render the correct colour + if is_selected { + if cache_bg != selection_bg { + display!(self, selection_bg); + cache_bg = selection_bg; + } + if cache_fg != selection_fg { + display!(self, selection_fg); + cache_fg = selection_fg; + } + } else { + if cache_bg != editor_bg { + display!(self, editor_bg); + cache_bg = editor_bg; + } + if cache_fg != colour { + display!(self, colour); + cache_fg = colour; + } } + // Render multi-cursors + let underline = SetAttribute(Attribute::Underlined); + let no_underline = SetAttribute(Attribute::NoUnderline); + let at_loc = Loc { y: idx, x: at_x }; + let multi_cursor_here = self.doc().has_cursor(at_loc).is_some(); + if multi_cursor_here { + display!(self, underline, Bg(Color::White), Fg(Color::Black)); + } + // Render the character display!(self, c); + // Reset any multi-cursor display + if multi_cursor_here { + display!(self, no_underline, cache_bg, cache_fg); + } x_pos += 1; } } @@ -167,7 +192,7 @@ impl Editor { display!(self, " ".repeat(required_width)); } // Render help message if applicable (otherwise, just output padding to clear buffer) - if self.config.help_message.borrow().enabled && (start..=end).contains(&y) { + if config!(self.config, help_message).enabled && (start..=end).contains(&y) { let idx = y.saturating_sub(start); let line = message .get(idx as usize) @@ -184,7 +209,7 @@ impl Editor { let mut idx = 0; let mut length = 0; let mut offset = 0; - let tab_line = self.config.tab_line.borrow(); + let tab_line = config!(self.config, tab_line); for (c, file) in self.files.iter().enumerate() { let render = tab_line.render(lua, file, &mut self.feedback); length += width(&render, 4) + 1; @@ -206,10 +231,10 @@ impl Editor { #[allow(clippy::similar_names)] pub fn render_tab_line(&mut self, lua: &Lua, w: usize) -> Result<()> { self.terminal.goto(0_usize, 0_usize)?; - let tab_inactive_bg = Bg(self.config.colors.borrow().tab_inactive_bg.to_color()?); - let tab_inactive_fg = Fg(self.config.colors.borrow().tab_inactive_fg.to_color()?); - let tab_active_bg = Bg(self.config.colors.borrow().tab_active_bg.to_color()?); - let tab_active_fg = Fg(self.config.colors.borrow().tab_active_fg.to_color()?); + let tab_inactive_bg = Bg(config!(self.config, colors).tab_inactive_bg.to_color()?); + let tab_inactive_fg = Fg(config!(self.config, colors).tab_inactive_fg.to_color()?); + let tab_active_bg = Bg(config!(self.config, colors).tab_active_bg.to_color()?); + let tab_active_fg = Fg(config!(self.config, colors).tab_active_fg.to_color()?); let (tabs, idx, _) = self.get_tab_parts(lua, w); display!(self, tab_inactive_fg, tab_inactive_bg); for (c, header) in tabs.iter().enumerate() { @@ -237,11 +262,11 @@ impl Editor { #[allow(clippy::similar_names)] pub fn render_status_line(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { self.terminal.goto(0, h + self.push_down)?; - let editor_bg = Bg(self.config.colors.borrow().editor_bg.to_color()?); - let editor_fg = Fg(self.config.colors.borrow().editor_fg.to_color()?); - let status_bg = Bg(self.config.colors.borrow().status_bg.to_color()?); - let status_fg = Fg(self.config.colors.borrow().status_fg.to_color()?); - match self.config.status_line.borrow().render(self, lua, w) { + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + let status_bg = Bg(config!(self.config, colors).status_bg.to_color()?); + let status_fg = Fg(config!(self.config, colors).status_fg.to_color()?); + match config!(self.config, status_line).render(self, lua, w) { Ok(content) => { display!( self, @@ -274,15 +299,15 @@ impl Editor { /// Render the feedback line pub fn render_feedback_line(&mut self, w: usize, h: usize) -> Result<()> { self.terminal.goto(0, h + 2)?; - let content = self.feedback.render(&self.config.colors.borrow(), w)?; + let content = self.feedback.render(&config!(self.config, colors), w)?; display!(self, content); Ok(()) } /// Render the help message fn render_greeting(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { - let colors = self.config.colors.borrow(); - let greeting = self.config.greeting_message.borrow().render(lua, &colors)?; + let colors = config!(self.config, colors); + let greeting = config!(self.config, greeting_message).render(lua, &colors)?; let message: Vec<&str> = greeting.split('\n').collect(); for (c, line) in message.iter().enumerate().take(h.saturating_sub(h / 4)) { self.terminal.goto(4, h / 4 + c + 1)?; @@ -304,7 +329,7 @@ impl Editor { // Render prompt message self.terminal.prepare_line(h)?; self.terminal.show_cursor()?; - let editor_bg = Bg(self.config.colors.borrow().editor_bg.to_color()?); + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); display!( self, editor_bg, @@ -316,7 +341,9 @@ impl Editor { self.terminal.goto(prompt.len() + input.len() + 2, h)?; self.terminal.flush()?; // Handle events - if let Some((modifiers, code)) = key_event(&read()?) { + if let Some((modifiers, code)) = + key_event(&wait_for_event_hog(self), &mut self.macro_man) + { match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, @@ -383,9 +410,9 @@ impl Editor { .chars() .skip(input.chars().count()) .collect::(); - let editor_fg = Fg(self.config.colors.borrow().editor_fg.to_color()?); - let editor_bg = Bg(self.config.colors.borrow().editor_bg.to_color()?); - let tab_width = self.config.document.borrow().tab_width; + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); + let tab_width = config!(self.config, document).tab_width; let total_width = width(&input, tab_width) + width(&suggestion_text, tab_width); let padding = " ".repeat(size()?.w.saturating_sub(total_width)); display!( @@ -398,11 +425,13 @@ impl Editor { padding, editor_fg ); - let tab_width = self.config.document.borrow_mut().tab_width; + let tab_width = config!(self.config, document).tab_width; self.terminal.goto(6 + width(&input, tab_width), h)?; self.terminal.flush()?; // Handle events - if let Some((modifiers, code)) = key_event(&read()?) { + if let Some((modifiers, code)) = + key_event(&wait_for_event_hog(self), &mut self.macro_man) + { match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, @@ -451,7 +480,9 @@ impl Editor { self.render_feedback_line(w, h)?; self.terminal.flush()?; // Handle events - if let Some((modifiers, code)) = key_event(&read()?) { + if let Some((modifiers, code)) = + key_event(&wait_for_event_hog(self), &mut self.macro_man) + { match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Esc) => { @@ -506,9 +537,9 @@ impl Editor { /// Work out how much to push the document to the right (to make way for line numbers) pub fn dent(&self) -> usize { - if self.config.line_numbers.borrow().enabled { - let padding_left = self.config.line_numbers.borrow().padding_left; - let padding_right = self.config.line_numbers.borrow().padding_right; + if config!(self.config, line_numbers).enabled { + let padding_left = config!(self.config, line_numbers).padding_left; + let padding_right = config!(self.config, line_numbers).padding_right; self.doc().len_lines().to_string().len() + 1 + padding_left + padding_right } else { 0 diff --git a/src/editor/macros.rs b/src/editor/macros.rs new file mode 100644 index 0000000..668a3d5 --- /dev/null +++ b/src/editor/macros.rs @@ -0,0 +1,76 @@ +/// Tools for recording and playing back macros for bulk editing +use crossterm::event::{Event as CEvent, KeyCode, KeyEvent, KeyModifiers}; + +/// Macro manager struct +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct MacroMan { + pub sequence: Vec, + pub recording: bool, + pub playing: bool, + pub ptr: usize, + pub just_completed: bool, + pub reps: usize, +} + +impl MacroMan { + /// Register an event + pub fn register(&mut self, ev: CEvent) { + self.just_completed = false; + let valid_event = matches!(ev, CEvent::Key(_) | CEvent::Mouse(_) | CEvent::Paste(_)); + if self.recording && valid_event { + self.sequence.push(ev); + } + } + + /// Activate recording + pub fn record(&mut self) { + self.just_completed = false; + self.sequence.clear(); + self.recording = true; + } + + /// Stop recording + pub fn finish(&mut self) { + self.just_completed = false; + self.recording = false; + self.remove_macro_calls(); + } + + /// Activate macro + pub fn play(&mut self, reps: usize) { + self.reps = reps; + self.just_completed = false; + self.playing = true; + self.ptr = 0; + } + + /// Get next event from macro man + pub fn next(&mut self) -> Option { + if self.playing { + let result = self.sequence.get(self.ptr).cloned(); + self.ptr += 1; + if self.ptr >= self.sequence.len() { + self.reps = self.reps.saturating_sub(1); + self.playing = self.reps != 0; + self.ptr = 0; + self.just_completed = true; + } + result + } else { + self.just_completed = false; + None + } + } + + /// Remove the stop key binding from being included + pub fn remove_macro_calls(&mut self) { + if let Some(CEvent::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Esc, + .. + })) = self.sequence.last() + { + self.sequence.pop(); + } + } +} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 90e9684..9e57ac8 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,3 +1,4 @@ +use crate::config; /// Main functionality of the editor use crate::config::{Config, Indentation}; use crate::error::{OxError, Result}; @@ -20,11 +21,14 @@ mod documents; mod editing; mod filetypes; mod interface; +mod macros; mod mouse; mod scanning; +pub use cursor::{allowed_by_multi_cursor, handle_multiple_cursors}; pub use documents::FileContainer; pub use filetypes::{FileType, FileTypes}; +pub use macros::MacroMan; /// For managing all editing and rendering of cactus #[allow(clippy::struct_excessive_bools)] @@ -59,6 +63,8 @@ pub struct Editor { pub last_click: Option<(Instant, MouseEvent)>, /// Stores whether or not we're in a double click pub in_dbl_click: bool, + /// Macro manager + pub macro_man: MacroMan, } impl Editor { @@ -81,6 +87,7 @@ impl Editor { plugin_active: false, last_click: None, in_dbl_click: false, + macro_man: MacroMan::default(), }) } @@ -95,7 +102,7 @@ impl Editor { let mut size = size()?; size.h = size.h.saturating_sub(1 + self.push_down); let mut doc = Document::new(size); - doc.set_tab_width(self.config.document.borrow().tab_width); + doc.set_tab_width(config!(self.config, document).tab_width); doc.event_mgmt.force_not_with_disk = true; // Load all the lines within viewport into the document doc.load_to(size.h); @@ -128,7 +135,7 @@ impl Editor { // If no documents were provided, create a new empty document if self.files.is_empty() { self.blank()?; - self.greet = self.config.greeting_message.borrow().enabled; + self.greet = config!(self.config, greeting_message).enabled; } Ok(()) } @@ -144,8 +151,8 @@ impl Editor { size.h = size.h.saturating_sub(1 + self.push_down); let mut doc = Document::open(size, file_name)?; // Collect various data from the document - let tab_width = self.config.document.borrow().tab_width; - let file_type = self.config.document.borrow().file_types.identify(&mut doc); + let tab_width = config!(self.config, document).tab_width; + let file_type = config!(self.config, document).file_types.identify(&mut doc); // Set up the document doc.set_tab_width(tab_width); doc.load_to(size.h); @@ -187,11 +194,8 @@ impl Editor { let file = self.files.last_mut().unwrap(); file.doc.file_name = Some(file_name); // Work out information for the document - let tab_width = self.config.document.borrow().tab_width; - let file_type = self - .config - .document - .borrow() + let tab_width = config!(self.config, document).tab_width; + let file_type = config!(self.config, document) .file_types .identify(&mut file.doc); // Set up the document @@ -240,11 +244,8 @@ impl Editor { if self.doc().file_name.is_none() { // Get information about the document let file = self.files.last_mut().unwrap(); - let tab_width = self.config.document.borrow().tab_width; - let file_type = self - .config - .document - .borrow() + let tab_width = config!(self.config, document).tab_width; + let file_type = config!(self.config, document) .file_types .identify(&mut file.doc); // Reattach an appropriate highlighter @@ -362,16 +363,20 @@ impl Editor { _ => unreachable!(), } // Calculate the correct push down based on config - self.push_down = usize::from(self.config.tab_line.borrow().enabled); + self.push_down = usize::from(config!(self.config, tab_line).enabled); None } /// Handle event pub fn handle_event(&mut self, lua: &Lua, event: CEvent) -> Result<()> { + // Register this event for macro purposes + self.macro_man.register(event.clone()); + // Determine if a rerender is needed self.needs_rerender = match event { CEvent::Mouse(event) => event.kind != MouseEventKind::Moved, _ => true, }; + // Pass event down to special handlers match event { CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, CEvent::Resize(w, h) => self.handle_resize(w, h), @@ -388,7 +393,7 @@ impl Editor { let end = Instant::now(); let inactivity = end.duration_since(self.last_active).as_millis() as usize; // Commit if over user-defined period of inactivity - if inactivity > self.config.document.borrow().undo_period * 1000 { + if inactivity > config!(self.config, document).undo_period * 1000 { self.doc_mut().commit(); } // Register this activity @@ -429,10 +434,10 @@ impl Editor { /// Handle tab character being inserted pub fn handle_tab(&mut self) -> Result<()> { - if self.config.document.borrow().indentation == Indentation::Tabs { + if config!(self.config, document).indentation == Indentation::Tabs { self.character('\t')?; } else { - let tab_width = self.config.document.borrow().tab_width; + let tab_width = config!(self.config, document).tab_width; for _ in 0..tab_width { self.character(' ')?; } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index 8993414..2ada5d7 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -1,6 +1,7 @@ -use crate::ui::size; /// For handling mouse events -use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; +use crate::config; +use crate::ui::size; +use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use kaolinite::{utils::width, Loc}; use mlua::Lua; use std::time::{Duration, Instant}; @@ -20,7 +21,7 @@ enum MouseLocation { impl Editor { /// Finds the position of the mouse within the viewport fn find_mouse_location(&mut self, lua: &Lua, event: MouseEvent) -> MouseLocation { - let tab_enabled = self.config.tab_line.borrow().enabled; + let tab_enabled = config!(self.config, tab_line).enabled; let tab = usize::from(tab_enabled); if event.row == 0 && tab_enabled { let (tabs, _, offset) = self.get_tab_parts(lua, size().map_or(0, |s| s.w)); @@ -46,95 +47,108 @@ impl Editor { /// Handles a mouse event (dragging / clicking) pub fn handle_mouse_event(&mut self, lua: &Lua, event: MouseEvent) { - match event.kind { - // Single click - MouseEventKind::Down(MouseButton::Left) => { - // Determine if there has been a click within 500ms - if let Some((time, last_event)) = self.last_click { - let now = Instant::now(); - let short_period = now.duration_since(time) <= Duration::from_millis(500); - let same_location = - last_event.column == event.column && last_event.row == event.row; - if short_period && same_location { - self.in_dbl_click = true; - self.handle_double_click(lua, event); - return; + match event.modifiers { + KeyModifiers::NONE => match event.kind { + // Single click + MouseEventKind::Down(MouseButton::Left) => { + // Determine if there has been a click within 500ms + if let Some((time, last_event)) = self.last_click { + let now = Instant::now(); + let short_period = now.duration_since(time) <= Duration::from_millis(500); + let same_location = + last_event.column == event.column && last_event.row == event.row; + if short_period && same_location { + self.in_dbl_click = true; + self.handle_double_click(lua, event); + return; + } } - } - match self.find_mouse_location(lua, event) { - MouseLocation::File(mut loc) => { - loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().move_to(&loc); - self.doc_mut().old_cursor = self.doc().loc().x; + match self.find_mouse_location(lua, event) { + MouseLocation::File(mut loc) => { + self.doc_mut().clear_cursors(); + loc.x = self.doc_mut().character_idx(&loc); + self.doc_mut().move_to(&loc); + self.doc_mut().old_cursor = self.doc().loc().x; + } + MouseLocation::Tabs(i) => { + self.ptr = i; + self.update_cwd(); + } + MouseLocation::Out => (), } - MouseLocation::Tabs(i) => { - self.ptr = i; - self.update_cwd(); + } + MouseEventKind::Down(MouseButton::Right) => { + // Select the current line + if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { + self.doc_mut().select_line_at(loc.y); } - MouseLocation::Out => (), } - } - MouseEventKind::Down(MouseButton::Right) => { - // Select the current line - if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { - self.doc_mut().select_line_at(loc.y); + // Double click detection + MouseEventKind::Up(MouseButton::Left) => { + self.in_dbl_click = false; + let now = Instant::now(); + // Register this click as having happened + self.last_click = Some((now, event)); } - } - // Double click detection - MouseEventKind::Up(MouseButton::Left) => { - self.in_dbl_click = false; - let now = Instant::now(); - // Register this click as having happened - self.last_click = Some((now, event)); - } - // Mouse drag - MouseEventKind::Drag(MouseButton::Left) => match self.find_mouse_location(lua, event) { - MouseLocation::File(mut loc) => { - loc.x = self.doc_mut().character_idx(&loc); - if self.in_dbl_click { - if loc.x >= self.doc().cursor.selection_end.x { - // Find boundary of next word - let next = self.doc().next_word_close(loc); - self.doc_mut().select_to(&Loc { x: next, y: loc.y }); - } else { - // Find boundary of previous word - let next = self.doc().prev_word_close(loc); - self.doc_mut().select_to(&Loc { x: next, y: loc.y }); + // Mouse drag + MouseEventKind::Drag(MouseButton::Left) => { + match self.find_mouse_location(lua, event) { + MouseLocation::File(mut loc) => { + loc.x = self.doc_mut().character_idx(&loc); + if self.in_dbl_click { + if loc.x >= self.doc().cursor.selection_end.x { + // Find boundary of next word + let next = self.doc().next_word_close(loc); + self.doc_mut().select_to(&Loc { x: next, y: loc.y }); + } else { + // Find boundary of previous word + let next = self.doc().prev_word_close(loc); + self.doc_mut().select_to(&Loc { x: next, y: loc.y }); + } + } else { + self.doc_mut().select_to(&loc); + } } - } else { - loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().select_to(&loc); + MouseLocation::Tabs(_) | MouseLocation::Out => (), } } - MouseLocation::Tabs(_) | MouseLocation::Out => (), - }, - MouseEventKind::Drag(MouseButton::Right) => { - match self.find_mouse_location(lua, event) { - MouseLocation::File(mut loc) => { - loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().select_to_y(loc.y); + MouseEventKind::Drag(MouseButton::Right) => { + match self.find_mouse_location(lua, event) { + MouseLocation::File(mut loc) => { + loc.x = self.doc_mut().character_idx(&loc); + self.doc_mut().select_to_y(loc.y); + } + MouseLocation::Tabs(_) | MouseLocation::Out => (), } - MouseLocation::Tabs(_) | MouseLocation::Out => (), } - } - // Mouse scroll behaviour - MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { - if let MouseLocation::File(_) = self.find_mouse_location(lua, event) { - let scroll_amount = self.config.terminal.borrow().scroll_amount; - for _ in 0..scroll_amount { - if event.kind == MouseEventKind::ScrollDown { - self.doc_mut().scroll_down(); - } else { - self.doc_mut().scroll_up(); + // Mouse scroll behaviour + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { + if let MouseLocation::File(_) = self.find_mouse_location(lua, event) { + let scroll_amount = config!(self.config, terminal).scroll_amount; + for _ in 0..scroll_amount { + if event.kind == MouseEventKind::ScrollDown { + self.doc_mut().scroll_down(); + } else { + self.doc_mut().scroll_up(); + } } } } - } - MouseEventKind::ScrollLeft => { - self.doc_mut().move_left(); - } - MouseEventKind::ScrollRight => { - self.doc_mut().move_right(); + MouseEventKind::ScrollLeft => { + self.doc_mut().move_left(); + } + MouseEventKind::ScrollRight => { + self.doc_mut().move_right(); + } + _ => (), + }, + // Multi cursor behaviour + KeyModifiers::CONTROL => { + if let MouseEventKind::Down(MouseButton::Left) = event.kind { + if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { + self.doc_mut().new_cursor(loc); + } + } } _ => (), } diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index ce4f35a..2f86a1f 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -1,9 +1,10 @@ -use crate::display; /// Functions for searching and replacing use crate::error::{OxError, Result}; +use crate::events::wait_for_event_hog; use crate::ui::{key_event, size}; +use crate::{config, display}; use crossterm::{ - event::{read, KeyCode as KCode, KeyModifiers as KMod}, + event::{KeyCode as KCode, KeyModifiers as KMod}, queue, style::{Attribute, Print, SetAttribute, SetBackgroundColor as Bg}, }; @@ -24,7 +25,7 @@ impl Editor { // Render prompt message self.terminal.prepare_line(h)?; self.terminal.show_cursor()?; - let editor_bg = Bg(self.config.colors.borrow().editor_bg.to_color()?); + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); display!( self, editor_bg, @@ -44,7 +45,9 @@ impl Editor { self.terminal.hide_cursor()?; } self.terminal.flush()?; - if let Some((modifiers, code)) = key_event(&read()?) { + if let Some((modifiers, code)) = + key_event(&wait_for_event_hog(self), &mut self.macro_man) + { match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, @@ -95,7 +98,9 @@ impl Editor { } self.terminal.flush()?; // Handle events - if let Some((modifiers, code)) = key_event(&read()?) { + if let Some((modifiers, code)) = + key_event(&wait_for_event_hog(self), &mut self.macro_man) + { match (modifiers, code) { // On return or escape key, exit menu (KMod::NONE, KCode::Enter) => done = true, @@ -189,7 +194,9 @@ impl Editor { } self.terminal.flush()?; // Handle events - if let Some((modifiers, code)) = key_event(&read()?) { + if let Some((modifiers, code)) = + key_event(&wait_for_event_hog(self), &mut self.macro_man) + { match (modifiers, code) { // On escape key, exit (KMod::NONE, KCode::Esc) => done = true, diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..59e27cb --- /dev/null +++ b/src/events.rs @@ -0,0 +1,82 @@ +use crate::{ged, handle_lua_error, CEvent, Editor, Feedback, KeyEvent, KeyEventKind, Result}; +use crossterm::event::{poll, read}; +use mlua::{AnyUserData, Lua}; +use std::time::Duration; + +pub fn wait_for_event(editor: &AnyUserData, lua: &Lua) -> Result { + loop { + let mm_active = ged!(mut &editor).macro_man.playing; + // While waiting for an event to come along, service the task manager + if !mm_active { + while let (false, Ok(false)) = (mm_active, poll(Duration::from_millis(50))) { + let exec = ged!(mut &editor) + .config + .task_manager + .lock() + .unwrap() + .execution_list(); + for task in exec { + if let Ok(target) = lua.globals().get::(task.clone()) { + // Run the code + handle_lua_error("task", target.call(()), &mut ged!(mut &editor).feedback); + } else { + ged!(mut &editor).feedback = + Feedback::Warning(format!("Function '{task}' was not found")); + } + } + } + } + + // Attempt to get an event + let Some(event) = get_event(&mut ged!(mut &editor)) else { + // No event available, back to the beginning + continue; + }; + + // Block certain events from passing through + if !matches!( + event, + CEvent::Key(KeyEvent { + kind: KeyEventKind::Release, + .. + }) + ) { + return Ok(event); + } + } +} + +/// Wait for event, but without the task manager (and it hogs editor) +pub fn wait_for_event_hog(editor: &mut Editor) -> CEvent { + loop { + // Attempt to get an event + let Some(event) = get_event(editor) else { + // No event available, back to the beginning + continue; + }; + + // Block certain events from passing through + if !matches!( + event, + CEvent::Key(KeyEvent { + kind: KeyEventKind::Release, + .. + }) + ) { + return event; + } + } +} + +// Find out where to source an event from and source it +pub fn get_event(editor: &mut Editor) -> Option { + if let Some(ev) = editor.macro_man.next() { + // Take from macro man + Some(ev) + } else if let Ok(ev) = read() { + // Use standard crossterm event + Some(ev) + } else { + None + } +} diff --git a/src/main.rs b/src/main.rs index bdaa9c8..ad38ec0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod cli; mod config; mod editor; mod error; +mod events; mod ui; use cli::CommandLineInterface; @@ -12,20 +13,30 @@ use config::{ PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN, }; use crossterm::event::{Event as CEvent, KeyEvent, KeyEventKind}; -use editor::{Editor, FileTypes}; +use editor::{allowed_by_multi_cursor, handle_multiple_cursors, Editor, FileTypes}; use error::{OxError, Result}; +use events::wait_for_event; use kaolinite::event::{Error as KError, Event}; use kaolinite::searching::Searcher; use kaolinite::utils::{file_or_dir, get_cwd}; use kaolinite::Loc; use mlua::Error::{RuntimeError, SyntaxError}; -use mlua::{FromLua, Lua, Value}; -use std::cell::RefCell; +use mlua::{AnyUserData, FromLua, Lua, Value}; use std::io::ErrorKind; -use std::rc::Rc; use std::result::Result as RResult; use ui::{fatal_error, Feedback}; +/// Get editor helper macro +#[macro_export] +macro_rules! ged { + ($editor:expr) => { + $editor.borrow::().unwrap() + }; + (mut $editor:expr) => { + $editor.borrow_mut::().unwrap() + }; +} + /// Entry point - grabs command line arguments and runs the editor fn main() { // Interact with user to find out what they want to do @@ -62,29 +73,29 @@ fn run(cli: &CommandLineInterface) -> Result<()> { }; // Push editor into lua - let editor = Rc::new(RefCell::new(editor)); + let editor = lua.create_userdata(editor)?; lua.globals().set("editor", editor.clone())?; // Inject the networking library for plug-ins to use handle_lua_error( "", lua.load(PLUGIN_NETWORKING).exec(), - &mut editor.borrow_mut().feedback, + &mut ged!(mut &editor).feedback, ); // Load config and initialise lua.load(PLUGIN_BOOTSTRAP).exec()?; - let result = editor.borrow_mut().load_config(&cli.config_path, &lua); + let result = ged!(mut &editor).load_config(&cli.config_path, &lua); if let Some(err) = result { // Handle error if available - handle_lua_error("configuration", Err(err), &mut editor.borrow_mut().feedback); + handle_lua_error("configuration", Err(err), &mut ged!(mut &editor).feedback); }; // Run plug-ins handle_lua_error( "", lua.load(PLUGIN_RUN).exec(), - &mut editor.borrow_mut().feedback, + &mut ged!(mut &editor).feedback, ); // Load in the file types @@ -93,50 +104,50 @@ fn run(cli: &CommandLineInterface) -> Result<()> { .get("file_types") .unwrap_or(Value::Table(lua.create_table()?)); let file_types = FileTypes::from_lua(file_types, &lua).unwrap_or_default(); - editor.borrow_mut().config.document.borrow_mut().file_types = file_types; - + ged!(mut &editor) + .config + .document + .borrow_mut::() + .unwrap() + .file_types = file_types; // Open files user has asked to open let cwd = get_cwd().unwrap_or(".".to_string()); for (c, file) in cli.to_open.iter().enumerate() { // Reset cwd let _ = std::env::set_current_dir(&cwd); // Open the file - let result = editor.borrow_mut().open_or_new(file.to_string()); + let result = ged!(mut &editor).open_or_new(file.to_string()); handle_file_opening(&editor, result, file); // Set read only if applicable if cli.flags.read_only { - editor.borrow_mut().get_doc(c).info.read_only = true; + ged!(mut &editor).get_doc(c).info.read_only = true; } // Set highlighter if applicable if let Some(ref file_type) = cli.file_type { - let tab_width = editor.borrow().config.document.borrow().tab_width; - let file_type = editor - .borrow_mut() - .config - .document - .borrow() + let tab_width = config!(ged!(&editor).config, document).tab_width; + let file_type = config!(ged!(mut &editor).config, document) .file_types .get_name(file_type) .unwrap_or_default(); - let mut highlighter = file_type.get_highlighter(&editor.borrow().config, tab_width); - highlighter.run(&editor.borrow_mut().get_doc(c).lines); - let mut editor = editor.borrow_mut(); + let mut highlighter = file_type.get_highlighter(&ged!(&editor).config, tab_width); + highlighter.run(&ged!(mut &editor).get_doc(c).lines); + let mut editor = ged!(mut &editor); let file = editor.files.get_mut(c).unwrap(); file.highlighter = highlighter; file.file_type = Some(file_type); } // Move the pointer to the file we just created - editor.borrow_mut().next(); + ged!(mut &editor).next(); } // Reset the pointer back to the first document - editor.borrow_mut().ptr = 0; + ged!(mut &editor).ptr = 0; // Handle stdin if applicable if cli.flags.stdin { let stdin = cli::get_stdin(); - editor.borrow_mut().blank()?; - let this_doc = editor.borrow_mut().doc_len().saturating_sub(1); - let mut holder = editor.borrow_mut(); + let mut holder = ged!(mut &editor); + holder.blank()?; + let this_doc = holder.doc_len().saturating_sub(1); let doc = holder.get_doc(this_doc); doc.exe(Event::Insert(Loc { x: 0, y: 0 }, stdin))?; doc.load_to(doc.size.h); @@ -149,128 +160,111 @@ fn run(cli: &CommandLineInterface) -> Result<()> { } // Create a blank document if none are opened - editor.borrow_mut().new_if_empty()?; + ged!(mut &editor).new_if_empty()?; // Add in the plugin manager handle_lua_error( "", lua.load(PLUGIN_MANAGER).exec(), - &mut editor.borrow_mut().feedback, + &mut ged!(mut &editor).feedback, ); // Run the editor and handle errors if applicable - editor.borrow().update_cwd(); - editor.borrow_mut().init()?; - let mut event; - while editor.borrow().active { - // Render and wait for event - editor.borrow_mut().render(&lua)?; - // Keep requesting events until a valid one is found - loop { - // While waiting for an event to come along, service the task manager - while let Ok(false) = crossterm::event::poll(std::time::Duration::from_millis(100)) { - let exec = editor - .borrow_mut() - .config - .task_manager - .lock() - .unwrap() - .execution_list(); - for task in exec { - if let Ok(target) = lua.globals().get::<_, mlua::Function>(task.clone()) { - // Run the code - handle_lua_error( - "task", - target.call(()), - &mut editor.borrow_mut().feedback, - ); - } else { - editor.borrow_mut().feedback = - Feedback::Warning(format!("Function '{task}' was not found")); - } - } - } - - // Read the event - event = crossterm::event::read()?; - - // Block certain events from passing through - match event { - // Key release events cause duplicate and initial key press events which should be ignored - CEvent::Key(KeyEvent { - kind: KeyEventKind::Release, - .. - }) => (), - _ => break, - } + ged!(&editor).update_cwd(); + ged!(mut &editor).init()?; + while ged!(&editor).active { + // Render (unless a macro is being played, in which case, don't bother) + if !ged!(&editor).macro_man.playing || ged!(&editor).macro_man.just_completed { + ged!(mut &editor).render(&lua)?; } - // Clear screen of temporary items (expect on resize event) - if !matches!(event, CEvent::Resize(_, _)) { - editor.borrow_mut().greet = false; - editor.borrow_mut().feedback = Feedback::None; - } + // Wait for an event + let event = wait_for_event(&editor, &lua)?; - // Handle plug-in before key press mappings - if let CEvent::Key(key) = event { - let key_str = key_to_string(key.modifiers, key.code); - let code = run_key_before(&key_str); - let result = lua.load(&code).exec(); - handle_lua_error(&key_str, result, &mut editor.borrow_mut().feedback); - } + // Handle the event + let original_loc = ged!(&editor).doc().char_loc(); + handle_event(&editor, &event, &lua)?; - // Handle paste event (before event) - if let CEvent::Paste(ref paste_text) = event { - let listeners = get_listeners("before:paste", &lua)?; - for listener in listeners { - handle_lua_error( - "paste", - listener.call(paste_text.clone()), - &mut editor.borrow_mut().feedback, - ); + // Handle multi cursors + if let CEvent::Key(_) = event { + let has_multicursors = !ged!(&editor) + .try_doc() + .map_or(true, |doc| doc.secondary_cursors.is_empty()); + if ged!(&editor).active && allowed_by_multi_cursor(&event) && has_multicursors { + handle_multiple_cursors(&editor, &event, &lua, &original_loc)?; } } - // Actually handle editor event (errors included) - if let Err(err) = editor.borrow_mut().handle_event(&lua, event.clone()) { - editor.borrow_mut().feedback = Feedback::Error(format!("{err:?}")); - } - - // Handle paste event (after event) - if let CEvent::Paste(ref paste_text) = event { - let listeners = get_listeners("paste", &lua)?; - for listener in listeners { - handle_lua_error( - "paste", - listener.call(paste_text.clone()), - &mut editor.borrow_mut().feedback, - ); - } - } - - // Handle plug-in after key press mappings (if no errors occured) - if let CEvent::Key(key) = event { - let key_str = key_to_string(key.modifiers, key.code); - let code = run_key(&key_str); - let result = lua.load(&code).exec(); - handle_lua_error(&key_str, result, &mut editor.borrow_mut().feedback); - } - - editor.borrow_mut().update_highlighter(); + ged!(mut &editor).update_highlighter(); // Check for any commands to run - let command = editor.borrow().command.clone(); + let command = ged!(&editor).command.clone(); if let Some(command) = command { run_editor_command(&editor, &command, &lua); } - editor.borrow_mut().command = None; + ged!(mut &editor).command = None; } // Run any plugin cleanup operations let result = lua.load(run_key("exit")).exec(); - handle_lua_error("exit", result, &mut editor.borrow_mut().feedback); + handle_lua_error("exit", result, &mut ged!(mut &editor).feedback); + + ged!(mut &editor).terminal.end()?; + Ok(()) +} + +fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { + // Clear screen of temporary items (expect on resize event) + if !matches!(event, CEvent::Resize(_, _)) { + ged!(mut &editor).greet = false; + ged!(mut &editor).feedback = Feedback::None; + } + + // Handle plug-in before key press mappings + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key_before(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); + } + + // Handle paste event (before event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("before:paste", lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut ged!(mut &editor).feedback, + ); + } + } + + // Actually handle editor event (errors included) + if let Err(err) = ged!(mut &editor).handle_event(lua, event.clone()) { + ged!(mut &editor).feedback = Feedback::Error(format!("{err:?}")); + } + + // Handle paste event (after event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("paste", lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut ged!(mut &editor).feedback, + ); + } + } + + // Handle plug-in after key press mappings (if no errors occured) + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&key_str, result, &mut ged!(mut &editor).feedback); + } - editor.borrow_mut().terminal.end()?; Ok(()) } @@ -342,7 +336,7 @@ fn handle_lua_error(key_str: &str, error: RResult<(), mlua::Error>, feedback: &m } /// Handle opening files -fn handle_file_opening(editor: &Rc>, result: Result<()>, name: &str) { +fn handle_file_opening(editor: &AnyUserData, result: Result<()>, name: &str) { // TEMPORARY WORK-AROUND: Delete after Rust 1.83 if file_or_dir(name) == "directory" { fatal_error(&format!("'{name}' is a directory, not a file")); @@ -350,8 +344,8 @@ fn handle_file_opening(editor: &Rc>, result: Result<()>, name: & match result { Ok(()) => (), Err(OxError::AlreadyOpen { .. }) => { - let len = editor.borrow().files.len().saturating_sub(1); - editor.borrow_mut().ptr = len; + let len = ged!(&editor).files.len().saturating_sub(1); + ged!(mut &editor).ptr = len; } Err(OxError::Kaolinite(kerr)) => { match kerr { @@ -380,7 +374,7 @@ fn handle_file_opening(editor: &Rc>, result: Result<()>, name: & } /// Run a command in the editor -fn run_editor_command(editor: &Rc>, cmd: &str, lua: &Lua) { +fn run_editor_command(editor: &AnyUserData, cmd: &str, lua: &Lua) { let cmd = cmd.replace('\'', "\\'").to_string(); if let [subcmd, arguments @ ..] = cmd.split(' ').collect::>().as_slice() { let arguments = arguments.join("', '"); @@ -389,7 +383,7 @@ fn run_editor_command(editor: &Rc>, cmd: &str, lua: &Lua) { handle_lua_error( subcmd, lua.load(code).exec(), - &mut editor.borrow_mut().feedback, + &mut ged!(mut &editor).feedback, ); } } diff --git a/src/ui.rs b/src/ui.rs index 832eb88..68eabc8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,5 +1,6 @@ /// Utilities for rendering the user interface use crate::config::{Colors, Terminal as TerminalConfig}; +use crate::editor::MacroMan; use crate::error::Result; use base64::prelude::*; use crossterm::{ @@ -17,11 +18,10 @@ use crossterm::{ }, }; use kaolinite::utils::Size; -use std::cell::RefCell; +use mlua::AnyUserData; use std::collections::HashMap; use std::env; use std::io::{stdout, Stdout, Write}; -use std::rc::Rc; /// Printing macro #[macro_export] @@ -56,7 +56,8 @@ pub fn fatal_error(msg: &str) { } /// Shorthand to read key events -pub fn key_event(kev: &CEvent) -> Option<(KMod, KCode)> { +pub fn key_event(kev: &CEvent, mm: &mut MacroMan) -> Option<(KMod, KCode)> { + mm.register(kev.clone()); if let CEvent::Key(KeyEvent { modifiers, code, @@ -123,11 +124,11 @@ impl Feedback { pub struct Terminal { pub stdout: Stdout, - pub config: Rc>, + pub config: AnyUserData, } impl Terminal { - pub fn new(config: Rc>) -> Self { + pub fn new(config: AnyUserData) -> Self { Terminal { stdout: stdout(), config, @@ -155,7 +156,8 @@ impl Terminal { DisableLineWrap, EnableBracketedPaste, )?; - if self.config.borrow().mouse_enabled { + let cfg = self.config.borrow::().unwrap(); + if cfg.mouse_enabled { execute!(self.stdout, EnableMouseCapture)?; } terminal::enable_raw_mode()?; @@ -178,7 +180,8 @@ impl Terminal { EnableLineWrap, DisableBracketedPaste )?; - if self.config.borrow().mouse_enabled { + let cfg = self.config.borrow::().unwrap(); + if cfg.mouse_enabled { execute!(self.stdout, DisableMouseCapture)?; } Ok(())