diff --git a/Cargo.lock b/Cargo.lock index 9938dd4..9fc9a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,9 +56,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.31" +version = "1.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" dependencies = [ "shlex", ] @@ -69,6 +69,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "char_index" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ef8669476802b7127a0a97612a0c34113e949ee65c695e4ac259f1f49aaa25" + [[package]] name = "crossterm" version = "0.28.1" @@ -210,9 +216,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libredox" @@ -311,6 +317,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "num-traits" version = "0.2.19" @@ -328,7 +340,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.7.0" +version = "0.7.1" dependencies = [ "alinio", "base64", @@ -494,9 +506,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ "bitflags", "errno", @@ -513,18 +525,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.213" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.213" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -584,9 +596,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "str_indices" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" [[package]] name = "sugars" @@ -596,9 +608,9 @@ checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a" [[package]] name = "syn" -version = "2.0.85" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -607,29 +619,31 @@ dependencies = [ [[package]] name = "synoptic" -version = "2.2.6" +version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4550857be213a870db94e3cb806b2698de900412ef555515f84772191c063fc9" +checksum = "21a4e6e4b1658d5a22ef24f9fc176f647d89d1eb78da2e853f6b49d8cbd7e02a" dependencies = [ + "char_index", "if_chain", + "nohash-hasher", "regex", "unicode-width 0.2.0", ] [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d1b509d..9e1d0b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,19 +3,17 @@ resolver = "2" members = [ "kaolinite", ] -exclude = ["cactus"] [package] name = "ox" -version = "0.7.0" +version = "0.7.1" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] -description = "A Rust powered text editor." +description = "A simple but flexible text editor." homepage = "https://github.com/curlpipe/ox" repository = "https://github.com/curlpipe/ox" readme = "README.md" include = ["src/*.rs", "Cargo.toml", "config/.oxrc"] -exclude = ["kaolinite/examples/cactus"] categories = ["text-editors"] keywords = ["text-editor", "editor", "terminal", "tui"] license = "GPL-2.0" @@ -42,4 +40,4 @@ kaolinite = { path = "./kaolinite" } mlua = { version = "0.10", features = ["lua54", "vendored"] } error_set = "0.7" shellexpand = "3.1.0" -synoptic = "2.2.6" +synoptic = "2.2.7" diff --git a/build.sh b/build.sh index 350e6ba..5992376 100644 --- a/build.sh +++ b/build.sh @@ -26,3 +26,6 @@ cp target/x86_64-apple-darwin/release/ox target/pkgs/ox-macos 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 + +# Clean up +rm .intentionally-empty-file.o diff --git a/kaolinite/src/document/cursor.rs b/kaolinite/src/document/cursor.rs index b993ea9..bf9e8d8 100644 --- a/kaolinite/src/document/cursor.rs +++ b/kaolinite/src/document/cursor.rs @@ -345,22 +345,43 @@ impl Document { /// Will return the bounds of the current active selection #[must_use] - pub fn selection_loc_bound(&self) -> (Loc, Loc) { + pub fn selection_loc_bound_disp(&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) } + /// Will return the bounds of the current active selection + #[must_use] + pub fn selection_loc_bound(&self) -> (Loc, Loc) { + let (mut left, mut right) = self.selection_loc_bound_disp(); + // Convert into character indices + left.x = self.character_idx(&left); + right.x = self.character_idx(&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(); + self.is_this_loc_selected(loc, self.selection_loc_bound()) + } + + /// Returns true if the provided location is within the provided selection argument + #[must_use] + pub fn is_this_loc_selected(&self, loc: Loc, selection_bound: (Loc, Loc)) -> bool { + let (left, right) = selection_bound; + left <= loc && loc < right + } + + /// Returns true if the provided location is within the provided selection argument + #[must_use] + pub fn is_this_loc_selected_disp(&self, loc: Loc, selection_bound: (Loc, Loc)) -> bool { + let (left, right) = selection_bound; left <= loc && loc < right } diff --git a/kaolinite/src/document/disk.rs b/kaolinite/src/document/disk.rs index 79659b5..1b46fe9 100644 --- a/kaolinite/src/document/disk.rs +++ b/kaolinite/src/document/disk.rs @@ -5,7 +5,7 @@ use crate::utils::get_absolute_path; use crate::{Document, Loc, Size}; use ropey::Rope; use std::fs::File; -use std::io::{BufReader, BufWriter}; +use std::io::{BufRead, BufReader, BufWriter, Read}; /// A document info struct to store information about the file it represents #[derive(Clone, PartialEq, Eq, Debug)] @@ -54,7 +54,7 @@ impl Document { #[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 = load_rope_from_reader(BufReader::new(File::open(&file_name)?)); let file_name = get_absolute_path(&file_name); Ok(Self { info: DocumentInfo { @@ -140,3 +140,39 @@ impl Document { } } } + +pub fn load_rope_from_reader(mut reader: T) -> Rope { + let mut buffer = [0u8; 2048]; // Buffer to read chunks + let mut valid_string = String::new(); + let mut incomplete_bytes = Vec::new(); // Buffer to handle partial UTF-8 sequences + + while let Ok(bytes_read) = reader.read(&mut buffer) { + if bytes_read == 0 { + break; // EOF reached + } + + // Combine leftover bytes with current chunk + incomplete_bytes.extend_from_slice(&buffer[..bytes_read]); + + // Attempt to decode as much UTF-8 as possible + match String::from_utf8(incomplete_bytes.clone()) { + Ok(decoded) => { + valid_string.push_str(&decoded); // Append valid data + incomplete_bytes.clear(); // Clear incomplete bytes + } + Err(err) => { + // Handle valid and invalid parts separately + let valid_up_to = err.utf8_error().valid_up_to(); + valid_string.push_str(&String::from_utf8_lossy(&incomplete_bytes[..valid_up_to])); + incomplete_bytes = incomplete_bytes[valid_up_to..].to_vec(); // Retain invalid/partial + } + } + } + + // Append any remaining valid UTF-8 data + if !incomplete_bytes.is_empty() { + valid_string.push_str(&String::from_utf8_lossy(&incomplete_bytes)); + } + + Rope::from_str(&valid_string) +} diff --git a/kaolinite/src/document/words.rs b/kaolinite/src/document/words.rs index 7f923b6..887a626 100644 --- a/kaolinite/src/document/words.rs +++ b/kaolinite/src/document/words.rs @@ -17,7 +17,7 @@ impl Document { 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 starts: Vec = searcher.lfinds_raw(line); let mut ends: Vec = starts.clone(); ends.iter_mut() .for_each(|m| m.loc.x += m.text.chars().count()); @@ -28,15 +28,16 @@ impl Document { /// 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 { + pub fn cursor_word_state(&self, line: &str, words: &[(usize, usize)], x: usize) -> WordState { + let byte_x = Searcher::char_to_raw(x, line); let in_word = words .iter() - .position(|(start, end)| *start <= x && x <= *end); + .position(|(start, end)| *start <= byte_x && byte_x <= *end); if let Some(idx) = in_word { let (word_start, word_end) = words[idx]; - if x == word_end { + if byte_x == word_end { WordState::AtEnd(idx) - } else if x == word_start { + } else if byte_x == word_start { WordState::AtStart(idx) } else { WordState::InCenter(idx) @@ -52,24 +53,26 @@ impl Document { 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); + let state = self.cursor_word_state(&line, &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::AtEnd(idx) | WordState::InCenter(idx) => { + Searcher::raw_to_char(words[idx.saturating_sub(1)].1, &line) + } + WordState::AtStart(idx) => Searcher::raw_to_char(words[idx.saturating_sub(1)].0, &line), 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) { + while let WordState::Out = self.cursor_word_state(&line, &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, + match self.cursor_word_state(&line, &words, shift_back) { + WordState::AtEnd(idx) => Searcher::raw_to_char(words[idx].0, &line), _ => 0, } } @@ -82,24 +85,26 @@ impl Document { 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); + let state = self.cursor_word_state(&line, &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::AtEnd(idx) | WordState::InCenter(idx) => { + Searcher::raw_to_char(words[idx.saturating_sub(1)].1, &line) + } + WordState::AtStart(idx) => Searcher::raw_to_char(words[idx.saturating_sub(1)].0, &line), 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) { + while let WordState::Out = self.cursor_word_state(&line, &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, + match self.cursor_word_state(&line, &words, shift_back) { + WordState::AtEnd(idx) => Searcher::raw_to_char(words[idx].1, &line), _ => 0, } } @@ -128,12 +133,12 @@ impl Document { 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); + let state = self.cursor_word_state(&line, &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 + Searcher::raw_to_char(word.1, &line) } else { // No next word exists, just go to end of line line.chars().count() @@ -142,7 +147,7 @@ impl Document { WordState::AtStart(idx) => { // Cursor is at the start of a word, move to next start if let Some(word) = words.get(idx) { - word.0 + Searcher::raw_to_char(word.0, &line) } else { // No next word exists, just go to end of line line.chars().count() @@ -151,14 +156,14 @@ impl Document { 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) { + while let WordState::Out = self.cursor_word_state(&line, &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, + match self.cursor_word_state(&line, &words, shift_forward) { + WordState::AtStart(idx) => Searcher::raw_to_char(words[idx].0, &line), _ => line.chars().count(), } } @@ -171,12 +176,12 @@ impl Document { 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); + let state = self.cursor_word_state(&line, &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 + Searcher::raw_to_char(word.1, &line) } else { // No next word exists, just go to end of line line.chars().count() @@ -185,7 +190,7 @@ impl Document { 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 + Searcher::raw_to_char(word.0, &line) } else { // No next word exists, just go to end of line line.chars().count() @@ -194,14 +199,14 @@ impl Document { 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) { + while let WordState::Out = self.cursor_word_state(&line, &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, + match self.cursor_word_state(&line, &words, shift_forward) { + WordState::AtStart(idx) => Searcher::raw_to_char(words[idx].0, &line), _ => line.chars().count(), } } @@ -232,33 +237,35 @@ impl Document { 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 state = self.cursor_word_state(&line, &words, x); let delete_upto = match state { WordState::InCenter(idx) | WordState::AtEnd(idx) => { // Delete back to start of this word - words[idx].0 + Searcher::raw_to_char(words[idx].0, &line) } WordState::AtStart(0) => 0, WordState::AtStart(idx) => { // Delete back to start of the previous word - words[idx.saturating_sub(1)].0 + Searcher::raw_to_char(words[idx.saturating_sub(1)].0, &line) } 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) { + while let WordState::Out = self.cursor_word_state(&line, &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); + let state = self.cursor_word_state(&line, &words, shift_back); match (char, state) { // Shift to start of previous word if there is a space - (Some(' '), WordState::AtEnd(idx)) => words[idx].0, + (Some(' '), WordState::AtEnd(idx)) => { + Searcher::raw_to_char(words[idx].0, &line) + } // Shift to end of previous word if there is not a space - (_, WordState::AtEnd(idx)) => words[idx].1, + (_, WordState::AtEnd(idx)) => Searcher::raw_to_char(words[idx].1, &line), _ => 0, } } diff --git a/kaolinite/src/searching.rs b/kaolinite/src/searching.rs index cd99a81..222927e 100644 --- a/kaolinite/src/searching.rs +++ b/kaolinite/src/searching.rs @@ -67,6 +67,20 @@ impl Searcher { result } + /// Finds all the matches to the left from a certain point onwards + pub fn lfinds_raw(&mut self, st: &str) -> Vec { + let mut result = vec![]; + for cap in self.re.captures_iter(st) { + if let Some(c) = cap.get(cap.len().saturating_sub(1)) { + result.push(Match { + loc: Loc::at(c.start(), 0), + text: c.as_str().to_string(), + }); + } + } + result + } + /// Finds all the matches to the right pub fn rfinds(&mut self, st: &str) -> Vec { let mut result = vec![]; @@ -87,13 +101,17 @@ impl Searcher { /// Converts a raw index into a character index, so that matches are in character indices #[must_use] pub fn raw_to_char(x: usize, st: &str) -> usize { - let mut raw = 0; - for (c, ch) in st.chars().enumerate() { - if raw == x { - return c; + for (acc_char, (acc_byte, _)) in st.char_indices().enumerate() { + if acc_byte == x { + return acc_char; } - raw += ch.len_utf8(); } st.chars().count() } + + /// Converts a raw index into a character index, so that matches are in character indices + #[must_use] + pub fn char_to_raw(x: usize, st: &str) -> usize { + st.char_indices().nth(x).map_or(st.len(), |(byte, _)| byte) + } } diff --git a/plugins/quickcomment.lua b/plugins/quickcomment.lua index 04577d7..1c79549 100644 --- a/plugins/quickcomment.lua +++ b/plugins/quickcomment.lua @@ -1,5 +1,5 @@ --[[ -Quickcomment v0.1 +Quickcomment v0.2 A plug-in to help you comment and uncomment lines quickly ]]-- @@ -52,14 +52,44 @@ end function quickcomment:comment_start() if editor.document_type == "Shell" then comment_start = "#" - elseif editor.document_type == "Python" then + elseif editor.document_type == "Python" then comment_start = "#" - elseif editor.document_type == "Ruby" then + elseif editor.document_type == "Ruby" then comment_start = "#" - elseif editor.document_type == "Lua" then + elseif editor.document_type == "TOML" then + comment_start = "#" + elseif editor.document_type == "Lua" then + comment_start = "--" + elseif editor.document_type == "Haskell" then comment_start = "--" - elseif editor.document_type == "Haskell" then + elseif editor.document_type == "Assembly" then + comment_start = ";" + elseif editor.document_type == "Ada" then comment_start = "--" + elseif editor.document_type == "Crystal" then + comment_start = "#" + elseif editor.document_type == "Makefile" then + comment_start = "#" + elseif editor.document_type == "Julia" then + comment_start = "#" + elseif editor.document_type == "Lisp" then + comment_start = ";" + elseif editor.document_type == "Perl" then + comment_start = "#" + elseif editor.document_type == "R" then + comment_start = "#" + elseif editor.document_type == "Racket" then + comment_start = ";" + elseif editor.document_type == "SQL" then + comment_start = "--" + elseif editor.document_type == "Zsh" then + comment_start = "#" + elseif editor.document_type == "Yaml" then + comment_start = "#" + elseif editor.document_type == "Clojure" then + comment_start = ";" + elseif editor.document_type == "Zsh" then + comment_start = "#" else comment_start = "//" end diff --git a/src/config/editor.rs b/src/config/editor.rs index 84c06f4..fceccf9 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -588,7 +588,7 @@ impl LuaUserData for 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 }); h = h.saturating_sub(1 + editor.push_down); - let _ = editor.terminal.hide_cursor(); + editor.terminal.hide_cursor(); // Apply render and restore cursor if editor.try_doc().is_some() { let _ = editor.render_feedback_line(w, h); @@ -596,10 +596,10 @@ impl LuaUserData for Editor { 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); + editor.terminal.goto(x + max, y + editor.push_down); } } - let _ = editor.terminal.show_cursor(); + editor.terminal.show_cursor(); let _ = editor.terminal.flush(); Ok(()) }); @@ -607,14 +607,14 @@ impl LuaUserData for 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 }); h = h.saturating_sub(1 + editor.push_down); - let _ = editor.terminal.hide_cursor(); + editor.terminal.hide_cursor(); let _ = editor.render_status_line(lua, 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); + editor.terminal.goto(x + max, y + editor.push_down); } - let _ = editor.terminal.show_cursor(); + editor.terminal.show_cursor(); let _ = editor.terminal.flush(); Ok(()) }); diff --git a/src/editor/interface.rs b/src/editor/interface.rs index a5d1d57..e723ae6 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -6,10 +6,7 @@ use crate::ui::{key_event, size, Feedback}; use crate::{display, handle_lua_error}; use crossterm::{ event::{KeyCode as KCode, KeyModifiers as KMod}, - queue, - style::{ - Attribute, Color, Print, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg, - }, + style::{Attribute, Color, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, }; use kaolinite::utils::{file_or_dir, get_cwd, get_parent, list_dir, width, Loc, Size}; use mlua::Lua; @@ -24,7 +21,7 @@ impl Editor { return Ok(()); } self.needs_rerender = false; - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); let Size { w, mut h } = size()?; h = h.saturating_sub(1 + self.push_down); // Update the width of the document in case of update @@ -46,8 +43,8 @@ impl Editor { self.render_feedback_line(w, h)?; // Move cursor to the correct location and perform render if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { - self.terminal.show_cursor()?; - self.terminal.goto(x + max, y + self.push_down)?; + self.terminal.show_cursor(); + self.terminal.goto(x + max, y + self.push_down); } self.terminal.flush()?; Ok(()) @@ -79,6 +76,8 @@ impl Editor { let first_line = (h / 2).saturating_sub(message.len() / 2) + 1; let start = u16::try_from(first_line).unwrap_or(u16::MAX); let end = start + u16::try_from(message.len()).unwrap_or(u16::MAX); + // Get other information + let selection = self.doc().selection_loc_bound_disp(); // Render each line of the document for y in 0..u16::try_from(h).unwrap_or(0) { // Work out how long the line should be (accounting for help message if necessary) @@ -89,7 +88,7 @@ impl Editor { w.saturating_sub(self.dent()) }; // Go to the right location - self.terminal.goto(0, y as usize + self.push_down)?; + self.terminal.goto(0, y as usize + self.push_down); // Start colours let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); @@ -97,7 +96,6 @@ impl Editor { let line_number_fg = Fg(config!(self.config, colors).line_number_fg.to_color()?); let selection_bg = Bg(config!(self.config, colors).selection_bg.to_color()?); let selection_fg = Fg(config!(self.config, colors).selection_fg.to_color()?); - display!(self, editor_bg, editor_fg); // Write line number of document if config!(self.config, line_numbers).enabled { let num = self.doc().line_number(y as usize + self.doc().offset.y); @@ -114,6 +112,8 @@ impl Editor { editor_fg, editor_bg ); + } else { + display!(self, editor_bg, editor_fg); } // Render line if it exists let idx = y as usize + self.doc().offset.y; @@ -124,7 +124,8 @@ impl Editor { // 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; + let mut x_disp = self.doc().offset.x; + let mut x_char = self.doc().character_idx(&self.doc().offset); for token in tokens { // Find out the text (and colour of that text) let (text, colour) = match token { @@ -146,9 +147,12 @@ impl Editor { TokOpt::None(text) => (text, editor_fg), }; // Do the rendering (including selection where applicable) + let underline = SetAttribute(Attribute::Underlined); + let no_underline = SetAttribute(Attribute::NoUnderline); 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 }); + let disp_loc = Loc { y: idx, x: x_disp }; + let char_loc = Loc { y: idx, x: x_char }; + let is_selected = self.doc().is_this_loc_selected_disp(disp_loc, selection); // Render the correct colour if is_selected { if cache_bg != selection_bg { @@ -170,10 +174,7 @@ impl Editor { } } // Render multi-cursors - let underline = SetAttribute(Attribute::Underlined); - let no_underline = SetAttribute(Attribute::NoUnderline); - let at_loc = Loc { y: idx, x: at_x }; - let multi_cursor_here = self.doc().has_cursor(at_loc).is_some(); + let multi_cursor_here = self.doc().has_cursor(char_loc).is_some(); if multi_cursor_here { display!(self, underline, Bg(Color::White), Fg(Color::Black)); } @@ -183,7 +184,8 @@ impl Editor { if multi_cursor_here { display!(self, no_underline, cache_bg, cache_fg); } - x_pos += 1; + x_char += 1; + x_disp += width(&c.to_string(), tab_width); } } display!(self, editor_fg, editor_bg); @@ -217,7 +219,7 @@ impl Editor { if c == self.ptr { idx = headers.len().saturating_sub(1); } - while c == self.ptr && length > w { + while c == self.ptr && length > w && headers.len() > 1 { headers.remove(0); length = length.saturating_sub(width(&headers[0], 4) + 1); idx = headers.len().saturating_sub(1); @@ -230,7 +232,7 @@ impl Editor { /// Render the tab line at the top of the document #[allow(clippy::similar_names)] pub fn render_tab_line(&mut self, lua: &Lua, w: usize) -> Result<()> { - self.terminal.goto(0_usize, 0_usize)?; + self.terminal.goto(0_usize, 0_usize); let tab_inactive_bg = Bg(config!(self.config, colors).tab_inactive_bg.to_color()?); let tab_inactive_fg = Fg(config!(self.config, colors).tab_inactive_fg.to_color()?); let tab_active_bg = Bg(config!(self.config, colors).tab_active_bg.to_color()?); @@ -261,7 +263,7 @@ impl Editor { /// Render the status line at the bottom of the document #[allow(clippy::similar_names)] pub fn render_status_line(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { - self.terminal.goto(0, h + self.push_down)?; + self.terminal.goto(0, h + self.push_down); let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); let status_bg = Bg(config!(self.config, colors).status_bg.to_color()?); @@ -298,7 +300,7 @@ impl Editor { /// Render the feedback line pub fn render_feedback_line(&mut self, w: usize, h: usize) -> Result<()> { - self.terminal.goto(0, h + 2)?; + self.terminal.goto(0, h + 2); let content = self.feedback.render(&config!(self.config, colors), w)?; display!(self, content); Ok(()) @@ -310,7 +312,7 @@ impl Editor { let greeting = config!(self.config, greeting_message).render(lua, &colors)?; let message: Vec<&str> = greeting.split('\n').collect(); for (c, line) in message.iter().enumerate().take(h.saturating_sub(h / 4)) { - self.terminal.goto(4, h / 4 + c + 1)?; + self.terminal.goto(4, h / 4 + c + 1); let content = alinio::align::center(line, w.saturating_sub(4)).unwrap_or_default(); display!(self, content); } @@ -327,8 +329,8 @@ impl Editor { let h = size()?.h; let w = size()?.w; // Render prompt message - self.terminal.prepare_line(h)?; - self.terminal.show_cursor()?; + self.terminal.prepare_line(h); + self.terminal.show_cursor(); let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); display!( self, @@ -338,7 +340,7 @@ impl Editor { input.clone(), " ".to_string().repeat(w) ); - self.terminal.goto(prompt.len() + input.len() + 2, h)?; + self.terminal.goto(prompt.len() + input.len() + 2, h); self.terminal.flush()?; // Handle events if let Some((modifiers, code)) = @@ -404,8 +406,8 @@ impl Editor { .unwrap_or(input.clone()); // Render prompt message let h = size()?.h; - self.terminal.prepare_line(h)?; - self.terminal.show_cursor()?; + self.terminal.prepare_line(h); + self.terminal.show_cursor(); let suggestion_text = suggestion .chars() .skip(input.chars().count()) @@ -426,7 +428,7 @@ impl Editor { editor_fg ); let tab_width = config!(self.config, document).tab_width; - self.terminal.goto(6 + width(&input, tab_width), h)?; + self.terminal.goto(6 + width(&input, tab_width), h); self.terminal.flush()?; // Handle events if let Some((modifiers, code)) = @@ -471,7 +473,7 @@ impl Editor { let mut done = false; let mut result = false; // Enter into the confirmation menu - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); while !done { let h = size()?.h; let w = size()?.w; @@ -499,7 +501,7 @@ impl Editor { } } } - self.terminal.show_cursor()?; + self.terminal.show_cursor(); Ok(result) } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 9e57ac8..061b13f 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,5 +1,5 @@ -use crate::config; /// Main functionality of the editor +use crate::config; use crate::config::{Config, Indentation}; use crate::error::{OxError, Result}; use crate::ui::{size, Feedback, Terminal}; @@ -8,7 +8,7 @@ use crossterm::event::{ }; use kaolinite::event::Error as KError; use kaolinite::utils::{get_absolute_path, get_file_name}; -use kaolinite::Document; +use kaolinite::{Document, Loc}; use mlua::{Error as LuaError, Lua}; use std::env; use std::io::ErrorKind; @@ -62,7 +62,7 @@ pub struct Editor { /// 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, + pub alt_click_state: Option<(Loc, Loc)>, /// Macro manager pub macro_man: MacroMan, } @@ -86,7 +86,7 @@ impl Editor { config_path: "~/.oxrc".to_string(), plugin_active: false, last_click: None, - in_dbl_click: false, + alt_click_state: None, macro_man: MacroMan::default(), }) } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index 2ada5d7..c52d7db 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -46,6 +46,7 @@ impl Editor { } /// Handles a mouse event (dragging / clicking) + #[allow(clippy::too_many_lines)] pub fn handle_mouse_event(&mut self, lua: &Lua, event: MouseEvent) { match event.modifiers { KeyModifiers::NONE => match event.kind { @@ -58,7 +59,6 @@ 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,11 +81,25 @@ impl Editor { // Select the current line if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { self.doc_mut().select_line_at(loc.y); + let line = self.doc().line(loc.y).unwrap_or_default(); + self.alt_click_state = Some(( + Loc { + x: 0, + y: self.doc().loc().y, + }, + Loc { + x: line.chars().count(), + y: self.doc().loc().y, + }, + )); } } + MouseEventKind::Up(MouseButton::Right) => { + self.alt_click_state = None; + } // Double click detection MouseEventKind::Up(MouseButton::Left) => { - self.in_dbl_click = false; + self.alt_click_state = None; let now = Instant::now(); // Register this click as having happened self.last_click = Some((now, event)); @@ -95,14 +109,16 @@ impl Editor { match self.find_mouse_location(lua, event) { MouseLocation::File(mut loc) => { loc.x = self.doc_mut().character_idx(&loc); - if self.in_dbl_click { - if loc.x >= self.doc().cursor.selection_end.x { + if let Some((dbl_start, dbl_end)) = self.alt_click_state { + 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().move_to(&dbl_start); 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().move_to(&dbl_end); self.doc_mut().select_to(&Loc { x: next, y: loc.y }); } } else { @@ -116,7 +132,21 @@ impl Editor { match self.find_mouse_location(lua, event) { MouseLocation::File(mut loc) => { loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().select_to_y(loc.y); + if let Some((line_start, line_end)) = self.alt_click_state { + if loc.y > self.doc().cursor.selection_end.y { + let line = self.doc().line(loc.y).unwrap_or_default(); + self.doc_mut().move_to(&line_start); + self.doc_mut().select_to(&Loc { + x: line.chars().count(), + y: loc.y, + }); + } else { + self.doc_mut().move_to(&line_end); + self.doc_mut().select_to(&Loc { x: 0, y: loc.y }); + } + } else { + self.doc_mut().select_to(&loc); + } } MouseLocation::Tabs(_) | MouseLocation::Out => (), } @@ -159,6 +189,11 @@ impl Editor { // Select the current word if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { self.doc_mut().select_word_at(&loc); + let mut selection = self.doc().cursor.selection_end; + let mut cursor = self.doc().cursor.loc; + selection.x = self.doc().character_idx(&selection); + cursor.x = self.doc().character_idx(&cursor); + self.alt_click_state = Some((selection, cursor)); } } } diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 2f86a1f..56dafe0 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -5,7 +5,6 @@ use crate::ui::{key_event, size}; use crate::{config, display}; use crossterm::{ event::{KeyCode as KCode, KeyModifiers as KMod}, - queue, style::{Attribute, Print, SetAttribute, SetBackgroundColor as Bg}, }; use kaolinite::utils::{Loc, Size}; @@ -23,8 +22,8 @@ impl Editor { while !done { let Size { w, h } = size()?; // Render prompt message - self.terminal.prepare_line(h)?; - self.terminal.show_cursor()?; + self.terminal.prepare_line(h); + self.terminal.show_cursor(); let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); display!( self, @@ -34,15 +33,15 @@ impl Editor { "│", " ".to_string().repeat(w) ); - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); self.render_document(lua, w, h.saturating_sub(2))?; // Move back to correct cursor position if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { let max = self.dent(); - self.terminal.goto(x + max, y + 1)?; - self.terminal.show_cursor()?; + self.terminal.goto(x + max, y + 1); + self.terminal.show_cursor(); } else { - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); } self.terminal.flush()?; if let Some((modifiers, code)) = @@ -80,21 +79,21 @@ impl Editor { // Enter into search menu while !done { // Render just the document part - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); self.render_document(lua, w, h.saturating_sub(2))?; // Render custom status line with mode information - self.terminal.goto(0, h)?; - queue!( - self.terminal.stdout, + self.terminal.goto(0, h); + display!( + self, Print("[<-]: Search previous | [->]: Search next | [Enter] Finish | [Esc] Cancel") - )?; + ); // Move back to correct cursor position if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { let max = self.dent(); - self.terminal.goto(x + max, y + 1)?; - self.terminal.show_cursor()?; + self.terminal.goto(x + max, y + 1); + self.terminal.show_cursor(); } else { - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); } self.terminal.flush()?; // Handle events @@ -174,23 +173,23 @@ impl Editor { // Enter into the replace menu while !done { // Render just the document part - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); self.render_document(lua, w, h.saturating_sub(2))?; // Write custom status line for the replace mode - self.terminal.goto(0, h)?; - queue!( - self.terminal.stdout, + self.terminal.goto(0, h); + display!( + self, Print( "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All | [Esc] Exit" ) - )?; + ); // Move back to correct cursor location if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { let max = self.dent(); - self.terminal.goto(x + max, y + 1)?; - self.terminal.show_cursor()?; + self.terminal.goto(x + max, y + 1); + self.terminal.show_cursor(); } else { - self.terminal.hide_cursor()?; + self.terminal.hide_cursor(); } self.terminal.flush()?; // Handle events diff --git a/src/ui.rs b/src/ui.rs index 68eabc8..a4acd5a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -27,9 +27,9 @@ use std::io::{stdout, Stdout, Write}; #[macro_export] macro_rules! display { ( $self:expr, $( $x:expr ),* ) => { - queue!($self.terminal.stdout, SetAttribute(Attribute::NormalIntensity))?; + $self.terminal.cache += &SetAttribute(Attribute::NormalIntensity).to_string(); $( - queue!($self.terminal.stdout, Print($x))?; + $self.terminal.cache += &$x.to_string(); )* }; } @@ -124,6 +124,7 @@ impl Feedback { pub struct Terminal { pub stdout: Stdout, + pub cache: String, pub config: AnyUserData, } @@ -131,6 +132,7 @@ impl Terminal { pub fn new(config: AnyUserData) -> Self { Terminal { stdout: stdout(), + cache: String::with_capacity(size().map(|s| s.w * s.h).unwrap_or(1000)), config, } } @@ -167,12 +169,13 @@ impl Terminal { PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) )?; } + self.flush()?; Ok(()) } /// Restore terminal back to state before the editor was started pub fn end(&mut self) -> Result<()> { - self.show_cursor()?; + self.show_cursor(); terminal::disable_raw_mode()?; execute!( self.stdout, @@ -184,49 +187,49 @@ impl Terminal { if cfg.mouse_enabled { execute!(self.stdout, DisableMouseCapture)?; } + self.flush()?; Ok(()) } /// Shows the cursor on the screen - pub fn show_cursor(&mut self) -> Result<()> { - queue!(self.stdout, Show)?; - Ok(()) + pub fn show_cursor(&mut self) { + self.cache += &Show.to_string(); } /// Hides the cursor on the screen - pub fn hide_cursor(&mut self) -> Result<()> { - queue!(self.stdout, Hide)?; - Ok(()) + pub fn hide_cursor(&mut self) { + self.cache += &Hide.to_string(); } /// Moves the cursor to a specific position on screen - pub fn goto>(&mut self, x: Num, y: Num) -> Result<()> { + pub fn goto>(&mut self, x: Num, y: Num) { let x: usize = x.into(); let y: usize = y.into(); - queue!( - self.stdout, - MoveTo( - u16::try_from(x).unwrap_or(u16::MAX), - u16::try_from(y).unwrap_or(u16::MAX) - ) - )?; - Ok(()) + self.cache += &MoveTo( + u16::try_from(x).unwrap_or(u16::MAX), + u16::try_from(y).unwrap_or(u16::MAX), + ) + .to_string(); } /// Clears the current line - pub fn clear_current_line(&mut self) -> Result<()> { - queue!(self.stdout, Clear(ClType::CurrentLine))?; - Ok(()) + pub fn clear_current_line(&mut self) { + self.cache += &Clear(ClType::CurrentLine).to_string(); } /// Moves to a line and makes sure it is cleared - pub fn prepare_line(&mut self, y: usize) -> Result<()> { - self.goto(0, y)?; - self.clear_current_line() + pub fn prepare_line(&mut self, y: usize) { + self.goto(0, y); + self.clear_current_line(); } /// Flush the stdout (push the queued events to the screen) pub fn flush(&mut self) -> Result<()> { + let mut queue = String::new(); + std::mem::swap(&mut queue, &mut self.cache); + queue!(self.stdout, crossterm::style::Print(&queue))?; + queue.clear(); + std::mem::swap(&mut queue, &mut self.cache); self.stdout.flush()?; Ok(()) }