From a23cddfa2f570af912440d4a95778603537ea195 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:03:51 +0000 Subject: [PATCH 01/29] dependency bump --- Cargo.lock | 42 +++++++++++++++++++++--------------------- Cargo.toml | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9fc9a869..0eefc465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "serde", @@ -56,9 +56,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.36" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6ddbd668297c46be4bdea6c599dcc1f001a129586272d53170b7ac0a62961e" +checksum = "0ae9546e4a268c309804e8bbb7526e31cbfdedca7cd60ac1b987d0b212e0d876" dependencies = [ "bstr", "either", @@ -306,9 +306,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9eebac25c35a13285456c88ee2fde93d9aee8bcfdaf03f9d6d12be3391351ec" +checksum = "efa6bf1a64f06848749b7e7727417f4ec2121599e2a10ef0a8a3888b0e9a5a0d" dependencies = [ "cc", "cfg-if", @@ -340,7 +340,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.7.1" +version = "0.7.2" dependencies = [ "alinio", "base64", @@ -473,9 +473,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -506,9 +506,9 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags", "errno", @@ -525,18 +525,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -632,18 +632,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9e1d0b8d..040ab10a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [package] name = "ox" -version = "0.7.1" +version = "0.7.2" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A simple but flexible text editor." From 3f34bcf91b7536039aedab70c6a6b1e96bd2d73a Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:05:37 +0000 Subject: [PATCH 02/29] Groundwork for splits --- src/config/editor.rs | 53 ++--- src/config/interface.rs | 50 +++-- src/editor/documents.rs | 234 ++++++++++++++++++++- src/editor/editing.rs | 35 ++-- src/editor/interface.rs | 451 +++++++++++++++++++++------------------- src/editor/mod.rs | 170 ++++++++------- src/editor/mouse.rs | 5 +- src/editor/scanning.rs | 18 +- src/main.rs | 97 ++++++++- 9 files changed, 728 insertions(+), 385 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index fceccf99..e02af83e 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -1,6 +1,6 @@ /// Defines the Editor API for plug-ins to use use crate::cli::VERSION; -use crate::editor::Editor; +use crate::editor::{Editor, FileContainer}; use crate::ui::Feedback; use crate::{config, fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; @@ -46,13 +46,10 @@ impl LuaUserData for Editor { } }); fields.add_field_method_get("version", |_, _| Ok(VERSION)); - fields.add_field_method_get("current_document_id", |_, editor| Ok(editor.ptr)); - fields.add_field_method_get("document_count", |_, editor| Ok(editor.files.len())); + fields.add_field_method_get("current_document_id", |_, editor| Ok(editor.files.get_atom(editor.ptr.clone()).map(|a| a.1))); + fields.add_field_method_get("document_count", |_, editor| Ok(editor.files.get_all(editor.ptr.clone()).len())); fields.add_field_method_get("document_type", |_, editor| { - Ok(editor.files[editor.ptr] - .file_type - .clone() - .map_or("Unknown".to_string(), |t| t.name)) + Ok(editor.files.get(editor.ptr.clone()).unwrap_or(&FileContainer::default()).file_type.clone().map_or("Unknown".to_string(), |ft| ft.name)) }); fields.add_field_method_get("file_name", |_, editor| { if let Some(doc) = editor.try_doc() { @@ -473,7 +470,7 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("move_to_document", |_, editor, id: usize| { - editor.ptr = id; + editor.files.move_to(editor.ptr.clone(), id); Ok(()) }); methods.add_method_mut("new", |_, editor, ()| { @@ -569,8 +566,10 @@ impl LuaUserData for Editor { if let Some(file_type) = doc.file_types.get_name(&name) { let mut highlighter = file_type.get_highlighter(&editor.config, 4); highlighter.run(&editor.doc().lines); - editor.files[editor.ptr].highlighter = highlighter; - editor.files[editor.ptr].file_type = Some(file_type); + if let Some(file) = editor.files.get_mut(editor.ptr.clone()) { + file.highlighter = highlighter; + file.file_type = Some(file_type); + } } else { editor.feedback = Feedback::Error(format!("Invalid file type: {name}")); } @@ -584,38 +583,18 @@ impl LuaUserData for Editor { let _ = editor.render(lua); Ok(()) }); - methods.add_method_mut("rerender_feedback_line", |_, editor, ()| { + methods.add_method_mut("rerender_feedback_line", |lua, editor, ()| { + // Force a re-render + editor.needs_rerender = true; // 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); - editor.terminal.hide_cursor(); - // Apply render and restore cursor - if editor.try_doc().is_some() { - 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() { - editor.terminal.goto(x + max, y + editor.push_down); - } - } - editor.terminal.show_cursor(); - let _ = editor.terminal.flush(); + let _ = editor.render(lua); Ok(()) }); methods.add_method_mut("rerender_status_line", |lua, editor, ()| { + // Force a re-render + editor.needs_rerender = true; // 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); - 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() { - editor.terminal.goto(x + max, y + editor.push_down); - } - editor.terminal.show_cursor(); - let _ = editor.terminal.flush(); + let _ = editor.render(lua); Ok(()) }); // Miscellaneous diff --git a/src/config/interface.rs b/src/config/interface.rs index 31768c40..8ad75cf7 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -8,6 +8,7 @@ use kaolinite::searching::Searcher; use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; use mlua::prelude::*; use std::result::Result as RResult; +use std::ops::Range; use super::{issue_warning, Colors}; @@ -98,13 +99,23 @@ impl Default for GreetingMessage { impl GreetingMessage { /// Take the configuration information and render the greeting message - pub fn render(&self, lua: &Lua, colors: &Colors) -> Result { - let highlight = Fg(colors.highlight.to_color()?).to_string(); - let editor_fg = Fg(colors.editor_fg.to_color()?).to_string(); + pub fn render(&self, lua: &Lua) -> Result<(String, Vec)> { let mut result = self.format.clone(); + // Substitute in simple values result = result.replace("{version}", VERSION).to_string(); - result = result.replace("{highlight_start}", &highlight).to_string(); - result = result.replace("{highlight_end}", &editor_fg).to_string(); + result = result.replace("\t", " ").to_string(); + // Handle highlighted part + let start = result.find("{highlight_start}"); + let end = result.find("{highlight_end}"); + let highlighted = if let (Some(s), Some(e)) = (start, end) { + let s = result.chars().take(s).filter(|c| *c == '\n').count(); + let e = result.chars().take(e).filter(|c| *c == '\n').count(); + (s..=e).collect() + } else { + vec![] + }; + result = result.replace("{highlight_start}", "").to_string(); + result = result.replace("{highlight_end}", "").to_string(); // Find functions to call and substitute in let mut searcher = Searcher::new(r"\{[A-Za-z_][A-Za-z0-9_]*\}"); while let Some(m) = searcher.lfind(&result) { @@ -124,7 +135,7 @@ impl GreetingMessage { break; } } - Ok(result) + Ok((result, highlighted)) } } @@ -321,34 +332,27 @@ impl Default for StatusLine { impl StatusLine { /// Take the configuration information and render the status line - pub fn render(&self, editor: &Editor, lua: &Lua, w: usize) -> RResult { - let file = &editor.files[editor.ptr]; + pub fn render(&self, ptr: &Vec, editor: &Editor, lua: &Lua, w: usize) -> RResult { let mut result = vec![]; - let path = editor - .doc() + let fc = editor.files.get(ptr.clone()).unwrap(); + let doc = &fc.doc; + let path = doc .file_name .clone() .unwrap_or_else(|| "[No Name]".to_string()); let file_extension = get_file_ext(&path).unwrap_or_else(|| "Unknown".to_string()); 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 file_type = file - .file_type - .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() - .event_mgmt - .with_disk(&editor.doc().take_snapshot()) - { + let file_type = fc.file_type.clone().map_or("Unknown".to_string(), |ft| ft.name); + let icon = fc.file_type.clone().map_or("󰈙 ".to_string(), |ft| ft.icon); + let modified = if 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(); - let line_count = editor.doc().len_lines().to_string(); + let cursor_y = (doc.loc().y + 1).to_string(); + let cursor_x = doc.char_ptr.to_string(); + let line_count = doc.len_lines().to_string(); for part in &self.parts { let mut part = part.clone(); diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 5dee2b90..0d391f6d 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -1,10 +1,242 @@ /// Tools for placing all information about open files into one place -use crate::editor::FileType; +use crate::editor::{FileType, get_absolute_path}; use kaolinite::Document; use synoptic::Highlighter; +use std::ops::Range; +use kaolinite::Size; +// File split structure +#[derive(Debug)] +pub enum FileLayout { + /// Side-by-side documents (with proportions) + SideBySide(Vec<(FileLayout, f64)>), + /// Top-to-bottom documents (with proportions) + TopToBottom(Vec<(FileLayout, f64)>), + /// Single file container (and pointer for tabs) + Atom(Vec, usize), + /// Placeholder for an empty file split + None, +} + +impl Default for FileLayout { + fn default() -> Self { + Self::None + } +} + +impl FileLayout { + /// Will return file containers and what span of columns and rows they take up + /// In the format of (container, rows, columns) + pub fn span(&self, idx: Vec, size: Size) -> Vec<(Vec, Range, Range)> { + match self { + Self::None => vec![], + Self::Atom(containers, ptr) => vec![(idx, 0..size.h, 0..size.w)], + Self::SideBySide(layouts) => { + let mut result = vec![]; + let mut at = 0; + for (c, (layout, props)) in layouts.iter().enumerate() { + let mut subidx = idx.clone(); + subidx.push(c); + let this_size = Size { w: at + (size.w as f64 * props) as usize, h: size.h }; + for mut sub in layout.span(subidx, this_size) { + let mut end = sub.2.end; + if c == layouts.len().saturating_sub(1) { + end += size.w.saturating_sub(sub.2.end) + } else { + end -= 1; + } + sub.2 = at..end; + result.push(sub); + } + at = this_size.w; + } + result + } + Self::TopToBottom(layouts) => { + let mut result = vec![]; + let mut at = 0; + for (c, (layout, props)) in layouts.iter().enumerate() { + let mut subidx = idx.clone(); + subidx.push(c); + let this_size = Size { w: size.w, h: at + (size.h as f64 * props) as usize }; + for mut sub in layout.span(subidx, this_size) { + let mut end = sub.1.end; + if c == layouts.len().saturating_sub(1) { + end += size.h.saturating_sub(sub.1.end) + } else { + end -= 1; + result.push((vec![42].repeat(100), sub.1.clone(), sub.2.clone())); + } + sub.1 = at..end; + result.push(sub); + } + at = this_size.h; + } + result + } + } + } + + /// Work out which file containers to render where on a particular line and in what order + pub fn line(y: usize, spans: &Vec<(Vec, Range, Range)>) -> Vec<(Vec, Range, Range)> { + let mut appropriate: Vec<_> = spans + .iter() + .filter_map(|(ptr, rows, columns)| + if rows.contains(&y) { + Some((ptr.clone(), rows.clone(), columns.clone())) + } else { + None + } + ) + .collect(); + appropriate.sort_by(|a, b| a.1.start.cmp(&b.1.start)); + appropriate + } + + /// Work out how many files are currently open + pub fn len(&self) -> usize { + match self { + Self::None => 0, + Self::Atom(containers, _) => containers.len(), + Self::SideBySide(layouts) => { + layouts.iter().map(|(layout, _)| layout.len()).sum() + } + Self::TopToBottom(layouts) => { + layouts.iter().map(|(layout, _)| layout.len()).sum() + } + } + } + + /// Find a file container location from it's path + pub fn find(&self, idx: Vec, path: &str) -> Option<(Vec, usize)> { + match self { + Self::None => None, + Self::Atom(containers, _) => { + // Scan this atom for any documents + for (ptr, container) in containers.iter().enumerate() { + let file_path = container.doc.file_name.as_ref(); + let file_path = file_path.map(|f| get_absolute_path(f).unwrap_or_default()); + if file_path == Some(path.to_string()) { + return Some((idx, ptr)); + } + } + None + }, + Self::SideBySide(layouts) => { + // Recursively scan + for (nth, (layout, _)) in layouts.iter().enumerate() { + let mut this_idx = idx.clone(); + this_idx.push(nth); + let result = layout.find(this_idx, path.clone()); + if result.is_some() { + return result; + } + } + None + } + Self::TopToBottom(layouts) => { + // Recursively scan + for (nth, (layout, _)) in layouts.iter().enumerate() { + let mut this_idx = idx.clone(); + this_idx.push(nth); + let result = layout.find(this_idx, path.clone()); + if result.is_some() { + return result; + } + } + None + } + } + } + + /// Given an index, find the file containers in the tree + pub fn get_atom(&self, mut idx: Vec) -> Option<(Vec<&FileContainer>, usize)> { + match self { + Self::None => None, + Self::Atom(containers, ptr) => Some((containers.iter().collect(), *ptr)), + Self::SideBySide(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.get_atom(idx) + } + Self::TopToBottom(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.get_atom(idx) + } + } + } + + /// Given an index, find the file containers in the tree + pub fn get_atom_mut(&mut self, mut idx: Vec) -> Option<(&mut Vec, &mut usize)> { + match self { + Self::None => None, + Self::Atom(ref mut containers, ref mut ptr) => Some((containers, ptr)), + Self::SideBySide(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.get_atom_mut(idx) + } + Self::TopToBottom(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.get_atom_mut(idx) + } + } + } + + /// Given an index, find the file container in the tree + pub fn get_all(&self, idx: Vec) -> Vec<&FileContainer> { + self.get_atom(idx).map_or(vec![], |(fcs, _)| fcs) + } + + /// Given an index, find the file container in the tree + pub fn get_all_mut(&mut self, idx: Vec) -> Vec<&mut FileContainer> { + self.get_atom_mut(idx).map_or(vec![], |(fcs, _)| fcs.iter_mut().collect()) + } + + /// Given an index, find the file container in the tree + pub fn get(&self, idx: Vec) -> Option<&FileContainer> { + let (fcs, ptr) = self.get_atom(idx)?; + Some(fcs.get(ptr)?) + } + + /// Given an index, find the file container in the tree + pub fn get_mut(&mut self, idx: Vec) -> Option<&mut FileContainer> { + let (fcs, ptr) = self.get_atom_mut(idx)?; + Some(fcs.get_mut(*ptr)?) + } + + /// In the currently active atom, move to a different document + pub fn move_to(&mut self, mut idx: Vec, ptr: usize) { + match self { + Self::None => (), + Self::Atom(_, ref mut old_ptr) => *old_ptr = ptr, + Self::SideBySide(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.move_to(idx, ptr) + } + Self::TopToBottom(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.move_to(idx, ptr) + } + } + } +} + +/// Container for a file +#[derive(Debug)] pub struct FileContainer { + /// Document (stores kaolinite information) pub doc: Document, + /// Highlighter (stores synoptic information) pub highlighter: Highlighter, + /// File type (stores which file type this file is) pub file_type: Option, } + +impl Default for FileContainer { + fn default() -> Self { + Self { + doc: Document::new(Size { w: 10, h: 10 }), + highlighter: Highlighter::new(4), + file_type: None, + } + } +} diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 6bc54135..63f71d53 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -33,9 +33,10 @@ impl Editor { } else { let loc = self.doc().char_loc(); self.exe(Event::Insert(loc, ch.to_string()))?; - let file = &mut self.files[self.ptr]; - if !file.doc.info.read_only { - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + if !file.doc.info.read_only { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } } } Ok(()) @@ -51,12 +52,13 @@ impl Editor { // Enter pressed in the start, middle or end of the line let loc = self.doc().char_loc(); self.exe(Event::SplitDown(loc))?; - let file = &mut self.files[self.ptr]; - if !file.doc.info.read_only { - let line = &file.doc.lines[loc.y + 1]; - file.highlighter.insert_line(loc.y + 1, line); - let line = &file.doc.lines[loc.y]; - file.highlighter.edit(loc.y, line); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + if !file.doc.info.read_only { + let line = &file.doc.lines[loc.y + 1]; + file.highlighter.insert_line(loc.y + 1, line); + let line = &file.doc.lines[loc.y]; + file.highlighter.edit(loc.y, line); + } } } Ok(()) @@ -78,15 +80,15 @@ impl Editor { // Backspace was pressed on the start of the line, move line to the top self.new_row()?; let mut loc = self.doc().char_loc(); - let file = &self.files[self.ptr]; + let file = self.files.get_mut(self.ptr.clone()).unwrap(); if !file.doc.info.read_only { self.highlighter().remove_line(loc.y); } loc.y = loc.y.saturating_sub(1); - let file = &mut self.files[self.ptr]; + let file = self.files.get_mut(self.ptr.clone()).unwrap(); loc.x = file.doc.line(loc.y).unwrap().chars().count(); self.exe(Event::SpliceUp(loc))?; - let file = &mut self.files[self.ptr]; + let file = self.files.get_mut(self.ptr.clone()).unwrap(); let line = &file.doc.lines[loc.y]; if !file.doc.info.read_only { file.highlighter.edit(loc.y, line); @@ -101,7 +103,7 @@ impl Editor { y: self.doc().loc().y, }; self.exe(Event::Delete(loc, ch.to_string()))?; - let file = &mut self.files[self.ptr]; + let file = self.files.get_mut(self.ptr.clone()).unwrap(); if !file.doc.info.read_only { file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); } @@ -121,9 +123,10 @@ impl Editor { y: self.doc().loc().y, }; self.exe(Event::Delete(loc, ch.to_string()))?; - let file = &mut self.files[self.ptr]; - if !file.doc.info.read_only { - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + if !file.doc.info.read_only { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } } } } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index e723ae60..3a2e903a 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,9 +1,9 @@ /// Functions for rendering the UI -use crate::config; use crate::error::{OxError, Result}; use crate::events::wait_for_event_hog; use crate::ui::{key_event, size, Feedback}; -use crate::{display, handle_lua_error}; +use crate::editor::{FileLayout, FileContainer}; +use crate::{display, handle_lua_error, config}; use crossterm::{ event::{KeyCode as KCode, KeyModifiers as KMod}, style::{Attribute, Color, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, @@ -11,35 +11,87 @@ use crossterm::{ use kaolinite::utils::{file_or_dir, get_cwd, get_parent, list_dir, width, Loc, Size}; use mlua::Lua; use synoptic::{trim_fit, Highlighter, TokOpt}; +use std::ops::Range; use super::Editor; +/// Render cache to store the results of any calculations during rendering +#[derive(Default)] +pub struct RenderCache { + greeting_message: (String, Vec), + span: Vec<(Vec, Range, Range)>, +} + impl Editor { + pub fn update_render_cache(&mut self, lua: &Lua, size: Size) { + // Calculate greeting message + if config!(self.config, tab_line).enabled && self.greet { + if let Ok(gm) = config!(self.config, greeting_message).render(lua) { + self.render_cache.greeting_message = gm; + } + } + // Calculate span + self.render_cache.span = self.files.span(vec![], size); + } + + pub fn render_line(&mut self, y: usize, size: Size, lua: &Lua) -> Result { + let tab_line_enabled = config!(self.config, tab_line).enabled; + let mut result = String::new(); + let fcs = FileLayout::line(y, &self.render_cache.span); + for (mut c, (fc, rows, range)) in fcs.iter().enumerate() { + let length = range.end.saturating_sub(range.start); + // Insert horizontal bar where appropriate (horribly janky implementation, but it works) + if vec![42].repeat(100) == *fc { + if y == rows.end.saturating_sub(1) { + result += &"─".repeat(length); + } + continue; + } + let rel_y = y.saturating_sub(rows.start); + if y == rows.start && tab_line_enabled { + // Tab line + result += &self.render_tab_line(&fc, lua, length)?; + } else if y == rows.end.saturating_sub(1) { + // Status line + result += &self.render_status_line(&fc, lua, length)?; + } else { + // Line of file + result += &self.render_document(&fc, rel_y.saturating_sub(self.push_down), lua, length, size.h)?; + } + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + // Insert vertical bar where appropriate + if c != fcs.len().saturating_sub(1) { + result += &format!("{editor_bg}{editor_fg}│"); + } + } + Ok(result) + } + /// Render a single frame of the editor in it's current state pub fn render(&mut self, lua: &Lua) -> Result<()> { + // Determine if re-rendering is needed if !self.needs_rerender { return Ok(()); } self.needs_rerender = false; - self.terminal.hide_cursor(); - let Size { w, mut h } = size()?; + // Get size information and update the document's size + let mut size = size()?; + let Size { w, mut h } = size.clone(); h = h.saturating_sub(1 + self.push_down); - // Update the width of the document in case of update let max = self.dent(); self.doc_mut().size.w = w.saturating_sub(max); - // Render the tab line - if config!(self.config, tab_line).enabled { - self.render_tab_line(lua, w)?; - } - // Run through each line of the terminal, rendering the correct line - self.render_document(lua, w, h)?; - // Leave last line for status line - self.render_status_line(lua, w, h)?; - // Render greeting if applicable - if self.greet { - self.render_greeting(lua, w, h)?; + // Update the cache before rendering + self.update_render_cache(lua, size); + // Hide the cursor before rendering + self.terminal.hide_cursor(); + // Render each line of the document + for y in 0..size.h { + let line = self.render_line(y, size, lua)?; + self.terminal.goto(0, y); + display!(self, line); } - // Render feedback line + // Render the feedback line 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() { @@ -51,175 +103,140 @@ impl Editor { } /// Render the lines of the document - #[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 + pub fn render_document(&mut self, ptr: &Vec, y: usize, lua: &Lua, mut w: usize, h: usize) -> Result { + let mut result = String::new(); + // Get various information + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + let line_number_bg = Bg(config!(self.config, colors).line_number_bg.to_color()?); + let line_number_fg = Fg(config!(self.config, colors).line_number_fg.to_color()?); + let selection_bg = Bg(config!(self.config, colors).selection_bg.to_color()?); + let selection_fg = Fg(config!(self.config, colors).selection_fg.to_color()?); let colors = config!(self.config, colors).highlight.to_color()?; + let underline = SetAttribute(Attribute::Underlined); + let no_underline = SetAttribute(Attribute::NoUnderline); let tab_width = config!(self.config, document).tab_width; - let message = config!(self.config, help_message).render(lua); - let max_width = message - .iter() - .map(|(_, line)| width(line, tab_width)) - .max() - .unwrap_or(0) - + 5; - let message = message - .iter() - .map(|(hl, line)| { - if *hl { - format!("{}{line}", Fg(colors)) - } else { - line.to_owned() - } - }) - .collect::>(); - 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 line_numbers_enabled = config!(self.config, line_numbers).enabled; + let ln_pad_left = config!(self.config, line_numbers).padding_left; + let ln_pad_right = config!(self.config, line_numbers).padding_right; 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) - let required_width = - if config!(self.config, help_message).enabled && (start..=end).contains(&y) { - w.saturating_sub(self.dent()).saturating_sub(max_width) - } else { - w.saturating_sub(self.dent()) + let fc = self.files.get(ptr.clone()).unwrap(); + let doc = &fc.doc; + let mut total_width = 0; + // Render the line numbers if enabled + if line_numbers_enabled { + let num = doc.line_number(y + doc.offset.y); + let padding_left = " ".repeat(ln_pad_left); + let padding_right = " ".repeat(ln_pad_right); + result += &format!("{line_number_bg}{line_number_fg}{padding_left}{num}{padding_right}│{editor_fg}{editor_bg}"); + total_width += ln_pad_left + ln_pad_right + width(&num, tab_width) + 1; + w = w.saturating_sub(total_width); + } else { + result += &format!("{editor_fg}{editor_bg}"); + } + // Render the body of the document if available + let at_line = y + doc.offset.y; + if let Some(line) = doc.line(at_line) { + // Reset the cache + let mut cache_bg = editor_bg; + let mut cache_fg = editor_fg; + // Gather the tokens + let tokens = fc.highlighter.line(at_line, &line); + let tokens = trim_fit(&tokens, doc.offset.x, w, tab_width); + let mut x_disp = doc.offset.x; + let mut x_char = doc.character_idx(&doc.offset); + for token in tokens { + // Find out the text (and colour of that text) + let (text, colour) = match token { + // Non-highlighted text + TokOpt::Some(text, kind) => { + let colour = config!(self.config, syntax).get_theme(&kind); + let colour = match colour { + // Success, write token + Ok(col) => Fg(col), + // Failure, show error message and don't highlight this token + Err(err) => { + self.feedback = Feedback::Error(err.to_string()); + editor_fg + } + }; + (text, colour) + } + // Highlighted text + TokOpt::None(text) => (text, editor_fg), }; - // Go to the right location - 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()?); - let line_number_bg = Bg(config!(self.config, colors).line_number_bg.to_color()?); - let line_number_fg = Fg(config!(self.config, colors).line_number_fg.to_color()?); - let selection_bg = Bg(config!(self.config, colors).selection_bg.to_color()?); - let selection_fg = Fg(config!(self.config, colors).selection_fg.to_color()?); - // 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); - let padding_left = " ".repeat(config!(self.config, line_numbers).padding_left); - let padding_right = " ".repeat(config!(self.config, line_numbers).padding_right); - display!( - self, - line_number_bg, - line_number_fg, - padding_left, - num, - padding_right, - "│", - 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; - 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_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 { - // Non-highlighted text - TokOpt::Some(text, kind) => { - let colour = config!(self.config, syntax).get_theme(&kind); - let colour = match colour { - // Success, write token - Ok(col) => Fg(col), - // Failure, show error message and don't highlight this token - Err(err) => { - self.feedback = Feedback::Error(err.to_string()); - editor_fg - } - }; - (text, colour) + // Do the rendering (including selection where applicable) + for c in text.chars() { + let disp_loc = Loc { y: at_line, x: x_disp }; + let char_loc = Loc { y: at_line, x: x_char }; + // Work out selection + 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 { + result += &selection_bg.to_string(); + cache_bg = selection_bg; } - // Highlighted text - 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 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 { - display!(self, selection_bg); - cache_bg = selection_bg; - } - if cache_fg != selection_fg { - display!(self, selection_fg); - cache_fg = selection_fg; - } - } else { - if cache_bg != editor_bg { - display!(self, editor_bg); - cache_bg = editor_bg; - } - if cache_fg != colour { - display!(self, colour); - cache_fg = colour; - } + if cache_fg != selection_fg { + result += &selection_fg.to_string(); + cache_fg = selection_fg; } - // Render multi-cursors - 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)); + } else { + if cache_bg != editor_bg { + result += &editor_bg.to_string(); + cache_bg = editor_bg; } - // Render the character - display!(self, c); - // Reset any multi-cursor display - if multi_cursor_here { - display!(self, no_underline, cache_bg, cache_fg); + if cache_fg != colour { + result += &colour.to_string(); + cache_fg = colour; } - x_char += 1; - x_disp += width(&c.to_string(), tab_width); } + // Render multi-cursors + let multi_cursor_here = self.doc().has_cursor(char_loc).is_some(); + if multi_cursor_here { + result += &format!("{underline}{}{}", Bg(Color::White), Fg(Color::Black)); + } + // Render the character + result.push(c); + // Reset any multi-cursor display + if multi_cursor_here { + result += &format!("{no_underline}{cache_bg}{cache_fg}"); + } + x_char += 1; + let c_width = width(&c.to_string(), tab_width); + x_disp += c_width; + total_width += c_width; } - display!(self, editor_fg, editor_bg); - } else { - // Empty line, just pad out with spaces to prevent artefacts - display!(self, " ".repeat(required_width)); - } - // Render help message if applicable (otherwise, just output padding to clear buffer) - if config!(self.config, help_message).enabled && (start..=end).contains(&y) { - let idx = y.saturating_sub(start); - let line = message - .get(idx as usize) - .map_or(" ".repeat(max_width), std::string::ToString::to_string); - display!(self, line, " ".repeat(max_width)); } + result += &format!("{editor_fg}{editor_bg}{cache_fg}"); + result += &" ".repeat(w.saturating_sub(total_width)); + } else if config!(self.config, greeting_message).enabled && self.greet { + // Render the greeting message (if enabled) + result += &self.render_greeting(y, lua, w, h)?; + } else { + // Empty line, just pad out with spaces to prevent artefacts + result += &" ".repeat(w); } - Ok(()) + // Send out the result + Ok(result) } /// Get list of tabs - pub fn get_tab_parts(&mut self, lua: &Lua, w: usize) -> (Vec, usize, usize) { + pub fn get_tab_parts(&mut self, ptr: &Vec, lua: &Lua, w: usize) -> (Vec, usize, usize) { let mut headers: Vec = vec![]; let mut idx = 0; let mut length = 0; let mut offset = 0; let tab_line = config!(self.config, tab_line); - for (c, file) in self.files.iter().enumerate() { + for (c, file) in self.files.get_all(ptr.to_vec()).iter().enumerate() { let render = tab_line.render(lua, file, &mut self.feedback); length += width(&render, 4) + 1; headers.push(render); - if c == self.ptr { + let ptr = self.files.get_atom(self.ptr.clone()).map_or(0, |(_, ptr)| ptr); + if c == ptr { idx = headers.len().saturating_sub(1); } - while c == self.ptr && length > w && headers.len() > 1 { + while c == ptr && length > w && headers.len() > 1 { headers.remove(0); length = length.saturating_sub(width(&headers[0], 4) + 1); idx = headers.len().saturating_sub(1); @@ -231,71 +248,59 @@ 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); + pub fn render_tab_line(&mut self, ptr: &Vec, lua: &Lua, w: usize) -> Result { let tab_inactive_bg = Bg(config!(self.config, colors).tab_inactive_bg.to_color()?); let tab_inactive_fg = Fg(config!(self.config, colors).tab_inactive_fg.to_color()?); let tab_active_bg = Bg(config!(self.config, colors).tab_active_bg.to_color()?); let tab_active_fg = Fg(config!(self.config, colors).tab_active_fg.to_color()?); - let (tabs, idx, _) = self.get_tab_parts(lua, w); - display!(self, tab_inactive_fg, tab_inactive_bg); + let tab_width = config!(self.config, document).tab_width; + let mut current_width = 0; + let (tabs, idx, _) = self.get_tab_parts(ptr, lua, w); + let mut result = format!("{tab_inactive_fg}{tab_inactive_bg}"); for (c, header) in tabs.iter().enumerate() { if c == idx { - display!( - self, - tab_active_bg, - tab_active_fg, + result += &format!( + "{tab_active_bg}{tab_active_fg}{}{header}{}{tab_inactive_fg}{tab_inactive_bg}│", SetAttribute(Attribute::Bold), - header, SetAttribute(Attribute::Reset), - tab_inactive_fg, - tab_inactive_bg, - "│" ); } else { - display!(self, header, "│"); + result += &format!("{header}│"); } + current_width += width(header, tab_width) + 1; } - display!(self, " ".to_string().repeat(w)); - Ok(()) + result += &" ".to_string().repeat(w.saturating_sub(current_width)); + Ok(result) } /// 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); + pub fn render_status_line(&mut self, ptr: &Vec, lua: &Lua, w: usize) -> Result { let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); let status_bg = Bg(config!(self.config, colors).status_bg.to_color()?); let status_fg = Fg(config!(self.config, colors).status_fg.to_color()?); - match config!(self.config, status_line).render(self, lua, w) { + let mut result = String::new(); + result += &format!("{status_bg}{status_fg}"); + match config!(self.config, status_line).render(ptr, self, lua, w) { Ok(content) => { - display!( - self, - status_bg, - status_fg, - SetAttribute(Attribute::Bold), - content, - SetAttribute(Attribute::Reset), - editor_fg, - editor_bg - ); + if content.is_empty() { + result += &" ".repeat(w); + } else { + result += &format!( + "{}{content}{}", + SetAttribute(Attribute::Bold), + SetAttribute(Attribute::Reset), + ); + } } Err(lua_error) => { - display!( - self, - status_bg, - status_fg, - SetAttribute(Attribute::Bold), - " ".repeat(w), - SetAttribute(Attribute::Reset), - editor_fg, - editor_bg - ); + result += &" ".repeat(w); handle_lua_error("status_line", Err(lua_error), &mut self.feedback); } } - Ok(()) + result += &format!("{editor_fg}{editor_bg}"); + Ok(result) } /// Render the feedback line @@ -306,17 +311,27 @@ impl Editor { Ok(()) } - /// Render the help message - fn render_greeting(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { + /// Render the greeting message + fn render_greeting(&mut self, y: usize, lua: &Lua, w: usize, h: usize) -> Result { + // Produce the greeting message let colors = config!(self.config, colors); - let greeting = config!(self.config, greeting_message).render(lua, &colors)?; - let message: Vec<&str> = greeting.split('\n').collect(); - for (c, line) in message.iter().enumerate().take(h.saturating_sub(h / 4)) { - self.terminal.goto(4, h / 4 + c + 1); - let content = alinio::align::center(line, w.saturating_sub(4)).unwrap_or_default(); - display!(self, content); + let highlight = Fg(colors.highlight.to_color()?).to_string(); + let editor_fg = Fg(colors.editor_fg.to_color()?).to_string(); + let (message, highlights) = &self.render_cache.greeting_message; + let message: Vec<&str> = message.split('\n').collect(); + // Select the correct line + let greeting_span = (h / 4)..(h / 4 + message.len()); + let line = if greeting_span.contains(&y) { + message[y.saturating_sub(h / 4)] + } else { + "" + }; + let mut content = alinio::align::center(line, w).unwrap_or_default(); + if highlights.contains(&y.saturating_sub(h / 4)) { + content = format!("{highlight}{content}{editor_fg}"); } - Ok(()) + // Output + Ok(content) } /// Display a prompt in the document @@ -508,14 +523,15 @@ impl Editor { /// Append any missed lines to the syntax highlighter pub fn update_highlighter(&mut self) { if self.active { - let actual = self.files.get(self.ptr).map_or(0, |d| d.doc.info.loaded_to); + let actual = self.files.get(self.ptr.clone()).map_or(0, |fc| fc.doc.info.loaded_to); let percieved = self.highlighter().line_ref.len(); if percieved < actual { let diff = actual.saturating_sub(percieved); for i in 0..diff { - let file = &mut self.files[self.ptr]; - let line = &file.doc.lines[percieved + i]; - file.highlighter.append(line); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + let line = &file.doc.lines[percieved + i]; + file.highlighter.append(line); + } } } } @@ -523,18 +539,19 @@ impl Editor { /// Returns a highlighter at a certain index pub fn get_highlighter(&mut self, idx: usize) -> &mut Highlighter { - &mut self.files.get_mut(idx).unwrap().highlighter + &mut self.files.get_atom_mut(self.ptr.clone()).unwrap().0[idx].highlighter } /// Gets a mutable reference to the current document pub fn highlighter(&mut self) -> &mut Highlighter { - &mut self.files.get_mut(self.ptr).unwrap().highlighter + &mut self.files.get_mut(self.ptr.clone()).unwrap().highlighter } /// Reload the whole document in the highlighter pub fn reload_highlight(&mut self) { - let file = &mut self.files[self.ptr]; - file.highlighter.run(&file.doc.lines); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + file.highlighter.run(&file.doc.lines); + } } /// Work out how much to push the document to the right (to make way for line numbers) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 061b13f5..6bb81cb0 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -26,8 +26,9 @@ mod mouse; mod scanning; pub use cursor::{allowed_by_multi_cursor, handle_multiple_cursors}; -pub use documents::FileContainer; +pub use documents::{FileContainer, FileLayout}; pub use filetypes::{FileType, FileTypes}; +pub use interface::RenderCache; pub use macros::MacroMan; /// For managing all editing and rendering of cactus @@ -40,9 +41,9 @@ pub struct Editor { /// Configuration information for the editor pub config: Config, /// Storage of all the documents opened in the editor - pub files: Vec, + pub files: FileLayout, /// Pointer to the document that is currently being edited - pub ptr: usize, + pub ptr: Vec, /// true if the editor is still running, false otherwise pub active: bool, /// true if the editor should show a greeting message on next render @@ -65,6 +66,8 @@ pub struct Editor { pub alt_click_state: Option<(Loc, Loc)>, /// Macro manager pub macro_man: MacroMan, + /// Render cache + pub render_cache: RenderCache, } impl Editor { @@ -72,8 +75,8 @@ impl Editor { pub fn new(lua: &Lua) -> Result { let config = Config::new(lua)?; Ok(Self { - files: vec![], - ptr: 0, + files: FileLayout::Atom(vec![], 0), + ptr: vec![], terminal: Terminal::new(config.terminal.clone()), config, active: true, @@ -88,6 +91,7 @@ impl Editor { last_click: None, alt_click_state: None, macro_man: MacroMan::default(), + render_cache: RenderCache::default(), }) } @@ -115,10 +119,12 @@ impl Editor { file_type: Some(FileType::default()), doc, }; - if self.ptr + 1 >= self.files.len() { - self.files.push(file); - } else { - self.files.insert(self.ptr + 1, file); + if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + if *ptr + 1 >= files.len() { + files.push(file); + } else { + files.insert(*ptr + 1, file); + } } Ok(()) } @@ -133,7 +139,7 @@ impl Editor { /// Create a blank document if none are already opened pub fn new_if_empty(&mut self) -> Result<()> { // If no documents were provided, create a new empty document - if self.files.is_empty() { + if self.files.len() == 0 { self.blank()?; self.greet = config!(self.config, greeting_message).enabled; } @@ -142,8 +148,12 @@ impl Editor { /// Function to open a document into the 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; + // Check if a file is already opened + if let Some((idx, ptr)) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) { + // Move to existing file + self.ptr = idx.clone(); + self.files.move_to(idx, ptr); + // Send out error message let file = get_file_name(file_name).unwrap_or_default(); return Err(OxError::AlreadyOpen { file }); } @@ -167,10 +177,16 @@ impl Editor { highlighter, file_type, }; - if self.ptr + 1 >= self.files.len() { - self.files.push(file); + if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + // Atom already exists + if *ptr + 1 >= files.len() { + files.push(file); + } else { + files.insert(*ptr + 1, file); + } } else { - self.files.insert(self.ptr + 1, file); + // Atom ought to be created + self.files = FileLayout::Atom(vec![file], 0); } Ok(()) } @@ -191,22 +207,24 @@ impl Editor { if os.kind() == ErrorKind::NotFound { // Create a new document if not found self.blank()?; - let file = self.files.last_mut().unwrap(); - file.doc.file_name = Some(file_name); - // Work out information for the document - let tab_width = config!(self.config, document).tab_width; - let file_type = config!(self.config, document) - .file_types - .identify(&mut file.doc); - // Set up the document - file.doc.set_tab_width(tab_width); - // Attach the correct highlighter - let highlighter = file_type.clone().map_or(Highlighter::new(tab_width), |t| { - t.get_highlighter(&self.config, tab_width) - }); - file.highlighter = highlighter; - file.highlighter.run(&file.doc.lines); - file.file_type = file_type; + if let Some((files, _)) = self.files.get_atom_mut(self.ptr.clone()) { + let file = files.last_mut().unwrap(); + file.doc.file_name = Some(file_name); + // Work out information for the document + let tab_width = config!(self.config, document).tab_width; + let file_type = config!(self.config, document) + .file_types + .identify(&mut file.doc); + // Set up the document + file.doc.set_tab_width(tab_width); + // Attach the correct highlighter + let highlighter = file_type.clone().map_or(Highlighter::new(tab_width), |t| { + t.get_highlighter(&self.config, tab_width) + }); + file.highlighter = highlighter; + file.highlighter.run(&file.doc.lines); + file.file_type = file_type; + } Ok(()) } else { file @@ -217,15 +235,9 @@ impl Editor { } /// Determine if a file is already open - pub fn already_open(&mut self, abs_path: &str) -> Option { - for (ptr, file) in self.files.iter().enumerate() { - let file_path = file.doc.file_name.as_ref(); - let file_path = file_path.map(|f| get_absolute_path(f).unwrap_or_default()); - if file_path == Some(abs_path.to_string()) { - return Some(ptr); - } - } - None + /// TODO: Requires a rewrite (you shouldn't be able to open a duplicate file in another split) + pub fn already_open(&mut self, abs_path: &str) -> Option<(Vec, usize)> { + self.files.find(vec![], abs_path) } /// save the document to the disk @@ -242,19 +254,21 @@ impl Editor { let file_name = self.prompt("Save as")?; self.doc_mut().save_as(&file_name)?; if self.doc().file_name.is_none() { - // Get information about the document - let file = self.files.last_mut().unwrap(); - let tab_width = config!(self.config, document).tab_width; - let file_type = config!(self.config, document) - .file_types - .identify(&mut file.doc); - // Reattach an appropriate highlighter - let highlighter = file_type.map_or(Highlighter::new(tab_width), |t| { - t.get_highlighter(&self.config, tab_width) - }); - file.highlighter = highlighter; - file.highlighter.run(&file.doc.lines); - file.doc.file_name = Some(file_name.clone()); + if let Some((files, _)) = self.files.get_atom_mut(self.ptr.clone()) { + // Get information about the document + let file = files.last_mut().unwrap(); + let tab_width = config!(self.config, document).tab_width; + let file_type = config!(self.config, document) + .file_types + .identify(&mut file.doc); + // Reattach an appropriate highlighter + let highlighter = file_type.map_or(Highlighter::new(tab_width), |t| { + t.get_highlighter(&self.config, tab_width) + }); + file.highlighter = highlighter; + file.highlighter.run(&file.doc.lines); + file.doc.file_name = Some(file_name.clone()); + } } // Commit events to event manager (for undo / redo) self.doc_mut().commit(); @@ -265,10 +279,12 @@ impl Editor { /// Save all the open documents to the disk pub fn save_all(&mut self) -> Result<()> { - for file in &mut self.files { - file.doc.save()?; - // Commit events to event manager (for undo / redo) - file.doc.commit(); + if let Some((files, _)) = self.files.get_atom_mut(self.ptr.clone()) { + for file in files { + file.doc.save()?; + // Commit events to event manager (for undo / redo) + file.doc.commit(); + } } self.feedback = Feedback::Info("Saved all documents".to_string()); Ok(()) @@ -276,32 +292,38 @@ impl Editor { /// Quit the editor pub fn quit(&mut self) -> Result<()> { - self.active = !self.files.is_empty(); + self.active = self.files.len() != 0; // 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)? { - self.files.remove(self.ptr); - self.prev(); + if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + files.remove(*ptr); + self.prev(); + } } } - self.active = !self.files.is_empty(); + self.active = self.files.len() != 0; Ok(()) } /// Move to the next document opened in the editor pub fn next(&mut self) { - if self.ptr + 1 < self.files.len() { - self.ptr += 1; - self.update_cwd(); + if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + if *ptr + 1 < files.len() { + *ptr += 1; + self.update_cwd(); + } } } /// Move to the previous document opened in the editor pub fn prev(&mut self) { - if self.ptr != 0 { - self.ptr = self.ptr.saturating_sub(1); - self.update_cwd(); + if let Some((_, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + if *ptr != 0 { + *ptr = ptr.saturating_sub(1); + self.update_cwd(); + } } } @@ -317,32 +339,32 @@ impl Editor { /// Try to get a document pub fn try_doc(&self) -> Option<&Document> { - self.files.get(self.ptr).map(|file| &file.doc) + self.files.get(self.ptr.clone()).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) + self.files.get_mut(self.ptr.clone()).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 + &mut self.files.get_atom_mut(self.ptr.clone()).unwrap().0[idx].doc } /// Gets a reference to the current document pub fn doc(&self) -> &Document { - &self.files.get(self.ptr).unwrap().doc + &self.files.get(self.ptr.clone()).unwrap().doc } /// Gets a mutable reference to the current document pub fn doc_mut(&mut self) -> &mut Document { - &mut self.files.get_mut(self.ptr).unwrap().doc + &mut self.files.get_mut(self.ptr.clone()).unwrap().doc } /// Gets the number of documents currently open pub fn doc_len(&mut self) -> usize { - self.files.len() + self.files.get_atom(self.ptr.clone()).unwrap().0.len() } /// Load the configuration values @@ -416,7 +438,7 @@ impl Editor { // Ensure all lines in viewport are loaded let max = self.dent(); self.doc_mut().size.w = w.saturating_sub(u16::try_from(max).unwrap_or(u16::MAX)) as usize; - self.doc_mut().size.h = h.saturating_sub(3) as usize; + self.doc_mut().size.h = h.saturating_sub(2) as usize; let max = self.doc().offset.y + self.doc().size.h; self.doc_mut().load_to(max + 1); } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index c52d7db8..ec0707ee 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -24,7 +24,7 @@ impl Editor { let tab_enabled = config!(self.config, tab_line).enabled; let tab = usize::from(tab_enabled); if event.row == 0 && tab_enabled { - let (tabs, _, offset) = self.get_tab_parts(lua, size().map_or(0, |s| s.w)); + let (tabs, _, offset) = self.get_tab_parts(&self.ptr.clone(), lua, size().map_or(0, |s| s.w)); let mut c = event.column + 2; for (i, header) in tabs.iter().enumerate() { let header_len = width(header, 4) + 1; @@ -71,7 +71,8 @@ impl Editor { self.doc_mut().old_cursor = self.doc().loc().x; } MouseLocation::Tabs(i) => { - self.ptr = i; + todo!("CHANGING TABS"); + //self.ptr = i; self.update_cwd(); } MouseLocation::Out => (), diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 56dafe04..8e128115 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -34,7 +34,7 @@ impl Editor { " ".to_string().repeat(w) ); self.terminal.hide_cursor(); - self.render_document(lua, w, h.saturating_sub(2))?; + self.render(lua)?; // Move back to correct cursor position if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { let max = self.dent(); @@ -80,7 +80,7 @@ impl Editor { while !done { // Render just the document part self.terminal.hide_cursor(); - self.render_document(lua, w, h.saturating_sub(2))?; + self.render(lua)?; // Render custom status line with mode information self.terminal.goto(0, h); display!( @@ -174,7 +174,7 @@ impl Editor { while !done { // Render just the document part self.terminal.hide_cursor(); - self.render_document(lua, w, h.saturating_sub(2))?; + self.render(lua)?; // Write custom status line for the replace mode self.terminal.goto(0, h); display!( @@ -227,8 +227,9 @@ impl Editor { self.doc_mut().move_to(&loc); // Update syntax highlighter self.update_highlighter(); - let file = &mut self.files[self.ptr]; - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } Ok(()) } @@ -241,9 +242,10 @@ impl Editor { while let Some(mtch) = self.doc_mut().next_match(target, 1) { drop(self.doc_mut().replace(mtch.loc, &mtch.text, into)); self.update_highlighter(); - let file = &mut self.files[self.ptr]; - file.highlighter - .edit(mtch.loc.y, &file.doc.lines[mtch.loc.y]); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + file.highlighter + .edit(mtch.loc.y, &file.doc.lines[mtch.loc.y]); + } } } } diff --git a/src/main.rs b/src/main.rs index ad38ec06..c7f4e85d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,86 @@ macro_rules! ged { }; } +/* +/// TEMPORARY - REMOVE WHEN SPLITS ARE IMPLEMENTED +use crate::editor::{FileLayout, FileContainer}; +use kaolinite::{Document, Size}; +use synoptic::Highlighter; +fn main() { + let lua = Lua::new(); + // Create editor + let mut editor = match Editor::new(&lua) { + Ok(editor) => editor, + Err(error) => panic!("Editor failed to start: {error:?}"), + }; + + lua.load(PLUGIN_BOOTSTRAP).exec().unwrap(); + editor.load_config("/home/luke/.oxrc", &lua); + lua.load(PLUGIN_RUN).exec().unwrap(); + + editor.files = FileLayout::SideBySide(vec![ + ( + FileLayout::Atom(vec![ + FileContainer { + doc: Document::open(Size { w: 0, h: 0 }, "src/main.rs").unwrap(), + highlighter: synoptic::from_extension("rs", 4).unwrap(), + file_type: None, + } + ], 0), + 0.45, + ), + ( + FileLayout::TopToBottom(vec![ + ( + FileLayout::Atom(vec![ + FileContainer { + doc: Document::new(Size { w: 0, h: 0 }), + highlighter: Highlighter::new(4), + file_type: None, + } + ], 0), + 0.4, + ), + ( + FileLayout::Atom(vec![ + FileContainer { + doc: Document::open(Size { w: 0, h: 0 }, "plugins/todo.lua").unwrap(), + highlighter: synoptic::from_extension("lua", 4).unwrap(), + file_type: None, + } + ], 0), + 0.6, + ), + ]), + 0.55, + ), + ]); + editor.active = true; + editor.ptr = vec![1, 0]; + editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + editor.update_highlighter(); + editor.ptr = vec![1, 1]; + editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + editor.update_highlighter(); + editor.ptr = vec![0]; + editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + editor.update_highlighter(); + + let viewport = Size { w: 154, h: 40 }; + + for i in 0..viewport.h { + let output = editor.render_line( + i, + viewport, + &lua + ).unwrap(); + + println!("{output}"); + } +} +*/ + + /// Entry point - grabs command line arguments and runs the editor fn main() { // Interact with user to find out what they want to do @@ -59,7 +139,6 @@ fn main() { panic!("{err:?}"); } } - /// Run the editor #[allow(clippy::too_many_lines)] fn run(cli: &CommandLineInterface) -> Result<()> { @@ -132,7 +211,8 @@ fn run(cli: &CommandLineInterface) -> Result<()> { let mut highlighter = file_type.get_highlighter(&ged!(&editor).config, tab_width); highlighter.run(&ged!(mut &editor).get_doc(c).lines); let mut editor = ged!(mut &editor); - let file = editor.files.get_mut(c).unwrap(); + let current_ptr = editor.ptr.clone(); + let file = &mut editor.files.get_atom_mut(current_ptr).unwrap().0[c]; file.highlighter = highlighter; file.file_type = Some(file_type); } @@ -140,15 +220,17 @@ fn run(cli: &CommandLineInterface) -> Result<()> { ged!(mut &editor).next(); } // Reset the pointer back to the first document - ged!(mut &editor).ptr = 0; - + let current_ptr = ged!(mut &editor).ptr.clone(); + ged!(mut &editor).files.move_to(current_ptr, 0); + // Handle stdin if applicable if cli.flags.stdin { let stdin = cli::get_stdin(); let mut holder = ged!(mut &editor); holder.blank()?; let this_doc = holder.doc_len().saturating_sub(1); - let doc = holder.get_doc(this_doc); + let current_ptr = holder.ptr.clone(); + let mut doc = &mut holder.files.get_atom_mut(current_ptr).unwrap().0[this_doc].doc; doc.exe(Event::Insert(Loc { x: 0, y: 0 }, stdin))?; doc.load_to(doc.size.h); let lines = doc.lines.clone(); @@ -161,7 +243,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // Create a blank document if none are opened ged!(mut &editor).new_if_empty()?; - + // Add in the plugin manager handle_lua_error( "", @@ -213,6 +295,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { Ok(()) } + fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { // Clear screen of temporary items (expect on resize event) if !matches!(event, CEvent::Resize(_, _)) { @@ -345,7 +428,7 @@ fn handle_file_opening(editor: &AnyUserData, result: Result<()>, name: &str) { Ok(()) => (), Err(OxError::AlreadyOpen { .. }) => { let len = ged!(&editor).files.len().saturating_sub(1); - ged!(mut &editor).ptr = len; + ged!(mut &editor).files.move_to(ged!(&editor).ptr.clone(), len); } Err(OxError::Kaolinite(kerr)) => { match kerr { From ead315689b4a113c4541a12543de6ca968e65ada Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:13:31 +0000 Subject: [PATCH 03/29] Slightly cleaner code --- src/config/interface.rs | 14 ++++++++------ src/editor/documents.rs | 6 ++++-- src/editor/interface.rs | 2 ++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/config/interface.rs b/src/config/interface.rs index 8ad75cf7..8bbcc386 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -12,6 +12,8 @@ use std::ops::Range; use super::{issue_warning, Colors}; +type LuaRes = RResult; + /// For storing general configuration related to the terminal functionality #[derive(Debug)] pub struct Terminal { @@ -246,8 +248,8 @@ impl Default for TabLine { impl TabLine { /// Take the configuration information and render the tab line - pub fn render(&self, lua: &Lua, file: &FileContainer, feedback: &mut Feedback) -> String { - let path = file + pub fn render(&self, lua: &Lua, fc: &FileContainer, fb: &mut Feedback) -> String { + let path = fc .doc .file_name .clone() @@ -255,8 +257,8 @@ impl TabLine { let file_extension = get_file_ext(&path).unwrap_or_else(|| "Unknown".to_string()); 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.event_mgmt.with_disk(&file.doc.take_snapshot()) { + let icon = fc.file_type.clone().map_or("󰈙 ".to_string(), |t| t.icon); + let modified = if fc.doc.event_mgmt.with_disk(&fc.doc.take_snapshot()) { "" } else { "[+]" @@ -287,7 +289,7 @@ impl TabLine { result = result.replace(&m.text, r.to_string_lossy().as_str()); } Err(e) => { - *feedback = Feedback::Error(format!("Error occured in tab line: {e:?}")); + *fb = Feedback::Error(format!("Error occured in tab line: {e:?}")); break; } } @@ -332,7 +334,7 @@ impl Default for StatusLine { impl StatusLine { /// Take the configuration information and render the status line - pub fn render(&self, ptr: &Vec, editor: &Editor, lua: &Lua, w: usize) -> RResult { + pub fn render(&self, ptr: &Vec, editor: &Editor, lua: &Lua, w: usize) -> LuaRes { let mut result = vec![]; let fc = editor.files.get(ptr.clone()).unwrap(); let doc = &fc.doc; diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 0d391f6d..dd54c493 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -5,6 +5,8 @@ use synoptic::Highlighter; use std::ops::Range; use kaolinite::Size; +pub type Span = Vec<(Vec, Range, Range)>; + // File split structure #[derive(Debug)] pub enum FileLayout { @@ -27,7 +29,7 @@ impl Default for FileLayout { impl FileLayout { /// Will return file containers and what span of columns and rows they take up /// In the format of (container, rows, columns) - pub fn span(&self, idx: Vec, size: Size) -> Vec<(Vec, Range, Range)> { + pub fn span(&self, idx: Vec, size: Size) -> Span { match self { Self::None => vec![], Self::Atom(containers, ptr) => vec![(idx, 0..size.h, 0..size.w)], @@ -78,7 +80,7 @@ impl FileLayout { } /// Work out which file containers to render where on a particular line and in what order - pub fn line(y: usize, spans: &Vec<(Vec, Range, Range)>) -> Vec<(Vec, Range, Range)> { + pub fn line(y: usize, spans: &Span) -> Span { let mut appropriate: Vec<_> = spans .iter() .filter_map(|(ptr, rows, columns)| diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 3a2e903a..068c8774 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -23,6 +23,7 @@ pub struct RenderCache { } impl Editor { + /// Update the render cache pub fn update_render_cache(&mut self, lua: &Lua, size: Size) { // Calculate greeting message if config!(self.config, tab_line).enabled && self.greet { @@ -34,6 +35,7 @@ impl Editor { self.render_cache.span = self.files.span(vec![], size); } + /// Render a specific line pub fn render_line(&mut self, y: usize, size: Size, lua: &Lua) -> Result { let tab_line_enabled = config!(self.config, tab_line).enabled; let mut result = String::new(); From 3a0891cb50a79b0b8796e6a81bb5a59face2485e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:16:59 +0000 Subject: [PATCH 04/29] Rustfmt --- src/config/editor.rs | 16 +++++++++-- src/config/interface.rs | 7 +++-- src/editor/documents.rs | 62 ++++++++++++++++++++++------------------- src/editor/interface.rs | 54 +++++++++++++++++++++++++++-------- src/editor/mod.rs | 8 ++++-- src/editor/mouse.rs | 3 +- src/main.rs | 20 ++++++------- 7 files changed, 113 insertions(+), 57 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index e02af83e..6de8e26f 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -46,10 +46,20 @@ impl LuaUserData for Editor { } }); fields.add_field_method_get("version", |_, _| Ok(VERSION)); - fields.add_field_method_get("current_document_id", |_, editor| Ok(editor.files.get_atom(editor.ptr.clone()).map(|a| a.1))); - fields.add_field_method_get("document_count", |_, editor| Ok(editor.files.get_all(editor.ptr.clone()).len())); + fields.add_field_method_get("current_document_id", |_, editor| { + Ok(editor.files.get_atom(editor.ptr.clone()).map(|a| a.1)) + }); + fields.add_field_method_get("document_count", |_, editor| { + Ok(editor.files.get_all(editor.ptr.clone()).len()) + }); fields.add_field_method_get("document_type", |_, editor| { - Ok(editor.files.get(editor.ptr.clone()).unwrap_or(&FileContainer::default()).file_type.clone().map_or("Unknown".to_string(), |ft| ft.name)) + Ok(editor + .files + .get(editor.ptr.clone()) + .unwrap_or(&FileContainer::default()) + .file_type + .clone() + .map_or("Unknown".to_string(), |ft| ft.name)) }); fields.add_field_method_get("file_name", |_, editor| { if let Some(doc) = editor.try_doc() { diff --git a/src/config/interface.rs b/src/config/interface.rs index 8bbcc386..7e49dcde 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -7,8 +7,8 @@ use crossterm::style::SetForegroundColor as Fg; use kaolinite::searching::Searcher; use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; use mlua::prelude::*; -use std::result::Result as RResult; use std::ops::Range; +use std::result::Result as RResult; use super::{issue_warning, Colors}; @@ -345,7 +345,10 @@ impl StatusLine { let file_extension = get_file_ext(&path).unwrap_or_else(|| "Unknown".to_string()); 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 file_type = fc.file_type.clone().map_or("Unknown".to_string(), |ft| ft.name); + let file_type = fc + .file_type + .clone() + .map_or("Unknown".to_string(), |ft| ft.name); let icon = fc.file_type.clone().map_or("󰈙 ".to_string(), |ft| ft.icon); let modified = if doc.event_mgmt.with_disk(&editor.doc().take_snapshot()) { "" diff --git a/src/editor/documents.rs b/src/editor/documents.rs index dd54c493..cbde7216 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -1,9 +1,9 @@ /// Tools for placing all information about open files into one place -use crate::editor::{FileType, get_absolute_path}; +use crate::editor::{get_absolute_path, FileType}; use kaolinite::Document; -use synoptic::Highlighter; -use std::ops::Range; use kaolinite::Size; +use std::ops::Range; +use synoptic::Highlighter; pub type Span = Vec<(Vec, Range, Range)>; @@ -39,10 +39,13 @@ impl FileLayout { for (c, (layout, props)) in layouts.iter().enumerate() { let mut subidx = idx.clone(); subidx.push(c); - let this_size = Size { w: at + (size.w as f64 * props) as usize, h: size.h }; + let this_size = Size { + w: at + (size.w as f64 * props) as usize, + h: size.h, + }; for mut sub in layout.span(subidx, this_size) { let mut end = sub.2.end; - if c == layouts.len().saturating_sub(1) { + if c == layouts.len().saturating_sub(1) { end += size.w.saturating_sub(sub.2.end) } else { end -= 1; @@ -60,7 +63,10 @@ impl FileLayout { for (c, (layout, props)) in layouts.iter().enumerate() { let mut subidx = idx.clone(); subidx.push(c); - let this_size = Size { w: size.w, h: at + (size.h as f64 * props) as usize }; + let this_size = Size { + w: size.w, + h: at + (size.h as f64 * props) as usize, + }; for mut sub in layout.span(subidx, this_size) { let mut end = sub.1.end; if c == layouts.len().saturating_sub(1) { @@ -78,37 +84,33 @@ impl FileLayout { } } } - + /// Work out which file containers to render where on a particular line and in what order pub fn line(y: usize, spans: &Span) -> Span { let mut appropriate: Vec<_> = spans .iter() - .filter_map(|(ptr, rows, columns)| - if rows.contains(&y) { + .filter_map(|(ptr, rows, columns)| { + if rows.contains(&y) { Some((ptr.clone(), rows.clone(), columns.clone())) } else { None } - ) + }) .collect(); appropriate.sort_by(|a, b| a.1.start.cmp(&b.1.start)); appropriate } - + /// Work out how many files are currently open pub fn len(&self) -> usize { match self { Self::None => 0, Self::Atom(containers, _) => containers.len(), - Self::SideBySide(layouts) => { - layouts.iter().map(|(layout, _)| layout.len()).sum() - } - Self::TopToBottom(layouts) => { - layouts.iter().map(|(layout, _)| layout.len()).sum() - } + Self::SideBySide(layouts) => layouts.iter().map(|(layout, _)| layout.len()).sum(), + Self::TopToBottom(layouts) => layouts.iter().map(|(layout, _)| layout.len()).sum(), } } - + /// Find a file container location from it's path pub fn find(&self, idx: Vec, path: &str) -> Option<(Vec, usize)> { match self { @@ -123,7 +125,7 @@ impl FileLayout { } } None - }, + } Self::SideBySide(layouts) => { // Recursively scan for (nth, (layout, _)) in layouts.iter().enumerate() { @@ -150,7 +152,7 @@ impl FileLayout { } } } - + /// Given an index, find the file containers in the tree pub fn get_atom(&self, mut idx: Vec) -> Option<(Vec<&FileContainer>, usize)> { match self { @@ -166,9 +168,12 @@ impl FileLayout { } } } - + /// Given an index, find the file containers in the tree - pub fn get_atom_mut(&mut self, mut idx: Vec) -> Option<(&mut Vec, &mut usize)> { + pub fn get_atom_mut( + &mut self, + mut idx: Vec, + ) -> Option<(&mut Vec, &mut usize)> { match self { Self::None => None, Self::Atom(ref mut containers, ref mut ptr) => Some((containers, ptr)), @@ -182,29 +187,30 @@ impl FileLayout { } } } - + /// Given an index, find the file container in the tree pub fn get_all(&self, idx: Vec) -> Vec<&FileContainer> { self.get_atom(idx).map_or(vec![], |(fcs, _)| fcs) } - + /// Given an index, find the file container in the tree pub fn get_all_mut(&mut self, idx: Vec) -> Vec<&mut FileContainer> { - self.get_atom_mut(idx).map_or(vec![], |(fcs, _)| fcs.iter_mut().collect()) + self.get_atom_mut(idx) + .map_or(vec![], |(fcs, _)| fcs.iter_mut().collect()) } - + /// Given an index, find the file container in the tree pub fn get(&self, idx: Vec) -> Option<&FileContainer> { let (fcs, ptr) = self.get_atom(idx)?; Some(fcs.get(ptr)?) } - + /// Given an index, find the file container in the tree pub fn get_mut(&mut self, idx: Vec) -> Option<&mut FileContainer> { let (fcs, ptr) = self.get_atom_mut(idx)?; Some(fcs.get_mut(*ptr)?) } - + /// In the currently active atom, move to a different document pub fn move_to(&mut self, mut idx: Vec, ptr: usize) { match self { diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 068c8774..8187da40 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,17 +1,17 @@ +use crate::editor::{FileContainer, FileLayout}; /// Functions for rendering the UI use crate::error::{OxError, Result}; use crate::events::wait_for_event_hog; use crate::ui::{key_event, size, Feedback}; -use crate::editor::{FileLayout, FileContainer}; -use crate::{display, handle_lua_error, config}; +use crate::{config, display, handle_lua_error}; use crossterm::{ event::{KeyCode as KCode, KeyModifiers as KMod}, 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; -use synoptic::{trim_fit, Highlighter, TokOpt}; use std::ops::Range; +use synoptic::{trim_fit, Highlighter, TokOpt}; use super::Editor; @@ -58,7 +58,15 @@ impl Editor { result += &self.render_status_line(&fc, lua, length)?; } else { // Line of file - result += &self.render_document(&fc, rel_y.saturating_sub(self.push_down), lua, length, size.h)?; + result += &self.render_document( + &fc, + rel_y.saturating_sub(self.push_down), + lua, + Size { + w: length, + h: size.h, + }, + )?; } let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); @@ -105,7 +113,14 @@ impl Editor { } /// Render the lines of the document - pub fn render_document(&mut self, ptr: &Vec, y: usize, lua: &Lua, mut w: usize, h: usize) -> Result { + pub fn render_document( + &mut self, + ptr: &Vec, + y: usize, + lua: &Lua, + size: Size, + ) -> Result { + let Size { mut w, h } = size; let mut result = String::new(); // Get various information let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); @@ -169,8 +184,14 @@ impl Editor { }; // Do the rendering (including selection where applicable) for c in text.chars() { - let disp_loc = Loc { y: at_line, x: x_disp }; - let char_loc = Loc { y: at_line, x: x_char }; + let disp_loc = Loc { + y: at_line, + x: x_disp, + }; + let char_loc = Loc { + y: at_line, + x: x_char, + }; // Work out selection let is_selected = self.doc().is_this_loc_selected_disp(disp_loc, selection); // Render the correct colour @@ -213,7 +234,7 @@ impl Editor { result += &format!("{editor_fg}{editor_bg}{cache_fg}"); result += &" ".repeat(w.saturating_sub(total_width)); } else if config!(self.config, greeting_message).enabled && self.greet { - // Render the greeting message (if enabled) + // Render the greeting message (if enabled) result += &self.render_greeting(y, lua, w, h)?; } else { // Empty line, just pad out with spaces to prevent artefacts @@ -224,7 +245,12 @@ impl Editor { } /// Get list of tabs - pub fn get_tab_parts(&mut self, ptr: &Vec, lua: &Lua, w: usize) -> (Vec, usize, usize) { + pub fn get_tab_parts( + &mut self, + ptr: &Vec, + lua: &Lua, + w: usize, + ) -> (Vec, usize, usize) { let mut headers: Vec = vec![]; let mut idx = 0; let mut length = 0; @@ -234,7 +260,10 @@ impl Editor { let render = tab_line.render(lua, file, &mut self.feedback); length += width(&render, 4) + 1; headers.push(render); - let ptr = self.files.get_atom(self.ptr.clone()).map_or(0, |(_, ptr)| ptr); + let ptr = self + .files + .get_atom(self.ptr.clone()) + .map_or(0, |(_, ptr)| ptr); if c == ptr { idx = headers.len().saturating_sub(1); } @@ -525,7 +554,10 @@ impl Editor { /// Append any missed lines to the syntax highlighter pub fn update_highlighter(&mut self) { if self.active { - let actual = self.files.get(self.ptr.clone()).map_or(0, |fc| fc.doc.info.loaded_to); + let actual = self + .files + .get(self.ptr.clone()) + .map_or(0, |fc| fc.doc.info.loaded_to); let percieved = self.highlighter().line_ref.len(); if percieved < actual { let diff = actual.saturating_sub(percieved); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 6bb81cb0..27e88be6 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -149,7 +149,9 @@ impl Editor { /// Function to open a document into the editor pub fn open(&mut self, file_name: &str) -> Result<()> { // Check if a file is already opened - if let Some((idx, ptr)) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) { + if let Some((idx, ptr)) = + self.already_open(&get_absolute_path(file_name).unwrap_or_default()) + { // Move to existing file self.ptr = idx.clone(); self.files.move_to(idx, ptr); @@ -344,7 +346,9 @@ impl Editor { /// Try to get a document pub fn try_doc_mut(&mut self) -> Option<&mut Document> { - self.files.get_mut(self.ptr.clone()).map(|file| &mut file.doc) + self.files + .get_mut(self.ptr.clone()) + .map(|file| &mut file.doc) } /// Returns a document at a certain index diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index ec0707ee..39e7f264 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -24,7 +24,8 @@ impl Editor { let tab_enabled = config!(self.config, tab_line).enabled; let tab = usize::from(tab_enabled); if event.row == 0 && tab_enabled { - let (tabs, _, offset) = self.get_tab_parts(&self.ptr.clone(), lua, size().map_or(0, |s| s.w)); + let (tabs, _, offset) = + self.get_tab_parts(&self.ptr.clone(), lua, size().map_or(0, |s| s.w)); let mut c = event.column + 2; for (i, header) in tabs.iter().enumerate() { let header_len = width(header, 4) + 1; diff --git a/src/main.rs b/src/main.rs index c7f4e85d..848b22fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,11 +49,11 @@ fn main() { Ok(editor) => editor, Err(error) => panic!("Editor failed to start: {error:?}"), }; - + lua.load(PLUGIN_BOOTSTRAP).exec().unwrap(); editor.load_config("/home/luke/.oxrc", &lua); lua.load(PLUGIN_RUN).exec().unwrap(); - + editor.files = FileLayout::SideBySide(vec![ ( FileLayout::Atom(vec![ @@ -103,20 +103,19 @@ fn main() { editor.update_highlighter(); let viewport = Size { w: 154, h: 40 }; - + for i in 0..viewport.h { let output = editor.render_line( i, - viewport, + viewport, &lua ).unwrap(); - + println!("{output}"); } } */ - /// Entry point - grabs command line arguments and runs the editor fn main() { // Interact with user to find out what they want to do @@ -222,7 +221,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // Reset the pointer back to the first document let current_ptr = ged!(mut &editor).ptr.clone(); ged!(mut &editor).files.move_to(current_ptr, 0); - + // Handle stdin if applicable if cli.flags.stdin { let stdin = cli::get_stdin(); @@ -243,7 +242,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // Create a blank document if none are opened ged!(mut &editor).new_if_empty()?; - + // Add in the plugin manager handle_lua_error( "", @@ -295,7 +294,6 @@ fn run(cli: &CommandLineInterface) -> Result<()> { Ok(()) } - fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { // Clear screen of temporary items (expect on resize event) if !matches!(event, CEvent::Resize(_, _)) { @@ -428,7 +426,9 @@ fn handle_file_opening(editor: &AnyUserData, result: Result<()>, name: &str) { Ok(()) => (), Err(OxError::AlreadyOpen { .. }) => { let len = ged!(&editor).files.len().saturating_sub(1); - ged!(mut &editor).files.move_to(ged!(&editor).ptr.clone(), len); + ged!(mut &editor) + .files + .move_to(ged!(&editor).ptr.clone(), len); } Err(OxError::Kaolinite(kerr)) => { match kerr { From 3ec5a0c20b4f085c604a0021c2ce3fa69fa9e831 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:48:49 +0000 Subject: [PATCH 05/29] Started split API and restored help message --- src/editor/documents.rs | 69 +++++++++++++++++++++++++++++++++++++++-- src/editor/interface.rs | 49 +++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index cbde7216..e48896f7 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -8,7 +8,7 @@ use synoptic::Highlighter; pub type Span = Vec<(Vec, Range, Range)>; // File split structure -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum FileLayout { /// Side-by-side documents (with proportions) SideBySide(Vec<(FileLayout, f64)>), @@ -153,6 +153,54 @@ impl FileLayout { } } + /// Get the FileLayout at a certain index + pub fn get_raw(&self, mut idx: Vec) -> Option<&FileLayout> { + match self { + Self::None => Some(self), + Self::Atom(containers, ptr) => Some(self), + Self::SideBySide(layouts) => { + if idx.get(0).is_some() { + let subidx = idx.remove(0); + layouts[subidx].0.get_raw(idx) + } else { + Some(self) + } + } + Self::TopToBottom(layouts) => { + if idx.get(0).is_some() { + let subidx = idx.remove(0); + layouts[subidx].0.get_raw(idx) + } else { + Some(self) + } + } + } + } + + /// Get the FileLayout at a certain index + pub fn set(&mut self, mut idx: Vec, fl: FileLayout) { + match self { + Self::None => *self = fl, + Self::Atom(_, _) => *self = fl, + Self::SideBySide(layouts) => { + if idx.get(0).is_some() { + let subidx = idx.remove(0); + layouts[subidx].0.set(idx, fl) + } else { + *self = fl; + } + } + Self::TopToBottom(layouts) => { + if idx.get(0).is_some() { + let subidx = idx.remove(0); + layouts[subidx].0.set(idx, fl) + } else { + *self = fl; + } + } + } + } + /// Given an index, find the file containers in the tree pub fn get_atom(&self, mut idx: Vec) -> Option<(Vec<&FileContainer>, usize)> { match self { @@ -226,10 +274,27 @@ impl FileLayout { } } } + + /// Open a split to the right of the current pointer + pub fn open_right(&mut self, at: Vec, fl: FileLayout) { + if let Some(old_fl) = self.get_raw(at.clone()) { + let new_fl = match old_fl { + Self::None => fl, + Self::Atom(containers, ptr) => { + Self::SideBySide(vec![(self.clone(), 0.5), (fl, 0.5)]) + } + Self::SideBySide(layouts) => Self::SideBySide(vec![(self.clone(), 0.5), (fl, 0.5)]), + Self::TopToBottom(layouts) => { + Self::SideBySide(vec![(self.clone(), 0.5), (fl, 0.5)]) + } + }; + self.set(at, new_fl); + } + } } /// Container for a file -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FileContainer { /// Document (stores kaolinite information) pub doc: Document, diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 8187da40..a45f886e 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -20,6 +20,9 @@ use super::Editor; pub struct RenderCache { greeting_message: (String, Vec), span: Vec<(Vec, Range, Range)>, + help_message: Vec<(bool, String)>, + help_message_width: usize, + help_message_span: Range, } impl Editor { @@ -33,6 +36,22 @@ impl Editor { } // Calculate span self.render_cache.span = self.files.span(vec![], size); + // Calculate help message information + let tab_width = config!(self.config, document).tab_width; + self.render_cache.help_message = config!(self.config, help_message).render(lua); + self.render_cache.help_message_width = self + .render_cache + .help_message + .iter() + .map(|(_, line)| width(line, tab_width)) + .max() + .unwrap_or(0) + + 5; + let help_length = self.render_cache.help_message.len(); + let help_start = + usize::try_from((size.h / 2).saturating_sub(help_length / 2) + 1).unwrap_or(usize::MAX); + let help_end = help_start + usize::try_from(help_length).unwrap_or(usize::MAX) as usize; + self.render_cache.help_message_span = help_start..help_end + 1; } /// Render a specific line @@ -129,7 +148,7 @@ 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()?); - let colors = config!(self.config, colors).highlight.to_color()?; + let colors = Fg(config!(self.config, colors).highlight.to_color()?); let underline = SetAttribute(Attribute::Underlined); let no_underline = SetAttribute(Attribute::NoUnderline); let tab_width = config!(self.config, document).tab_width; @@ -139,7 +158,14 @@ impl Editor { let selection = self.doc().selection_loc_bound_disp(); let fc = self.files.get(ptr.clone()).unwrap(); let doc = &fc.doc; - let mut total_width = 0; + let help_message_here = config!(self.config, help_message).enabled + && self.render_cache.help_message_span.contains(&y); + // Render short of the help message + let mut total_width = if help_message_here { + self.render_cache.help_message_width + } else { + 0 + }; // Render the line numbers if enabled if line_numbers_enabled { let num = doc.line_number(y + doc.offset.y); @@ -147,10 +173,10 @@ impl Editor { let padding_right = " ".repeat(ln_pad_right); result += &format!("{line_number_bg}{line_number_fg}{padding_left}{num}{padding_right}│{editor_fg}{editor_bg}"); total_width += ln_pad_left + ln_pad_right + width(&num, tab_width) + 1; - w = w.saturating_sub(total_width); } else { result += &format!("{editor_fg}{editor_bg}"); } + w = w.saturating_sub(total_width); // Render the body of the document if available let at_line = y + doc.offset.y; if let Some(line) = doc.line(at_line) { @@ -240,6 +266,23 @@ impl Editor { // Empty line, just pad out with spaces to prevent artefacts result += &" ".repeat(w); } + // Add on help message if applicable + if help_message_here { + let at = y.saturating_sub(self.render_cache.help_message_span.start); + let max_width = self.render_cache.help_message_width; + let (hl, msg) = self + .render_cache + .help_message + .get(at) + .map(|(hl, content)| (*hl, content.to_string())) + .unwrap_or((false, " ".repeat(max_width))); + let extra_padding = " ".repeat(max_width.saturating_sub(width(&msg, tab_width))); + if hl { + result += &format!("{colors}{msg}{extra_padding}{editor_fg}"); + } else { + result += &format!("{editor_fg}{msg}{extra_padding}"); + } + } // Send out the result Ok(result) } From cc46f32f821661dc20f3e5dea5be691d2b7617f3 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:04:38 +0000 Subject: [PATCH 06/29] Added backend API for creating splits --- src/editor/documents.rs | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index e48896f7..f01e34dc 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -275,6 +275,57 @@ impl FileLayout { } } + /// Open a split above the current pointer + pub fn open_top(&mut self, at: Vec, fl: FileLayout) { + if let Some(old_fl) = self.get_raw(at.clone()) { + let new_fl = match old_fl { + Self::None => fl, + Self::Atom(containers, ptr) => { + Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) + } + Self::SideBySide(layouts) => Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]), + Self::TopToBottom(layouts) => { + Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) + } + }; + self.set(at, new_fl); + } + } + + /// Open a split below the current pointer + pub fn open_bottom(&mut self, at: Vec, fl: FileLayout) { + if let Some(old_fl) = self.get_raw(at.clone()) { + let new_fl = match old_fl { + Self::None => fl, + Self::Atom(containers, ptr) => { + Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) + } + Self::SideBySide(layouts) => Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]), + Self::TopToBottom(layouts) => { + Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) + } + }; + self.set(at, new_fl); + } + } + + /// Open a split to the left of the current pointer + pub fn open_left(&mut self, at: Vec, fl: FileLayout) { + if let Some(old_fl) = self.get_raw(at.clone()) { + let new_fl = match old_fl { + Self::None => fl, + Self::Atom(containers, ptr) => { + Self::SideBySide(vec![(fl, 0.5), (self.clone(), 0.5)]) + } + Self::SideBySide(layouts) => Self::SideBySide(vec![(fl, 0.5), (self.clone(), 0.5)]), + Self::TopToBottom(layouts) => { + Self::SideBySide(vec![(fl, 0.5), (self.clone(), 0.5)]) + } + }; + self.set(at, new_fl); + } + } + /// Open a split to the right of the current pointer pub fn open_right(&mut self, at: Vec, fl: FileLayout) { if let Some(old_fl) = self.get_raw(at.clone()) { From e649fe9df4c33114f89144d47dda4b0d30439bcf Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:04:57 +0000 Subject: [PATCH 07/29] rustfmt --- src/editor/documents.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index f01e34dc..82796e9a 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -283,7 +283,9 @@ impl FileLayout { Self::Atom(containers, ptr) => { Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) } - Self::SideBySide(layouts) => Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]), + Self::SideBySide(layouts) => { + Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) + } Self::TopToBottom(layouts) => { Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) } @@ -300,7 +302,9 @@ impl FileLayout { Self::Atom(containers, ptr) => { Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) } - Self::SideBySide(layouts) => Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]), + Self::SideBySide(layouts) => { + Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) + } Self::TopToBottom(layouts) => { Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) } From 99ce7ee8e381d95dce5d6c69668d3566175b9345 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 24 Nov 2024 17:11:57 +0000 Subject: [PATCH 08/29] Added split control command --- config/.oxrc | 38 ++++++++ src/config/editor.rs | 77 ++++++++++++++- src/editor/documents.rs | 209 +++++++++++++++++++++++++++++++++++++--- src/editor/mod.rs | 30 +++--- 4 files changed, 328 insertions(+), 26 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index bc3fe9c3..230ef216 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -308,6 +308,44 @@ commands = { editor:reload_config() editor:display_info("Configuration file reloaded") end, + ["split"] = function(arguments) + local file = arguments[2] + local result = false + if arguments[1] == "left" then + result = editor:open_split_left(file) + elseif arguments[1] == "right" then + result = editor:open_split_right(file) + elseif arguments[1] == "top" then + result = editor:open_split_top(file) + elseif arguments[1] == "bottom" then + result = editor:open_split_bottom(file) + elseif arguments[1] == "grow" then + result = true + editor:grow_split(0.2) + elseif arguments[1] == "shrink" then + result = true + editor:shrink_split(0.2) + elseif arguments[1] == "focus" then + result = true + if arguments[2] == "up" then + editor:focus_split_up() + elseif arguments[2] == "down" then + editor:focus_split_down() + elseif arguments[2] == "left" then + editor:focus_split_left() + elseif arguments[2] == "right" then + editor:focus_split_right() + else + editor:display_error("Unknown direction for split focus") + end + else + result = true + editor:display_error(tostring(arguments[1]) .. " is not a valid split command") + end + if not result then + editor:display_error("Failed to open file, please check your path") + end + end, ["macro"] = function(arguments) if arguments[1] == "record" then editor:macro_record_start() diff --git a/src/config/editor.rs b/src/config/editor.rs index 6de8e26f..0ef6ec11 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -1,6 +1,6 @@ /// Defines the Editor API for plug-ins to use use crate::cli::VERSION; -use crate::editor::{Editor, FileContainer}; +use crate::editor::{Editor, FileContainer, FileLayout}; use crate::ui::Feedback; use crate::{config, fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; @@ -539,6 +539,81 @@ impl LuaUserData for Editor { } Ok(()) }); + // Split management + methods.add_method_mut("open_split_top", |_, editor, file: String| { + if let Ok(fc) = editor.open_fc(&file) { + editor.ptr = editor + .files + .open_top(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("open_split_bottom", |_, editor, file: String| { + if let Ok(fc) = editor.open_fc(&file) { + editor.ptr = editor + .files + .open_bottom(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("open_split_left", |_, editor, file: String| { + if let Ok(fc) = editor.open_fc(&file) { + editor.ptr = editor + .files + .open_left(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("open_split_right", |_, editor, file: String| { + if let Ok(fc) = editor.open_fc(&file) { + editor.ptr = editor + .files + .open_right(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + Ok(true) + } else { + Ok(false) + } + }); + methods.add_method_mut("grow_split", |_, editor, amount: f64| { + let current = editor.files.get_proportion(editor.ptr.clone()); + if current + amount <= 1.0 { + editor + .files + .set_proportion(editor.ptr.clone(), current + amount) + } + Ok(()) + }); + methods.add_method_mut("shrink_split", |_, editor, amount: f64| { + let current = editor.files.get_proportion(editor.ptr.clone()); + if current > amount { + editor + .files + .set_proportion(editor.ptr.clone(), current - amount) + } + Ok(()) + }); + methods.add_method_mut("focus_split_up", |_, editor, ()| { + editor.ptr = editor.files.move_up(editor.ptr.clone()); + Ok(()) + }); + methods.add_method_mut("focus_split_down", |_, editor, ()| { + editor.ptr = editor.files.move_down(editor.ptr.clone()); + Ok(()) + }); + methods.add_method_mut("focus_split_left", |_, editor, ()| { + editor.ptr = editor.files.move_left(editor.ptr.clone()); + Ok(()) + }); + methods.add_method_mut("focus_split_right", |_, editor, ()| { + editor.ptr = editor.files.move_right(editor.ptr.clone()); + Ok(()) + }); // Searching and replacing methods.add_method_mut("search", |lua, editor, ()| { if let Err(err) = editor.search(lua) { diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 82796e9a..886887f2 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -177,6 +177,26 @@ impl FileLayout { } } + /// Get the FileLayout at a certain index (mutable) + pub fn get_raw_mut(&mut self, mut idx: Vec) -> Option<&mut FileLayout> { + if idx.get(0).is_none() { + Some(self) + } else { + match self { + Self::None => Some(self), + Self::Atom(containers, ptr) => Some(self), + Self::SideBySide(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.get_raw_mut(idx) + } + Self::TopToBottom(layouts) => { + let subidx = idx.remove(0); + layouts[subidx].0.get_raw_mut(idx) + } + } + } + } + /// Get the FileLayout at a certain index pub fn set(&mut self, mut idx: Vec, fl: FileLayout) { match self { @@ -276,75 +296,238 @@ impl FileLayout { } /// Open a split above the current pointer - pub fn open_top(&mut self, at: Vec, fl: FileLayout) { + pub fn open_top(&mut self, at: Vec, fl: FileLayout) -> Vec { + let mut new_ptr = at.clone(); if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, Self::Atom(containers, ptr) => { + new_ptr.push(0); Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) } Self::SideBySide(layouts) => { + new_ptr.push(0); Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) } Self::TopToBottom(layouts) => { + new_ptr.push(0); Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) } }; self.set(at, new_fl); } + new_ptr } /// Open a split below the current pointer - pub fn open_bottom(&mut self, at: Vec, fl: FileLayout) { + pub fn open_bottom(&mut self, at: Vec, fl: FileLayout) -> Vec { + let mut new_ptr = at.clone(); if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, Self::Atom(containers, ptr) => { - Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) + new_ptr.push(1); + Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } Self::SideBySide(layouts) => { - Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) + new_ptr.push(1); + Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } Self::TopToBottom(layouts) => { - Self::TopToBottom(vec![(self.clone(), 0.5), (fl, 0.5)]) + new_ptr.push(1); + Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } }; self.set(at, new_fl); } + new_ptr } /// Open a split to the left of the current pointer - pub fn open_left(&mut self, at: Vec, fl: FileLayout) { + pub fn open_left(&mut self, at: Vec, fl: FileLayout) -> Vec { + let mut new_ptr = at.clone(); if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, Self::Atom(containers, ptr) => { - Self::SideBySide(vec![(fl, 0.5), (self.clone(), 0.5)]) + new_ptr.push(0); + Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) + } + Self::SideBySide(layouts) => { + new_ptr.push(0); + Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } - Self::SideBySide(layouts) => Self::SideBySide(vec![(fl, 0.5), (self.clone(), 0.5)]), Self::TopToBottom(layouts) => { - Self::SideBySide(vec![(fl, 0.5), (self.clone(), 0.5)]) + new_ptr.push(0); + Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } }; self.set(at, new_fl); } + new_ptr } /// Open a split to the right of the current pointer - pub fn open_right(&mut self, at: Vec, fl: FileLayout) { + pub fn open_right(&mut self, at: Vec, fl: FileLayout) -> Vec { + let mut new_ptr = at.clone(); if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, Self::Atom(containers, ptr) => { - Self::SideBySide(vec![(self.clone(), 0.5), (fl, 0.5)]) + new_ptr.push(1); + Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) + } + Self::SideBySide(layouts) => { + new_ptr.push(1); + Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } - Self::SideBySide(layouts) => Self::SideBySide(vec![(self.clone(), 0.5), (fl, 0.5)]), Self::TopToBottom(layouts) => { - Self::SideBySide(vec![(self.clone(), 0.5), (fl, 0.5)]) + new_ptr.push(1); + Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } }; self.set(at, new_fl); } + new_ptr + } + + /// Get the proportion of a certain node in the tree + pub fn get_proportion(&self, mut at: Vec) -> f64 { + if let Some(last_idx) = at.pop() { + if let Some(FileLayout::SideBySide(layouts) | FileLayout::TopToBottom(layouts)) = + self.get_raw(at) + { + layouts[last_idx].1 + } else { + 1.0 + } + } else { + 1.0 + } + } + + /// Get the proportion of a certain node in the tree + pub fn set_proportion(&mut self, mut at: Vec, amount: f64) { + if let Some(last_idx) = at.pop() { + if let Some(FileLayout::SideBySide(layouts) | FileLayout::TopToBottom(layouts)) = + self.get_raw_mut(at) + { + layouts[last_idx].1 = amount; + // Potential problem with this algorithm - it could cause underflow (resulting in props not adding up to 1) + let reduce = layouts.iter().map(|(_, prop)| prop).sum::() - 1.0; + let reduce_each = reduce / layouts.len().saturating_sub(1) as f64; + layouts + .iter_mut() + .enumerate() + .filter(|(c, _)| *c != last_idx) + .for_each(|(_, (_, prop))| *prop -= reduce_each); + } + } + } + + /// Find the nearest parent sidebyside, returns the pointer and we were in it + pub fn get_sidebyside_parent(&self, mut at: Vec) -> Option<(Vec, usize)> { + // "Zoom out" to try and find a sidebyside parent + let mut subidx = None; + while let Some(FileLayout::TopToBottom(_) | FileLayout::Atom(_, _) | FileLayout::None) = + self.get_raw(at.clone()) + { + subidx = at.pop(); + } + subidx.map(|s| (at, s)) + } + + /// Find the nearest parent toptobottom, returns the pointer and we were in it + pub fn get_toptobottom_parent(&self, mut at: Vec) -> Option<(Vec, usize)> { + // "Zoom out" to try and find a sidebyside parent + let mut subidx = None; + while let Some(FileLayout::SideBySide(_) | FileLayout::Atom(_, _) | FileLayout::None) = + self.get_raw(at.clone()) + { + subidx = at.pop(); + } + subidx.map(|s| (at, s)) + } + + /// Find the new cursor position when moving left + pub fn move_left(&self, at: Vec) -> Vec { + if let Some((mut parent_idx, to_change)) = self.get_sidebyside_parent(at.clone()) { + // Move backward from where we were + let to_change = to_change.saturating_sub(1); + parent_idx.push(to_change); + // "Zoom in" down to atom level + while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = + self.get_raw(parent_idx.clone()) + { + parent_idx.push(0); + } + parent_idx + } else { + at + } + } + + /// Find the new cursor position when moving right + pub fn move_right(&self, at: Vec) -> Vec { + if let Some((mut parent_idx, mut to_change)) = self.get_sidebyside_parent(at.clone()) { + // Move backward from where we were + if let Some(FileLayout::SideBySide(layouts)) = self.get_raw(parent_idx.clone()) { + if to_change + 1 < layouts.len() { + to_change = to_change + 1; + } + } + parent_idx.push(to_change); + // "Zoom in" down to atom level + while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = + self.get_raw(parent_idx.clone()) + { + parent_idx.push(0); + } + parent_idx + } else { + at + } + } + + /// Find the new cursor position when moving up + pub fn move_up(&self, at: Vec) -> Vec { + if let Some((mut parent_idx, to_change)) = self.get_toptobottom_parent(at.clone()) { + // Move backward from where we were + let to_change = to_change.saturating_sub(1); + parent_idx.push(to_change); + // "Zoom in" down to atom level + while let Some( + FileLayout::TopToBottom(_) | FileLayout::SideBySide(_) | FileLayout::None, + ) = self.get_raw(parent_idx.clone()) + { + parent_idx.push(0); + } + parent_idx + } else { + at + } + } + + /// Find the new cursor position when moving down + pub fn move_down(&self, at: Vec) -> Vec { + if let Some((mut parent_idx, mut to_change)) = self.get_toptobottom_parent(at.clone()) { + // Move backward from where we were + if let Some(FileLayout::TopToBottom(layouts)) = self.get_raw(parent_idx.clone()) { + if to_change + 1 < layouts.len() { + to_change = to_change + 1; + } + } + parent_idx.push(to_change); + // "Zoom in" down to atom level + while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = + self.get_raw(parent_idx.clone()) + { + parent_idx.push(0); + } + parent_idx + } else { + at + } } } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 27e88be6..4c554ef7 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -148,6 +148,23 @@ impl Editor { /// Function to open a document into the editor pub fn open(&mut self, file_name: &str) -> Result<()> { + let file = self.open_fc(file_name)?; + if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + // Atom already exists + if *ptr + 1 >= files.len() { + files.push(file); + } else { + files.insert(*ptr + 1, file); + } + } else { + // Atom ought to be created + self.files = FileLayout::Atom(vec![file], 0); + } + Ok(()) + } + + /// Function to create a file container + pub fn open_fc(&mut self, file_name: &str) -> Result { // Check if a file is already opened if let Some((idx, ptr)) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) @@ -179,18 +196,7 @@ impl Editor { highlighter, file_type, }; - if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { - // Atom already exists - if *ptr + 1 >= files.len() { - files.push(file); - } else { - files.insert(*ptr + 1, file); - } - } else { - // Atom ought to be created - self.files = FileLayout::Atom(vec![file], 0); - } - Ok(()) + Ok(file) } /// Function to ask the user for a file to open From bd10c8e4fc7c62ada65a40ec7a2c151e156474df Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 24 Nov 2024 22:53:40 +0000 Subject: [PATCH 09/29] Fixed cursor and selection issues --- src/config/editor.rs | 4 ++++ src/editor/documents.rs | 6 +++--- src/editor/interface.rs | 20 +++++++++++++++++--- src/editor/scanning.rs | 33 ++++++++++++++++----------------- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 0ef6ec11..05d0cc1b 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -620,6 +620,8 @@ impl LuaUserData for Editor { editor.feedback = Feedback::Error(err.to_string()); } editor.update_highlighter(); + editor.needs_rerender = true; + editor.render(lua); Ok(()) }); methods.add_method_mut("replace", |lua, editor, ()| { @@ -627,6 +629,8 @@ impl LuaUserData for Editor { editor.feedback = Feedback::Error(err.to_string()); } editor.update_highlighter(); + editor.needs_rerender = true; + editor.render(lua); Ok(()) }); methods.add_method_mut("move_next_match", |_, editor, query: String| { diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 886887f2..b5a66a28 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -303,15 +303,15 @@ impl FileLayout { Self::None => fl, Self::Atom(containers, ptr) => { new_ptr.push(0); - Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) + Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } Self::SideBySide(layouts) => { new_ptr.push(0); - Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) + Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } Self::TopToBottom(layouts) => { new_ptr.push(0); - Self::TopToBottom(vec![(fl, 0.5), (self.clone(), 0.5)]) + Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } }; self.set(at, new_fl); diff --git a/src/editor/interface.rs b/src/editor/interface.rs index a45f886e..dff95294 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -123,14 +123,28 @@ impl Editor { // Render the feedback line 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() { + if let Some(Loc { x, y }) = self.cursor_position() { self.terminal.show_cursor(); - self.terminal.goto(x + max, y + self.push_down); + self.terminal.goto(x, y); } self.terminal.flush()?; Ok(()) } + /// Function to calculate the cursor's position on screen + pub fn cursor_position(&self) -> Option { + let Loc { x, y } = self.doc().cursor_loc_in_screen()?; + for (ptr, rows, cols) in &self.render_cache.span { + if ptr == &self.ptr { + return Some(Loc { + x: cols.start + x + self.dent(), + y: rows.start + y + self.push_down, + }); + } + } + None + } + /// Render the lines of the document pub fn render_document( &mut self, @@ -219,7 +233,7 @@ impl Editor { x: x_char, }; // Work out selection - let is_selected = self.doc().is_this_loc_selected_disp(disp_loc, selection); + let is_selected = &self.ptr == ptr && self.doc().is_this_loc_selected_disp(disp_loc, selection); // Render the correct colour if is_selected { if cache_bg != selection_bg { diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 8e128115..3b934c0b 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -15,16 +15,18 @@ use super::Editor; impl Editor { /// Use search feature pub fn search(&mut self, lua: &Lua) -> Result<()> { + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let cache = self.doc().char_loc(); // Prompt for a search term let mut target = String::new(); let mut done = false; while !done { let Size { w, h } = size()?; + // Rerender the editor + self.needs_rerender = true; + self.render(lua)?; // Render prompt message self.terminal.prepare_line(h); - self.terminal.show_cursor(); - let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); display!( self, editor_bg, @@ -33,12 +35,9 @@ impl Editor { "│", " ".to_string().repeat(w) ); - self.terminal.hide_cursor(); - self.render(lua)?; // 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); + if let Some(Loc { x, y }) = self.cursor_position() { + self.terminal.goto(x, y); self.terminal.show_cursor(); } else { self.terminal.hide_cursor(); @@ -78,19 +77,20 @@ impl Editor { let Size { w, h } = size()?; // Enter into search menu while !done { - // Render just the document part - self.terminal.hide_cursor(); + // Rerender the editor + self.needs_rerender = true; self.render(lua)?; // Render custom status line with mode information - self.terminal.goto(0, h); + self.terminal.prepare_line(h); display!( self, - Print("[<-]: Search previous | [->]: Search next | [Enter] Finish | [Esc] Cancel") + editor_bg, + Print("[<-]: Search previous | [->]: Search next | [Enter] Finish | [Esc] Cancel"), + Print(" ".repeat(w.saturating_sub(73))) ); // 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); + if let Some(Loc { x, y }) = self.cursor_position() { + self.terminal.goto(x, y); self.terminal.show_cursor(); } else { self.terminal.hide_cursor(); @@ -184,9 +184,8 @@ impl Editor { ) ); // 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); + if let Some(Loc { x, y }) = self.cursor_position() { + self.terminal.goto(x, y); self.terminal.show_cursor(); } else { self.terminal.hide_cursor(); From 0b0a56b12b1832cb3eabfd40261534c26fad36fd Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:38:35 +0000 Subject: [PATCH 10/29] Fixed bug with splits not rendering correctly --- src/editor/documents.rs | 79 +++++++++++++++++++++++++++++++---------- src/editor/interface.rs | 25 ++++++------- src/main.rs | 24 +++++-------- 3 files changed, 80 insertions(+), 48 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index b5a66a28..9008989e 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -40,20 +40,19 @@ impl FileLayout { let mut subidx = idx.clone(); subidx.push(c); let this_size = Size { - w: at + (size.w as f64 * props) as usize, + w: (size.w as f64 * props) as usize, h: size.h, }; for mut sub in layout.span(subidx, this_size) { - let mut end = sub.2.end; - if c == layouts.len().saturating_sub(1) { - end += size.w.saturating_sub(sub.2.end) - } else { - end -= 1; + // Shift this range up to it's correct location + sub.2.start += at; + sub.2.end += at; + if c != layouts.len().saturating_sub(1) { + sub.2.end -= 1; } - sub.2 = at..end; result.push(sub); } - at = this_size.w; + at += this_size.w; } result } @@ -65,20 +64,17 @@ impl FileLayout { subidx.push(c); let this_size = Size { w: size.w, - h: at + (size.h as f64 * props) as usize, + h: (size.h as f64 * props) as usize, }; for mut sub in layout.span(subidx, this_size) { - let mut end = sub.1.end; - if c == layouts.len().saturating_sub(1) { - end += size.h.saturating_sub(sub.1.end) - } else { - end -= 1; - result.push((vec![42].repeat(100), sub.1.clone(), sub.2.clone())); + sub.1.start += at; + sub.1.end += at; + if c != layouts.len().saturating_sub(1) { + sub.1.end -= 1; } - sub.1 = at..end; - result.push(sub); + result.push(sub.clone()); } - at = this_size.h; + at += this_size.h; } result } @@ -101,6 +97,53 @@ impl FileLayout { appropriate } + /// Fixes span underflow (where nodes are shorter than desired due to division errors) + pub fn fix_underflow(mut span: Span, desired: Size) -> Span { + // FIX FOR WIDTH + // Go through each line in a span + for y in 0..desired.h { + let line = Self::line(y, &span); + if let Some((idx, _, cols)) = line.get(line.len().saturating_sub(1)) { + // If this line has the width shorter than desired + if cols.end < desired.w { + if let Some((_, _, ref mut col_span)) = span.iter_mut().find(|(checking_idx, _, _)| checking_idx == idx) { + // Take the idx of the last node and push it up to ensure it fits + let shift_by = desired.w.saturating_sub(cols.end); + col_span.end += shift_by; + } + } + } + } + + // FIX FOR HEIGHT + // Work out: + // - The number of vacant line entries at the end of the desired height + // - The last non-empty entry in the line registry + let mut last_active_line = 0; + let mut empty_last_lines = 0; + for y in 0..desired.h { + let line = Self::line(y, &span); + if line.is_empty() { + empty_last_lines += 1; + } else { + last_active_line = y; + empty_last_lines = 0; + } + } + let last_panes = Self::line(last_active_line, &span).into_iter().map(|(idx, cols, rows)| idx).collect::>(); + // For each pane on the last non-empty line: + for pane_idx in last_panes { + if let Some((_, ref mut row_span, _)) = span.iter_mut().find(|(checking_idx, _, _)| *checking_idx == pane_idx) { + // Set the end of the rows range to the desired height (in effect expanding them downwards) + let shift_by = desired.h.saturating_sub(1 + last_active_line); + row_span.end += shift_by; + } + } + + // Return the modified result + span + } + /// Work out how many files are currently open pub fn len(&self) -> usize { match self { diff --git a/src/editor/interface.rs b/src/editor/interface.rs index dff95294..00890e90 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -18,11 +18,11 @@ use super::Editor; /// Render cache to store the results of any calculations during rendering #[derive(Default)] pub struct RenderCache { - greeting_message: (String, Vec), - span: Vec<(Vec, Range, Range)>, - help_message: Vec<(bool, String)>, - help_message_width: usize, - help_message_span: Range, + pub greeting_message: (String, Vec), + pub span: Vec<(Vec, Range, Range)>, + pub help_message: Vec<(bool, String)>, + pub help_message_width: usize, + pub help_message_span: Range, } impl Editor { @@ -35,7 +35,7 @@ impl Editor { } } // Calculate span - self.render_cache.span = self.files.span(vec![], size); + self.render_cache.span = FileLayout::fix_underflow(self.files.span(vec![], size), size); // Calculate help message information let tab_width = config!(self.config, document).tab_width; self.render_cache.help_message = config!(self.config, help_message).render(lua); @@ -59,15 +59,11 @@ impl Editor { let tab_line_enabled = config!(self.config, tab_line).enabled; let mut result = String::new(); let fcs = FileLayout::line(y, &self.render_cache.span); + if fcs.is_empty() && y < size.h { + return Ok("─".repeat(size.w)); + } for (mut c, (fc, rows, range)) in fcs.iter().enumerate() { let length = range.end.saturating_sub(range.start); - // Insert horizontal bar where appropriate (horribly janky implementation, but it works) - if vec![42].repeat(100) == *fc { - if y == rows.end.saturating_sub(1) { - result += &"─".repeat(length); - } - continue; - } let rel_y = y.saturating_sub(rows.start); if y == rows.start && tab_line_enabled { // Tab line @@ -233,7 +229,8 @@ impl Editor { x: x_char, }; // Work out selection - let is_selected = &self.ptr == ptr && self.doc().is_this_loc_selected_disp(disp_loc, selection); + let is_selected = &self.ptr == ptr + && self.doc().is_this_loc_selected_disp(disp_loc, selection); // Render the correct colour if is_selected { if cache_bg != selection_bg { diff --git a/src/main.rs b/src/main.rs index 848b22fc..1c8d0a3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,7 +54,7 @@ fn main() { editor.load_config("/home/luke/.oxrc", &lua); lua.load(PLUGIN_RUN).exec().unwrap(); - editor.files = FileLayout::SideBySide(vec![ + editor.files = FileLayout::TopToBottom(vec![ ( FileLayout::Atom(vec![ FileContainer { @@ -63,7 +63,7 @@ fn main() { file_type: None, } ], 0), - 0.45, + 0.5, ), ( FileLayout::TopToBottom(vec![ @@ -75,7 +75,7 @@ fn main() { file_type: None, } ], 0), - 0.4, + 0.5, ), ( FileLayout::Atom(vec![ @@ -85,10 +85,10 @@ fn main() { file_type: None, } ], 0), - 0.6, + 0.5, ), ]), - 0.55, + 0.5, ), ]); editor.active = true; @@ -102,17 +102,9 @@ fn main() { editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); editor.update_highlighter(); - let viewport = Size { w: 154, h: 40 }; - - for i in 0..viewport.h { - let output = editor.render_line( - i, - viewport, - &lua - ).unwrap(); - - println!("{output}"); - } + // editor.update_render_cache(&lua, Size { w: 151, h: 15 }); + // editor.render(&lua); + // println!("\n\n\n{:#?}", editor.render_cache.span); } */ From 14fedb418a2f652f27a2c891d45beb6f6d090d23 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:46:08 +0000 Subject: [PATCH 11/29] rustfmt --- src/editor/documents.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 9008989e..eb3a7986 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -106,7 +106,10 @@ impl FileLayout { if let Some((idx, _, cols)) = line.get(line.len().saturating_sub(1)) { // If this line has the width shorter than desired if cols.end < desired.w { - if let Some((_, _, ref mut col_span)) = span.iter_mut().find(|(checking_idx, _, _)| checking_idx == idx) { + if let Some((_, _, ref mut col_span)) = span + .iter_mut() + .find(|(checking_idx, _, _)| checking_idx == idx) + { // Take the idx of the last node and push it up to ensure it fits let shift_by = desired.w.saturating_sub(cols.end); col_span.end += shift_by; @@ -130,10 +133,16 @@ impl FileLayout { empty_last_lines = 0; } } - let last_panes = Self::line(last_active_line, &span).into_iter().map(|(idx, cols, rows)| idx).collect::>(); + let last_panes = Self::line(last_active_line, &span) + .into_iter() + .map(|(idx, cols, rows)| idx) + .collect::>(); // For each pane on the last non-empty line: for pane_idx in last_panes { - if let Some((_, ref mut row_span, _)) = span.iter_mut().find(|(checking_idx, _, _)| *checking_idx == pane_idx) { + if let Some((_, ref mut row_span, _)) = span + .iter_mut() + .find(|(checking_idx, _, _)| *checking_idx == pane_idx) + { // Set the end of the rows range to the desired height (in effect expanding them downwards) let shift_by = desired.h.saturating_sub(1 + last_active_line); row_span.end += shift_by; From 3cfd73cd32997787194d2bdda792d813a50ea75b Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:56:10 +0000 Subject: [PATCH 12/29] Proper quitting implementation with splits --- config/.oxrc | 8 ++-- src/config/editor.rs | 8 ++-- src/editor/documents.rs | 101 ++++++++++++++++++++++++++++++++++++---- src/editor/mod.rs | 24 ++++++---- src/main.rs | 20 ++++---- 5 files changed, 124 insertions(+), 37 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 230ef216..6629293b 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -315,10 +315,10 @@ commands = { result = editor:open_split_left(file) elseif arguments[1] == "right" then result = editor:open_split_right(file) - elseif arguments[1] == "top" then - result = editor:open_split_top(file) - elseif arguments[1] == "bottom" then - result = editor:open_split_bottom(file) + elseif arguments[1] == "up" then + result = editor:open_split_up(file) + elseif arguments[1] == "down" then + result = editor:open_split_down(file) elseif arguments[1] == "grow" then result = true editor:grow_split(0.2) diff --git a/src/config/editor.rs b/src/config/editor.rs index 05d0cc1b..1eb89347 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -540,21 +540,21 @@ impl LuaUserData for Editor { Ok(()) }); // Split management - methods.add_method_mut("open_split_top", |_, editor, file: String| { + methods.add_method_mut("open_split_up", |_, editor, file: String| { if let Ok(fc) = editor.open_fc(&file) { editor.ptr = editor .files - .open_top(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + .open_up(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); Ok(true) } else { Ok(false) } }); - methods.add_method_mut("open_split_bottom", |_, editor, file: String| { + methods.add_method_mut("open_split_down", |_, editor, file: String| { if let Ok(fc) = editor.open_fc(&file) { editor.ptr = editor .files - .open_bottom(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + .open_down(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); Ok(true) } else { Ok(false) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index eb3a7986..1573831e 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -213,7 +213,7 @@ impl FileLayout { Self::SideBySide(layouts) => { if idx.get(0).is_some() { let subidx = idx.remove(0); - layouts[subidx].0.get_raw(idx) + layouts.get(subidx)?.0.get_raw(idx) } else { Some(self) } @@ -221,7 +221,7 @@ impl FileLayout { Self::TopToBottom(layouts) => { if idx.get(0).is_some() { let subidx = idx.remove(0); - layouts[subidx].0.get_raw(idx) + layouts.get(subidx)?.0.get_raw(idx) } else { Some(self) } @@ -239,11 +239,11 @@ impl FileLayout { Self::Atom(containers, ptr) => Some(self), Self::SideBySide(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.get_raw_mut(idx) + layouts.get_mut(subidx)?.0.get_raw_mut(idx) } Self::TopToBottom(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.get_raw_mut(idx) + layouts.get_mut(subidx)?.0.get_raw_mut(idx) } } } @@ -280,11 +280,11 @@ impl FileLayout { Self::Atom(containers, ptr) => Some((containers.iter().collect(), *ptr)), Self::SideBySide(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.get_atom(idx) + layouts.get(subidx)?.0.get_atom(idx) } Self::TopToBottom(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.get_atom(idx) + layouts.get(subidx)?.0.get_atom(idx) } } } @@ -299,11 +299,11 @@ impl FileLayout { Self::Atom(ref mut containers, ref mut ptr) => Some((containers, ptr)), Self::SideBySide(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.get_atom_mut(idx) + layouts.get_mut(subidx)?.0.get_atom_mut(idx) } Self::TopToBottom(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.get_atom_mut(idx) + layouts.get_mut(subidx)?.0.get_atom_mut(idx) } } } @@ -347,8 +347,89 @@ impl FileLayout { } } + /// Remove any empty atoms + pub fn clean_up(&mut self) { + // Continue checking for obselete nodes until none are remaining + while let Some(empty_idx) = self.empty_atoms(vec![]) { + // Delete the empty node + self.remove(empty_idx.clone()); + } + } + + /// Remove a certain index from this tree + pub fn remove(&mut self, at: Vec) { + // Get parent of the node we wish to delete + let mut at_parent = at.clone(); + if let Some(within_parent) = at_parent.pop() { + // Determine behaviour based on parent + if let Some(parent) = self.get_raw_mut(at_parent) { + match parent { + Self::None | Self::Atom(_, _) => unreachable!(), + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { + // Get the proportion of what we're removing + let removed_prop = layouts[within_parent].1; + // Remove from the parent + layouts.remove(within_parent); + // Redistribute proportions + let redistributed = removed_prop / layouts.len() as f64; + for (_, prop) in layouts.iter_mut() { + *prop += redistributed; + } + } + } + } + } else { + // This is the root node of the entire tree! + // In this case, we just set the whole thing to FileLayout::None + self.set(at, FileLayout::None); + } + } + + /// Traverse the tree and return a list of indices to empty atoms + pub fn empty_atoms(&self, at: Vec) -> Option> { + match self { + Self::None => None, + Self::Atom(fcs, _) => if fcs.is_empty() { + Some(at) + } else { + None + }, + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { + if layouts.is_empty() { + Some(at) + } else { + for (c, layout) in layouts.iter().enumerate() { + let mut idx = at.clone(); + idx.push(c); + if let Some(result) = layout.0.empty_atoms(idx) { + return Some(result) + } + } + None + } + } + } + } + + /// Find a new pointer position when something is removed + pub fn new_pointer_position(&self, old: Vec) -> Vec { + // Zoom out until a sidebyside or toptobottom is found + let mut copy = old.clone(); + while let Some(Self::None | Self::Atom(_, _)) | None = self.get_raw(copy.clone()) { + copy.pop(); + if copy.is_empty() { break } + } + // Zoom in to find a new cursor position + while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = + self.get_raw(copy.clone()) + { + copy.push(0); + } + copy + } + /// Open a split above the current pointer - pub fn open_top(&mut self, at: Vec, fl: FileLayout) -> Vec { + pub fn open_up(&mut self, at: Vec, fl: FileLayout) -> Vec { let mut new_ptr = at.clone(); if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { @@ -372,7 +453,7 @@ impl FileLayout { } /// Open a split below the current pointer - pub fn open_bottom(&mut self, at: Vec, fl: FileLayout) -> Vec { + pub fn open_down(&mut self, at: Vec, fl: FileLayout) -> Vec { let mut new_ptr = at.clone(); if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 4c554ef7..cb867439 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -300,18 +300,24 @@ impl Editor { /// Quit the editor pub fn quit(&mut self) -> Result<()> { - self.active = self.files.len() != 0; - // If there are still documents open, only close the requested document - if self.active { + // Get the atom we're currently at + if let Some((fcs, ptr)) = self.files.get_atom(self.ptr.clone()) { + // Remove the file that is currently open and selected 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 let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { - files.remove(*ptr); - self.prev(); - } + let doc = &fcs[ptr].doc; + if doc.event_mgmt.with_disk(&doc.take_snapshot()) || self.confirm(msg)? { + let (fcs, ptr) = self.files.get_atom_mut(self.ptr.clone()).unwrap(); + fcs.remove(*ptr); + self.prev(); } + let (fcs, ptr) = self.files.get_atom(self.ptr.clone()).unwrap(); + // Clean up the file structure + self.files.clean_up(); + // Find a new pointer position + self.ptr = self.files.new_pointer_position(self.ptr.clone()); } - self.active = self.files.len() != 0; + // If there are no longer any active atoms, quit the entire editor + self.active = !matches!(self.files, FileLayout::None); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 1c8d0a3d..99c4e4d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,7 +66,7 @@ fn main() { 0.5, ), ( - FileLayout::TopToBottom(vec![ + FileLayout::SideBySide(vec![ ( FileLayout::Atom(vec![ FileContainer { @@ -92,15 +92,15 @@ fn main() { ), ]); editor.active = true; - editor.ptr = vec![1, 0]; - editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - editor.update_highlighter(); - editor.ptr = vec![1, 1]; - editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - editor.update_highlighter(); - editor.ptr = vec![0]; - editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - editor.update_highlighter(); + // editor.ptr = vec![1, 0]; + // editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + // editor.update_highlighter(); + // editor.ptr = vec![1, 1]; + // editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + // editor.update_highlighter(); + // editor.ptr = vec![0]; + // editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + // editor.update_highlighter(); // editor.update_render_cache(&lua, Size { w: 151, h: 15 }); // editor.render(&lua); From 6df9abc0b9dc67fa2ccdcb1663087064120f0364 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:44:14 +0000 Subject: [PATCH 13/29] Fixed some rendering issues --- src/editor/documents.rs | 24 ++++++++++++++---------- src/editor/interface.rs | 15 +++++++++++---- src/main.rs | 31 ++++++++++++++++--------------- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 1573831e..04196e50 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -103,9 +103,9 @@ impl FileLayout { // Go through each line in a span for y in 0..desired.h { let line = Self::line(y, &span); - if let Some((idx, _, cols)) = line.get(line.len().saturating_sub(1)) { - // If this line has the width shorter than desired - if cols.end < desired.w { + if let Some((idx, rows, cols)) = line.get(line.len().saturating_sub(1)) { + // If this line has the width shorter than desired (and is the first of it's kind) + if cols.end < desired.w && y == rows.start { if let Some((_, _, ref mut col_span)) = span .iter_mut() .find(|(checking_idx, _, _)| checking_idx == idx) @@ -389,11 +389,13 @@ impl FileLayout { pub fn empty_atoms(&self, at: Vec) -> Option> { match self { Self::None => None, - Self::Atom(fcs, _) => if fcs.is_empty() { - Some(at) - } else { - None - }, + Self::Atom(fcs, _) => { + if fcs.is_empty() { + Some(at) + } else { + None + } + } Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { if layouts.is_empty() { Some(at) @@ -402,7 +404,7 @@ impl FileLayout { let mut idx = at.clone(); idx.push(c); if let Some(result) = layout.0.empty_atoms(idx) { - return Some(result) + return Some(result); } } None @@ -417,7 +419,9 @@ impl FileLayout { let mut copy = old.clone(); while let Some(Self::None | Self::Atom(_, _)) | None = self.get_raw(copy.clone()) { copy.pop(); - if copy.is_empty() { break } + if copy.is_empty() { + break; + } } // Zoom in to find a new cursor position while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 00890e90..a0910afd 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -59,9 +59,11 @@ impl Editor { let tab_line_enabled = config!(self.config, tab_line).enabled; let mut result = String::new(); let fcs = FileLayout::line(y, &self.render_cache.span); - if fcs.is_empty() && y < size.h { - return Ok("─".repeat(size.w)); - } + let shorted = size + .w + .saturating_sub(fcs.last().map(|ll| ll.2.end).unwrap_or(0)); + let mut bump = 0; + // Render each component of this line for (mut c, (fc, rows, range)) in fcs.iter().enumerate() { let length = range.end.saturating_sub(range.start); let rel_y = y.saturating_sub(rows.start); @@ -86,10 +88,15 @@ impl Editor { let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); // Insert vertical bar where appropriate - if c != fcs.len().saturating_sub(1) { + if shorted > 0 || c != fcs.len().saturating_sub(1) { result += &format!("{editor_bg}{editor_fg}│"); + bump += 1; } } + // If line falls short and we're in rendering range, render a vertical bar (gap between splits) + if shorted > 0 && y < size.h { + result += &"─".repeat(shorted.saturating_sub(bump)); + } Ok(result) } diff --git a/src/main.rs b/src/main.rs index 99c4e4d8..afc2251a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,6 @@ macro_rules! ged { $editor.borrow_mut::().unwrap() }; } - /* /// TEMPORARY - REMOVE WHEN SPLITS ARE IMPLEMENTED use crate::editor::{FileLayout, FileContainer}; @@ -54,7 +53,7 @@ fn main() { editor.load_config("/home/luke/.oxrc", &lua); lua.load(PLUGIN_RUN).exec().unwrap(); - editor.files = FileLayout::TopToBottom(vec![ + editor.files = FileLayout::SideBySide(vec![ ( FileLayout::Atom(vec![ FileContainer { @@ -92,19 +91,21 @@ fn main() { ), ]); editor.active = true; - // editor.ptr = vec![1, 0]; - // editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - // editor.update_highlighter(); - // editor.ptr = vec![1, 1]; - // editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - // editor.update_highlighter(); - // editor.ptr = vec![0]; - // editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - // editor.update_highlighter(); - - // editor.update_render_cache(&lua, Size { w: 151, h: 15 }); - // editor.render(&lua); - // println!("\n\n\n{:#?}", editor.render_cache.span); + editor.ptr = vec![1, 0]; + editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + editor.update_highlighter(); + editor.ptr = vec![1, 1]; + editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + editor.update_highlighter(); + editor.ptr = vec![0]; + editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); + editor.update_highlighter(); + + // editor.update_render_cache(&lua, Size { w: 150, h: 50 }); + editor.render(&lua); + editor.terminal.show_cursor(); + editor.terminal.flush(); + println!("\n\n\n{:#?}", editor.render_cache.span); } */ From 7636de4ae3d09e0ac423a3787e6cebe16d8fbd5c Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:15:00 +0000 Subject: [PATCH 14/29] Fixed some more rendering issues --- src/editor/documents.rs | 2 +- src/editor/interface.rs | 25 ++++++++++++++++--------- src/main.rs | 5 +++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 04196e50..1534599a 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -93,7 +93,7 @@ impl FileLayout { } }) .collect(); - appropriate.sort_by(|a, b| a.1.start.cmp(&b.1.start)); + appropriate.sort_by(|a, b| a.2.start.cmp(&b.2.start)); appropriate } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index a0910afd..485ee91f 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -59,12 +59,17 @@ impl Editor { let tab_line_enabled = config!(self.config, tab_line).enabled; let mut result = String::new(); let fcs = FileLayout::line(y, &self.render_cache.span); - let shorted = size - .w - .saturating_sub(fcs.last().map(|ll| ll.2.end).unwrap_or(0)); - let mut bump = 0; + // Accounted for is used to detect gaps in lines (which should be filled with vertical bars) + let mut accounted_for = 0; // Render each component of this line for (mut c, (fc, rows, range)) in fcs.iter().enumerate() { + // Check if we have encountered an area of discontinuity in the line + if range.start != accounted_for { + // Discontinuity detected, fill with vertical bar! + let fill_length = range.start.saturating_sub(accounted_for); + result += &"─".repeat(fill_length); + } + // Render this part of the line let length = range.end.saturating_sub(range.start); let rel_y = y.saturating_sub(rows.start); if y == rows.start && tab_line_enabled { @@ -88,14 +93,16 @@ impl Editor { let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); // Insert vertical bar where appropriate - if shorted > 0 || c != fcs.len().saturating_sub(1) { + if c != fcs.len().saturating_sub(1) { result += &format!("{editor_bg}{editor_fg}│"); - bump += 1; } + accounted_for = range.end + 1; } - // If line falls short and we're in rendering range, render a vertical bar (gap between splits) - if shorted > 0 && y < size.h { - result += &"─".repeat(shorted.saturating_sub(bump)); + // Tack on any last vertical bar that is needed + if size.w != accounted_for { + // Discontinuity detected at the end, fill with vertical bar! + let fill_length = (size.w + 1).saturating_sub(accounted_for); + result += &"─".repeat(fill_length); } Ok(result) } diff --git a/src/main.rs b/src/main.rs index afc2251a..fcff889e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ macro_rules! ged { $editor.borrow_mut::().unwrap() }; } + /* /// TEMPORARY - REMOVE WHEN SPLITS ARE IMPLEMENTED use crate::editor::{FileLayout, FileContainer}; @@ -65,7 +66,7 @@ fn main() { 0.5, ), ( - FileLayout::SideBySide(vec![ + FileLayout::TopToBottom(vec![ ( FileLayout::Atom(vec![ FileContainer { @@ -101,7 +102,7 @@ fn main() { editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); editor.update_highlighter(); - // editor.update_render_cache(&lua, Size { w: 150, h: 50 }); + // editor.update_render_cache(&lua, Size { w: 100, h: 20 }); editor.render(&lua); editor.terminal.show_cursor(); editor.terminal.flush(); From 2a314e0f187a967361b3ebad6c46fcede2b72b1b Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:33:27 +0000 Subject: [PATCH 15/29] Made it so documents are always the correct size --- src/editor/documents.rs | 22 +++++++++++++++++++++- src/editor/interface.rs | 25 ++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 1534599a..a6dc4901 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -1,5 +1,5 @@ /// Tools for placing all information about open files into one place -use crate::editor::{get_absolute_path, FileType}; +use crate::editor::{get_absolute_path, Editor, FileType}; use kaolinite::Document; use kaolinite::Size; use std::ops::Range; @@ -153,6 +153,26 @@ impl FileLayout { span } + /// Update the sizes of documents + pub fn update_doc_sizes(&self, span: &Span, ed: &Editor) -> Vec<(Vec, usize, Size)> { + let mut result = vec![]; + // For each atom + for (idx, rows, cols) in span { + if let Some((fcs, _)) = self.get_atom(idx.clone()) { + // For each document in this atom + for (doc, fc) in fcs.iter().enumerate() { + // Work out correct new document width + let new_size = Size { + h: rows.end.saturating_sub(rows.start + ed.push_down + 1), + w: cols.end.saturating_sub(cols.start + ed.dent_for(&idx, doc)), + }; + result.push((idx.clone(), doc, new_size)); + } + } + } + result + } + /// Work out how many files are currently open pub fn len(&self) -> usize { match self { diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 485ee91f..8ccd64d6 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -114,14 +114,20 @@ impl Editor { return Ok(()); } self.needs_rerender = false; - // Get size information and update the document's size + // Get size information let mut size = size()?; let Size { w, mut h } = size.clone(); h = h.saturating_sub(1 + self.push_down); let max = self.dent(); - self.doc_mut().size.w = w.saturating_sub(max); // Update the cache before rendering self.update_render_cache(lua, size); + // Update all document's size + let updates = self.files.update_doc_sizes(&self.render_cache.span, &self); + for (ptr, doc, new_size) in updates { + self.files.get_atom_mut(ptr.clone()).unwrap().0[doc] + .doc + .size = new_size; + } // Hide the cursor before rendering self.terminal.hide_cursor(); // Render each line of the document @@ -658,10 +664,23 @@ impl Editor { /// Work out how much to push the document to the right (to make way for line numbers) pub fn dent(&self) -> usize { + if let Some((_, doc)) = self.files.get_atom(self.ptr.clone()) { + self.dent_for(&self.ptr, doc) + } else { + 0 + } + } + + /// Work out how much to push the document to the right (to make way for line numbers) + pub fn dent_for(&self, at: &Vec, doc: usize) -> usize { if config!(self.config, line_numbers).enabled { let padding_left = config!(self.config, line_numbers).padding_left; let padding_right = config!(self.config, line_numbers).padding_right; - self.doc().len_lines().to_string().len() + 1 + padding_left + padding_right + if let Some((fcs, _)) = self.files.get_atom(at.clone()) { + fcs[doc].doc.len_lines().to_string().len() + 1 + padding_left + padding_right + } else { + 0 + } } else { 0 } From 5e5eb7ff3b8c61f61b3630ec1ca17b7822aab8a2 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:03:43 +0000 Subject: [PATCH 16/29] Fixed status line disk indicator issue when working with splits --- src/config/interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/interface.rs b/src/config/interface.rs index 7e49dcde..acb64e61 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -350,7 +350,7 @@ impl StatusLine { .clone() .map_or("Unknown".to_string(), |ft| ft.name); let icon = fc.file_type.clone().map_or("󰈙 ".to_string(), |ft| ft.icon); - let modified = if doc.event_mgmt.with_disk(&editor.doc().take_snapshot()) { + let modified = if doc.event_mgmt.with_disk(&doc.take_snapshot()) { "" } else { "[+]" From a34ea2cabb44e89c2b6209bda2273a9a42016963 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:14:38 +0000 Subject: [PATCH 17/29] Reworked mouse feature to work with splits --- src/editor/interface.rs | 5 +- src/editor/mouse.rs | 103 ++++++++++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 8ccd64d6..b324bace 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -334,10 +334,7 @@ impl Editor { let render = tab_line.render(lua, file, &mut self.feedback); length += width(&render, 4) + 1; headers.push(render); - let ptr = self - .files - .get_atom(self.ptr.clone()) - .map_or(0, |(_, ptr)| ptr); + let ptr = self.files.get_atom(ptr.clone()).map_or(0, |(_, ptr)| ptr); if c == ptr { idx = headers.len().saturating_sub(1); } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index 39e7f264..2378a8b2 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -11,9 +11,9 @@ use super::Editor; /// Represents where the mouse has clicked / been dragged enum MouseLocation { /// Where the mouse has clicked within a file - File(Loc), + File(Vec, Loc), /// Where the mouse has clicked on a tab - Tabs(usize), + Tabs(Vec, usize), /// Mouse has clicked nothing of importance Out, } @@ -21,28 +21,63 @@ enum MouseLocation { impl Editor { /// Finds the position of the mouse within the viewport fn find_mouse_location(&mut self, lua: &Lua, event: MouseEvent) -> MouseLocation { + // Calculate various things beforehand + let row = event.row as usize; + let col = event.column as usize; let tab_enabled = config!(self.config, tab_line).enabled; let tab = usize::from(tab_enabled); - if event.row == 0 && tab_enabled { - let (tabs, _, offset) = - self.get_tab_parts(&self.ptr.clone(), lua, size().map_or(0, |s| s.w)); - let mut c = event.column + 2; - for (i, header) in tabs.iter().enumerate() { - let header_len = width(header, 4) + 1; - c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); - if c == 0 { - return MouseLocation::Tabs(i + offset); + // From a mouse click, locate the split that the user has clicked on + let at_idx = self + .render_cache + .span + .iter() + .find(|(idx, rows, cols)| rows.contains(&row) && cols.contains(&col)); + if let Some((idx, rows, cols)) = at_idx { + let idx = idx.clone(); + // Calculate the current dent in this split + let doc_idx = self.files.get_atom(idx.clone()).unwrap().1; + let dent = self.dent_for(&idx, doc_idx); + // Split that user clicked in located - adjust event location + let mut clicked = Loc { + x: col.saturating_sub(cols.start), + y: row.saturating_sub(rows.start), + }; + // Work out where the user clicked + if clicked.y == 0 && tab_enabled { + // Clicked on tab line + let (tabs, _, offset) = + self.get_tab_parts(&idx, lua, cols.end.saturating_sub(cols.start)); + // Try to work out which tab we clicked on + let mut c = u16::try_from(clicked.x).unwrap_or(u16::MAX) + 2; + for (i, header) in tabs.iter().enumerate() { + let header_len = width(header, 4) + 1; + c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); + if c == 0 { + // This tab was clicked on + return MouseLocation::Tabs(idx.clone(), i + offset); + } } + // Did not click on a tab + MouseLocation::Out + } else if clicked.y == rows.end.saturating_sub(1) { + // Clicked on status line + MouseLocation::Out + } else if clicked.x < dent { + // Clicked on line numbers + MouseLocation::Out + } else { + // Clicked on document + let offset = self.doc().offset; + MouseLocation::File( + idx.clone(), + Loc { + x: clicked.x.saturating_sub(dent), + y: clicked.y.saturating_sub(tab) + offset.y, + }, + ) } - MouseLocation::Out - } else if (event.column as usize) < self.dent() { - MouseLocation::Out } else { - let offset = self.doc().offset; - MouseLocation::File(Loc { - x: (event.column as usize).saturating_sub(self.dent()) + offset.x, - y: (event.row as usize).saturating_sub(tab) + offset.y, - }) + MouseLocation::Out } } @@ -65,15 +100,16 @@ impl Editor { } } match self.find_mouse_location(lua, event) { - MouseLocation::File(mut loc) => { + MouseLocation::File(idx, mut loc) => { + self.ptr = idx.clone(); self.doc_mut().clear_cursors(); loc.x = self.doc_mut().character_idx(&loc); self.doc_mut().move_to(&loc); self.doc_mut().old_cursor = self.doc().loc().x; } - MouseLocation::Tabs(i) => { - todo!("CHANGING TABS"); - //self.ptr = i; + MouseLocation::Tabs(idx, i) => { + self.files.move_to(idx.clone(), i); + self.ptr = idx.clone(); self.update_cwd(); } MouseLocation::Out => (), @@ -81,7 +117,8 @@ impl Editor { } MouseEventKind::Down(MouseButton::Right) => { // Select the current line - if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { + if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { + self.ptr = idx.clone(); self.doc_mut().select_line_at(loc.y); let line = self.doc().line(loc.y).unwrap_or_default(); self.alt_click_state = Some(( @@ -109,7 +146,8 @@ impl Editor { // Mouse drag MouseEventKind::Drag(MouseButton::Left) => { match self.find_mouse_location(lua, event) { - MouseLocation::File(mut loc) => { + MouseLocation::File(idx, mut loc) => { + self.ptr = idx.clone(); loc.x = self.doc_mut().character_idx(&loc); if let Some((dbl_start, dbl_end)) = self.alt_click_state { if loc.x > self.doc().cursor.selection_end.x { @@ -127,12 +165,13 @@ impl Editor { self.doc_mut().select_to(&loc); } } - MouseLocation::Tabs(_) | MouseLocation::Out => (), + MouseLocation::Tabs(_, _) | MouseLocation::Out => (), } } MouseEventKind::Drag(MouseButton::Right) => { match self.find_mouse_location(lua, event) { - MouseLocation::File(mut loc) => { + MouseLocation::File(idx, mut loc) => { + self.ptr = idx.clone(); loc.x = self.doc_mut().character_idx(&loc); if let Some((line_start, line_end)) = self.alt_click_state { if loc.y > self.doc().cursor.selection_end.y { @@ -150,12 +189,13 @@ impl Editor { self.doc_mut().select_to(&loc); } } - MouseLocation::Tabs(_) | MouseLocation::Out => (), + MouseLocation::Tabs(_, _) | MouseLocation::Out => (), } } // Mouse scroll behaviour MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { - if let MouseLocation::File(_) = self.find_mouse_location(lua, event) { + if let MouseLocation::File(idx, _) = self.find_mouse_location(lua, event) { + self.ptr = idx.clone(); let scroll_amount = config!(self.config, terminal).scroll_amount; for _ in 0..scroll_amount { if event.kind == MouseEventKind::ScrollDown { @@ -177,7 +217,8 @@ impl Editor { // Multi cursor behaviour KeyModifiers::CONTROL => { if let MouseEventKind::Down(MouseButton::Left) = event.kind { - if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { + if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { + self.ptr = idx.clone(); self.doc_mut().new_cursor(loc); } } @@ -189,7 +230,7 @@ impl Editor { /// Handle a double-click event pub fn handle_double_click(&mut self, lua: &Lua, event: MouseEvent) { // Select the current word - if let MouseLocation::File(loc) = self.find_mouse_location(lua, event) { + if let MouseLocation::File(idx, 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; From 918d94e3c5c33f5172c7f4cb2a8ff415be4f4804 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:11:00 +0000 Subject: [PATCH 18/29] Used geometric implementation when moving split focus --- src/config/editor.rs | 16 +++- src/editor/documents.rs | 185 ++++++++++++++++++++++++++++------------ 2 files changed, 141 insertions(+), 60 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 1eb89347..1bca928f 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -599,19 +599,27 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("focus_split_up", |_, editor, ()| { - editor.ptr = editor.files.move_up(editor.ptr.clone()); + editor.ptr = editor + .files + .move_up(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); methods.add_method_mut("focus_split_down", |_, editor, ()| { - editor.ptr = editor.files.move_down(editor.ptr.clone()); + editor.ptr = editor + .files + .move_down(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); methods.add_method_mut("focus_split_left", |_, editor, ()| { - editor.ptr = editor.files.move_left(editor.ptr.clone()); + editor.ptr = editor + .files + .move_left(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); methods.add_method_mut("focus_split_right", |_, editor, ()| { - editor.ptr = editor.files.move_right(editor.ptr.clone()); + editor.ptr = editor + .files + .move_right(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); // Searching and replacing diff --git a/src/editor/documents.rs b/src/editor/documents.rs index a6dc4901..30b8d87c 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -1,5 +1,6 @@ /// Tools for placing all information about open files into one place use crate::editor::{get_absolute_path, Editor, FileType}; +use crate::Loc; use kaolinite::Document; use kaolinite::Size; use std::ops::Range; @@ -606,85 +607,157 @@ impl FileLayout { subidx.map(|s| (at, s)) } + /// Find the current location in a particular span + pub fn get_line_pos(&self, at: Vec, span: &Span) -> Option<(usize, usize)> { + // Find the first line that has this split rendered in + let mut blank_count = 0; + let mut y = 0; + while blank_count < 2 { + let line = Self::line(y, span); + // Update blank detection + if line.is_empty() { + blank_count += 1; + } else { + blank_count = 0; + } + // Check whether this line contains the current split + let find_position = line + .iter() + .enumerate() + .find(|(c, (idx, _, _))| *idx == at) + .map(|(c, _)| c); + if let Some(our_idx) = find_position { + return Some((y, our_idx)); + } + // Ready for next iteration + y += 1; + } + None + } + + /// Find the current location in a particular span (last line) + pub fn get_line_pos_last(&self, at: Vec, span: &Span) -> Option<(usize, usize)> { + // Find the last line that has this split rendered in + let mut blank_count = 0; + let mut y = 0; + let mut result = None; + while blank_count < 2 { + let line = Self::line(y, span); + // Update blank detection + if line.is_empty() { + blank_count += 1; + } else { + blank_count = 0; + } + // Check whether this line contains the current split + let find_position = line + .iter() + .enumerate() + .find(|(c, (idx, _, _))| *idx == at) + .map(|(c, _)| c); + if let Some(our_idx) = find_position { + result = Some((y, our_idx)); + } else if result.is_some() { + break; + } + // Ready for next iteration + y += 1; + } + result + } + /// Find the new cursor position when moving left - pub fn move_left(&self, at: Vec) -> Vec { - if let Some((mut parent_idx, to_change)) = self.get_sidebyside_parent(at.clone()) { - // Move backward from where we were - let to_change = to_change.saturating_sub(1); - parent_idx.push(to_change); - // "Zoom in" down to atom level - while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = - self.get_raw(parent_idx.clone()) - { - parent_idx.push(0); + pub fn move_left(&self, at: Vec, span: &Span) -> Vec { + // Get the geometric location + if let Some((y, our_idx)) = self.get_line_pos(at.clone(), span) { + // Try to find the one before it + let prior_idx = our_idx.saturating_sub(1); + if let Some((new_idx, _, _)) = Self::line(y, span).get(prior_idx) { + new_idx.to_vec() + } else { + at } - parent_idx } else { at } } /// Find the new cursor position when moving right - pub fn move_right(&self, at: Vec) -> Vec { - if let Some((mut parent_idx, mut to_change)) = self.get_sidebyside_parent(at.clone()) { - // Move backward from where we were - if let Some(FileLayout::SideBySide(layouts)) = self.get_raw(parent_idx.clone()) { - if to_change + 1 < layouts.len() { - to_change = to_change + 1; - } - } - parent_idx.push(to_change); - // "Zoom in" down to atom level - while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = - self.get_raw(parent_idx.clone()) - { - parent_idx.push(0); + pub fn move_right(&self, at: Vec, span: &Span) -> Vec { + // Get the geometric location + if let Some((y, our_idx)) = self.get_line_pos(at.clone(), span) { + // Try to find the one after it + let next_idx = our_idx + 1; + if let Some((new_idx, _, _)) = Self::line(y, span).get(next_idx) { + return new_idx.to_vec(); + } else { + at } - parent_idx } else { at } } /// Find the new cursor position when moving up - pub fn move_up(&self, at: Vec) -> Vec { - if let Some((mut parent_idx, to_change)) = self.get_toptobottom_parent(at.clone()) { - // Move backward from where we were - let to_change = to_change.saturating_sub(1); - parent_idx.push(to_change); - // "Zoom in" down to atom level - while let Some( - FileLayout::TopToBottom(_) | FileLayout::SideBySide(_) | FileLayout::None, - ) = self.get_raw(parent_idx.clone()) - { - parent_idx.push(0); + pub fn move_up(&self, at: Vec, span: &Span) -> Vec { + // Get geometric location + if let Some((y, our_idx)) = self.get_line_pos(at.clone(), span) { + if let Some((_, _, cols)) = Self::line(y, span).get(our_idx) { + // Get the starting column index + let at_x = cols.start; + // Work from this y position upwards + let mut at_y = y.saturating_sub(1); + loop { + // Attempt to find part containing matching at_x value + if let Some((idx, _, _)) = Self::line(at_y, span) + .iter() + .find(|(idx, rows, cols)| cols.contains(&at_x)) + { + // Found match! + return idx.to_vec(); + } + if at_y == 0 { + break; + } else { + at_y = at_y.saturating_sub(1); + } + } } - parent_idx - } else { - at } + at } /// Find the new cursor position when moving down - pub fn move_down(&self, at: Vec) -> Vec { - if let Some((mut parent_idx, mut to_change)) = self.get_toptobottom_parent(at.clone()) { - // Move backward from where we were - if let Some(FileLayout::TopToBottom(layouts)) = self.get_raw(parent_idx.clone()) { - if to_change + 1 < layouts.len() { - to_change = to_change + 1; + pub fn move_down(&self, at: Vec, span: &Span) -> Vec { + // Get geometric location + if let Some((y, our_idx)) = self.get_line_pos_last(at.clone(), span) { + if let Some((_, _, cols)) = Self::line(y, span).get(our_idx) { + // Get the starting column index + let at_x = cols.start; + // Work from this y position downwards + let mut at_y = y + 1; + let mut blanks = 0; + loop { + // Attempt to find part containing matching at_x value + let line_reg = Self::line(at_y, span); + if let Some((idx, _, _)) = line_reg + .iter() + .find(|(idx, rows, cols)| cols.contains(&at_x)) + { + // Found match! + return idx.to_vec(); + } else if line_reg.is_empty() { + blanks += 1; + } + if blanks >= 2 { + break; + } else { + at_y += 1; + } } } - parent_idx.push(to_change); - // "Zoom in" down to atom level - while let Some(FileLayout::TopToBottom(_) | FileLayout::SideBySide(_)) = - self.get_raw(parent_idx.clone()) - { - parent_idx.push(0); - } - parent_idx - } else { - at } + at } } From c2807ad2503be4efa8de13d6b4258359629ac2ae Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:55:59 +0000 Subject: [PATCH 19/29] Fixed horizontal bars being placed where they shouldn't --- src/editor/documents.rs | 7 ++++++- src/main.rs | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 30b8d87c..70af53fb 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -149,7 +149,12 @@ impl FileLayout { row_span.end += shift_by; } } - + // Handle the case where heights within splits are awkwardly different + for (idx, rows, cols) in span.iter_mut() { + if rows.end == desired.h.saturating_sub(1) { + rows.end = desired.h; + } + } // Return the modified result span } diff --git a/src/main.rs b/src/main.rs index fcff889e..b19f06ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,11 +102,12 @@ fn main() { editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); editor.update_highlighter(); - // editor.update_render_cache(&lua, Size { w: 100, h: 20 }); + editor.update_render_cache(&lua, Size { w: 100, h: 47 }); editor.render(&lua); editor.terminal.show_cursor(); editor.terminal.flush(); - println!("\n\n\n{:#?}", editor.render_cache.span); + println!("{}", "\n".repeat(crossterm::terminal::size().unwrap().1.into())); + // println!("\n\n\n{:#?}", editor.render_cache.span); } */ From bba95f77f1b7d8461e31c0b8ac4531c572441e37 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:08:44 +0000 Subject: [PATCH 20/29] Simplified intro and added more info to README --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fccb1409..2fd12506 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,7 @@ Ox is an independent text editor that can be used to write everything from text If you're looking for a text editor that... 1. :feather: Is lightweight and efficient 2. :wrench: Can be configured to your heart's content -3. :package: Has features out of the box, including - - syntax highlighting - - undo and redo - - search and replace - - line numbers - - opening multiple files - - full mouse cursor interaction +3. :package: Has useful features out of the box ...then Ox is right up your street @@ -66,6 +60,8 @@ It works best on linux, but macOS and Windows are also supported. - :eye: UI that shows you the state of the editor and file - :computer_mouse: You can move the cursor and select text with your mouse - :writing_hand: Convenient shortcuts when writing code +- :crossed_swords: Multi-editing features such as multiple cursors and recordable macros +- :window: Splits to view multiple documents on the same screen at the same time ### Robustness From e8f842643d07ce38ebd09353c7b945fe9abb5c3d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:57:55 +0000 Subject: [PATCH 21/29] Much better resizing split command --- config/.oxrc | 4 +-- src/config/editor.rs | 40 ++++++++++++++------------ src/editor/documents.rs | 64 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 21 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 6629293b..6ff83b49 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -321,10 +321,10 @@ commands = { result = editor:open_split_down(file) elseif arguments[1] == "grow" then result = true - editor:grow_split(0.2) + editor:grow_split(0.15, arguments[2]) elseif arguments[1] == "shrink" then result = true - editor:shrink_split(0.2) + editor:shrink_split(0.15, arguments[2]) elseif arguments[1] == "focus" then result = true if arguments[2] == "up" then diff --git a/src/config/editor.rs b/src/config/editor.rs index 1bca928f..f1b62ae7 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -580,24 +580,28 @@ impl LuaUserData for Editor { Ok(false) } }); - methods.add_method_mut("grow_split", |_, editor, amount: f64| { - let current = editor.files.get_proportion(editor.ptr.clone()); - if current + amount <= 1.0 { - editor - .files - .set_proportion(editor.ptr.clone(), current + amount) - } - Ok(()) - }); - methods.add_method_mut("shrink_split", |_, editor, amount: f64| { - let current = editor.files.get_proportion(editor.ptr.clone()); - if current > amount { - editor - .files - .set_proportion(editor.ptr.clone(), current - amount) - } - Ok(()) - }); + methods.add_method_mut( + "grow_split", + |_, editor, (amount, direction): (f64, String)| { + match direction.as_str() { + "width" => editor.files.grow_width(editor.ptr.clone(), amount), + "height" => editor.files.grow_height(editor.ptr.clone(), amount), + _ => (), + } + Ok(()) + }, + ); + methods.add_method_mut( + "shrink_split", + |_, editor, (amount, direction): (f64, String)| { + match direction.as_str() { + "width" => editor.files.shrink_width(editor.ptr.clone(), amount), + "height" => editor.files.shrink_height(editor.ptr.clone(), amount), + _ => (), + } + Ok(()) + }, + ); methods.add_method_mut("focus_split_up", |_, editor, ()| { editor.ptr = editor .files diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 70af53fb..5757673b 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -588,13 +588,72 @@ impl FileLayout { } } - /// Find the nearest parent sidebyside, returns the pointer and we were in it + /// Shrink this split's width + pub fn shrink_width(&mut self, at: Vec, amount: f64) { + // Find the parent + if let Some((idx, one_down)) = self.get_sidebyside_parent(at.clone()) { + // Got a side by side parent! Adjust the proportion + let mut child = idx.clone(); + child.push(one_down); + let current_prop = self.get_proportion(child.clone()); + if current_prop > amount { + self.set_proportion(child, current_prop - amount); + } + } + } + + /// Grow this split's width + pub fn grow_width(&mut self, mut at: Vec, amount: f64) { + // Find the parent + if let Some((idx, one_down)) = self.get_sidebyside_parent(at.clone()) { + // Got a side by side parent! Adjust the proportion + let mut child = idx.clone(); + child.push(one_down); + let current_prop = self.get_proportion(child.clone()); + if current_prop + amount < 1.0 { + self.set_proportion(child, current_prop + amount); + } + } + } + + /// Shrink this split's height + pub fn shrink_height(&mut self, mut at: Vec, amount: f64) { + // Find the parent + if let Some((idx, one_down)) = self.get_toptobottom_parent(at.clone()) { + // Got a top to bottom parent! Adjust the proportion + let mut child = idx.clone(); + child.push(one_down); + let current_prop = self.get_proportion(child.clone()); + if current_prop > amount { + self.set_proportion(child, current_prop - amount); + } + } + } + + /// Grow this split's height + pub fn grow_height(&mut self, mut at: Vec, amount: f64) { + // Find the parent + if let Some((idx, one_down)) = self.get_toptobottom_parent(at.clone()) { + // Got a top to bottom parent! Adjust the proportion + let mut child = idx.clone(); + child.push(one_down); + let current_prop = self.get_proportion(child.clone()); + if current_prop + amount < 1.0 { + self.set_proportion(child, current_prop + amount); + } + } + } + + /// Find the nearest parent sidebyside, returns the pointer and where we were in it pub fn get_sidebyside_parent(&self, mut at: Vec) -> Option<(Vec, usize)> { // "Zoom out" to try and find a sidebyside parent let mut subidx = None; while let Some(FileLayout::TopToBottom(_) | FileLayout::Atom(_, _) | FileLayout::None) = self.get_raw(at.clone()) { + if at.is_empty() { + return None; + } subidx = at.pop(); } subidx.map(|s| (at, s)) @@ -607,6 +666,9 @@ impl FileLayout { while let Some(FileLayout::SideBySide(_) | FileLayout::Atom(_, _) | FileLayout::None) = self.get_raw(at.clone()) { + if at.is_empty() { + return None; + } subidx = at.pop(); } subidx.map(|s| (at, s)) From 4aefad7cb029b5b2e4aa9bcfe5ca5703fb691292 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:05:09 +0000 Subject: [PATCH 22/29] Splits can now be grown/shrunk with an optional size parameter --- config/.oxrc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 6ff83b49..01a03886 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -321,10 +321,12 @@ commands = { result = editor:open_split_down(file) elseif arguments[1] == "grow" then result = true - editor:grow_split(0.15, arguments[2]) + local amount = tonumber(arguments[3]) or 0.15 + editor:grow_split(amount, arguments[2]) elseif arguments[1] == "shrink" then result = true - editor:shrink_split(0.15, arguments[2]) + local amount = tonumber(arguments[3]) or 0.15 + editor:shrink_split(amount, arguments[2]) elseif arguments[1] == "focus" then result = true if arguments[2] == "up" then From 32ae6b24f0dfe9b643829a0cdf07ee3772d16394 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:50:25 +0000 Subject: [PATCH 23/29] Completely rewrote super dodgy span algorithm --- src/editor/documents.rs | 148 +++++++++++++++------------------------- src/editor/interface.rs | 2 +- src/editor/mod.rs | 1 + src/main.rs | 10 +-- 4 files changed, 61 insertions(+), 100 deletions(-) diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 5757673b..944e06dc 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -30,52 +30,73 @@ impl Default for FileLayout { impl FileLayout { /// Will return file containers and what span of columns and rows they take up /// In the format of (container, rows, columns) - pub fn span(&self, idx: Vec, size: Size) -> Span { + pub fn span(&self, idx: Vec, size: Size, at: Loc) -> Span { match self { Self::None => vec![], - Self::Atom(containers, ptr) => vec![(idx, 0..size.h, 0..size.w)], + // Atom: stretches from starting position through to end of it's container + Self::Atom(containers, ptr) => vec![(idx, at.y..at.y + size.h, at.x..at.x + size.w)], + // SideBySide: distributes available container space to each sub-layout Self::SideBySide(layouts) => { let mut result = vec![]; - let mut at = 0; - for (c, (layout, props)) in layouts.iter().enumerate() { - let mut subidx = idx.clone(); - subidx.push(c); - let this_size = Size { - w: (size.w as f64 * props) as usize, + let mut remaining = size.w.saturating_sub(1); + let mut up_to = at.x; + for (c, (layout, prop)) in layouts.iter().enumerate() { + let last = c == layouts.len().saturating_sub(1); + // Calculate the width + let mut base_width = ((size.w as f64 * prop) as usize); + if last { + // Tack on any remaining things + base_width += remaining.saturating_sub(base_width); + } else { + // Leave room for a vertical bar + base_width = base_width.saturating_sub(1); + } + // Calculate location and size information for this layout in particular + let sub_size = Size { + w: base_width, h: size.h, }; - for mut sub in layout.span(subidx, this_size) { - // Shift this range up to it's correct location - sub.2.start += at; - sub.2.end += at; - if c != layouts.len().saturating_sub(1) { - sub.2.end -= 1; - } - result.push(sub); - } - at += this_size.w; + let sub_at = Loc { x: up_to, y: at.y }; + // Calculate new index + let mut sub_idx = idx.clone(); + sub_idx.push(c); + let mut sub_span = layout.span(sub_idx, sub_size, sub_at); + // Update values + result.append(&mut sub_span); + remaining = remaining.saturating_sub(sub_size.w); + up_to += sub_size.w + if last { 0 } else { 1 }; } result } Self::TopToBottom(layouts) => { let mut result = vec![]; - let mut at = 0; - for (c, (layout, props)) in layouts.iter().enumerate() { - let mut subidx = idx.clone(); - subidx.push(c); - let this_size = Size { + let mut remaining = size.h.saturating_sub(1); + let mut up_to = at.y; + for (c, (layout, prop)) in layouts.iter().enumerate() { + let last = c == layouts.len().saturating_sub(1); + // Calculate the height + let mut base_height = ((size.h as f64 * prop) as usize); + if last { + // Tack on any remaining things + base_height += remaining.saturating_sub(base_height); + } else { + // Leave room for a horizontal bar + base_height = base_height.saturating_sub(1); + } + // Calculate location and size information for this layout in particular + let sub_size = Size { + h: base_height, w: size.w, - h: (size.h as f64 * props) as usize, }; - for mut sub in layout.span(subidx, this_size) { - sub.1.start += at; - sub.1.end += at; - if c != layouts.len().saturating_sub(1) { - sub.1.end -= 1; - } - result.push(sub.clone()); - } - at += this_size.h; + let sub_at = Loc { x: at.x, y: up_to }; + // Calculate new index + let mut sub_idx = idx.clone(); + sub_idx.push(c); + let mut sub_span = layout.span(sub_idx, sub_size, sub_at); + // Update values + result.append(&mut sub_span); + remaining = remaining.saturating_sub(sub_size.h); + up_to += sub_size.h + if last { 0 } else { 1 }; } result } @@ -98,67 +119,6 @@ impl FileLayout { appropriate } - /// Fixes span underflow (where nodes are shorter than desired due to division errors) - pub fn fix_underflow(mut span: Span, desired: Size) -> Span { - // FIX FOR WIDTH - // Go through each line in a span - for y in 0..desired.h { - let line = Self::line(y, &span); - if let Some((idx, rows, cols)) = line.get(line.len().saturating_sub(1)) { - // If this line has the width shorter than desired (and is the first of it's kind) - if cols.end < desired.w && y == rows.start { - if let Some((_, _, ref mut col_span)) = span - .iter_mut() - .find(|(checking_idx, _, _)| checking_idx == idx) - { - // Take the idx of the last node and push it up to ensure it fits - let shift_by = desired.w.saturating_sub(cols.end); - col_span.end += shift_by; - } - } - } - } - - // FIX FOR HEIGHT - // Work out: - // - The number of vacant line entries at the end of the desired height - // - The last non-empty entry in the line registry - let mut last_active_line = 0; - let mut empty_last_lines = 0; - for y in 0..desired.h { - let line = Self::line(y, &span); - if line.is_empty() { - empty_last_lines += 1; - } else { - last_active_line = y; - empty_last_lines = 0; - } - } - let last_panes = Self::line(last_active_line, &span) - .into_iter() - .map(|(idx, cols, rows)| idx) - .collect::>(); - // For each pane on the last non-empty line: - for pane_idx in last_panes { - if let Some((_, ref mut row_span, _)) = span - .iter_mut() - .find(|(checking_idx, _, _)| *checking_idx == pane_idx) - { - // Set the end of the rows range to the desired height (in effect expanding them downwards) - let shift_by = desired.h.saturating_sub(1 + last_active_line); - row_span.end += shift_by; - } - } - // Handle the case where heights within splits are awkwardly different - for (idx, rows, cols) in span.iter_mut() { - if rows.end == desired.h.saturating_sub(1) { - rows.end = desired.h; - } - } - // Return the modified result - span - } - /// Update the sizes of documents pub fn update_doc_sizes(&self, span: &Span, ed: &Editor) -> Vec<(Vec, usize, Size)> { let mut result = vec![]; diff --git a/src/editor/interface.rs b/src/editor/interface.rs index b324bace..1b8266e9 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -35,7 +35,7 @@ impl Editor { } } // Calculate span - self.render_cache.span = FileLayout::fix_underflow(self.files.span(vec![], size), size); + self.render_cache.span = self.files.span(vec![], size, Loc::at(0, 0)); // Calculate help message information let tab_width = config!(self.config, document).tab_width; self.render_cache.help_message = config!(self.config, help_message).render(lua); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index cb867439..bbed5a78 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -444,6 +444,7 @@ impl Editor { (KMod::NONE, KCode::Backspace) => self.backspace()?, (KMod::NONE, KCode::Delete) => self.delete()?, (KMod::NONE, KCode::Enter) => self.enter()?, + (KMod::CONTROL, KCode::Char('1')) => panic!("{:?}", self.render_cache.span), _ => (), } Ok(()) diff --git a/src/main.rs b/src/main.rs index b19f06ef..0e912121 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,11 +102,11 @@ fn main() { editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); editor.update_highlighter(); - editor.update_render_cache(&lua, Size { w: 100, h: 47 }); - editor.render(&lua); - editor.terminal.show_cursor(); - editor.terminal.flush(); - println!("{}", "\n".repeat(crossterm::terminal::size().unwrap().1.into())); + // editor.update_render_cache(&lua, Size { w: 100, h: 47 }); + // editor.render(&lua); + // editor.terminal.show_cursor(); + // editor.terminal.flush(); + // println!("{}", "\n".repeat(crossterm::terminal::size().unwrap().1.into())); // println!("\n\n\n{:#?}", editor.render_cache.span); } */ From 1b6660af693f7cf82fc7d6b4a69a65bfc000b3f4 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:37:59 +0000 Subject: [PATCH 24/29] Ran through clippy lints --- src/config/editor.rs | 30 +++--- src/config/interface.rs | 15 ++- src/editor/documents.rs | 196 ++++++++++++++-------------------------- src/editor/interface.rs | 137 +++++++++++++++------------- src/editor/mod.rs | 5 +- src/editor/mouse.rs | 20 ++-- src/editor/scanning.rs | 2 +- src/main.rs | 2 +- 8 files changed, 171 insertions(+), 236 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index f1b62ae7..2a6c0ef9 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -4,7 +4,7 @@ use crate::editor::{Editor, FileContainer, FileLayout}; use crate::ui::Feedback; use crate::{config, fatal_error, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN}; use kaolinite::utils::{get_absolute_path, get_cwd, get_file_ext, get_file_name}; -use kaolinite::{Loc, Size}; +use kaolinite::Loc; use mlua::prelude::*; impl LuaUserData for Editor { @@ -584,8 +584,8 @@ impl LuaUserData for Editor { "grow_split", |_, editor, (amount, direction): (f64, String)| { match direction.as_str() { - "width" => editor.files.grow_width(editor.ptr.clone(), amount), - "height" => editor.files.grow_height(editor.ptr.clone(), amount), + "width" => editor.files.grow_width(&editor.ptr, amount), + "height" => editor.files.grow_height(&editor.ptr, amount), _ => (), } Ok(()) @@ -595,35 +595,27 @@ impl LuaUserData for Editor { "shrink_split", |_, editor, (amount, direction): (f64, String)| { match direction.as_str() { - "width" => editor.files.shrink_width(editor.ptr.clone(), amount), - "height" => editor.files.shrink_height(editor.ptr.clone(), amount), + "width" => editor.files.shrink_width(&editor.ptr, amount), + "height" => editor.files.shrink_height(&editor.ptr, amount), _ => (), } Ok(()) }, ); methods.add_method_mut("focus_split_up", |_, editor, ()| { - editor.ptr = editor - .files - .move_up(editor.ptr.clone(), &editor.render_cache.span); + editor.ptr = FileLayout::move_up(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); methods.add_method_mut("focus_split_down", |_, editor, ()| { - editor.ptr = editor - .files - .move_down(editor.ptr.clone(), &editor.render_cache.span); + editor.ptr = FileLayout::move_down(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); methods.add_method_mut("focus_split_left", |_, editor, ()| { - editor.ptr = editor - .files - .move_left(editor.ptr.clone(), &editor.render_cache.span); + editor.ptr = FileLayout::move_left(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); methods.add_method_mut("focus_split_right", |_, editor, ()| { - editor.ptr = editor - .files - .move_right(editor.ptr.clone(), &editor.render_cache.span); + editor.ptr = FileLayout::move_right(editor.ptr.clone(), &editor.render_cache.span); Ok(()) }); // Searching and replacing @@ -633,7 +625,7 @@ impl LuaUserData for Editor { } editor.update_highlighter(); editor.needs_rerender = true; - editor.render(lua); + let _ = editor.render(lua); Ok(()) }); methods.add_method_mut("replace", |lua, editor, ()| { @@ -642,7 +634,7 @@ impl LuaUserData for Editor { } editor.update_highlighter(); editor.needs_rerender = true; - editor.render(lua); + let _ = editor.render(lua); Ok(()) }); methods.add_method_mut("move_next_match", |_, editor, query: String| { diff --git a/src/config/interface.rs b/src/config/interface.rs index acb64e61..43139ac5 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -1,16 +1,13 @@ /// Utilities for configuring and rendering parts of the interface use crate::cli::VERSION; use crate::editor::{Editor, FileContainer}; -use crate::error::Result; use crate::Feedback; -use crossterm::style::SetForegroundColor as Fg; use kaolinite::searching::Searcher; use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; use mlua::prelude::*; -use std::ops::Range; use std::result::Result as RResult; -use super::{issue_warning, Colors}; +use super::issue_warning; type LuaRes = RResult; @@ -101,11 +98,11 @@ impl Default for GreetingMessage { impl GreetingMessage { /// Take the configuration information and render the greeting message - pub fn render(&self, lua: &Lua) -> Result<(String, Vec)> { + pub fn render(&self, lua: &Lua) -> (String, Vec) { let mut result = self.format.clone(); // Substitute in simple values result = result.replace("{version}", VERSION).to_string(); - result = result.replace("\t", " ").to_string(); + result = result.replace('\t', " ").to_string(); // Handle highlighted part let start = result.find("{highlight_start}"); let end = result.find("{highlight_end}"); @@ -137,7 +134,7 @@ impl GreetingMessage { break; } } - Ok((result, highlighted)) + (result, highlighted) } } @@ -334,9 +331,9 @@ impl Default for StatusLine { impl StatusLine { /// Take the configuration information and render the status line - pub fn render(&self, ptr: &Vec, editor: &Editor, lua: &Lua, w: usize) -> LuaRes { + pub fn render(&self, ptr: &[usize], editor: &Editor, lua: &Lua, w: usize) -> LuaRes { let mut result = vec![]; - let fc = editor.files.get(ptr.clone()).unwrap(); + let fc = editor.files.get(ptr.to_vec()).unwrap(); let doc = &fc.doc; let path = doc .file_name diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 944e06dc..6ebcbbb3 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -30,11 +30,16 @@ impl Default for FileLayout { impl FileLayout { /// Will return file containers and what span of columns and rows they take up /// In the format of (container, rows, columns) + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss + )] pub fn span(&self, idx: Vec, size: Size, at: Loc) -> Span { match self { Self::None => vec![], // Atom: stretches from starting position through to end of it's container - Self::Atom(containers, ptr) => vec![(idx, at.y..at.y + size.h, at.x..at.x + size.w)], + Self::Atom(_, _) => vec![(idx, at.y..at.y + size.h, at.x..at.x + size.w)], // SideBySide: distributes available container space to each sub-layout Self::SideBySide(layouts) => { let mut result = vec![]; @@ -43,7 +48,7 @@ impl FileLayout { for (c, (layout, prop)) in layouts.iter().enumerate() { let last = c == layouts.len().saturating_sub(1); // Calculate the width - let mut base_width = ((size.w as f64 * prop) as usize); + let mut base_width = (size.w as f64 * prop) as usize; if last { // Tack on any remaining things base_width += remaining.saturating_sub(base_width); @@ -64,7 +69,7 @@ impl FileLayout { // Update values result.append(&mut sub_span); remaining = remaining.saturating_sub(sub_size.w); - up_to += sub_size.w + if last { 0 } else { 1 }; + up_to += sub_size.w + usize::from(!last); } result } @@ -75,7 +80,7 @@ impl FileLayout { for (c, (layout, prop)) in layouts.iter().enumerate() { let last = c == layouts.len().saturating_sub(1); // Calculate the height - let mut base_height = ((size.h as f64 * prop) as usize); + let mut base_height = (size.h as f64 * prop) as usize; if last { // Tack on any remaining things base_height += remaining.saturating_sub(base_height); @@ -96,7 +101,7 @@ impl FileLayout { // Update values result.append(&mut sub_span); remaining = remaining.saturating_sub(sub_size.h); - up_to += sub_size.h + if last { 0 } else { 1 }; + up_to += sub_size.h + usize::from(!last); } result } @@ -126,11 +131,11 @@ impl FileLayout { for (idx, rows, cols) in span { if let Some((fcs, _)) = self.get_atom(idx.clone()) { // For each document in this atom - for (doc, fc) in fcs.iter().enumerate() { + for (doc, _) in fcs.iter().enumerate() { // Work out correct new document width let new_size = Size { h: rows.end.saturating_sub(rows.start + ed.push_down + 1), - w: cols.end.saturating_sub(cols.start + ed.dent_for(&idx, doc)), + w: cols.end.saturating_sub(cols.start + ed.dent_for(idx, doc)), }; result.push((idx.clone(), doc, new_size)); } @@ -164,24 +169,12 @@ impl FileLayout { } None } - Self::SideBySide(layouts) => { - // Recursively scan - for (nth, (layout, _)) in layouts.iter().enumerate() { - let mut this_idx = idx.clone(); - this_idx.push(nth); - let result = layout.find(this_idx, path.clone()); - if result.is_some() { - return result; - } - } - None - } - Self::TopToBottom(layouts) => { + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { // Recursively scan for (nth, (layout, _)) in layouts.iter().enumerate() { let mut this_idx = idx.clone(); this_idx.push(nth); - let result = layout.find(this_idx, path.clone()); + let result = layout.find(this_idx, path); if result.is_some() { return result; } @@ -191,13 +184,12 @@ impl FileLayout { } } - /// Get the FileLayout at a certain index + /// Get the `FileLayout` at a certain index pub fn get_raw(&self, mut idx: Vec) -> Option<&FileLayout> { match self { - Self::None => Some(self), - Self::Atom(containers, ptr) => Some(self), + Self::None | Self::Atom(_, _) => Some(self), Self::SideBySide(layouts) => { - if idx.get(0).is_some() { + if idx.first().is_some() { let subidx = idx.remove(0); layouts.get(subidx)?.0.get_raw(idx) } else { @@ -205,7 +197,7 @@ impl FileLayout { } } Self::TopToBottom(layouts) => { - if idx.get(0).is_some() { + if idx.first().is_some() { let subidx = idx.remove(0); layouts.get(subidx)?.0.get_raw(idx) } else { @@ -215,14 +207,13 @@ impl FileLayout { } } - /// Get the FileLayout at a certain index (mutable) + /// Get the `FileLayout` at a certain index (mutable) pub fn get_raw_mut(&mut self, mut idx: Vec) -> Option<&mut FileLayout> { - if idx.get(0).is_none() { + if idx.first().is_none() { Some(self) } else { match self { - Self::None => Some(self), - Self::Atom(containers, ptr) => Some(self), + Self::None | Self::Atom(_, _) => Some(self), Self::SideBySide(layouts) => { let subidx = idx.remove(0); layouts.get_mut(subidx)?.0.get_raw_mut(idx) @@ -235,23 +226,14 @@ impl FileLayout { } } - /// Get the FileLayout at a certain index + /// Get the `FileLayout` at a certain index pub fn set(&mut self, mut idx: Vec, fl: FileLayout) { match self { - Self::None => *self = fl, - Self::Atom(_, _) => *self = fl, - Self::SideBySide(layouts) => { - if idx.get(0).is_some() { - let subidx = idx.remove(0); - layouts[subidx].0.set(idx, fl) - } else { - *self = fl; - } - } - Self::TopToBottom(layouts) => { - if idx.get(0).is_some() { + Self::None | Self::Atom(_, _) => *self = fl, + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { + if idx.first().is_some() { let subidx = idx.remove(0); - layouts[subidx].0.set(idx, fl) + layouts[subidx].0.set(idx, fl); } else { *self = fl; } @@ -299,12 +281,6 @@ impl FileLayout { self.get_atom(idx).map_or(vec![], |(fcs, _)| fcs) } - /// Given an index, find the file container in the tree - pub fn get_all_mut(&mut self, idx: Vec) -> Vec<&mut FileContainer> { - self.get_atom_mut(idx) - .map_or(vec![], |(fcs, _)| fcs.iter_mut().collect()) - } - /// Given an index, find the file container in the tree pub fn get(&self, idx: Vec) -> Option<&FileContainer> { let (fcs, ptr) = self.get_atom(idx)?; @@ -314,7 +290,7 @@ impl FileLayout { /// Given an index, find the file container in the tree pub fn get_mut(&mut self, idx: Vec) -> Option<&mut FileContainer> { let (fcs, ptr) = self.get_atom_mut(idx)?; - Some(fcs.get_mut(*ptr)?) + fcs.get_mut(*ptr) } /// In the currently active atom, move to a different document @@ -322,13 +298,9 @@ impl FileLayout { match self { Self::None => (), Self::Atom(_, ref mut old_ptr) => *old_ptr = ptr, - Self::SideBySide(layouts) => { - let subidx = idx.remove(0); - layouts[subidx].0.move_to(idx, ptr) - } - Self::TopToBottom(layouts) => { + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { let subidx = idx.remove(0); - layouts[subidx].0.move_to(idx, ptr) + layouts[subidx].0.move_to(idx, ptr); } } } @@ -343,6 +315,7 @@ impl FileLayout { } /// Remove a certain index from this tree + #[allow(clippy::cast_precision_loss)] pub fn remove(&mut self, at: Vec) { // Get parent of the node we wish to delete let mut at_parent = at.clone(); @@ -400,9 +373,9 @@ impl FileLayout { } /// Find a new pointer position when something is removed - pub fn new_pointer_position(&self, old: Vec) -> Vec { + pub fn new_pointer_position(&self, old: &[usize]) -> Vec { // Zoom out until a sidebyside or toptobottom is found - let mut copy = old.clone(); + let mut copy = old.to_owned(); while let Some(Self::None | Self::Atom(_, _)) | None = self.get_raw(copy.clone()) { copy.pop(); if copy.is_empty() { @@ -424,15 +397,7 @@ impl FileLayout { if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(containers, ptr) => { - new_ptr.push(0); - Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) - } - Self::SideBySide(layouts) => { - new_ptr.push(0); - Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) - } - Self::TopToBottom(layouts) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { new_ptr.push(0); Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } @@ -448,15 +413,7 @@ impl FileLayout { if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(containers, ptr) => { - new_ptr.push(1); - Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) - } - Self::SideBySide(layouts) => { - new_ptr.push(1); - Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) - } - Self::TopToBottom(layouts) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { new_ptr.push(1); Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } @@ -472,15 +429,7 @@ impl FileLayout { if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(containers, ptr) => { - new_ptr.push(0); - Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) - } - Self::SideBySide(layouts) => { - new_ptr.push(0); - Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) - } - Self::TopToBottom(layouts) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { new_ptr.push(0); Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } @@ -496,15 +445,7 @@ impl FileLayout { if let Some(old_fl) = self.get_raw(at.clone()) { let new_fl = match old_fl { Self::None => fl, - Self::Atom(containers, ptr) => { - new_ptr.push(1); - Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) - } - Self::SideBySide(layouts) => { - new_ptr.push(1); - Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) - } - Self::TopToBottom(layouts) => { + Self::Atom(_, _) | Self::SideBySide(_) | Self::TopToBottom(_) => { new_ptr.push(1); Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } @@ -530,6 +471,7 @@ impl FileLayout { } /// Get the proportion of a certain node in the tree + #[allow(clippy::cast_precision_loss)] pub fn set_proportion(&mut self, mut at: Vec, amount: f64) { if let Some(last_idx) = at.pop() { if let Some(FileLayout::SideBySide(layouts) | FileLayout::TopToBottom(layouts)) = @@ -549,9 +491,9 @@ impl FileLayout { } /// Shrink this split's width - pub fn shrink_width(&mut self, at: Vec, amount: f64) { + pub fn shrink_width(&mut self, at: &[usize], amount: f64) { // Find the parent - if let Some((idx, one_down)) = self.get_sidebyside_parent(at.clone()) { + if let Some((idx, one_down)) = self.get_sidebyside_parent(at.to_vec()) { // Got a side by side parent! Adjust the proportion let mut child = idx.clone(); child.push(one_down); @@ -563,9 +505,9 @@ impl FileLayout { } /// Grow this split's width - pub fn grow_width(&mut self, mut at: Vec, amount: f64) { + pub fn grow_width(&mut self, at: &[usize], amount: f64) { // Find the parent - if let Some((idx, one_down)) = self.get_sidebyside_parent(at.clone()) { + if let Some((idx, one_down)) = self.get_sidebyside_parent(at.to_vec()) { // Got a side by side parent! Adjust the proportion let mut child = idx.clone(); child.push(one_down); @@ -577,9 +519,9 @@ impl FileLayout { } /// Shrink this split's height - pub fn shrink_height(&mut self, mut at: Vec, amount: f64) { + pub fn shrink_height(&mut self, at: &[usize], amount: f64) { // Find the parent - if let Some((idx, one_down)) = self.get_toptobottom_parent(at.clone()) { + if let Some((idx, one_down)) = self.get_toptobottom_parent(at.to_vec()) { // Got a top to bottom parent! Adjust the proportion let mut child = idx.clone(); child.push(one_down); @@ -591,9 +533,9 @@ impl FileLayout { } /// Grow this split's height - pub fn grow_height(&mut self, mut at: Vec, amount: f64) { + pub fn grow_height(&mut self, at: &[usize], amount: f64) { // Find the parent - if let Some((idx, one_down)) = self.get_toptobottom_parent(at.clone()) { + if let Some((idx, one_down)) = self.get_toptobottom_parent(at.to_vec()) { // Got a top to bottom parent! Adjust the proportion let mut child = idx.clone(); child.push(one_down); @@ -635,7 +577,7 @@ impl FileLayout { } /// Find the current location in a particular span - pub fn get_line_pos(&self, at: Vec, span: &Span) -> Option<(usize, usize)> { + pub fn get_line_pos(at: &[usize], span: &Span) -> Option<(usize, usize)> { // Find the first line that has this split rendered in let mut blank_count = 0; let mut y = 0; @@ -651,7 +593,7 @@ impl FileLayout { let find_position = line .iter() .enumerate() - .find(|(c, (idx, _, _))| *idx == at) + .find(|(_, (idx, _, _))| *idx == at) .map(|(c, _)| c); if let Some(our_idx) = find_position { return Some((y, our_idx)); @@ -663,7 +605,7 @@ impl FileLayout { } /// Find the current location in a particular span (last line) - pub fn get_line_pos_last(&self, at: Vec, span: &Span) -> Option<(usize, usize)> { + pub fn get_line_pos_last(at: &[usize], span: &Span) -> Option<(usize, usize)> { // Find the last line that has this split rendered in let mut blank_count = 0; let mut y = 0; @@ -680,7 +622,7 @@ impl FileLayout { let find_position = line .iter() .enumerate() - .find(|(c, (idx, _, _))| *idx == at) + .find(|(_, (idx, _, _))| *idx == at) .map(|(c, _)| c); if let Some(our_idx) = find_position { result = Some((y, our_idx)); @@ -694,13 +636,13 @@ impl FileLayout { } /// Find the new cursor position when moving left - pub fn move_left(&self, at: Vec, span: &Span) -> Vec { + pub fn move_left(at: Vec, span: &Span) -> Vec { // Get the geometric location - if let Some((y, our_idx)) = self.get_line_pos(at.clone(), span) { + if let Some((y, our_idx)) = Self::get_line_pos(&at, span) { // Try to find the one before it let prior_idx = our_idx.saturating_sub(1); if let Some((new_idx, _, _)) = Self::line(y, span).get(prior_idx) { - new_idx.to_vec() + new_idx.clone() } else { at } @@ -710,25 +652,24 @@ impl FileLayout { } /// Find the new cursor position when moving right - pub fn move_right(&self, at: Vec, span: &Span) -> Vec { + pub fn move_right(at: Vec, span: &Span) -> Vec { // Get the geometric location - if let Some((y, our_idx)) = self.get_line_pos(at.clone(), span) { + if let Some((y, our_idx)) = Self::get_line_pos(&at, span) { // Try to find the one after it let next_idx = our_idx + 1; if let Some((new_idx, _, _)) = Self::line(y, span).get(next_idx) { - return new_idx.to_vec(); - } else { - at + return new_idx.clone(); } + at } else { at } } /// Find the new cursor position when moving up - pub fn move_up(&self, at: Vec, span: &Span) -> Vec { + pub fn move_up(at: Vec, span: &Span) -> Vec { // Get geometric location - if let Some((y, our_idx)) = self.get_line_pos(at.clone(), span) { + if let Some((y, our_idx)) = Self::get_line_pos(&at, span) { if let Some((_, _, cols)) = Self::line(y, span).get(our_idx) { // Get the starting column index let at_x = cols.start; @@ -738,16 +679,15 @@ impl FileLayout { // Attempt to find part containing matching at_x value if let Some((idx, _, _)) = Self::line(at_y, span) .iter() - .find(|(idx, rows, cols)| cols.contains(&at_x)) + .find(|(_, _, cols)| cols.contains(&at_x)) { // Found match! - return idx.to_vec(); + return idx.clone(); } if at_y == 0 { break; - } else { - at_y = at_y.saturating_sub(1); } + at_y = at_y.saturating_sub(1); } } } @@ -755,9 +695,9 @@ impl FileLayout { } /// Find the new cursor position when moving down - pub fn move_down(&self, at: Vec, span: &Span) -> Vec { + pub fn move_down(at: Vec, span: &Span) -> Vec { // Get geometric location - if let Some((y, our_idx)) = self.get_line_pos_last(at.clone(), span) { + if let Some((y, our_idx)) = Self::get_line_pos_last(&at, span) { if let Some((_, _, cols)) = Self::line(y, span).get(our_idx) { // Get the starting column index let at_x = cols.start; @@ -767,20 +707,18 @@ impl FileLayout { loop { // Attempt to find part containing matching at_x value let line_reg = Self::line(at_y, span); - if let Some((idx, _, _)) = line_reg - .iter() - .find(|(idx, rows, cols)| cols.contains(&at_x)) + if let Some((idx, _, _)) = + line_reg.iter().find(|(_, _, cols)| cols.contains(&at_x)) { // Found match! - return idx.to_vec(); + return idx.clone(); } else if line_reg.is_empty() { blanks += 1; } if blanks >= 2 { break; - } else { - at_y += 1; } + at_y += 1; } } } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 1b8266e9..7f1ed6d7 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,5 +1,5 @@ -use crate::editor::{FileContainer, FileLayout}; /// Functions for rendering the UI +use crate::editor::FileLayout; use crate::error::{OxError, Result}; use crate::events::wait_for_event_hog; use crate::ui::{key_event, size, Feedback}; @@ -27,12 +27,11 @@ pub struct RenderCache { impl Editor { /// Update the render cache + #[allow(clippy::range_plus_one)] pub fn update_render_cache(&mut self, lua: &Lua, size: Size) { // Calculate greeting message if config!(self.config, tab_line).enabled && self.greet { - if let Ok(gm) = config!(self.config, greeting_message).render(lua) { - self.render_cache.greeting_message = gm; - } + self.render_cache.greeting_message = config!(self.config, greeting_message).render(lua); } // Calculate span self.render_cache.span = self.files.span(vec![], size, Loc::at(0, 0)); @@ -48,13 +47,13 @@ impl Editor { .unwrap_or(0) + 5; let help_length = self.render_cache.help_message.len(); - let help_start = - usize::try_from((size.h / 2).saturating_sub(help_length / 2) + 1).unwrap_or(usize::MAX); - let help_end = help_start + usize::try_from(help_length).unwrap_or(usize::MAX) as usize; + let help_start = (size.h / 2).saturating_sub(help_length / 2) + 1; + let help_end = help_start + help_length; self.render_cache.help_message_span = help_start..help_end + 1; } /// Render a specific line + #[allow(clippy::similar_names)] pub fn render_line(&mut self, y: usize, size: Size, lua: &Lua) -> Result { let tab_line_enabled = config!(self.config, tab_line).enabled; let mut result = String::new(); @@ -62,7 +61,7 @@ impl Editor { // Accounted for is used to detect gaps in lines (which should be filled with vertical bars) let mut accounted_for = 0; // Render each component of this line - for (mut c, (fc, rows, range)) in fcs.iter().enumerate() { + for (c, (fc, rows, range)) in fcs.iter().enumerate() { // Check if we have encountered an area of discontinuity in the line if range.start != accounted_for { // Discontinuity detected, fill with vertical bar! @@ -74,16 +73,15 @@ impl Editor { let rel_y = y.saturating_sub(rows.start); if y == rows.start && tab_line_enabled { // Tab line - result += &self.render_tab_line(&fc, lua, length)?; + result += &self.render_tab_line(fc, lua, length)?; } else if y == rows.end.saturating_sub(1) { // Status line - result += &self.render_status_line(&fc, lua, length)?; + result += &self.render_status_line(fc, lua, length)?; } else { // Line of file result += &self.render_document( - &fc, + fc, rel_y.saturating_sub(self.push_down), - lua, Size { w: length, h: size.h, @@ -115,14 +113,13 @@ impl Editor { } self.needs_rerender = false; // Get size information - let mut size = size()?; - let Size { w, mut h } = size.clone(); + let size = size()?; + let Size { w, mut h } = size; h = h.saturating_sub(1 + self.push_down); - let max = self.dent(); // Update the cache before rendering self.update_render_cache(lua, size); // Update all document's size - let updates = self.files.update_doc_sizes(&self.render_cache.span, &self); + let updates = self.files.update_doc_sizes(&self.render_cache.span, self); for (ptr, doc, new_size) in updates { self.files.get_atom_mut(ptr.clone()).unwrap().0[doc] .doc @@ -162,13 +159,8 @@ impl Editor { } /// Render the lines of the document - pub fn render_document( - &mut self, - ptr: &Vec, - y: usize, - lua: &Lua, - size: Size, - ) -> Result { + #[allow(clippy::similar_names)] + pub fn render_document(&mut self, ptr: &Vec, y: usize, size: Size) -> Result { let Size { mut w, h } = size; let mut result = String::new(); // Get various information @@ -178,7 +170,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()?); - let colors = Fg(config!(self.config, colors).highlight.to_color()?); let underline = SetAttribute(Attribute::Underlined); let no_underline = SetAttribute(Attribute::NoUnderline); let tab_width = config!(self.config, document).tab_width; @@ -220,24 +211,7 @@ impl Editor { let mut x_char = doc.character_idx(&doc.offset); for token in tokens { // Find out the text (and colour of that text) - let (text, colour) = match token { - // Non-highlighted text - TokOpt::Some(text, kind) => { - let colour = config!(self.config, syntax).get_theme(&kind); - let colour = match colour { - // Success, write token - Ok(col) => Fg(col), - // Failure, show error message and don't highlight this token - Err(err) => { - self.feedback = Feedback::Error(err.to_string()); - editor_fg - } - }; - (text, colour) - } - // Highlighted text - TokOpt::None(text) => (text, editor_fg), - }; + let (text, colour) = self.breakdown_token(token)?; // Do the rendering (including selection where applicable) for c in text.chars() { let disp_loc = Loc { @@ -292,36 +266,68 @@ impl Editor { result += &" ".repeat(w.saturating_sub(total_width)); } else if config!(self.config, greeting_message).enabled && self.greet { // Render the greeting message (if enabled) - result += &self.render_greeting(y, lua, w, h)?; + result += &self.render_greeting(y, w, h)?; } else { // Empty line, just pad out with spaces to prevent artefacts result += &" ".repeat(w); } // Add on help message if applicable if help_message_here { - let at = y.saturating_sub(self.render_cache.help_message_span.start); - let max_width = self.render_cache.help_message_width; - let (hl, msg) = self - .render_cache - .help_message - .get(at) - .map(|(hl, content)| (*hl, content.to_string())) - .unwrap_or((false, " ".repeat(max_width))); - let extra_padding = " ".repeat(max_width.saturating_sub(width(&msg, tab_width))); - if hl { - result += &format!("{colors}{msg}{extra_padding}{editor_fg}"); - } else { - result += &format!("{editor_fg}{msg}{extra_padding}"); - } + result += &self.render_help_message(y)?; } // Send out the result Ok(result) } + /// Render help message + pub fn render_help_message(&self, y: usize) -> Result { + let tab_width = config!(self.config, document).tab_width; + let colors = Fg(config!(self.config, colors).highlight.to_color()?); + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + let at = y.saturating_sub(self.render_cache.help_message_span.start); + let max_width = self.render_cache.help_message_width; + let (hl, msg) = self + .render_cache + .help_message + .get(at) + .map_or((false, " ".repeat(max_width)), |(hl, content)| { + (*hl, content.to_string()) + }); + let extra_padding = " ".repeat(max_width.saturating_sub(width(&msg, tab_width))); + if hl { + Ok(format!("{colors}{msg}{extra_padding}{editor_fg}")) + } else { + Ok(format!("{editor_fg}{msg}{extra_padding}")) + } + } + + /// Take a token and try to break it down into a colour and text + pub fn breakdown_token(&mut self, token: TokOpt) -> Result<(String, Fg)> { + let editor_fg = Fg(config!(self.config, colors).editor_fg.to_color()?); + match token { + // Non-highlighted text + TokOpt::Some(text, kind) => { + let colour = config!(self.config, syntax).get_theme(&kind); + let colour = match colour { + // Success, write token + Ok(col) => Fg(col), + // Failure, show error message and don't highlight this token + Err(err) => { + self.feedback = Feedback::Error(err.to_string()); + editor_fg + } + }; + Ok((text, colour)) + } + // Highlighted text + TokOpt::None(text) => Ok((text, editor_fg)), + } + } + /// Get list of tabs pub fn get_tab_parts( &mut self, - ptr: &Vec, + ptr: &[usize], lua: &Lua, w: usize, ) -> (Vec, usize, usize) { @@ -334,7 +340,10 @@ impl Editor { let render = tab_line.render(lua, file, &mut self.feedback); length += width(&render, 4) + 1; headers.push(render); - let ptr = self.files.get_atom(ptr.clone()).map_or(0, |(_, ptr)| ptr); + let ptr = self + .files + .get_atom(ptr.to_owned()) + .map_or(0, |(_, ptr)| ptr); if c == ptr { idx = headers.len().saturating_sub(1); } @@ -350,7 +359,7 @@ impl Editor { /// Render the tab line at the top of the document #[allow(clippy::similar_names)] - pub fn render_tab_line(&mut self, ptr: &Vec, lua: &Lua, w: usize) -> Result { + pub fn render_tab_line(&mut self, ptr: &[usize], lua: &Lua, w: usize) -> Result { 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()?); @@ -377,7 +386,7 @@ impl Editor { /// Render the status line at the bottom of the document #[allow(clippy::similar_names)] - pub fn render_status_line(&mut self, ptr: &Vec, lua: &Lua, w: usize) -> Result { + pub fn render_status_line(&mut self, ptr: &[usize], lua: &Lua, w: usize) -> Result { 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()?); @@ -414,7 +423,7 @@ impl Editor { } /// Render the greeting message - fn render_greeting(&mut self, y: usize, lua: &Lua, w: usize, h: usize) -> Result { + fn render_greeting(&mut self, y: usize, w: usize, h: usize) -> Result { // Produce the greeting message let colors = config!(self.config, colors); let highlight = Fg(colors.highlight.to_color()?).to_string(); @@ -669,11 +678,11 @@ impl Editor { } /// Work out how much to push the document to the right (to make way for line numbers) - pub fn dent_for(&self, at: &Vec, doc: usize) -> usize { + pub fn dent_for(&self, at: &[usize], doc: usize) -> usize { if config!(self.config, line_numbers).enabled { let padding_left = config!(self.config, line_numbers).padding_left; let padding_right = config!(self.config, line_numbers).padding_right; - if let Some((fcs, _)) = self.files.get_atom(at.clone()) { + if let Some((fcs, _)) = self.files.get_atom(at.to_owned()) { fcs[doc].doc.len_lines().to_string().len() + 1 + padding_left + padding_right } else { 0 diff --git a/src/editor/mod.rs b/src/editor/mod.rs index bbed5a78..20ffca6e 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -170,7 +170,7 @@ impl Editor { self.already_open(&get_absolute_path(file_name).unwrap_or_default()) { // Move to existing file - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); self.files.move_to(idx, ptr); // Send out error message let file = get_file_name(file_name).unwrap_or_default(); @@ -310,11 +310,10 @@ impl Editor { fcs.remove(*ptr); self.prev(); } - let (fcs, ptr) = self.files.get_atom(self.ptr.clone()).unwrap(); // Clean up the file structure self.files.clean_up(); // Find a new pointer position - self.ptr = self.files.new_pointer_position(self.ptr.clone()); + self.ptr = self.files.new_pointer_position(&self.ptr); } // If there are no longer any active atoms, quit the entire editor self.active = !matches!(self.files, FileLayout::None); diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index 2378a8b2..cd2a37a1 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -1,6 +1,5 @@ /// For handling mouse events use crate::config; -use crate::ui::size; use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use kaolinite::{utils::width, Loc}; use mlua::Lua; @@ -31,14 +30,14 @@ impl Editor { .render_cache .span .iter() - .find(|(idx, rows, cols)| rows.contains(&row) && cols.contains(&col)); + .find(|(_, rows, cols)| rows.contains(&row) && cols.contains(&col)); if let Some((idx, rows, cols)) = at_idx { let idx = idx.clone(); // Calculate the current dent in this split let doc_idx = self.files.get_atom(idx.clone()).unwrap().1; let dent = self.dent_for(&idx, doc_idx); // Split that user clicked in located - adjust event location - let mut clicked = Loc { + let clicked = Loc { x: col.saturating_sub(cols.start), y: row.saturating_sub(rows.start), }; @@ -101,7 +100,7 @@ impl Editor { } match self.find_mouse_location(lua, event) { MouseLocation::File(idx, mut loc) => { - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); self.doc_mut().clear_cursors(); loc.x = self.doc_mut().character_idx(&loc); self.doc_mut().move_to(&loc); @@ -109,7 +108,7 @@ impl Editor { } MouseLocation::Tabs(idx, i) => { self.files.move_to(idx.clone(), i); - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); self.update_cwd(); } MouseLocation::Out => (), @@ -118,7 +117,7 @@ impl Editor { MouseEventKind::Down(MouseButton::Right) => { // Select the current line if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); self.doc_mut().select_line_at(loc.y); let line = self.doc().line(loc.y).unwrap_or_default(); self.alt_click_state = Some(( @@ -147,7 +146,7 @@ impl Editor { MouseEventKind::Drag(MouseButton::Left) => { match self.find_mouse_location(lua, event) { MouseLocation::File(idx, mut loc) => { - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); loc.x = self.doc_mut().character_idx(&loc); if let Some((dbl_start, dbl_end)) = self.alt_click_state { if loc.x > self.doc().cursor.selection_end.x { @@ -171,7 +170,7 @@ impl Editor { MouseEventKind::Drag(MouseButton::Right) => { match self.find_mouse_location(lua, event) { MouseLocation::File(idx, mut loc) => { - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); loc.x = self.doc_mut().character_idx(&loc); if let Some((line_start, line_end)) = self.alt_click_state { if loc.y > self.doc().cursor.selection_end.y { @@ -195,7 +194,7 @@ impl Editor { // Mouse scroll behaviour MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { if let MouseLocation::File(idx, _) = self.find_mouse_location(lua, event) { - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); let scroll_amount = config!(self.config, terminal).scroll_amount; for _ in 0..scroll_amount { if event.kind == MouseEventKind::ScrollDown { @@ -218,7 +217,7 @@ impl Editor { KeyModifiers::CONTROL => { if let MouseEventKind::Down(MouseButton::Left) = event.kind { if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { - self.ptr = idx.clone(); + self.ptr.clone_from(&idx); self.doc_mut().new_cursor(loc); } } @@ -231,6 +230,7 @@ impl Editor { pub fn handle_double_click(&mut self, lua: &Lua, event: MouseEvent) { // Select the current word if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { + self.ptr.clone_from(&idx); self.doc_mut().select_word_at(&loc); let mut selection = self.doc().cursor.selection_end; let mut cursor = self.doc().cursor.loc; diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 3b934c0b..2915703a 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -155,7 +155,7 @@ impl Editor { let target = self.prompt("Replace")?; let into = self.prompt("With")?; let mut done = false; - let Size { w, h } = size()?; + let h = size()?.h; // Jump to match let mut mtch; if let Some(m) = self.next_match(&target) { diff --git a/src/main.rs b/src/main.rs index 0e912121..3f4098b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -224,7 +224,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { holder.blank()?; let this_doc = holder.doc_len().saturating_sub(1); let current_ptr = holder.ptr.clone(); - let mut doc = &mut holder.files.get_atom_mut(current_ptr).unwrap().0[this_doc].doc; + let doc = &mut holder.files.get_atom_mut(current_ptr).unwrap().0[this_doc].doc; doc.exe(Event::Insert(Loc { x: 0, y: 0 }, stdin))?; doc.load_to(doc.size.h); let lines = doc.lines.clone(); From f25ad71c0c6984546aa7aa30314459e6e6b1a3ae Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:13:33 +0000 Subject: [PATCH 25/29] Fixed mouse not taking into account x offset --- src/editor/mouse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index cd2a37a1..c829d3c7 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -70,7 +70,7 @@ impl Editor { MouseLocation::File( idx.clone(), Loc { - x: clicked.x.saturating_sub(dent), + x: clicked.x.saturating_sub(dent) + offset.x, y: clicked.y.saturating_sub(tab) + offset.y, }, ) From ab1e1ce510f4ceb7d71215bd1ece3d883b4a010f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:37:06 +0000 Subject: [PATCH 26/29] Fixed replace feature not rendering correctly --- src/editor/scanning.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 2915703a..09c9716e 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -151,11 +151,12 @@ impl Editor { /// Use replace feature pub fn replace(&mut self, lua: &Lua) -> Result<()> { + let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); // Request replace information let target = self.prompt("Replace")?; let into = self.prompt("With")?; let mut done = false; - let h = size()?.h; + let Size { w, h } = size()?; // Jump to match let mut mtch; if let Some(m) = self.next_match(&target) { @@ -172,16 +173,18 @@ impl Editor { self.update_highlighter(); // Enter into the replace menu while !done { - // Render just the document part - self.terminal.hide_cursor(); + // Rerender + self.needs_rerender = true; self.render(lua)?; // Write custom status line for the replace mode - self.terminal.goto(0, h); + self.terminal.prepare_line(h); display!( self, + editor_bg, Print( "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All | [Esc] Exit" - ) + ), + Print(" ".repeat(w.saturating_sub(76))) ); // Move back to correct cursor location if let Some(Loc { x, y }) = self.cursor_position() { From ee680eee908ad47499f406ecbef02e66693af5fe Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:27:00 +0000 Subject: [PATCH 27/29] Removed debug infrastructure from code --- src/editor/mod.rs | 2 -- src/main.rs | 74 ----------------------------------------------- 2 files changed, 76 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 20ffca6e..8b0851c9 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -243,7 +243,6 @@ impl Editor { } /// Determine if a file is already open - /// TODO: Requires a rewrite (you shouldn't be able to open a duplicate file in another split) pub fn already_open(&mut self, abs_path: &str) -> Option<(Vec, usize)> { self.files.find(vec![], abs_path) } @@ -443,7 +442,6 @@ impl Editor { (KMod::NONE, KCode::Backspace) => self.backspace()?, (KMod::NONE, KCode::Delete) => self.delete()?, (KMod::NONE, KCode::Enter) => self.enter()?, - (KMod::CONTROL, KCode::Char('1')) => panic!("{:?}", self.render_cache.span), _ => (), } Ok(()) diff --git a/src/main.rs b/src/main.rs index 3f4098b4..1392581b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,80 +37,6 @@ macro_rules! ged { }; } -/* -/// TEMPORARY - REMOVE WHEN SPLITS ARE IMPLEMENTED -use crate::editor::{FileLayout, FileContainer}; -use kaolinite::{Document, Size}; -use synoptic::Highlighter; -fn main() { - let lua = Lua::new(); - // Create editor - let mut editor = match Editor::new(&lua) { - Ok(editor) => editor, - Err(error) => panic!("Editor failed to start: {error:?}"), - }; - - lua.load(PLUGIN_BOOTSTRAP).exec().unwrap(); - editor.load_config("/home/luke/.oxrc", &lua); - lua.load(PLUGIN_RUN).exec().unwrap(); - - editor.files = FileLayout::SideBySide(vec![ - ( - FileLayout::Atom(vec![ - FileContainer { - doc: Document::open(Size { w: 0, h: 0 }, "src/main.rs").unwrap(), - highlighter: synoptic::from_extension("rs", 4).unwrap(), - file_type: None, - } - ], 0), - 0.5, - ), - ( - FileLayout::TopToBottom(vec![ - ( - FileLayout::Atom(vec![ - FileContainer { - doc: Document::new(Size { w: 0, h: 0 }), - highlighter: Highlighter::new(4), - file_type: None, - } - ], 0), - 0.5, - ), - ( - FileLayout::Atom(vec![ - FileContainer { - doc: Document::open(Size { w: 0, h: 0 }, "plugins/todo.lua").unwrap(), - highlighter: synoptic::from_extension("lua", 4).unwrap(), - file_type: None, - } - ], 0), - 0.5, - ), - ]), - 0.5, - ), - ]); - editor.active = true; - editor.ptr = vec![1, 0]; - editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - editor.update_highlighter(); - editor.ptr = vec![1, 1]; - editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - editor.update_highlighter(); - editor.ptr = vec![0]; - editor.files.get_mut(editor.ptr.clone()).unwrap().doc.load_to(100); - editor.update_highlighter(); - - // editor.update_render_cache(&lua, Size { w: 100, h: 47 }); - // editor.render(&lua); - // editor.terminal.show_cursor(); - // editor.terminal.flush(); - // println!("{}", "\n".repeat(crossterm::terminal::size().unwrap().1.into())); - // println!("\n\n\n{:#?}", editor.render_cache.span); -} -*/ - /// Entry point - grabs command line arguments and runs the editor fn main() { // Interact with user to find out what they want to do From 97e65b97ba59287a67f038cbcc6c503956786669 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:01:24 +0000 Subject: [PATCH 28/29] Fixed issues with resizing --- src/editor/interface.rs | 40 +++++++++++++++++++++++++--------------- src/editor/mod.rs | 12 ++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 7f1ed6d7..c7f204e1 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -120,10 +120,11 @@ impl Editor { self.update_render_cache(lua, size); // Update all document's size let updates = self.files.update_doc_sizes(&self.render_cache.span, self); - for (ptr, doc, new_size) in updates { - self.files.get_atom_mut(ptr.clone()).unwrap().0[doc] - .doc - .size = new_size; + for (ptr, doc_idx, new_size) in updates { + let doc = &mut self.files.get_atom_mut(ptr.clone()).unwrap().0[doc_idx].doc; + doc.size = new_size; + doc.load_to(doc.offset.y + doc.size.h + 1); + self.update_highlighter_for(&ptr, doc_idx); } // Hide the cursor before rendering self.terminal.hide_cursor(); @@ -633,18 +634,22 @@ impl Editor { /// Append any missed lines to the syntax highlighter pub fn update_highlighter(&mut self) { + if let Some((_, doc_idx)) = self.files.get_atom(self.ptr.clone()) { + self.update_highlighter_for(&self.ptr.clone(), doc_idx); + } + } + + /// Update highlighter of a certain document + pub fn update_highlighter_for(&mut self, ptr: &[usize], doc: usize) { + let percieved = self.highlighter_for(ptr.to_owned(), doc).line_ref.len(); if self.active { - let actual = self - .files - .get(self.ptr.clone()) - .map_or(0, |fc| fc.doc.info.loaded_to); - let percieved = self.highlighter().line_ref.len(); - if percieved < actual { - let diff = actual.saturating_sub(percieved); - for i in 0..diff { - if let Some(file) = self.files.get_mut(self.ptr.clone()) { - let line = &file.doc.lines[percieved + i]; - file.highlighter.append(line); + if let Some((ref mut fcs, _)) = self.files.get_atom_mut(ptr.to_owned()) { + let actual = fcs[doc].doc.info.loaded_to; + if percieved < actual { + let diff = actual.saturating_sub(percieved); + for i in 0..diff { + let line = fcs[doc].doc.lines[percieved + i].clone(); + fcs[doc].highlighter.append(&line); } } } @@ -661,6 +666,11 @@ impl Editor { &mut self.files.get_mut(self.ptr.clone()).unwrap().highlighter } + /// Gets a mutable reference to the current document + pub fn highlighter_for(&self, ptr: Vec, doc: usize) -> &Highlighter { + &self.files.get_atom(ptr).unwrap().0[doc].highlighter + } + /// Reload the whole document in the highlighter pub fn reload_highlight(&mut self) { if let Some(file) = self.files.get_mut(self.ptr.clone()) { diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 8b0851c9..2dd337d5 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -415,7 +415,7 @@ impl Editor { // Pass event down to special handlers match event { CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, - CEvent::Resize(w, h) => self.handle_resize(w, h), + CEvent::Resize(_, _) => self.handle_resize(lua)?, CEvent::Mouse(mouse_event) => self.handle_mouse_event(lua, mouse_event), CEvent::Paste(text) => self.handle_paste(&text)?, _ => (), @@ -448,13 +448,9 @@ impl Editor { } /// Handle resize - pub fn handle_resize(&mut self, w: u16, h: u16) { - // Ensure all lines in viewport are loaded - let max = self.dent(); - self.doc_mut().size.w = w.saturating_sub(u16::try_from(max).unwrap_or(u16::MAX)) as usize; - self.doc_mut().size.h = h.saturating_sub(2) as usize; - let max = self.doc().offset.y + self.doc().size.h; - self.doc_mut().load_to(max + 1); + pub fn handle_resize(&mut self, lua: &Lua) -> Result<()> { + // Rerender the editor (that'll handle everything with the new size) + self.render(lua) } /// Handle paste From 4989c08be87cf682625c7b92b9c2cac1133e9bf5 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:10:35 +0000 Subject: [PATCH 29/29] Fixed mlua's userdataborrowmuterror issue when opening duplicate files from cli --- src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1392581b..3ea84fc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -347,9 +347,8 @@ fn handle_file_opening(editor: &AnyUserData, result: Result<()>, name: &str) { Ok(()) => (), Err(OxError::AlreadyOpen { .. }) => { let len = ged!(&editor).files.len().saturating_sub(1); - ged!(mut &editor) - .files - .move_to(ged!(&editor).ptr.clone(), len); + let current_ptr = ged!(&editor).ptr.clone(); + ged!(mut &editor).files.move_to(current_ptr, len); } Err(OxError::Kaolinite(kerr)) => { match kerr {