From 3afdf58e7da2495eca5aa3dba37d72ffa27b7489 Mon Sep 17 00:00:00 2001 From: mcmah309 Date: Tue, 22 Oct 2024 22:51:14 +0000 Subject: [PATCH 01/73] chore: Replace quick_error with error_set --- Cargo.lock | 62 +++++++++++++++++++++++++++++++++----- Cargo.toml | 2 +- kaolinite/Cargo.toml | 2 +- kaolinite/README.md | 2 +- kaolinite/src/event.rs | 27 ++++++----------- src/config/colors.rs | 8 +++-- src/config/highlighting.rs | 6 ++-- src/config/mod.rs | 4 ++- src/editor/mod.rs | 2 +- src/error.rs | 57 +++++++++++++++-------------------- 10 files changed, 104 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0232a3b2..b7de91bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,24 @@ 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" @@ -131,6 +149,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error_set" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a8a70e1c5e3557e22af5af1e78f546303c9953638e60aee2c547322076cfabf" +dependencies = [ + "error_set_impl", +] + +[[package]] +name = "error_set_impl" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f8c9888cc6d3349076683c776fc48d3b0b8685aa2ca05107cfa1df72445157" +dependencies = [ + "dyn-fmt", + "indices", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -163,6 +203,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "indices" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e125c680c3871dc195cfd3ff94a877a78f31d5e9c1a3a62cce82689ece14bf7" + [[package]] name = "jargon-args" version = "0.2.7" @@ -173,7 +219,7 @@ checksum = "b9d38e5712e29fb0c2caeb33b1803c8feade6e3380c7a92788fb219999b2849e" name = "kaolinite" version = "0.9.5" dependencies = [ - "quick-error", + "error_set", "rand", "regex", "ropey", @@ -203,6 +249,12 @@ 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,10 +363,10 @@ dependencies = [ "alinio", "base64", "crossterm", + "error_set", "jargon-args", "kaolinite", "mlua", - "quick-error", "shellexpand", "synoptic", ] @@ -366,12 +418,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" version = "1.0.37" diff --git a/Cargo.toml b/Cargo.toml index 03ac7a40..d7192b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,6 @@ crossterm = "0.28.1" jargon-args = "0.2.7" kaolinite = { path = "./kaolinite" } mlua = { version = "0.9.9", features = ["lua54", "vendored"] } -quick-error = "2.0.1" +error_set = "0.6" shellexpand = "3.1.0" synoptic = "2" diff --git a/kaolinite/Cargo.toml b/kaolinite/Cargo.toml index 64af6d22..8086c3d6 100644 --- a/kaolinite/Cargo.toml +++ b/kaolinite/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["unicode", "text-processing"] categories = ["text-processing"] [dependencies] -quick-error = "2.0.1" +error_set = "0.6" regex = "1.10.6" ropey = "1.6.1" unicode-width = "0.2" diff --git a/kaolinite/README.md b/kaolinite/README.md index e59850be..e09aa52b 100644 --- a/kaolinite/README.md +++ b/kaolinite/README.md @@ -60,7 +60,7 @@ This software uses the following open source crates: - [unicode-width](https://github.com/unicode-rs/unicode-width) - [regex](https://github.com/rust-lang/regex) - [ropey](https://github.com/cessen/ropey) -- [quick_error](https://github.com/tailhook/quick-error) +- [error_set](https://github.com/mcmah309/error_set) ## License diff --git a/kaolinite/src/event.rs b/kaolinite/src/event.rs index 485f0751..6b4c4f57 100644 --- a/kaolinite/src/event.rs +++ b/kaolinite/src/event.rs @@ -1,6 +1,6 @@ /// event.rs - manages editing events and provides tools for error handling use crate::{document::Cursor, utils::Loc, Document}; -use quick_error::quick_error; +use error_set::error_set; use ropey::Rope; #[derive(Debug, Clone, PartialEq, Eq)] @@ -75,24 +75,17 @@ pub enum Status { /// Easy result type for unified error handling pub type Result = std::result::Result; -quick_error! { +error_set! { /// Error enum for handling all possible errors - #[derive(Debug)] - pub enum Error { - Io(err: std::io::Error) { - from() - display("I/O error: {}", err) - source(err) - } - Rope(err: ropey::Error) { - from() - display("Rope error: {}", err) - source(err) - } - NoFileName - OutOfRange + Error = { + #[display("I/O error: {0}")] + Io(std::io::Error), + #[display("Rope error: {0}")] + Rope(ropey::Error), + NoFileName, + OutOfRange, ReadOnlyFile - } + }; } /// For managing events for purposes of undo and redo diff --git a/src/config/colors.rs b/src/config/colors.rs index 053b6dc3..43354f47 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -343,7 +343,9 @@ impl Color { // Ensure the hex code is exactly 6 characters long if hex.len() != 6 { let msg = "Invalid hex code used in configuration file - ensure they are of length 6"; - return Err(OxError::Config(msg.to_string())); + return Err(OxError::Config { + msg: msg.to_string(), + }); } // Parse the hex string into the RGB components @@ -354,7 +356,9 @@ impl Color { tri.push(val); } else { let msg = "Invalid hex code used in configuration file - ensure all digits are between 0 and F"; - return Err(OxError::Config(msg.to_string())); + return Err(OxError::Config { + msg: msg.to_string(), + }); } } Ok((tri[0], tri[1], tri[2])) diff --git a/src/config/highlighting.rs b/src/config/highlighting.rs index d462b958..c89bcc8a 100644 --- a/src/config/highlighting.rs +++ b/src/config/highlighting.rs @@ -23,9 +23,9 @@ impl SyntaxHighlighting { if let Some(col) = self.theme.get(name) { col.to_color() } else { - Err(OxError::Config(format!( - "{name} has not been given a colour in the theme", - ))) + Err(OxError::Config { + msg: format!("{name} has not been given a colour in the theme",), + }) } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index aa334db4..c573bb48 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -184,7 +184,9 @@ impl Config { if user_provided_config { Ok(()) } else { - Err(OxError::Config("Not Found".to_string())) + Err(OxError::Config { + msg: "Not Found".to_string(), + }) } } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 46b47fb4..a8e1f806 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -317,7 +317,7 @@ impl Editor { // Display any warnings if the user configuration couldn't be found match result { Ok(()) => (), - Err(OxError::Config(msg)) => { + Err(OxError::Config { msg }) => { if msg == "Not Found" { let warn = "No configuration file found, using default configuration".to_string(); diff --git a/src/error.rs b/src/error.rs index e4438c96..14f0d3df 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,39 +1,30 @@ /// Error handling utilities use kaolinite::event::Error as KError; -use quick_error::quick_error; +use error_set::error_set; -quick_error! { - /// Error type used throughout the editor - #[derive(Debug)] - pub enum OxError { - Render(err: std::io::Error) { - from() - display("Error in I/O: {}", err) - } - Kaolinite(err: KError) { - from() - display("{}", { - match err { - KError::NoFileName => "This document has no file name, please use 'save as' instead".to_string(), - KError::OutOfRange => "Requested operation is out of range".to_string(), - KError::ReadOnlyFile => "This file is read only and can't be saved or edited".to_string(), - KError::Rope(rerr) => format!("Backend had an issue processing text: {rerr}"), - KError::Io(ioerr) => format!("I/O Error: {ioerr}"), - } - }) - } - Config(msg: String) { - display("Error in config file: {}", msg) - } - Lua(err: mlua::prelude::LuaError) { - from() - display("Error in lua: {}", err) - } - Cancelled { - display("Operation Cancelled") - } - None - } +error_set! { + OxError = { + #[display("Error in I/O: {0}")] + Render(std::io::Error), + #[display("{}", + match source { + KError::NoFileName => "This document has no file name, please use 'save as' instead".to_string(), + KError::OutOfRange => "Requested operation is out of range".to_string(), + KError::ReadOnlyFile => "This file is read only and can't be saved or edited".to_string(), + KError::Rope(rerr) => format!("Backend had an issue processing text: {rerr}"), + KError::Io(ioerr) => format!("I/O Error: {ioerr}"), + } + )] + Kaolinite(KError), + #[display("Error in config file: {}", msg)] + Config { + msg: String + }, + #[display("Error in lua: {0}")] + Lua(mlua::prelude::LuaError), + #[display("Operation Cancelled")] + Cancelled + }; } /// Easy syntax sugar to have functions return the custom error type From 50535e63150ae12fb9ef74d9f49c1642508afe4e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:42:06 +0000 Subject: [PATCH 02/73] version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 951b35e0..eab47853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.6.9" +version = "0.6.10" dependencies = [ "alinio", "base64", diff --git a/Cargo.toml b/Cargo.toml index 023e7e96..90bb6edc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["cactus"] [package] name = "ox" -version = "0.6.9" +version = "0.6.10" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A Rust powered text editor." From 5171d73cabd33aa3fb9b7b4e3f831fb99b652dbc Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:53:53 +0000 Subject: [PATCH 03/73] dependency bump --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eab47853..f086a37c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe026d6bd1583a9cf9080e189030ddaea7e6f5f0deb366a8e26f8a26c4135b8" +checksum = "e9eebac25c35a13285456c88ee2fde93d9aee8bcfdaf03f9d6d12be3391351ec" dependencies = [ "cc", "cfg-if", @@ -359,9 +359,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -433,9 +433,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -478,9 +478,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags", "errno", @@ -497,18 +497,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", @@ -580,9 +580,9 @@ checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a" [[package]] name = "syn" -version = "2.0.80" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e185e337f816bc8da115b8afcb3324006ccc82eeaddf35113888d3bd8e44ac" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -602,18 +602,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", From 07abc4d2c44e18e72ae8baa72b60350b5ef034c9 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:57:53 +0000 Subject: [PATCH 04/73] Fixed issues with already open error --- src/editor/mod.rs | 6 +++--- src/error.rs | 2 +- src/main.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 88070420..7a639745 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -135,9 +135,9 @@ impl Editor { pub fn open(&mut self, file_name: &str) -> Result<()> { if let Some(idx) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) { self.ptr = idx; - return Err(OxError::AlreadyOpen( - get_file_name(file_name).unwrap_or_default(), - )); + return Err(OxError::AlreadyOpen { + file: get_file_name(file_name).unwrap_or_default(), + }); } let mut size = size()?; size.h = size.h.saturating_sub(1 + self.push_down); diff --git a/src/error.rs b/src/error.rs index 9bef643f..e1f9010c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ /// Error handling utilities -use kaolinite::event::Error as KError; use error_set::error_set; +use kaolinite::event::Error as KError; error_set! { OxError = { diff --git a/src/main.rs b/src/main.rs index e3bd2d67..8fb02917 100644 --- a/src/main.rs +++ b/src/main.rs @@ -327,7 +327,7 @@ fn handle_file_opening(editor: &Rc>, result: Result<()>, name: & } match result { Ok(()) => (), - Err(OxError::AlreadyOpen(_)) => { + Err(OxError::AlreadyOpen { .. }) => { let len = editor.borrow().files.len().saturating_sub(1); editor.borrow_mut().ptr = len; } From d6ed736c9360230622b08ecedb3f4b47c4fc90a8 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:15:12 +0000 Subject: [PATCH 05/73] Code cleanup --- src/config/colors.rs | 13 +++++-------- src/config/highlighting.rs | 5 ++--- src/config/mod.rs | 5 ++--- src/editor/mod.rs | 5 ++--- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/config/colors.rs b/src/config/colors.rs index 769ffb7d..daf24824 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -342,10 +342,9 @@ impl Color { // Ensure the hex code is exactly 6 characters long if hex.len() != 6 { - let msg = "Invalid hex code used in configuration file - ensure they are of length 6"; - return Err(OxError::Config { - msg: msg.to_string(), - }); + let msg = "Invalid hex code used in configuration file - ensure they are of length 6" + .to_string(); + return Err(OxError::Config { msg }); } // Parse the hex string into the RGB components @@ -355,10 +354,8 @@ impl Color { if let Ok(val) = u8::from_str_radix(section, 16) { tri.push(val); } else { - let msg = "Invalid hex code used in configuration file - ensure all digits are between 0 and F"; - return Err(OxError::Config { - msg: msg.to_string(), - }); + let msg = "Invalid hex code used in configuration file - ensure all digits are between 0 and F".to_string(); + return Err(OxError::Config { msg }); } } Ok((tri[0], tri[1], tri[2])) diff --git a/src/config/highlighting.rs b/src/config/highlighting.rs index e020498a..bb369550 100644 --- a/src/config/highlighting.rs +++ b/src/config/highlighting.rs @@ -61,9 +61,8 @@ impl SyntaxHighlighting { if let Some(col) = self.theme.get(name) { col.to_color() } else { - Err(OxError::Config { - msg: format!("{name} has not been given a colour in the theme",), - }) + let msg = format!("{name} has not been given a colour in the theme"); + Err(OxError::Config { msg }) } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 69c990db..57d4916b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -179,9 +179,8 @@ impl Config { if user_provided_config { Ok(()) } else { - Err(OxError::Config { - msg: "Not Found".to_string(), - }) + let msg = "Not Found".to_string(); + Err(OxError::Config { msg }) } } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 7a639745..4fb300ec 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -135,9 +135,8 @@ impl Editor { pub fn open(&mut self, file_name: &str) -> Result<()> { if let Some(idx) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) { self.ptr = idx; - return Err(OxError::AlreadyOpen { - file: get_file_name(file_name).unwrap_or_default(), - }); + let file = get_file_name(file_name).unwrap_or_default(); + return Err(OxError::AlreadyOpen { file }); } let mut size = size()?; size.h = size.h.saturating_sub(1 + self.push_down); From 0f2e8095de2746d9e8683598bac7fce662cd648e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:25:25 +0000 Subject: [PATCH 06/73] Fixed inefficiency issues with livehtml plugin --- plugins/live_html.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/live_html.lua b/plugins/live_html.lua index ee3a49cf..62c52123 100644 --- a/plugins/live_html.lua +++ b/plugins/live_html.lua @@ -1,5 +1,5 @@ --[[ -Live HTML v0.1 +Live HTML v0.2 As you develop a website, you can view it in your browser without needing to refresh with every change ]]-- @@ -33,7 +33,6 @@ end function live_html_refresh() if editor.file_path == live_html.entry_point then local contents = editor:get():gsub('"', '\\"'):gsub("\n", "") - editor:rerender() http.post("localhost:5000/update", contents) end end @@ -56,7 +55,9 @@ commands["html"] = function(args) end event_mapping["*"] = function() - after(1, "live_html_refresh") + if live_html.pid ~= nil then + after(1, "live_html_refresh") + end end event_mapping["exit"] = function() From d785f191a77c2b1d922e8a542fb775736d4a7692 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:31:34 +0000 Subject: [PATCH 07/73] Added line selection keyboard shortcuts --- config/.oxrc | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 8b37a91f..07ba078c 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -13,18 +13,6 @@ event_mapping = { ["right"] = function() editor:move_right() end, - ["shift_up"] = function() - editor:select_up() - end, - ["shift_down"] = function() - editor:select_down() - end, - ["shift_left"] = function() - editor:select_left() - end, - ["shift_right"] = function() - editor:select_right() - end, ["ctrl_up"] = function() editor:move_top() end, @@ -49,9 +37,6 @@ event_mapping = { ["pagedown"] = function() editor:move_page_down() end, - ["esc"] = function() - editor:cancel_selection() - end, ["alt_v"] = function() editor:cursor_to_viewport() end, @@ -59,6 +44,34 @@ event_mapping = { local line = editor:prompt("Go to line") editor:move_to(0, tonumber(line)) end, + -- Selection + ["shift_up"] = function() + editor:select_up() + end, + ["shift_down"] = function() + editor:select_down() + end, + ["shift_left"] = function() + editor:select_left() + end, + ["shift_right"] = function() + editor:select_right() + end, + ["esc"] = function() + editor:cancel_selection() + end, + ["shift_home"] = function() + local n_moves = editor.cursor.x + for i = 1, n_moves do + editor:select_left() + end + end, + ["shift_end"] = function() + local n_moves = #editor:get_line() - editor.cursor.x + for i = 1, n_moves do + editor:select_right() + end + end, -- Searching & Replacing ["ctrl_f"] = function() editor:search() From 200659ff69febc2f10d570fcdf561378f63e387d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:29:56 +0000 Subject: [PATCH 08/73] Added key bindings to select words left and right --- config/.oxrc | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/config/.oxrc b/config/.oxrc index 07ba078c..c8c267e8 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -72,6 +72,38 @@ event_mapping = { editor:select_right() end end, + ["ctrl_shift_left"] = function() + local no_select = editor.cursor.x == editor.selection.x and editor.cursor.y == editor.selection.y + if no_select then + local cache = editor.cursor + editor:move_previous_word() + local after = editor.cursor + editor:move_to(cache.x, cache.y) + editor:select_to(after.x, after.y) + else + local start = editor.selection + editor:move_previous_word() + local cache = editor.cursor + editor:move_to(start.x, start.y) + editor:select_to(cache.x, cache.y) + end + end, + ["ctrl_shift_right"] = function() + local no_select = editor.cursor.x == editor.selection.x and editor.cursor.y == editor.selection.y + if no_select then + local cache = editor.cursor + editor:move_next_word() + local after = editor.cursor + editor:move_to(cache.x, cache.y) + editor:select_to(after.x, after.y) + else + local start = editor.selection + editor:move_next_word() + local cache = editor.cursor + editor:move_to(start.x, start.y) + editor:select_to(cache.x, cache.y) + end + end, -- Searching & Replacing ["ctrl_f"] = function() editor:search() From d406b10e3a40c9dd1850d7b27c6983747cf973fe Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:48:11 +0000 Subject: [PATCH 09/73] Added key bindings to select page up and page down --- config/.oxrc | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/config/.oxrc b/config/.oxrc index c8c267e8..6283b0ae 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -104,6 +104,38 @@ event_mapping = { editor:select_to(cache.x, cache.y) end end, + ["shift_pageup"] = function() + local no_select = editor.cursor.x == editor.selection.x and editor.cursor.y == editor.selection.y + if no_select then + local cache = editor.cursor + editor:move_page_up() + local after = editor.cursor + editor:move_to(cache.x, cache.y) + editor:select_to(after.x, after.y) + else + local start = editor.selection + editor:move_page_up() + local cache = editor.cursor + editor:move_to(start.x, start.y) + editor:select_to(cache.x, cache.y) + end + end, + ["shift_pagedown"] = function() + local no_select = editor.cursor.x == editor.selection.x and editor.cursor.y == editor.selection.y + if no_select then + local cache = editor.cursor + editor:move_page_down() + local after = editor.cursor + editor:move_to(cache.x, cache.y) + editor:select_to(after.x, after.y) + else + local start = editor.selection + editor:move_page_down() + local cache = editor.cursor + editor:move_to(start.x, start.y) + editor:select_to(cache.x, cache.y) + end + end, -- Searching & Replacing ["ctrl_f"] = function() editor:search() From 4df7aef4b1bc7ab5ee19eb6c5809b94177a43ec9 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:35:00 +0000 Subject: [PATCH 10/73] Bulk delete selected lines with ctrl+d --- config/.oxrc | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/config/.oxrc b/config/.oxrc index 6283b0ae..f75629aa 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -193,7 +193,25 @@ event_mapping = { help_message.enabled = not help_message.enabled end, ["ctrl_d"] = function() - editor:remove_line() + local cursor = editor.cursor + local select = editor.selection + local no_select = select.x == cursor.x and select.y == cursor.y + if no_select then + editor:remove_line() + else + -- delete a group of lines + if cursor.y > select.y then + editor:move_to(cursor.x, select.y) + for line = select.y, cursor.y do + editor:remove_line() + end + else + editor:move_to(cursor.x, cursor.y) + for line = cursor.y, select.y do + editor:remove_line() + end + end + end end, ["ctrl_k"] = function() editor:open_command_line() From f2f1b063805d9aae251236c57a1fe1f62743d271 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:18:51 +0000 Subject: [PATCH 11/73] Improved word selection --- kaolinite/src/document.rs | 131 +++++++++++++++++++++++++++++++------- src/editor/mod.rs | 3 + src/editor/mouse.rs | 17 ++++- 3 files changed, 128 insertions(+), 23 deletions(-) diff --git a/kaolinite/src/document.rs b/kaolinite/src/document.rs index 2f774ff3..57ef862a 100644 --- a/kaolinite/src/document.rs +++ b/kaolinite/src/document.rs @@ -693,7 +693,8 @@ impl Document { } /// Find the word boundaries - pub fn word_boundaries(&mut self, line: &str) -> Vec<(usize, usize)> { + #[must_use] + pub fn word_boundaries(&self, line: &str) -> Vec<(usize, usize)> { let re = r"(\s{2,}|[A-Za-z0-9_]+|\.)"; let mut searcher = Searcher::new(re); let starts: Vec = searcher.lfinds(line); @@ -706,7 +707,8 @@ impl Document { } /// Find the current state of the cursor in relation to words - pub fn cursor_word_state(&mut self, words: &[(usize, usize)], x: usize) -> WordState { + #[must_use] + pub fn cursor_word_state(&self, words: &[(usize, usize)], x: usize) -> WordState { let in_word = words .iter() .position(|(start, end)| *start <= x && x <= *end); @@ -724,19 +726,44 @@ impl Document { } } - /// Moves to the previous word in the document - pub fn move_prev_word(&mut self) -> Status { - let Loc { x, y } = self.char_loc(); - // Handle case where we're at the beginning of the line - if x == 0 && y != 0 { - return Status::StartOfLine; + /// Find the index of the next word + #[must_use] + pub fn prev_word_close(&self, from: Loc) -> usize { + let Loc { x, y } = from; + let line = self.line(y).unwrap_or_default(); + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + match state { + // Go to start of line if at beginning + WordState::AtEnd(0) | WordState::InCenter(0) | WordState::AtStart(0) => 0, + // Cursor is at the middle / end of a word, move to previous end + WordState::AtEnd(idx) | WordState::InCenter(idx) => words[idx.saturating_sub(1)].1, + WordState::AtStart(idx) => words[idx.saturating_sub(1)].0, + WordState::Out => { + // Cursor is not touching any words, find previous end + let mut shift_back = x; + while let WordState::Out = self.cursor_word_state(&words, shift_back) { + shift_back = shift_back.saturating_sub(1); + if shift_back == 0 { + break; + } + } + match self.cursor_word_state(&words, shift_back) { + WordState::AtEnd(idx) => words[idx].0, + _ => 0, + } + } } - // Find where all the words are + } + + /// Find the index of the next word + #[must_use] + pub fn prev_word_index(&self, from: Loc) -> usize { + let Loc { x, y } = from; let line = self.line(y).unwrap_or_default(); let words = self.word_boundaries(&line); let state = self.cursor_word_state(&words, x); - // Work out where to move to - let new_x = match state { + match state { // Go to start of line if at beginning WordState::AtEnd(0) | WordState::InCenter(0) | WordState::AtStart(0) => 0, // Cursor is at the middle / end of a word, move to previous end @@ -756,7 +783,18 @@ impl Document { _ => 0, } } - }; + } + } + + /// Moves to the previous word in the document + pub fn move_prev_word(&mut self) -> Status { + let Loc { x, y } = self.char_loc(); + // Handle case where we're at the beginning of the line + if x == 0 && y != 0 { + return Status::StartOfLine; + } + // Work out where to move to + let new_x = self.prev_word_index(self.char_loc()); // Perform the move self.move_to_x(new_x); // Clean up @@ -764,20 +802,57 @@ impl Document { Status::None } - /// Moves to the next word in the document - pub fn move_next_word(&mut self) -> Status { - let Loc { x, y } = self.char_loc(); + /// Find the index of the next word + #[must_use] + pub fn next_word_close(&self, from: Loc) -> usize { + let Loc { x, y } = from; let line = self.line(y).unwrap_or_default(); - // Handle case where we're at the end of the line - if x == line.chars().count() && y != self.len_lines() { - return Status::EndOfLine; + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + match state { + // Cursor is at the middle / end of a word, move to next end + WordState::AtEnd(idx) | WordState::InCenter(idx) => { + if let Some(word) = words.get(idx) { + word.1 + } else { + // No next word exists, just go to end of line + line.chars().count() + } + } + WordState::AtStart(idx) => { + // Cursor is at the start of a word, move to next start + if let Some(word) = words.get(idx) { + word.0 + } else { + // No next word exists, just go to end of line + line.chars().count() + } + } + WordState::Out => { + // Cursor is not touching any words, find next start + let mut shift_forward = x; + while let WordState::Out = self.cursor_word_state(&words, shift_forward) { + shift_forward += 1; + if shift_forward >= line.chars().count() { + break; + } + } + match self.cursor_word_state(&words, shift_forward) { + WordState::AtStart(idx) => words[idx].0, + _ => line.chars().count(), + } + } } - // Find and move to the next word + } + + /// Find the index of the next word + #[must_use] + pub fn next_word_index(&self, from: Loc) -> usize { + let Loc { x, y } = from; let line = self.line(y).unwrap_or_default(); let words = self.word_boundaries(&line); let state = self.cursor_word_state(&words, x); - // Work out where to move to - let new_x = match state { + match state { // Cursor is at the middle / end of a word, move to next end WordState::AtEnd(idx) | WordState::InCenter(idx) => { if let Some(word) = words.get(idx + 1) { @@ -810,7 +885,19 @@ impl Document { _ => line.chars().count(), } } - }; + } + } + + /// Moves to the next word in the document + pub fn move_next_word(&mut self) -> Status { + let Loc { x, y } = self.char_loc(); + let line = self.line(y).unwrap_or_default(); + // Handle case where we're at the end of the line + if x == line.chars().count() && y != self.len_lines() { + return Status::EndOfLine; + } + // Work out where to move to + let new_x = self.next_word_index(self.char_loc()); // Perform the move self.move_to_x(new_x); // Clean up diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 4fb300ec..6db24fad 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -57,6 +57,8 @@ pub struct Editor { pub plugin_active: bool, /// Stores the last click the user made (in order to detect double-click) pub last_click: Option<(Instant, MouseEvent)>, + /// Stores whether or not we're in a double click + pub in_dbl_click: bool, } impl Editor { @@ -78,6 +80,7 @@ impl Editor { config_path: "~/.oxrc".to_string(), plugin_active: false, last_click: None, + in_dbl_click: false, }) } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index dbfbeb6c..89934146 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -56,6 +56,7 @@ impl Editor { 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; } @@ -81,6 +82,7 @@ impl Editor { } // 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)); @@ -89,7 +91,20 @@ impl Editor { MouseEventKind::Drag(MouseButton::Left) => match self.find_mouse_location(lua, event) { MouseLocation::File(mut loc) => { loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().select_to(&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 { + loc.x = self.doc_mut().character_idx(&loc); + self.doc_mut().select_to(&loc); + } } MouseLocation::Tabs(_) | MouseLocation::Out => (), }, From 56908f2edebe57474f7bb478fee2848fbe82f03a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:58:48 +0000 Subject: [PATCH 12/73] Stripped back kaolinite --- kaolinite/Cargo.toml | 6 - kaolinite/LICENSE | 21 - kaolinite/README.md | 73 --- kaolinite/examples/cactus/Cargo.toml | 19 - kaolinite/examples/cactus/src/main.rs | 660 ---------------------- kaolinite/examples/cactus/src/main_old.rs | 586 ------------------- kaolinite/examples/cactus/test.txt | 1 - kaolinite/examples/debug.rs | 13 - kaolinite/examples/open.rs | 22 - kaolinite/examples/searching.rs | 11 - kaolinite/examples/tabs.rs | 17 - kaolinite/examples/trim.rs | 10 - 12 files changed, 1439 deletions(-) delete mode 100644 kaolinite/LICENSE delete mode 100644 kaolinite/README.md delete mode 100644 kaolinite/examples/cactus/Cargo.toml delete mode 100644 kaolinite/examples/cactus/src/main.rs delete mode 100644 kaolinite/examples/cactus/src/main_old.rs delete mode 100644 kaolinite/examples/cactus/test.txt delete mode 100644 kaolinite/examples/debug.rs delete mode 100644 kaolinite/examples/open.rs delete mode 100644 kaolinite/examples/searching.rs delete mode 100644 kaolinite/examples/tabs.rs delete mode 100644 kaolinite/examples/trim.rs diff --git a/kaolinite/Cargo.toml b/kaolinite/Cargo.toml index 8086c3d6..5a01e3de 100644 --- a/kaolinite/Cargo.toml +++ b/kaolinite/Cargo.toml @@ -23,9 +23,3 @@ sugars = "3.0.1" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } - -[[example]] -name = "open" - -[[example]] -name = "trim" diff --git a/kaolinite/LICENSE b/kaolinite/LICENSE deleted file mode 100644 index 294909dd..00000000 --- a/kaolinite/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 curlpipe - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/kaolinite/README.md b/kaolinite/README.md deleted file mode 100644 index e09aa52b..00000000 --- a/kaolinite/README.md +++ /dev/null @@ -1,73 +0,0 @@ -

-
- Markdownify -
- Kaolinite -
-

- -

A crate to assist in the creation of TUI text editors.

- -

- Key Features • - How To Use • - Credits • - License -

- -## Key Features - -- Buffers files to prevent hold ups when opening and saving your files -- Unicode safe - supports double width characters on the terminal -- Handles scrolling and cursor - No more janky cursor incrementing code -- Dynamically handles formatting of files - Determines style on read, keeps that style on write - + Unix and DOS line endings - + Tabs & Spaces -- Includes searching & replacing features -- Line number formatting utility -- File type recognition -- Advanced moving abilities (by page, words, characters) -- Includes undo & redo functionality -- Lightweight - very few dependencies for quick compilation of your editor -- Front-end agnostic - Feel free to use [Crossterm](https://github.com/crossterm-rs/crossterm) or [Termion](https://gitlab.redox-os.org/redox-os/termion) or anything else! - -## How To Use - -You'll need to have a modern Rust toolchain. Click [here](https://www.rust-lang.org/tools/install) if you need that. - -```bash -# If you already have a project set up, ignore this step -$ cargo new [app name] -$ cd [app name] - -# Simplest way to add to your project is using cargo-edit -# You can also manually add it into your Cargo.toml if you wish -$ cargo install cargo-edit -$ cargo add kaolinite - -# You should be ready to use the crate now! -``` - -If you require documentation, please consult https://docs.rs/kaolinite. You'll find detailed API explainations and examples. - -Don't hesitate to contact me (see bottom of readme) if you require assistance. - - -## Credits - -This software uses the following open source crates: - -- [unicode-width](https://github.com/unicode-rs/unicode-width) -- [regex](https://github.com/rust-lang/regex) -- [ropey](https://github.com/cessen/ropey) -- [error_set](https://github.com/mcmah309/error_set) - -## License - -MIT - ---- - -> Github [@curlpipe](https://github.com/curlpipe)  ·  -> Discord [curlpipe#1496](https://discord.com)  ·  -> Crates.io [curlpipe](https://crates.io/users/curlpipe) diff --git a/kaolinite/examples/cactus/Cargo.toml b/kaolinite/examples/cactus/Cargo.toml deleted file mode 100644 index 4554b11b..00000000 --- a/kaolinite/examples/cactus/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "cactus" -version = "0.1.0" -edition = "2021" - -#[profile.release] -#strip = true -#debug = true -#lto = "fat" -#codegen-units = 1 -#panic = "abort" - - -[dependencies] -alinio = "0.2.1" -crossterm = "0.25.0" -jargon-args = "0.2.5" -kaolinite = { version = "0", path = "../../" } -synoptic = { version = "2" } diff --git a/kaolinite/examples/cactus/src/main.rs b/kaolinite/examples/cactus/src/main.rs deleted file mode 100644 index 90558931..00000000 --- a/kaolinite/examples/cactus/src/main.rs +++ /dev/null @@ -1,660 +0,0 @@ -/* - Cactus - A complete text editor in under 500 source lines of code - - File buffering for efficient file reading and writing - - Full double width character support - - Openining multiple files - - Undo and redo capability - - File type detection - - Line numbers and status bar - - Quickly move by pages, words, and get to the top or bottom of a document instantly - - Search forward and backward in a document - - Replace text in a document - - Efficient syntax highlighting - - Compiles in under half a minute on most modern computers -*/ - -#![allow(unused_must_use)] - -use crossterm::{ - cursor::{Hide, MoveTo, Show}, - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, - execute, - style::{Color, SetBackgroundColor as Bg, SetForegroundColor as Fg}, - terminal::{self, Clear, ClearType as ClType, EnterAlternateScreen, LeaveAlternateScreen, EnableLineWrap, DisableLineWrap}, -}; -use jargon_args::Jargon; -use kaolinite::event::{Event, Result, Status}; -use kaolinite::utils::{filetype, Loc, Size}; -use kaolinite::Document; -use synoptic::{Highlighter, TokOpt, trim, from_extension}; -use std::io::{stdout, Stdout, Write}; - -/// Store the version number at compile time -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// Help text for the command line interface -const HELP: &str = "\ -Cactus: A compact and complete kaolinite implementation - -USAGE: cactus [options] [files] - -OPTIONS: - --help, -h : Show this help message - --version, -v : Show the version number - -EXAMPLES: - cactus test.txt - cactus test.txt test2.txt - cactus /home/user/docs/test.txt -"; - -fn main() { - // Reset teriminal in the event of a crash - std::panic::set_hook(Box::new(|e| { - terminal::disable_raw_mode().unwrap(); - execute!(stdout(), LeaveAlternateScreen, Show).unwrap(); - eprintln!("{}", e); - })); - // Run cactus - if let Err(e) = run() { - terminal::disable_raw_mode().unwrap(); - execute!(stdout(), LeaveAlternateScreen, Show).unwrap(); - eprintln!("{}", e); - } -} - -/// This will parse arguments, and run cactus, handling any errors that occur -fn run() -> Result<()> { - let mut args = Jargon::from_env(); - if args.contains(["-h", "--help"]) { - print!("{}", HELP); - } else if args.contains(["-v", "--version"]) { - println!("{}", VERSION); - } else { - let mut e = Editor::new()?; - let mut error = false; - // Try to open the requested files - for file in args.finish() { - if let Err(err) = e.open(file.clone()) { - // If the file failed to open, make a note of it and display it - println!("Couldn't open file \"{}\": {}", file, err); - error = true; - } - } - // If all files opened without error, run cactus - if !error { - e.run()?; - } - } - Ok(()) -} - -/// Gets the size of the terminal -fn size() -> Result { - let (w, h) = terminal::size()?; - Ok(Size { - w: w as usize, - h: (h as usize).saturating_sub(1), - }) -} - -/// For managing all editing and rendering of cactus -pub struct Editor { - /// Interface for writing to the terminal - stdout: Stdout, - /// Storage of all the documents opened in the editor - doc: Vec, - /// Syntax highlighting integration - highlighter: Highlighter, - /// Pointer to the document that is currently being edited - ptr: usize, - /// true if the editor is still running, false otherwise - active: bool, -} - -impl Editor { - /// Create a new instance of the editor - pub fn new() -> Result { - Ok(Self { - doc: vec![], - ptr: 0, - stdout: stdout(), - active: true, - highlighter: Highlighter::new(4), - }) - } - - /// Function to open a document into the editor - pub fn open(&mut self, file_name: String) -> Result<()> { - let size = size()?; - let mut doc = Document::open(size, file_name.clone())?; - // Load all the lines within viewport into the document - doc.load_to(size.h); - // Update in the syntax highlighter - let ext = file_name.split('.').last().unwrap(); - self.highlighter = from_extension(ext, 4).unwrap_or(Highlighter::new(4)); - self.highlighter.run(&doc.lines); - // Add document to documents - self.doc.push(doc); - Ok(()) - } - - /// Gets a reference to the current document - pub fn doc(&self) -> &Document { - self.doc.get(self.ptr).unwrap() - } - - /// Gets a mutable reference to the current document - pub fn doc_mut(&mut self) -> &mut Document { - self.doc.get_mut(self.ptr).unwrap() - } - - /// Set up the terminal so that it is clean and doesn't effect existing terminal text - pub fn start(&mut self) -> Result<()> { - execute!(self.stdout, EnterAlternateScreen, Clear(ClType::All), DisableLineWrap)?; - terminal::enable_raw_mode()?; - Ok(()) - } - - /// Restore terminal back to state before the editor was started - pub fn end(&mut self) -> Result<()> { - terminal::disable_raw_mode()?; - execute!(self.stdout, LeaveAlternateScreen, EnableLineWrap)?; - Ok(()) - } - - /// Execute an edit event - pub fn exe(&mut self, ev: Event) -> Result<()> { - self.doc_mut().exe(ev) - } - - /// Initialise, render and handle events as they come in - pub fn run(&mut self) -> Result<()> { - // If no documents were provided, just exit - self.active = !self.doc.is_empty(); - self.start()?; - while self.active { - self.render()?; - // Wait for an event - match read()? { - CEvent::Key(key) => match (key.modifiers, key.code) { - // Movement - (KMod::NONE, KCode::Up) => self.up(), - (KMod::NONE, KCode::Down) => self.down(), - (KMod::NONE, KCode::Left) => self.left(), - (KMod::NONE, KCode::Right) => self.right(), - (KMod::CONTROL, KCode::Up) => self.doc_mut().move_top(), - (KMod::CONTROL, KCode::Down) => self.doc_mut().move_bottom(), - (KMod::CONTROL, KCode::Left) => self.prev_word(), - (KMod::CONTROL, KCode::Right) => self.next_word(), - (KMod::NONE, KCode::Home) => self.doc_mut().move_home(), - (KMod::NONE, KCode::End) => self.doc_mut().move_end(), - (KMod::NONE, KCode::PageUp) => self.doc_mut().move_page_up(), - (KMod::NONE, KCode::PageDown) => self.doc_mut().move_page_down(), - // Searching & Replacing - (KMod::CONTROL, KCode::Char('f')) => self.search()?, - (KMod::CONTROL, KCode::Char('r')) => self.replace()?, - // Document management - (KMod::CONTROL, KCode::Char('s')) => self.save(), - (KMod::ALT, KCode::Char('s')) => self.save_as()?, - (KMod::CONTROL, KCode::Char('a')) => self.save_all(), - (KMod::CONTROL, KCode::Char('q')) => self.quit(), - (KMod::SHIFT, KCode::Left) => self.prev(), - (KMod::SHIFT, KCode::Right) => self.next(), - // Undo & Redo - (KMod::CONTROL, KCode::Char('z')) => self.doc_mut().undo()?, - (KMod::CONTROL, KCode::Char('y')) => self.doc_mut().redo()?, - // Editing - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch), - (KMod::NONE, KCode::Tab) => self.character('\t'), - (KMod::NONE, KCode::Backspace) => self.backspace(), - (KMod::NONE, KCode::Enter) => self.enter(), - (KMod::CONTROL, KCode::Char('d')) => self.delete_line(), - _ => (), - }, - CEvent::Resize(w, h) => { - // Ensure all lines in viewport are loaded - let max = self.doc().len_lines().to_string().len() + 2; - self.doc_mut().size.w = w.saturating_sub(max as u16) as usize; - self.doc_mut().size.h = h.saturating_sub(1) as usize; - let max = self.doc().offset.x + self.doc().size.h; - self.doc_mut().load_to(max); - } - _ => (), - } - // Append any missed lines to the syntax highlighter - let actual = self.doc.get(self.ptr).and_then(|d| Some(d.loaded_to)).unwrap_or(0); - let percieved = self.highlighter.line_ref.len(); - if percieved < actual { - let diff = actual - percieved; - for i in 0..diff { - let line = &self.doc[self.ptr].lines[percieved + i]; - self.highlighter.append(line); - } - } - } - self.end()?; - Ok(()) - } - - /// Render a single frame of the editor in it's current state - pub fn render(&mut self) -> Result<()> { - execute!(self.stdout, Hide)?; - let Size { w, h } = size()?; - // Update the width of the document in case of update - let max = self.doc().len_lines().to_string().len() + 2; - self.doc_mut().size.w = w.saturating_sub(max) as usize; - // Run through each line of the terminal, rendering the correct line - self.render_document(w, h)?; - // Leave last line for status line - self.render_status_line(w, h)?; - // Move cursor to the correct location and perform render - let Loc { x, y } = self.doc().cursor; - execute!(self.stdout, Show, MoveTo((x + max) as u16, y as u16))?; - self.stdout.flush()?; - Ok(()) - } - - /// Render the lines of the document - fn render_document(&mut self, w: usize, h: usize) -> Result<()> { - for y in 0..(h as u16) { - execute!(self.stdout, MoveTo(0, y))?; - // Write line number of document - let num = self.doc().line_number(y as usize + self.doc().offset.y); - write!( - self.stdout, - "{}{} │{}{}", - Fg(Color::Rgb { r: 150, g: 150, b: 150 }), - num, - Fg(Color::Reset), - Clear(ClType::UntilNewLine), - )?; - // Render line if it exists - let idx = y as usize + self.doc().offset.y; - if let Some(line) = self.doc().line(idx) { - let tokens = self.highlighter.line(idx, &line); - let tokens = trim(&tokens, self.doc().offset.x); - for token in tokens { - match token { - TokOpt::Some(text, kind) => write!( - self.stdout, - "{}{text}{}", - self.highlight_colour(&kind), - Fg(Color::Reset) - ), - TokOpt::None(text) => write!(self.stdout, "{text}"), - }? - } - } - } - Ok(()) - } - - /// Render the status line at the bottom of the document - fn render_status_line(&mut self, w: usize, h: usize) -> Result<()> { - execute!(self.stdout, MoveTo(0, h as u16))?; - let ext = self.doc().file_name.as_ref().unwrap().split('.').last().unwrap().to_string(); - // Form left hand side of status bar - let lhs = format!( - "{}{} │ {} │", - self.doc().file_name.as_ref().unwrap().split('/').last().unwrap(), - if self.doc().modified { "[+]" } else { "" }, - filetype(&ext).unwrap_or(ext) - ); - // Form right hand side of status bar - let rhs = format!( - "│ {}/{} {} {}", - self.doc().loc().y + 1, - self.doc().len_lines(), - self.doc().char_ptr, - self.doc().loc().x, - ); - // Use alinio to align left and right with padding between - let status_line = alinio::align::between(&[&lhs, &rhs], w.saturating_sub(2)) - .unwrap_or_else(|| "".to_string()); - // Write the status bar - write!( - self.stdout, - "{}{} {} {}{}", - Fg(Color::Black), - //Bg(Color::Rgb { r: 54, g: 161, b: 102 }), - Bg(Color::Rgb { r: 91, g: 157, b: 72 }), - status_line, - Bg(Color::Reset), - Fg(Color::Reset), - )?; - Ok(()) - } - - /// Display a prompt in the document - fn prompt>(&mut self, prompt: S) -> Result { - let prompt = prompt.into(); - let mut input = String::new(); - let mut done = false; - // Enter into a menu that asks for a prompt - while !done { - let h = size()?.h; - // Render prompt message - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - write!(self.stdout, "{}: {}", prompt, input); - self.stdout.flush()?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // Exit the menu when the enter key is pressed - (KMod::NONE, KCode::Enter) => done = true, - // Remove from the input string if the user presses backspace - (KMod::NONE, KCode::Backspace) => { input.pop(); }, - // Add to the input string if the user presses a character - (KMod::NONE | KMod::SHIFT, KCode::Char(c)) => input.push(c), - _ => (), - } - } - } - // Return input string result - Ok(input) - } - - /// Find the appropriate syntax highlighting colour - fn highlight_colour(&self, name: &str) -> String { - match name { - "string" => Fg(Color::Rgb { r: 54, g: 161, b: 102 }), - "comment" => Fg(Color::Rgb { r: 108, g: 107, b: 90 }), - "digit" => Fg(Color::Rgb { r: 157, g: 108, b: 124 }), - "keyword" => Fg(Color::Rgb { r: 91, g: 157, b: 72 }), - "attribute" => Fg(Color::Rgb { r: 95, g: 145, b: 130 }), - "character" => Fg(Color::Rgb { r: 125, g: 151, b: 38 }), - "type" => Fg(Color::Rgb { r: 165, g: 152, b: 13 }), - "function" => Fg(Color::Rgb { r: 174, g: 115, b: 19 }), - "header" => Fg(Color::Rgb { r: 174, g: 115, b: 19 }), - "macro" => Fg(Color::Rgb { r: 157, g: 108, b: 124 }), - "namespace" => Fg(Color::Rgb { r: 125, g: 151, b: 38 }), - "struct" => Fg(Color::Rgb { r: 125, g: 151, b: 38 }), - "operator" => Fg(Color::Rgb { r: 95, g: 145, b: 130 }), - "boolean" => Fg(Color::Rgb { r: 54, g: 161, b: 102 }), - "reference" => Fg(Color::Rgb { r: 91, g: 157, b: 72 }), - "tag" => Fg(Color::Rgb { r: 95, g: 145, b: 130 }), - "heading" => Fg(Color::Rgb { r: 174, g: 115, b: 19 }), - "link" => Fg(Color::Rgb { r: 157, g: 108, b: 124 }), - "key" => Fg(Color::Rgb { r: 157, g: 108, b: 124 }), - _ => panic!("Invalid token name: {name}"), - }.to_string() - } - - /// Move to the next document opened in the editor - fn next(&mut self) { - if self.ptr + 1 < self.doc.len() { - self.ptr += 1; - } - } - - /// Move to the previous document opened in the editor - fn prev(&mut self) { - if self.ptr != 0 { - self.ptr -= 1; - } - } - - /// Move the cursor up - fn up(&mut self) { - self.doc_mut().move_up(); - } - - /// Move the cursor down - fn down(&mut self) { - self.doc_mut().move_down(); - } - - /// Move the cursor left - fn left(&mut self) { - let status = self.doc_mut().move_left(); - // Cursor wrapping if cursor hits the start of the line - if status == Status::StartOfLine && self.doc().loc().y != 0 { - self.doc_mut().move_up(); - self.doc_mut().move_end(); - } - } - - /// Move the cursor right - fn right(&mut self) { - let status = self.doc_mut().move_right(); - // Cursor wrapping if cursor hits the end of a line - if status == Status::EndOfLine { - self.doc_mut().move_down(); - self.doc_mut().move_home(); - } - } - - /// Move the cursor to the previous word in the line - fn prev_word(&mut self) { - let status = self.doc_mut().move_prev_word(); - if status == Status::StartOfLine { - self.doc_mut().move_up(); - self.doc_mut().move_end(); - } - } - - /// Move the cursor to the next word in the line - fn next_word(&mut self) { - let status = self.doc_mut().move_next_word(); - if status == Status::EndOfLine { - self.doc_mut().move_down(); - self.doc_mut().move_home(); - } - } - - /// Insert a character into the document, creating a new row if editing - /// on the last line of the document - fn character(&mut self, ch: char) { - self.new_row(); - let loc = self.doc().char_loc(); - self.exe(Event::Insert(loc, ch.to_string())); - self.highlighter.edit(loc.y, &self.doc[self.ptr].lines[loc.y]); - } - - /// Handle the return key - fn enter(&mut self) { - if self.doc().loc().y != self.doc().len_lines() { - // Enter pressed in the start, middle or end of the line - let loc = self.doc().char_loc(); - self.exe(Event::SplitDown(loc)); - let line = &self.doc[self.ptr].lines[loc.y + 1]; - self.highlighter.insert_line(loc.y + 1, line); - let line = &self.doc[self.ptr].lines[loc.y]; - self.highlighter.edit(loc.y, line); - } else { - // Enter pressed on the empty line at the bottom of the document - self.new_row(); - } - } - - /// Handle the backspace key - fn backspace(&mut self) { - let mut c = self.doc().char_ptr; - let on_first_line = self.doc().loc().y == 0; - let out_of_range = self.doc().out_of_range(0, self.doc().loc().y).is_err(); - if c == 0 && !on_first_line && !out_of_range { - // Backspace was pressed on the start of the line, move line to the top - self.new_row(); - let mut loc = self.doc().char_loc(); - self.highlighter.remove_line(loc.y); - loc.y -= 1; - loc.x = self.doc().line(loc.y).unwrap().chars().count(); - self.exe(Event::SpliceUp(loc)); - let line = &self.doc[self.ptr].lines[loc.y]; - self.highlighter.edit(loc.y, line); - } else { - // Backspace was pressed in the middle of the line, delete the character - c -= 1; - if let Some(line) = self.doc().line(self.doc().loc().y) { - if let Some(ch) = line.chars().nth(c) { - let loc = Loc { x: c, y: self.doc().loc().y }; - self.exe(Event::Delete(loc, ch.to_string())); - self.highlighter.edit(loc.y, &self.doc[self.ptr].lines[loc.y]); - } - } - } - } - - /// Insert a new row at the end of the document if the cursor is on it - fn new_row(&mut self) { - if self.doc().loc().y == self.doc().len_lines() { - self.exe(Event::InsertLine(self.doc().loc().y, "".to_string())); - self.highlighter.append(&"".to_string()); - } - } - - /// Delete the current line - fn delete_line(&mut self) { - if self.doc().loc().y < self.doc().len_lines() { - let y = self.doc().loc().y; - let line = self.doc().line(y).unwrap(); - self.exe(Event::DeleteLine(y, line)); - self.highlighter.remove_line(y); - } - } - - /// Use search feature - pub fn search(&mut self) -> Result<()> { - // Prompt for a search term - let target = self.prompt("Search")?; - let mut done = false; - let Size { w, h } = size()?; - // Jump to the next match after search term is provided - self.next_match(&target); - // Enter into search menu - while !done { - // Render just the document part - self.render_document(w, h)?; - // Render custom status line with mode information - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - write!(self.stdout, "[<-]: Search previous | [->]: Search next"); - self.stdout.flush()?; - // Move back to correct cursor position - let Loc { x, y } = self.doc().cursor; - let max = self.doc().len_lines().to_string().len() + 2; - execute!(self.stdout, MoveTo((x + max) as u16, y as u16))?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // On return or escape key, exit menu - (KMod::NONE, KCode::Enter | KCode::Esc) => done = true, - // On left key, move to the previous match in the document - (KMod::NONE, KCode::Left) => std::mem::drop(self.prev_match(&target)), - // On right key, move to the next match in the document - (KMod::NONE, KCode::Right) => std::mem::drop(self.next_match(&target)), - _ => (), - } - } - } - Ok(()) - } - - /// Move to the next match - fn next_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().next_match(target, 1)?; - self.doc_mut().goto(&mtch.loc); - Some(mtch.text) - } - - /// Move to the previous match - fn prev_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().prev_match(target)?; - self.doc_mut().goto(&mtch.loc); - Some(mtch.text) - } - - /// Use replace feature - pub fn replace(&mut self) -> Result<()> { - // Request replace information - let target = self.prompt("Replace")?; - let into = self.prompt("With")?; - let mut done = false; - let Size { w, h } = size()?; - // Jump to match - let mut mtch; - if let Some(m) = self.next_match(&target) { - // Automatically move to next match, keeping note of what that match is - mtch = m; - } else if let Some(m) = self.prev_match(&target) { - // Automatically move to previous match, keeping not of what that match is - // This happens if there are no matches further down the document, only above - mtch = m; - } else { - // Exit if there are no matches in the document - return Ok(()); - } - // Enter into the replace menu - while !done { - // Render just the document part - self.render_document(w, h)?; - // Write custom status line for the replace mode - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - write!(self.stdout, "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All"); - self.stdout.flush()?; - // Move back to correct cursor location - let Loc { x, y } = self.doc().cursor; - let max = self.doc().len_lines().to_string().len() + 2; - execute!(self.stdout, MoveTo((x + max) as u16, y as u16))?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // On escape key, exit - (KMod::NONE, KCode::Esc) => done = true, - // On right key, move to the previous match, keeping note of what that match is - (KMod::NONE, KCode::Left) => mtch = self.prev_match(&target).unwrap_or(mtch), - // On left key, move to the next match, keeping note of what that match is - (KMod::NONE, KCode::Right) => mtch = self.next_match(&target).unwrap_or(mtch), - // On return key, perform replacement - (KMod::NONE, KCode::Enter) => self.do_replace(&into, &mtch), - // On tab key, replace all instances within the document - (KMod::NONE, KCode::Tab) => self.do_replace_all(&target, &into), - _ => (), - } - } - } - Ok(()) - } - - /// Replace an instance in a document - fn do_replace(&mut self, into: &str, text: &str) { - let loc = self.doc().char_loc(); - self.doc_mut().replace(loc, text, into); - self.doc_mut().goto(&loc); - } - - /// Replace all instances in a document - fn do_replace_all(&mut self, target: &str, into: &str) { - self.doc_mut().replace_all(target, into); - } - - /// save the document to the disk - pub fn save(&mut self) { - self.doc_mut().save(); - } - - /// save the document to the disk at a specified path - pub fn save_as(&mut self) -> Result<()> { - let file_name = self.prompt("Save as")?; - self.doc_mut().save_as(&file_name)?; - Ok(()) - } - - /// Save all the open documents to the disk - pub fn save_all(&mut self) { - for doc in self.doc.iter_mut() { - doc.save(); - } - } - - /// Quit the editor - pub fn quit(&mut self) { - self.active = !self.doc.is_empty(); - // If there are still documents open, only close the requested document - if self.active { - self.doc.remove(self.ptr); - self.prev(); - } - self.active = !self.doc.is_empty(); - } -} diff --git a/kaolinite/examples/cactus/src/main_old.rs b/kaolinite/examples/cactus/src/main_old.rs deleted file mode 100644 index bf7a753f..00000000 --- a/kaolinite/examples/cactus/src/main_old.rs +++ /dev/null @@ -1,586 +0,0 @@ -/* - Cactus - A tiny text editor in under 450 source lines of code - - File buffering for efficient file reading and writing - - Full double width character support - - Openining multiple files - - Undo and redo capability - - File type detection - - Line numbers and status bar - - Quickly move by pages, words, and get to the top or bottom of a document instantly - - Search forward and backward in a document - - Replace text in a document - - Compiles in under half a minute on most modern computers -*/ - -#![allow(unused_must_use)] - -use crossterm::{ - cursor::{Hide, MoveTo, Show}, - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, - execute, - style::{Color, SetBackgroundColor as Bg, SetForegroundColor as Fg}, - terminal::{self, Clear, ClearType as ClType, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use jargon_args::Jargon; -use kaolinite::event::{Event, Result, Status}; -use kaolinite::utils::{filetype, Loc, Size}; -use kaolinite::Document; -use std::io::{stdout, Stdout, Write}; - -/// Store the version number at compile time -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// Help text for the command line interface -const HELP: &str = "\ -Cactus: A compact and complete kaolinite implementation - -USAGE: cactus [options] [files] - -OPTIONS: - --help, -h : Show this help message - --version, -v : Show the version number - -EXAMPLES: - cactus test.txt - cactus test.txt test2.txt - cactus /home/user/docs/test.txt -"; - -fn main() { - // Reset teriminal in the event of a crash - std::panic::set_hook(Box::new(|e| { - terminal::disable_raw_mode().unwrap(); - execute!(stdout(), LeaveAlternateScreen, Show).unwrap(); - eprintln!("{}", e); - })); - // Run cactus - if let Err(e) = run() { - terminal::disable_raw_mode().unwrap(); - execute!(stdout(), LeaveAlternateScreen, Show).unwrap(); - eprintln!("{}", e); - } -} - -/// This will parse arguments, and run cactus, handling any errors that occur -fn run() -> Result<()> { - let mut args = Jargon::from_env(); - if args.contains(["-h", "--help"]) { - print!("{}", HELP); - } else if args.contains(["-v", "--version"]) { - println!("{}", VERSION); - } else { - let mut cactus = Editor::new()?; - let mut error = false; - // Try to open the requested files - for file in args.finish() { - if let Err(err) = cactus.open(file.clone()) { - // If the file failed to open, make a note of it and display it - println!("Couldn't open file \"{}\": {}", file, err); - error = true; - } - } - // If all files opened without error, run cactus - if !error { - cactus.run()?; - } - } - Ok(()) -} - -/// Gets the size of the terminal -fn size() -> Result { - let (w, h) = terminal::size()?; - Ok(Size { - w: w as usize, - h: (h as usize).saturating_sub(1), - }) -} - -/// For managing all editing and rendering of cactus -pub struct Editor { - /// Interface for writing to the terminal - stdout: Stdout, - /// Storage of all the documents opened in the editor - doc: Vec, - /// Pointer to the document that is currently being edited - ptr: usize, - /// true if the editor is still running, false otherwise - active: bool, -} - -impl Editor { - /// Create a new instance of the editor - pub fn new() -> Result { - Ok(Self { - doc: vec![], - ptr: 0, - stdout: stdout(), - active: true, - }) - } - - /// Function to open a document into the editor - pub fn open(&mut self, file_name: String) -> Result<()> { - let size = size()?; - let mut doc = Document::open(size, file_name)?; - // Load all the lines within viewport into the document - doc.load_to(size.h); - self.doc.push(doc); - Ok(()) - } - - /// Gets a reference to the current document - pub fn doc(&self) -> &Document { - self.doc.get(self.ptr).unwrap() - } - - /// Gets a mutable reference to the current document - pub fn doc_mut(&mut self) -> &mut Document { - self.doc.get_mut(self.ptr).unwrap() - } - - /// Set up the terminal so that it is clean and doesn't effect existing terminal text - pub fn start(&mut self) -> Result<()> { - execute!(self.stdout, EnterAlternateScreen, Clear(ClType::All))?; - terminal::enable_raw_mode()?; - Ok(()) - } - - /// Restore terminal back to state before the editor was started - pub fn end(&mut self) -> Result<()> { - terminal::disable_raw_mode()?; - execute!(self.stdout, LeaveAlternateScreen)?; - Ok(()) - } - - /// Execute an edit event - pub fn exe(&mut self, ev: Event) -> Result<()> { - self.doc_mut().exe(ev) - } - - /// Initialise, render and handle events as they come in - pub fn run(&mut self) -> Result<()> { - // If no documents were provided, just exit - self.active = !self.doc.is_empty(); - self.start()?; - while self.active { - self.render()?; - // Wait for an event - match read()? { - CEvent::Key(key) => match (key.modifiers, key.code) { - // Movement - (KMod::NONE, KCode::Up) => self.up(), - (KMod::NONE, KCode::Down) => self.down(), - (KMod::NONE, KCode::Left) => self.left(), - (KMod::NONE, KCode::Right) => self.right(), - (KMod::CONTROL, KCode::Up) => self.doc_mut().move_top(), - (KMod::CONTROL, KCode::Down) => self.doc_mut().move_bottom(), - (KMod::CONTROL, KCode::Left) => self.prev_word(), - (KMod::CONTROL, KCode::Right) => self.next_word(), - (KMod::NONE, KCode::Home) => self.doc_mut().move_home(), - (KMod::NONE, KCode::End) => self.doc_mut().move_end(), - (KMod::NONE, KCode::PageUp) => self.doc_mut().move_page_up(), - (KMod::NONE, KCode::PageDown) => self.doc_mut().move_page_down(), - // Searching & Replacing - (KMod::CONTROL, KCode::Char('f')) => self.search()?, - (KMod::CONTROL, KCode::Char('r')) => self.replace()?, - // Document management - (KMod::CONTROL, KCode::Char('s')) => self.save(), - (KMod::ALT, KCode::Char('s')) => self.save_as()?, - (KMod::CONTROL, KCode::Char('a')) => self.save_all(), - (KMod::CONTROL, KCode::Char('q')) => self.quit(), - (KMod::SHIFT, KCode::Left) => self.prev(), - (KMod::SHIFT, KCode::Right) => self.next(), - // Undo & Redo - (KMod::CONTROL, KCode::Char('z')) => self.doc_mut().undo()?, - (KMod::CONTROL, KCode::Char('y')) => self.doc_mut().redo()?, - // Editing - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch), - (KMod::NONE, KCode::Tab) => self.character('\t'), - (KMod::NONE, KCode::Backspace) => self.backspace(), - (KMod::NONE, KCode::Enter) => self.enter(), - (KMod::CONTROL, KCode::Char('d')) => self.delete_line(), - _ => (), - }, - CEvent::Resize(w, h) => { - // Ensure all lines in viewport are loaded - let max = self.doc().len_lines().to_string().len() + 2; - self.doc_mut().size.w = w.saturating_sub(max as u16) as usize; - self.doc_mut().size.h = h.saturating_sub(1) as usize; - let max = self.doc().offset.x + self.doc().size.h; - self.doc_mut().load_to(max); - } - _ => (), - } - } - self.end()?; - Ok(()) - } - - /// Render a single frame of the editor in it's current state - pub fn render(&mut self) -> Result<()> { - execute!(self.stdout, Hide)?; - let Size { w, h } = size()?; - // Update the width of the document in case of update - let max = self.doc().len_lines().to_string().len() + 2; - self.doc_mut().size.w = w.saturating_sub(max) as usize; - // Run through each line of the terminal, rendering the correct line - self.render_document(w, h)?; - // Leave last line for status line - self.render_status_line(w, h)?; - // Move cursor to the correct location and perform render - let Loc { x, y } = self.doc().cursor; - execute!(self.stdout, Show, MoveTo((x + max) as u16, y as u16))?; - self.stdout.flush()?; - Ok(()) - } - - /// Render the lines of the document - fn render_document(&mut self, w: usize, h: usize) -> Result<()> { - for y in 0..(h as u16) { - execute!(self.stdout, MoveTo(0, y), Clear(ClType::CurrentLine))?; - // Write line number of document - let num = self.doc().line_number(y as usize + self.doc().offset.y); - write!( - self.stdout, - "{}{} │{}", - Fg(Color::Rgb { r: 150, g: 150, b: 150 }), - num, - Fg(Color::Reset) - )?; - // Render line if it exists - let idx = y as usize + self.doc().offset.y; - if let Some(line) = self.doc().line_trim(idx, self.doc().offset.x, w) { - write!(self.stdout, "{}", line)?; - } - } - Ok(()) - } - - /// Render the status line at the bottom of the document - fn render_status_line(&mut self, w: usize, h: usize) -> Result<()> { - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - let ext = self.doc().file_name.split('.').last().unwrap().to_string(); - // Form left hand side of status bar - let lhs = format!( - "{}{} │ {} │", - self.doc().file_name.split('/').last().unwrap(), - if self.doc().modified { "[+]" } else { "" }, - filetype(&ext).unwrap_or(ext) - ); - // Form right hand side of status bar - let rhs = format!( - "│ {}/{} {} {}", - self.doc().loc().y + 1, - self.doc().len_lines(), - self.doc().char_ptr, - self.doc().loc().x, - ); - // Use alinio to align left and right with padding between - let status_line = alinio::align::between(&[&lhs, &rhs], w.saturating_sub(2)) - .unwrap_or_else(|| "".to_string()); - // Write the status bar - write!( - self.stdout, - "{}{} {} {}{}", - //Bg(Color::Rgb { r: 31, g: 92, b: 62 }), - Fg(Color::Black), - Bg(Color::Rgb { r: 40, g: 209, b: 99 }), - status_line, - Bg(Color::Reset), - Fg(Color::Reset), - )?; - Ok(()) - } - - /// Display a prompt in the document - fn prompt>(&mut self, prompt: S) -> Result { - let prompt = prompt.into(); - let mut input = String::new(); - let mut done = false; - // Enter into a menu that asks for a prompt - while !done { - let h = size()?.h; - // Render prompt message - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - write!(self.stdout, "{}: {}", prompt, input); - self.stdout.flush()?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // Exit the menu when the enter key is pressed - (KMod::NONE, KCode::Enter) => done = true, - // Remove from the input string if the user presses backspace - (KMod::NONE, KCode::Backspace) => { input.pop(); }, - // Add to the input string if the user presses a character - (KMod::NONE | KMod::SHIFT, KCode::Char(c)) => input.push(c), - _ => (), - } - } - } - // Return input string result - Ok(input) - } - - /// Move to the next document opened in the editor - fn next(&mut self) { - if self.ptr + 1 < self.doc.len() { - self.ptr += 1; - } - } - - /// Move to the previous document opened in the editor - fn prev(&mut self) { - if self.ptr != 0 { - self.ptr -= 1; - } - } - - /// Move the cursor up - fn up(&mut self) { - self.doc_mut().move_up(); - } - - /// Move the cursor down - fn down(&mut self) { - self.doc_mut().move_down(); - } - - /// Move the cursor left - fn left(&mut self) { - let status = self.doc_mut().move_left(); - // Cursor wrapping if cursor hits the start of the line - if status == Status::StartOfLine && self.doc().loc().y != 0 { - self.doc_mut().move_up(); - self.doc_mut().move_end(); - } - } - - /// Move the cursor right - fn right(&mut self) { - let status = self.doc_mut().move_right(); - // Cursor wrapping if cursor hits the end of a line - if status == Status::EndOfLine { - self.doc_mut().move_down(); - self.doc_mut().move_home(); - } - } - - /// Move the cursor to the previous word in the line - fn prev_word(&mut self) { - let status = self.doc_mut().move_prev_word(); - if status == Status::StartOfLine { - self.doc_mut().move_up(); - self.doc_mut().move_end(); - } - } - - /// Move the cursor to the next word in the line - fn next_word(&mut self) { - let status = self.doc_mut().move_next_word(); - if status == Status::EndOfLine { - self.doc_mut().move_down(); - self.doc_mut().move_home(); - } - } - - /// Insert a character into the document, creating a new row if editing - /// on the last line of the document - fn character(&mut self, ch: char) { - self.new_row(); - self.exe(Event::Insert(self.doc().char_loc(), ch.to_string())); - } - - /// Handle the return key - fn enter(&mut self) { - if self.doc().loc().y != self.doc().len_lines() { - // Enter pressed in the middle or end of the line - self.exe(Event::SplitDown(self.doc().char_loc())); - } else { - // Enter pressed on the empty line at the bottom of the document - self.new_row(); - } - } - - /// Handle the backspace key - fn backspace(&mut self) { - let mut c = self.doc().char_ptr; - let on_first_line = self.doc().loc().y == 0; - let out_of_range = self.doc().out_of_range(0, self.doc().loc().y).is_err(); - if c == 0 && !on_first_line && !out_of_range { - // Backspace was pressed on the start of the line, move line to the top - self.new_row(); - let mut loc = self.doc().char_loc(); - loc.y -= 1; - loc.x = self.doc().line(loc.y).unwrap().chars().count(); - self.exe(Event::SpliceUp(loc)); - } else { - // Backspace was pressed in the middle of the line, delete the character - c -= 1; - if let Some(line) = self.doc().line(self.doc().loc().y) { - if let Some(ch) = line.chars().nth(c) { - self.exe(Event::Delete(Loc { x: c, y: self.doc().loc().y }, ch.to_string())); - } - } - } - } - - /// Insert a new row at the end of the document if the cursor is on it - fn new_row(&mut self) { - if self.doc().loc().y == self.doc().len_lines() { - self.exe(Event::InsertLine(self.doc().loc().y, "".to_string())); - } - } - - /// Delete the current line - fn delete_line(&mut self) { - if self.doc().loc().y < self.doc().len_lines() { - let line = self.doc().line(self.doc().loc().y).unwrap(); - self.exe(Event::DeleteLine(self.doc().loc().y, line)); - } - } - - /// Use search feature - pub fn search(&mut self) -> Result<()> { - // Prompt for a search term - let target = self.prompt("Search")?; - let mut done = false; - let Size { w, h } = size()?; - // Jump to the next match after search term is provided - self.next_match(&target); - // Enter into search menu - while !done { - // Render just the document part - self.render_document(w, h)?; - // Render custom status line with mode information - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - write!(self.stdout, "[<-]: Search previous | [->]: Search next"); - self.stdout.flush()?; - // Move back to correct cursor position - let Loc { x, y } = self.doc().cursor; - let max = self.doc().len_lines().to_string().len() + 2; - execute!(self.stdout, MoveTo((x + max) as u16, y as u16))?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // On return or escape key, exit menu - (KMod::NONE, KCode::Enter | KCode::Esc) => done = true, - // On left key, move to the previous match in the document - (KMod::NONE, KCode::Left) => std::mem::drop(self.prev_match(&target)), - // On right key, move to the next match in the document - (KMod::NONE, KCode::Right) => std::mem::drop(self.next_match(&target)), - _ => (), - } - } - } - Ok(()) - } - - /// Move to the next match - fn next_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().next_match(target, 1)?; - self.doc_mut().goto(&mtch.loc); - Some(mtch.text) - } - - /// Move to the previous match - fn prev_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().prev_match(target)?; - self.doc_mut().goto(&mtch.loc); - Some(mtch.text) - } - - /// Use replace feature - pub fn replace(&mut self) -> Result<()> { - // Request replace information - let target = self.prompt("Replace")?; - let into = self.prompt("With")?; - let mut done = false; - let Size { w, h } = size()?; - // Jump to match - let mut mtch; - if let Some(m) = self.next_match(&target) { - // Automatically move to next match, keeping note of what that match is - mtch = m; - } else if let Some(m) = self.prev_match(&target) { - // Automatically move to previous match, keeping not of what that match is - // This happens if there are no matches further down the document, only above - mtch = m; - } else { - // Exit if there are no matches in the document - return Ok(()); - } - // Enter into the replace menu - while !done { - // Render just the document part - self.render_document(w, h)?; - // Write custom status line for the replace mode - execute!(self.stdout, MoveTo(0, h as u16), Clear(ClType::CurrentLine))?; - write!(self.stdout, "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All"); - self.stdout.flush()?; - // Move back to correct cursor location - let Loc { x, y } = self.doc().cursor; - let max = self.doc().len_lines().to_string().len() + 2; - execute!(self.stdout, MoveTo((x + max) as u16, y as u16))?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // On escape key, exit - (KMod::NONE, KCode::Esc) => done = true, - // On right key, move to the previous match, keeping note of what that match is - (KMod::NONE, KCode::Left) => mtch = self.prev_match(&target).unwrap_or(mtch), - // On left key, move to the next match, keeping note of what that match is - (KMod::NONE, KCode::Right) => mtch = self.next_match(&target).unwrap_or(mtch), - // On return key, perform replacement - (KMod::NONE, KCode::Enter) => self.do_replace(&into, &mtch), - // On tab key, replace all instances within the document - (KMod::NONE, KCode::Tab) => self.do_replace_all(&target, &into), - _ => (), - } - } - } - Ok(()) - } - - /// Replace an instance in a document - fn do_replace(&mut self, into: &str, text: &str) { - let loc = self.doc().char_loc(); - self.doc_mut().replace(loc, text, into); - self.doc_mut().goto(&loc); - } - - /// Replace all instances in a document - fn do_replace_all(&mut self, target: &str, into: &str) { - self.doc_mut().replace_all(target, into); - } - - /// save the document to the disk - pub fn save(&mut self) { - self.doc_mut().save(); - } - - /// save the document to the disk at a specified path - pub fn save_as(&mut self) -> Result<()> { - let file_name = self.prompt("Save as")?; - self.doc_mut().save_as(&file_name)?; - Ok(()) - } - - /// Save all the open documents to the disk - pub fn save_all(&mut self) { - for doc in self.doc.iter_mut() { - doc.save(); - } - } - - /// Quit the editor - pub fn quit(&mut self) { - self.active = !self.doc.is_empty(); - // If there are still documents open, only close the requested document - if self.active { - self.doc.remove(self.ptr); - self.prev(); - } - self.active = !self.doc.is_empty(); - } -} diff --git a/kaolinite/examples/cactus/test.txt b/kaolinite/examples/cactus/test.txt deleted file mode 100644 index e915abd2..00000000 --- a/kaolinite/examples/cactus/test.txt +++ /dev/null @@ -1 +0,0 @@ - line diff --git a/kaolinite/examples/debug.rs b/kaolinite/examples/debug.rs deleted file mode 100644 index e2ad458a..00000000 --- a/kaolinite/examples/debug.rs +++ /dev/null @@ -1,13 +0,0 @@ -use kaolinite::{event::Event, Document, Size}; - -fn main() { - // Open document with the size of 10 characters by 10 characters - let mut doc = - Document::open(Size::is(100, 100), "cactus/fresh.txt").expect("File couldn't be opened"); - // Load viewport - doc.load_to(100); - println!("{:?}", doc); - doc.exe(Event::InsertLine(doc.loc().y, "".to_string())) - .unwrap(); - println!("{:?}", doc); -} diff --git a/kaolinite/examples/open.rs b/kaolinite/examples/open.rs deleted file mode 100644 index 8230bf85..00000000 --- a/kaolinite/examples/open.rs +++ /dev/null @@ -1,22 +0,0 @@ -use kaolinite::{Document, Size}; -use std::time::Instant; - -fn main() { - let start = Instant::now(); - // Open document with the size of 10 characters by 10 characters - let mut doc = Document::open(Size::is(10, 10), "demos/7.txt").expect("File couldn't be opened"); - // Load viewport - doc.load_to(10); - // Display the first 100 characters from the first 10 lines - let mut len = 0; - for i in 0..10 { - println!( - "{}", - doc.line(i).unwrap().chars().take(100).collect::() - ); - len += doc.line(i).unwrap().chars().count(); - } - println!("{}", len); - let end = Instant::now(); - println!("ran in {:?}", end - start); -} diff --git a/kaolinite/examples/searching.rs b/kaolinite/examples/searching.rs deleted file mode 100644 index 6565e35b..00000000 --- a/kaolinite/examples/searching.rs +++ /dev/null @@ -1,11 +0,0 @@ -use kaolinite::{Document, Size}; - -fn main() { - // Open document with the size of 10 characters by 10 characters - let mut doc = Document::open(Size::is(10, 10), "demos/7.txt").expect("File couldn't be opened"); - // Load viewport - doc.load_to(1); - // Find something out of buffer - let m = doc.next_match("spine", 0); - println!("{m:?}"); -} diff --git a/kaolinite/examples/tabs.rs b/kaolinite/examples/tabs.rs deleted file mode 100644 index 2125e845..00000000 --- a/kaolinite/examples/tabs.rs +++ /dev/null @@ -1,17 +0,0 @@ -use kaolinite::{Document, Loc, Size}; - -fn main() { - // Open document with the size of 10 characters by 10 characters - let mut doc = - Document::open(Size::is(10, 10), "demos/10.txt").expect("File couldn't be opened"); - // Load viewport - doc.load_to(10); - doc.move_to(&Loc { x: 0, y: 0 }); - // Find something out of buffer - println!("{:?}", doc.line(0)); - println!("{:?}", doc.line(1)); - println!(); - println!("{:?}", doc.loc()); - doc.move_right(); - println!("{:?}", doc.loc()); -} diff --git a/kaolinite/examples/trim.rs b/kaolinite/examples/trim.rs deleted file mode 100644 index 31e7c35a..00000000 --- a/kaolinite/examples/trim.rs +++ /dev/null @@ -1,10 +0,0 @@ -use kaolinite::utils; - -fn main() { - let string = "\tblack你milk好attack好".to_string(); - for i in 0..26 { - // With the string, cut it so that it starts at i and is a length of 5 - // it will render tabs as 4 spaces - println!("{:?}", utils::trim(&string, i, 5, 4)); - } -} From a2991e97ff33cd878aa2f7e76b2e353731d85f41 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:32:12 +0000 Subject: [PATCH 13/73] Split up document.rs --- kaolinite/src/document.rs | 1460 ----------------------------- kaolinite/src/document/cursor.rs | 403 ++++++++ kaolinite/src/document/disk.rs | 151 +++ kaolinite/src/document/editing.rs | 190 ++++ kaolinite/src/document/lines.rs | 73 ++ kaolinite/src/document/mod.rs | 385 ++++++++ kaolinite/src/document/words.rs | 298 ++++++ 7 files changed, 1500 insertions(+), 1460 deletions(-) delete mode 100644 kaolinite/src/document.rs create mode 100644 kaolinite/src/document/cursor.rs create mode 100644 kaolinite/src/document/disk.rs create mode 100644 kaolinite/src/document/editing.rs create mode 100644 kaolinite/src/document/lines.rs create mode 100644 kaolinite/src/document/mod.rs create mode 100644 kaolinite/src/document/words.rs diff --git a/kaolinite/src/document.rs b/kaolinite/src/document.rs deleted file mode 100644 index 57ef862a..00000000 --- a/kaolinite/src/document.rs +++ /dev/null @@ -1,1460 +0,0 @@ -/// document.rs - has Document, for opening, editing and saving documents -use crate::event::{Error, Event, Result, Status, UndoMgmt}; -use crate::map::{form_map, CharMap}; -use crate::searching::{Match, Searcher}; -use crate::utils::{ - get_absolute_path, get_range, modeline, tab_boundaries_backward, tab_boundaries_forward, trim, - width, Loc, Size, -}; -use ropey::Rope; -use std::fs::File; -use std::io::{BufReader, BufWriter}; -use std::ops::{Range, RangeBounds}; -use std::path::Path; - -/// A document info struct to store information about the file it represents -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct DocumentInfo { - /// Whether or not the document can be edited - pub read_only: bool, - /// Flag for an EOL - pub eol: bool, - /// true if the file has been modified since saving, false otherwise - pub modified: bool, - /// Contains the number of lines buffered into the document - pub loaded_to: usize, -} - -/// A document struct manages a file. -/// It has tools to read, write and traverse a document. -/// By default, it uses file buffering so it can open almost immediately. -/// To start executing events, remember to use the `Document::exe` function and check out -/// the documentation for `Event` to learn how to form editing events. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Document { - /// The file name of the document opened - pub file_name: Option, - /// The rope of the document to facilitate reading and writing to disk - pub file: Rope, - /// Cache of all the loaded lines in this document - pub lines: Vec, - /// Stores information about the underlying file - pub info: DocumentInfo, - /// Stores the locations of double width characters - pub dbl_map: CharMap, - /// Stores the locations of tab characters - pub tab_map: CharMap, - /// Contains the size of this document for purposes of offset - pub size: Size, - /// Contains the cursor data structure - pub cursor: Cursor, - /// Contains the offset (scrolling for longer documents) - pub offset: Loc, - /// Keeps track of where the character pointer is - pub char_ptr: usize, - /// Manages events, for the purpose of undo and redo - pub undo_mgmt: UndoMgmt, - /// Storage of the old cursor x position (to snap back to) - pub old_cursor: usize, - /// Flag for if the editor is currently in a redo action - pub in_redo: bool, - /// The number of spaces a tab should be rendered as - pub tab_width: usize, -} - -impl Document { - /// Creates a new, empty document with no file name. - #[cfg(not(tarpaulin_include))] - #[must_use] - pub fn new(size: Size) -> Self { - let mut this = Self { - file: Rope::from_str("\n"), - lines: vec![String::new()], - dbl_map: CharMap::default(), - tab_map: CharMap::default(), - file_name: None, - cursor: Cursor::default(), - offset: Loc::default(), - size, - char_ptr: 0, - undo_mgmt: UndoMgmt::default(), - tab_width: 4, - old_cursor: 0, - in_redo: false, - info: DocumentInfo { - loaded_to: 1, - eol: false, - read_only: false, - modified: false, - }, - }; - this.undo_mgmt.undo.push(this.take_snapshot()); - this.undo_mgmt.saved(); - this - } - - /// Open a document from a file name. - /// # Errors - /// Returns an error when file doesn't exist, or has incorrect permissions. - /// Also returns an error if the rope fails to initialise due to character set issues or - /// disk errors. - #[cfg(not(tarpaulin_include))] - pub fn open>(size: Size, file_name: S) -> Result { - let file_name = file_name.into(); - let file = Rope::from_reader(BufReader::new(File::open(&file_name)?))?; - let file_name = get_absolute_path(&file_name); - let mut this = Self { - info: DocumentInfo { - loaded_to: 0, - eol: !file - .line(file.len_lines().saturating_sub(1)) - .to_string() - .is_empty(), - read_only: false, - modified: false, - }, - file, - lines: vec![], - dbl_map: CharMap::default(), - tab_map: CharMap::default(), - file_name, - cursor: Cursor::default(), - offset: Loc::default(), - size, - char_ptr: 0, - undo_mgmt: UndoMgmt::default(), - tab_width: 4, - old_cursor: 0, - in_redo: false, - }; - this.undo_mgmt.undo.push(this.take_snapshot()); - this.undo_mgmt.saved(); - Ok(this) - } - - /// Determine the file type of this file (represented by an extension) - #[allow(clippy::missing_panics_doc)] - #[must_use] - pub fn get_file_type(&self) -> Option<&str> { - let mut result = None; - // Try to use modeline first off - if let Some(first_line) = self.lines.first() { - result = modeline(first_line); - } - // If an extension is available, use that instead - if let Some(file_name) = &self.file_name { - if let Some(extension) = Path::new(file_name).extension() { - result = extension.to_str(); - } - } - result - } - - /// Sets the tab display width measured in spaces, default being 4 - pub fn set_tab_width(&mut self, tab_width: usize) { - self.tab_width = tab_width; - } - - /// Save back to the file the document was opened from. - /// # Errors - /// Returns an error if the file fails to write, due to permissions - /// or character set issues. - pub fn save(&mut self) -> Result<()> { - if self.info.read_only { - Err(Error::ReadOnlyFile) - } else if let Some(file_name) = &self.file_name { - self.file - .write_to(BufWriter::new(File::create(file_name)?))?; - self.undo_mgmt.saved(); - self.info.modified = false; - Ok(()) - } else { - Err(Error::NoFileName) - } - } - - /// Save to a specified file. - /// # Errors - /// Returns an error if the file fails to write, due to permissions - /// or character set issues. - pub fn save_as(&self, file_name: &str) -> Result<()> { - if self.info.read_only { - Err(Error::ReadOnlyFile) - } else { - self.file - .write_to(BufWriter::new(File::create(file_name)?))?; - Ok(()) - } - } - - /// Execute an event, registering it in the undo / redo. - /// You should always edit a document through this method to ensure undo and redo work. - /// # Errors - /// Will return an error if the event was unable to be completed. - pub fn exe(&mut self, ev: Event) -> Result<()> { - if !self.info.read_only { - self.undo_mgmt.last_event = ev.clone(); - self.undo_mgmt.set_dirty(); - self.forth(ev)?; - } - self.cancel_selection(); - Ok(()) - } - - /// Undo the last patch in the document. - /// # Errors - /// Will return an error if any of the events failed to be reversed. - pub fn undo(&mut self) -> Result<()> { - if let Some(s) = self.undo_mgmt.undo(self.take_snapshot()) { - self.apply_snapshot(s); - self.info.modified = true; - } - if self.undo_mgmt.at_file() { - self.info.modified = false; - } - Ok(()) - } - - /// Redo the last patch in the document. - /// # Errors - /// Will return an error if any of the events failed to be re-executed. - pub fn redo(&mut self) -> Result<()> { - if let Some(s) = self.undo_mgmt.redo() { - self.apply_snapshot(s); - self.info.modified = true; - } - if self.undo_mgmt.at_file() { - self.info.modified = false; - } - Ok(()) - } - - /// Handle an editing event, use the method `exe` for executing events. - /// # Errors - /// Returns an error if there is a problem with the specified operation. - pub fn forth(&mut self, ev: Event) -> Result<()> { - match ev { - Event::Insert(loc, ch) => self.insert(&loc, &ch), - Event::Delete(loc, st) => self.delete_with_tab(&loc, &st), - Event::InsertLine(loc, st) => self.insert_line(loc, st), - Event::DeleteLine(loc, _) => self.delete_line(loc), - Event::SplitDown(loc) => self.split_down(&loc), - Event::SpliceUp(loc) => self.splice_up(loc.y), - } - } - - /// Takes a loc and converts it into a char index for ropey - #[must_use] - pub fn loc_to_file_pos(&self, loc: &Loc) -> usize { - self.file.line_to_char(loc.y) + loc.x - } - - /// Inserts a string into this document. - /// # Errors - /// Returns an error if location is out of range. - pub fn insert(&mut self, loc: &Loc, st: &str) -> Result<()> { - self.out_of_range(loc.x, loc.y)?; - self.info.modified = true; - // Move cursor to location - self.move_to(loc); - // Update rope - let idx = self.loc_to_file_pos(loc); - self.file.insert(idx, st); - // Update cache - let line: String = self.file.line(loc.y).chars().collect(); - self.lines[loc.y] = line.trim_end_matches(['\n', '\r']).to_string(); - // Update unicode map - let dbl_start = self.dbl_map.shift_insertion(loc, st, self.tab_width); - let tab_start = self.tab_map.shift_insertion(loc, st, self.tab_width); - // Register new double widths and tabs - let (mut dbls, mut tabs) = form_map(st, self.tab_width); - // Shift up to match insertion position in the document - let tab_shift = self.tab_width.saturating_sub(1) * tab_start; - for e in &mut dbls { - *e = (e.0 + loc.x + dbl_start + tab_shift, e.1 + loc.x); - } - for e in &mut tabs { - *e = (e.0 + loc.x + tab_shift + dbl_start, e.1 + loc.x); - } - self.dbl_map.splice(loc, dbl_start, dbls); - self.tab_map.splice(loc, tab_start, tabs); - // Go to end x position - self.move_to_x(loc.x + st.chars().count()); - self.old_cursor = self.loc().x; - Ok(()) - } - - /// Deletes a character at a location whilst checking for tab spaces - /// - /// # Errors - /// This code will error if the location is invalid - pub fn delete_with_tab(&mut self, loc: &Loc, st: &str) -> Result<()> { - // Check for tab spaces - let boundaries = - tab_boundaries_backward(&self.line(loc.y).unwrap_or_default(), self.tab_width); - if boundaries.contains(&loc.x.saturating_add(1)) && !self.in_redo { - // Register other delete actions to delete the whole tab - let mut loc_copy = *loc; - self.delete(loc.x..=loc.x + st.chars().count(), loc.y)?; - for _ in 1..self.tab_width { - loc_copy.x = loc_copy.x.saturating_sub(1); - self.exe(Event::Delete(loc_copy, " ".to_string()))?; - } - Ok(()) - } else { - // Normal character delete - self.delete(loc.x..=loc.x + st.chars().count(), loc.y) - } - } - - /// Deletes a range from this document. - /// # Errors - /// Returns an error if location is out of range. - pub fn delete(&mut self, x: R, y: usize) -> Result<()> - where - R: RangeBounds, - { - let line_start = self.file.try_line_to_char(y)?; - let line_end = line_start + self.line(y).ok_or(Error::OutOfRange)?.chars().count(); - // Extract range information - let (mut start, mut end) = get_range(&x, line_start, line_end); - self.valid_range(start, end, y)?; - self.info.modified = true; - self.move_to(&Loc::at(start, y)); - start += line_start; - end += line_start; - let removed = self.file.slice(start..end).to_string(); - // Update unicode and tab map - self.dbl_map.shift_deletion( - &Loc::at(line_start, y), - (start, end), - &removed, - self.tab_width, - ); - self.tab_map.shift_deletion( - &Loc::at(line_start, y), - (start, end), - &removed, - self.tab_width, - ); - // Update rope - self.file.remove(start..end); - // Update cache - let line: String = self.file.line(y).chars().collect(); - self.lines[y] = line.trim_end_matches(['\n', '\r']).to_string(); - self.old_cursor = self.loc().x; - Ok(()) - } - - /// Inserts a line into the document. - /// # Errors - /// Returns an error if location is out of range. - pub fn insert_line(&mut self, loc: usize, contents: String) -> Result<()> { - if !(self.lines.is_empty() || self.len_lines() == 0 && loc == 0) { - self.out_of_range(0, loc.saturating_sub(1))?; - } - self.info.modified = true; - // Update unicode and tab map - self.dbl_map.shift_down(loc); - self.tab_map.shift_down(loc); - // Calculate the unicode map and tab map of this line - let (dbl_map, tab_map) = form_map(&contents, self.tab_width); - self.dbl_map.insert(loc, dbl_map); - self.tab_map.insert(loc, tab_map); - // Update cache - self.lines.insert(loc, contents.to_string()); - // Update rope - let char_idx = self.file.line_to_char(loc); - self.file.insert(char_idx, &(contents + "\n")); - self.info.loaded_to += 1; - // Goto line - self.move_to_y(loc); - self.old_cursor = self.loc().x; - Ok(()) - } - - /// Deletes a line from the document. - /// # Errors - /// Returns an error if location is out of range. - pub fn delete_line(&mut self, loc: usize) -> Result<()> { - self.out_of_range(0, loc)?; - // Update tab & unicode map - self.dbl_map.delete(loc); - self.tab_map.delete(loc); - self.info.modified = true; - // Shift down other line numbers in the hashmap - self.dbl_map.shift_up(loc); - self.tab_map.shift_up(loc); - // Update cache - self.lines.remove(loc); - // Update rope - let idx_start = self.file.line_to_char(loc); - let idx_end = self.file.line_to_char(loc + 1); - self.file.remove(idx_start..idx_end); - self.info.loaded_to = self.info.loaded_to.saturating_sub(1); - // Goto line - self.move_to_y(loc); - self.old_cursor = self.loc().x; - Ok(()) - } - - /// Split a line in half, putting the right hand side below on a new line. - /// For when the return key is pressed. - /// # Errors - /// Returns an error if location is out of range. - pub fn split_down(&mut self, loc: &Loc) -> Result<()> { - self.out_of_range(loc.x, loc.y)?; - self.info.modified = true; - // Gather context - let line = self.line(loc.y).ok_or(Error::OutOfRange)?; - let rhs: String = line.chars().skip(loc.x).collect(); - self.delete(loc.x.., loc.y)?; - self.insert_line(loc.y + 1, rhs)?; - self.move_to(&Loc::at(0, loc.y + 1)); - self.old_cursor = self.loc().x; - Ok(()) - } - - /// Remove the line below the specified location and append that to it. - /// For when backspace is pressed on the start of a line. - /// # Errors - /// Returns an error if location is out of range. - pub fn splice_up(&mut self, y: usize) -> Result<()> { - self.out_of_range(0, y + 1)?; - self.info.modified = true; - // Gather context - let length = self.line(y).ok_or(Error::OutOfRange)?.chars().count(); - let below = self.line(y + 1).ok_or(Error::OutOfRange)?; - self.delete_line(y + 1)?; - self.insert(&Loc::at(length, y), &below)?; - self.move_to(&Loc::at(length, y)); - self.old_cursor = self.loc().x; - Ok(()) - } - - /// Swap a line upwards - /// # Errors - /// When out of bounds - pub fn swap_line_up(&mut self) -> Result<()> { - let cursor = self.char_loc(); - let line = self.line(cursor.y).ok_or(Error::OutOfRange)?; - self.insert_line(cursor.y.saturating_sub(1), line)?; - self.delete_line(cursor.y + 1)?; - self.move_to(&Loc { - x: cursor.x, - y: cursor.y.saturating_sub(1), - }); - Ok(()) - } - - /// Swap a line downwards - /// # Errors - /// When out of bounds - pub fn swap_line_down(&mut self) -> Result<()> { - let cursor = self.char_loc(); - let line = self.line(cursor.y).ok_or(Error::OutOfRange)?; - self.insert_line(cursor.y + 2, line)?; - self.delete_line(cursor.y)?; - self.move_to(&Loc { - x: cursor.x, - y: cursor.y + 1, - }); - Ok(()) - } - - /// Cancels the current selection - pub fn cancel_selection(&mut self) { - self.cursor.selection_end = self.cursor.loc; - } - - /// Move the view down - pub fn scroll_down(&mut self) { - self.offset.y += 1; - self.load_to(self.offset.y + self.size.h); - } - - /// Move the view up - pub fn scroll_up(&mut self) { - self.offset.y = self.offset.y.saturating_sub(1); - self.load_to(self.offset.y + self.size.h); - } - - /// Move the cursor up - pub fn move_up(&mut self) -> Status { - let r = self.select_up(); - self.cancel_selection(); - r - } - - /// Select with the cursor up - pub fn select_up(&mut self) -> Status { - // Return if already at start of document - if self.loc().y == 0 { - return Status::StartOfFile; - } - self.cursor.loc.y = self.cursor.loc.y.saturating_sub(1); - self.cursor.loc.x = self.old_cursor; - // Snap to end of line - self.fix_dangling_cursor(); - // Move back if in the middle of a longer character - self.fix_split(); - // Update the character pointer - self.update_char_ptr(); - self.bring_cursor_in_viewport(); - Status::None - } - - /// Move the cursor down - pub fn move_down(&mut self) -> Status { - let r = self.select_down(); - self.cancel_selection(); - r - } - - /// Select with the cursor down - pub fn select_down(&mut self) -> Status { - // Return if already on end of document - if self.len_lines() < self.loc().y + 1 { - return Status::EndOfFile; - } - self.cursor.loc.y += 1; - self.cursor.loc.x = self.old_cursor; - // Snap to end of line - self.fix_dangling_cursor(); - // Move back if in the middle of a longer character - self.fix_split(); - // Update the character pointer - self.update_char_ptr(); - self.bring_cursor_in_viewport(); - Status::None - } - - /// Move the cursor left - pub fn move_left(&mut self) -> Status { - let r = self.select_left(); - self.cancel_selection(); - r - } - - /// Select with the cursor left - pub fn select_left(&mut self) -> Status { - // Return if already at start of line - if self.loc().x == 0 { - return Status::StartOfLine; - } - // Determine the width of the character to traverse - let line = self.line(self.loc().y).unwrap_or_default(); - let boundaries = tab_boundaries_backward(&line, self.tab_width); - let width = if boundaries.contains(&self.char_ptr) { - // Push the character pointer up - self.char_ptr = self - .char_ptr - .saturating_sub(self.tab_width.saturating_sub(1)); - // There are spaces that should be treated as tabs (so should traverse the tab width) - self.tab_width - } else { - // There are no spaces that should be treated as tabs - self.width_of(self.loc().y, self.char_ptr.saturating_sub(1)) - }; - // Move back the correct amount - self.cursor.loc.x = self.cursor.loc.x.saturating_sub(width); - // Update the character pointer - self.char_ptr = self.char_ptr.saturating_sub(1); - self.bring_cursor_in_viewport(); - self.old_cursor = self.loc().x; - Status::None - } - - /// Move the cursor right - pub fn move_right(&mut self) -> Status { - let r = self.select_right(); - self.cancel_selection(); - r - } - - /// Select with the cursor right - pub fn select_right(&mut self) -> Status { - // Return if already on end of line - let line = self.line(self.loc().y).unwrap_or_default(); - let width = width(&line, self.tab_width); - if width == self.loc().x { - return Status::EndOfLine; - } - // Determine the width of the character to traverse - let boundaries = tab_boundaries_forward(&line, self.tab_width); - let width = if boundaries.contains(&self.char_ptr) { - // Push the character pointer up - self.char_ptr += self.tab_width.saturating_sub(1); - // There are spaces that should be treated as tabs (so should traverse the tab width) - self.tab_width - } else { - // There are no spaces that should be treated as tabs - self.width_of(self.loc().y, self.char_ptr) - }; - // Move forward the correct amount - self.cursor.loc.x += width; - // Update the character pointer - self.char_ptr += 1; - self.bring_cursor_in_viewport(); - self.old_cursor = self.loc().x; - Status::None - } - - /// Move to the start of the line - pub fn move_home(&mut self) { - self.select_home(); - self.cancel_selection(); - } - - /// Select to the start of the line - pub fn select_home(&mut self) { - self.cursor.loc.x = 0; - self.char_ptr = 0; - self.old_cursor = 0; - self.bring_cursor_in_viewport(); - } - - /// Move to the end of the line - pub fn move_end(&mut self) { - self.select_end(); - self.cancel_selection(); - } - - /// Select to the end of the line - pub fn select_end(&mut self) { - let line = self.line(self.loc().y).unwrap_or_default(); - let length = line.chars().count(); - self.select_to_x(length); - self.old_cursor = self.loc().x; - } - - /// Move to the top of the document - pub fn move_top(&mut self) { - self.move_to(&Loc::at(0, 0)); - } - - /// Move to the bottom of the document - pub fn move_bottom(&mut self) { - let last = self.len_lines(); - self.move_to(&Loc::at(0, last)); - } - - /// Select to the top of the document - pub fn select_top(&mut self) { - self.select_to(&Loc::at(0, 0)); - self.old_cursor = self.loc().x; - } - - /// Select to the bottom of the document - pub fn select_bottom(&mut self) { - let last = self.len_lines(); - self.select_to(&Loc::at(0, last)); - self.old_cursor = self.loc().x; - } - - /// Move up by 1 page - pub fn move_page_up(&mut self) { - // Set x to 0 - self.cursor.loc.x = 0; - self.char_ptr = 0; - self.old_cursor = 0; - // Calculate where to move the cursor - let new_cursor_y = self.cursor.loc.y.saturating_sub(self.size.h); - // Move to the new location and shift down offset proportionally - self.cursor.loc.y = new_cursor_y; - self.offset.y = self.offset.y.saturating_sub(self.size.h); - // Clean up - self.cancel_selection(); - } - - /// Move down by 1 page - pub fn move_page_down(&mut self) { - // Set x to 0 - self.cursor.loc.x = 0; - self.char_ptr = 0; - self.old_cursor = 0; - // Calculate where to move the cursor - let new_cursor_y = self.cursor.loc.y + self.size.h; - if new_cursor_y <= self.len_lines() { - // Cursor is in range, move to the new location and shift down offset proportionally - self.cursor.loc.y = new_cursor_y; - self.offset.y += self.size.h; - } else if self.len_lines() < self.offset.y + self.size.h { - // End line is in view, no need to move offset - self.cursor.loc.y = self.len_lines().saturating_sub(1); - } else { - // Cursor would be out of range (adjust to bottom of document) - self.cursor.loc.y = self.len_lines().saturating_sub(1); - self.offset.y = self.len_lines().saturating_sub(self.size.h); - } - // Clean up - self.load_to(self.offset.y + self.size.h); - self.cancel_selection(); - } - - /// Find the word boundaries - #[must_use] - pub fn word_boundaries(&self, line: &str) -> Vec<(usize, usize)> { - let re = r"(\s{2,}|[A-Za-z0-9_]+|\.)"; - let mut searcher = Searcher::new(re); - let starts: Vec = searcher.lfinds(line); - let mut ends: Vec = starts.clone(); - ends.iter_mut() - .for_each(|m| m.loc.x += m.text.chars().count()); - let starts: Vec = starts.iter().map(|m| m.loc.x).collect(); - let ends: Vec = ends.iter().map(|m| m.loc.x).collect(); - starts.into_iter().zip(ends).collect() - } - - /// Find the current state of the cursor in relation to words - #[must_use] - pub fn cursor_word_state(&self, words: &[(usize, usize)], x: usize) -> WordState { - let in_word = words - .iter() - .position(|(start, end)| *start <= x && x <= *end); - if let Some(idx) = in_word { - let (word_start, word_end) = words[idx]; - if x == word_end { - WordState::AtEnd(idx) - } else if x == word_start { - WordState::AtStart(idx) - } else { - WordState::InCenter(idx) - } - } else { - WordState::Out - } - } - - /// Find the index of the next word - #[must_use] - pub fn prev_word_close(&self, from: Loc) -> usize { - let Loc { x, y } = from; - let line = self.line(y).unwrap_or_default(); - let words = self.word_boundaries(&line); - let state = self.cursor_word_state(&words, x); - match state { - // Go to start of line if at beginning - WordState::AtEnd(0) | WordState::InCenter(0) | WordState::AtStart(0) => 0, - // Cursor is at the middle / end of a word, move to previous end - WordState::AtEnd(idx) | WordState::InCenter(idx) => words[idx.saturating_sub(1)].1, - WordState::AtStart(idx) => words[idx.saturating_sub(1)].0, - WordState::Out => { - // Cursor is not touching any words, find previous end - let mut shift_back = x; - while let WordState::Out = self.cursor_word_state(&words, shift_back) { - shift_back = shift_back.saturating_sub(1); - if shift_back == 0 { - break; - } - } - match self.cursor_word_state(&words, shift_back) { - WordState::AtEnd(idx) => words[idx].0, - _ => 0, - } - } - } - } - - /// Find the index of the next word - #[must_use] - pub fn prev_word_index(&self, from: Loc) -> usize { - let Loc { x, y } = from; - let line = self.line(y).unwrap_or_default(); - let words = self.word_boundaries(&line); - let state = self.cursor_word_state(&words, x); - match state { - // Go to start of line if at beginning - WordState::AtEnd(0) | WordState::InCenter(0) | WordState::AtStart(0) => 0, - // Cursor is at the middle / end of a word, move to previous end - WordState::AtEnd(idx) | WordState::InCenter(idx) => words[idx.saturating_sub(1)].1, - WordState::AtStart(idx) => words[idx.saturating_sub(1)].0, - WordState::Out => { - // Cursor is not touching any words, find previous end - let mut shift_back = x; - while let WordState::Out = self.cursor_word_state(&words, shift_back) { - shift_back = shift_back.saturating_sub(1); - if shift_back == 0 { - break; - } - } - match self.cursor_word_state(&words, shift_back) { - WordState::AtEnd(idx) => words[idx].1, - _ => 0, - } - } - } - } - - /// Moves to the previous word in the document - pub fn move_prev_word(&mut self) -> Status { - let Loc { x, y } = self.char_loc(); - // Handle case where we're at the beginning of the line - if x == 0 && y != 0 { - return Status::StartOfLine; - } - // Work out where to move to - let new_x = self.prev_word_index(self.char_loc()); - // Perform the move - self.move_to_x(new_x); - // Clean up - self.old_cursor = self.loc().x; - Status::None - } - - /// Find the index of the next word - #[must_use] - pub fn next_word_close(&self, from: Loc) -> usize { - let Loc { x, y } = from; - let line = self.line(y).unwrap_or_default(); - let words = self.word_boundaries(&line); - let state = self.cursor_word_state(&words, x); - match state { - // Cursor is at the middle / end of a word, move to next end - WordState::AtEnd(idx) | WordState::InCenter(idx) => { - if let Some(word) = words.get(idx) { - word.1 - } else { - // No next word exists, just go to end of line - line.chars().count() - } - } - WordState::AtStart(idx) => { - // Cursor is at the start of a word, move to next start - if let Some(word) = words.get(idx) { - word.0 - } else { - // No next word exists, just go to end of line - line.chars().count() - } - } - WordState::Out => { - // Cursor is not touching any words, find next start - let mut shift_forward = x; - while let WordState::Out = self.cursor_word_state(&words, shift_forward) { - shift_forward += 1; - if shift_forward >= line.chars().count() { - break; - } - } - match self.cursor_word_state(&words, shift_forward) { - WordState::AtStart(idx) => words[idx].0, - _ => line.chars().count(), - } - } - } - } - - /// Find the index of the next word - #[must_use] - pub fn next_word_index(&self, from: Loc) -> usize { - let Loc { x, y } = from; - let line = self.line(y).unwrap_or_default(); - let words = self.word_boundaries(&line); - let state = self.cursor_word_state(&words, x); - match state { - // Cursor is at the middle / end of a word, move to next end - WordState::AtEnd(idx) | WordState::InCenter(idx) => { - if let Some(word) = words.get(idx + 1) { - word.1 - } else { - // No next word exists, just go to end of line - line.chars().count() - } - } - WordState::AtStart(idx) => { - // Cursor is at the start of a word, move to next start - if let Some(word) = words.get(idx + 1) { - word.0 - } else { - // No next word exists, just go to end of line - line.chars().count() - } - } - WordState::Out => { - // Cursor is not touching any words, find next start - let mut shift_forward = x; - while let WordState::Out = self.cursor_word_state(&words, shift_forward) { - shift_forward += 1; - if shift_forward >= line.chars().count() { - break; - } - } - match self.cursor_word_state(&words, shift_forward) { - WordState::AtStart(idx) => words[idx].0, - _ => line.chars().count(), - } - } - } - } - - /// Moves to the next word in the document - pub fn move_next_word(&mut self) -> Status { - let Loc { x, y } = self.char_loc(); - let line = self.line(y).unwrap_or_default(); - // Handle case where we're at the end of the line - if x == line.chars().count() && y != self.len_lines() { - return Status::EndOfLine; - } - // Work out where to move to - let new_x = self.next_word_index(self.char_loc()); - // Perform the move - self.move_to_x(new_x); - // Clean up - self.old_cursor = self.loc().x; - Status::None - } - - /// Function to delete a word at a certain location - /// # Errors - /// Errors if out of range - pub fn delete_word(&mut self) -> Result<()> { - let Loc { x, y } = self.char_loc(); - let line = self.line(y).unwrap_or_default(); - let words = self.word_boundaries(&line); - let state = self.cursor_word_state(&words, x); - let delete_upto = match state { - WordState::InCenter(idx) | WordState::AtEnd(idx) => { - // Delete back to start of this word - words[idx].0 - } - WordState::AtStart(0) => 0, - WordState::AtStart(idx) => { - // Delete back to start of the previous word - words[idx.saturating_sub(1)].0 - } - WordState::Out => { - // Delete back to the end of the previous word - let mut shift_back = x; - while let WordState::Out = self.cursor_word_state(&words, shift_back) { - shift_back = shift_back.saturating_sub(1); - if shift_back == 0 { - break; - } - } - let char = line.chars().nth(shift_back); - let state = self.cursor_word_state(&words, shift_back); - match (char, state) { - // Shift to start of previous word if there is a space - (Some(' '), WordState::AtEnd(idx)) => words[idx].0, - // Shift to end of previous word if there is not a space - (_, WordState::AtEnd(idx)) => words[idx].1, - _ => 0, - } - } - }; - self.delete(delete_upto..=x, y) - } - - /// Function to search the document to find the next occurance of a regex - pub fn next_match(&mut self, regex: &str, inc: usize) -> Option { - // Prepare - let mut srch = Searcher::new(regex); - // Check current line for matches - let current: String = self - .line(self.loc().y)? - .chars() - .skip(self.char_ptr + inc) - .collect(); - if let Some(mut mtch) = srch.lfind(¤t) { - mtch.loc.y = self.loc().y; - mtch.loc.x += self.char_ptr + inc; - return Some(mtch); - } - // Check subsequent lines for matches - let mut line_no = self.loc().y + 1; - self.load_to(line_no + 1); - while let Some(line) = self.line(line_no) { - if let Some(mut mtch) = srch.lfind(&line) { - mtch.loc.y = line_no; - return Some(mtch); - } - line_no += 1; - self.load_to(line_no + 1); - } - None - } - - /// Function to search the document to find the previous occurance of a regex - pub fn prev_match(&mut self, regex: &str) -> Option { - // Prepare - let mut srch = Searcher::new(regex); - // Check current line for matches - let current: String = self - .line(self.loc().y)? - .chars() - .take(self.char_ptr) - .collect(); - if let Some(mut mtch) = srch.rfind(¤t) { - mtch.loc.y = self.loc().y; - return Some(mtch); - } - // Check antecedent lines for matches - self.load_to(self.loc().y + 1); - let mut line_no = self.loc().y.saturating_sub(1); - while let Some(line) = self.line(line_no) { - if let Some(mut mtch) = srch.rfind(&line) { - mtch.loc.y = line_no; - return Some(mtch); - } - if line_no == 0 { - break; - } - line_no = line_no.saturating_sub(1); - } - None - } - - /// Replace a specific part of the document with another string. - /// # Errors - /// Will error if the replacement failed to be executed. - pub fn replace(&mut self, loc: Loc, target: &str, into: &str) -> Result<()> { - self.exe(Event::Delete(loc, target.to_string()))?; - self.exe(Event::Insert(loc, into.to_string()))?; - Ok(()) - } - - /// Replace all instances of a regex with another string - pub fn replace_all(&mut self, target: &str, into: &str) { - self.move_to(&Loc::at(0, 0)); - while let Some(mtch) = self.next_match(target, 1) { - drop(self.replace(mtch.loc, &mtch.text, into)); - } - } - - /// Function to go to a specific position - pub fn move_to(&mut self, loc: &Loc) { - self.select_to(loc); - self.cancel_selection(); - } - - /// Function to go to a specific position - pub fn select_to(&mut self, loc: &Loc) { - self.select_to_y(loc.y); - self.select_to_x(loc.x); - } - - /// Function to go to a specific x position - pub fn move_to_x(&mut self, x: usize) { - self.select_to_x(x); - self.cancel_selection(); - } - - /// Function to select to a specific x position - pub fn select_to_x(&mut self, x: usize) { - let line = self.line(self.loc().y).unwrap_or_default(); - // If the move position is out of bounds, move to the end of the line - if line.chars().count() < x { - let line = self.line(self.loc().y).unwrap_or_default(); - let length = line.chars().count(); - self.select_to_x(length); - return; - } - // Update char position - self.char_ptr = x; - // Calculate display index - let x = self.display_idx(&Loc::at(x, self.loc().y)); - // Move cursor - self.cursor.loc.x = x; - self.bring_cursor_in_viewport(); - } - - /// Function to go to a specific y position - pub fn move_to_y(&mut self, y: usize) { - self.select_to_y(y); - self.cancel_selection(); - } - - /// Function to select to a specific y position - pub fn select_to_y(&mut self, y: usize) { - // Bounds checking - if self.loc().y != y && y <= self.len_lines() { - self.cursor.loc.y = y; - } else if y > self.len_lines() { - self.cursor.loc.y = self.len_lines(); - } - // Snap to end of line - self.fix_dangling_cursor(); - // Ensure cursor isn't in the middle of a longer character - self.fix_split(); - // Correct the character pointer - self.update_char_ptr(); - self.bring_cursor_in_viewport(); - // Load any lines necessary - self.load_to(self.offset.y + self.size.h); - } - - /// Select a word at a location - pub fn select_word_at(&mut self, loc: &Loc) { - let y = loc.y; - let x = self.character_idx(loc); - let re = format!("(\t| {{{}}}|^|\\W| )", self.tab_width); - let start = if let Some(mut mtch) = self.prev_match(&re) { - let len = mtch.text.chars().count(); - let same = mtch.loc.x + len == x; - if !same { - mtch.loc.x += len; - } - self.move_to(&mtch.loc); - if same && self.loc().x != 0 { - self.move_prev_word(); - } - mtch.loc.x - } else { - 0 - }; - let re = format!("(\t| {{{}}}|\\W|$|^ +| )", self.tab_width); - let end = if let Some(mtch) = self.next_match(&re, 0) { - mtch.loc.x - } else { - self.line(y).unwrap_or_default().chars().count() - }; - self.move_to(&Loc { x: start, y }); - self.select_to(&Loc { x: end, y }); - self.old_cursor = self.loc().x; - } - - /// Select a line at a location - pub fn select_line_at(&mut self, y: usize) { - let len = self.line(y).unwrap_or_default().chars().count(); - self.move_to(&Loc { x: 0, y }); - self.select_to(&Loc { x: len, y }); - } - - /// Brings the cursor into the viewport so it can be seen - pub fn bring_cursor_in_viewport(&mut self) { - if self.offset.y > self.cursor.loc.y { - self.offset.y = self.cursor.loc.y; - } - if self.offset.y + self.size.h <= self.cursor.loc.y { - self.offset.y = self.cursor.loc.y.saturating_sub(self.size.h) + 1; - } - if self.offset.x > self.cursor.loc.x { - self.offset.x = self.cursor.loc.x; - } - if self.offset.x + self.size.w <= self.cursor.loc.x { - self.offset.x = self.cursor.loc.x.saturating_sub(self.size.w) + 1; - } - self.load_to(self.offset.y + self.size.h); - } - - /// Determines if specified coordinates are out of range of the document. - /// # Errors - /// Returns an error when the given coordinates are out of range. - /// # Panics - /// When you try using this function on a location that has not yet been loaded into buffer - /// If you see this error, you should double check that you have used `Document::load_to` - /// enough - pub fn out_of_range(&self, x: usize, y: usize) -> Result<()> { - let msg = "Did you forget to use load_to?"; - if y >= self.len_lines() || x > self.line(y).expect(msg).chars().count() { - return Err(Error::OutOfRange); - } - Ok(()) - } - - /// Determines if a range is in range of the document. - /// # Errors - /// Returns an error when the given range is out of range. - pub fn valid_range(&self, start: usize, end: usize, y: usize) -> Result<()> { - self.out_of_range(start, y)?; - self.out_of_range(end, y)?; - if start > end { - return Err(Error::OutOfRange); - } - Ok(()) - } - - /// Calculate the character index from the display index on a certain line - #[must_use] - pub fn character_idx(&self, loc: &Loc) -> usize { - let mut idx = loc.x; - // 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), - ); - idx - } - - /// Calculate the display index from the character index on a certain line - fn display_idx(&self, loc: &Loc) -> usize { - let mut idx = loc.x; - // Account for double width characters - idx += self.dbl_map.count(loc, false).unwrap_or(0); - // Account for tab characters - idx += self.tab_map.count(loc, false).unwrap_or(0) * self.tab_width.saturating_sub(1); - idx - } - - /// A utility function to update the character pointer when moving up or down - fn update_char_ptr(&mut self) { - let mut idx = self.loc().x; - let dbl_count = self.dbl_map.count(&self.loc(), true).unwrap_or(0); - idx = idx.saturating_sub(dbl_count); - let tab_count = self.tab_map.count(&self.loc(), true).unwrap_or(0); - idx = idx.saturating_sub(tab_count * self.tab_width.saturating_sub(1)); - self.char_ptr = idx; - } - - /// A utility function to make sure the cursor doesn't go out of range when moving - fn fix_dangling_cursor(&mut self) { - if let Some(line) = self.line(self.loc().y) { - if self.loc().x > width(&line, self.tab_width) { - self.select_to_x(line.chars().count()); - } - } else { - self.select_home(); - } - } - - /// Fixes double width and tab boundary issues - fn fix_split(&mut self) { - let mut magnitude = 0; - let Loc { x, y } = self.loc(); - if let Some(map) = self.dbl_map.get(y) { - let last_dbl = self - .dbl_map - .count(&self.loc(), true) - .unwrap() - .saturating_sub(1); - let start = map[last_dbl].0; - if x == start + 1 { - magnitude += 1; - } - } - if let Some(map) = self.tab_map.get(y) { - let last_tab = self - .tab_map - .count(&self.loc(), true) - .unwrap() - .saturating_sub(1); - let start = map[last_tab].0; - let range = start..start + self.tab_width; - if range.contains(&x) { - magnitude += x.saturating_sub(start); - } - } - self.cursor.loc.x = self.cursor.loc.x.saturating_sub(magnitude); - } - - /// Load lines in this document up to a specified index. - /// This must be called before starting to edit the document as - /// this is the function that actually load and processes the text. - pub fn load_to(&mut self, mut to: usize) { - // Make sure to doesn't go over the number of lines in the buffer - let len_lines = self.file.len_lines(); - if to >= len_lines { - to = len_lines; - } - // Only act if there are lines we haven't loaded yet - if to > self.info.loaded_to { - // For each line, run through each character and make note of any double width characters - for i in self.info.loaded_to..to { - let line: String = self.file.line(i).chars().collect(); - // Add to char maps - let (dbl_map, tab_map) = form_map(&line, self.tab_width); - self.dbl_map.insert(i, dbl_map); - self.tab_map.insert(i, tab_map); - // Cache this line - self.lines - .push(line.trim_end_matches(['\n', '\r']).to_string()); - } - // Store new loaded point - self.info.loaded_to = to; - } - } - - /// Get the line at a specified index - #[must_use] - pub fn line(&self, line: usize) -> Option { - Some(self.lines.get(line)?.to_string()) - } - - /// Get the line at a specified index and trim it - #[must_use] - pub fn line_trim(&self, line: usize, start: usize, length: usize) -> Option { - let line = self.line(line); - Some(trim(&line?, start, length, self.tab_width)) - } - - /// Returns the number of lines in the document - #[must_use] - pub fn len_lines(&self) -> usize { - self.file.len_lines().saturating_sub(1) + usize::from(self.info.eol) - } - - /// Evaluate the line number text for a specific line - #[must_use] - pub fn line_number(&self, request: usize) -> String { - let total = self.len_lines().to_string().len(); - let num = if request + 1 > self.len_lines() { - "~".to_string() - } else { - (request + 1).to_string() - }; - format!("{}{}", " ".repeat(total.saturating_sub(num.len())), num) - } - - /// Determine if a character at a certain location is a double width character. - /// x is the display index. - #[must_use] - pub fn is_dbl_width(&self, y: usize, x: usize) -> bool { - if let Some(line) = self.dbl_map.get(y) { - line.iter().any(|i| x == i.1) - } else { - false - } - } - - /// Determine if a character at a certain location is a tab character. - /// x is the display index. - #[must_use] - pub fn is_tab(&self, y: usize, x: usize) -> bool { - if let Some(line) = self.tab_map.get(y) { - line.iter().any(|i| x == i.1) - } else { - false - } - } - - /// Determine the width of a character at a certain location - #[must_use] - pub fn width_of(&self, y: usize, x: usize) -> usize { - if self.is_dbl_width(y, x) { - 2 - } else if self.is_tab(y, x) { - self.tab_width - } else { - 1 - } - } - - /// Get the current position within the document, including offset - #[must_use] - pub const fn loc(&self) -> Loc { - Loc { - x: self.cursor.loc.x, - y: self.cursor.loc.y, - } - } - - /// Get the current position within the document, with x being the character index - #[must_use] - pub const fn char_loc(&self) -> Loc { - Loc { - x: self.char_ptr, - y: self.cursor.loc.y, - } - } - - /// If the cursor is within the viewport, this will return where it is relatively - #[must_use] - pub fn cursor_loc_in_screen(&self) -> Option { - if self.cursor.loc.x < self.offset.x { - return None; - } - if self.cursor.loc.y < self.offset.y { - return None; - } - let result = Loc { - x: self.cursor.loc.x.saturating_sub(self.offset.x), - y: self.cursor.loc.y.saturating_sub(self.offset.y), - }; - if result.x > self.size.w || result.y >= self.size.h { - return None; - } - Some(result) - } - - /// Returns true if there is no active selection and vice versa - #[must_use] - pub fn is_selection_empty(&self) -> bool { - self.cursor.loc == self.cursor.selection_end - } - - /// Will return the bounds of the current active selection - #[must_use] - pub fn selection_loc_bound(&self) -> (Loc, Loc) { - let mut left = self.cursor.loc; - let mut right = self.cursor.selection_end; - // Convert into character indices - left.x = self.character_idx(&left); - right.x = self.character_idx(&right); - if left > right { - std::mem::swap(&mut left, &mut right); - } - (left, right) - } - - /// Returns true if the provided location is within the current active selection - #[must_use] - pub fn is_loc_selected(&self, loc: Loc) -> bool { - let (left, right) = self.selection_loc_bound(); - left <= loc && loc < right - } - - /// Will return the current active selection as a range over file characters - #[must_use] - pub fn selection_range(&self) -> Range { - let mut cursor = self.cursor.loc; - let mut selection_end = self.cursor.selection_end; - cursor.x = self.character_idx(&cursor); - selection_end.x = self.character_idx(&selection_end); - let mut left = self.loc_to_file_pos(&cursor); - let mut right = self.loc_to_file_pos(&selection_end); - if left > right { - std::mem::swap(&mut left, &mut right); - } - left..right - } - - /// Will return the text contained within the current selection - #[must_use] - pub fn selection_text(&self) -> String { - self.file.slice(self.selection_range()).to_string() - } - - /// Commit a change to the undo management system - pub fn commit(&mut self) { - let s = self.take_snapshot(); - self.undo_mgmt.backpatch_cursor(&self.cursor); - self.undo_mgmt.commit(s); - } - - /// Completely reload the file - pub fn reload_lines(&mut self) { - let to = std::mem::take(&mut self.info.loaded_to); - self.lines.clear(); - self.load_to(to); - } - - /// Delete the currently selected text - pub fn remove_selection(&mut self) { - self.file.remove(self.selection_range()); - self.reload_lines(); - let mut goto = self.selection_loc_bound().0; - goto.x = self.display_idx(&goto); - self.cursor.loc = goto; - self.char_ptr = self.character_idx(&self.cursor.loc); - self.cancel_selection(); - self.bring_cursor_in_viewport(); - self.info.modified = true; - } -} - -/// Defines a cursor's position and any selection it may be covering -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub struct Cursor { - pub loc: Loc, - pub selection_end: Loc, -} - -/// State of a word -pub enum WordState { - AtStart(usize), - AtEnd(usize), - InCenter(usize), - Out, -} diff --git a/kaolinite/src/document/cursor.rs b/kaolinite/src/document/cursor.rs new file mode 100644 index 00000000..cb2cf3b7 --- /dev/null +++ b/kaolinite/src/document/cursor.rs @@ -0,0 +1,403 @@ +use crate::{Loc, Document}; +use crate::event::{Status}; +use crate::utils::{tab_boundaries_backward, width, tab_boundaries_forward}; +use std::ops::Range; + +/// Defines a cursor's position and any selection it may be covering +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct Cursor { + pub loc: Loc, + pub selection_end: Loc, +} + +impl Document { + /// Move the cursor up + pub fn move_up(&mut self) -> Status { + let r = self.select_up(); + self.cancel_selection(); + r + } + + /// Select with the cursor up + pub fn select_up(&mut self) -> Status { + // Return if already at start of document + if self.loc().y == 0 { + return Status::StartOfFile; + } + self.cursor.loc.y = self.cursor.loc.y.saturating_sub(1); + self.cursor.loc.x = self.old_cursor; + // Snap to end of line + self.fix_dangling_cursor(); + // Move back if in the middle of a longer character + self.fix_split(); + // Update the character pointer + self.update_char_ptr(); + self.bring_cursor_in_viewport(); + Status::None + } + + /// Move the cursor down + pub fn move_down(&mut self) -> Status { + let r = self.select_down(); + self.cancel_selection(); + r + } + + /// Select with the cursor down + pub fn select_down(&mut self) -> Status { + // Return if already on end of document + if self.len_lines() < self.loc().y + 1 { + return Status::EndOfFile; + } + self.cursor.loc.y += 1; + self.cursor.loc.x = self.old_cursor; + // Snap to end of line + self.fix_dangling_cursor(); + // Move back if in the middle of a longer character + self.fix_split(); + // Update the character pointer + self.update_char_ptr(); + self.bring_cursor_in_viewport(); + Status::None + } + + /// Move the cursor left + pub fn move_left(&mut self) -> Status { + let r = self.select_left(); + self.cancel_selection(); + r + } + + /// Select with the cursor left + pub fn select_left(&mut self) -> Status { + // Return if already at start of line + if self.loc().x == 0 { + return Status::StartOfLine; + } + // Determine the width of the character to traverse + let line = self.line(self.loc().y).unwrap_or_default(); + let boundaries = tab_boundaries_backward(&line, self.tab_width); + let width = if boundaries.contains(&self.char_ptr) { + // Push the character pointer up + self.char_ptr = self + .char_ptr + .saturating_sub(self.tab_width.saturating_sub(1)); + // There are spaces that should be treated as tabs (so should traverse the tab width) + self.tab_width + } else { + // There are no spaces that should be treated as tabs + self.width_of(self.loc().y, self.char_ptr.saturating_sub(1)) + }; + // Move back the correct amount + self.cursor.loc.x = self.cursor.loc.x.saturating_sub(width); + // Update the character pointer + self.char_ptr = self.char_ptr.saturating_sub(1); + self.bring_cursor_in_viewport(); + self.old_cursor = self.loc().x; + Status::None + } + + /// Move the cursor right + pub fn move_right(&mut self) -> Status { + let r = self.select_right(); + self.cancel_selection(); + r + } + + /// Select with the cursor right + pub fn select_right(&mut self) -> Status { + // Return if already on end of line + let line = self.line(self.loc().y).unwrap_or_default(); + let width = width(&line, self.tab_width); + if width == self.loc().x { + return Status::EndOfLine; + } + // Determine the width of the character to traverse + let boundaries = tab_boundaries_forward(&line, self.tab_width); + let width = if boundaries.contains(&self.char_ptr) { + // Push the character pointer up + self.char_ptr += self.tab_width.saturating_sub(1); + // There are spaces that should be treated as tabs (so should traverse the tab width) + self.tab_width + } else { + // There are no spaces that should be treated as tabs + self.width_of(self.loc().y, self.char_ptr) + }; + // Move forward the correct amount + self.cursor.loc.x += width; + // Update the character pointer + self.char_ptr += 1; + self.bring_cursor_in_viewport(); + self.old_cursor = self.loc().x; + Status::None + } + + /// Move to the start of the line + pub fn move_home(&mut self) { + self.select_home(); + self.cancel_selection(); + } + + /// Select to the start of the line + pub fn select_home(&mut self) { + self.cursor.loc.x = 0; + self.char_ptr = 0; + self.old_cursor = 0; + self.bring_cursor_in_viewport(); + } + + /// Move to the end of the line + pub fn move_end(&mut self) { + self.select_end(); + self.cancel_selection(); + } + + /// Select to the end of the line + pub fn select_end(&mut self) { + let line = self.line(self.loc().y).unwrap_or_default(); + let length = line.chars().count(); + self.select_to_x(length); + self.old_cursor = self.loc().x; + } + + /// Move to the top of the document + pub fn move_top(&mut self) { + self.move_to(&Loc::at(0, 0)); + } + + /// Move to the bottom of the document + pub fn move_bottom(&mut self) { + let last = self.len_lines(); + self.move_to(&Loc::at(0, last)); + } + + /// Select to the top of the document + pub fn select_top(&mut self) { + self.select_to(&Loc::at(0, 0)); + self.old_cursor = self.loc().x; + } + + /// Select to the bottom of the document + pub fn select_bottom(&mut self) { + let last = self.len_lines(); + self.select_to(&Loc::at(0, last)); + self.old_cursor = self.loc().x; + } + + /// Move up by 1 page + pub fn move_page_up(&mut self) { + // Set x to 0 + self.cursor.loc.x = 0; + self.char_ptr = 0; + self.old_cursor = 0; + // Calculate where to move the cursor + let new_cursor_y = self.cursor.loc.y.saturating_sub(self.size.h); + // Move to the new location and shift down offset proportionally + self.cursor.loc.y = new_cursor_y; + self.offset.y = self.offset.y.saturating_sub(self.size.h); + // Clean up + self.cancel_selection(); + } + + /// Move down by 1 page + pub fn move_page_down(&mut self) { + // Set x to 0 + self.cursor.loc.x = 0; + self.char_ptr = 0; + self.old_cursor = 0; + // Calculate where to move the cursor + let new_cursor_y = self.cursor.loc.y + self.size.h; + if new_cursor_y <= self.len_lines() { + // Cursor is in range, move to the new location and shift down offset proportionally + self.cursor.loc.y = new_cursor_y; + self.offset.y += self.size.h; + } else if self.len_lines() < self.offset.y + self.size.h { + // End line is in view, no need to move offset + self.cursor.loc.y = self.len_lines().saturating_sub(1); + } else { + // Cursor would be out of range (adjust to bottom of document) + self.cursor.loc.y = self.len_lines().saturating_sub(1); + self.offset.y = self.len_lines().saturating_sub(self.size.h); + } + // Clean up + self.load_to(self.offset.y + self.size.h); + self.cancel_selection(); + } + + /// Function to go to a specific position + pub fn move_to(&mut self, loc: &Loc) { + self.select_to(loc); + self.cancel_selection(); + } + + /// Function to go to a specific position + pub fn select_to(&mut self, loc: &Loc) { + self.select_to_y(loc.y); + self.select_to_x(loc.x); + } + + /// Function to go to a specific x position + pub fn move_to_x(&mut self, x: usize) { + self.select_to_x(x); + self.cancel_selection(); + } + + /// Function to select to a specific x position + pub fn select_to_x(&mut self, x: usize) { + let line = self.line(self.loc().y).unwrap_or_default(); + // If the move position is out of bounds, move to the end of the line + if line.chars().count() < x { + let line = self.line(self.loc().y).unwrap_or_default(); + let length = line.chars().count(); + self.select_to_x(length); + return; + } + // Update char position + self.char_ptr = x; + // Calculate display index + let x = self.display_idx(&Loc::at(x, self.loc().y)); + // Move cursor + self.cursor.loc.x = x; + self.bring_cursor_in_viewport(); + } + + /// Function to go to a specific y position + pub fn move_to_y(&mut self, y: usize) { + self.select_to_y(y); + self.cancel_selection(); + } + + /// Function to select to a specific y position + pub fn select_to_y(&mut self, y: usize) { + // Bounds checking + if self.loc().y != y && y <= self.len_lines() { + self.cursor.loc.y = y; + } else if y > self.len_lines() { + self.cursor.loc.y = self.len_lines(); + } + // Snap to end of line + self.fix_dangling_cursor(); + // Ensure cursor isn't in the middle of a longer character + self.fix_split(); + // Correct the character pointer + self.update_char_ptr(); + self.bring_cursor_in_viewport(); + // Load any lines necessary + self.load_to(self.offset.y + self.size.h); + } + + /// Move the view down + pub fn scroll_down(&mut self) { + self.offset.y += 1; + self.load_to(self.offset.y + self.size.h); + } + + /// Move the view up + pub fn scroll_up(&mut self) { + self.offset.y = self.offset.y.saturating_sub(1); + self.load_to(self.offset.y + self.size.h); + } + + /// Get the current position within the document, including offset + #[must_use] + pub const fn loc(&self) -> Loc { + Loc { + x: self.cursor.loc.x, + y: self.cursor.loc.y, + } + } + + /// Get the current position within the document, with x being the character index + #[must_use] + pub const fn char_loc(&self) -> Loc { + Loc { + x: self.char_ptr, + y: self.cursor.loc.y, + } + } + + /// If the cursor is within the viewport, this will return where it is relatively + #[must_use] + pub fn cursor_loc_in_screen(&self) -> Option { + if self.cursor.loc.x < self.offset.x { + return None; + } + if self.cursor.loc.y < self.offset.y { + return None; + } + let result = Loc { + x: self.cursor.loc.x.saturating_sub(self.offset.x), + y: self.cursor.loc.y.saturating_sub(self.offset.y), + }; + if result.x > self.size.w || result.y >= self.size.h { + return None; + } + Some(result) + } + + /// Returns true if there is no active selection and vice versa + #[must_use] + pub fn is_selection_empty(&self) -> bool { + self.cursor.loc == self.cursor.selection_end + } + + /// Will return the bounds of the current active selection + #[must_use] + pub fn selection_loc_bound(&self) -> (Loc, Loc) { + let mut left = self.cursor.loc; + let mut right = self.cursor.selection_end; + // Convert into character indices + left.x = self.character_idx(&left); + right.x = self.character_idx(&right); + if left > right { + std::mem::swap(&mut left, &mut right); + } + (left, right) + } + + /// Returns true if the provided location is within the current active selection + #[must_use] + pub fn is_loc_selected(&self, loc: Loc) -> bool { + let (left, right) = self.selection_loc_bound(); + left <= loc && loc < right + } + + /// Will return the current active selection as a range over file characters + #[must_use] + pub fn selection_range(&self) -> Range { + let mut cursor = self.cursor.loc; + let mut selection_end = self.cursor.selection_end; + cursor.x = self.character_idx(&cursor); + selection_end.x = self.character_idx(&selection_end); + let mut left = self.loc_to_file_pos(&cursor); + let mut right = self.loc_to_file_pos(&selection_end); + if left > right { + std::mem::swap(&mut left, &mut right); + } + left..right + } + + /// Will return the text contained within the current selection + #[must_use] + pub fn selection_text(&self) -> String { + self.file.slice(self.selection_range()).to_string() + } + + /// Delete the currently selected text + pub fn remove_selection(&mut self) { + self.file.remove(self.selection_range()); + self.reload_lines(); + let mut goto = self.selection_loc_bound().0; + goto.x = self.display_idx(&goto); + self.cursor.loc = goto; + self.char_ptr = self.character_idx(&self.cursor.loc); + self.cancel_selection(); + self.bring_cursor_in_viewport(); + self.info.modified = true; + } + + /// Cancels the current selection + pub fn cancel_selection(&mut self) { + self.cursor.selection_end = self.cursor.loc; + } +} diff --git a/kaolinite/src/document/disk.rs b/kaolinite/src/document/disk.rs new file mode 100644 index 00000000..4e1ce97e --- /dev/null +++ b/kaolinite/src/document/disk.rs @@ -0,0 +1,151 @@ +use crate::{Size, Loc, Document}; +use crate::map::{form_map, CharMap}; +use crate::event::{UndoMgmt, Result, Error}; +use crate::document::Cursor; +use ropey::Rope; +use std::io::{BufWriter, BufReader}; +use std::fs::File; +use crate::utils::get_absolute_path; + +/// A document info struct to store information about the file it represents +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct DocumentInfo { + /// Whether or not the document can be edited + pub read_only: bool, + /// Flag for an EOL + pub eol: bool, + /// true if the file has been modified since saving, false otherwise + pub modified: bool, + /// Contains the number of lines buffered into the document + pub loaded_to: usize, +} + +impl Document { + /// Creates a new, empty document with no file name. + #[cfg(not(tarpaulin_include))] + #[must_use] + pub fn new(size: Size) -> Self { + let mut this = Self { + file: Rope::from_str("\n"), + lines: vec![String::new()], + dbl_map: CharMap::default(), + tab_map: CharMap::default(), + file_name: None, + cursor: Cursor::default(), + offset: Loc::default(), + size, + char_ptr: 0, + undo_mgmt: UndoMgmt::default(), + tab_width: 4, + old_cursor: 0, + in_redo: false, + info: DocumentInfo { + loaded_to: 1, + eol: false, + read_only: false, + modified: false, + }, + }; + this.undo_mgmt.undo.push(this.take_snapshot()); + this.undo_mgmt.saved(); + this + } + + /// Open a document from a file name. + /// # Errors + /// Returns an error when file doesn't exist, or has incorrect permissions. + /// Also returns an error if the rope fails to initialise due to character set issues or + /// disk errors. + #[cfg(not(tarpaulin_include))] + pub fn open>(size: Size, file_name: S) -> Result { + let file_name = file_name.into(); + let file = Rope::from_reader(BufReader::new(File::open(&file_name)?))?; + let file_name = get_absolute_path(&file_name); + let mut this = Self { + info: DocumentInfo { + loaded_to: 0, + eol: !file + .line(file.len_lines().saturating_sub(1)) + .to_string() + .is_empty(), + read_only: false, + modified: false, + }, + file, + lines: vec![], + dbl_map: CharMap::default(), + tab_map: CharMap::default(), + file_name, + cursor: Cursor::default(), + offset: Loc::default(), + size, + char_ptr: 0, + undo_mgmt: UndoMgmt::default(), + tab_width: 4, + old_cursor: 0, + in_redo: false, + }; + this.undo_mgmt.undo.push(this.take_snapshot()); + this.undo_mgmt.saved(); + Ok(this) + } + + /// Save back to the file the document was opened from. + /// # Errors + /// Returns an error if the file fails to write, due to permissions + /// or character set issues. + pub fn save(&mut self) -> Result<()> { + if self.info.read_only { + Err(Error::ReadOnlyFile) + } else if let Some(file_name) = &self.file_name { + self.file + .write_to(BufWriter::new(File::create(file_name)?))?; + self.undo_mgmt.saved(); + self.info.modified = false; + Ok(()) + } else { + Err(Error::NoFileName) + } + } + + /// Save to a specified file. + /// # Errors + /// Returns an error if the file fails to write, due to permissions + /// or character set issues. + pub fn save_as(&self, file_name: &str) -> Result<()> { + if self.info.read_only { + Err(Error::ReadOnlyFile) + } else { + self.file + .write_to(BufWriter::new(File::create(file_name)?))?; + Ok(()) + } + } + + /// Load lines in this document up to a specified index. + /// This must be called before starting to edit the document as + /// this is the function that actually load and processes the text. + pub fn load_to(&mut self, mut to: usize) { + // Make sure to doesn't go over the number of lines in the buffer + let len_lines = self.file.len_lines(); + if to >= len_lines { + to = len_lines; + } + // Only act if there are lines we haven't loaded yet + if to > self.info.loaded_to { + // For each line, run through each character and make note of any double width characters + for i in self.info.loaded_to..to { + let line: String = self.file.line(i).chars().collect(); + // Add to char maps + let (dbl_map, tab_map) = form_map(&line, self.tab_width); + self.dbl_map.insert(i, dbl_map); + self.tab_map.insert(i, tab_map); + // Cache this line + self.lines + .push(line.trim_end_matches(['\n', '\r']).to_string()); + } + // Store new loaded point + self.info.loaded_to = to; + } + } +} diff --git a/kaolinite/src/document/editing.rs b/kaolinite/src/document/editing.rs new file mode 100644 index 00000000..4917d6e7 --- /dev/null +++ b/kaolinite/src/document/editing.rs @@ -0,0 +1,190 @@ +use crate::{Document, Loc}; +use crate::map::form_map; +use crate::utils::{tab_boundaries_backward, get_range}; +use crate::event::{Result, Event, Error}; +use std::ops::RangeBounds; + +impl Document { + /// Inserts a string into this document. + /// # Errors + /// Returns an error if location is out of range. + pub fn insert(&mut self, loc: &Loc, st: &str) -> Result<()> { + self.out_of_range(loc.x, loc.y)?; + self.info.modified = true; + // Move cursor to location + self.move_to(loc); + // Update rope + let idx = self.loc_to_file_pos(loc); + self.file.insert(idx, st); + // Update cache + let line: String = self.file.line(loc.y).chars().collect(); + self.lines[loc.y] = line.trim_end_matches(['\n', '\r']).to_string(); + // Update unicode map + let dbl_start = self.dbl_map.shift_insertion(loc, st, self.tab_width); + let tab_start = self.tab_map.shift_insertion(loc, st, self.tab_width); + // Register new double widths and tabs + let (mut dbls, mut tabs) = form_map(st, self.tab_width); + // Shift up to match insertion position in the document + let tab_shift = self.tab_width.saturating_sub(1) * tab_start; + for e in &mut dbls { + *e = (e.0 + loc.x + dbl_start + tab_shift, e.1 + loc.x); + } + for e in &mut tabs { + *e = (e.0 + loc.x + tab_shift + dbl_start, e.1 + loc.x); + } + self.dbl_map.splice(loc, dbl_start, dbls); + self.tab_map.splice(loc, tab_start, tabs); + // Go to end x position + self.move_to_x(loc.x + st.chars().count()); + self.old_cursor = self.loc().x; + Ok(()) + } + + /// Deletes a character at a location whilst checking for tab spaces + /// + /// # Errors + /// This code will error if the location is invalid + pub fn delete_with_tab(&mut self, loc: &Loc, st: &str) -> Result<()> { + // Check for tab spaces + let boundaries = + tab_boundaries_backward(&self.line(loc.y).unwrap_or_default(), self.tab_width); + if boundaries.contains(&loc.x.saturating_add(1)) && !self.in_redo { + // Register other delete actions to delete the whole tab + let mut loc_copy = *loc; + self.delete(loc.x..=loc.x + st.chars().count(), loc.y)?; + for _ in 1..self.tab_width { + loc_copy.x = loc_copy.x.saturating_sub(1); + self.exe(Event::Delete(loc_copy, " ".to_string()))?; + } + Ok(()) + } else { + // Normal character delete + self.delete(loc.x..=loc.x + st.chars().count(), loc.y) + } + } + + /// Deletes a range from this document. + /// # Errors + /// Returns an error if location is out of range. + pub fn delete(&mut self, x: R, y: usize) -> Result<()> + where + R: RangeBounds, + { + let line_start = self.file.try_line_to_char(y)?; + let line_end = line_start + self.line(y).ok_or(Error::OutOfRange)?.chars().count(); + // Extract range information + let (mut start, mut end) = get_range(&x, line_start, line_end); + self.valid_range(start, end, y)?; + self.info.modified = true; + self.move_to(&Loc::at(start, y)); + start += line_start; + end += line_start; + let removed = self.file.slice(start..end).to_string(); + // Update unicode and tab map + self.dbl_map.shift_deletion( + &Loc::at(line_start, y), + (start, end), + &removed, + self.tab_width, + ); + self.tab_map.shift_deletion( + &Loc::at(line_start, y), + (start, end), + &removed, + self.tab_width, + ); + // Update rope + self.file.remove(start..end); + // Update cache + let line: String = self.file.line(y).chars().collect(); + self.lines[y] = line.trim_end_matches(['\n', '\r']).to_string(); + self.old_cursor = self.loc().x; + Ok(()) + } + + /// Inserts a line into the document. + /// # Errors + /// Returns an error if location is out of range. + pub fn insert_line(&mut self, loc: usize, contents: String) -> Result<()> { + if !(self.lines.is_empty() || self.len_lines() == 0 && loc == 0) { + self.out_of_range(0, loc.saturating_sub(1))?; + } + self.info.modified = true; + // Update unicode and tab map + self.dbl_map.shift_down(loc); + self.tab_map.shift_down(loc); + // Calculate the unicode map and tab map of this line + let (dbl_map, tab_map) = form_map(&contents, self.tab_width); + self.dbl_map.insert(loc, dbl_map); + self.tab_map.insert(loc, tab_map); + // Update cache + self.lines.insert(loc, contents.to_string()); + // Update rope + let char_idx = self.file.line_to_char(loc); + self.file.insert(char_idx, &(contents + "\n")); + self.info.loaded_to += 1; + // Goto line + self.move_to_y(loc); + self.old_cursor = self.loc().x; + Ok(()) + } + + /// Deletes a line from the document. + /// # Errors + /// Returns an error if location is out of range. + pub fn delete_line(&mut self, loc: usize) -> Result<()> { + self.out_of_range(0, loc)?; + // Update tab & unicode map + self.dbl_map.delete(loc); + self.tab_map.delete(loc); + self.info.modified = true; + // Shift down other line numbers in the hashmap + self.dbl_map.shift_up(loc); + self.tab_map.shift_up(loc); + // Update cache + self.lines.remove(loc); + // Update rope + let idx_start = self.file.line_to_char(loc); + let idx_end = self.file.line_to_char(loc + 1); + self.file.remove(idx_start..idx_end); + self.info.loaded_to = self.info.loaded_to.saturating_sub(1); + // Goto line + self.move_to_y(loc); + self.old_cursor = self.loc().x; + Ok(()) + } + + /// Split a line in half, putting the right hand side below on a new line. + /// For when the return key is pressed. + /// # Errors + /// Returns an error if location is out of range. + pub fn split_down(&mut self, loc: &Loc) -> Result<()> { + self.out_of_range(loc.x, loc.y)?; + self.info.modified = true; + // Gather context + let line = self.line(loc.y).ok_or(Error::OutOfRange)?; + let rhs: String = line.chars().skip(loc.x).collect(); + self.delete(loc.x.., loc.y)?; + self.insert_line(loc.y + 1, rhs)?; + self.move_to(&Loc::at(0, loc.y + 1)); + self.old_cursor = self.loc().x; + Ok(()) + } + + /// Remove the line below the specified location and append that to it. + /// For when backspace is pressed on the start of a line. + /// # Errors + /// Returns an error if location is out of range. + pub fn splice_up(&mut self, y: usize) -> Result<()> { + self.out_of_range(0, y + 1)?; + self.info.modified = true; + // Gather context + let length = self.line(y).ok_or(Error::OutOfRange)?.chars().count(); + let below = self.line(y + 1).ok_or(Error::OutOfRange)?; + self.delete_line(y + 1)?; + self.insert(&Loc::at(length, y), &below)?; + self.move_to(&Loc::at(length, y)); + self.old_cursor = self.loc().x; + Ok(()) + } +} diff --git a/kaolinite/src/document/lines.rs b/kaolinite/src/document/lines.rs new file mode 100644 index 00000000..a1f3ccfc --- /dev/null +++ b/kaolinite/src/document/lines.rs @@ -0,0 +1,73 @@ +use crate::{Loc, Document}; +use crate::utils::{trim}; +use crate::event::{Result, Error}; + +impl Document { + /// Get the line at a specified index + #[must_use] + pub fn line(&self, line: usize) -> Option { + Some(self.lines.get(line)?.to_string()) + } + + /// Get the line at a specified index and trim it + #[must_use] + pub fn line_trim(&self, line: usize, start: usize, length: usize) -> Option { + let line = self.line(line); + Some(trim(&line?, start, length, self.tab_width)) + } + + /// Returns the number of lines in the document + #[must_use] + pub fn len_lines(&self) -> usize { + self.file.len_lines().saturating_sub(1) + usize::from(self.info.eol) + } + + /// Evaluate the line number text for a specific line + #[must_use] + pub fn line_number(&self, request: usize) -> String { + let total = self.len_lines().to_string().len(); + let num = if request + 1 > self.len_lines() { + "~".to_string() + } else { + (request + 1).to_string() + }; + format!("{}{}", " ".repeat(total.saturating_sub(num.len())), num) + } + + /// Swap a line upwards + /// # Errors + /// When out of bounds + pub fn swap_line_up(&mut self) -> Result<()> { + let cursor = self.char_loc(); + let line = self.line(cursor.y).ok_or(Error::OutOfRange)?; + self.insert_line(cursor.y.saturating_sub(1), line)?; + self.delete_line(cursor.y + 1)?; + self.move_to(&Loc { + x: cursor.x, + y: cursor.y.saturating_sub(1), + }); + Ok(()) + } + + /// Swap a line downwards + /// # Errors + /// When out of bounds + pub fn swap_line_down(&mut self) -> Result<()> { + let cursor = self.char_loc(); + let line = self.line(cursor.y).ok_or(Error::OutOfRange)?; + self.insert_line(cursor.y + 2, line)?; + self.delete_line(cursor.y)?; + self.move_to(&Loc { + x: cursor.x, + y: cursor.y + 1, + }); + Ok(()) + } + + /// Select a line at a location + pub fn select_line_at(&mut self, y: usize) { + let len = self.line(y).unwrap_or_default().chars().count(); + self.move_to(&Loc { x: 0, y }); + self.select_to(&Loc { x: len, y }); + } +} diff --git a/kaolinite/src/document/mod.rs b/kaolinite/src/document/mod.rs new file mode 100644 index 00000000..cde3db2b --- /dev/null +++ b/kaolinite/src/document/mod.rs @@ -0,0 +1,385 @@ +/// document.rs - has Document, for opening, editing and saving documents +use crate::event::{Error, Event, Result, UndoMgmt}; +use crate::map::CharMap; +use crate::searching::{Match, Searcher}; +use crate::utils::{ + modeline, + width, Loc, Size, +}; +use ropey::Rope; +use std::path::Path; + +pub mod cursor; +pub mod disk; +pub mod editing; +pub mod words; +pub mod lines; + +pub use cursor::Cursor; +pub use disk::DocumentInfo; + +/// A document struct manages a file. +/// It has tools to read, write and traverse a document. +/// By default, it uses file buffering so it can open almost immediately. +/// To start executing events, remember to use the `Document::exe` function and check out +/// the documentation for `Event` to learn how to form editing events. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Document { + /// The file name of the document opened + pub file_name: Option, + /// The rope of the document to facilitate reading and writing to disk + pub file: Rope, + /// Cache of all the loaded lines in this document + pub lines: Vec, + /// Stores information about the underlying file + pub info: DocumentInfo, + /// Stores the locations of double width characters + pub dbl_map: CharMap, + /// Stores the locations of tab characters + pub tab_map: CharMap, + /// Contains the size of this document for purposes of offset + pub size: Size, + /// Contains the cursor data structure + pub cursor: Cursor, + /// Contains the offset (scrolling for longer documents) + pub offset: Loc, + /// Keeps track of where the character pointer is + pub char_ptr: usize, + /// Manages events, for the purpose of undo and redo + pub undo_mgmt: UndoMgmt, + /// Storage of the old cursor x position (to snap back to) + pub old_cursor: usize, + /// Flag for if the editor is currently in a redo action + pub in_redo: bool, + /// The number of spaces a tab should be rendered as + pub tab_width: usize, +} + +impl Document { + /// Determine the file type of this file (represented by an extension) + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn get_file_type(&self) -> Option<&str> { + let mut result = None; + // Try to use modeline first off + if let Some(first_line) = self.lines.first() { + result = modeline(first_line); + } + // If an extension is available, use that instead + if let Some(file_name) = &self.file_name { + if let Some(extension) = Path::new(file_name).extension() { + result = extension.to_str(); + } + } + result + } + + /// Sets the tab display width measured in spaces, default being 4 + pub fn set_tab_width(&mut self, tab_width: usize) { + self.tab_width = tab_width; + } + + /// Execute an event, registering it in the undo / redo. + /// You should always edit a document through this method to ensure undo and redo work. + /// # Errors + /// Will return an error if the event was unable to be completed. + pub fn exe(&mut self, ev: Event) -> Result<()> { + if !self.info.read_only { + self.undo_mgmt.last_event = ev.clone(); + self.undo_mgmt.set_dirty(); + self.forth(ev)?; + } + self.cancel_selection(); + Ok(()) + } + + /// Undo the last patch in the document. + /// # Errors + /// Will return an error if any of the events failed to be reversed. + pub fn undo(&mut self) -> Result<()> { + if let Some(s) = self.undo_mgmt.undo(self.take_snapshot()) { + self.apply_snapshot(s); + self.info.modified = true; + } + if self.undo_mgmt.at_file() { + self.info.modified = false; + } + Ok(()) + } + + /// Redo the last patch in the document. + /// # Errors + /// Will return an error if any of the events failed to be re-executed. + pub fn redo(&mut self) -> Result<()> { + if let Some(s) = self.undo_mgmt.redo() { + self.apply_snapshot(s); + self.info.modified = true; + } + if self.undo_mgmt.at_file() { + self.info.modified = false; + } + Ok(()) + } + + /// Handle an editing event, use the method `exe` for executing events. + /// # Errors + /// Returns an error if there is a problem with the specified operation. + pub fn forth(&mut self, ev: Event) -> Result<()> { + match ev { + Event::Insert(loc, ch) => self.insert(&loc, &ch), + Event::Delete(loc, st) => self.delete_with_tab(&loc, &st), + Event::InsertLine(loc, st) => self.insert_line(loc, st), + Event::DeleteLine(loc, _) => self.delete_line(loc), + Event::SplitDown(loc) => self.split_down(&loc), + Event::SpliceUp(loc) => self.splice_up(loc.y), + } + } + + /// Takes a loc and converts it into a char index for ropey + #[must_use] + pub fn loc_to_file_pos(&self, loc: &Loc) -> usize { + self.file.line_to_char(loc.y) + loc.x + } + + /// Function to search the document to find the next occurance of a regex + pub fn next_match(&mut self, regex: &str, inc: usize) -> Option { + // Prepare + let mut srch = Searcher::new(regex); + // Check current line for matches + let current: String = self + .line(self.loc().y)? + .chars() + .skip(self.char_ptr + inc) + .collect(); + if let Some(mut mtch) = srch.lfind(¤t) { + mtch.loc.y = self.loc().y; + mtch.loc.x += self.char_ptr + inc; + return Some(mtch); + } + // Check subsequent lines for matches + let mut line_no = self.loc().y + 1; + self.load_to(line_no + 1); + while let Some(line) = self.line(line_no) { + if let Some(mut mtch) = srch.lfind(&line) { + mtch.loc.y = line_no; + return Some(mtch); + } + line_no += 1; + self.load_to(line_no + 1); + } + None + } + + /// Function to search the document to find the previous occurance of a regex + pub fn prev_match(&mut self, regex: &str) -> Option { + // Prepare + let mut srch = Searcher::new(regex); + // Check current line for matches + let current: String = self + .line(self.loc().y)? + .chars() + .take(self.char_ptr) + .collect(); + if let Some(mut mtch) = srch.rfind(¤t) { + mtch.loc.y = self.loc().y; + return Some(mtch); + } + // Check antecedent lines for matches + self.load_to(self.loc().y + 1); + let mut line_no = self.loc().y.saturating_sub(1); + while let Some(line) = self.line(line_no) { + if let Some(mut mtch) = srch.rfind(&line) { + mtch.loc.y = line_no; + return Some(mtch); + } + if line_no == 0 { + break; + } + line_no = line_no.saturating_sub(1); + } + None + } + + /// Replace a specific part of the document with another string. + /// # Errors + /// Will error if the replacement failed to be executed. + pub fn replace(&mut self, loc: Loc, target: &str, into: &str) -> Result<()> { + self.exe(Event::Delete(loc, target.to_string()))?; + self.exe(Event::Insert(loc, into.to_string()))?; + Ok(()) + } + + /// Replace all instances of a regex with another string + pub fn replace_all(&mut self, target: &str, into: &str) { + self.move_to(&Loc::at(0, 0)); + while let Some(mtch) = self.next_match(target, 1) { + drop(self.replace(mtch.loc, &mtch.text, into)); + } + } + + /// Brings the cursor into the viewport so it can be seen + pub fn bring_cursor_in_viewport(&mut self) { + if self.offset.y > self.cursor.loc.y { + self.offset.y = self.cursor.loc.y; + } + if self.offset.y + self.size.h <= self.cursor.loc.y { + self.offset.y = self.cursor.loc.y.saturating_sub(self.size.h) + 1; + } + if self.offset.x > self.cursor.loc.x { + self.offset.x = self.cursor.loc.x; + } + if self.offset.x + self.size.w <= self.cursor.loc.x { + self.offset.x = self.cursor.loc.x.saturating_sub(self.size.w) + 1; + } + self.load_to(self.offset.y + self.size.h); + } + + /// Determines if specified coordinates are out of range of the document. + /// # Errors + /// Returns an error when the given coordinates are out of range. + /// # Panics + /// When you try using this function on a location that has not yet been loaded into buffer + /// If you see this error, you should double check that you have used `Document::load_to` + /// enough + pub fn out_of_range(&self, x: usize, y: usize) -> Result<()> { + let msg = "Did you forget to use load_to?"; + if y >= self.len_lines() || x > self.line(y).expect(msg).chars().count() { + return Err(Error::OutOfRange); + } + Ok(()) + } + + /// Determines if a range is in range of the document. + /// # Errors + /// Returns an error when the given range is out of range. + pub fn valid_range(&self, start: usize, end: usize, y: usize) -> Result<()> { + self.out_of_range(start, y)?; + self.out_of_range(end, y)?; + if start > end { + return Err(Error::OutOfRange); + } + Ok(()) + } + + /// Calculate the character index from the display index on a certain line + #[must_use] + pub fn character_idx(&self, loc: &Loc) -> usize { + let mut idx = loc.x; + // 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), + ); + idx + } + + /// Calculate the display index from the character index on a certain line + fn display_idx(&self, loc: &Loc) -> usize { + let mut idx = loc.x; + // Account for double width characters + idx += self.dbl_map.count(loc, false).unwrap_or(0); + // Account for tab characters + idx += self.tab_map.count(loc, false).unwrap_or(0) * self.tab_width.saturating_sub(1); + idx + } + + /// A utility function to update the character pointer when moving up or down + fn update_char_ptr(&mut self) { + let mut idx = self.loc().x; + let dbl_count = self.dbl_map.count(&self.loc(), true).unwrap_or(0); + idx = idx.saturating_sub(dbl_count); + let tab_count = self.tab_map.count(&self.loc(), true).unwrap_or(0); + idx = idx.saturating_sub(tab_count * self.tab_width.saturating_sub(1)); + self.char_ptr = idx; + } + + /// A utility function to make sure the cursor doesn't go out of range when moving + fn fix_dangling_cursor(&mut self) { + if let Some(line) = self.line(self.loc().y) { + if self.loc().x > width(&line, self.tab_width) { + self.select_to_x(line.chars().count()); + } + } else { + self.select_home(); + } + } + + /// Fixes double width and tab boundary issues + fn fix_split(&mut self) { + let mut magnitude = 0; + let Loc { x, y } = self.loc(); + if let Some(map) = self.dbl_map.get(y) { + let last_dbl = self + .dbl_map + .count(&self.loc(), true) + .unwrap() + .saturating_sub(1); + let start = map[last_dbl].0; + if x == start + 1 { + magnitude += 1; + } + } + if let Some(map) = self.tab_map.get(y) { + let last_tab = self + .tab_map + .count(&self.loc(), true) + .unwrap() + .saturating_sub(1); + let start = map[last_tab].0; + let range = start..start + self.tab_width; + if range.contains(&x) { + magnitude += x.saturating_sub(start); + } + } + self.cursor.loc.x = self.cursor.loc.x.saturating_sub(magnitude); + } + + /// Determine if a character at a certain location is a double width character. + /// x is the display index. + #[must_use] + pub fn is_dbl_width(&self, y: usize, x: usize) -> bool { + if let Some(line) = self.dbl_map.get(y) { + line.iter().any(|i| x == i.1) + } else { + false + } + } + + /// Determine if a character at a certain location is a tab character. + /// x is the display index. + #[must_use] + pub fn is_tab(&self, y: usize, x: usize) -> bool { + if let Some(line) = self.tab_map.get(y) { + line.iter().any(|i| x == i.1) + } else { + false + } + } + + /// Determine the width of a character at a certain location + #[must_use] + pub fn width_of(&self, y: usize, x: usize) -> usize { + if self.is_dbl_width(y, x) { + 2 + } else if self.is_tab(y, x) { + self.tab_width + } else { + 1 + } + } + + /// Commit a change to the undo management system + pub fn commit(&mut self) { + let s = self.take_snapshot(); + self.undo_mgmt.backpatch_cursor(&self.cursor); + self.undo_mgmt.commit(s); + } + + /// Completely reload the file + pub fn reload_lines(&mut self) { + let to = std::mem::take(&mut self.info.loaded_to); + self.lines.clear(); + self.load_to(to); + } +} diff --git a/kaolinite/src/document/words.rs b/kaolinite/src/document/words.rs new file mode 100644 index 00000000..94f743a1 --- /dev/null +++ b/kaolinite/src/document/words.rs @@ -0,0 +1,298 @@ +use crate::{Loc, Document}; +use crate::searching::Searcher; +use crate::event::{Result, Status}; +use crate::searching::Match; + +/// State of a word +pub enum WordState { + AtStart(usize), + AtEnd(usize), + InCenter(usize), + Out, +} + +impl Document { + /// Find the word boundaries + #[must_use] + pub fn word_boundaries(&self, line: &str) -> Vec<(usize, usize)> { + let re = r"(\s{2,}|[A-Za-z0-9_]+|\.)"; + let mut searcher = Searcher::new(re); + let starts: Vec = searcher.lfinds(line); + let mut ends: Vec = starts.clone(); + ends.iter_mut() + .for_each(|m| m.loc.x += m.text.chars().count()); + let starts: Vec = starts.iter().map(|m| m.loc.x).collect(); + let ends: Vec = ends.iter().map(|m| m.loc.x).collect(); + starts.into_iter().zip(ends).collect() + } + + /// Find the current state of the cursor in relation to words + #[must_use] + pub fn cursor_word_state(&self, words: &[(usize, usize)], x: usize) -> WordState { + let in_word = words + .iter() + .position(|(start, end)| *start <= x && x <= *end); + if let Some(idx) = in_word { + let (word_start, word_end) = words[idx]; + if x == word_end { + WordState::AtEnd(idx) + } else if x == word_start { + WordState::AtStart(idx) + } else { + WordState::InCenter(idx) + } + } else { + WordState::Out + } + } + + /// Find the index of the next word + #[must_use] + pub fn prev_word_close(&self, from: Loc) -> usize { + let Loc { x, y } = from; + let line = self.line(y).unwrap_or_default(); + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + match state { + // Go to start of line if at beginning + WordState::AtEnd(0) | WordState::InCenter(0) | WordState::AtStart(0) => 0, + // Cursor is at the middle / end of a word, move to previous end + WordState::AtEnd(idx) | WordState::InCenter(idx) => words[idx.saturating_sub(1)].1, + WordState::AtStart(idx) => words[idx.saturating_sub(1)].0, + WordState::Out => { + // Cursor is not touching any words, find previous end + let mut shift_back = x; + while let WordState::Out = self.cursor_word_state(&words, shift_back) { + shift_back = shift_back.saturating_sub(1); + if shift_back == 0 { + break; + } + } + match self.cursor_word_state(&words, shift_back) { + WordState::AtEnd(idx) => words[idx].0, + _ => 0, + } + } + } + } + + /// Find the index of the next word + #[must_use] + pub fn prev_word_index(&self, from: Loc) -> usize { + let Loc { x, y } = from; + let line = self.line(y).unwrap_or_default(); + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + match state { + // Go to start of line if at beginning + WordState::AtEnd(0) | WordState::InCenter(0) | WordState::AtStart(0) => 0, + // Cursor is at the middle / end of a word, move to previous end + WordState::AtEnd(idx) | WordState::InCenter(idx) => words[idx.saturating_sub(1)].1, + WordState::AtStart(idx) => words[idx.saturating_sub(1)].0, + WordState::Out => { + // Cursor is not touching any words, find previous end + let mut shift_back = x; + while let WordState::Out = self.cursor_word_state(&words, shift_back) { + shift_back = shift_back.saturating_sub(1); + if shift_back == 0 { + break; + } + } + match self.cursor_word_state(&words, shift_back) { + WordState::AtEnd(idx) => words[idx].1, + _ => 0, + } + } + } + } + + /// Moves to the previous word in the document + pub fn move_prev_word(&mut self) -> Status { + let Loc { x, y } = self.char_loc(); + // Handle case where we're at the beginning of the line + if x == 0 && y != 0 { + return Status::StartOfLine; + } + // Work out where to move to + let new_x = self.prev_word_index(self.char_loc()); + // Perform the move + self.move_to_x(new_x); + // Clean up + self.old_cursor = self.loc().x; + Status::None + } + + /// Find the index of the next word + #[must_use] + pub fn next_word_close(&self, from: Loc) -> usize { + let Loc { x, y } = from; + let line = self.line(y).unwrap_or_default(); + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + match state { + // Cursor is at the middle / end of a word, move to next end + WordState::AtEnd(idx) | WordState::InCenter(idx) => { + if let Some(word) = words.get(idx) { + word.1 + } else { + // No next word exists, just go to end of line + line.chars().count() + } + } + WordState::AtStart(idx) => { + // Cursor is at the start of a word, move to next start + if let Some(word) = words.get(idx) { + word.0 + } else { + // No next word exists, just go to end of line + line.chars().count() + } + } + WordState::Out => { + // Cursor is not touching any words, find next start + let mut shift_forward = x; + while let WordState::Out = self.cursor_word_state(&words, shift_forward) { + shift_forward += 1; + if shift_forward >= line.chars().count() { + break; + } + } + match self.cursor_word_state(&words, shift_forward) { + WordState::AtStart(idx) => words[idx].0, + _ => line.chars().count(), + } + } + } + } + + /// Find the index of the next word + #[must_use] + pub fn next_word_index(&self, from: Loc) -> usize { + let Loc { x, y } = from; + let line = self.line(y).unwrap_or_default(); + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + match state { + // Cursor is at the middle / end of a word, move to next end + WordState::AtEnd(idx) | WordState::InCenter(idx) => { + if let Some(word) = words.get(idx + 1) { + word.1 + } else { + // No next word exists, just go to end of line + line.chars().count() + } + } + WordState::AtStart(idx) => { + // Cursor is at the start of a word, move to next start + if let Some(word) = words.get(idx + 1) { + word.0 + } else { + // No next word exists, just go to end of line + line.chars().count() + } + } + WordState::Out => { + // Cursor is not touching any words, find next start + let mut shift_forward = x; + while let WordState::Out = self.cursor_word_state(&words, shift_forward) { + shift_forward += 1; + if shift_forward >= line.chars().count() { + break; + } + } + match self.cursor_word_state(&words, shift_forward) { + WordState::AtStart(idx) => words[idx].0, + _ => line.chars().count(), + } + } + } + } + + /// Moves to the next word in the document + pub fn move_next_word(&mut self) -> Status { + let Loc { x, y } = self.char_loc(); + let line = self.line(y).unwrap_or_default(); + // Handle case where we're at the end of the line + if x == line.chars().count() && y != self.len_lines() { + return Status::EndOfLine; + } + // Work out where to move to + let new_x = self.next_word_index(self.char_loc()); + // Perform the move + self.move_to_x(new_x); + // Clean up + self.old_cursor = self.loc().x; + Status::None + } + + /// Function to delete a word at a certain location + /// # Errors + /// Errors if out of range + pub fn delete_word(&mut self) -> Result<()> { + let Loc { x, y } = self.char_loc(); + let line = self.line(y).unwrap_or_default(); + let words = self.word_boundaries(&line); + let state = self.cursor_word_state(&words, x); + let delete_upto = match state { + WordState::InCenter(idx) | WordState::AtEnd(idx) => { + // Delete back to start of this word + words[idx].0 + } + WordState::AtStart(0) => 0, + WordState::AtStart(idx) => { + // Delete back to start of the previous word + words[idx.saturating_sub(1)].0 + } + WordState::Out => { + // Delete back to the end of the previous word + let mut shift_back = x; + while let WordState::Out = self.cursor_word_state(&words, shift_back) { + shift_back = shift_back.saturating_sub(1); + if shift_back == 0 { + break; + } + } + let char = line.chars().nth(shift_back); + let state = self.cursor_word_state(&words, shift_back); + match (char, state) { + // Shift to start of previous word if there is a space + (Some(' '), WordState::AtEnd(idx)) => words[idx].0, + // Shift to end of previous word if there is not a space + (_, WordState::AtEnd(idx)) => words[idx].1, + _ => 0, + } + } + }; + self.delete(delete_upto..=x, y) + } + + /// Select a word at a location + pub fn select_word_at(&mut self, loc: &Loc) { + let y = loc.y; + let x = self.character_idx(loc); + let re = format!("(\t| {{{}}}|^|\\W| )", self.tab_width); + let start = if let Some(mut mtch) = self.prev_match(&re) { + let len = mtch.text.chars().count(); + let same = mtch.loc.x + len == x; + if !same { + mtch.loc.x += len; + } + self.move_to(&mtch.loc); + if same && self.loc().x != 0 { + self.move_prev_word(); + } + mtch.loc.x + } else { + 0 + }; + let re = format!("(\t| {{{}}}|\\W|$|^ +| )", self.tab_width); + let end = if let Some(mtch) = self.next_match(&re, 0) { + mtch.loc.x + } else { + self.line(y).unwrap_or_default().chars().count() + }; + self.move_to(&Loc { x: start, y }); + self.select_to(&Loc { x: end, y }); + self.old_cursor = self.loc().x; + } +} From 51707f3c6bedbce0ee7f5a08c334426c821a77a9 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:33:18 +0000 Subject: [PATCH 14/73] rustfmt --- kaolinite/src/document/cursor.rs | 8 ++++---- kaolinite/src/document/disk.rs | 12 ++++++------ kaolinite/src/document/editing.rs | 6 +++--- kaolinite/src/document/lines.rs | 6 +++--- kaolinite/src/document/mod.rs | 7 ++----- kaolinite/src/document/words.rs | 4 ++-- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/kaolinite/src/document/cursor.rs b/kaolinite/src/document/cursor.rs index cb2cf3b7..82d8187b 100644 --- a/kaolinite/src/document/cursor.rs +++ b/kaolinite/src/document/cursor.rs @@ -1,6 +1,6 @@ -use crate::{Loc, Document}; -use crate::event::{Status}; -use crate::utils::{tab_boundaries_backward, width, tab_boundaries_forward}; +use crate::event::Status; +use crate::utils::{tab_boundaries_backward, tab_boundaries_forward, width}; +use crate::{Document, Loc}; use std::ops::Range; /// Defines a cursor's position and any selection it may be covering @@ -395,7 +395,7 @@ impl Document { self.bring_cursor_in_viewport(); self.info.modified = true; } - + /// Cancels the current selection pub fn cancel_selection(&mut self) { self.cursor.selection_end = self.cursor.loc; diff --git a/kaolinite/src/document/disk.rs b/kaolinite/src/document/disk.rs index 4e1ce97e..a3e5e70f 100644 --- a/kaolinite/src/document/disk.rs +++ b/kaolinite/src/document/disk.rs @@ -1,11 +1,11 @@ -use crate::{Size, Loc, Document}; -use crate::map::{form_map, CharMap}; -use crate::event::{UndoMgmt, Result, Error}; use crate::document::Cursor; +use crate::event::{Error, Result, UndoMgmt}; +use crate::map::{form_map, CharMap}; +use crate::utils::get_absolute_path; +use crate::{Document, Loc, Size}; use ropey::Rope; -use std::io::{BufWriter, BufReader}; use std::fs::File; -use crate::utils::get_absolute_path; +use std::io::{BufReader, BufWriter}; /// A document info struct to store information about the file it represents #[derive(Clone, PartialEq, Eq, Debug)] @@ -121,7 +121,7 @@ impl Document { Ok(()) } } - + /// Load lines in this document up to a specified index. /// This must be called before starting to edit the document as /// this is the function that actually load and processes the text. diff --git a/kaolinite/src/document/editing.rs b/kaolinite/src/document/editing.rs index 4917d6e7..f6990fbe 100644 --- a/kaolinite/src/document/editing.rs +++ b/kaolinite/src/document/editing.rs @@ -1,7 +1,7 @@ -use crate::{Document, Loc}; +use crate::event::{Error, Event, Result}; use crate::map::form_map; -use crate::utils::{tab_boundaries_backward, get_range}; -use crate::event::{Result, Event, Error}; +use crate::utils::{get_range, tab_boundaries_backward}; +use crate::{Document, Loc}; use std::ops::RangeBounds; impl Document { diff --git a/kaolinite/src/document/lines.rs b/kaolinite/src/document/lines.rs index a1f3ccfc..2409be40 100644 --- a/kaolinite/src/document/lines.rs +++ b/kaolinite/src/document/lines.rs @@ -1,6 +1,6 @@ -use crate::{Loc, Document}; -use crate::utils::{trim}; -use crate::event::{Result, Error}; +use crate::event::{Error, Result}; +use crate::utils::trim; +use crate::{Document, Loc}; impl Document { /// Get the line at a specified index diff --git a/kaolinite/src/document/mod.rs b/kaolinite/src/document/mod.rs index cde3db2b..aee64af6 100644 --- a/kaolinite/src/document/mod.rs +++ b/kaolinite/src/document/mod.rs @@ -2,18 +2,15 @@ use crate::event::{Error, Event, Result, UndoMgmt}; use crate::map::CharMap; use crate::searching::{Match, Searcher}; -use crate::utils::{ - modeline, - width, Loc, Size, -}; +use crate::utils::{modeline, width, Loc, Size}; use ropey::Rope; use std::path::Path; pub mod cursor; pub mod disk; pub mod editing; -pub mod words; pub mod lines; +pub mod words; pub use cursor::Cursor; pub use disk::DocumentInfo; diff --git a/kaolinite/src/document/words.rs b/kaolinite/src/document/words.rs index 94f743a1..7f923b63 100644 --- a/kaolinite/src/document/words.rs +++ b/kaolinite/src/document/words.rs @@ -1,7 +1,7 @@ -use crate::{Loc, Document}; -use crate::searching::Searcher; use crate::event::{Result, Status}; use crate::searching::Match; +use crate::searching::Searcher; +use crate::{Document, Loc}; /// State of a word pub enum WordState { From 836cf9df0c0cf7a35a837582474a60af4986f57e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:42:18 +0000 Subject: [PATCH 15/73] moved themes around --- {src => plugins}/themes/galaxy.lua | 0 {src => plugins}/themes/transparent.lua | 0 {src => plugins}/themes/tropical.lua | 0 src/config/assistant.rs | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename {src => plugins}/themes/galaxy.lua (100%) rename {src => plugins}/themes/transparent.lua (100%) rename {src => plugins}/themes/tropical.lua (100%) diff --git a/src/themes/galaxy.lua b/plugins/themes/galaxy.lua similarity index 100% rename from src/themes/galaxy.lua rename to plugins/themes/galaxy.lua diff --git a/src/themes/transparent.lua b/plugins/themes/transparent.lua similarity index 100% rename from src/themes/transparent.lua rename to plugins/themes/transparent.lua diff --git a/src/themes/tropical.lua b/plugins/themes/tropical.lua similarity index 100% rename from src/themes/tropical.lua rename to plugins/themes/tropical.lua diff --git a/src/config/assistant.rs b/src/config/assistant.rs index 1fb058fb..1b602bf6 100644 --- a/src/config/assistant.rs +++ b/src/config/assistant.rs @@ -12,9 +12,9 @@ use std::cell::RefCell; use std::io::{stdout, Write}; use std::rc::Rc; -pub const TROPICAL: &str = include_str!("../themes/tropical.lua"); -pub const GALAXY: &str = include_str!("../themes/galaxy.lua"); -pub const TRANSPARENT: &str = include_str!("../themes/transparent.lua"); +pub const TROPICAL: &str = include_str!("../../plugins/themes/tropical.lua"); +pub const GALAXY: &str = include_str!("../../plugins/themes/galaxy.lua"); +pub const TRANSPARENT: &str = include_str!("../../plugins/themes/transparent.lua"); #[macro_export] macro_rules! gets { From 828a800e82a3e4e75d47678fa01064eb7417680b Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:18:31 +0000 Subject: [PATCH 16/73] Fixed issue with ` characters in html files for livehtml --- plugins/live_html.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/live_html.lua b/plugins/live_html.lua index 62c52123..53e392b2 100644 --- a/plugins/live_html.lua +++ b/plugins/live_html.lua @@ -32,7 +32,7 @@ end function live_html_refresh() if editor.file_path == live_html.entry_point then - local contents = editor:get():gsub('"', '\\"'):gsub("\n", "") + local contents = editor:get():gsub('"', '\\"'):gsub("\n", ""):gsub("`", "\\`") http.post("localhost:5000/update", contents) end end From b6ac5e5f2c18053698aed6087dd0ec781a65affb Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:20:14 +0000 Subject: [PATCH 17/73] Kaolinite version bump --- kaolinite/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kaolinite/Cargo.toml b/kaolinite/Cargo.toml index 5a01e3de..15a8c3a3 100644 --- a/kaolinite/Cargo.toml +++ b/kaolinite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kaolinite" -version = "0.9.5" +version = "0.10.0" authors = ["curlpipe <11898833+curlpipe@users.noreply.github.com>"] edition = "2021" license = "MIT" From 16dc2b9651918e66fc59f171d802a2d7a5a3f493 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:56:13 +0000 Subject: [PATCH 18/73] Upped default scroll sensitivity --- Cargo.lock | 2 +- config/.oxrc | 2 +- src/config/assistant.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59d69119..a5042400 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,7 +217,7 @@ checksum = "b9d38e5712e29fb0c2caeb33b1803c8feade6e3380c7a92788fb219999b2849e" [[package]] name = "kaolinite" -version = "0.9.5" +version = "0.10.0" dependencies = [ "error_set", "rand", diff --git a/config/.oxrc b/config/.oxrc index f75629aa..c8e1cdd9 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -342,7 +342,7 @@ line_numbers.padding_right = 1 -- Configure Mouse Behaviour -- terminal.mouse_enabled = true -terminal.scroll_amount = 2 +terminal.scroll_amount = 4 -- Configure Tab Line -- tab_line.enabled = true diff --git a/src/config/assistant.rs b/src/config/assistant.rs index 1b602bf6..264bd1c9 100644 --- a/src/config/assistant.rs +++ b/src/config/assistant.rs @@ -191,7 +191,7 @@ impl Default for Assistant { greeting_message: true, // Mouse and Cursor Behaviour mouse: true, - scroll_sensitivity: 2, + scroll_sensitivity: 4, cursor_wrap: true, // Plug-ins plugins: vec![Plugin::AutoIndent, Plugin::Pairs, Plugin::QuickComment], @@ -405,8 +405,8 @@ impl Assistant { ); println!("{red}🖰 ⭥ {reset} {yellow}🖰 ⭥ {reset} {green}🖰 ⭥ {reset}\n"); result.scroll_sensitivity = Self::integer( - "How sensitive should scrolling be, 1 = least sensitive, 5 = very sensitive", - 2, + "How sensitive should scrolling be, 1 = least sensitive, 7 = very sensitive", + 4, ); println!(" Cursor wraps{pink}|{reset}→ \n ↳ {pink}|{reset}Onto new line\n"); result.cursor_wrap = Self::confirmation( From 5f38c75485d71d2a680d6c0552622d7f585574bb Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 00:11:18 +0000 Subject: [PATCH 19/73] Less ANSI code spam - more efficient rendering --- src/editor/interface.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 586fe005..a707e866 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -53,7 +53,7 @@ impl Editor { } /// Render the lines of the document - #[allow(clippy::similar_names)] + #[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()?; @@ -117,6 +117,10 @@ impl Editor { // Render line if it exists let idx = y as usize + self.doc().offset.y; if let Some(line) = self.doc().line(idx) { + // Reset the cache + let mut cache_bg = editor_bg; + let mut cache_fg = editor_fg; + // Gather the tokens let tokens = self.highlighter().line(idx, &line); let tokens = trim_fit(&tokens, self.doc().offset.x, required_width, tab_width); let mut x_pos = self.doc().offset.x; @@ -144,10 +148,14 @@ 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 { + if is_selected && (cache_bg != selection_bg || cache_fg != selection_fg) { display!(self, selection_bg, selection_fg); - } else { + 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; } display!(self, c); x_pos += 1; From 4ecf4137597c40d1a3cef8456029c8d10289a3f6 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:10:52 +0000 Subject: [PATCH 20/73] Cleaner, more effective refactored undo/redo system --- kaolinite/src/document/cursor.rs | 1 - kaolinite/src/document/disk.rs | 27 ++---- kaolinite/src/document/editing.rs | 6 -- kaolinite/src/document/mod.rs | 22 ++--- kaolinite/src/event.rs | 136 +++++++++++++++--------------- src/config/interface.rs | 16 +++- src/editor/editing.rs | 9 +- src/editor/mod.rs | 9 +- 8 files changed, 103 insertions(+), 123 deletions(-) diff --git a/kaolinite/src/document/cursor.rs b/kaolinite/src/document/cursor.rs index 82d8187b..8e04cf50 100644 --- a/kaolinite/src/document/cursor.rs +++ b/kaolinite/src/document/cursor.rs @@ -393,7 +393,6 @@ impl Document { self.char_ptr = self.character_idx(&self.cursor.loc); self.cancel_selection(); self.bring_cursor_in_viewport(); - self.info.modified = true; } /// Cancels the current selection diff --git a/kaolinite/src/document/disk.rs b/kaolinite/src/document/disk.rs index a3e5e70f..3e56f084 100644 --- a/kaolinite/src/document/disk.rs +++ b/kaolinite/src/document/disk.rs @@ -1,5 +1,5 @@ use crate::document::Cursor; -use crate::event::{Error, Result, UndoMgmt}; +use crate::event::{Error, EventMgmt, Result}; use crate::map::{form_map, CharMap}; use crate::utils::get_absolute_path; use crate::{Document, Loc, Size}; @@ -14,8 +14,6 @@ pub struct DocumentInfo { pub read_only: bool, /// Flag for an EOL pub eol: bool, - /// true if the file has been modified since saving, false otherwise - pub modified: bool, /// Contains the number of lines buffered into the document pub loaded_to: usize, } @@ -25,7 +23,7 @@ impl Document { #[cfg(not(tarpaulin_include))] #[must_use] pub fn new(size: Size) -> Self { - let mut this = Self { + Self { file: Rope::from_str("\n"), lines: vec![String::new()], dbl_map: CharMap::default(), @@ -35,7 +33,7 @@ impl Document { offset: Loc::default(), size, char_ptr: 0, - undo_mgmt: UndoMgmt::default(), + event_mgmt: EventMgmt::default(), tab_width: 4, old_cursor: 0, in_redo: false, @@ -43,12 +41,8 @@ impl Document { loaded_to: 1, eol: false, read_only: false, - modified: false, }, - }; - this.undo_mgmt.undo.push(this.take_snapshot()); - this.undo_mgmt.saved(); - this + } } /// Open a document from a file name. @@ -61,7 +55,7 @@ impl Document { let file_name = file_name.into(); let file = Rope::from_reader(BufReader::new(File::open(&file_name)?))?; let file_name = get_absolute_path(&file_name); - let mut this = Self { + Ok(Self { info: DocumentInfo { loaded_to: 0, eol: !file @@ -69,7 +63,6 @@ impl Document { .to_string() .is_empty(), read_only: false, - modified: false, }, file, lines: vec![], @@ -80,14 +73,11 @@ impl Document { offset: Loc::default(), size, char_ptr: 0, - undo_mgmt: UndoMgmt::default(), + event_mgmt: EventMgmt::default(), tab_width: 4, old_cursor: 0, in_redo: false, - }; - this.undo_mgmt.undo.push(this.take_snapshot()); - this.undo_mgmt.saved(); - Ok(this) + }) } /// Save back to the file the document was opened from. @@ -100,8 +90,7 @@ impl Document { } else if let Some(file_name) = &self.file_name { self.file .write_to(BufWriter::new(File::create(file_name)?))?; - self.undo_mgmt.saved(); - self.info.modified = false; + self.event_mgmt.disk_write(&self.take_snapshot()); Ok(()) } else { Err(Error::NoFileName) diff --git a/kaolinite/src/document/editing.rs b/kaolinite/src/document/editing.rs index f6990fbe..a2372883 100644 --- a/kaolinite/src/document/editing.rs +++ b/kaolinite/src/document/editing.rs @@ -10,7 +10,6 @@ impl Document { /// Returns an error if location is out of range. pub fn insert(&mut self, loc: &Loc, st: &str) -> Result<()> { self.out_of_range(loc.x, loc.y)?; - self.info.modified = true; // Move cursor to location self.move_to(loc); // Update rope @@ -75,7 +74,6 @@ impl Document { // Extract range information let (mut start, mut end) = get_range(&x, line_start, line_end); self.valid_range(start, end, y)?; - self.info.modified = true; self.move_to(&Loc::at(start, y)); start += line_start; end += line_start; @@ -109,7 +107,6 @@ impl Document { if !(self.lines.is_empty() || self.len_lines() == 0 && loc == 0) { self.out_of_range(0, loc.saturating_sub(1))?; } - self.info.modified = true; // Update unicode and tab map self.dbl_map.shift_down(loc); self.tab_map.shift_down(loc); @@ -137,7 +134,6 @@ impl Document { // Update tab & unicode map self.dbl_map.delete(loc); self.tab_map.delete(loc); - self.info.modified = true; // Shift down other line numbers in the hashmap self.dbl_map.shift_up(loc); self.tab_map.shift_up(loc); @@ -160,7 +156,6 @@ impl Document { /// Returns an error if location is out of range. pub fn split_down(&mut self, loc: &Loc) -> Result<()> { self.out_of_range(loc.x, loc.y)?; - self.info.modified = true; // Gather context let line = self.line(loc.y).ok_or(Error::OutOfRange)?; let rhs: String = line.chars().skip(loc.x).collect(); @@ -177,7 +172,6 @@ impl Document { /// Returns an error if location is out of range. pub fn splice_up(&mut self, y: usize) -> Result<()> { self.out_of_range(0, y + 1)?; - self.info.modified = true; // Gather context let length = self.line(y).ok_or(Error::OutOfRange)?.chars().count(); let below = self.line(y + 1).ok_or(Error::OutOfRange)?; diff --git a/kaolinite/src/document/mod.rs b/kaolinite/src/document/mod.rs index aee64af6..84c468fa 100644 --- a/kaolinite/src/document/mod.rs +++ b/kaolinite/src/document/mod.rs @@ -1,5 +1,5 @@ /// document.rs - has Document, for opening, editing and saving documents -use crate::event::{Error, Event, Result, UndoMgmt}; +use crate::event::{Error, Event, EventMgmt, Result}; use crate::map::CharMap; use crate::searching::{Match, Searcher}; use crate::utils::{modeline, width, Loc, Size}; @@ -43,7 +43,7 @@ pub struct Document { /// Keeps track of where the character pointer is pub char_ptr: usize, /// Manages events, for the purpose of undo and redo - pub undo_mgmt: UndoMgmt, + pub event_mgmt: EventMgmt, /// Storage of the old cursor x position (to snap back to) pub old_cursor: usize, /// Flag for if the editor is currently in a redo action @@ -82,8 +82,7 @@ impl Document { /// Will return an error if the event was unable to be completed. pub fn exe(&mut self, ev: Event) -> Result<()> { if !self.info.read_only { - self.undo_mgmt.last_event = ev.clone(); - self.undo_mgmt.set_dirty(); + self.event_mgmt.last_event = Some(ev.clone()); self.forth(ev)?; } self.cancel_selection(); @@ -94,12 +93,8 @@ impl Document { /// # Errors /// Will return an error if any of the events failed to be reversed. pub fn undo(&mut self) -> Result<()> { - if let Some(s) = self.undo_mgmt.undo(self.take_snapshot()) { + if let Some(s) = self.event_mgmt.undo(self.take_snapshot()) { self.apply_snapshot(s); - self.info.modified = true; - } - if self.undo_mgmt.at_file() { - self.info.modified = false; } Ok(()) } @@ -108,12 +103,8 @@ impl Document { /// # Errors /// Will return an error if any of the events failed to be re-executed. pub fn redo(&mut self) -> Result<()> { - if let Some(s) = self.undo_mgmt.redo() { + if let Some(s) = self.event_mgmt.redo(&self.take_snapshot()) { self.apply_snapshot(s); - self.info.modified = true; - } - if self.undo_mgmt.at_file() { - self.info.modified = false; } Ok(()) } @@ -369,8 +360,7 @@ impl Document { /// Commit a change to the undo management system pub fn commit(&mut self) { let s = self.take_snapshot(); - self.undo_mgmt.backpatch_cursor(&self.cursor); - self.undo_mgmt.commit(s); + self.event_mgmt.commit(s); } /// Completely reload the file diff --git a/kaolinite/src/event.rs b/kaolinite/src/event.rs index 4637e0c4..01045d7c 100644 --- a/kaolinite/src/event.rs +++ b/kaolinite/src/event.rs @@ -3,10 +3,11 @@ use crate::{document::Cursor, utils::Loc, Document}; use error_set::error_set; use ropey::Rope; +/// A snapshot stores the state of a document at a certain time #[derive(Debug, Clone, PartialEq, Eq)] pub struct Snapshot { - content: Rope, - cursor: Cursor, + pub content: Rope, + pub cursor: Cursor, } /// Represents an editing event. @@ -89,30 +90,16 @@ error_set! { } /// For managing events for purposes of undo and redo -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct UndoMgmt { - /// Whether the file touched since the latest commit - pub is_dirty: bool, - /// Undo contains all the patches that have been applied - pub undo: Vec, - /// Redo contains all the patches that have been undone - pub redo: Vec, +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct EventMgmt { + /// Contains all the snapshots in the current timeline + pub history: Vec, + /// Stores where the document currently is + pub ptr: Option, /// Store where the file on the disk is currently at - pub on_disk: usize, + pub on_disk: Option, /// Store the last event to occur (so that we can see if there is a change) - pub last_event: Event, -} - -impl Default for UndoMgmt { - fn default() -> Self { - Self { - is_dirty: false, - undo: vec![], - redo: vec![], - on_disk: 0, - last_event: Event::Insert(Loc { x: 0, y: 0 }, " ".to_string()), - } - } + pub last_event: Option, } impl Document { @@ -133,60 +120,77 @@ impl Document { } } -impl UndoMgmt { - /// Register that an event has occurred and the last snapshot is not update - pub fn set_dirty(&mut self) { - self.redo.clear(); - self.is_dirty = true; - } - - /// This will commit take a snapshot and add it to the undo stack, ready to be undone. - /// You can call this after every space character, for example, which would - /// make it so that every undo action would remove the previous word the user typed. - pub fn commit(&mut self, current_snapshot: Snapshot) { - if self.is_dirty { - self.is_dirty = false; - self.undo.push(current_snapshot); +impl EventMgmt { + /// In the event of some changes, redo should be cleared + pub fn clear_redo(&mut self) { + if let Some(ptr) = self.ptr { + self.history.drain(ptr + 1..); } } - /// Provide a snapshot of the desired state of the document for purposes - /// of undoing - pub fn undo(&mut self, current_snapshot: Snapshot) -> Option { - self.commit(current_snapshot); - if self.undo.len() < 2 { - return None; + /// To be called when a snapshot needs to be registered + pub fn commit(&mut self, snapshot: Snapshot) { + // Only commit when previous snapshot differs + let ptr = self.ptr.unwrap_or(0); + if self.history.get(ptr).map(|s| &s.content) != Some(&snapshot.content) { + self.clear_redo(); + self.history.push(snapshot); + self.ptr = Some(self.history.len().saturating_sub(1)); } - let snapshot_to_remove = self.undo.pop()?; - let snapshot_to_apply = self.undo.last()?.clone(); - self.redo.push(snapshot_to_remove); - - Some(snapshot_to_apply) } - /// Provide a snapshot of the desired state of the document for purposes of - /// redoing - pub fn redo(&mut self) -> Option { - let ev = self.redo.pop()?; - self.undo.push(ev.clone()); - Some(ev) + /// To be called when writing to disk + pub fn disk_write(&mut self, snapshot: &Snapshot) { + self.commit(snapshot.clone()); + self.on_disk = self.ptr; } - /// On file save, mark where the document is to match it on the disk - pub fn saved(&mut self) { - self.on_disk = self.undo.len(); + /// A way to query whether we're currently up to date with the disk + #[must_use] + pub fn with_disk(&self, snapshot: &Snapshot) -> bool { + if let Some(disk) = self.on_disk { + self.history.get(disk).map(|s| &s.content) == Some(&snapshot.content) + } else if self.history.is_empty() { + true + } else { + self.history.first().map(|s| &s.content) == Some(&snapshot.content) + } } - /// Determine if the state of the document is currently that of what is on the disk - #[must_use] - pub fn at_file(&self) -> bool { - self.undo.len() == self.on_disk + /// Get previous snapshot to restore to + pub fn undo(&mut self, snapshot: Snapshot) -> Option { + // Push cursor back by 1 + self.commit(snapshot); + if let Some(ptr) = self.ptr { + if ptr != 0 { + let new_ptr = ptr.saturating_sub(1); + self.ptr = Some(new_ptr); + self.history.get(new_ptr).cloned() + } else { + None + } + } else { + None + } } - /// Change the cursor position of the previous snapshot - pub fn backpatch_cursor(&mut self, cursor: &Cursor) { - if let Some(snapshot) = self.undo.last_mut() { - snapshot.cursor = *cursor; + /// Get snapshot that used to be in place + pub fn redo(&mut self, snapshot: &Snapshot) -> Option { + if let Some(ptr) = self.ptr { + // If the user has edited since the undo, wipe the redo stack + if self.history.get(ptr).map(|s| &s.content) != Some(&snapshot.content) { + self.clear_redo(); + } + // Perform the redo + let new_ptr = if ptr + 1 < self.history.len() { + ptr + 1 + } else { + return None; + }; + self.ptr = Some(new_ptr); + self.history.get(new_ptr).cloned() + } else { + None } } } diff --git a/src/config/interface.rs b/src/config/interface.rs index 43a1a303..de6e38c1 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -245,7 +245,11 @@ impl TabLine { let absolute_path = get_absolute_path(&path).unwrap_or_else(|| "[No Name]".to_string()); let file_name = get_file_name(&path).unwrap_or_else(|| "[No Name]".to_string()); let icon = file.file_type.clone().map_or("󰈙 ".to_string(), |t| t.icon); - let modified = if file.doc.info.modified { "[+]" } else { "" }; + let modified = if file.doc.event_mgmt.with_disk(&file.doc.take_snapshot()) { + "" + } else { + "[+]" + }; let mut result = self.format.clone(); result = result .replace("{file_extension}", &file_extension) @@ -333,10 +337,14 @@ impl StatusLine { .clone() .map_or("Unknown".to_string(), |t| t.name); let icon = file.file_type.clone().map_or("󰈙 ".to_string(), |t| t.icon); - let modified = if editor.doc().info.modified { - "[+]" - } else { + let modified = if editor + .doc() + .event_mgmt + .with_disk(&editor.doc().take_snapshot()) + { "" + } else { + "[+]" }; let cursor_y = (editor.doc().loc().y + 1).to_string(); let cursor_x = editor.doc().char_ptr.to_string(); diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 649683e5..7e66ff7c 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -8,8 +8,12 @@ use super::Editor; impl Editor { /// Execute an edit event pub fn exe(&mut self, ev: Event) -> Result<()> { - if !self.doc().undo_mgmt.last_event.same_type(&ev) && !self.plugin_active { - self.doc_mut().commit(); + if !self.plugin_active { + let same_type = self.doc().event_mgmt.last_event.as_ref(); + // As long as event isn't present and the same as this one, commit + if same_type.map(|e| e.same_type(&ev)) != Some(true) { + self.doc_mut().commit(); + } } self.doc_mut().exe(ev)?; Ok(()) @@ -63,7 +67,6 @@ impl Editor { if !self.doc().is_selection_empty() && !self.doc().info.read_only { // Removing a selection is significant and worth an undo commit self.doc_mut().commit(); - self.doc_mut().undo_mgmt.set_dirty(); self.doc_mut().remove_selection(); self.reload_highlight(); return Ok(()); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 6db24fad..7f4ca4b9 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -101,8 +101,6 @@ impl Editor { // Update in the syntax highlighter let mut highlighter = Highlighter::new(4); highlighter.run(&doc.lines); - // Mark as not saved on disk - doc.info.modified = true; // Add document to documents let file = FileContainer { highlighter, @@ -150,7 +148,6 @@ impl Editor { // Set up the document doc.set_tab_width(tab_width); doc.load_to(size.h); - doc.undo_mgmt.saved(); // Update in the syntax highlighter let mut highlighter = file_type.as_ref().map_or(Highlighter::new(tab_width), |t| { t.get_highlighter(&self.config, tab_width) @@ -197,7 +194,6 @@ impl Editor { .file_types .identify(&mut file.doc); // Set up the document - file.doc.info.modified = true; file.doc.set_tab_width(tab_width); // Attach the correct highlighter let highlighter = file_type.clone().map_or(Highlighter::new(tab_width), |t| { @@ -229,8 +225,6 @@ impl Editor { /// save the document to the disk pub fn save(&mut self) -> Result<()> { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); // Perform the save self.doc_mut().save()?; // All done @@ -259,7 +253,6 @@ impl Editor { file.highlighter = highlighter; file.highlighter.run(&file.doc.lines); file.doc.file_name = Some(file_name.clone()); - file.doc.info.modified = false; } // Commit events to event manager (for undo / redo) self.doc_mut().commit(); @@ -285,7 +278,7 @@ impl Editor { // If there are still documents open, only close the requested document if self.active { let msg = "This document isn't saved, press Ctrl + Q to force quit or Esc to cancel"; - if !self.doc().info.modified || self.confirm(msg)? { + if !self.doc().event_mgmt.with_disk(&self.doc().take_snapshot()) || self.confirm(msg)? { self.files.remove(self.ptr); self.prev(); } From cbee9fa89de1d3f5887d0166296c11007b35b50b Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:21:29 +0000 Subject: [PATCH 21/73] Updated tests to new undo/redo api --- kaolinite/tests/test.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/kaolinite/tests/test.rs b/kaolinite/tests/test.rs index 83b316f2..2156da31 100644 --- a/kaolinite/tests/test.rs +++ b/kaolinite/tests/test.rs @@ -402,21 +402,19 @@ fn document_deletion() { fn document_undo_redo() { let mut doc = Document::open(Size::is(100, 10), "tests/data/unicode.txt").unwrap(); doc.load_to(100); - assert!(doc.undo_mgmt.undo(doc.take_snapshot()).is_none()); + assert!(doc.event_mgmt.undo(doc.take_snapshot()).is_none()); assert!(doc.redo().is_ok()); - assert!(!doc.info.modified); + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); doc.exe(Event::InsertLine(0, st!("hello你bye好hello"))); doc.exe(Event::Delete(Loc { x: 0, y: 2 }, st!("\t"))); doc.exe(Event::Insert(Loc { x: 3, y: 2 }, st!("a"))); doc.commit(); - assert!(doc.info.modified); + assert!(!doc.event_mgmt.with_disk(&doc.take_snapshot())); assert!(doc.undo().is_ok()); - assert!(!doc.info.modified); assert_eq!(doc.line(0), Some(st!(" 你好"))); assert_eq!(doc.line(1), Some(st!("\thello"))); assert_eq!(doc.line(2), Some(st!(" hello"))); assert!(doc.redo().is_ok()); - assert!(doc.info.modified); assert_eq!(doc.line(0), Some(st!("hello你bye好hello"))); assert_eq!(doc.line(2), Some(st!("helalo"))); } From 7e3727fdeb9cd2f29a2e2d73c82677d378f83c81 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:28:26 +0000 Subject: [PATCH 22/73] Fixed issues with blank files being unmodified and haivng to force quit all the time --- kaolinite/src/event.rs | 7 ++++++- src/editor/mod.rs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/kaolinite/src/event.rs b/kaolinite/src/event.rs index 01045d7c..a2e895ff 100644 --- a/kaolinite/src/event.rs +++ b/kaolinite/src/event.rs @@ -100,6 +100,8 @@ pub struct EventMgmt { pub on_disk: Option, /// Store the last event to occur (so that we can see if there is a change) pub last_event: Option, + /// Flag to force the file not to be with disk (i.e. file only exists in memory) + pub force_not_with_disk: bool, } impl Document { @@ -141,6 +143,7 @@ impl EventMgmt { /// To be called when writing to disk pub fn disk_write(&mut self, snapshot: &Snapshot) { + self.force_not_with_disk = false; self.commit(snapshot.clone()); self.on_disk = self.ptr; } @@ -148,7 +151,9 @@ impl EventMgmt { /// A way to query whether we're currently up to date with the disk #[must_use] pub fn with_disk(&self, snapshot: &Snapshot) -> bool { - if let Some(disk) = self.on_disk { + if self.force_not_with_disk { + false + } else if let Some(disk) = self.on_disk { self.history.get(disk).map(|s| &s.content) == Some(&snapshot.content) } else if self.history.is_empty() { true diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 7f4ca4b9..652d37cf 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -96,6 +96,7 @@ impl Editor { 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.event_mgmt.force_not_with_disk = true; // Load all the lines within viewport into the document doc.load_to(size.h); // Update in the syntax highlighter @@ -278,7 +279,7 @@ impl Editor { // If there are still documents open, only close the requested document if self.active { let msg = "This document isn't saved, press Ctrl + Q to force quit or Esc to cancel"; - if !self.doc().event_mgmt.with_disk(&self.doc().take_snapshot()) || self.confirm(msg)? { + if self.doc().event_mgmt.with_disk(&self.doc().take_snapshot()) || self.confirm(msg)? { self.files.remove(self.ptr); self.prev(); } From f23531b1285e6d1543f3018c14bb4a62d5354e4f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:32:32 +0000 Subject: [PATCH 23/73] Autoindent now cursor snaps to prevent awkward cursor movement --- plugins/autoindent.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index 0d4b0bd3..62416de8 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.11 +Auto Indent v0.12 Helps you when programming by guessing where indentation should go and then automatically applying these guesses as you program @@ -114,6 +114,7 @@ function autoindent:set_indent(y, new_indent) -- Place the cursor at a sensible position if x < 0 then x = 0 end editor:move_to(x, y) + editor:cursor_snap() end -- Get how indented a line is at a certain y index From d4863ab766bd6339d7a7a623e055f482019daa3c Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:35:38 +0000 Subject: [PATCH 24/73] Improved interaction with event stack amongst plug-ins and default config and fixed rehighlighting issues --- config/.oxrc | 2 ++ plugins/quickcomment.lua | 1 + src/config/editor.rs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/config/.oxrc b/config/.oxrc index c8e1cdd9..e4f43655 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -220,6 +220,7 @@ event_mapping = { local cursor = editor.cursor local select = editor.selection local single = select.x == cursor.x and select.y == cursor.y + editor:commit() if single then -- move single line editor:move_line_up() @@ -245,6 +246,7 @@ event_mapping = { local cursor = editor.cursor local select = editor.selection local single = select.x == cursor.x and select.y == cursor.y + editor:commit() if single then -- move single line editor:move_line_down() diff --git a/plugins/quickcomment.lua b/plugins/quickcomment.lua index 6f64d202..04577d72 100644 --- a/plugins/quickcomment.lua +++ b/plugins/quickcomment.lua @@ -67,6 +67,7 @@ function quickcomment:comment_start() end event_mapping["alt_c"] = function() + editor:commit() if quickcomment:is_commented(editor.cursor.y) then quickcomment:uncomment(editor.cursor.y) else diff --git a/src/config/editor.rs b/src/config/editor.rs index 0a12d501..8042c0fb 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -173,6 +173,8 @@ impl LuaUserData for Editor { }); methods.add_method_mut("remove_word", |_, editor, ()| { let _ = editor.doc_mut().delete_word(); + editor.update_highlighter(); + editor.hl_edit(editor.doc().loc().y); Ok(()) }); // Cursor moving From 60b6a54e836211c232acfa27d918cb06e5801ed8 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:40:57 +0000 Subject: [PATCH 25/73] Live HTML can now track alternative files --- plugins/live_html.lua | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/plugins/live_html.lua b/plugins/live_html.lua index 53e392b2..8966a756 100644 --- a/plugins/live_html.lua +++ b/plugins/live_html.lua @@ -8,7 +8,9 @@ live_html = { has_python = python_interop:installation() ~= nil, has_flask_module = python_interop:has_module("flask"), entry_point = nil, + tracking = {}, pid = nil, + last_request = "" } function live_html:ready() @@ -31,9 +33,19 @@ function live_html:stop() end function live_html_refresh() + local tracked_file_changed = false + for _, v in ipairs(live_html.tracking) do + if v == editor.file_name then + tracked_file_changed = true + break + end + end if editor.file_path == live_html.entry_point then local contents = editor:get():gsub('"', '\\"'):gsub("\n", ""):gsub("`", "\\`") + live_html.last_request = contents http.post("localhost:5000/update", contents) + elseif tracked_file_changed then + http.post("localhost:5000/forceupdate", live_html.last_request) end end @@ -48,6 +60,10 @@ commands["html"] = function(args) after(5, "live_html_refresh") elseif args[1] == "stop" then live_html:stop() + elseif args[1] == "track" then + local file = args[2] + table.insert(live_html.tracking, file) + editor:display_info("Now tracking file " .. file) end else editor:display_error("Live HTML: python or flask module not found") @@ -166,6 +182,17 @@ def update_html(): # Return a 200 status on successful update return "Update successful", 200 +@app.route('/forceupdate', methods=['POST']) +def force_update_html(): + global html_content + # Get the new HTML content from the POST request + new_code = request.get_data().decode('utf-8') + # Update the HTML content with the new code + html_content = new_code + notify_clients() # Notify all clients to reload + # Return a 200 status on successful update + return "Update successful", 200 + @app.route('/reload') def reload(): def stream(): From 594fad8dc63faca3ffcd44cae67958b779032aff Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:46:51 +0000 Subject: [PATCH 26/73] Live HTML can now refresh on save (new default) --- plugins/live_html.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/live_html.lua b/plugins/live_html.lua index 8966a756..c053ad82 100644 --- a/plugins/live_html.lua +++ b/plugins/live_html.lua @@ -10,7 +10,8 @@ live_html = { entry_point = nil, tracking = {}, pid = nil, - last_request = "" + last_request = "", + refresh_when = (live_html or { refresh_when = "save" }).refresh_when, } function live_html:ready() @@ -71,7 +72,13 @@ commands["html"] = function(args) end event_mapping["*"] = function() - if live_html.pid ~= nil then + if live_html.pid ~= nil and live_html.refresh_when == "keypress" then + after(1, "live_html_refresh") + end +end + +event_mapping["ctrl_s"] = function() + if live_html.pid ~= nil and live_html.refresh_when == "save" then after(1, "live_html_refresh") end end From 65653758cdba81ce4195aca65ed2ad8c01c18116 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:44:07 +0000 Subject: [PATCH 27/73] Added plug-in update command --- src/plugin/plugin_manager.lua | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index 2ecd6e61..563df23a 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -218,6 +218,47 @@ function plugin_manager:remove_from_config(plugin) return nil end +-- Find the local version of a plug-in that is installed +function plugin_manager:local_version(plugin) + -- Open the file + local file = io.open(plugin_path .. "/" .. plugin .. ".lua", "r") + if not file then return nil end + -- Attempt to find a version indicator in the first 10 lines of the file + local version = nil + for i = 1, 10 do + -- Read the line + local line = file:read("*line") + if not line then break end + -- See if there is a match + local match = line:match("(v%d+%.%d+)") + if match then + version = match + break + end + end + file:close() + return version +end + +-- Find the latest online version of a plug-in +function plugin_manager:latest_version(plugin) + -- Download the plug-in's source + local url = "https://raw.githubusercontent.com/curlpipe/ox/refs/heads/master/plugins/" .. plugin .. ".lua" + local resp = http.get(url) + if resp == "404: Not Found" then return nil end + -- Attempt to find a version indicator in the first 10 lines of the file + local version = nil + for line in resp:gmatch("[^\r\n]+") do + -- See if there is a match + local match = line:match("(v%d+%.%d+)") + if match then + version = match + break + end + end + return version +end + commands["plugin"] = function(arguments) if arguments[1] == "install" then local result = plugin_manager:install(arguments[2]) @@ -228,5 +269,41 @@ commands["plugin"] = function(arguments) plugin_manager:uninstall(arguments[2]) elseif arguments[1] == "status" then plugin_manager:status() + elseif arguments[1] == "update" then + -- editor:display_info(tostring(local_copy) .. " locally vs " .. tostring(latest_copy) .. " latest") + editor:display_info("Please wait whilst versions are checked...") + editor:rerender_feedback_line() + local outdated = {} + for _, plugin in ipairs(plugins) do + local name = plugin:match("([^/]+)%.lua$") + local local_copy = plugin_manager:local_version(name) + local latest_copy = plugin_manager:latest_version(name) + if local_copy ~= latest_copy then + table.insert(outdated, {name, local_copy, latest_copy}) + end + end + for _, data in ipairs(outdated) do + local name = data[1] + local local_copy = data[2] + local latest_copy = data[3] + local response = editor:prompt( + string.format( + "%s needs an update: you have %s, latest is %s, update plugin? (y/n)", + name, + local_copy, + latest_copy + ) + ) + if response == "y" then + editor:display_info("Updating " .. name .. ", please wait...") + editor:rerender_feedback_line() + local result = plugin_manager:download_plugin(name) + if result ~= nil then + editor:display_error("Failed to download plug-in: " .. result) + return + end + end + end + editor:display_info("Update check-up completed, you're all set") end end From 89e5a54bfcee69b6e4a7f3024d7f084f21cbd7ab Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:48:49 +0000 Subject: [PATCH 28/73] Added some adjustments to allow basic Ox editing to work normally on Windows --- src/main.rs | 51 ++++++++++++++++++++++++++++++++------------------- src/ui.rs | 10 ++++++---- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8fb02917..cfdad28d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use config::{ get_listeners, key_to_string, run_key, run_key_before, Assistant, Config, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN, }; -use crossterm::event::Event as CEvent; +use crossterm::event::{Event as CEvent, KeyEvent, KeyEventKind}; use editor::{Editor, FileTypes}; use error::{OxError, Result}; use kaolinite::event::{Error as KError, Event}; @@ -161,31 +161,44 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // 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)?; - - // 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")); + // 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, + } } - let event = crossterm::event::read()?; + // Clear feedback editor.borrow_mut().feedback = Feedback::None; // Handle plug-in before key press mappings diff --git a/src/ui.rs b/src/ui.rs index 456402cb..11290181 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -141,10 +141,12 @@ impl Terminal { execute!(self.stdout, EnableMouseCapture)?; } terminal::enable_raw_mode()?; - execute!( - self.stdout, - PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) - )?; + if cfg!(not(target_os = "windows")) { + execute!( + self.stdout, + PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) + )?; + } Ok(()) } From a62719d887baa1a420c5313794c69d06ab8ac710 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:12:10 +0000 Subject: [PATCH 29/73] Globally ignore all key release events --- src/editor/interface.rs | 16 ++++++++-------- src/editor/scanning.rs | 16 ++++++++-------- src/main.rs | 11 +++++++++-- src/ui.rs | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index a707e866..f49fbf25 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,9 +1,9 @@ use crate::error::{OxError, Result}; -use crate::ui::{size, Feedback}; +use crate::ui::{key_event, size, Feedback}; /// Functions for rendering the UI use crate::{display, handle_lua_error}; use crossterm::{ - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, + event::{read, KeyCode as KCode, KeyModifiers as KMod}, queue, style::{ Attribute, Color, Print, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg, @@ -316,8 +316,8 @@ impl Editor { self.terminal.goto(prompt.len() + input.len() + 2, h)?; self.terminal.flush()?; // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { + if let Some((modifiers, code)) = key_event(&read()?) { + match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, // Cancel operation @@ -394,8 +394,8 @@ impl Editor { self.terminal.goto(6 + width(&input, tab_width), h)?; self.terminal.flush()?; // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { + if let Some((modifiers, code)) = key_event(&read()?) { + match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, // Cancel when escape key is pressed @@ -443,8 +443,8 @@ impl Editor { self.render_feedback_line(w, h)?; self.terminal.flush()?; // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { + if let Some((modifiers, code)) = key_event(&read()?) { + match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Esc) => { done = true; diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 3768c410..ce4f35a4 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -1,9 +1,9 @@ use crate::display; /// Functions for searching and replacing use crate::error::{OxError, Result}; -use crate::ui::size; +use crate::ui::{key_event, size}; use crossterm::{ - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, + event::{read, KeyCode as KCode, KeyModifiers as KMod}, queue, style::{Attribute, Print, SetAttribute, SetBackgroundColor as Bg}, }; @@ -44,8 +44,8 @@ impl Editor { self.terminal.hide_cursor()?; } self.terminal.flush()?; - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { + if let Some((modifiers, code)) = key_event(&read()?) { + match (modifiers, code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, // Cancel operation @@ -95,8 +95,8 @@ impl Editor { } self.terminal.flush()?; // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { + if let Some((modifiers, code)) = key_event(&read()?) { + match (modifiers, code) { // On return or escape key, exit menu (KMod::NONE, KCode::Enter) => done = true, (KMod::NONE, KCode::Esc) => { @@ -189,8 +189,8 @@ impl Editor { } self.terminal.flush()?; // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { + if let Some((modifiers, code)) = key_event(&read()?) { + match (modifiers, code) { // On escape key, exit (KMod::NONE, KCode::Esc) => done = true, // On right key, move to the previous match, keeping note of what that match is diff --git a/src/main.rs b/src/main.rs index cfdad28d..12a05f87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,7 +179,11 @@ fn run(cli: &CommandLineInterface) -> Result<()> { 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); + handle_lua_error( + "task", + target.call(()), + &mut editor.borrow_mut().feedback, + ); } else { editor.borrow_mut().feedback = Feedback::Warning(format!("Function '{task}' was not found")); @@ -193,7 +197,10 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // 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, .. }) => (), + CEvent::Key(KeyEvent { + kind: KeyEventKind::Release, + .. + }) => (), _ => break, } } diff --git a/src/ui.rs b/src/ui.rs index 11290181..78aede1f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -6,6 +6,7 @@ use crossterm::{ cursor::{Hide, MoveTo, Show}, event::{ DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, + Event as CEvent, KeyCode as KCode, KeyEvent, KeyEventKind, KeyModifiers as KMod, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }, execute, queue, @@ -52,6 +53,21 @@ pub fn fatal_error(msg: &str) { std::process::exit(1); } +/// Shorthand to read key events +pub fn key_event(kev: &CEvent) -> Option<(KMod, KCode)> { + if let CEvent::Key(KeyEvent { + modifiers, + code, + kind: KeyEventKind::Press, + .. + }) = kev + { + Some((*modifiers, *code)) + } else { + None + } +} + /// Represents different status messages #[derive(Debug)] pub enum Feedback { From 46ffe2503e7b2e6121e77c19874e3921ca9b5d9c Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:30:10 +0000 Subject: [PATCH 30/73] Greeting message does not hide on resize --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 12a05f87..1a20010e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -254,7 +254,9 @@ fn run(cli: &CommandLineInterface) -> Result<()> { } editor.borrow_mut().update_highlighter(); - editor.borrow_mut().greet = false; + if !matches!(event, CEvent::Resize(_, _)) { + editor.borrow_mut().greet = false; + } // Check for any commands to run let command = editor.borrow().command.clone(); From 308cd013fa2478f6adfda100664aaa538f56c168 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:39:23 +0000 Subject: [PATCH 31/73] Changed around networking on windows to use curl --- src/plugin/networking.lua | 47 +++++---------------------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/src/plugin/networking.lua b/src/plugin/networking.lua index cc8f09fd..b959cf97 100644 --- a/src/plugin/networking.lua +++ b/src/plugin/networking.lua @@ -1,8 +1,8 @@ -- Networking library (for plug-ins to use) --- Uses curl on unix based systems and powershell on windows +-- Uses curl http = { - backend = package.config:sub(1,1) == '\\' and 'powershell' or 'curl', + backend = "curl", } local function execute(cmd) @@ -13,56 +13,21 @@ local function execute(cmd) end function http.get(url) - -- Using curl for the request - local cmd - if http.backend == 'curl' then - cmd = "curl -s -X GET '" .. url .. "'" - else - cmd = table.concat({ - 'powershell -Command "Invoke-WebRequest -Uri \'', url, - '\' -UseBasicParsing | Select-Object -ExpandProperty Content"' - }) - end + local cmd = "curl -s -X GET '" .. url .. "'" return execute(cmd) end function http.post(url, data) - local cmd - if http.backend == 'curl' then - cmd = "curl -s -X POST -d \"" .. data .. "\" '" .. url .. "'" - else - cmd = table.concat({ - 'powershell -Command "Invoke-WebRequest -Uri \'', url, - '\' -Method POST -Body \'', data, - '\' -UseBasicParsing | Select-Object -ExpandProperty Content"' - }) - end + local cmd = "curl -s -X POST -d \"" .. data .. "\" '" .. url .. "'" return execute(cmd) end function http.put(url, data) - local cmd - if http.backend == 'curl' then - cmd = "curl -s -X PUT -d '" .. data .. "' '" .. url .. "'" - else - cmd = table.concat({ - 'powershell -Command "Invoke-WebRequest -Uri \'', url, - '\' -Method PUT -Body \'', data, - '\' -UseBasicParsing | Select-Object -ExpandProperty Content"' - }) - end + local cmd = "curl -s -X PUT -d '" .. data .. "' '" .. url .. "'" return execute(cmd) end function http.delete(url) - local cmd - if http.backend == 'curl' then - cmd = "curl -s -X DELETE '" .. url .. "'" - else - cmd = table.concat({ - 'powershell -Command "Invoke-WebRequest -Uri \'', url, - '\' -Method DELETE -UseBasicParsing | Select-Object -ExpandProperty Content"' - }) - end + local cmd = "curl -s -X DELETE '" .. url .. "'" return execute(cmd) end From 7e4cf3d63edf69d23e87f51463a8f6cdadad3f77 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:56:25 +0000 Subject: [PATCH 32/73] Adjusted plug-in APIs to work better on windows --- src/plugin/bootstrap.lua | 4 ++-- src/plugin/networking.lua | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index a741d871..5eccdd76 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -2,7 +2,7 @@ home = os.getenv("HOME") or os.getenv("USERPROFILE") if package.config:sub(1,1) == "\\" then - plugin_path = home .. "/ox" + plugin_path = home .. "\\ox" else plugin_path = home .. "/.config/ox" end @@ -24,7 +24,7 @@ plugin_issues = false function load_plugin(base) path_cross = base path_unix = home .. "/.config/ox/" .. base - path_win = home .. "/ox/" .. base + path_win = home .. "\\ox\\" .. base if file_exists(path_cross) then path = path_cross elseif file_exists(path_unix) then diff --git a/src/plugin/networking.lua b/src/plugin/networking.lua index b959cf97..a9a49d3c 100644 --- a/src/plugin/networking.lua +++ b/src/plugin/networking.lua @@ -13,21 +13,21 @@ local function execute(cmd) end function http.get(url) - local cmd = "curl -s -X GET '" .. url .. "'" + local cmd = 'curl -s -X GET "' .. url .. '"' return execute(cmd) end function http.post(url, data) - local cmd = "curl -s -X POST -d \"" .. data .. "\" '" .. url .. "'" + local cmd = 'curl -s -X POST -d "' .. data .. '"' .. url .. '"' return execute(cmd) end function http.put(url, data) - local cmd = "curl -s -X PUT -d '" .. data .. "' '" .. url .. "'" + local cmd = 'curl -s -X PUT -d "' .. data .. '" "' .. url .. '"' return execute(cmd) end function http.delete(url) - local cmd = "curl -s -X DELETE '" .. url .. "'" + local cmd = 'curl -s -X DELETE "' .. url .. '"' return execute(cmd) end From 0b42f854c4171981e55c58cdb82e72191ba8442f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:09:11 +0000 Subject: [PATCH 33/73] Attempted to modify shell API to work on windows --- src/plugin/bootstrap.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 5eccdd76..8e06f783 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -94,11 +94,17 @@ function python_interop:has_module(module_name) end -- Command line interaction -shell = {} +shell = { + is_windows = os.getenv("OS") and os.getenv("OS"):find("Windows") ~= nil, +} function shell:run(cmd) -- Runs a command (silently) and return the exit code - return select(3, os.execute(cmd .. " > /dev/null 2>&1")) + if self.is_windows then + return select(3, os.execute(cmd .. " > NUL 2>&1")) + else + return select(3, os.execute(cmd .. " > /dev/null 2>&1")) + end end function shell:output(cmd) @@ -113,7 +119,12 @@ end function shell:spawn(cmd) -- Spawns a command (silently), and have it run in the background -- Returns PID so process can be killed later - local command = cmd .. " > /dev/null 2>&1 & echo $!" + local command + if self.is_windows then + command = cmd .. " > NUL 2>&1 & echo $!" + else + command = cmd .. " > /dev/null 2>&1 & echo $!" + end local pid = shell:output(command) pid = pid:gsub("%s+", "") pid = pid:gsub("\\n", "") From 158052d72cdbf3f82af56ed772950185608e3268 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:39:00 +0000 Subject: [PATCH 34/73] Attempted to update shell:spawn and shell:kill to work on windows --- src/plugin/bootstrap.lua | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 8e06f783..e3724983 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -119,17 +119,37 @@ end function shell:spawn(cmd) -- Spawns a command (silently), and have it run in the background -- Returns PID so process can be killed later - local command if self.is_windows then - command = cmd .. " > NUL 2>&1 & echo $!" + local command = cmd .. " > /dev/null 2>&1 & echo $!" + local pid = shell:output(command) + pid = pid:gsub("%s+", "") + pid = pid:gsub("\\n", "") + pid = pid:gsub("\\t", "") + return pid else - command = cmd .. " > /dev/null 2>&1 & echo $!" + -- Write the command to a batch file + local temp = os.tmpname() .. ".bat" + local handle = io.open(temp, "w") + if not handle then return nil end + handle:write(cmd .. " > NUL 2>&1") + handle:close() + -- Run the process + self:run("start /B cmd /C \"" .. temp .. "\"") + -- Find the PID of the latest program to be run + local tasks = self:output("tasklist /fo csv /nh") + local lastPID = nil + for line in tasks:gmatch("[^\r\n]+") do + local columns = {} + for column in line:gmatch('("[^"]*"|%S+)') do + table.insert(columns, column:gsub('"', '')) + end + if #columns > 1 and line:match('"tasklist%.exe"') == nil then + lastPID = columns[2] + end + end + -- Return the PID + return lastPID end - local pid = shell:output(command) - pid = pid:gsub("%s+", "") - pid = pid:gsub("\\n", "") - pid = pid:gsub("\\t", "") - return pid end function shell:kill(pid) From d29cd2572a2cea639e27229b1987eaa188845a6c Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:42:15 +0000 Subject: [PATCH 35/73] Fixed platform mismatch in shell API --- src/plugin/bootstrap.lua | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index e3724983..be3e7b28 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -120,13 +120,6 @@ function shell:spawn(cmd) -- Spawns a command (silently), and have it run in the background -- Returns PID so process can be killed later if self.is_windows then - local command = cmd .. " > /dev/null 2>&1 & echo $!" - local pid = shell:output(command) - pid = pid:gsub("%s+", "") - pid = pid:gsub("\\n", "") - pid = pid:gsub("\\t", "") - return pid - else -- Write the command to a batch file local temp = os.tmpname() .. ".bat" local handle = io.open(temp, "w") @@ -149,12 +142,23 @@ function shell:spawn(cmd) end -- Return the PID return lastPID + else + local command = cmd .. " > /dev/null 2>&1 & echo $!" + local pid = shell:output(command) + pid = pid:gsub("%s+", "") + pid = pid:gsub("\\n", "") + pid = pid:gsub("\\t", "") + return pid end end function shell:kill(pid) if pid ~= nil then - shell:run("kill " .. tostring(pid)) + if self.is_windows then + shell:run("kill " .. tostring(pid)) + else + shell:run("taskkill /PID " .. tostring(pid) .. " /F") + end end end From 43e6b18a61843ebe42471ec7b577112aa4015118 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:31:30 +0000 Subject: [PATCH 36/73] Unimplemented shell spawning on windows --- src/plugin/bootstrap.lua | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index be3e7b28..bcc69b5e 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -120,28 +120,9 @@ function shell:spawn(cmd) -- Spawns a command (silently), and have it run in the background -- Returns PID so process can be killed later if self.is_windows then - -- Write the command to a batch file - local temp = os.tmpname() .. ".bat" - local handle = io.open(temp, "w") - if not handle then return nil end - handle:write(cmd .. " > NUL 2>&1") - handle:close() - -- Run the process - self:run("start /B cmd /C \"" .. temp .. "\"") - -- Find the PID of the latest program to be run - local tasks = self:output("tasklist /fo csv /nh") - local lastPID = nil - for line in tasks:gmatch("[^\r\n]+") do - local columns = {} - for column in line:gmatch('("[^"]*"|%S+)') do - table.insert(columns, column:gsub('"', '')) - end - if #columns > 1 and line:match('"tasklist%.exe"') == nil then - lastPID = columns[2] - end - end - -- Return the PID - return lastPID + editor:display_error("Shell spawning is unavailable on Windows") + editor:rerender_feedback_line() + return nil else local command = cmd .. " > /dev/null 2>&1 & echo $!" local pid = shell:output(command) @@ -153,12 +134,12 @@ function shell:spawn(cmd) end function shell:kill(pid) - if pid ~= nil then - if self.is_windows then - shell:run("kill " .. tostring(pid)) - else - shell:run("taskkill /PID " .. tostring(pid) .. " /F") - end + if self.is_windows then + editor:display_error("Shell spawning is unavailable on Windows") + editor:rerender_feedback_line() + return nil + elseif pid ~= nil then + shell:run("kill " .. tostring(pid)) end end From 82492d5fe1c39c96c7ea92c0542bf8f85e5c0cbe Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:41:57 +0000 Subject: [PATCH 37/73] Made it so the plug-in directory is created if it isn't already created --- src/plugin/bootstrap.lua | 14 ++++++++++++++ src/plugin/plugin_manager.lua | 9 +++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index bcc69b5e..ce7fc570 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -17,6 +17,20 @@ function file_exists(file_path) end end +function dir_exists(dir_path) + -- Check if the directory exists using the appropriate command + local is_windows = package.config:sub(1, 1) == '\\' -- Check if Windows + local command + if is_windows then + command = "if exist \"" .. dir_path .. "\" (exit 0) else (exit 1)" + else + command = "if [ -d \"" .. dir_path .. "\" ]; then exit 0; else exit 1; fi" + end + -- Execute the command + local result = shell:run(command) + return result == 0 +end + plugins = {} builtins = {} plugin_issues = false diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index 563df23a..9802e3ce 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -133,8 +133,13 @@ function plugin_manager:download_plugin(plugin) return "Plug-in not found in repository" end -- Find the path to download it to - local path = package.config:sub(1,1) == '\\' and home .. "/ox" or home .. "/.config/ox" - path = path .. "/" .. plugin .. ".lua" + local path = plugin_path .. "/" .. plugin .. ".lua" + -- Create the plug-in directory if it doesn't already exist + if not dir_exists(plugin_path) then + if shell:run("mkdir " .. plugin_path) ~= 0 then + return "Failed to make directory at " .. plugin_path + end + end -- Write it to a file file = io.open(path, "w") if not file then From 038a9fd5e8d9fd7df8695352ef6c6517730ee412 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:44:37 +0000 Subject: [PATCH 38/73] Prevented strange terminal hijacking in windows --- src/plugin/plugin_manager.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index 9802e3ce..e9bf6ea4 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -55,6 +55,7 @@ function plugin_manager:install(plugin) end -- Reload configuration file and plugins just to be safe editor:reload_plugins() + editor:reset_terminal() editor:display_info("Plugin was installed successfully") return true end @@ -85,6 +86,7 @@ function plugin_manager:uninstall(plugin) end -- Reload configuration file and plugins just to be safe editor:reload_plugins() + editor:reset_terminal() editor:display_info("Plugin was uninstalled successfully") end From b39a325abc028bdb4918886100534b89f32734af Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:04:26 +0000 Subject: [PATCH 39/73] Used correct path separation --- src/plugin/bootstrap.lua | 3 ++- src/plugin/plugin_manager.lua | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index ce7fc570..79f925cb 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -1,7 +1,8 @@ -- Bootstrap code provides plug-ins and configuration with APIs and other utilities home = os.getenv("HOME") or os.getenv("USERPROFILE") +path_sep = package.config:sub(1,1) -if package.config:sub(1,1) == "\\" then +if path_sep == "\\" then plugin_path = home .. "\\ox" else plugin_path = home .. "/.config/ox" diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index e9bf6ea4..3df2b8e3 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -119,7 +119,7 @@ function plugin_manager:plugin_downloaded(plugin) local base = plugin .. ".lua" local path_cross = base local path_unix = home .. "/.config/ox/" .. base - local path_win = home .. "/ox/" .. base + local path_win = home .. "\\ox\\" .. base local installed = file_exists(path_cross) or file_exists(path_unix) or file_exists(path_win) -- Return true if plug-ins are built in local builtin = self:plugin_is_builtin(plugin) @@ -135,7 +135,7 @@ function plugin_manager:download_plugin(plugin) return "Plug-in not found in repository" end -- Find the path to download it to - local path = plugin_path .. "/" .. plugin .. ".lua" + local path = plugin_path .. path_sep .. plugin .. ".lua" -- Create the plug-in directory if it doesn't already exist if not dir_exists(plugin_path) then if shell:run("mkdir " .. plugin_path) ~= 0 then @@ -155,8 +155,8 @@ end -- Remove a plug-in from the configuration directory function plugin_manager:remove_plugin(plugin) -- Obtain the path - local path = package.config:sub(1,1) == '\\' and home .. "/ox" or home .. "/.config/ox" - path = path .. "/" .. plugin .. ".lua" + local path = path_sep == '\\' and home .. "\\ox" or home .. "/.config/ox" + path = path .. path_sep .. plugin .. ".lua" -- Remove the file local success, err = os.remove(path) if not success then @@ -169,7 +169,7 @@ end -- Verify whether the plug-in is being imported in the configuration file function plugin_manager:plugin_in_config(plugin) -- Find the configuration file path - local path = home .. "/.oxrc" + local path = home .. path_sep .. ".oxrc" -- Open the document local file = io.open(path, "r") if not file then return false end @@ -188,7 +188,7 @@ end -- Append the plug-in import code to the configuration file so it is loaded function plugin_manager:append_to_config(plugin) - local path = home .. "/.oxrc" + local path = home .. path_sep .. ".oxrc" local file = io.open(path, "a") if not file then return "Failed to open configuration file" @@ -201,7 +201,7 @@ end -- Remove plug-in import code from the configuration file function plugin_manager:remove_from_config(plugin) -- Find the configuration file path - local path = home .. "/.oxrc" + local path = home .. path_sep .. ".oxrc" -- Open the configuration file local file = io.open(path, "r") if not file then @@ -228,7 +228,7 @@ end -- Find the local version of a plug-in that is installed function plugin_manager:local_version(plugin) -- Open the file - local file = io.open(plugin_path .. "/" .. plugin .. ".lua", "r") + local file = io.open(plugin_path .. path_sep .. plugin .. ".lua", "r") if not file then return nil end -- Attempt to find a version indicator in the first 10 lines of the file local version = nil From 546280e878366d29268d2a5f6e6f29b663dc9da2 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:13:00 +0000 Subject: [PATCH 40/73] Fixed issue with plug-ins not able to be loaded on Windows --- src/plugin/bootstrap.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 79f925cb..7b280abb 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -45,7 +45,7 @@ function load_plugin(base) elseif file_exists(path_unix) then path = path_unix elseif file_exists(path_win) then - path = file_win + path = path_win else path = nil -- Prevent warning if plug-in is built-in From c9eb381c2c44c55332dbabeb8108d1b8beced393 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:43:10 +0000 Subject: [PATCH 41/73] Added key normalisation across platforms --- src/config/keys.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/config/keys.rs b/src/config/keys.rs index 243ee129..b05278ec 100644 --- a/src/config/keys.rs +++ b/src/config/keys.rs @@ -48,6 +48,14 @@ pub fn get_listeners<'a>(name: &'a str, lua: &'a Lua) -> Result = "!\"£$%^&*(){}:@~<>?~|¬".chars().collect(); + for c in punctuation { + *code = code.replace(&format!("shift_{c}"), &c.to_string()); + } +} + /// Converts a key taken from a crossterm event into string format pub fn key_to_string(modifiers: KMod, key: KCode) -> String { let mut result = String::new(); @@ -123,5 +131,7 @@ pub fn key_to_string(modifiers: KMod, key: KCode) -> String { } .to_string(), }; + // Ensure consistent key codes across platforms + key_normalise(&mut result); result } From 20c6f8dddc633488e0f9447bd5d251312698a7b5 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:07:09 +0000 Subject: [PATCH 42/73] Enforced order of built in plug-ins --- src/config/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 57d4916b..93a3b101 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,7 +2,6 @@ use crate::editor::{FileType, FileTypes}; use crate::error::{OxError, Result}; use mlua::prelude::*; -use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::{ cell::RefCell, @@ -165,10 +164,11 @@ impl Config { } // Determine whether or not to load built-in plugins - let mut builtins: HashMap<&str, &str> = HashMap::default(); - builtins.insert("pairs.lua", PAIRS); - builtins.insert("autoindent.lua", AUTOINDENT); - builtins.insert("quickcomment.lua", QUICKCOMMENT); + let builtins: Vec<(&str, &str)> = vec![ + ("autoindent.lua", AUTOINDENT), + ("quickcomment.lua", QUICKCOMMENT), + ("pairs.lua", PAIRS), + ]; for (name, code) in &builtins { if Self::load_bi(name, user_provided_config, lua) { lua.load(*code).exec()?; From 1441b186690443202ff0b1bde8cb5fd5501f1b85 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:11:23 +0000 Subject: [PATCH 43/73] Made plugin updating system crossplatform --- src/plugin/plugin_manager.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index 3df2b8e3..77be85fd 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -119,7 +119,7 @@ function plugin_manager:plugin_downloaded(plugin) local base = plugin .. ".lua" local path_cross = base local path_unix = home .. "/.config/ox/" .. base - local path_win = home .. "\\ox\\" .. base + local path_win = home .. "/ox/" .. base local installed = file_exists(path_cross) or file_exists(path_unix) or file_exists(path_win) -- Return true if plug-ins are built in local builtin = self:plugin_is_builtin(plugin) @@ -135,7 +135,7 @@ function plugin_manager:download_plugin(plugin) return "Plug-in not found in repository" end -- Find the path to download it to - local path = plugin_path .. path_sep .. plugin .. ".lua" + local path = plugin_path .. "/" .. plugin .. ".lua" -- Create the plug-in directory if it doesn't already exist if not dir_exists(plugin_path) then if shell:run("mkdir " .. plugin_path) ~= 0 then @@ -155,8 +155,8 @@ end -- Remove a plug-in from the configuration directory function plugin_manager:remove_plugin(plugin) -- Obtain the path - local path = path_sep == '\\' and home .. "\\ox" or home .. "/.config/ox" - path = path .. path_sep .. plugin .. ".lua" + local path = package.config:sub(1,1) == '\\' and home .. "/ox" or home .. "/.config/ox" + path = path .. "/" .. plugin .. ".lua" -- Remove the file local success, err = os.remove(path) if not success then @@ -169,7 +169,7 @@ end -- Verify whether the plug-in is being imported in the configuration file function plugin_manager:plugin_in_config(plugin) -- Find the configuration file path - local path = home .. path_sep .. ".oxrc" + local path = home .. "/.oxrc" -- Open the document local file = io.open(path, "r") if not file then return false end @@ -188,7 +188,7 @@ end -- Append the plug-in import code to the configuration file so it is loaded function plugin_manager:append_to_config(plugin) - local path = home .. path_sep .. ".oxrc" + local path = home .. "/.oxrc" local file = io.open(path, "a") if not file then return "Failed to open configuration file" @@ -201,7 +201,7 @@ end -- Remove plug-in import code from the configuration file function plugin_manager:remove_from_config(plugin) -- Find the configuration file path - local path = home .. path_sep .. ".oxrc" + local path = home .. "/.oxrc" -- Open the configuration file local file = io.open(path, "r") if not file then @@ -282,7 +282,7 @@ commands["plugin"] = function(arguments) editor:rerender_feedback_line() local outdated = {} for _, plugin in ipairs(plugins) do - local name = plugin:match("([^/]+)%.lua$") + local name = plugin:match("([^/\\]+)%.lua$") local local_copy = plugin_manager:local_version(name) local latest_copy = plugin_manager:latest_version(name) if local_copy ~= latest_copy then From 93401b7aa65f7541739fedad32d8686f1c095f98 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:16:18 +0000 Subject: [PATCH 44/73] Don't clear feedback on resize --- src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1a20010e..a4a3e10b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,8 +205,11 @@ fn run(cli: &CommandLineInterface) -> Result<()> { } } - // Clear feedback - editor.borrow_mut().feedback = Feedback::None; + // 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; + } // Handle plug-in before key press mappings if let CEvent::Key(key) = event { @@ -254,9 +257,6 @@ fn run(cli: &CommandLineInterface) -> Result<()> { } editor.borrow_mut().update_highlighter(); - if !matches!(event, CEvent::Resize(_, _)) { - editor.borrow_mut().greet = false; - } // Check for any commands to run let command = editor.borrow().command.clone(); From ac6d79afa45068d035d254183fb8f625ad2d0f1d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:22:30 +0000 Subject: [PATCH 45/73] Fixed forced icons in git plug-in in configuration assistant --- src/config/assistant.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/assistant.rs b/src/config/assistant.rs index 264bd1c9..049fb5f3 100644 --- a/src/config/assistant.rs +++ b/src/config/assistant.rs @@ -768,7 +768,7 @@ impl Assistant { result += "\n-- Load Plug-Ins --\n"; for plugin in &self.plugins { result += &plugin.to_config(); - if plugin == &Plugin::Git { + if plugin == &Plugin::Git && self.icons { result += "git = { icons = true }\n"; } } From 13946cf9a4723a315ace2254873509e267e9e85a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:34:07 +0000 Subject: [PATCH 46/73] Modified git plugin to work better on windows --- plugins/git.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/git.lua b/plugins/git.lua index 8c3a68f4..05312b0e 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -1,5 +1,5 @@ --[[ -Git v0.4 +Git v0.5 A plug-in for git integration that provides features to: - Choose which files to add to a commit @@ -22,7 +22,7 @@ end function git:repo_path() local repo_path_output = shell:output("git rev-parse --show-toplevel") - return repo_path_output:gsub("[\r\n]+", "") + return repo_path_output:gsub("[\r\n]+", ""):gsub("/", path_sep) end function git:refresh_status() @@ -32,7 +32,7 @@ function git:refresh_status() for line in status_output:gmatch("[^\r\n]+") do local staged_status = line:sub(1, 1) local unstaged_status = line:sub(2, 2) - local file_name = repo_path .. "/" .. line:sub(4) + local file_name = repo_path .. path_sep .. line:sub(4) local staged local modified if self.icons then @@ -166,7 +166,7 @@ commands["git"] = function(args) elseif args[1] == "stat" then local stats = git:get_stats() for _, t in ipairs(stats.files) do - if repo_path .. "/" .. t.file == editor.file_path then + if repo_path .. path_sep .. t.file == editor.file_path then editor:display_info(string.format( "%s: %s insertions, %s deletions", t.file, t.insertions, t.deletions From 5c1bffa9f9a475f83c810e9607d0b5ce4d67ec17 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:59:37 +0000 Subject: [PATCH 47/73] Reduced git plug-in spam on windows --- plugins/git.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/git.lua b/plugins/git.lua index 05312b0e..1152f17d 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -22,11 +22,20 @@ end function git:repo_path() local repo_path_output = shell:output("git rev-parse --show-toplevel") - return repo_path_output:gsub("[\r\n]+", ""):gsub("/", path_sep) + repo_path_output = repo_path_output:gsub("[\r\n]+", ""):gsub("/", path_sep) + if repo_path_output:match("fatal: not a git repository") ~= nil then + return nil + else + return repo_path_output + end end function git:refresh_status() local repo_path = self:repo_path() + if repo_path == nil then + self.status = {} + return + end local status_output = shell:output("git status --porcelain") local status = {} for line in status_output:gmatch("[^\r\n]+") do From 8ff68f42687b7217160bf3b75b0fb0ed868e144f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 19:44:39 +0000 Subject: [PATCH 48/73] Better CWD handling utilities --- kaolinite/src/utils.rs | 5 ++++- src/config/editor.rs | 5 ++++- src/main.rs | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/kaolinite/src/utils.rs b/kaolinite/src/utils.rs index 7e9634dd..6eefda04 100644 --- a/kaolinite/src/utils.rs +++ b/kaolinite/src/utils.rs @@ -167,7 +167,10 @@ pub fn get_file_ext(path: &str) -> Option { #[must_use] #[cfg(not(tarpaulin_include))] pub fn get_cwd() -> Option { - Some(std::env::current_dir().ok()?.display().to_string()) + let mut cwd = std::env::current_dir().ok()?; + // Really hacky solution to clean up messy path + cwd.push(""); + Some(cwd.display().to_string()) } /// Will list a directory diff --git a/src/config/editor.rs b/src/config/editor.rs index 8042c0fb..84ccb408 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -3,7 +3,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; use crate::{PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; -use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; +use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name, get_cwd}; use kaolinite::{Loc, Size}; use mlua::prelude::*; @@ -81,6 +81,9 @@ impl LuaUserData for Editor { Ok(None) } }); + fields.add_field_method_get("cwd", |_, _| { + Ok(get_cwd()) + }); } #[allow(clippy::too_many_lines)] diff --git a/src/main.rs b/src/main.rs index a4a3e10b..bdaa9c88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use editor::{Editor, FileTypes}; use error::{OxError, Result}; use kaolinite::event::{Error as KError, Event}; use kaolinite::searching::Searcher; -use kaolinite::utils::file_or_dir; +use kaolinite::utils::{file_or_dir, get_cwd}; use kaolinite::Loc; use mlua::Error::{RuntimeError, SyntaxError}; use mlua::{FromLua, Lua, Value}; @@ -96,7 +96,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { editor.borrow_mut().config.document.borrow_mut().file_types = file_types; // Open files user has asked to open - let cwd = std::env::current_dir()?; + 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); From 585f5dacfb9f7e3bef1dfaf6bf416c27c95ae797 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:06:56 +0000 Subject: [PATCH 49/73] Undid changes to git plugin --- plugins/git.lua | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/plugins/git.lua b/plugins/git.lua index 1152f17d..8c3a68f4 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -1,5 +1,5 @@ --[[ -Git v0.5 +Git v0.4 A plug-in for git integration that provides features to: - Choose which files to add to a commit @@ -22,26 +22,17 @@ end function git:repo_path() local repo_path_output = shell:output("git rev-parse --show-toplevel") - repo_path_output = repo_path_output:gsub("[\r\n]+", ""):gsub("/", path_sep) - if repo_path_output:match("fatal: not a git repository") ~= nil then - return nil - else - return repo_path_output - end + return repo_path_output:gsub("[\r\n]+", "") end function git:refresh_status() local repo_path = self:repo_path() - if repo_path == nil then - self.status = {} - return - end local status_output = shell:output("git status --porcelain") local status = {} for line in status_output:gmatch("[^\r\n]+") do local staged_status = line:sub(1, 1) local unstaged_status = line:sub(2, 2) - local file_name = repo_path .. path_sep .. line:sub(4) + local file_name = repo_path .. "/" .. line:sub(4) local staged local modified if self.icons then @@ -175,7 +166,7 @@ commands["git"] = function(args) elseif args[1] == "stat" then local stats = git:get_stats() for _, t in ipairs(stats.files) do - if repo_path .. path_sep .. t.file == editor.file_path then + if repo_path .. "/" .. t.file == editor.file_path then editor:display_info(string.format( "%s: %s insertions, %s deletions", t.file, t.insertions, t.deletions From 948bf531a0e57bbba5c099aaa0b48ed7da859eb3 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:11:31 +0000 Subject: [PATCH 50/73] git plug-in is not supported on windows --- plugins/git.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/git.lua b/plugins/git.lua index 8c3a68f4..2e72160b 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -1,5 +1,5 @@ --[[ -Git v0.4 +Git v0.5 A plug-in for git integration that provides features to: - Choose which files to add to a commit @@ -14,6 +14,7 @@ git = { status = {}, icons = (git or { icons = false }).icons, has_git = shell:output("git --version"):find("git version"), + is_supported = path_sep ~= "\\", } function git:ready() @@ -26,6 +27,7 @@ function git:repo_path() end function git:refresh_status() + if not self.is_supported then return end local repo_path = self:repo_path() local status_output = shell:output("git status --porcelain") local status = {} @@ -87,12 +89,16 @@ function git:diff_all() end function git_branch() - git:refresh_status() - local branch = shell:output("git rev-parse --abbrev-ref HEAD") - if branch == "" or branch:match("fatal") then - return "N/A" + if git.is_supported then + git:refresh_status() + local branch = shell:output("git rev-parse --abbrev-ref HEAD") + if branch == "" or branch:match("fatal") then + return "N/A" + else + return branch:gsub("[\r\n]+", "") + end else - return branch:gsub("[\r\n]+", "") + return "Unsupported" end end @@ -124,6 +130,8 @@ commands["git"] = function(args) -- Check if git is installed if not git:ready() then editor:display_error("Git: git installation not found") + elseif not git.is_supported then + editor:display_error("Git plug-in is not supported on Windows") else local repo_path = git:repo_path() if args[1] == "commit" then From 8e83d46160efcc9d69a8cd970c55df9179c3583f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:21:23 +0000 Subject: [PATCH 51/73] More resilient editor API --- src/config/editor.rs | 94 ++++++++++++++++++++++++++------------------ src/editor/mod.rs | 5 +++ 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 84ccb408..8cbf83e3 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -3,7 +3,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; use crate::{PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; -use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name, get_cwd}; +use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; use kaolinite::{Loc, Size}; use mlua::prelude::*; @@ -81,9 +81,7 @@ impl LuaUserData for Editor { Ok(None) } }); - fields.add_field_method_get("cwd", |_, _| { - Ok(get_cwd()) - }); + fields.add_field_method_get("cwd", |_, _| Ok(get_cwd())); } #[allow(clippy::too_many_lines)] @@ -396,47 +394,65 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("get", |_, editor, ()| { - let lines = editor.doc().len_lines(); - editor.doc_mut().load_to(lines); - let contents = editor.doc().lines.join("\n"); - Ok(contents) + if let Some(doc) = editor.try_doc_mut() { + let lines = doc.len_lines(); + doc.load_to(lines); + let contents = doc.lines.join("\n"); + Ok(Some(contents)) + } else { + Ok(None) + } }); methods.add_method("get_character", |_, editor, ()| { - let loc = editor.doc().char_loc(); - let ch = editor - .doc() - .line(loc.y) - .unwrap_or_default() - .chars() - .nth(loc.x) - .map(|ch| ch.to_string()) - .unwrap_or_default(); - Ok(ch) + if let Some(doc) = editor.try_doc() { + let loc = doc.char_loc(); + let ch = doc + .line(loc.y) + .unwrap_or_default() + .chars() + .nth(loc.x) + .map(|ch| ch.to_string()) + .unwrap_or_default(); + Ok(Some(ch)) + } else { + Ok(None) + } }); methods.add_method_mut("get_character_at", |_, editor, (x, y): (usize, usize)| { - editor.doc_mut().load_to(y); - let y = y.saturating_sub(1); - let ch = editor - .doc() - .line(y) - .unwrap_or_default() - .chars() - .nth(x) - .map_or_else(String::new, |ch| ch.to_string()); - editor.update_highlighter(); - Ok(ch) + if let Some(doc) = editor.try_doc_mut() { + doc.load_to(y); + let y = y.saturating_sub(1); + let ch = doc + .line(y) + .unwrap_or_default() + .chars() + .nth(x) + .map_or_else(String::new, |ch| ch.to_string()); + editor.update_highlighter(); + Ok(Some(ch)) + } else { + Ok(None) + } }); methods.add_method("get_line", |_, editor, ()| { - let loc = editor.doc().char_loc(); - let line = editor.doc().line(loc.y).unwrap_or_default(); - Ok(line) + if let Some(doc) = editor.try_doc() { + let loc = doc.char_loc(); + let line = doc.line(loc.y).unwrap_or_default(); + Ok(Some(line)) + } else { + Ok(None) + } }); methods.add_method_mut("get_line_at", |_, editor, y: usize| { - editor.doc_mut().load_to(y); - let y = y.saturating_sub(1); - let line = editor.doc().line(y).unwrap_or_default(); - editor.update_highlighter(); - Ok(line) + if let Some(doc) = editor.try_doc_mut() { + doc.load_to(y); + let y = y.saturating_sub(1); + let line = doc.line(y).unwrap_or_default(); + editor.update_highlighter(); + Ok(Some(line)) + } else { + Ok(None) + } }); // Document management methods.add_method_mut("previous_tab", |_, editor, ()| { @@ -502,7 +518,9 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("commit", |_, editor, ()| { - editor.doc_mut().commit(); + if let Some(doc) = editor.try_doc_mut() { + editor.doc_mut().commit(); + } Ok(()) }); // Searching and replacing diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 652d37cf..90e9684c 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -319,6 +319,11 @@ impl Editor { self.files.get(self.ptr).map(|file| &file.doc) } + /// Try to get a document + pub fn try_doc_mut(&mut self) -> Option<&mut Document> { + self.files.get_mut(self.ptr).map(|file| &mut file.doc) + } + /// Returns a document at a certain index pub fn get_doc(&mut self, idx: usize) -> &mut Document { &mut self.files.get_mut(idx).unwrap().doc From e5bfa0af9c5a0cc5868263306d6a2b3d97d706f6 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:22:21 +0000 Subject: [PATCH 52/73] Fixed clippy warning --- src/config/editor.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 8cbf83e3..1aacae8b 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -3,7 +3,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; use crate::{PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; -use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; +use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name, get_cwd}; use kaolinite::{Loc, Size}; use mlua::prelude::*; @@ -81,7 +81,9 @@ impl LuaUserData for Editor { Ok(None) } }); - fields.add_field_method_get("cwd", |_, _| Ok(get_cwd())); + fields.add_field_method_get("cwd", |_, _| { + Ok(get_cwd()) + }); } #[allow(clippy::too_many_lines)] @@ -519,7 +521,7 @@ impl LuaUserData for Editor { }); methods.add_method_mut("commit", |_, editor, ()| { if let Some(doc) = editor.try_doc_mut() { - editor.doc_mut().commit(); + doc.commit(); } Ok(()) }); From de912b15cf2e2b7fe5202e701c87b7c316ba5035 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:33:23 +0000 Subject: [PATCH 53/73] more reinforcement of editor api --- src/config/editor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 1aacae8b..5692e646 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -584,8 +584,10 @@ impl LuaUserData for Editor { let _ = editor.render_feedback_line(w, h); // Apply render and restore cursor let max = editor.dent(); - if let Some(Loc { x, y }) = editor.doc().cursor_loc_in_screen() { - let _ = editor.terminal.goto(x + max, y + editor.push_down); + if let Some(doc) = editor.try_doc() { + if let Some(Loc { x, y }) = doc.cursor_loc_in_screen() { + let _ = editor.terminal.goto(x + max, y + editor.push_down); + } } let _ = editor.terminal.show_cursor(); let _ = editor.terminal.flush(); From e437a90cdacf6898818a22419326a91d3d85f945 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:35:21 +0000 Subject: [PATCH 54/73] even more reinforcement of editor api --- src/config/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 5692e646..e57b21c8 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -579,12 +579,12 @@ impl LuaUserData for Editor { methods.add_method_mut("rerender_feedback_line", |_, editor, ()| { // If you can't render the editor, you're pretty much done for anyway let Size { w, mut h } = crate::ui::size().unwrap_or(Size { w: 0, h: 0 }); + let max = editor.dent(); h = h.saturating_sub(1 + editor.push_down); let _ = editor.terminal.hide_cursor(); - let _ = editor.render_feedback_line(w, h); // Apply render and restore cursor - let max = editor.dent(); if let Some(doc) = editor.try_doc() { + let _ = editor.render_feedback_line(w, h); if let Some(Loc { x, y }) = doc.cursor_loc_in_screen() { let _ = editor.terminal.goto(x + max, y + editor.push_down); } From c45c556b0ce7d2938aa81bba9bb92ca9689f9c2e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:36:38 +0000 Subject: [PATCH 55/73] fixed compilation issues --- src/config/editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index e57b21c8..cdb7960c 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -583,8 +583,10 @@ impl LuaUserData for Editor { h = h.saturating_sub(1 + editor.push_down); let _ = editor.terminal.hide_cursor(); // Apply render and restore cursor - if let Some(doc) = editor.try_doc() { + if editor.try_doc().is_some() { let _ = editor.render_feedback_line(w, h); + } + if let Some(doc) = editor.try_doc() { if let Some(Loc { x, y }) = doc.cursor_loc_in_screen() { let _ = editor.terminal.goto(x + max, y + editor.push_down); } From 05b5b00493f92a546cbecabbbbedc3928fc93c22 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:37:51 +0000 Subject: [PATCH 56/73] fixed rerender feedback line panics --- src/config/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index cdb7960c..69b48702 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -579,7 +579,6 @@ impl LuaUserData for Editor { methods.add_method_mut("rerender_feedback_line", |_, editor, ()| { // If you can't render the editor, you're pretty much done for anyway let Size { w, mut h } = crate::ui::size().unwrap_or(Size { w: 0, h: 0 }); - let max = editor.dent(); h = h.saturating_sub(1 + editor.push_down); let _ = editor.terminal.hide_cursor(); // Apply render and restore cursor @@ -587,6 +586,7 @@ impl LuaUserData for Editor { let _ = editor.render_feedback_line(w, h); } if let Some(doc) = editor.try_doc() { + let max = editor.dent(); if let Some(Loc { x, y }) = doc.cursor_loc_in_screen() { let _ = editor.terminal.goto(x + max, y + editor.push_down); } From b5f1490db57319b720d424b561481a756889faa7 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:41:23 +0000 Subject: [PATCH 57/73] rustfmt --- src/config/editor.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 69b48702..78b8dd6c 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -3,7 +3,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; use crate::{PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; -use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name, get_cwd}; +use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; use kaolinite::{Loc, Size}; use mlua::prelude::*; @@ -81,9 +81,7 @@ impl LuaUserData for Editor { Ok(None) } }); - fields.add_field_method_get("cwd", |_, _| { - Ok(get_cwd()) - }); + fields.add_field_method_get("cwd", |_, _| Ok(get_cwd())); } #[allow(clippy::too_many_lines)] From 9f89b4b7e9c931d61bb066efacddd314d994679a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:01:23 +0000 Subject: [PATCH 58/73] Emmet plug-in now works on linux and allows whitespace --- plugins/emmet.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/emmet.lua b/plugins/emmet.lua index 7e83894d..2e71c32a 100644 --- a/plugins/emmet.lua +++ b/plugins/emmet.lua @@ -1,5 +1,5 @@ --[[ -Emmet v0.3 +Emmet v0.4 Implementation of Emmet for Ox for rapid web development ]]-- @@ -17,9 +17,10 @@ end function emmet:expand() -- Get the emmet code local unexpanded = editor:get_line() - unexpanded = unexpanded:gsub("%s+", "") + unexpanded = unexpanded:gsub("^%s+", "") + unexpanded = unexpanded:gsub("%s+$", "") -- Request the expanded equivalent - local command = string.format("python %s/oxemmet.py '%s'", plugin_path, unexpanded) + local command = string.format("python %s/oxemmet.py \"%s\"", plugin_path, unexpanded) local expanded = shell:output(command) expanded = expanded:gsub("\n$", "") -- Keep track of the level of indentation From 6818988ff4a19c902bd7e2007034adb9dac39678 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:10:24 +0000 Subject: [PATCH 59/73] Path prompt now works much better on Windows --- src/editor/interface.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index f49fbf25..75089269 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -339,14 +339,14 @@ impl Editor { /// Prompt for selecting a file #[allow(clippy::similar_names)] pub fn path_prompt(&mut self) -> Result { - let mut input = get_cwd().map(|s| s + "/").unwrap_or_default(); + let mut input = get_cwd().unwrap_or_default(); let mut offset = 0; let mut done = false; let mut old_suggestions = vec![]; // Enter into a menu that asks for a prompt while !done { // Find the suggested files and folders - let parent = if input.ends_with('/') { + let parent = if input.ends_with('/') || input.ends_with('\\') { input.to_string() } else { get_parent(&input).unwrap_or_default() @@ -409,7 +409,7 @@ impl Editor { // Autocomplete path (KMod::NONE, KCode::Right) => { if file_or_dir(&suggestion) == "directory" { - suggestion += "/"; + suggestion.push(std::path::MAIN_SEPARATOR); } input = suggestion; offset = 0; From ae523be542ad51f4d7fbeda0ffed174a70c7081d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:30:38 +0000 Subject: [PATCH 60/73] Prevented quote input issues --- src/config/keys.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/config/keys.rs b/src/config/keys.rs index b05278ec..dfd2af3f 100644 --- a/src/config/keys.rs +++ b/src/config/keys.rs @@ -4,7 +4,10 @@ use crossterm::event::{KeyCode as KCode, KeyModifiers as KMod, MediaKeyCode, Mod use mlua::prelude::*; /// This contains the code for running code after a key binding is pressed -pub fn run_key(key: &str) -> String { +pub fn run_key(mut key: &str) -> String { + if key == "\"" { + key = "\\\""; + } format!( " globalevent = (global_event_mapping[\"*\"] or {{}}) @@ -20,7 +23,10 @@ pub fn run_key(key: &str) -> String { } /// This contains the code for running code before a key binding is fully processed -pub fn run_key_before(key: &str) -> String { +pub fn run_key_before(mut key: &str) -> String { + if key == "\"" { + key = "\\\""; + } format!( " globalevent = (global_event_mapping[\"before:*\"] or {{}}) @@ -52,7 +58,11 @@ pub fn get_listeners<'a>(name: &'a str, lua: &'a Lua) -> Result = "!\"£$%^&*(){}:@~<>?~|¬".chars().collect(); for c in punctuation { - *code = code.replace(&format!("shift_{c}"), &c.to_string()); + if c == '"' { + *code = code.replace(&format!("shift_\\\""), &c.to_string()); + } else { + *code = code.replace(&format!("shift_{c}"), &c.to_string()); + } } } From 1308857cc8e65327d3938d0ca28b234cdf7ffba2 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:25:03 +0000 Subject: [PATCH 61/73] Re-enabled git plug-in on windows with fixes for paths --- kaolinite/src/utils.rs | 17 ++++++++++++----- plugins/git.lua | 18 +++++------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/kaolinite/src/utils.rs b/kaolinite/src/utils.rs index 6eefda04..f0003d0a 100644 --- a/kaolinite/src/utils.rs +++ b/kaolinite/src/utils.rs @@ -142,7 +142,11 @@ pub fn tab_boundaries_backward(line: &str, tab_width: usize) -> Vec { #[must_use] pub fn get_absolute_path(path: &str) -> Option { let abs = std::fs::canonicalize(path).ok()?; - Some(abs.to_string_lossy().to_string()) + let mut abs = abs.to_string_lossy().to_string(); + if abs.starts_with("\\\\?\\") { + abs = abs[4..].to_string(); + } + Some(abs) } /// Will get the file name from a file @@ -167,10 +171,13 @@ pub fn get_file_ext(path: &str) -> Option { #[must_use] #[cfg(not(tarpaulin_include))] pub fn get_cwd() -> Option { - let mut cwd = std::env::current_dir().ok()?; - // Really hacky solution to clean up messy path - cwd.push(""); - Some(cwd.display().to_string()) + let cwd = std::env::current_dir().ok()?; + let mut cwd = cwd.display().to_string(); + // Strip away annoying verbatim component + if cwd.starts_with("\\\\?\\") { + cwd = cwd[4..].to_string(); + } + Some(cwd) } /// Will list a directory diff --git a/plugins/git.lua b/plugins/git.lua index 2e72160b..1691cd66 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -14,7 +14,6 @@ git = { status = {}, icons = (git or { icons = false }).icons, has_git = shell:output("git --version"):find("git version"), - is_supported = path_sep ~= "\\", } function git:ready() @@ -27,7 +26,6 @@ function git:repo_path() end function git:refresh_status() - if not self.is_supported then return end local repo_path = self:repo_path() local status_output = shell:output("git status --porcelain") local status = {} @@ -89,16 +87,12 @@ function git:diff_all() end function git_branch() - if git.is_supported then - git:refresh_status() - local branch = shell:output("git rev-parse --abbrev-ref HEAD") - if branch == "" or branch:match("fatal") then - return "N/A" - else - return branch:gsub("[\r\n]+", "") - end + git:refresh_status() + local branch = shell:output("git rev-parse --abbrev-ref HEAD") + if branch == "" or branch:match("fatal") then + return "N/A" else - return "Unsupported" + return branch:gsub("[\r\n]+", "") end end @@ -130,8 +124,6 @@ commands["git"] = function(args) -- Check if git is installed if not git:ready() then editor:display_error("Git: git installation not found") - elseif not git.is_supported then - editor:display_error("Git plug-in is not supported on Windows") else local repo_path = git:repo_path() if args[1] == "commit" then From ec7f9411c96f608864d1d04eeb8086aaf9fb8c08 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:08:54 +0000 Subject: [PATCH 62/73] Fix various issues on linux side --- src/config/editor.rs | 7 ++++++- src/config/keys.rs | 2 +- src/editor/interface.rs | 10 +++++++++- src/plugin/networking.lua | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 78b8dd6c..55415e79 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -2,7 +2,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; -use crate::{PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; +use crate::{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::*; @@ -86,6 +86,11 @@ impl LuaUserData for Editor { #[allow(clippy::too_many_lines)] fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Debugging methods + methods.add_method_mut("panic", |_, _, msg: String| { + fatal_error(&msg); + Ok(()) + }); // Reload the configuration file methods.add_method_mut("reset_terminal", |_, editor, ()| { let _ = editor.terminal.start(); diff --git a/src/config/keys.rs b/src/config/keys.rs index dfd2af3f..8a29fb9e 100644 --- a/src/config/keys.rs +++ b/src/config/keys.rs @@ -59,7 +59,7 @@ pub fn key_normalise(code: &mut String) { let punctuation: Vec = "!\"£$%^&*(){}:@~<>?~|¬".chars().collect(); for c in punctuation { if c == '"' { - *code = code.replace(&format!("shift_\\\""), &c.to_string()); + *code = code.replace("shift_\\\"", &c.to_string()); } else { *code = code.replace(&format!("shift_{c}"), &c.to_string()); } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 75089269..99b08726 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -339,7 +339,15 @@ impl Editor { /// Prompt for selecting a file #[allow(clippy::similar_names)] pub fn path_prompt(&mut self) -> Result { - let mut input = get_cwd().unwrap_or_default(); + let mut input = get_cwd() + .map(|p| { + if p.ends_with(std::path::MAIN_SEPARATOR) { + p + } else { + p + std::path::MAIN_SEPARATOR_STR + } + }) + .unwrap_or_default(); let mut offset = 0; let mut done = false; let mut old_suggestions = vec![]; diff --git a/src/plugin/networking.lua b/src/plugin/networking.lua index a9a49d3c..0da08b7f 100644 --- a/src/plugin/networking.lua +++ b/src/plugin/networking.lua @@ -18,7 +18,7 @@ function http.get(url) end function http.post(url, data) - local cmd = 'curl -s -X POST -d "' .. data .. '"' .. url .. '"' + local cmd = 'curl -s -X POST -d "' .. data .. '" "' .. url .. '"' return execute(cmd) end From 131e170ecf5ebd9e3d8edbea0b09baa5c31b3b71 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:13:06 +0000 Subject: [PATCH 63/73] Changed readme wording for crossplatform support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ef8bbd1..81f0b2a7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ If you're looking for a text editor that... It runs in your terminal as a text-user-interface, just like vim, nano and micro, however, it is not based on any existing editors and has been built from the ground up. -It is mainly designed on linux systems, but macOS and Windows users (via WSL) are free to give it a go. Work is currently underway to get it working perfectly on all systems. +It works best on linux, but macOS and Windows are also supported. ## Selling Points From c30efade52ecf7df352b801cf6f2cf95c0d6bf4f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:28:12 +0000 Subject: [PATCH 64/73] More advanced build script including deb, rpm and binaries for unix/windows all in one --- build.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build.sh b/build.sh index a964e57f..9fec1233 100644 --- a/build.sh +++ b/build.sh @@ -1,4 +1,21 @@ +# Initial set-up +mkdir -p target/pkgs +rm target/pkgs/* + +# Build for Linux +## Binary cargo build --release strip -s target/release/ox +cp target/release/ox target/pkgs/ox +## RPM +rm target/generate-rpm/*.rpm cargo generate-rpm +cp target/generate-rpm/*.rpm target/pkgs/ +## DEB cargo deb +cp target/debian/*.deb target/pkgs/ + +# Build for Windows (binary) +cargo build --release --target x86_64-pc-windows-gnu +strip -s target/x86_64-pc-windows-gnu/release/ox.exe +cp target/x86_64-pc-windows-gnu/release/ox.exe target/pkgs/ox.exe From 894e0e78088f3f2ec81f01350cccc0f4abb093f0 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:35:17 +0000 Subject: [PATCH 65/73] Added in-house 256 conversion system for non-true-colour terminals because apple are crazy --- src/config/colors.rs | 27 +++++++++++++----- src/ui.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/config/colors.rs b/src/config/colors.rs index daf24824..0eb648d5 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -1,5 +1,6 @@ -/// For dealing with colours in the configuration file use crate::error::{OxError, Result}; +/// For dealing with colours in the configuration file +use crate::ui::{rgb_to_xterm256, supports_true_color}; use crossterm::style::Color as CColor; use mlua::prelude::*; @@ -304,16 +305,28 @@ impl Color { /// Returns a colour as a crossterm colour, ready to turn into ANSI codes pub fn to_color(&self) -> Result { + let true_color = supports_true_color(); + // Perform conversion Ok(match self { Color::Hex(hex) => { let (r, g, b) = Self::hex_to_rgb(hex)?; - CColor::Rgb { r, g, b } + if true_color { + CColor::Rgb { r, g, b } + } else { + CColor::AnsiValue(rgb_to_xterm256(r, g, b)) + } + } + Color::Rgb(r, g, b) => { + if true_color { + CColor::Rgb { + r: *r, + g: *g, + b: *b, + } + } else { + CColor::AnsiValue(rgb_to_xterm256(*r, *g, *b)) + } } - Color::Rgb(r, g, b) => CColor::Rgb { - r: *r, - g: *g, - b: *b, - }, Color::Ansi(code) => CColor::AnsiValue(*code), Color::Black => CColor::Black, Color::DarkGrey => CColor::DarkGrey, diff --git a/src/ui.rs b/src/ui.rs index 78aede1f..0fbc3318 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,6 +18,8 @@ use crossterm::{ }; use kaolinite::utils::Size; use std::cell::RefCell; +use std::collections::HashMap; +use std::env; use std::io::{stdout, Stdout, Write}; use std::rc::Rc; @@ -236,3 +238,66 @@ impl Terminal { Ok(()) } } + +/// Determines if this terminal supports 256 bit colours +pub fn supports_true_color() -> bool { + // Get the TERM and COLORTERM environment variables + let term = env::var("TERM").unwrap_or_default(); + let colorterm = env::var("COLORTERM").unwrap_or_default(); + // Check for common true color indicators + if term.contains("truecolor") || term.contains("xterm") || term.contains("screen") { + return true; + } + // Some terminals use COLORTERM to indicate support for true color + if colorterm.contains("truecolor") || colorterm.contains("24bit") { + return true; + } + // Check for specific terminal types that support true color + match term.as_str() { + "xterm-256color" | "screen-256color" | "tmux-256color" => true, + // No indicators found + _ => false, + } +} + +/// Converts rgb to the closest xterm equivalent +pub fn rgb_to_xterm256(r: u8, g: u8, b: u8) -> u8 { + let lookup = get_xterm_lookup(); + let mut min_distance = f64::INFINITY; + let mut closest_index = 0; + for (index, &(xr, xg, xb)) in &lookup { + // Calculate the Euclidean distance in RGB space + let distance = ((f64::from(r) - f64::from(xr)).powi(2) + + (f64::from(g) - f64::from(xg)).powi(2) + + (f64::from(b) - f64::from(xb)).powi(2)) + .sqrt(); + if distance < min_distance { + min_distance = distance; + closest_index = *index; + } + } + + closest_index +} + +/// Data representing xterm colours and their equivalent RGB values +pub const XTERMLOOKUP: &str = "0:0,0,0|1:128,0,0|2:0,128,0|3:128,128,0|4:0,0,128|5:128,0,128|6:0,128,128|7:192,192,192|8:128,128,128|9:255,0,0|10:0,255,0|11:255,255,0|12:0,0,255|13:255,0,255|14:0,255,255|15:255,255,255|16:0,0,0|17:0,0,95|18:0,0,135|19:0,0,175|20:0,0,215|21:0,0,255|22:0,95,0|23:0,95,95|24:0,95,135|25:0,95,175|26:0,95,215|27:0,95,255|28:0,135,0|29:0,135,95|30:0,135,135|31:0,135,175|32:0,135,215|33:0,135,255|34:0,175,0|35:0,175,95|36:0,175,135|37:0,175,175|38:0,175,215|39:0,175,255|40:0,215,0|41:0,215,95|42:0,215,135|43:0,215,175|44:0,215,215|45:0,215,255|46:0,255,0|47:0,255,95|48:0,255,135|49:0,255,175|50:0,255,215|51:0,255,255|52:95,0,0|53:95,0,95|54:95,0,135|55:95,0,175|56:95,0,215|57:95,0,255|58:95,95,0|59:95,95,95|60:95,95,135|61:95,95,175|62:95,95,215|63:95,95,255|64:95,135,0|65:95,135,95|66:95,135,135|67:95,135,175|68:95,135,215|69:95,135,255|70:95,175,0|71:95,175,95|72:95,175,135|73:95,175,175|74:95,175,215|75:95,175,255|76:95,215,0|77:95,215,95|78:95,215,135|79:95,215,175|80:95,215,215|81:95,215,255|82:95,255,0|83:95,255,95|84:95,255,135|85:95,255,175|86:95,255,215|87:95,255,255|88:135,0,0|89:135,0,95|90:135,0,135|91:135,0,175|92:135,0,215|93:135,0,255|94:135,95,0|95:135,95,95|96:135,95,135|97:135,95,175|98:135,95,215|99:135,95,255|100:135,135,0|101:135,135,95|102:135,135,135|103:135,135,175|104:135,135,215|105:135,135,255|106:135,175,0|107:135,175,95|108:135,175,135|109:135,175,175|110:135,175,215|111:135,175,255|112:135,215,0|113:135,215,95|114:135,215,135|115:135,215,175|116:135,215,215|117:135,215,255|118:135,255,0|119:135,255,95|120:135,255,135|121:135,255,175|122:135,255,215|123:135,255,255|124:175,0,0|125:175,0,95|126:175,0,135|127:175,0,175|128:175,0,215|129:175,0,255|130:175,95,0|131:175,95,95|132:175,95,135|133:175,95,175|134:175,95,215|135:175,95,255|136:175,135,0|137:175,135,95|138:175,135,135|139:175,135,175|140:175,135,215|141:175,135,255|142:175,175,0|143:175,175,95|144:175,175,135|145:175,175,175|146:175,175,215|147:175,175,255|148:175,215,0|149:175,215,95|150:175,215,135|151:175,215,175|152:175,215,215|153:175,215,255|154:175,255,0|155:175,255,95|156:175,255,135|157:175,255,175|158:175,255,215|159:175,255,255|160:215,0,0|161:215,0,95|162:215,0,135|163:215,0,175|164:215,0,215|165:215,0,255|166:215,95,0|167:215,95,95|168:215,95,135|169:215,95,175|170:215,95,215|171:215,95,255|172:215,135,0|173:215,135,95|174:215,135,135|175:215,135,175|176:215,135,215|177:215,135,255|178:215,175,0|179:215,175,95|180:215,175,135|181:215,175,175|182:215,175,215|183:215,175,255|184:215,215,0|185:215,215,95|186:215,215,135|187:215,215,175|188:215,215,215|189:215,215,255|190:215,255,0|191:215,255,95|192:215,255,135|193:215,255,175|194:215,255,215|195:215,255,255|196:255,0,0|197:255,0,95|198:255,0,135|199:255,0,175|200:255,0,215|201:255,0,255|202:255,95,0|203:255,95,95|204:255,95,135|205:255,95,175|206:255,95,215|207:255,95,255|208:255,135,0|209:255,135,95|210:255,135,135|211:255,135,175|212:255,135,215|213:255,135,255|214:255,175,0|215:255,175,95|216:255,175,135|217:255,175,175|218:255,175,215|219:255,175,255|220:255,215,0|221:255,215,95|222:255,215,135|223:255,215,175|224:255,215,215|225:255,215,255|226:255,255,0|227:255,255,95|228:255,255,135|229:255,255,175|230:255,255,215|231:255,255,255|232:8,8,8|233:18,18,18|234:28,28,28|235:38,38,38|236:48,48,48|237:58,58,58|238:68,68,68|239:78,78,78|240:88,88,88|241:98,98,98|242:108,108,108|243:118,118,118|244:128,128,128|245:138,138,138|246:148,148,148|247:158,158,158|248:168,168,168|249:178,178,178|250:188,188,188|251:198,198,198|252:208,208,208|253:218,218,218|254:228,228,228|255:238,238,238"; + +/// Based on the xterm lookup data, generate a rust hashmap to interpret +pub fn get_xterm_lookup() -> HashMap { + let mut result = HashMap::default(); + for line in XTERMLOOKUP.split('|') { + let mut parts = line.split(':'); + let (id, mut rgb_str) = ( + parts.next().unwrap().parse::().unwrap(), + parts.next().unwrap().split(','), + ); + let (r, g, b) = ( + rgb_str.next().unwrap().parse::().unwrap(), + rgb_str.next().unwrap().parse::().unwrap(), + rgb_str.next().unwrap().parse::().unwrap(), + ); + result.insert(id, (r, g, b)); + } + result +} From 78b17afb247aecc323b068fbf486d8baaa27268f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:41:52 +0000 Subject: [PATCH 66/73] Fixed 24bit colour detection --- src/ui.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 0fbc3318..832eb880 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -245,19 +245,14 @@ pub fn supports_true_color() -> bool { let term = env::var("TERM").unwrap_or_default(); let colorterm = env::var("COLORTERM").unwrap_or_default(); // Check for common true color indicators - if term.contains("truecolor") || term.contains("xterm") || term.contains("screen") { + if term.contains("truecolor") || term.contains("screen") { return true; } // Some terminals use COLORTERM to indicate support for true color if colorterm.contains("truecolor") || colorterm.contains("24bit") { return true; } - // Check for specific terminal types that support true color - match term.as_str() { - "xterm-256color" | "screen-256color" | "tmux-256color" => true, - // No indicators found - _ => false, - } + false } /// Converts rgb to the closest xterm equivalent From dd73beb2b1467b3a107ac218fb3e01e2c42080b4 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 01:25:14 +0000 Subject: [PATCH 67/73] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 81f0b2a7..a9ebb9ad 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ It works best on linux, but macOS and Windows are also supported. - :globe_with_meridians: Handles double-width unicode characters like a charm, including those of the Chinese, Korean and Japanese languages and emojis - :boxing_glove: Backend has been thoroughly tested via automated unit tests +- :rainbow: Automatically adapts your colour schemes to work on terminals with limited colours ## Installation From cdc556350eec9f0df369ffb73bd9869bb2a22eb3 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 13:17:27 +0000 Subject: [PATCH 68/73] More resilient package manager on fresh unix systems --- src/plugin/plugin_manager.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index 77be85fd..6dc2c4e6 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -138,7 +138,13 @@ function plugin_manager:download_plugin(plugin) local path = plugin_path .. "/" .. plugin .. ".lua" -- Create the plug-in directory if it doesn't already exist if not dir_exists(plugin_path) then - if shell:run("mkdir " .. plugin_path) ~= 0 then + local command + if package.config.sub(1,1) == '\\' then + command = "mkdir " .. plugin_path + else + command = "mkdir -p " .. plugin_path + end + if shell:run(command) ~= 0 then return "Failed to make directory at " .. plugin_path end end From f8f834880dee646d5df1d87874ed702f79ff2692 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:07:58 +0000 Subject: [PATCH 69/73] Added macOS binary build to build script --- build.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.sh b/build.sh index 9fec1233..47a9510a 100644 --- a/build.sh +++ b/build.sh @@ -15,6 +15,13 @@ cp target/generate-rpm/*.rpm target/pkgs/ cargo deb cp target/debian/*.deb target/pkgs/ +# Build for macOS (binary) +export SDKROOT=../../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 +cp target/x86_64-apple-darwin/release/ox target/pkgs/ox-macos + # Build for Windows (binary) cargo build --release --target x86_64-pc-windows-gnu strip -s target/x86_64-pc-windows-gnu/release/ox.exe From 3209911245e7ba70eb2b036b81bb96948d7c961f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:59:42 +0000 Subject: [PATCH 70/73] Updated test suite --- kaolinite/tests/test.rs | 58 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/kaolinite/tests/test.rs b/kaolinite/tests/test.rs index 2156da31..11bbc86b 100644 --- a/kaolinite/tests/test.rs +++ b/kaolinite/tests/test.rs @@ -396,6 +396,10 @@ fn document_deletion() { doc.move_to(&Loc { x: 11, y: 1 }); doc.delete_word(); assert_eq!(doc.line(1).unwrap(), st!(" world---")); + doc.exe(Event::InsertLine(1, st!("match => this"))); + doc.move_to(&Loc { x: 8, y: 1 }); + doc.delete_word(); + assert_eq!(doc.line(1).unwrap(), st!(" this")); } #[test] @@ -403,20 +407,37 @@ fn document_undo_redo() { let mut doc = Document::open(Size::is(100, 10), "tests/data/unicode.txt").unwrap(); doc.load_to(100); assert!(doc.event_mgmt.undo(doc.take_snapshot()).is_none()); + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); + doc.event_mgmt.force_not_with_disk = true; + assert!(!doc.event_mgmt.with_disk(&doc.take_snapshot())); + doc.event_mgmt.force_not_with_disk = false; + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); + assert!(doc.event_mgmt.undo(doc.take_snapshot()).is_none()); assert!(doc.redo().is_ok()); assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); doc.exe(Event::InsertLine(0, st!("hello你bye好hello"))); doc.exe(Event::Delete(Loc { x: 0, y: 2 }, st!("\t"))); doc.exe(Event::Insert(Loc { x: 3, y: 2 }, st!("a"))); + assert!(!doc.event_mgmt.with_disk(&doc.take_snapshot())); doc.commit(); assert!(!doc.event_mgmt.with_disk(&doc.take_snapshot())); assert!(doc.undo().is_ok()); + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); assert_eq!(doc.line(0), Some(st!(" 你好"))); assert_eq!(doc.line(1), Some(st!("\thello"))); assert_eq!(doc.line(2), Some(st!(" hello"))); assert!(doc.redo().is_ok()); + assert!(!doc.event_mgmt.with_disk(&doc.take_snapshot())); assert_eq!(doc.line(0), Some(st!("hello你bye好hello"))); assert_eq!(doc.line(2), Some(st!("helalo"))); + assert!(!doc.event_mgmt.with_disk(&doc.take_snapshot())); + doc.event_mgmt.disk_write(&doc.take_snapshot()); + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); + let mut doc = Document::open(Size::is(100, 10), "tests/data/unicode.txt").unwrap(); + doc.load_to(100); + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); + doc.exe(Event::InsertLine(0, st!("hello你bye好hello"))); + assert!(doc.event_mgmt.with_disk(&doc.take_snapshot())); } #[test] @@ -607,10 +628,29 @@ fn document_moving() { doc.move_prev_word(); assert_eq!(doc.loc(), Loc { x: 0, y: 10 }); assert_eq!(doc.move_prev_word(), Status::StartOfLine); - doc.exe(Event::InsertLine(11, st!("----test"))); + doc.exe(Event::InsertLine(11, st!("----test hello there----"))); doc.move_to(&Loc { x: 7, y: 11 }); doc.move_next_word(); - assert_eq!(doc.loc(), Loc { x: 8, y: 11 }); + assert_eq!(doc.loc(), Loc { x: 14, y: 11 }); + doc.move_to(&Loc { x: 0, y: 11 }); + assert_eq!(doc.prev_word_close(Loc { x: 0, y: 11 }), 0); + assert_eq!(doc.prev_word_close(Loc { x: 2, y: 11 }), 0); + assert_eq!(doc.prev_word_close(Loc { x: 9, y: 11 }), 4); + assert_eq!(doc.prev_word_close(Loc { x: 8, y: 11 }), 0); + assert_eq!(doc.prev_word_close(Loc { x: 14, y: 11 }), 8); + assert_eq!(doc.prev_word_close(Loc { x: 20, y: 11 }), 14); + assert_eq!(doc.prev_word_close(Loc { x: 24, y: 11 }), 15); + assert_eq!(doc.prev_word_close(Loc { x: 22, y: 11 }), 15); + assert_eq!(doc.next_word_close(Loc { x: 4, y: 11 }), 4); + assert_eq!(doc.next_word_close(Loc { x: 1, y: 11 }), 4); + assert_eq!(doc.next_word_close(Loc { x: 8, y: 11 }), 8); + assert_eq!(doc.next_word_close(Loc { x: 9, y: 11 }), 9); + assert_eq!(doc.next_word_close(Loc { x: 14, y: 11 }), 14); + assert_eq!(doc.next_word_close(Loc { x: 20, y: 11 }), 20); + assert_eq!(doc.next_word_close(Loc { x: 24, y: 11 }), 24); + assert_eq!(doc.next_word_close(Loc { x: 22, y: 11 }), 24); + doc.move_to(&Loc { x: 0, y: 10000000000 }); + assert_eq!(doc.loc(), Loc { x: 0, y: doc.len_lines() }); } #[test] @@ -665,6 +705,11 @@ fn document_selection() { doc.selection_loc_bound(), (Loc { x: 0, y: 2 }, Loc { x: 5, y: 2 }) ); + doc.select_word_at(&Loc { x: 5, y: 2 }); + assert_eq!( + doc.selection_loc_bound(), + (Loc { x: 0, y: 2 }, Loc { x: 5, y: 2 }) + ); } #[test] @@ -755,6 +800,15 @@ fn document_line_editing() { assert_eq!(doc.line(5), Some(st!("forever"))); doc.exe(Event::DeleteLine(5, st!("forever"))); assert_eq!(doc.line(5), Some(st!(""))); + // Line swapping + let mut doc = Document::open(Size::is(100, 10), "tests/data/unicode.txt").unwrap(); + doc.load_to(1000); + doc.swap_line_down().unwrap(); + assert_eq!(doc.line(0), Some(st!("\thello"))); + assert_eq!(doc.line(1), Some(st!(" 你好"))); + doc.swap_line_up().unwrap(); + assert_eq!(doc.line(0), Some(st!(" 你好"))); + assert_eq!(doc.line(1), Some(st!("\thello"))); } #[test] From bf01c2707b96d6eab0da0102f11b1208d0465386 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:00:23 +0000 Subject: [PATCH 71/73] rustfmt --- kaolinite/tests/test.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/kaolinite/tests/test.rs b/kaolinite/tests/test.rs index 11bbc86b..b099f075 100644 --- a/kaolinite/tests/test.rs +++ b/kaolinite/tests/test.rs @@ -649,8 +649,17 @@ fn document_moving() { assert_eq!(doc.next_word_close(Loc { x: 20, y: 11 }), 20); assert_eq!(doc.next_word_close(Loc { x: 24, y: 11 }), 24); assert_eq!(doc.next_word_close(Loc { x: 22, y: 11 }), 24); - doc.move_to(&Loc { x: 0, y: 10000000000 }); - assert_eq!(doc.loc(), Loc { x: 0, y: doc.len_lines() }); + doc.move_to(&Loc { + x: 0, + y: 10000000000, + }); + assert_eq!( + doc.loc(), + Loc { + x: 0, + y: doc.len_lines() + } + ); } #[test] From 3a4f2fe6fec9a6e096cffd7e87f9ccb833517a5a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:10:17 +0000 Subject: [PATCH 72/73] prevent useless git update --- plugins/git.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/git.lua b/plugins/git.lua index 1691cd66..8c3a68f4 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -1,5 +1,5 @@ --[[ -Git v0.5 +Git v0.4 A plug-in for git integration that provides features to: - Choose which files to add to a commit From c429308baf6eb31b98dc9b2b88b0589ded69e3e2 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:44:05 +0000 Subject: [PATCH 73/73] Updated readme installation guide --- README.md | 93 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index a9ebb9ad..fccb1409 100644 --- a/README.md +++ b/README.md @@ -75,61 +75,62 @@ It works best on linux, but macOS and Windows are also supported. ## Installation -> :warning: Huge Warning: A lot of these (except manual, AUR and homebrew) are quite out of date, it is quite a huge task having to push to all these sources each time I update and I'd rather focus on creating high-quality updates +To get started, please click on your operating system -### Prerequisites +- :penguin: [Linux](#linux) +- :window: [Windows](#windows) +- :apple: [MacOS](#macos) -Because Ox is written in Rust, you must have a modern and working version of `rustc` and `cargo`. +### Linux -On Arch Linux, you can run this command: -```sh -sudo pacman -S rustup -rustup toolchain install stable -``` +Here are the list of available methods for installing on Linux: +- [Manually](#manual) +- [Binary](#binaries) +- [Arch Linux](#arch-linux) +- [Fedora](#fedora) +- [Debian / Ubuntu](#debian) -If you are not using Arch, you can easily set it up on other distros by running the distro-neutral command: -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -/usr/bin/rustup toolchain install stable -``` -You must have `curl` installed to run this command. +#### Arch Linux -### Installation Routes +Install one of the following from the AUR: +- `ox-bin` - install the pre-compiled binary (fastest) +- `ox-git` - compile from source (best) -#### Manual +#### Fedora -The absolute best way to install Ox, it will ensure you always have the latest version +You can find an RPM in the [releases page](https://github.com/curlpipe/ox/releases) -It should only really take at most 1 minute on most modern hardware. +Install using the following command: ```sh -cargo install --git https://github.com/curlpipe/ox ox +sudo dnf install /path/to/rpm/file ``` -#### Arch Linux +#### Debian -Install `ox-bin` or `ox-git` from the Arch User Repository. +You can find a deb file in the [releases page](https://github.com/curlpipe/ox/releases) -That's all there is to it! +Install using the following command: -#### Fedora/CentOS +```sh +sudo dpkg -i /path/to/deb/file +``` -Install `ox` from the [COPR Repository](https://copr.fedorainfracloud.org/coprs/atim/ox/): +### Windows -``` -sudo dnf copr enable atim/ox -y -sudo dnf install ox -``` +Here are the list of available methods for installing on Windows: +- [Manually (best)](#manual) +- [Binary](#binaries) -You can also find an RPM file in the releases page -#### Debian -You can find a deb file on the releases page, which can be installed via dpkg: +### MacOS -```sh -dpkg -i ox_version.deb -``` +Here are the list of available methods for installing on macOS: +- [Manually](#manual) +- [Binary](#binaries) +- [Homebrew](#homebrew) +- [MacPorts](#macports) #### Homebrew @@ -148,6 +149,30 @@ sudo port selfupdate sudo port install ox ``` +### Binaries + +There are precompiled binaries available for all platforms in the [releases page](https://github.com/curlpipe/ox/releases). + +- For Linux: download the `ox` executable and copy it to `/usr/bin/ox`, then run `sudo chmod +x /usr/bin/ox` +- For MacOS: download the `ox-macos` executable and copy it to `/usr/local/bin/ox`, then run `sudo chmod +x /usr/local/bin/ox` +- For Windows: download the `ox.exe` executable and copy it into a location in `PATH` see [this guide](https://zwbetz.com/how-to-add-a-binary-to-your-path-on-macos-linux-windows/#windows-cli) for how to do it + +### Manual + +This is the absolute best way to install Ox, it will ensure you always have the latest version and everything works for your system. + +You must have a working installation of the Rust compiler to use this method. Visit the website for [rustup](https://rustup.rs/) and follow the instructions there for your operating system. + +Now with a working version of rust, you can run the command: + +```sh +cargo install --git https://github.com/curlpipe/ox +``` + +This will take at worst around 2 minutes. On some more modern systems, it will take around 30 seconds. + +Please note that you should add `.cargo/bin` to your path, which is where the `ox` executable will live, although `rustup` will likely do that for you, so no need to worry too much. + ## Quick Start Guide This is just a quick guide to help get you up to speed quickly with how to use the editor. You dive into more details in the documentation section below, but this quick start guide is a good place to start.