From 7370785f221496b578105fe25cfd999e8644a5c4 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:55:49 +0100 Subject: [PATCH 01/31] improved autoindentation plug-in readability and improved empty block handling --- plugins/autoindent.lua | 205 +++++++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 91 deletions(-) diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index 33a27ba9..b2954323 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.4 +Auto Indent v0.5 You will be able to press return at the start of a block and have Ox automatically indent for you. @@ -9,114 +9,105 @@ the character to the left of the cursor being an opening bracket or other syntax that indicates a block has started e.g. ":" in python ]]-- --- Automatic Indentation event_mapping["enter"] = function() - -- Get line the cursor was on - y = editor.cursor.y - 1 - line = editor:get_line_at(y) - -- Work out what the last character on the line was - sline = line:gsub("^%s*(.-)%s*$", "%1") - local function starts(prefix) - return sline:sub(1, #prefix) == prefix - end - local function ends(suffix) - return suffix == "" or sline:sub(-#suffix) == suffix + -- Indent where appropriate + if autoindent:causes_indent(editor.cursor.y - 1) then + editor:display_warning("yay") + local new_level = autoindent:get_indent(editor.cursor.y) + 1 + autoindent:set_indent(editor.cursor.y, new_level) end - -- Work out how indented the line was - indents = #(line:match("^\t+") or "") + #(line:match("^ +") or "") / document.tab_width - -- Account for common groups of block starting characters - is_bracket = ends("{") or ends("[") or ends("(") - if is_bracket then indents = indents + 1 end + -- Give newly created line a boost to match it up relatively with the line before it + local added_level = autoindent:get_indent(editor.cursor.y) + autoindent:get_indent(editor.cursor.y - 1) + autoindent:set_indent(editor.cursor.y, added_level) +end + +event_mapping["*"] = function() + -- Dedent where appropriate + if autoindent:causes_dedent(editor.cursor.y) then + local new_level = autoindent:get_indent(editor.cursor.y) - 1 + autoindent:set_indent(editor.cursor.y, new_level) + end +end + +autoindent = {} + +-- Determine if a line starts with a certain string +function autoindent:starts(y, starting) + local line = editor:get_line_at(y) + return line:match("^" .. starting) +end + +-- Determine if a line ends with a certain string +function autoindent:ends(y, ending) + local line = editor:get_line_at(y) + return line:match(ending .. "$") +end + +-- Determine if the line causes an indent when return is pressed on the end +function autoindent:causes_indent(y) + -- Always indent on open brackets + local is_bracket = self:ends(y, "%{") or self:ends(y, "%[") or self:ends(y, "%(") + if is_bracket then return true end -- Language specific additions if editor.document_type == "Python" then - if ends(":") then indents = indents + 1 end + if self:ends(y, ":") then return true end elseif editor.document_type == "Ruby" then - if ends("do") then indents = indents + 1 end + if self:ends(y, "do") then return true end elseif editor.document_type == "Lua" then - func = ends(")") and (starts("function") or starts("local function")) - if ends("else") or ends("do") or ends("then") or func then indents = indents + 1 end + local func = self:ends(y, "%)") and (self:starts(y, "function") or self:starts(y, "local function")) + if self:ends(y, "else") or self:ends(y, "do") or self:ends(y, "then") or func then return true end elseif editor.document_type == "Haskell" then - if ends("where") or ends("let") or ends("do") then indents = indents + 1 end + if self:ends(y, "where") or self:ends(y, "let") or self:ends(y, "do") then return true end elseif editor.document_type == "Shell" then - if ends("then") or ends("do") then indents = indents + 1 end - end - -- Indent the correct number of times - for i = 1, indents do - editor:insert("\t") - end - -- Handle the case where enter is pressed between two brackets - local last_char = string.sub(line, string.len(line), string.len(line)) - local current_char = editor:get_character() - local potential_pair = last_char .. current_char - local old_cursor = editor.cursor - if potential_pair == "{}" or potential_pair == "[]" or potential_pair == "()" then - editor:insert_line() - editor:move_to(old_cursor.x, old_cursor.y) + if self:ends(y, "then") or self:ends(y, "do") then return true end end + return false end --- Automatic Dedenting -local function do_dedent() - local current_line = editor:get_line() - if current_line:match("\t") ~= nil then - editor:insert_line_at(current_line:gsub("\t", "", 1), editor.cursor.y) - editor:remove_line_at(editor.cursor.y + 1) - else - editor:insert_line_at(current_line:gsub(string.rep(" ", document.tab_width), "", 1), editor.cursor.y) - editor:remove_line_at(editor.cursor.y + 1) - end -end - -event_mapping["*"] = function() - line = editor:get_line() - local function ends(suffix) - return line:match("^%s*" .. suffix .. "$") ~= nil - end +-- Determine if the line causes a dedent as soon as the pattern matches +function autoindent:causes_dedent(y) + -- Always dedent after closing brackets + local is_bracket = self:starts(y, "%s*%}") or self:starts(y, "%s*%]") or self:starts(y, "%s*%)") + if is_bracket then return true end + -- Check the line for token giveaways if editor.document_type == "Shell" then - if ends("fi") or ends("done") or ends("esac") or ends("}") or ends("elif") or ends("else") or ends(";;") then do_dedent() end + local end1 = self:starts(y, "%s*fi") or self:starts(y, "%s*done") or self:starts(y, "%s*esac") + local end2 = self:starts(y, "%s*elif") or self:starts(y, "%s*else") or self:starts(y, "%s*;;") + if end1 or end2 then return true end elseif editor.document_type == "Python" then - if ends("else") or ends("elif") or ends("except") or ends("finally") then do_dedent() end + local end1 = self:starts(y, "%s*else") or self:starts(y, "%s*elif") + local end2 = self:starts(y, "%s*except") or self:starts(y, "%s*finally") + if end1 or end2 then return true end elseif editor.document_type == "Ruby" then - if ends("end") or ends("else") or ends("elseif") or ends("ensure") or ends("rescue") or ends("when") or ends(";;") then do_dedent() end + local end1 = self:starts(y, "%s*end") or self:starts(y, "%s*else") or self:starts(y, "%s*elseif") + local end2 = self:starts(y, "%s*ensure") or self:starts(y, "%s*rescue") or self:starts(y, "%s*when") + if end1 or end2 or self:starts(y, "%s*;;") then return true end elseif editor.document_type == "Lua" then - if ends("end") or ends("else") or ends("elseif") or ends("until") then do_dedent() end + local end1 = self:starts(y, "%s*end") or self:starts(y, "%s*else") + local end2 = self:starts(y, "%s*elseif") or self:starts(y, "%s*until") + if end1 or end2 then return true end elseif editor.document_type == "Haskell" then - if ends("else") or ends("in") then do_dedent() end + if self:starts(y, "%s*else") or self:starts(y, "%s*in") then return true end end + return false end --- Utilties for when moving lines around -autoindent = {} - -function autoindent:fix_indent() - -- Check the indentation of the line above this one (and match the line the cursor is currently on) - local line_above = editor:get_line_at(editor.cursor.y - 1) - local indents_above = #(line_above:match("^\t+") or "") + #(line_above:match("^ +") or "") / document.tab_width - local line_below = editor:get_line_at(editor.cursor.y + 1) - local indents_below = #(line_below:match("^\t+") or "") + #(line_below:match("^ +") or "") / document.tab_width - local new_indent = nil - if editor.cursor.y == 1 then - -- Always remove all indent when on the first line - new_indent = 0 - elseif indents_below == indents_above then - new_indent = indents_below - elseif indents_below > indents_above then - new_indent = indents_below - else - new_indent = indents_above - end - -- Give a boost when entering empty blocks - if line_above:match("{%s*$") ~= nil and line_below:match("^%s*}") ~= nil then - new_indent = new_indent + 1; - end - -- Work out the contents of the new line - local line = editor:get_line() - local indents = #(line:match("^\t+") or "") + #(line:match("^ +") or "") / document.tab_width - local indent_change = new_indent - indents +-- Set an indent at a certain y index +function autoindent:set_indent(y, new_indent) + -- Handle awkward scenarios + if new_indent < 0 then return end + -- Find the indent of the line at the moment + local line = editor:get_line_at(y) + local current = autoindent:get_indent(y) + -- Work out how much to change and what to change + local indent_change = new_indent - current + local tabs = line:match("^\t") ~= nil + -- Prepare to form the new line contents local new_line = nil + -- Work out if adding or removing if indent_change > 0 then -- Insert indentation - if line:match("\t") ~= nil then + if tabs then -- Insert Tabs new_line = string.rep("\t", indent_change) .. line else @@ -125,7 +116,7 @@ function autoindent:fix_indent() end elseif indent_change < 0 then -- Remove indentation - if line:match("\t") ~= nil then + if tabs then -- Remove Tabs new_line = line:gsub("\t", "", -indent_change) else @@ -133,7 +124,39 @@ function autoindent:fix_indent() new_line = line:gsub(string.rep(" ", document.tab_width), "", -indent_change) end end - -- Perform replacement - editor:insert_line_at(new_line, editor.cursor.y) - editor:remove_line_at(editor.cursor.y + 1) + -- Perform the substitution with the new line + editor:insert_line_at(new_line, y) + editor:remove_line_at(y + 1) + -- Place the cursor at a sensible position + editor:move_end() +end + +-- Get how indented a line is at a certain y index +function autoindent:get_indent(y) + local line = editor:get_line_at(y) + return #(line:match("^\t+") or "") + #(line:match("^ +") or "") / document.tab_width +end + +-- Utilties for when moving lines around +function autoindent:fix_indent() + -- Check the indentation of the line above this one (and match the line the cursor is currently on) + local indents_above = autoindent:get_indent(editor.cursor.y - 1) + local indents_below = autoindent:get_indent(editor.cursor.y + 1) + local new_indent = nil + if editor.cursor.y == 1 then + -- Always remove all indent when on the first line + new_indent = 0 + elseif indents_below >= indents_above then + new_indent = indents_below + else + new_indent = indents_above + end + -- Give a boost when entering an empty block + local indenting_above = autoindent:causes_indent(editor.cursor.y - 1) + local dedenting_below = autoindent:causes_dedent(editor.cursor.y + 1) + if indenting_above and dedenting_below then + new_indent = new_indent + 1 + end + -- Set the indent + autoindent:set_indent(editor.cursor.y, new_indent) end From cc7fd3229858fa7f3fc4066e127525da2a852951 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:33:11 +0100 Subject: [PATCH 02/31] further improvements to autoindent, completing empty block cleanup and sensible cursor positions --- plugins/autoindent.lua | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index b2954323..cdbec970 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.5 +Auto Indent v0.6 You will be able to press return at the start of a block and have Ox automatically indent for you. @@ -12,13 +12,14 @@ other syntax that indicates a block has started e.g. ":" in python event_mapping["enter"] = function() -- Indent where appropriate if autoindent:causes_indent(editor.cursor.y - 1) then - editor:display_warning("yay") local new_level = autoindent:get_indent(editor.cursor.y) + 1 autoindent:set_indent(editor.cursor.y, new_level) end -- Give newly created line a boost to match it up relatively with the line before it local added_level = autoindent:get_indent(editor.cursor.y) + autoindent:get_indent(editor.cursor.y - 1) autoindent:set_indent(editor.cursor.y, added_level) + -- Handle the case where enter is pressed, creating a multi-line block that requires neatening up + autoindent:disperse_block() end event_mapping["*"] = function() @@ -105,30 +106,37 @@ function autoindent:set_indent(y, new_indent) -- Prepare to form the new line contents local new_line = nil -- Work out if adding or removing + local x = editor.cursor.x if indent_change > 0 then -- Insert indentation if tabs then -- Insert Tabs + x = x + indent_change new_line = string.rep("\t", indent_change) .. line else -- Insert Spaces + x = x + indent_change * document.tab_width new_line = string.rep(" ", indent_change * document.tab_width) .. line end elseif indent_change < 0 then -- Remove indentation if tabs then -- Remove Tabs + x = x - -indent_change new_line = line:gsub("\t", "", -indent_change) else -- Remove Spaces + x = x - -indent_change * document.tab_width new_line = line:gsub(string.rep(" ", document.tab_width), "", -indent_change) end + else + return end -- Perform the substitution with the new line editor:insert_line_at(new_line, y) editor:remove_line_at(y + 1) -- Place the cursor at a sensible position - editor:move_end() + editor:move_to(x, y) end -- Get how indented a line is at a certain y index @@ -160,3 +168,14 @@ function autoindent:fix_indent() -- Set the indent autoindent:set_indent(editor.cursor.y, new_indent) end + +-- Handle the case where the enter key is pressed between two brackets +function autoindent:disperse_block() + local indenting_above = autoindent:causes_indent(editor.cursor.y - 1) + local current_dedenting = autoindent:causes_dedent(editor.cursor.y) + if indenting_above and current_dedenting then + local old_cursor = editor.cursor + editor:insert_line() + editor:move_to(old_cursor.x, old_cursor.y) + end +end From 2e5e5d48864192a26f3464904d93875bc9ce6efb Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:22:39 +0100 Subject: [PATCH 03/31] added proper key remapping, allowing all space key bindings to use the word space --- src/plugin/run.lua | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/plugin/run.lua b/src/plugin/run.lua index 72289482..f225811b 100644 --- a/src/plugin/run.lua +++ b/src/plugin/run.lua @@ -17,23 +17,32 @@ for c, path in ipairs(plugins) do end merge_event_mapping() --- Remap ctrl_space to ctrl_ -local has_name = global_event_mapping["ctrl_space"] ~= nil -local has_char = global_event_mapping["ctrl_ "] ~= nil -if has_name then - if has_char then - -- Append name to char - for i = 1, #global_event_mapping["ctrl_space"] do - table.insert(global_event_mapping["ctrl_ "], global_event_mapping["ctrl_space"][i]) +-- Function to remap keys if necessary +function remap_keys(from, to) + local has_name = global_event_mapping[from] ~= nil + local has_char = global_event_mapping[to] ~= nil + if has_name then + if has_char then + -- Append name to char + for i = 1, #global_event_mapping[from] do + table.insert(global_event_mapping[to], global_event_mapping[from][i]) + end + global_event_mapping[from] = nil + else + -- Transfer name to char + global_event_mapping[to] = global_event_mapping[from] + global_event_mapping[from] = nil end - global_event_mapping["ctrl_space"] = nil - else - -- Transfer name to char - global_event_mapping["ctrl_ "] = global_event_mapping["ctrl_space"] - global_event_mapping["ctrl_space"] = nil end end +-- Remap space keys +remap_keys("space", " ") +remap_keys("ctrl_space", "ctrl_ ") +remap_keys("alt_space", "alt_ ") +remap_keys("ctrl_alt_space", "ctrl_alt_ ") + +-- Show warning if any plugins weren't able to be loaded if plugin_issues then print("Various plug-ins failed to load") print("You may download these plug-ins from the ox git repository (in the plugins folder)") From 7723102c9a28ae0b591fd6ab74e42baf90eee48d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:22:50 +0100 Subject: [PATCH 04/31] updated bracket plug-in to insert spaces between pairs evenly --- plugins/pairs.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/pairs.lua b/plugins/pairs.lua index 63ad9630..955bd1a5 100644 --- a/plugins/pairs.lua +++ b/plugins/pairs.lua @@ -1,5 +1,5 @@ --[[ -Bracket Pairs v0.2 +Bracket Pairs v0.3 This will automatically insert a closing bracket or quote when you type an opening one @@ -104,7 +104,6 @@ for i, str in ipairs(pairings) do end end --- Automatically delete pairs function includes(array, value) for _, v in ipairs(array) do if v == value then @@ -114,6 +113,7 @@ function includes(array, value) return false -- Value not found end +-- Automatically delete pairs event_mapping["backspace"] = function() local old_line = line_cache.line local potential_pair = string.sub(old_line, editor.cursor.x + 1, editor.cursor.x + 2) @@ -122,3 +122,14 @@ event_mapping["backspace"] = function() line_cache = { y = editor.cursor.y, line = editor:get_line() } end end + +-- Space out pairs when pressing space between pairs +event_mapping["space"] = function() + local first = editor:get_character_at(editor.cursor.x - 2, editor.cursor.y) + local second = editor:get_character_at(editor.cursor.x, editor.cursor.y) + local potential_pair = first .. second + if includes(pairings, potential_pair) then + editor:insert(" ") + editor:move_left() + end +end From 6c60b53abb039aec64c77c68da6614757cfd2709 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:29:21 +0100 Subject: [PATCH 05/31] fixed some bracket pairs not deleting under certain circumstances --- plugins/pairs.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/pairs.lua b/plugins/pairs.lua index 955bd1a5..9361829b 100644 --- a/plugins/pairs.lua +++ b/plugins/pairs.lua @@ -1,5 +1,5 @@ --[[ -Bracket Pairs v0.3 +Bracket Pairs v0.4 This will automatically insert a closing bracket or quote when you type an opening one @@ -91,6 +91,7 @@ for i, str in ipairs(pairings) do -- Undo it for them editor:remove_at(editor.cursor.x - 1, editor.cursor.y) just_paired = { x = nil, y = nil } + line_cache = { y = editor.cursor.y, line = editor:get_line() } end end event_mapping[start_pair] = function() From e1f7e8ba834af9c372c9706d9cf262d83981ae8c Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:50:16 +0100 Subject: [PATCH 06/31] improved error reporting, cleaned up code and added before event mappings --- .gitattributes | 1 + src/config.rs | 18 ++++++- src/editor.rs | 96 ++++++++++++++++------------------- src/main.rs | 124 ++++++++++++++++++++++++++------------------- src/plugin/run.lua | 4 ++ 5 files changed, 138 insertions(+), 105 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d393a8e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.oxrc linguist-language=Lua diff --git a/src/config.rs b/src/config.rs index 275460e9..a5366eca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,7 +32,7 @@ pub const PLUGIN_BOOTSTRAP: &str = include_str!("plugin/bootstrap.lua"); /// This contains the code for running the plugins pub const PLUGIN_RUN: &str = include_str!("plugin/run.lua"); -/// This contains the code for handling a key binding +/// This contains the code for running code after a key binding is pressed pub fn run_key(key: &str) -> String { format!( " @@ -48,6 +48,22 @@ pub fn run_key(key: &str) -> String { ) } +/// This contains the code for running code before a key binding is fully processed +pub fn run_key_before(key: &str) -> String { + format!( + " + globalevent = (global_event_mapping[\"before:*\"] or {{}}) + for _, f in ipairs(globalevent) do + f() + end + key = (global_event_mapping[\"before:{key}\"] or {{}}) + for _, f in ipairs(key) do + f() + end + " + ) +} + /// The struct that holds all the configuration information #[derive(Debug)] pub struct Config { diff --git a/src/editor.rs b/src/editor.rs index 13c7df94..207dd51d 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,8 +1,8 @@ -use crate::config::{key_to_string, Config}; +use crate::config::Config; use crate::error::{OxError, Result}; use crate::ui::{size, Feedback, Terminal}; use crossterm::{ - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, + event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}, style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, terminal::{Clear, ClearType as ClType}, }; @@ -232,61 +232,55 @@ impl Editor { Ok(()) } - /// Complete one cycle of the editor - /// This function will return a key press code if applicable - pub fn cycle(&mut self, lua: &Lua) -> Result> { - // Run the editor - self.render(&lua)?; - // Wait for an event - let event = read()?; + /// Handle event + pub fn handle_event(&mut self, event: CEvent) -> Result<()> { self.needs_rerender = match event { - CEvent::Mouse(event) => match event.kind { - crossterm::event::MouseEventKind::Moved => false, - _ => true, - }, + CEvent::Mouse(event) => event.kind != MouseEventKind::Moved, _ => true, }; match event { - CEvent::Key(key) => { - // Check period of inactivity - let end = Instant::now(); - let inactivity = end.duration_since(self.last_active).as_millis() as usize; - if inactivity > self.config.document.borrow().undo_period * 1000 { - self.doc_mut().commit(); - } - // Predict whether the user is currently pasting text (based on rapid activity) - self.paste_flag = inactivity < 5; - // Register this activity - self.last_active = Instant::now(); - // Editing - these key bindings can't be modified (only added to)! - match (key.modifiers, key.code) { - // Core key bindings (non-configurable behaviour) - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, - (KMod::NONE, KCode::Tab) => self.character('\t')?, - (KMod::NONE, KCode::Backspace) => self.backspace()?, - (KMod::NONE, KCode::Delete) => self.delete()?, - (KMod::NONE, KCode::Enter) => self.enter()?, - _ => (), - } - // Check user-defined key combinations (includes defaults if not modified) - return Ok(Some(key_to_string(key.modifiers, key.code))); - } - CEvent::Resize(w, h) => { - // Ensure all lines in viewport are loaded - let max = self.dent(); - self.doc_mut().size.w = w.saturating_sub(max as u16) as usize; - self.doc_mut().size.h = h.saturating_sub(3) as usize; - let max = self.doc().offset.x + self.doc().size.h; - self.doc_mut().load_to(max + 1); - } - CEvent::Mouse(mouse_event) => { - self.handle_mouse_event(mouse_event); - return Ok(None); - } + CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, + CEvent::Resize(w, h) => self.handle_resize(w, h), + CEvent::Mouse(mouse_event) => self.handle_mouse_event(mouse_event), _ => (), } - self.feedback = Feedback::None; - Ok(None) + Ok(()) + } + + /// Handle key event + pub fn handle_key_event(&mut self, modifiers: KMod, code: KCode) -> Result<()> { + // Check period of inactivity + let end = Instant::now(); + let inactivity = end.duration_since(self.last_active).as_millis() as usize; + if inactivity > self.config.document.borrow().undo_period * 1000 { + self.doc_mut().commit(); + } + // Predict whether the user is currently pasting text (based on rapid activity) + self.paste_flag = inactivity < 5; + // Register this activity + self.last_active = Instant::now(); + // Editing - these key bindings can't be modified (only added to)! + match (modifiers, code) { + // Core key bindings (non-configurable behaviour) + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, + (KMod::NONE, KCode::Tab) => self.character('\t')?, + (KMod::NONE, KCode::Backspace) => self.backspace()?, + (KMod::NONE, KCode::Delete) => self.delete()?, + (KMod::NONE, KCode::Enter) => self.enter()?, + _ => (), + } + // Check user-defined key combinations (includes defaults if not modified) + Ok(()) + } + + /// 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(max as u16) as usize; + self.doc_mut().size.h = h.saturating_sub(3) as usize; + let max = self.doc().offset.x + self.doc().size.h; + self.doc_mut().load_to(max + 1); } /// Append any missed lines to the syntax highlighter diff --git a/src/main.rs b/src/main.rs index db568443..039e34e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ mod error; mod ui; use cli::CommandLineInterface; -use config::{run_key, PLUGIN_BOOTSTRAP, PLUGIN_RUN}; +use config::{key_to_string, run_key, run_key_before, PLUGIN_BOOTSTRAP, PLUGIN_RUN}; +use crossterm::event::Event as CEvent; use editor::Editor; use error::Result; use kaolinite::event::Event; @@ -14,6 +15,7 @@ use mlua::Error::RuntimeError; use mlua::Lua; use std::cell::RefCell; use std::rc::Rc; +use std::result::Result as RResult; use ui::Feedback; fn main() { @@ -97,62 +99,42 @@ fn run(cli: CommandLineInterface) -> Result<()> { editor.borrow_mut().new_if_empty()?; // Run plug-ins - lua.load(PLUGIN_RUN).exec()?; + handle_lua_error(&editor, "", lua.load(PLUGIN_RUN).exec())?; // Run the editor and handle errors if applicable editor.borrow_mut().init()?; while editor.borrow().active { - let cycle = editor.borrow_mut().cycle(&lua); - match cycle { - Ok(Some(mut key)) => { - // Form the corresponding lua code to run and run it - let code = run_key(&key); - let result = lua.load(&code).exec(); - // Check the result - match result { - // Handle any runtime errors - Err(RuntimeError(msg)) => { - // Work out if the key has been bound - if msg.contains(&"key not bound") { - if key.contains(&"_") && key != "_" && !key.starts_with("shift") { - if key.ends_with(" ") { - key.pop(); - key = format!("{key}space"); - } - editor.borrow_mut().feedback = - Feedback::Error(format!("The key binding {key} is not set")); - } - } else { - let mut msg: String = msg.split("\n").next().unwrap_or("").to_string(); - // Work out the type of error and display an appropriate message - if msg.starts_with("[") { - msg = msg.split(":").skip(3).collect::>().join(":"); - msg = format!(" on line {msg}"); - } else { - msg = format!(": {msg}"); - } - // Send out the error - editor.borrow_mut().feedback = - Feedback::Error(format!("Lua error occured{msg}")); - } - } - Err(err) => { - editor.borrow_mut().feedback = - Feedback::Error(format!("Error occured: {err}")); - } - _ => (), - } - } - // Display error from editor cycle - Err(e) => editor.borrow_mut().feedback = Feedback::Error(e.to_string()), - _ => (), + // Render and wait for event + editor.borrow_mut().render(&lua)?; + let event = crossterm::event::read()?; + editor.borrow_mut().feedback = Feedback::None; + + // Handle plug-in before key press mappings + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key_before(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&editor, &key_str, result)?; } + + // Actually handle editor event (errors included) + editor.borrow_mut().handle_event(event.clone())?; + + // Handle plug-in after key press mappings (if no errors occured) + if let CEvent::Key(key) = event { + let key_str = key_to_string(key.modifiers, key.code); + let code = run_key(&key_str); + let result = lua.load(&code).exec(); + handle_lua_error(&editor, &key_str, result)?; + } + editor.borrow_mut().update_highlighter()?; editor.borrow_mut().greet = false; + // Check for any commands to run let command = editor.borrow().command.clone(); if let Some(command) = command { - run_editor_command(&editor, command, &lua) + run_editor_command(&editor, command, &lua)?; } editor.borrow_mut().command = None; } @@ -162,17 +144,53 @@ fn run(cli: CommandLineInterface) -> Result<()> { Ok(()) } -fn run_editor_command(editor: &Rc>, cmd: String, lua: &Lua) { +fn handle_lua_error( + editor: &Rc>, + key_str: &str, + error: RResult<(), mlua::Error>, +) -> Result<()> { + match error { + // All good + Ok(_) => (), + // Handle a runtime error + Err(RuntimeError(msg)) => { + let msg = msg.split('\n').nth(0).unwrap_or("No Message Text"); + if msg.ends_with("key not bound") { + // Key was not bound, issue a warning would be helpful + let key_str = key_str.replace(" ", "space"); + if key_str.contains(&"_") && key_str != "_" && !key_str.starts_with("shift") { + editor.borrow_mut().feedback = + Feedback::Warning(format!("The key {key_str} is not bound")); + } + } else if msg.ends_with("command not found") { + // Command was not found, issue an error + editor.borrow_mut().feedback = + Feedback::Error(format!("The command '{key_str}' is not defined")); + } else { + // Some other runtime error + editor.borrow_mut().feedback = Feedback::Error(format!("{msg}")); + } + } + // Other miscellaneous error + Err(err) => { + editor.borrow_mut().feedback = + Feedback::Error(format!("Failed to run Lua code: {err:?}")) + } + } + Ok(()) +} + +// Run a command in the editor +fn run_editor_command(editor: &Rc>, cmd: String, lua: &Lua) -> Result<()> { let cmd = cmd.replace("'", "\\'").to_string(); match cmd.split(' ').collect::>().as_slice() { [subcmd, arguments @ ..] => { let arguments = arguments.join("', '"); - let code = format!("commands['{subcmd}']({{'{arguments}'}})"); - if let Err(err) = lua.load(code).exec() { - let line = err.to_string().split("\n").next().unwrap_or("").to_string(); - editor.borrow_mut().feedback = Feedback::Error(line); - } + let code = + format!("(commands['{subcmd}'] or error('command not found'))({{'{arguments}'}})"); + handle_lua_error(editor, subcmd, lua.load(code).exec())?; } _ => (), } + Ok(()) } diff --git a/src/plugin/run.lua b/src/plugin/run.lua index f225811b..0abc331c 100644 --- a/src/plugin/run.lua +++ b/src/plugin/run.lua @@ -41,6 +41,10 @@ remap_keys("space", " ") remap_keys("ctrl_space", "ctrl_ ") remap_keys("alt_space", "alt_ ") remap_keys("ctrl_alt_space", "ctrl_alt_ ") +remap_keys("before:space", "before: ") +remap_keys("before:ctrl_space", "before:ctrl_ ") +remap_keys("before:alt_space", "before:alt_ ") +remap_keys("before:ctrl_alt_space", "before:ctrl_alt_ ") -- Show warning if any plugins weren't able to be loaded if plugin_issues then From e99596e8e5b178c4758775211df55ad982a95bbf Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 28 Sep 2024 20:46:10 +0100 Subject: [PATCH 07/31] broke up editor.rs into dedicated module --- src/editor.rs | 1081 --------------------------------------- src/editor/cursor.rs | 95 ++++ src/editor/editing.rs | 164 ++++++ src/editor/interface.rs | 355 +++++++++++++ src/editor/mod.rs | 342 +++++++++++++ src/editor/scanning.rs | 164 ++++++ src/main.rs | 4 +- 7 files changed, 1123 insertions(+), 1082 deletions(-) delete mode 100644 src/editor.rs create mode 100644 src/editor/cursor.rs create mode 100644 src/editor/editing.rs create mode 100644 src/editor/interface.rs create mode 100644 src/editor/mod.rs create mode 100644 src/editor/scanning.rs diff --git a/src/editor.rs b/src/editor.rs deleted file mode 100644 index 207dd51d..00000000 --- a/src/editor.rs +++ /dev/null @@ -1,1081 +0,0 @@ -use crate::config::Config; -use crate::error::{OxError, Result}; -use crate::ui::{size, Feedback, Terminal}; -use crossterm::{ - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}, - style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, - terminal::{Clear, ClearType as ClType}, -}; -use kaolinite::event::{Error as KError, Event, Status}; -use kaolinite::utils::{Loc, Size}; -use kaolinite::Document; -use mlua::Lua; -use std::io::{ErrorKind, Write}; -use std::time::Instant; -use synoptic::{trim, Highlighter, TokOpt}; - -mod mouse; - -/// For managing all editing and rendering of cactus -pub struct Editor { - /// Interface for writing to the terminal - pub terminal: Terminal, - /// Whether to rerender the editor on the next cycle - pub needs_rerender: bool, - /// Configuration information for the editor - pub config: Config, - /// Storage of all the documents opened in the editor - pub doc: Vec, - /// Syntax highlighting integration - pub highlighter: Vec, - /// Pointer to the document that is currently being edited - pub ptr: usize, - /// true if the editor is still running, false otherwise - pub active: bool, - /// true if the editor should show a greeting message on next render - pub greet: bool, - /// The feedback message to display below the status line - pub feedback: Feedback, - /// Will be some if there is an outstanding command to be run - pub command: Option, - /// Will store the last time the editor was interacted with (to track inactivity) - pub last_active: Instant, - /// Used for storing amount to push document down - push_down: usize, - /// Used to cache the location of the configuration file - pub config_path: String, - /// This is a handy place to figure out if the user is currently pasting something or not - pub paste_flag: bool, -} - -impl Editor { - /// Create a new instance of the editor - pub fn new(lua: &Lua) -> Result { - let config = Config::new(lua)?; - Ok(Self { - doc: vec![], - ptr: 0, - terminal: Terminal::new(config.terminal.clone()), - config, - active: true, - greet: false, - needs_rerender: true, - highlighter: vec![], - feedback: Feedback::None, - command: None, - last_active: Instant::now(), - push_down: 1, - config_path: "~/.oxrc".to_string(), - paste_flag: false, - }) - } - - /// Load the configuration values - pub fn load_config(&mut self, path: String, lua: &Lua) -> Result<()> { - self.config_path = path.clone(); - let result = self.config.read(path, lua); - // Display any warnings if the user configuration couldn't be found - if let Err(OxError::Config(msg)) = result { - if msg == "Not Found" { - let warn = "No configuration file found, using default configuration".to_string(); - self.feedback = Feedback::Warning(warn); - } - } else { - result? - }; - // Calculate the correct push down based on config - self.push_down = if self.config.tab_line.borrow().enabled { - 1 - } else { - 0 - }; - Ok(()) - } - - /// Function to create a new document - pub fn blank(&mut self) -> Result<()> { - let mut size = size()?; - size.h = size.h.saturating_sub(1 + self.push_down); - let mut doc = Document::new(size); - doc.set_tab_width(self.config.document.borrow().tab_width); - // Load all the lines within viewport into the document - doc.load_to(size.h); - // Update in the syntax highlighter - let mut highlighter = Highlighter::new(4); - highlighter.run(&doc.lines); - self.highlighter.push(highlighter); - // Add document to documents - self.doc.push(doc); - Ok(()) - } - - /// Create a new document and move to it - pub fn new_document(&mut self) -> Result<()> { - self.blank()?; - self.ptr = self.doc.len().saturating_sub(1); - Ok(()) - } - - /// Function to open a document into the editor - pub fn open(&mut self, file_name: String) -> Result<()> { - let mut size = size()?; - size.h = size.h.saturating_sub(1 + self.push_down); - let mut doc = Document::open(size, file_name.clone())?; - doc.set_tab_width(self.config.document.borrow().tab_width); - // Load all the lines within viewport into the document - doc.load_to(size.h); - // Update in the syntax highlighter - let mut ext = file_name.split('.').last().unwrap_or(""); - if ext == "oxrc" { - ext = "lua" - } - let mut highlighter = self - .config - .syntax_highlighting - .borrow() - .get_highlighter(&ext); - highlighter.run(&doc.lines); - self.highlighter.push(highlighter); - doc.undo_mgmt.saved(); - // Add document to documents - self.doc.push(doc); - Ok(()) - } - - /// Function to ask the user for a file to open - pub fn open_document(&mut self) -> Result<()> { - let path = self.prompt("File to open")?; - self.open(path)?; - self.ptr = self.doc.len().saturating_sub(1); - Ok(()) - } - - /// Function to try opening a document, and if it doesn't exist, create it - pub fn open_or_new(&mut self, file_name: String) -> Result<()> { - let file = self.open(file_name.clone()); - if let Err(OxError::Kaolinite(KError::Io(ref os))) = file { - if os.kind() == ErrorKind::NotFound { - self.blank()?; - let binding = file_name.clone(); - let ext = binding.split('.').last().unwrap_or(""); - self.doc.last_mut().unwrap().file_name = Some(file_name); - self.doc.last_mut().unwrap().modified = true; - let highlighter = self - .config - .syntax_highlighting - .borrow() - .get_highlighter(&ext); - *self.highlighter.last_mut().unwrap() = highlighter; - self.highlighter - .last_mut() - .unwrap() - .run(&self.doc.last_mut().unwrap().lines); - Ok(()) - } else { - file - } - } else { - file - } - } - - /// Returns a document at a certain index - pub fn get_doc(&mut self, idx: usize) -> &mut Document { - self.doc.get_mut(idx).unwrap() - } - - /// Gets a reference to the current document - pub fn doc(&self) -> &Document { - self.doc.get(self.ptr).unwrap() - } - - /// Gets a mutable reference to the current document - pub fn doc_mut(&mut self) -> &mut Document { - self.doc.get_mut(self.ptr).unwrap() - } - - /// Gets the number of documents currently open - pub fn doc_len(&mut self) -> usize { - self.doc.len() - } - - /// Returns a highlighter at a certain index - pub fn get_highlighter(&mut self, idx: usize) -> &mut Highlighter { - self.highlighter.get_mut(idx).unwrap() - } - - /// Gets a mutable reference to the current document - pub fn highlighter(&mut self) -> &mut Highlighter { - self.highlighter.get_mut(self.ptr).unwrap() - } - - /// Execute an edit event - pub fn exe(&mut self, ev: Event) -> Result<()> { - self.doc_mut().exe(ev)?; - // TODO: Check for change in event type and commit to undo/redo stack if present - Ok(()) - } - - /// Initialise the editor - pub fn init(&mut self) -> Result<()> { - self.terminal.start()?; - Ok(()) - } - - /// 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.doc.is_empty() { - self.blank()?; - self.greet = true && self.config.greeting_message.borrow().enabled; - } - Ok(()) - } - - /// Handle event - pub fn handle_event(&mut self, event: CEvent) -> Result<()> { - self.needs_rerender = match event { - CEvent::Mouse(event) => event.kind != MouseEventKind::Moved, - _ => true, - }; - match event { - CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, - CEvent::Resize(w, h) => self.handle_resize(w, h), - CEvent::Mouse(mouse_event) => self.handle_mouse_event(mouse_event), - _ => (), - } - Ok(()) - } - - /// Handle key event - pub fn handle_key_event(&mut self, modifiers: KMod, code: KCode) -> Result<()> { - // Check period of inactivity - let end = Instant::now(); - let inactivity = end.duration_since(self.last_active).as_millis() as usize; - if inactivity > self.config.document.borrow().undo_period * 1000 { - self.doc_mut().commit(); - } - // Predict whether the user is currently pasting text (based on rapid activity) - self.paste_flag = inactivity < 5; - // Register this activity - self.last_active = Instant::now(); - // Editing - these key bindings can't be modified (only added to)! - match (modifiers, code) { - // Core key bindings (non-configurable behaviour) - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, - (KMod::NONE, KCode::Tab) => self.character('\t')?, - (KMod::NONE, KCode::Backspace) => self.backspace()?, - (KMod::NONE, KCode::Delete) => self.delete()?, - (KMod::NONE, KCode::Enter) => self.enter()?, - _ => (), - } - // Check user-defined key combinations (includes defaults if not modified) - Ok(()) - } - - /// 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(max as u16) as usize; - self.doc_mut().size.h = h.saturating_sub(3) as usize; - let max = self.doc().offset.x + self.doc().size.h; - self.doc_mut().load_to(max + 1); - } - - /// Append any missed lines to the syntax highlighter - pub fn update_highlighter(&mut self) -> Result<()> { - if self.active { - let actual = self - .doc - .get(self.ptr) - .and_then(|d| Some(d.loaded_to)) - .unwrap_or(0); - let percieved = self.highlighter().line_ref.len(); - if percieved < actual { - let diff = actual.saturating_sub(percieved); - for i in 0..diff { - let line = &self.doc[self.ptr].lines[percieved + i]; - self.highlighter[self.ptr].append(line); - } - } - } - Ok(()) - } - - /// Render a single frame of the editor in it's current state - pub fn render(&mut self, lua: &Lua) -> Result<()> { - if !self.needs_rerender { - return Ok(()); - } - self.needs_rerender = false; - self.terminal.hide_cursor()?; - let Size { w, mut h } = size()?; - h = h.saturating_sub(1 + self.push_down); - // Update the width of the document in case of update - let max = self.dent(); - self.doc_mut().size.w = w.saturating_sub(max) as usize; - // Render the tab line - let tab_enabled = self.config.tab_line.borrow().enabled; - if tab_enabled { - self.render_tab_line(w)?; - } - // Run through each line of the terminal, rendering the correct line - self.render_document(w, h)?; - // Leave last line for status line - self.render_status_line(&lua, w, h)?; - // Render greeting or help message if applicable - if self.greet { - self.render_greeting(lua, w, h)?; - } else if self.config.help_message.borrow().enabled { - self.render_help_message(lua, w, h)?; - } - // Render 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() { - self.terminal.show_cursor()?; - self.terminal.goto(x + max, y + self.push_down)?; - } - self.terminal.flush()?; - Ok(()) - } - - /// Render the lines of the document - fn render_document(&mut self, _w: usize, h: usize) -> Result<()> { - for y in 0..(h as u16) { - self.terminal - .goto(0, y as usize + self.push_down as usize)?; - // Start background colour - write!( - self.terminal.stdout, - "{}", - Bg(self.config.colors.borrow().editor_bg.to_color()?) - )?; - write!( - self.terminal.stdout, - "{}", - Fg(self.config.colors.borrow().editor_fg.to_color()?) - )?; - // Write line number of document - if self.config.line_numbers.borrow().enabled { - let num = self.doc().line_number(y as usize + self.doc().offset.y); - let padding_left = " ".repeat(self.config.line_numbers.borrow().padding_left); - let padding_right = " ".repeat(self.config.line_numbers.borrow().padding_right); - write!( - self.terminal.stdout, - "{}{}{}{}{}│{}{}", - Bg(self.config.colors.borrow().line_number_bg.to_color()?), - Fg(self.config.colors.borrow().line_number_fg.to_color()?), - padding_left, - num, - padding_right, - Fg(self.config.colors.borrow().editor_fg.to_color()?), - Bg(self.config.colors.borrow().editor_bg.to_color()?), - )?; - } - write!(self.terminal.stdout, "{}", Clear(ClType::UntilNewLine))?; - // Render line if it exists - let idx = y as usize + self.doc().offset.y; - if let Some(line) = self.doc().line(idx) { - let tokens = self.highlighter().line(idx, &line); - let tokens = trim(&tokens, self.doc().offset.x); - let mut x_pos = self.doc().offset.x; - for token in tokens { - let text = match token { - TokOpt::Some(text, kind) => { - // Try to get the corresponding colour for this token - let colour = self.config.syntax_highlighting.borrow().get_theme(&kind); - match colour { - // Success, write token - Ok(col) => { - write!(self.terminal.stdout, "{}", Fg(col),)?; - } - // Failure, show error message and don't highlight this token - Err(err) => { - self.feedback = Feedback::Error(err.to_string()); - } - } - text - } - TokOpt::None(text) => text, - }; - for c in text.chars() { - let at_x = self.doc().character_idx(&Loc { y: idx, x: x_pos }); - let is_selected = self.doc().is_loc_selected(Loc { y: idx, x: at_x }); - if is_selected { - write!( - self.terminal.stdout, - "{}{}", - Bg(self.config.colors.borrow().selection_bg.to_color()?), - Fg(self.config.colors.borrow().selection_fg.to_color()?), - )?; - } else { - write!( - self.terminal.stdout, - "{}", - Bg(self.config.colors.borrow().editor_bg.to_color()?) - )?; - } - write!(self.terminal.stdout, "{c}")?; - x_pos += 1; - } - write!( - self.terminal.stdout, - "{}", - Fg(self.config.colors.borrow().editor_fg.to_color()?) - )?; - } - } - } - Ok(()) - } - - /// Render the tab line at the top of the document - fn render_tab_line(&mut self, w: usize) -> Result<()> { - self.terminal.goto(0 as usize, 0 as usize)?; - write!( - self.terminal.stdout, - "{}{}", - Fg(self.config.colors.borrow().tab_inactive_fg.to_color()?), - Bg(self.config.colors.borrow().tab_inactive_bg.to_color()?) - )?; - for (c, document) in self.doc.iter().enumerate() { - let document_header = self.config.tab_line.borrow().render(document); - if c == self.ptr { - // Representing the document we're currently looking at - write!( - self.terminal.stdout, - "{}{}{}{document_header}{}{}{}│", - Bg(self.config.colors.borrow().tab_active_bg.to_color()?), - Fg(self.config.colors.borrow().tab_active_fg.to_color()?), - SetAttribute(Attribute::Bold), - SetAttribute(Attribute::Reset), - Fg(self.config.colors.borrow().tab_inactive_fg.to_color()?), - Bg(self.config.colors.borrow().tab_inactive_bg.to_color()?), - )?; - } else { - // Other document that is currently open - write!(self.terminal.stdout, "{document_header}│")?; - } - } - write!(self.terminal.stdout, "{}", " ".to_string().repeat(w))?; - Ok(()) - } - - /// Render the status line at the bottom of the document - fn render_status_line(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { - self.terminal.goto(0, h + self.push_down)?; - write!( - self.terminal.stdout, - "{}{}{}{}{}{}{}", - Bg(self.config.colors.borrow().status_bg.to_color()?), - Fg(self.config.colors.borrow().status_fg.to_color()?), - SetAttribute(Attribute::Bold), - self.config.status_line.borrow().render(&self, &lua, w), - SetAttribute(Attribute::Reset), - Fg(self.config.colors.borrow().editor_fg.to_color()?), - Bg(self.config.colors.borrow().editor_bg.to_color()?), - )?; - Ok(()) - } - - /// Render the feedback line - fn render_feedback_line(&mut self, w: usize, h: usize) -> Result<()> { - self.terminal.goto(0, h + 2)?; - write!( - self.terminal.stdout, - "{}", - self.feedback.render(&self.config.colors.borrow(), w)?, - )?; - Ok(()) - } - - /// Render the greeting message - fn render_help_message(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { - let colors = self.config.colors.borrow(); - let message = self.config.help_message.borrow().render(lua, &colors)?; - for (c, line) in message.iter().enumerate().take(h.saturating_sub(h / 4)) { - self.terminal.goto(w.saturating_sub(30), h / 4 + c + 1)?; - write!(self.terminal.stdout, "{line}")?; - } - Ok(()) - } - - /// Render the help message - fn render_greeting(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { - let colors = self.config.colors.borrow(); - let greeting = self.config.greeting_message.borrow().render(lua, &colors)?; - let 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)?; - write!( - self.terminal.stdout, - "{}", - alinio::align::center(&line, w.saturating_sub(4)).unwrap_or_else(|| "".to_string()), - )?; - } - Ok(()) - } - - /// Display a prompt in the document - pub fn prompt>(&mut self, prompt: S) -> Result { - let prompt = prompt.into(); - let mut input = String::new(); - let mut done = false; - // Enter into a menu that asks for a prompt - while !done { - let h = size()?.h; - let w = size()?.w; - // Render prompt message - self.terminal.prepare_line(h)?; - write!( - self.terminal.stdout, - "{}", - Bg(self.config.colors.borrow().editor_bg.to_color()?) - )?; - write!( - self.terminal.stdout, - "{}: {}{}", - prompt, - input, - " ".to_string().repeat(w) - )?; - self.terminal.goto(prompt.len() + input.len() + 2, h)?; - self.terminal.flush()?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // Exit the menu when the enter key is pressed - (KMod::NONE, KCode::Enter) => done = true, - // Remove from the input string if the user presses backspace - (KMod::NONE, KCode::Backspace) => { - input.pop(); - } - // Add to the input string if the user presses a character - (KMod::NONE | KMod::SHIFT, KCode::Char(c)) => input.push(c), - _ => (), - } - } - } - // Return input string result - Ok(input) - } - - /// Work out how much to push the document to the right (to make way for line numbers) - fn dent(&self) -> usize { - if self.config.line_numbers.borrow().enabled { - let padding_left = self.config.line_numbers.borrow().padding_left; - let padding_right = self.config.line_numbers.borrow().padding_right; - self.doc().len_lines().to_string().len() + 1 + padding_left + padding_right - } else { - 0 - } - } - - /// Move to the next document opened in the editor - pub fn next(&mut self) { - if self.ptr + 1 < self.doc.len() { - self.ptr += 1; - } - } - - /// 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); - } - } - - /// Copy the selected text - pub fn copy(&mut self) -> Result<()> { - let selected_text = self.doc().selection_text(); - self.terminal.copy(&selected_text) - } - - /// Cut the selected text - pub fn cut(&mut self) -> Result<()> { - self.copy()?; - self.doc_mut().remove_selection(); - self.reload_highlight(); - Ok(()) - } - - /// Move the cursor up - pub fn select_up(&mut self) { - self.doc_mut().select_up(); - } - - /// Move the cursor down - pub fn select_down(&mut self) { - self.doc_mut().select_down(); - } - - /// Move the cursor left - pub fn select_left(&mut self) { - let status = self.doc_mut().select_left(); - // Cursor wrapping if cursor hits the start of the line - let wrapping = self.config.document.borrow().wrap_cursor; - if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { - self.doc_mut().select_up(); - self.doc_mut().select_end(); - } - } - - /// Move the cursor right - pub fn select_right(&mut self) { - let status = self.doc_mut().select_right(); - // Cursor wrapping if cursor hits the end of a line - let wrapping = self.config.document.borrow().wrap_cursor; - if status == Status::EndOfLine && wrapping { - self.doc_mut().select_down(); - self.doc_mut().select_home(); - } - } - - /// Select the whole document - pub fn select_all(&mut self) { - self.doc_mut().move_top(); - self.doc_mut().select_bottom(); - } - - /// Move the cursor up - pub fn up(&mut self) { - self.doc_mut().move_up(); - } - - /// Move the cursor down - pub fn down(&mut self) { - self.doc_mut().move_down(); - } - - /// Move the cursor left - pub fn left(&mut self) { - let status = self.doc_mut().move_left(); - // Cursor wrapping if cursor hits the start of the line - let wrapping = self.config.document.borrow().wrap_cursor; - if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { - self.doc_mut().move_up(); - self.doc_mut().move_end(); - } - } - - /// Move the cursor right - pub fn right(&mut self) { - let status = self.doc_mut().move_right(); - // Cursor wrapping if cursor hits the end of a line - let wrapping = self.config.document.borrow().wrap_cursor; - if status == Status::EndOfLine && wrapping { - self.doc_mut().move_down(); - self.doc_mut().move_home(); - } - } - - /// Move the cursor to the previous word in the line - pub fn prev_word(&mut self) { - let status = self.doc_mut().move_prev_word(); - let wrapping = self.config.document.borrow().wrap_cursor; - if status == Status::StartOfLine && wrapping { - self.doc_mut().move_up(); - self.doc_mut().move_end(); - } - } - - /// Move the cursor to the next word in the line - pub fn next_word(&mut self) { - let status = self.doc_mut().move_next_word(); - let wrapping = self.config.document.borrow().wrap_cursor; - if status == Status::EndOfLine && wrapping { - self.doc_mut().move_down(); - self.doc_mut().move_home(); - } - } - - /// Insert a character into the document, creating a new row if editing - /// on the last line of the document - pub fn character(&mut self, ch: char) -> Result<()> { - if !self.doc().is_selection_empty() { - self.doc_mut().remove_selection(); - self.reload_highlight(); - } - self.new_row()?; - // Handle the character insertion - if ch == '\n' { - self.enter()?; - } else { - let loc = self.doc().char_loc(); - self.exe(Event::Insert(loc, ch.to_string()))?; - self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); - } - // Commit to event stack (for undo/redo if the character is a space) - if ch == ' ' { - self.doc_mut().commit(); - } - Ok(()) - } - - /// Handle the return key - pub fn enter(&mut self) -> Result<()> { - // When the return key is pressed, we want to commit to the undo/redo stack - self.doc_mut().commit(); - // Perform the changes - if self.doc().loc().y != self.doc().len_lines() { - // Enter pressed in the start, middle or end of the line - let loc = self.doc().char_loc(); - self.exe(Event::SplitDown(loc))?; - let line = &self.doc[self.ptr].lines[loc.y + 1]; - self.highlighter[self.ptr].insert_line(loc.y + 1, line); - let line = &self.doc[self.ptr].lines[loc.y]; - self.highlighter[self.ptr].edit(loc.y, line); - } else { - // Enter pressed on the empty line at the bottom of the document - self.new_row()?; - } - Ok(()) - } - - /// Handle the backspace key - pub fn backspace(&mut self) -> Result<()> { - if !self.doc().is_selection_empty() { - self.doc_mut().commit(); - self.doc_mut().undo_mgmt.set_dirty(); - self.doc_mut().remove_selection(); - // Removing a selection is significant and worth an undo commit - self.reload_highlight(); - return Ok(()); - } - let mut c = self.doc().char_ptr; - let on_first_line = self.doc().loc().y == 0; - let out_of_range = self.doc().out_of_range(0, self.doc().loc().y).is_err(); - if c == 0 && !on_first_line && !out_of_range { - // Backspace was pressed on the start of the line, move line to the top - self.new_row()?; - let mut loc = self.doc().char_loc(); - self.highlighter().remove_line(loc.y); - loc.y = loc.y.saturating_sub(1); - loc.x = self.doc().line(loc.y).unwrap().chars().count(); - self.exe(Event::SpliceUp(loc))?; - let line = &self.doc[self.ptr].lines[loc.y]; - self.highlighter[self.ptr].edit(loc.y, line); - } else { - // Backspace was pressed in the middle of the line, delete the character - c = c.saturating_sub(1); - if let Some(line) = self.doc().line(self.doc().loc().y) { - if let Some(ch) = line.chars().nth(c) { - let loc = Loc { - x: c, - y: self.doc().loc().y, - }; - self.exe(Event::Delete(loc, ch.to_string()))?; - self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); - } - } - } - Ok(()) - } - - /// Delete the character in place - pub fn delete(&mut self) -> Result<()> { - let c = self.doc().char_ptr; - if let Some(line) = self.doc().line(self.doc().loc().y) { - if let Some(ch) = line.chars().nth(c) { - let loc = Loc { - x: c, - y: self.doc().loc().y, - }; - self.exe(Event::Delete(loc, ch.to_string()))?; - self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); - } - } - Ok(()) - } - - /// Insert a new row at the end of the document if the cursor is on it - fn new_row(&mut self) -> Result<()> { - if self.doc().loc().y == self.doc().len_lines() { - self.exe(Event::InsertLine(self.doc().loc().y, "".to_string()))?; - self.highlighter().append(&"".to_string()); - } - Ok(()) - } - - /// Delete the current line - pub fn delete_line(&mut self) -> Result<()> { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // Delete the line - if self.doc().loc().y < self.doc().len_lines() { - let y = self.doc().loc().y; - let line = self.doc().line(y).unwrap(); - self.exe(Event::DeleteLine(y, line))?; - self.highlighter().remove_line(y); - } - Ok(()) - } - - /// Use search feature - pub fn search(&mut self) -> Result<()> { - // Prompt for a search term - let target = self.prompt("Search")?; - let mut done = false; - let Size { w, h } = size()?; - // Jump to the next match after search term is provided - self.next_match(&target); - // Enter into search menu - while !done { - // Render just the document part - self.terminal.hide_cursor()?; - self.render_document(w, h.saturating_sub(2))?; - // Render custom status line with mode information - self.terminal.goto(0, h)?; - write!( - self.terminal.stdout, - "[<-]: Search previous | [->]: Search next" - )?; - self.terminal.flush()?; - // Move back to correct cursor position - if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { - let max = self.dent(); - self.terminal.goto(x + max, y + 1)?; - self.terminal.show_cursor()?; - } else { - self.terminal.hide_cursor()?; - } - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // On return or escape key, exit menu - (KMod::NONE, KCode::Enter | KCode::Esc) => done = true, - // On left key, move to the previous match in the document - (KMod::NONE, KCode::Left) => std::mem::drop(self.prev_match(&target)), - // On right key, move to the next match in the document - (KMod::NONE, KCode::Right) => std::mem::drop(self.next_match(&target)), - _ => (), - } - } - self.update_highlighter()?; - } - Ok(()) - } - - /// Move to the next match - pub fn next_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().next_match(target, 1)?; - self.doc_mut().move_to(&mtch.loc); - // Update highlighting - self.update_highlighter().ok()?; - Some(mtch.text) - } - - /// Move to the previous match - pub fn prev_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().prev_match(target)?; - self.doc_mut().move_to(&mtch.loc); - // Update highlighting - self.update_highlighter().ok()?; - Some(mtch.text) - } - - /// Use replace feature - pub fn replace(&mut self) -> Result<()> { - // Request replace information - let target = self.prompt("Replace")?; - let into = self.prompt("With")?; - let mut done = false; - let Size { w, h } = size()?; - // Jump to match - let mut mtch; - if let Some(m) = self.next_match(&target) { - // Automatically move to next match, keeping note of what that match is - mtch = m; - } else if let Some(m) = self.prev_match(&target) { - // Automatically move to previous match, keeping not of what that match is - // This happens if there are no matches further down the document, only above - mtch = m; - } else { - // Exit if there are no matches in the document - return Ok(()); - } - self.update_highlighter()?; - // Enter into the replace menu - while !done { - // Render just the document part - self.terminal.hide_cursor()?; - self.render_document(w, h.saturating_sub(2))?; - // Write custom status line for the replace mode - self.terminal.goto(0, h)?; - write!( - self.terminal.stdout, - "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All" - )?; - self.terminal.flush()?; - // Move back to correct cursor location - if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { - let max = self.dent(); - self.terminal.goto(x + max, y + 1)?; - self.terminal.show_cursor()?; - } else { - self.terminal.hide_cursor()?; - } - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // On escape key, exit - (KMod::NONE, KCode::Esc) => done = true, - // On right key, move to the previous match, keeping note of what that match is - (KMod::NONE, KCode::Left) => mtch = self.prev_match(&target).unwrap_or(mtch), - // On left key, move to the next match, keeping note of what that match is - (KMod::NONE, KCode::Right) => mtch = self.next_match(&target).unwrap_or(mtch), - // On return key, perform replacement - (KMod::NONE, KCode::Enter) => self.do_replace(&into, &mtch)?, - // On tab key, replace all instances within the document - (KMod::NONE, KCode::Tab) => self.do_replace_all(&target, &into), - _ => (), - } - } - // Update syntax highlighter if necessary - self.update_highlighter()?; - } - Ok(()) - } - - /// Replace an instance in a document - fn do_replace(&mut self, into: &str, text: &str) -> Result<()> { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // Do the replacement - let loc = self.doc().char_loc(); - self.doc_mut().replace(loc, text, into)?; - self.doc_mut().move_to(&loc); - // Update syntax highlighter - self.update_highlighter()?; - self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); - Ok(()) - } - - /// Replace all instances in a document - fn do_replace_all(&mut self, target: &str, into: &str) { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // Replace everything top to bottom - self.doc_mut().move_to(&Loc::at(0, 0)); - while let Some(mtch) = self.doc_mut().next_match(target, 1) { - drop(self.doc_mut().replace(mtch.loc, &mtch.text, into)); - drop(self.update_highlighter()); - self.highlighter[self.ptr].edit(mtch.loc.y, &self.doc[self.ptr].lines[mtch.loc.y]); - } - } - - /// Reload the whole document in the highlighter - fn reload_highlight(&mut self) { - self.highlighter[self.ptr].run(&self.doc[self.ptr].lines); - } - - /// Perform redo action - pub fn redo(&mut self) -> Result<()> { - let result = Ok(self.doc_mut().redo()?); - self.reload_highlight(); - result - } - - /// Perform undo action - pub fn undo(&mut self) -> Result<()> { - let result = Ok(self.doc_mut().undo()?); - self.reload_highlight(); - result - } - - /// save the document to the disk - pub fn save(&mut self) -> Result<()> { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // Perform the save - self.doc_mut().save()?; - // All done - self.feedback = Feedback::Info("Document saved successfully".to_string()); - Ok(()) - } - - /// save the document to the disk at a specified path - pub fn save_as(&mut self) -> Result<()> { - let file_name = self.prompt("Save as")?; - self.doc_mut().save_as(&file_name)?; - if self.doc().file_name.is_none() { - let ext = file_name.split('.').last().unwrap_or(""); - self.highlighter[self.ptr] = self - .config - .syntax_highlighting - .borrow() - .get_highlighter(&ext); - self.doc_mut().file_name = Some(file_name.clone()); - self.doc_mut().modified = false; - } - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // All done - self.feedback = Feedback::Info(format!("Document saved as {file_name} successfully")); - Ok(()) - } - - /// Save all the open documents to the disk - pub fn save_all(&mut self) -> Result<()> { - for doc in self.doc.iter_mut() { - doc.save()?; - // Commit events to event manager (for undo / redo) - doc.commit(); - } - self.feedback = Feedback::Info(format!("Saved all documents")); - Ok(()) - } - - /// Quit the editor - pub fn quit(&mut self) -> Result<()> { - self.active = !self.doc.is_empty(); - // 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().modified || self.confirm(msg)? { - self.doc.remove(self.ptr); - self.highlighter.remove(self.ptr); - self.prev(); - } - } - self.active = !self.doc.is_empty(); - Ok(()) - } - - /// Confirmation dialog - pub fn confirm(&mut self, msg: &str) -> Result { - let mut done = false; - let mut result = false; - // Enter into the confirmation menu - self.terminal.hide_cursor()?; - while !done { - let h = size()?.h; - let w = size()?.w; - // Render message - self.feedback = Feedback::Warning(msg.to_string()); - self.render_feedback_line(w, h)?; - self.terminal.flush()?; - // Handle events - if let CEvent::Key(key) = read()? { - match (key.modifiers, key.code) { - // Exit the menu when the enter key is pressed - (KMod::NONE, KCode::Esc) => { - done = true; - self.feedback = Feedback::None; - } - // Add to the input string if the user presses a character - (KMod::CONTROL, KCode::Char('q')) => { - done = true; - result = true; - self.feedback = Feedback::None; - } - _ => (), - } - } - } - self.terminal.show_cursor()?; - Ok(result) - } -} diff --git a/src/editor/cursor.rs b/src/editor/cursor.rs new file mode 100644 index 00000000..5fa5ac7c --- /dev/null +++ b/src/editor/cursor.rs @@ -0,0 +1,95 @@ +use kaolinite::event::Status; + +use super::Editor; + +impl Editor { + /// Move the cursor up + pub fn select_up(&mut self) { + self.doc_mut().select_up(); + } + + /// Move the cursor down + pub fn select_down(&mut self) { + self.doc_mut().select_down(); + } + + /// Move the cursor left + pub fn select_left(&mut self) { + let status = self.doc_mut().select_left(); + // Cursor wrapping if cursor hits the start of the line + let wrapping = self.config.document.borrow().wrap_cursor; + if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { + self.doc_mut().select_up(); + self.doc_mut().select_end(); + } + } + + /// Move the cursor right + pub fn select_right(&mut self) { + let status = self.doc_mut().select_right(); + // Cursor wrapping if cursor hits the end of a line + let wrapping = self.config.document.borrow().wrap_cursor; + if status == Status::EndOfLine && wrapping { + self.doc_mut().select_down(); + self.doc_mut().select_home(); + } + } + + /// Select the whole document + pub fn select_all(&mut self) { + self.doc_mut().move_top(); + self.doc_mut().select_bottom(); + } + + /// Move the cursor up + pub fn up(&mut self) { + self.doc_mut().move_up(); + } + + /// Move the cursor down + pub fn down(&mut self) { + self.doc_mut().move_down(); + } + + /// Move the cursor left + pub fn left(&mut self) { + let status = self.doc_mut().move_left(); + // Cursor wrapping if cursor hits the start of the line + let wrapping = self.config.document.borrow().wrap_cursor; + if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { + self.doc_mut().move_up(); + self.doc_mut().move_end(); + } + } + + /// Move the cursor right + pub fn right(&mut self) { + let status = self.doc_mut().move_right(); + // Cursor wrapping if cursor hits the end of a line + let wrapping = self.config.document.borrow().wrap_cursor; + if status == Status::EndOfLine && wrapping { + self.doc_mut().move_down(); + self.doc_mut().move_home(); + } + } + + /// Move the cursor to the previous word in the line + pub fn prev_word(&mut self) { + let status = self.doc_mut().move_prev_word(); + let wrapping = self.config.document.borrow().wrap_cursor; + if status == Status::StartOfLine && wrapping { + self.doc_mut().move_up(); + self.doc_mut().move_end(); + } + } + + /// Move the cursor to the next word in the line + pub fn next_word(&mut self) { + let status = self.doc_mut().move_next_word(); + let wrapping = self.config.document.borrow().wrap_cursor; + if status == Status::EndOfLine && wrapping { + self.doc_mut().move_down(); + self.doc_mut().move_home(); + } + } +} diff --git a/src/editor/editing.rs b/src/editor/editing.rs new file mode 100644 index 00000000..862ab44c --- /dev/null +++ b/src/editor/editing.rs @@ -0,0 +1,164 @@ +use crate::error::Result; +use kaolinite::event::Event; +use kaolinite::utils::Loc; + +use super::Editor; + +impl Editor { + /// Execute an edit event + pub fn exe(&mut self, ev: Event) -> Result<()> { + self.doc_mut().exe(ev)?; + // TODO: Check for change in event type and commit to undo/redo stack if present + Ok(()) + } + + /// Insert a character into the document, creating a new row if editing + /// on the last line of the document + pub fn character(&mut self, ch: char) -> Result<()> { + if !self.doc().is_selection_empty() { + self.doc_mut().remove_selection(); + self.reload_highlight(); + } + self.new_row()?; + // Handle the character insertion + if ch == '\n' { + self.enter()?; + } else { + let loc = self.doc().char_loc(); + self.exe(Event::Insert(loc, ch.to_string()))?; + self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); + } + // Commit to event stack (for undo/redo if the character is a space) + if ch == ' ' { + self.doc_mut().commit(); + } + Ok(()) + } + + /// Handle the return key + pub fn enter(&mut self) -> Result<()> { + // When the return key is pressed, we want to commit to the undo/redo stack + self.doc_mut().commit(); + // Perform the changes + if self.doc().loc().y != self.doc().len_lines() { + // Enter pressed in the start, middle or end of the line + let loc = self.doc().char_loc(); + self.exe(Event::SplitDown(loc))?; + let line = &self.doc[self.ptr].lines[loc.y + 1]; + self.highlighter[self.ptr].insert_line(loc.y + 1, line); + let line = &self.doc[self.ptr].lines[loc.y]; + self.highlighter[self.ptr].edit(loc.y, line); + } else { + // Enter pressed on the empty line at the bottom of the document + self.new_row()?; + } + Ok(()) + } + + /// Handle the backspace key + pub fn backspace(&mut self) -> Result<()> { + if !self.doc().is_selection_empty() { + self.doc_mut().commit(); + self.doc_mut().undo_mgmt.set_dirty(); + self.doc_mut().remove_selection(); + // Removing a selection is significant and worth an undo commit + self.reload_highlight(); + return Ok(()); + } + let mut c = self.doc().char_ptr; + let on_first_line = self.doc().loc().y == 0; + let out_of_range = self.doc().out_of_range(0, self.doc().loc().y).is_err(); + if c == 0 && !on_first_line && !out_of_range { + // Backspace was pressed on the start of the line, move line to the top + self.new_row()?; + let mut loc = self.doc().char_loc(); + self.highlighter().remove_line(loc.y); + loc.y = loc.y.saturating_sub(1); + loc.x = self.doc().line(loc.y).unwrap().chars().count(); + self.exe(Event::SpliceUp(loc))?; + let line = &self.doc[self.ptr].lines[loc.y]; + self.highlighter[self.ptr].edit(loc.y, line); + } else { + // Backspace was pressed in the middle of the line, delete the character + c = c.saturating_sub(1); + if let Some(line) = self.doc().line(self.doc().loc().y) { + if let Some(ch) = line.chars().nth(c) { + let loc = Loc { + x: c, + y: self.doc().loc().y, + }; + self.exe(Event::Delete(loc, ch.to_string()))?; + self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); + } + } + } + Ok(()) + } + + /// Delete the character in place + pub fn delete(&mut self) -> Result<()> { + let c = self.doc().char_ptr; + if let Some(line) = self.doc().line(self.doc().loc().y) { + if let Some(ch) = line.chars().nth(c) { + let loc = Loc { + x: c, + y: self.doc().loc().y, + }; + self.exe(Event::Delete(loc, ch.to_string()))?; + self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); + } + } + Ok(()) + } + + /// Insert a new row at the end of the document if the cursor is on it + fn new_row(&mut self) -> Result<()> { + if self.doc().loc().y == self.doc().len_lines() { + self.exe(Event::InsertLine(self.doc().loc().y, "".to_string()))?; + self.highlighter().append(&"".to_string()); + } + Ok(()) + } + + /// Delete the current line + pub fn delete_line(&mut self) -> Result<()> { + // Commit events to event manager (for undo / redo) + self.doc_mut().commit(); + // Delete the line + if self.doc().loc().y < self.doc().len_lines() { + let y = self.doc().loc().y; + let line = self.doc().line(y).unwrap(); + self.exe(Event::DeleteLine(y, line))?; + self.highlighter().remove_line(y); + } + Ok(()) + } + + /// Perform redo action + pub fn redo(&mut self) -> Result<()> { + let result = Ok(self.doc_mut().redo()?); + self.reload_highlight(); + result + } + + /// Perform undo action + pub fn undo(&mut self) -> Result<()> { + let result = Ok(self.doc_mut().undo()?); + self.reload_highlight(); + result + } + + /// Copy the selected text + pub fn copy(&mut self) -> Result<()> { + let selected_text = self.doc().selection_text(); + self.terminal.copy(&selected_text) + } + + /// Cut the selected text + pub fn cut(&mut self) -> Result<()> { + self.copy()?; + self.doc_mut().remove_selection(); + self.reload_highlight(); + Ok(()) + } +} diff --git a/src/editor/interface.rs b/src/editor/interface.rs new file mode 100644 index 00000000..d9dd0885 --- /dev/null +++ b/src/editor/interface.rs @@ -0,0 +1,355 @@ +use crate::error::Result; +use crate::ui::{size, Feedback}; +use crossterm::{ + event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, + style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, + terminal::{Clear, ClearType as ClType}, +}; +use kaolinite::utils::{Loc, Size}; +use mlua::Lua; +use std::io::Write; +use synoptic::{trim, Highlighter, TokOpt}; + +use super::Editor; + +impl Editor { + /// Render a single frame of the editor in it's current state + pub fn render(&mut self, lua: &Lua) -> Result<()> { + if !self.needs_rerender { + return Ok(()); + } + self.needs_rerender = false; + self.terminal.hide_cursor()?; + let Size { w, mut h } = size()?; + h = h.saturating_sub(1 + self.push_down); + // Update the width of the document in case of update + let max = self.dent(); + self.doc_mut().size.w = w.saturating_sub(max) as usize; + // Render the tab line + let tab_enabled = self.config.tab_line.borrow().enabled; + if tab_enabled { + self.render_tab_line(w)?; + } + // Run through each line of the terminal, rendering the correct line + self.render_document(w, h)?; + // Leave last line for status line + self.render_status_line(&lua, w, h)?; + // Render greeting or help message if applicable + if self.greet { + self.render_greeting(lua, w, h)?; + } else if self.config.help_message.borrow().enabled { + self.render_help_message(lua, w, h)?; + } + // Render 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() { + self.terminal.show_cursor()?; + self.terminal.goto(x + max, y + self.push_down)?; + } + self.terminal.flush()?; + Ok(()) + } + + /// Render the lines of the document + pub fn render_document(&mut self, _w: usize, h: usize) -> Result<()> { + for y in 0..(h as u16) { + self.terminal + .goto(0, y as usize + self.push_down as usize)?; + // Start background colour + write!( + self.terminal.stdout, + "{}", + Bg(self.config.colors.borrow().editor_bg.to_color()?) + )?; + write!( + self.terminal.stdout, + "{}", + Fg(self.config.colors.borrow().editor_fg.to_color()?) + )?; + // Write line number of document + if self.config.line_numbers.borrow().enabled { + let num = self.doc().line_number(y as usize + self.doc().offset.y); + let padding_left = " ".repeat(self.config.line_numbers.borrow().padding_left); + let padding_right = " ".repeat(self.config.line_numbers.borrow().padding_right); + write!( + self.terminal.stdout, + "{}{}{}{}{}│{}{}", + Bg(self.config.colors.borrow().line_number_bg.to_color()?), + Fg(self.config.colors.borrow().line_number_fg.to_color()?), + padding_left, + num, + padding_right, + Fg(self.config.colors.borrow().editor_fg.to_color()?), + Bg(self.config.colors.borrow().editor_bg.to_color()?), + )?; + } + write!(self.terminal.stdout, "{}", Clear(ClType::UntilNewLine))?; + // Render line if it exists + let idx = y as usize + self.doc().offset.y; + if let Some(line) = self.doc().line(idx) { + let tokens = self.highlighter().line(idx, &line); + let tokens = trim(&tokens, self.doc().offset.x); + let mut x_pos = self.doc().offset.x; + for token in tokens { + let text = match token { + TokOpt::Some(text, kind) => { + // Try to get the corresponding colour for this token + let colour = self.config.syntax_highlighting.borrow().get_theme(&kind); + match colour { + // Success, write token + Ok(col) => { + write!(self.terminal.stdout, "{}", Fg(col),)?; + } + // Failure, show error message and don't highlight this token + Err(err) => { + self.feedback = Feedback::Error(err.to_string()); + } + } + text + } + TokOpt::None(text) => text, + }; + for c in text.chars() { + let at_x = self.doc().character_idx(&Loc { y: idx, x: x_pos }); + let is_selected = self.doc().is_loc_selected(Loc { y: idx, x: at_x }); + if is_selected { + write!( + self.terminal.stdout, + "{}{}", + Bg(self.config.colors.borrow().selection_bg.to_color()?), + Fg(self.config.colors.borrow().selection_fg.to_color()?), + )?; + } else { + write!( + self.terminal.stdout, + "{}", + Bg(self.config.colors.borrow().editor_bg.to_color()?) + )?; + } + write!(self.terminal.stdout, "{c}")?; + x_pos += 1; + } + write!( + self.terminal.stdout, + "{}", + Fg(self.config.colors.borrow().editor_fg.to_color()?) + )?; + } + } + } + Ok(()) + } + + /// Render the tab line at the top of the document + pub fn render_tab_line(&mut self, w: usize) -> Result<()> { + self.terminal.goto(0 as usize, 0 as usize)?; + write!( + self.terminal.stdout, + "{}{}", + Fg(self.config.colors.borrow().tab_inactive_fg.to_color()?), + Bg(self.config.colors.borrow().tab_inactive_bg.to_color()?) + )?; + for (c, document) in self.doc.iter().enumerate() { + let document_header = self.config.tab_line.borrow().render(document); + if c == self.ptr { + // Representing the document we're currently looking at + write!( + self.terminal.stdout, + "{}{}{}{document_header}{}{}{}│", + Bg(self.config.colors.borrow().tab_active_bg.to_color()?), + Fg(self.config.colors.borrow().tab_active_fg.to_color()?), + SetAttribute(Attribute::Bold), + SetAttribute(Attribute::Reset), + Fg(self.config.colors.borrow().tab_inactive_fg.to_color()?), + Bg(self.config.colors.borrow().tab_inactive_bg.to_color()?), + )?; + } else { + // Other document that is currently open + write!(self.terminal.stdout, "{document_header}│")?; + } + } + write!(self.terminal.stdout, "{}", " ".to_string().repeat(w))?; + Ok(()) + } + + /// Render the status line at the bottom of the document + pub fn render_status_line(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { + self.terminal.goto(0, h + self.push_down)?; + write!( + self.terminal.stdout, + "{}{}{}{}{}{}{}", + Bg(self.config.colors.borrow().status_bg.to_color()?), + Fg(self.config.colors.borrow().status_fg.to_color()?), + SetAttribute(Attribute::Bold), + self.config.status_line.borrow().render(&self, &lua, w), + SetAttribute(Attribute::Reset), + Fg(self.config.colors.borrow().editor_fg.to_color()?), + Bg(self.config.colors.borrow().editor_bg.to_color()?), + )?; + Ok(()) + } + + /// Render the feedback line + pub fn render_feedback_line(&mut self, w: usize, h: usize) -> Result<()> { + self.terminal.goto(0, h + 2)?; + write!( + self.terminal.stdout, + "{}", + self.feedback.render(&self.config.colors.borrow(), w)?, + )?; + Ok(()) + } + + /// Render the greeting message + fn render_help_message(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { + let colors = self.config.colors.borrow(); + let message = self.config.help_message.borrow().render(lua, &colors)?; + for (c, line) in message.iter().enumerate().take(h.saturating_sub(h / 4)) { + self.terminal.goto(w.saturating_sub(30), h / 4 + c + 1)?; + write!(self.terminal.stdout, "{line}")?; + } + Ok(()) + } + + /// Render the help message + fn render_greeting(&mut self, lua: &Lua, w: usize, h: usize) -> Result<()> { + let colors = self.config.colors.borrow(); + let greeting = self.config.greeting_message.borrow().render(lua, &colors)?; + let 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)?; + write!( + self.terminal.stdout, + "{}", + alinio::align::center(&line, w.saturating_sub(4)).unwrap_or_else(|| "".to_string()), + )?; + } + Ok(()) + } + + /// Display a prompt in the document + pub fn prompt>(&mut self, prompt: S) -> Result { + let prompt = prompt.into(); + let mut input = String::new(); + let mut done = false; + // Enter into a menu that asks for a prompt + while !done { + let h = size()?.h; + let w = size()?.w; + // Render prompt message + self.terminal.prepare_line(h)?; + write!( + self.terminal.stdout, + "{}", + Bg(self.config.colors.borrow().editor_bg.to_color()?) + )?; + write!( + self.terminal.stdout, + "{}: {}{}", + prompt, + input, + " ".to_string().repeat(w) + )?; + self.terminal.goto(prompt.len() + input.len() + 2, h)?; + self.terminal.flush()?; + // Handle events + if let CEvent::Key(key) = read()? { + match (key.modifiers, key.code) { + // Exit the menu when the enter key is pressed + (KMod::NONE, KCode::Enter) => done = true, + // Remove from the input string if the user presses backspace + (KMod::NONE, KCode::Backspace) => { + input.pop(); + } + // Add to the input string if the user presses a character + (KMod::NONE | KMod::SHIFT, KCode::Char(c)) => input.push(c), + _ => (), + } + } + } + // Return input string result + Ok(input) + } + + /// Confirmation dialog + pub fn confirm(&mut self, msg: &str) -> Result { + let mut done = false; + let mut result = false; + // Enter into the confirmation menu + self.terminal.hide_cursor()?; + while !done { + let h = size()?.h; + let w = size()?.w; + // Render message + self.feedback = Feedback::Warning(msg.to_string()); + self.render_feedback_line(w, h)?; + self.terminal.flush()?; + // Handle events + if let CEvent::Key(key) = read()? { + match (key.modifiers, key.code) { + // Exit the menu when the enter key is pressed + (KMod::NONE, KCode::Esc) => { + done = true; + self.feedback = Feedback::None; + } + // Add to the input string if the user presses a character + (KMod::CONTROL, KCode::Char('q')) => { + done = true; + result = true; + self.feedback = Feedback::None; + } + _ => (), + } + } + } + self.terminal.show_cursor()?; + Ok(result) + } + + /// Append any missed lines to the syntax highlighter + pub fn update_highlighter(&mut self) -> Result<()> { + if self.active { + let actual = self + .doc + .get(self.ptr) + .and_then(|d| Some(d.loaded_to)) + .unwrap_or(0); + let percieved = self.highlighter().line_ref.len(); + if percieved < actual { + let diff = actual.saturating_sub(percieved); + for i in 0..diff { + let line = &self.doc[self.ptr].lines[percieved + i]; + self.highlighter[self.ptr].append(line); + } + } + } + Ok(()) + } + + /// Returns a highlighter at a certain index + pub fn get_highlighter(&mut self, idx: usize) -> &mut Highlighter { + self.highlighter.get_mut(idx).unwrap() + } + + /// Gets a mutable reference to the current document + pub fn highlighter(&mut self) -> &mut Highlighter { + self.highlighter.get_mut(self.ptr).unwrap() + } + + /// Reload the whole document in the highlighter + pub fn reload_highlight(&mut self) { + self.highlighter[self.ptr].run(&self.doc[self.ptr].lines); + } + + /// Work out how much to push the document to the right (to make way for line numbers) + pub fn dent(&self) -> usize { + if self.config.line_numbers.borrow().enabled { + let padding_left = self.config.line_numbers.borrow().padding_left; + let padding_right = self.config.line_numbers.borrow().padding_right; + self.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 new file mode 100644 index 00000000..157ac0d7 --- /dev/null +++ b/src/editor/mod.rs @@ -0,0 +1,342 @@ +use crate::config::Config; +use crate::error::{OxError, Result}; +use crate::ui::{size, Feedback, Terminal}; +use crossterm::{ + event::{Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}, +}; +use kaolinite::event::Error as KError; +use kaolinite::Document; +use mlua::Lua; +use std::io::ErrorKind; +use std::time::Instant; +use synoptic::Highlighter; + +mod cursor; +mod mouse; +mod interface; +mod editing; +mod scanning; + +/// For managing all editing and rendering of cactus +pub struct Editor { + /// Interface for writing to the terminal + pub terminal: Terminal, + /// Whether to rerender the editor on the next cycle + pub needs_rerender: bool, + /// Configuration information for the editor + pub config: Config, + /// Storage of all the documents opened in the editor + pub doc: Vec, + /// Syntax highlighting integration + pub highlighter: Vec, + /// Pointer to the document that is currently being edited + pub ptr: usize, + /// true if the editor is still running, false otherwise + pub active: bool, + /// true if the editor should show a greeting message on next render + pub greet: bool, + /// The feedback message to display below the status line + pub feedback: Feedback, + /// Will be some if there is an outstanding command to be run + pub command: Option, + /// Will store the last time the editor was interacted with (to track inactivity) + pub last_active: Instant, + /// Used for storing amount to push document down + push_down: usize, + /// Used to cache the location of the configuration file + pub config_path: String, + /// This is a handy place to figure out if the user is currently pasting something or not + pub paste_flag: bool, +} + +impl Editor { + /// Create a new instance of the editor + pub fn new(lua: &Lua) -> Result { + let config = Config::new(lua)?; + Ok(Self { + doc: vec![], + ptr: 0, + terminal: Terminal::new(config.terminal.clone()), + config, + active: true, + greet: false, + needs_rerender: true, + highlighter: vec![], + feedback: Feedback::None, + command: None, + last_active: Instant::now(), + push_down: 1, + config_path: "~/.oxrc".to_string(), + paste_flag: false, + }) + } + + /// Initialise the editor + pub fn init(&mut self) -> Result<()> { + self.terminal.start()?; + Ok(()) + } + + /// Function to create a new document (without moving to it) + pub fn blank(&mut self) -> Result<()> { + let mut size = size()?; + size.h = size.h.saturating_sub(1 + self.push_down); + let mut doc = Document::new(size); + doc.set_tab_width(self.config.document.borrow().tab_width); + // Load all the lines within viewport into the document + doc.load_to(size.h); + // Update in the syntax highlighter + let mut highlighter = Highlighter::new(4); + highlighter.run(&doc.lines); + self.highlighter.push(highlighter); + // Add document to documents + self.doc.push(doc); + Ok(()) + } + + /// Create a new document and move to it + pub fn new_document(&mut self) -> Result<()> { + self.blank()?; + self.ptr = self.doc.len().saturating_sub(1); + Ok(()) + } + + /// 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.doc.is_empty() { + self.blank()?; + self.greet = true && self.config.greeting_message.borrow().enabled; + } + Ok(()) + } + + /// Function to open a document into the editor + pub fn open(&mut self, file_name: String) -> Result<()> { + let mut size = size()?; + size.h = size.h.saturating_sub(1 + self.push_down); + let mut doc = Document::open(size, file_name.clone())?; + doc.set_tab_width(self.config.document.borrow().tab_width); + // Load all the lines within viewport into the document + doc.load_to(size.h); + // Update in the syntax highlighter + let mut ext = file_name.split('.').last().unwrap_or(""); + if ext == "oxrc" { + ext = "lua" + } + let mut highlighter = self + .config + .syntax_highlighting + .borrow() + .get_highlighter(&ext); + highlighter.run(&doc.lines); + self.highlighter.push(highlighter); + doc.undo_mgmt.saved(); + // Add document to documents + self.doc.push(doc); + Ok(()) + } + + /// Function to ask the user for a file to open + pub fn open_document(&mut self) -> Result<()> { + let path = self.prompt("File to open")?; + self.open(path)?; + self.ptr = self.doc.len().saturating_sub(1); + Ok(()) + } + + /// Function to try opening a document, and if it doesn't exist, create it + pub fn open_or_new(&mut self, file_name: String) -> Result<()> { + let file = self.open(file_name.clone()); + if let Err(OxError::Kaolinite(KError::Io(ref os))) = file { + if os.kind() == ErrorKind::NotFound { + self.blank()?; + let binding = file_name.clone(); + let ext = binding.split('.').last().unwrap_or(""); + self.doc.last_mut().unwrap().file_name = Some(file_name); + self.doc.last_mut().unwrap().modified = true; + let highlighter = self + .config + .syntax_highlighting + .borrow() + .get_highlighter(&ext); + *self.highlighter.last_mut().unwrap() = highlighter; + self.highlighter + .last_mut() + .unwrap() + .run(&self.doc.last_mut().unwrap().lines); + Ok(()) + } else { + file + } + } else { + file + } + } + + /// save the document to the disk + pub fn save(&mut self) -> Result<()> { + // Commit events to event manager (for undo / redo) + self.doc_mut().commit(); + // Perform the save + self.doc_mut().save()?; + // All done + self.feedback = Feedback::Info("Document saved successfully".to_string()); + Ok(()) + } + + /// save the document to the disk at a specified path + pub fn save_as(&mut self) -> Result<()> { + let file_name = self.prompt("Save as")?; + self.doc_mut().save_as(&file_name)?; + if self.doc().file_name.is_none() { + let ext = file_name.split('.').last().unwrap_or(""); + self.highlighter[self.ptr] = self + .config + .syntax_highlighting + .borrow() + .get_highlighter(&ext); + self.doc_mut().file_name = Some(file_name.clone()); + self.doc_mut().modified = false; + } + // Commit events to event manager (for undo / redo) + self.doc_mut().commit(); + // All done + self.feedback = Feedback::Info(format!("Document saved as {file_name} successfully")); + Ok(()) + } + + /// Save all the open documents to the disk + pub fn save_all(&mut self) -> Result<()> { + for doc in self.doc.iter_mut() { + doc.save()?; + // Commit events to event manager (for undo / redo) + doc.commit(); + } + self.feedback = Feedback::Info(format!("Saved all documents")); + Ok(()) + } + + /// Quit the editor + pub fn quit(&mut self) -> Result<()> { + self.active = !self.doc.is_empty(); + // 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().modified || self.confirm(msg)? { + self.doc.remove(self.ptr); + self.highlighter.remove(self.ptr); + self.prev(); + } + } + self.active = !self.doc.is_empty(); + Ok(()) + } + + /// Move to the next document opened in the editor + pub fn next(&mut self) { + if self.ptr + 1 < self.doc.len() { + self.ptr += 1; + } + } + + /// 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); + } + } + + /// Returns a document at a certain index + pub fn get_doc(&mut self, idx: usize) -> &mut Document { + self.doc.get_mut(idx).unwrap() + } + + /// Gets a reference to the current document + pub fn doc(&self) -> &Document { + self.doc.get(self.ptr).unwrap() + } + + /// Gets a mutable reference to the current document + pub fn doc_mut(&mut self) -> &mut Document { + self.doc.get_mut(self.ptr).unwrap() + } + + /// Gets the number of documents currently open + pub fn doc_len(&mut self) -> usize { + self.doc.len() + } + + /// Load the configuration values + pub fn load_config(&mut self, path: String, lua: &Lua) -> Result<()> { + self.config_path = path.clone(); + let result = self.config.read(path, lua); + // Display any warnings if the user configuration couldn't be found + if let Err(OxError::Config(msg)) = result { + if msg == "Not Found" { + let warn = "No configuration file found, using default configuration".to_string(); + self.feedback = Feedback::Warning(warn); + } + } else { + result? + }; + // Calculate the correct push down based on config + self.push_down = if self.config.tab_line.borrow().enabled { + 1 + } else { + 0 + }; + Ok(()) + } + + /// Handle event + pub fn handle_event(&mut self, event: CEvent) -> Result<()> { + self.needs_rerender = match event { + CEvent::Mouse(event) => event.kind != MouseEventKind::Moved, + _ => true, + }; + match event { + CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, + CEvent::Resize(w, h) => self.handle_resize(w, h), + CEvent::Mouse(mouse_event) => self.handle_mouse_event(mouse_event), + _ => (), + } + Ok(()) + } + + /// Handle key event + pub fn handle_key_event(&mut self, modifiers: KMod, code: KCode) -> Result<()> { + // Check period of inactivity + let end = Instant::now(); + let inactivity = end.duration_since(self.last_active).as_millis() as usize; + if inactivity > self.config.document.borrow().undo_period * 1000 { + self.doc_mut().commit(); + } + // Predict whether the user is currently pasting text (based on rapid activity) + self.paste_flag = inactivity < 5; + // Register this activity + self.last_active = Instant::now(); + // Editing - these key bindings can't be modified (only added to)! + match (modifiers, code) { + // Core key bindings (non-configurable behaviour) + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, + (KMod::NONE, KCode::Tab) => self.character('\t')?, + (KMod::NONE, KCode::Backspace) => self.backspace()?, + (KMod::NONE, KCode::Delete) => self.delete()?, + (KMod::NONE, KCode::Enter) => self.enter()?, + _ => (), + } + // Check user-defined key combinations (includes defaults if not modified) + Ok(()) + } + + /// 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(max as u16) as usize; + self.doc_mut().size.h = h.saturating_sub(3) as usize; + let max = self.doc().offset.x + self.doc().size.h; + self.doc_mut().load_to(max + 1); + } +} diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs new file mode 100644 index 00000000..8b8fbaec --- /dev/null +++ b/src/editor/scanning.rs @@ -0,0 +1,164 @@ +use crate::error::Result; +use crate::ui::size; +use crossterm::{ + event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, +}; +use kaolinite::utils::{Loc, Size}; +use std::io::Write; + +use super::Editor; + +impl Editor { + /// Use search feature + pub fn search(&mut self) -> Result<()> { + // Prompt for a search term + let target = self.prompt("Search")?; + let mut done = false; + let Size { w, h } = size()?; + // Jump to the next match after search term is provided + self.next_match(&target); + // Enter into search menu + while !done { + // Render just the document part + self.terminal.hide_cursor()?; + self.render_document(w, h.saturating_sub(2))?; + // Render custom status line with mode information + self.terminal.goto(0, h)?; + write!( + self.terminal.stdout, + "[<-]: Search previous | [->]: Search next" + )?; + self.terminal.flush()?; + // Move back to correct cursor position + if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { + let max = self.dent(); + self.terminal.goto(x + max, y + 1)?; + self.terminal.show_cursor()?; + } else { + self.terminal.hide_cursor()?; + } + // Handle events + if let CEvent::Key(key) = read()? { + match (key.modifiers, key.code) { + // On return or escape key, exit menu + (KMod::NONE, KCode::Enter | KCode::Esc) => done = true, + // On left key, move to the previous match in the document + (KMod::NONE, KCode::Left) => std::mem::drop(self.prev_match(&target)), + // On right key, move to the next match in the document + (KMod::NONE, KCode::Right) => std::mem::drop(self.next_match(&target)), + _ => (), + } + } + self.update_highlighter()?; + } + Ok(()) + } + + /// Move to the next match + pub fn next_match(&mut self, target: &str) -> Option { + let mtch = self.doc_mut().next_match(target, 1)?; + self.doc_mut().move_to(&mtch.loc); + // Update highlighting + self.update_highlighter().ok()?; + Some(mtch.text) + } + + /// Move to the previous match + pub fn prev_match(&mut self, target: &str) -> Option { + let mtch = self.doc_mut().prev_match(target)?; + self.doc_mut().move_to(&mtch.loc); + // Update highlighting + self.update_highlighter().ok()?; + Some(mtch.text) + } + + /// Use replace feature + pub fn replace(&mut self) -> Result<()> { + // Request replace information + let target = self.prompt("Replace")?; + let into = self.prompt("With")?; + let mut done = false; + let Size { w, h } = size()?; + // Jump to match + let mut mtch; + if let Some(m) = self.next_match(&target) { + // Automatically move to next match, keeping note of what that match is + mtch = m; + } else if let Some(m) = self.prev_match(&target) { + // Automatically move to previous match, keeping not of what that match is + // This happens if there are no matches further down the document, only above + mtch = m; + } else { + // Exit if there are no matches in the document + return Ok(()); + } + self.update_highlighter()?; + // Enter into the replace menu + while !done { + // Render just the document part + self.terminal.hide_cursor()?; + self.render_document(w, h.saturating_sub(2))?; + // Write custom status line for the replace mode + self.terminal.goto(0, h)?; + write!( + self.terminal.stdout, + "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All" + )?; + self.terminal.flush()?; + // Move back to correct cursor location + if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { + let max = self.dent(); + self.terminal.goto(x + max, y + 1)?; + self.terminal.show_cursor()?; + } else { + self.terminal.hide_cursor()?; + } + // Handle events + if let CEvent::Key(key) = read()? { + match (key.modifiers, key.code) { + // On escape key, exit + (KMod::NONE, KCode::Esc) => done = true, + // On right key, move to the previous match, keeping note of what that match is + (KMod::NONE, KCode::Left) => mtch = self.prev_match(&target).unwrap_or(mtch), + // On left key, move to the next match, keeping note of what that match is + (KMod::NONE, KCode::Right) => mtch = self.next_match(&target).unwrap_or(mtch), + // On return key, perform replacement + (KMod::NONE, KCode::Enter) => self.do_replace(&into, &mtch)?, + // On tab key, replace all instances within the document + (KMod::NONE, KCode::Tab) => self.do_replace_all(&target, &into), + _ => (), + } + } + // Update syntax highlighter if necessary + self.update_highlighter()?; + } + Ok(()) + } + + /// Replace an instance in a document + fn do_replace(&mut self, into: &str, text: &str) -> Result<()> { + // Commit events to event manager (for undo / redo) + self.doc_mut().commit(); + // Do the replacement + let loc = self.doc().char_loc(); + self.doc_mut().replace(loc, text, into)?; + self.doc_mut().move_to(&loc); + // Update syntax highlighter + self.update_highlighter()?; + self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); + Ok(()) + } + + /// Replace all instances in a document + fn do_replace_all(&mut self, target: &str, into: &str) { + // Commit events to event manager (for undo / redo) + self.doc_mut().commit(); + // Replace everything top to bottom + self.doc_mut().move_to(&Loc::at(0, 0)); + while let Some(mtch) = self.doc_mut().next_match(target, 1) { + drop(self.doc_mut().replace(mtch.loc, &mtch.text, into)); + drop(self.update_highlighter()); + self.highlighter[self.ptr].edit(mtch.loc.y, &self.doc[self.ptr].lines[mtch.loc.y]); + } + } +} diff --git a/src/main.rs b/src/main.rs index 039e34e3..3eaac89b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,9 @@ fn run(cli: CommandLineInterface) -> Result<()> { } // Actually handle editor event (errors included) - editor.borrow_mut().handle_event(event.clone())?; + if let Err(err) = editor.borrow_mut().handle_event(event.clone()) { + editor.borrow_mut().feedback = Feedback::Error(format!("{err:?}")); + } // Handle plug-in after key press mappings (if no errors occured) if let CEvent::Key(key) = event { From 22f004a0c422d23f4e0da2a0cd7a46e2dcbc686d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 28 Sep 2024 20:46:56 +0100 Subject: [PATCH 08/31] rustfmt --- src/editor/mod.rs | 8 +++----- src/editor/scanning.rs | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 157ac0d7..26099bb7 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,9 +1,7 @@ use crate::config::Config; use crate::error::{OxError, Result}; use crate::ui::{size, Feedback, Terminal}; -use crossterm::{ - event::{Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}, -}; +use crossterm::event::{Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}; use kaolinite::event::Error as KError; use kaolinite::Document; use mlua::Lua; @@ -12,9 +10,9 @@ use std::time::Instant; use synoptic::Highlighter; mod cursor; -mod mouse; -mod interface; mod editing; +mod interface; +mod mouse; mod scanning; /// For managing all editing and rendering of cactus diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 8b8fbaec..e3acac52 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -1,8 +1,6 @@ use crate::error::Result; use crate::ui::size; -use crossterm::{ - event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, -}; +use crossterm::event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}; use kaolinite::utils::{Loc, Size}; use std::io::Write; From e1bac1208e0f6e5ad4e692d5e8ff66e5a3095c95 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 28 Sep 2024 21:32:45 +0100 Subject: [PATCH 09/31] broke up config file into components --- src/config.rs | 1600 ------------------------------------ src/config/colors.rs | 346 ++++++++ src/config/editor.rs | 443 ++++++++++ src/config/highlighting.rs | 138 ++++ src/config/interface.rs | 396 +++++++++ src/config/keys.rs | 111 +++ src/config/mod.rs | 197 +++++ 7 files changed, 1631 insertions(+), 1600 deletions(-) delete mode 100644 src/config.rs create mode 100644 src/config/colors.rs create mode 100644 src/config/editor.rs create mode 100644 src/config/highlighting.rs create mode 100644 src/config/interface.rs create mode 100644 src/config/keys.rs create mode 100644 src/config/mod.rs diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index a5366eca..00000000 --- a/src/config.rs +++ /dev/null @@ -1,1600 +0,0 @@ -use crate::cli::VERSION; -use crate::editor::Editor; -use crate::error::{OxError, Result}; -use crate::ui::Feedback; -use crossterm::{ - event::{KeyCode as KCode, KeyModifiers as KMod, MediaKeyCode, ModifierKeyCode}, - style::{Color, SetForegroundColor as Fg}, -}; -use kaolinite::searching::Searcher; -use kaolinite::utils::{filetype, get_absolute_path, get_file_ext, get_file_name, icon}; -use kaolinite::{Document, Loc}; -use mlua::prelude::*; -use std::collections::HashMap; -use std::{cell::RefCell, rc::Rc}; -use synoptic::{from_extension, Highlighter}; - -// Issue a warning to the user -fn issue_warning(msg: &str) { - eprintln!("[WARNING] {}", msg); -} - -/// This contains the default configuration lua file -const DEFAULT_CONFIG: &str = include_str!("../config/.oxrc"); - -/// Default plug-in code to use -const PAIRS: &str = include_str!("../plugins/pairs.lua"); -const AUTOINDENT: &str = include_str!("../plugins/autoindent.lua"); - -/// This contains the code for setting up plug-in infrastructure -pub const PLUGIN_BOOTSTRAP: &str = include_str!("plugin/bootstrap.lua"); - -/// This contains the code for running the plugins -pub const PLUGIN_RUN: &str = include_str!("plugin/run.lua"); - -/// This contains the code for running code after a key binding is pressed -pub fn run_key(key: &str) -> String { - format!( - " - globalevent = (global_event_mapping[\"*\"] or {{}}) - for _, f in ipairs(globalevent) do - f() - end - key = (global_event_mapping[\"{key}\"] or error(\"key not bound\")) - for _, f in ipairs(key) do - f() - end - " - ) -} - -/// This contains the code for running code before a key binding is fully processed -pub fn run_key_before(key: &str) -> String { - format!( - " - globalevent = (global_event_mapping[\"before:*\"] or {{}}) - for _, f in ipairs(globalevent) do - f() - end - key = (global_event_mapping[\"before:{key}\"] or {{}}) - for _, f in ipairs(key) do - f() - end - " - ) -} - -/// The struct that holds all the configuration information -#[derive(Debug)] -pub struct Config { - pub syntax_highlighting: Rc>, - pub line_numbers: Rc>, - pub colors: Rc>, - pub status_line: Rc>, - pub tab_line: Rc>, - pub greeting_message: Rc>, - pub help_message: Rc>, - pub terminal: Rc>, - pub document: Rc>, -} - -impl Config { - /// Take a lua instance, inject all the configuration tables and return a default config struct - pub fn new(lua: &Lua) -> Result { - // Set up structs to populate (the default values will be thrown away) - let syntax_highlighting = Rc::new(RefCell::new(SyntaxHighlighting::default())); - let line_numbers = Rc::new(RefCell::new(LineNumbers::default())); - let greeting_message = Rc::new(RefCell::new(GreetingMessage::default())); - let help_message = Rc::new(RefCell::new(HelpMessage::default())); - let colors = Rc::new(RefCell::new(Colors::default())); - let status_line = Rc::new(RefCell::new(StatusLine::default())); - let tab_line = Rc::new(RefCell::new(TabLine::default())); - let terminal = Rc::new(RefCell::new(TerminalConfig::default())); - let document = Rc::new(RefCell::new(DocumentConfig::default())); - - // Push in configuration globals - lua.globals().set("syntax", syntax_highlighting.clone())?; - lua.globals().set("line_numbers", line_numbers.clone())?; - lua.globals() - .set("greeting_message", greeting_message.clone())?; - lua.globals().set("help_message", help_message.clone())?; - lua.globals().set("status_line", status_line.clone())?; - lua.globals().set("tab_line", tab_line.clone())?; - lua.globals().set("colors", colors.clone())?; - lua.globals().set("terminal", terminal.clone())?; - lua.globals().set("document", document.clone())?; - - Ok(Config { - syntax_highlighting, - line_numbers, - greeting_message, - help_message, - tab_line, - status_line, - colors, - terminal, - document, - }) - } - - /// Actually take the configuration file, open it and interpret it - pub fn read(&mut self, path: String, lua: &Lua) -> Result<()> { - // Load the default config to start with - lua.load(DEFAULT_CONFIG).exec()?; - // Reset plugin status based on built-in configuration file - lua.load("plugins = {}").exec()?; - lua.load("builtins = {}").exec()?; - - // Judge pre-user config state - let status_parts = self.status_line.borrow().parts.len(); - - // Attempt to read config file from home directory - let mut user_provided_config = false; - if let Ok(path) = shellexpand::full(&path) { - if let Ok(config) = std::fs::read_to_string(path.to_string()) { - // Update configuration with user-defined values - lua.load(config).exec()?; - user_provided_config = true; - } - } - - // Remove any default values if necessary - if self.status_line.borrow().parts.len() > status_parts { - self.status_line.borrow_mut().parts.drain(0..status_parts); - } - - // Determine whether or not to load built-in plugins - let mut builtins: HashMap<&str, &str> = HashMap::default(); - builtins.insert("pairs.lua", PAIRS); - builtins.insert("autoindent.lua", AUTOINDENT); - for (name, code) in builtins.iter() { - if self.load_bi(name, user_provided_config, &lua) { - lua.load(*code).exec()?; - } - } - - if user_provided_config { - Ok(()) - } else { - Err(OxError::Config("Not Found".to_string())) - } - } - - /// Decide whether to load a built-in plugin - pub fn load_bi(&self, name: &str, user_provided_config: bool, lua: &Lua) -> bool { - if !user_provided_config { - // Load when the user hasn't provided a configuration file - true - } else { - // Get list of user-loaded plug-ins - let plugins: Vec = lua - .globals() - .get::<_, LuaTable>("builtins") - .unwrap() - .sequence_values() - .filter_map(std::result::Result::ok) - .collect(); - // If the user wants to load the plug-in but it isn't available - if let Some(idx) = plugins.iter().position(|p| p.ends_with(name)) { - // User wants the plug-in - let path = &plugins[idx]; - // true if plug-in isn't avilable - !std::path::Path::new(path).exists() - } else { - // User doesn't want the plug-in - false - } - } - } -} - -/// For storing general configuration related to the terminal functionality -#[derive(Debug)] -pub struct TerminalConfig { - pub mouse_enabled: bool, -} - -impl Default for TerminalConfig { - fn default() -> Self { - Self { - mouse_enabled: true, - } - } -} - -impl LuaUserData for TerminalConfig { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("mouse_enabled", |_, this| Ok(this.mouse_enabled)); - fields.add_field_method_set("mouse_enabled", |_, this, value| { - this.mouse_enabled = value; - Ok(()) - }); - } -} - -/// For storing configuration information related to syntax highlighting -#[derive(Debug)] -pub struct SyntaxHighlighting { - pub theme: HashMap, - pub user_rules: HashMap, -} - -impl Default for SyntaxHighlighting { - fn default() -> Self { - Self { - theme: HashMap::default(), - user_rules: HashMap::default(), - } - } -} - -impl SyntaxHighlighting { - /// Get a colour from the theme - pub fn get_theme(&self, name: &str) -> Result { - if let Some(col) = self.theme.get(name) { - col.to_color() - } else { - Err(OxError::Config(format!( - "{} has not been given a colour in the theme", - name - ))) - } - } - - /// Get a highlighter given a file extension - pub fn get_highlighter(&self, ext: &str) -> Highlighter { - self.user_rules - .get(ext) - .and_then(|h| Some(h.clone())) - .unwrap_or_else(|| from_extension(ext, 4).unwrap_or_else(|| Highlighter::new(4))) - } -} - -impl LuaUserData for SyntaxHighlighting { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method_mut( - "keywords", - |lua, _, (name, pattern): (String, Vec)| { - let table = lua.create_table()?; - table.set("kind", "keyword")?; - table.set("name", name)?; - table.set("pattern", format!("({})", pattern.join("|")))?; - Ok(table) - }, - ); - methods.add_method_mut("keyword", |lua, _, (name, pattern): (String, String)| { - let table = lua.create_table()?; - table.set("kind", "keyword")?; - table.set("name", name)?; - table.set("pattern", pattern)?; - Ok(table) - }); - methods.add_method_mut( - "bounded", - |lua, _, (name, start, end, escape): (String, String, String, bool)| { - let table = lua.create_table()?; - table.set("kind", "bounded")?; - table.set("name", name)?; - table.set("start", start)?; - table.set("end", end)?; - table.set("escape", escape.to_string())?; - Ok(table) - }, - ); - type BoundedInterpArgs = (String, String, String, String, String, bool); - methods.add_method_mut( - "bounded_interpolation", - |lua, _, (name, start, end, i_start, i_end, escape): BoundedInterpArgs| { - let table = lua.create_table()?; - table.set("kind", "bounded_interpolation")?; - table.set("name", name)?; - table.set("start", start)?; - table.set("end", end)?; - table.set("i_start", i_start)?; - table.set("i_end", i_end)?; - table.set("escape", escape.to_string())?; - Ok(table) - }, - ); - methods.add_method_mut( - "new", - |_, syntax_highlighting, (extensions, rules): (LuaTable, LuaTable)| { - // Make note of the highlighter - for ext_idx in 1..(extensions.len()? + 1) { - // Create highlighter - let mut highlighter = Highlighter::new(4); - // Add rules one by one - for rule_idx in 1..(rules.len()? + 1) { - // Get rule - let rule = rules.get::>(rule_idx)?; - // Find type of rule and attatch it to the highlighter - match rule["kind"].as_str() { - "keyword" => { - highlighter.keyword(rule["name"].clone(), &rule["pattern"]) - } - "bounded" => highlighter.bounded( - rule["name"].clone(), - rule["start"].clone(), - rule["end"].clone(), - rule["escape"] == "true", - ), - "bounded_interpolation" => highlighter.bounded_interp( - rule["name"].clone(), - rule["start"].clone(), - rule["end"].clone(), - rule["i_start"].clone(), - rule["i_end"].clone(), - rule["escape"] == "true", - ), - _ => unreachable!(), - } - } - let ext = extensions.get::(ext_idx)?; - syntax_highlighting.user_rules.insert(ext, highlighter); - } - Ok(()) - }, - ); - methods.add_method_mut("set", |_, syntax_highlighting, (name, value)| { - syntax_highlighting - .theme - .insert(name, ConfigColor::from_lua(value)); - Ok(()) - }); - } -} - -/// For storing configuration information related to line numbers -#[derive(Debug)] -pub struct LineNumbers { - pub enabled: bool, - pub padding_left: usize, - pub padding_right: usize, -} - -impl Default for LineNumbers { - fn default() -> Self { - Self { - enabled: true, - padding_left: 1, - padding_right: 1, - } - } -} - -impl LuaUserData for LineNumbers { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); - fields.add_field_method_set("enabled", |_, this, value| { - this.enabled = value; - Ok(()) - }); - fields.add_field_method_get("padding_left", |_, this| Ok(this.padding_left)); - fields.add_field_method_set("padding_left", |_, this, value| { - this.padding_left = value; - Ok(()) - }); - fields.add_field_method_get("padding_right", |_, this| Ok(this.padding_right)); - fields.add_field_method_set("padding_right", |_, this, value| { - this.padding_right = value; - Ok(()) - }); - } -} - -/// For storing configuration information related to the greeting message -#[derive(Debug)] -pub struct GreetingMessage { - pub enabled: bool, - pub format: String, -} - -impl Default for GreetingMessage { - fn default() -> Self { - Self { - enabled: true, - format: "".to_string(), - } - } -} - -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(); - let mut result = self.format.clone(); - result = result.replace("{version}", &VERSION).to_string(); - result = result.replace("{highlight_start}", &highlight).to_string(); - result = result.replace("{highlight_end}", &editor_fg).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) { - let name = m - .text - .chars() - .skip(1) - .take(m.text.chars().count().saturating_sub(2)) - .collect::(); - if let Ok(func) = lua.globals().get::(name) { - if let Ok(r) = func.call::<(), LuaString>(()) { - result = result.replace(&m.text, r.to_str().unwrap_or("")); - } else { - break; - } - } else { - break; - } - } - Ok(result) - } -} - -impl LuaUserData for GreetingMessage { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); - fields.add_field_method_set("enabled", |_, this, value| { - this.enabled = value; - Ok(()) - }); - fields.add_field_method_get("format", |_, this| Ok(this.format.clone())); - fields.add_field_method_set("format", |_, this, value| { - this.format = value; - Ok(()) - }); - } -} - -/// For storing configuration information related to the help message -#[derive(Debug)] -pub struct HelpMessage { - pub enabled: bool, - pub format: String, -} - -impl Default for HelpMessage { - fn default() -> Self { - Self { - enabled: true, - format: "".to_string(), - } - } -} - -impl HelpMessage { - /// Take the configuration information and render the help 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(); - let mut result = self.format.clone(); - result = result.replace("{version}", &VERSION).to_string(); - result = result.replace("{highlight_start}", &highlight).to_string(); - result = result.replace("{highlight_end}", &editor_fg).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) { - let name = m - .text - .chars() - .skip(1) - .take(m.text.chars().count().saturating_sub(2)) - .collect::(); - if let Ok(func) = lua.globals().get::(name) { - if let Ok(r) = func.call::<(), LuaString>(()) { - result = result.replace(&m.text, r.to_str().unwrap_or("")); - } else { - break; - } - } else { - break; - } - } - Ok(result.split('\n').map(|l| l.to_string()).collect()) - } -} - -impl LuaUserData for HelpMessage { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); - fields.add_field_method_set("enabled", |_, this, value| { - this.enabled = value; - Ok(()) - }); - fields.add_field_method_get("format", |_, this| Ok(this.format.clone())); - fields.add_field_method_set("format", |_, this, value| { - this.format = value; - Ok(()) - }); - } -} - -/// For storing configuration information related to the status line -#[derive(Debug)] -pub struct TabLine { - pub enabled: bool, - pub format: String, -} - -impl Default for TabLine { - fn default() -> Self { - Self { - enabled: true, - format: " {file_name}{modified} ".to_string(), - } - } -} - -impl TabLine { - pub fn render(&self, document: &Document) -> String { - let path = document - .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 icon = icon(&filetype(&file_extension).unwrap_or_else(|| "".to_string())); - let modified = if document.modified { "[+]" } else { "" }; - let mut result = self.format.clone(); - result = result - .replace("{file_extension}", &file_extension) - .to_string(); - result = result.replace("{file_name}", &file_name).to_string(); - result = result - .replace("{absolute_path}", &absolute_path) - .to_string(); - result = result.replace("{path}", &path).to_string(); - result = result.replace("{modified}", &modified).to_string(); - result = result.replace("{icon}", &icon).to_string(); - result - } -} - -impl LuaUserData for TabLine { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); - fields.add_field_method_set("enabled", |_, this, value| { - this.enabled = value; - Ok(()) - }); - fields.add_field_method_get("format", |_, this| Ok(this.format.clone())); - fields.add_field_method_set("format", |_, this, value| { - this.format = value; - Ok(()) - }); - } -} - -/// For storing configuration information related to the status line -#[derive(Debug)] -pub struct StatusLine { - pub parts: Vec, - pub alignment: StatusAlign, -} - -impl Default for StatusLine { - fn default() -> Self { - Self { - parts: vec![], - alignment: StatusAlign::Between, - } - } -} - -impl StatusLine { - pub fn render(&self, editor: &Editor, lua: &Lua, w: usize) -> String { - let mut result = vec![]; - let path = editor - .doc() - .file_name - .to_owned() - .unwrap_or_else(|| "[No Name]".to_string()); - let file_extension = get_file_ext(&path).unwrap_or_else(|| "".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 = filetype(&file_extension).unwrap_or_else(|| { - if file_extension.is_empty() { - "Unknown".to_string() - } else { - file_extension.to_string() - } - }); - let icon = icon(&filetype(&file_extension).unwrap_or_else(|| "".to_string())); - let modified = if editor.doc().modified { "[+]" } 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(); - - for part in &self.parts { - let mut part = part.clone(); - part = part.replace("{file_name}", &file_name).to_string(); - part = part - .replace("{file_extension}", &file_extension) - .to_string(); - part = part.replace("{icon}", &icon).to_string(); - part = part.replace("{path}", &path).to_string(); - part = part.replace("{absolute_path}", &absolute_path).to_string(); - part = part.replace("{modified}", &modified).to_string(); - part = part.replace("{file_type}", &file_type).to_string(); - part = part.replace("{cursor_y}", &cursor_y).to_string(); - part = part.replace("{cursor_x}", &cursor_x).to_string(); - part = part.replace("{line_count}", &line_count).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(&part) { - let name = m - .text - .chars() - .skip(1) - .take(m.text.chars().count().saturating_sub(2)) - .collect::(); - if let Ok(func) = lua.globals().get::(name) { - if let Ok(r) = func.call::<(), LuaString>(()) { - part = part.replace(&m.text, r.to_str().unwrap_or("")); - } else { - break; - } - } else { - break; - } - } - result.push(part); - } - let status: Vec<&str> = result.iter().map(|s| s.as_str()).collect(); - match self.alignment { - StatusAlign::Between => alinio::align::between(status.as_slice(), w), - StatusAlign::Around => alinio::align::around(status.as_slice(), w), - } - .unwrap_or_else(|| "".to_string()) - } -} - -impl LuaUserData for StatusLine { - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_method_mut("clear", |_, status_line, ()| { - status_line.parts.clear(); - Ok(()) - }); - methods.add_method_mut("add_part", |_, status_line, part| { - status_line.parts.push(part); - Ok(()) - }); - } - - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("alignment", |_, this| { - let alignment: String = this.alignment.clone().into(); - Ok(alignment) - }); - fields.add_field_method_set("alignment", |_, this, value| { - this.alignment = StatusAlign::from_string(value); - Ok(()) - }); - } -} - -#[derive(Debug, Clone)] -pub enum StatusAlign { - Around, - Between, -} - -impl StatusAlign { - pub fn from_string(string: String) -> Self { - match string.as_str() { - "around" => Self::Around, - "between" => Self::Between, - _ => { - issue_warning( - "\ - Invalid status line alignment used in configuration file - \ - make sure value is either 'around' or 'between' (defaulting to 'between')", - ); - Self::Between - } - } - } -} - -impl Into for StatusAlign { - fn into(self) -> String { - match self { - Self::Around => "around", - Self::Between => "between", - } - .to_string() - } -} - -#[derive(Debug)] -pub struct Colors { - pub editor_bg: ConfigColor, - pub editor_fg: ConfigColor, - - pub status_bg: ConfigColor, - pub status_fg: ConfigColor, - - pub highlight: ConfigColor, - - pub line_number_fg: ConfigColor, - pub line_number_bg: ConfigColor, - - pub tab_active_fg: ConfigColor, - pub tab_active_bg: ConfigColor, - pub tab_inactive_fg: ConfigColor, - pub tab_inactive_bg: ConfigColor, - - pub info_bg: ConfigColor, - pub info_fg: ConfigColor, - pub warning_bg: ConfigColor, - pub warning_fg: ConfigColor, - pub error_bg: ConfigColor, - pub error_fg: ConfigColor, - - pub selection_fg: ConfigColor, - pub selection_bg: ConfigColor, -} - -impl Default for Colors { - fn default() -> Self { - Self { - editor_bg: ConfigColor::Black, - editor_fg: ConfigColor::Black, - - status_bg: ConfigColor::Black, - status_fg: ConfigColor::Black, - - highlight: ConfigColor::Black, - - line_number_fg: ConfigColor::Black, - line_number_bg: ConfigColor::Black, - - tab_active_fg: ConfigColor::Black, - tab_active_bg: ConfigColor::Black, - tab_inactive_fg: ConfigColor::Black, - tab_inactive_bg: ConfigColor::Black, - - info_bg: ConfigColor::Black, - info_fg: ConfigColor::Black, - warning_bg: ConfigColor::Black, - warning_fg: ConfigColor::Black, - error_bg: ConfigColor::Black, - error_fg: ConfigColor::Black, - - selection_fg: ConfigColor::White, - selection_bg: ConfigColor::Blue, - } - } -} - -impl LuaUserData for Colors { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("editor_bg", |env, this| Ok(this.editor_bg.to_lua(env))); - fields.add_field_method_get("editor_fg", |env, this| Ok(this.editor_fg.to_lua(env))); - fields.add_field_method_get("status_bg", |env, this| Ok(this.status_bg.to_lua(env))); - fields.add_field_method_get("status_fg", |env, this| Ok(this.status_fg.to_lua(env))); - fields.add_field_method_get("highlight", |env, this| Ok(this.highlight.to_lua(env))); - fields.add_field_method_get("line_number_bg", |env, this| { - Ok(this.line_number_bg.to_lua(env)) - }); - fields.add_field_method_get("line_number_fg", |env, this| { - Ok(this.line_number_fg.to_lua(env)) - }); - fields.add_field_method_get("tab_active_fg", |env, this| { - Ok(this.tab_active_fg.to_lua(env)) - }); - fields.add_field_method_get("tab_active_bg", |env, this| { - Ok(this.tab_active_bg.to_lua(env)) - }); - fields.add_field_method_get("tab_inactive_fg", |env, this| { - Ok(this.tab_inactive_fg.to_lua(env)) - }); - fields.add_field_method_get("tab_inactive_bg", |env, this| { - Ok(this.tab_inactive_bg.to_lua(env)) - }); - fields.add_field_method_get("error_bg", |env, this| Ok(this.error_bg.to_lua(env))); - fields.add_field_method_get("error_fg", |env, this| Ok(this.error_fg.to_lua(env))); - fields.add_field_method_get("warning_bg", |env, this| Ok(this.warning_bg.to_lua(env))); - fields.add_field_method_get("warning_fg", |env, this| Ok(this.warning_fg.to_lua(env))); - fields.add_field_method_get("info_bg", |env, this| Ok(this.info_bg.to_lua(env))); - fields.add_field_method_get("info_fg", |env, this| Ok(this.info_fg.to_lua(env))); - fields.add_field_method_get("selection_fg", |env, this| { - Ok(this.selection_fg.to_lua(env)) - }); - fields.add_field_method_get("selection_bg", |env, this| { - Ok(this.selection_bg.to_lua(env)) - }); - fields.add_field_method_set("editor_bg", |_, this, value| { - this.editor_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("editor_fg", |_, this, value| { - this.editor_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("status_bg", |_, this, value| { - this.status_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("status_fg", |_, this, value| { - this.status_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("highlight", |_, this, value| { - this.highlight = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("line_number_bg", |_, this, value| { - this.line_number_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("line_number_fg", |_, this, value| { - this.line_number_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("tab_active_fg", |_, this, value| { - this.tab_active_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("tab_active_bg", |_, this, value| { - this.tab_active_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("tab_inactive_fg", |_, this, value| { - this.tab_inactive_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("tab_inactive_bg", |_, this, value| { - this.tab_inactive_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("error_bg", |_, this, value| { - this.error_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("error_fg", |_, this, value| { - this.error_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("warning_bg", |_, this, value| { - this.warning_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("warning_fg", |_, this, value| { - this.warning_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("info_bg", |_, this, value| { - this.info_bg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("info_fg", |_, this, value| { - this.info_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("selection_fg", |_, this, value| { - this.selection_fg = ConfigColor::from_lua(value); - Ok(()) - }); - fields.add_field_method_set("selection_bg", |_, this, value| { - this.selection_bg = ConfigColor::from_lua(value); - Ok(()) - }); - } -} - -#[derive(Debug)] -pub enum ConfigColor { - Rgb(u8, u8, u8), - Hex(String), - Black, - DarkGrey, - Red, - DarkRed, - Green, - DarkGreen, - Yellow, - DarkYellow, - Blue, - DarkBlue, - Magenta, - DarkMagenta, - Cyan, - DarkCyan, - White, - Grey, - Transparent, -} - -impl ConfigColor { - pub fn from_lua<'a>(value: LuaValue<'a>) -> Self { - match value { - LuaValue::String(string) => match string.to_str().unwrap_or("transparent") { - "black" => Self::Black, - "darkgrey" => Self::DarkGrey, - "red" => Self::Red, - "darkred" => Self::DarkRed, - "green" => Self::Green, - "darkgreen" => Self::DarkGreen, - "yellow" => Self::Yellow, - "darkyellow" => Self::DarkYellow, - "blue" => Self::Blue, - "darkblue" => Self::DarkBlue, - "magenta" => Self::Magenta, - "darkmagenta" => Self::DarkMagenta, - "cyan" => Self::Cyan, - "darkcyan" => Self::DarkCyan, - "white" => Self::White, - "grey" => Self::Grey, - "transparent" => Self::Transparent, - hex => Self::Hex(hex.to_string()), - }, - LuaValue::Table(table) => { - if table.len().unwrap_or(3) != 3 { - issue_warning("Invalid RGB sequence used in configuration file (must be a list of 3 numbers)"); - return Self::Transparent; - } - let mut tri: Vec = vec![]; - for _ in 0..3 { - if let Ok(val) = table.pop() { - tri.insert(0, val) - } else { - issue_warning("Invalid RGB sequence provided - please check your numerical values are between 0 and 255"); - tri.insert(0, 255); - } - } - Self::Rgb(tri[0], tri[1], tri[2]) - } - _ => { - issue_warning("Invalid data type used for colour in configuration file"); - Self::Transparent - } - } - } - - pub fn to_lua<'a>(&self, env: &'a Lua) -> LuaValue<'a> { - let msg = "Failed to create lua string"; - match self { - ConfigColor::Hex(hex) => { - let string = env.create_string(hex).expect(msg); - LuaValue::String(string) - } - ConfigColor::Rgb(r, g, b) => { - // Create lua table - let table = env.create_table().expect("Failed to create lua table"); - let _ = table.push(*r as isize); - let _ = table.push(*g as isize); - let _ = table.push(*b as isize); - LuaValue::Table(table) - } - ConfigColor::Black => LuaValue::String(env.create_string("black").expect(msg)), - ConfigColor::DarkGrey => LuaValue::String(env.create_string("darkgrey").expect(msg)), - ConfigColor::Red => LuaValue::String(env.create_string("red").expect(msg)), - ConfigColor::DarkRed => LuaValue::String(env.create_string("darkred").expect(msg)), - ConfigColor::Green => LuaValue::String(env.create_string("green").expect(msg)), - ConfigColor::DarkGreen => LuaValue::String(env.create_string("darkgreen").expect(msg)), - ConfigColor::Yellow => LuaValue::String(env.create_string("yellow").expect(msg)), - ConfigColor::DarkYellow => { - LuaValue::String(env.create_string("darkyellow").expect(msg)) - } - ConfigColor::Blue => LuaValue::String(env.create_string("blue").expect(msg)), - ConfigColor::DarkBlue => LuaValue::String(env.create_string("darkblue").expect(msg)), - ConfigColor::Magenta => LuaValue::String(env.create_string("magenta").expect(msg)), - ConfigColor::DarkMagenta => { - LuaValue::String(env.create_string("darkmagenta").expect(msg)) - } - ConfigColor::Cyan => LuaValue::String(env.create_string("cyan").expect(msg)), - ConfigColor::DarkCyan => LuaValue::String(env.create_string("darkcyan").expect(msg)), - ConfigColor::White => LuaValue::String(env.create_string("white").expect(msg)), - ConfigColor::Grey => LuaValue::String(env.create_string("grey").expect(msg)), - ConfigColor::Transparent => { - LuaValue::String(env.create_string("transparent").expect(msg)) - } - } - } - - pub fn to_color(&self) -> Result { - Ok(match self { - ConfigColor::Hex(hex) => { - let (r, g, b) = self.hex_to_rgb(hex)?; - Color::Rgb { r, g, b } - } - ConfigColor::Rgb(r, g, b) => Color::Rgb { - r: *r, - g: *g, - b: *b, - }, - ConfigColor::Black => Color::Black, - ConfigColor::DarkGrey => Color::DarkGrey, - ConfigColor::Red => Color::Red, - ConfigColor::DarkRed => Color::DarkRed, - ConfigColor::Green => Color::Green, - ConfigColor::DarkGreen => Color::DarkGreen, - ConfigColor::Yellow => Color::Yellow, - ConfigColor::DarkYellow => Color::DarkYellow, - ConfigColor::Blue => Color::Blue, - ConfigColor::DarkBlue => Color::DarkBlue, - ConfigColor::Magenta => Color::Magenta, - ConfigColor::DarkMagenta => Color::DarkMagenta, - ConfigColor::Cyan => Color::Cyan, - ConfigColor::DarkCyan => Color::DarkCyan, - ConfigColor::White => Color::White, - ConfigColor::Grey => Color::Grey, - ConfigColor::Transparent => Color::Reset, - }) - } - - fn hex_to_rgb(&self, hex: &str) -> Result<(u8, u8, u8)> { - // Remove the leading '#' if present - let hex = hex.trim_start_matches('#'); - - // Ensure the hex code is exactly 6 characters long - if hex.len() != 6 { - panic!("Invalid hex code used in configuration file - ensure they are of length 6"); - } - - // Parse the hex string into the RGB components - let mut tri: Vec = vec![]; - for i in 0..3 { - let section = &hex[(i * 2)..(i * 2 + 2)]; - if let Ok(val) = u8::from_str_radix(section, 16) { - tri.insert(0, val) - } else { - panic!("Invalid hex code used in configuration file - ensure all digits are between 0 and F"); - } - } - Ok((tri[0], tri[1], tri[2])) - } -} - -pub fn key_to_string(modifiers: KMod, key: KCode) -> String { - let mut result = "".to_string(); - // Deal with modifiers - if modifiers.contains(KMod::CONTROL) { - result += "ctrl_"; - } - if modifiers.contains(KMod::ALT) { - result += "alt_"; - } - if modifiers.contains(KMod::SHIFT) { - result += "shift_"; - } - result += &match key { - KCode::Char('\\') => "\\\\".to_string(), - KCode::Char('"') => "\\\"".to_string(), - KCode::Backspace => "backspace".to_string(), - KCode::Enter => "enter".to_string(), - KCode::Left => "left".to_string(), - KCode::Right => "right".to_string(), - KCode::Up => "up".to_string(), - KCode::Down => "down".to_string(), - KCode::Home => "home".to_string(), - KCode::End => "end".to_string(), - KCode::PageUp => "pageup".to_string(), - KCode::PageDown => "pagedown".to_string(), - KCode::Tab => "tab".to_string(), - KCode::BackTab => "backtab".to_string(), - KCode::Delete => "delete".to_string(), - KCode::Insert => "insert".to_string(), - KCode::F(num) => format!("f{num}"), - KCode::Char(ch) => format!("{}", ch.to_lowercase()), - KCode::Null => "null".to_string(), - KCode::Esc => "esc".to_string(), - KCode::CapsLock => "capslock".to_string(), - KCode::ScrollLock => "scrolllock".to_string(), - KCode::NumLock => "numlock".to_string(), - KCode::PrintScreen => "printscreen".to_string(), - KCode::Pause => "pause".to_string(), - KCode::Menu => "menu".to_string(), - KCode::KeypadBegin => "keypadbegin".to_string(), - KCode::Media(key) => match key { - MediaKeyCode::Play => "play", - MediaKeyCode::Pause => "pause", - MediaKeyCode::PlayPause => "playpause", - MediaKeyCode::Reverse => "reverse", - MediaKeyCode::Stop => "stop", - MediaKeyCode::FastForward => "fastforward", - MediaKeyCode::TrackNext => "next", - MediaKeyCode::TrackPrevious => "previous", - MediaKeyCode::Record => "record", - MediaKeyCode::Rewind => "rewind", - MediaKeyCode::LowerVolume => "lowervolume", - MediaKeyCode::RaiseVolume => "raisevolume", - MediaKeyCode::MuteVolume => "mutevolume", - } - .to_string(), - KCode::Modifier(key) => match key { - ModifierKeyCode::LeftShift => "lshift", - ModifierKeyCode::LeftControl => "lctrl", - ModifierKeyCode::LeftAlt => "lalt", - ModifierKeyCode::LeftSuper => "lsuper", - ModifierKeyCode::LeftHyper => "lhyper", - ModifierKeyCode::LeftMeta => "lmeta", - ModifierKeyCode::RightControl => "rctrl", - ModifierKeyCode::RightAlt => "ralt", - ModifierKeyCode::RightSuper => "rsuper", - ModifierKeyCode::RightHyper => "rhyper", - ModifierKeyCode::RightMeta => "rmeta", - ModifierKeyCode::RightShift => "rshift", - ModifierKeyCode::IsoLevel3Shift => "iso3shift", - ModifierKeyCode::IsoLevel5Shift => "iso5shift", - } - .to_string(), - }; - return result; -} - -fn update_highlighter(editor: &mut Editor) { - if let Err(err) = editor.update_highlighter() { - editor.feedback = Feedback::Error(err.to_string()); - } -} - -#[derive(Debug)] -pub struct DocumentConfig { - pub tab_width: usize, - pub undo_period: usize, - pub wrap_cursor: bool, -} - -impl Default for DocumentConfig { - fn default() -> Self { - Self { - tab_width: 4, - undo_period: 10, - wrap_cursor: true, - } - } -} - -impl LuaUserData for DocumentConfig { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("tab_width", |_, document| Ok(document.tab_width)); - fields.add_field_method_set("tab_width", |_, this, value| { - this.tab_width = value; - Ok(()) - }); - fields.add_field_method_get("undo_period", |_, document| Ok(document.undo_period)); - fields.add_field_method_set("undo_period", |_, this, value| { - this.undo_period = value; - Ok(()) - }); - fields.add_field_method_get("wrap_cursor", |_, document| Ok(document.wrap_cursor)); - fields.add_field_method_set("wrap_cursor", |_, this, value| { - this.wrap_cursor = value; - Ok(()) - }); - } -} - -impl LuaUserData for Editor { - fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("pasting", |_, editor| Ok(editor.paste_flag)); - fields.add_field_method_get("cursor", |_, editor| { - let loc = editor.doc().char_loc(); - Ok(LuaLoc { - x: loc.x, - y: loc.y + 1, - }) - }); - fields.add_field_method_get("document_name", |_, editor| { - let name = editor.doc().file_name.clone(); - Ok(name) - }); - fields.add_field_method_get("document_length", |_, editor| { - let len = editor.doc().len_lines(); - Ok(len) - }); - 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.doc.len())); - // DEPRECIATED - fields.add_field_method_get("help_visible", |_, _| Ok(false)); - fields.add_field_method_get("document_type", |_, editor| { - let ext = editor - .doc() - .file_name - .as_ref() - .and_then(|name| Some(name.split('.').last().unwrap_or(""))) - .unwrap_or(""); - let file_type = kaolinite::utils::filetype(ext); - Ok(file_type) - }); - } - - fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - // Reload the configuration file - methods.add_method_mut("reload_config", |lua, editor, ()| { - if editor - .load_config(editor.config_path.clone(), &lua) - .is_err() - { - editor.feedback = Feedback::Error("Failed to reload config".to_string()); - } - Ok(()) - }); - // Display messages - methods.add_method_mut("display_error", |_, editor, message: String| { - editor.feedback = Feedback::Error(message); - Ok(()) - }); - methods.add_method_mut("display_warning", |_, editor, message: String| { - editor.feedback = Feedback::Warning(message); - Ok(()) - }); - methods.add_method_mut("display_info", |_, editor, message: String| { - editor.feedback = Feedback::Info(message); - Ok(()) - }); - // Prompt the user - methods.add_method_mut("prompt", |_, editor, question: String| { - Ok(editor - .prompt(question) - .unwrap_or_else(|_| "error".to_string())) - }); - // Edit commands (relative) - methods.add_method_mut("insert", |_, editor, text: String| { - for ch in text.chars() { - if let Err(err) = editor.character(ch) { - editor.feedback = Feedback::Error(err.to_string()); - } - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("remove", |_, editor, ()| { - if let Err(err) = editor.backspace() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("insert_line", |_, editor, ()| { - if let Err(err) = editor.enter() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("remove_line", |_, editor, ()| { - if let Err(err) = editor.delete_line() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - // Cursor moving - methods.add_method_mut("move_to", |_, editor, (x, y): (usize, usize)| { - let y = y.saturating_sub(1); - editor.doc_mut().move_to(&Loc { x, y }); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_up", |_, editor, ()| { - editor.up(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_down", |_, editor, ()| { - editor.down(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_left", |_, editor, ()| { - editor.left(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_right", |_, editor, ()| { - editor.right(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("select_up", |_, editor, ()| { - editor.select_up(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("select_down", |_, editor, ()| { - editor.select_down(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("select_left", |_, editor, ()| { - editor.select_left(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("select_right", |_, editor, ()| { - editor.select_right(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("select_all", |_, editor, ()| { - editor.select_all(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("cut", |_, editor, ()| { - if let Err(err) = editor.cut() { - editor.feedback = Feedback::Error(err.to_string()); - } else { - editor.feedback = Feedback::Info("Text cut to clipboard".to_owned()); - } - Ok(()) - }); - methods.add_method_mut("copy", |_, editor, ()| { - if let Err(err) = editor.copy() { - editor.feedback = Feedback::Error(err.to_string()); - } else { - editor.feedback = Feedback::Info("Text copied to clipboard".to_owned()); - } - Ok(()) - }); - methods.add_method_mut("move_home", |_, editor, ()| { - editor.doc_mut().move_home(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_end", |_, editor, ()| { - editor.doc_mut().move_end(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_page_up", |_, editor, ()| { - editor.doc_mut().move_page_up(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_page_down", |_, editor, ()| { - editor.doc_mut().move_page_down(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_top", |_, editor, ()| { - editor.doc_mut().move_top(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_bottom", |_, editor, ()| { - editor.doc_mut().move_bottom(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_previous_word", |_, editor, ()| { - editor.prev_word(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("move_next_word", |_, editor, ()| { - editor.next_word(); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut( - "insert_at", - |_, editor, (text, x, y): (String, usize, usize)| { - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to(&Loc { x, y }); - for ch in text.chars() { - if let Err(err) = editor.character(ch) { - editor.feedback = Feedback::Error(err.to_string()); - } - } - editor.doc_mut().move_to(&location); - update_highlighter(editor); - Ok(()) - }, - ); - methods.add_method_mut("remove_at", |_, editor, (x, y): (usize, usize)| { - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to(&Loc { x, y }); - if let Err(err) = editor.delete() { - editor.feedback = Feedback::Error(err.to_string()); - } - editor.doc_mut().move_to(&location); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("insert_line_at", |_, editor, (text, y): (String, usize)| { - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - if y < editor.doc().len_lines() { - editor.doc_mut().move_to_y(y); - editor.doc_mut().move_home(); - if let Err(err) = editor.enter() { - editor.feedback = Feedback::Error(err.to_string()); - } - editor.up(); - } else { - editor.doc_mut().move_bottom(); - if let Err(err) = editor.enter() { - editor.feedback = Feedback::Error(err.to_string()); - } - } - for ch in text.chars() { - if let Err(err) = editor.character(ch) { - editor.feedback = Feedback::Error(err.to_string()); - } - } - editor.doc_mut().move_to(&location); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("remove_line_at", |_, editor, y: usize| { - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to_y(y); - if let Err(err) = editor.delete_line() { - editor.feedback = Feedback::Error(err.to_string()); - } - editor.doc_mut().move_to(&location); - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("open_command_line", |_, editor, ()| { - match editor.prompt("Command") { - Ok(command) => { - editor.command = Some(command); - } - Err(err) => { - editor.feedback = Feedback::Error(err.to_string()); - } - } - Ok(()) - }); - methods.add_method_mut("previous_tab", |_, editor, ()| { - editor.prev(); - Ok(()) - }); - methods.add_method_mut("next_tab", |_, editor, ()| { - editor.next(); - Ok(()) - }); - methods.add_method_mut("new", |_, editor, ()| { - if let Err(err) = editor.new_document() { - editor.feedback = Feedback::Error(err.to_string()); - } - Ok(()) - }); - methods.add_method_mut("open", |_, editor, ()| { - if let Err(err) = editor.open_document() { - editor.feedback = Feedback::Error(err.to_string()); - } - Ok(()) - }); - methods.add_method_mut("save", |_, editor, ()| { - if let Err(err) = editor.save() { - editor.feedback = Feedback::Error(err.to_string()); - } - Ok(()) - }); - methods.add_method_mut("save_as", |_, editor, ()| { - if let Err(err) = editor.save_as() { - editor.feedback = Feedback::Error(err.to_string()); - } - Ok(()) - }); - methods.add_method_mut("save_all", |_, editor, ()| { - if let Err(err) = editor.save_all() { - editor.feedback = Feedback::Error(err.to_string()); - } - Ok(()) - }); - methods.add_method_mut("quit", |_, editor, ()| { - if let Err(err) = editor.quit() { - editor.feedback = Feedback::Error(err.to_string()); - } - Ok(()) - }); - methods.add_method_mut("undo", |_, editor, ()| { - if let Err(err) = editor.undo() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("redo", |_, editor, ()| { - if let Err(err) = editor.redo() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("search", |_, editor, ()| { - if let Err(err) = editor.search() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method_mut("replace", |_, editor, ()| { - if let Err(err) = editor.replace() { - editor.feedback = Feedback::Error(err.to_string()); - } - update_highlighter(editor); - Ok(()) - }); - methods.add_method("get_character", |_, editor, ()| { - let loc = editor.doc().char_loc(); - let ch = editor - .doc() - .line(loc.y) - .unwrap_or_else(|| "".to_string()) - .chars() - .nth(loc.x) - .and_then(|ch| Some(ch.to_string())) - .unwrap_or_else(|| "".to_string()); - Ok(ch) - }); - methods.add_method_mut("get_character_at", |_, editor, (x, y): (usize, usize)| { - editor.doc_mut().load_to(y); - let y = y.saturating_sub(1); - let ch = editor - .doc() - .line(y) - .unwrap_or_else(|| "".to_string()) - .chars() - .nth(x) - .and_then(|ch| Some(ch.to_string())) - .unwrap_or_else(|| "".to_string()); - update_highlighter(editor); - Ok(ch) - }); - methods.add_method("get_line", |_, editor, ()| { - let loc = editor.doc().char_loc(); - let line = editor.doc().line(loc.y).unwrap_or_else(|| "".to_string()); - Ok(line) - }); - methods.add_method_mut("get_line_at", |_, editor, y: usize| { - editor.doc_mut().load_to(y); - let y = y.saturating_sub(1); - let line = editor.doc().line(y).unwrap_or_else(|| "".to_string()); - update_highlighter(editor); - Ok(line) - }); - methods.add_method_mut("move_to_document", |_, editor, id: usize| { - editor.ptr = id; - Ok(()) - }); - methods.add_method_mut("move_previous_match", |_, editor, query: String| { - editor.prev_match(&query); - update_highlighter(editor); - Ok(()) - }); - // DEPRECIATED - methods.add_method_mut("hide_help_message", |_, _, ()| Ok(())); - // DEPRECIATED - methods.add_method_mut("show_help_message", |_, _, ()| Ok(())); - methods.add_method_mut("set_read_only", |_, editor, status: bool| { - editor.doc_mut().read_only = status; - Ok(()) - }); - methods.add_method_mut("set_file_type", |_, editor, ext: String| { - let mut highlighter = editor - .config - .syntax_highlighting - .borrow() - .get_highlighter(&ext); - highlighter.run(&editor.doc().lines); - editor.highlighter[editor.ptr] = highlighter; - Ok(()) - }); - } -} - -pub struct LuaLoc { - x: usize, - y: usize, -} - -impl IntoLua<'_> for LuaLoc { - fn into_lua(self, lua: &Lua) -> std::result::Result, LuaError> { - let table = lua.create_table()?; - table.set("x", self.x)?; - table.set("y", self.y)?; - Ok(LuaValue::Table(table)) - } -} diff --git a/src/config/colors.rs b/src/config/colors.rs new file mode 100644 index 00000000..1c0fbf0b --- /dev/null +++ b/src/config/colors.rs @@ -0,0 +1,346 @@ +use crate::error::Result; +use crossterm::style::Color; +use mlua::prelude::*; + +use super::issue_warning; + +#[derive(Debug)] +pub struct Colors { + pub editor_bg: ConfigColor, + pub editor_fg: ConfigColor, + + pub status_bg: ConfigColor, + pub status_fg: ConfigColor, + + pub highlight: ConfigColor, + + pub line_number_fg: ConfigColor, + pub line_number_bg: ConfigColor, + + pub tab_active_fg: ConfigColor, + pub tab_active_bg: ConfigColor, + pub tab_inactive_fg: ConfigColor, + pub tab_inactive_bg: ConfigColor, + + pub info_bg: ConfigColor, + pub info_fg: ConfigColor, + pub warning_bg: ConfigColor, + pub warning_fg: ConfigColor, + pub error_bg: ConfigColor, + pub error_fg: ConfigColor, + + pub selection_fg: ConfigColor, + pub selection_bg: ConfigColor, +} + +impl Default for Colors { + fn default() -> Self { + Self { + editor_bg: ConfigColor::Black, + editor_fg: ConfigColor::Black, + + status_bg: ConfigColor::Black, + status_fg: ConfigColor::Black, + + highlight: ConfigColor::Black, + + line_number_fg: ConfigColor::Black, + line_number_bg: ConfigColor::Black, + + tab_active_fg: ConfigColor::Black, + tab_active_bg: ConfigColor::Black, + tab_inactive_fg: ConfigColor::Black, + tab_inactive_bg: ConfigColor::Black, + + info_bg: ConfigColor::Black, + info_fg: ConfigColor::Black, + warning_bg: ConfigColor::Black, + warning_fg: ConfigColor::Black, + error_bg: ConfigColor::Black, + error_fg: ConfigColor::Black, + + selection_fg: ConfigColor::White, + selection_bg: ConfigColor::Blue, + } + } +} + +impl LuaUserData for Colors { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("editor_bg", |env, this| Ok(this.editor_bg.to_lua(env))); + fields.add_field_method_get("editor_fg", |env, this| Ok(this.editor_fg.to_lua(env))); + fields.add_field_method_get("status_bg", |env, this| Ok(this.status_bg.to_lua(env))); + fields.add_field_method_get("status_fg", |env, this| Ok(this.status_fg.to_lua(env))); + fields.add_field_method_get("highlight", |env, this| Ok(this.highlight.to_lua(env))); + fields.add_field_method_get("line_number_bg", |env, this| { + Ok(this.line_number_bg.to_lua(env)) + }); + fields.add_field_method_get("line_number_fg", |env, this| { + Ok(this.line_number_fg.to_lua(env)) + }); + fields.add_field_method_get("tab_active_fg", |env, this| { + Ok(this.tab_active_fg.to_lua(env)) + }); + fields.add_field_method_get("tab_active_bg", |env, this| { + Ok(this.tab_active_bg.to_lua(env)) + }); + fields.add_field_method_get("tab_inactive_fg", |env, this| { + Ok(this.tab_inactive_fg.to_lua(env)) + }); + fields.add_field_method_get("tab_inactive_bg", |env, this| { + Ok(this.tab_inactive_bg.to_lua(env)) + }); + fields.add_field_method_get("error_bg", |env, this| Ok(this.error_bg.to_lua(env))); + fields.add_field_method_get("error_fg", |env, this| Ok(this.error_fg.to_lua(env))); + fields.add_field_method_get("warning_bg", |env, this| Ok(this.warning_bg.to_lua(env))); + fields.add_field_method_get("warning_fg", |env, this| Ok(this.warning_fg.to_lua(env))); + fields.add_field_method_get("info_bg", |env, this| Ok(this.info_bg.to_lua(env))); + fields.add_field_method_get("info_fg", |env, this| Ok(this.info_fg.to_lua(env))); + fields.add_field_method_get("selection_fg", |env, this| { + Ok(this.selection_fg.to_lua(env)) + }); + fields.add_field_method_get("selection_bg", |env, this| { + Ok(this.selection_bg.to_lua(env)) + }); + fields.add_field_method_set("editor_bg", |_, this, value| { + this.editor_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("editor_fg", |_, this, value| { + this.editor_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("status_bg", |_, this, value| { + this.status_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("status_fg", |_, this, value| { + this.status_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("highlight", |_, this, value| { + this.highlight = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("line_number_bg", |_, this, value| { + this.line_number_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("line_number_fg", |_, this, value| { + this.line_number_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("tab_active_fg", |_, this, value| { + this.tab_active_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("tab_active_bg", |_, this, value| { + this.tab_active_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("tab_inactive_fg", |_, this, value| { + this.tab_inactive_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("tab_inactive_bg", |_, this, value| { + this.tab_inactive_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("error_bg", |_, this, value| { + this.error_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("error_fg", |_, this, value| { + this.error_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("warning_bg", |_, this, value| { + this.warning_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("warning_fg", |_, this, value| { + this.warning_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("info_bg", |_, this, value| { + this.info_bg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("info_fg", |_, this, value| { + this.info_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("selection_fg", |_, this, value| { + this.selection_fg = ConfigColor::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("selection_bg", |_, this, value| { + this.selection_bg = ConfigColor::from_lua(value); + Ok(()) + }); + } +} + +#[derive(Debug)] +pub enum ConfigColor { + Rgb(u8, u8, u8), + Hex(String), + Black, + DarkGrey, + Red, + DarkRed, + Green, + DarkGreen, + Yellow, + DarkYellow, + Blue, + DarkBlue, + Magenta, + DarkMagenta, + Cyan, + DarkCyan, + White, + Grey, + Transparent, +} + +impl ConfigColor { + pub fn from_lua<'a>(value: LuaValue<'a>) -> Self { + match value { + LuaValue::String(string) => match string.to_str().unwrap_or("transparent") { + "black" => Self::Black, + "darkgrey" => Self::DarkGrey, + "red" => Self::Red, + "darkred" => Self::DarkRed, + "green" => Self::Green, + "darkgreen" => Self::DarkGreen, + "yellow" => Self::Yellow, + "darkyellow" => Self::DarkYellow, + "blue" => Self::Blue, + "darkblue" => Self::DarkBlue, + "magenta" => Self::Magenta, + "darkmagenta" => Self::DarkMagenta, + "cyan" => Self::Cyan, + "darkcyan" => Self::DarkCyan, + "white" => Self::White, + "grey" => Self::Grey, + "transparent" => Self::Transparent, + hex => Self::Hex(hex.to_string()), + }, + LuaValue::Table(table) => { + if table.len().unwrap_or(3) != 3 { + issue_warning("Invalid RGB sequence used in configuration file (must be a list of 3 numbers)"); + return Self::Transparent; + } + let mut tri: Vec = vec![]; + for _ in 0..3 { + if let Ok(val) = table.pop() { + tri.insert(0, val) + } else { + issue_warning("Invalid RGB sequence provided - please check your numerical values are between 0 and 255"); + tri.insert(0, 255); + } + } + Self::Rgb(tri[0], tri[1], tri[2]) + } + _ => { + issue_warning("Invalid data type used for colour in configuration file"); + Self::Transparent + } + } + } + + pub fn to_lua<'a>(&self, env: &'a Lua) -> LuaValue<'a> { + let msg = "Failed to create lua string"; + match self { + ConfigColor::Hex(hex) => { + let string = env.create_string(hex).expect(msg); + LuaValue::String(string) + } + ConfigColor::Rgb(r, g, b) => { + // Create lua table + let table = env.create_table().expect("Failed to create lua table"); + let _ = table.push(*r as isize); + let _ = table.push(*g as isize); + let _ = table.push(*b as isize); + LuaValue::Table(table) + } + ConfigColor::Black => LuaValue::String(env.create_string("black").expect(msg)), + ConfigColor::DarkGrey => LuaValue::String(env.create_string("darkgrey").expect(msg)), + ConfigColor::Red => LuaValue::String(env.create_string("red").expect(msg)), + ConfigColor::DarkRed => LuaValue::String(env.create_string("darkred").expect(msg)), + ConfigColor::Green => LuaValue::String(env.create_string("green").expect(msg)), + ConfigColor::DarkGreen => LuaValue::String(env.create_string("darkgreen").expect(msg)), + ConfigColor::Yellow => LuaValue::String(env.create_string("yellow").expect(msg)), + ConfigColor::DarkYellow => { + LuaValue::String(env.create_string("darkyellow").expect(msg)) + } + ConfigColor::Blue => LuaValue::String(env.create_string("blue").expect(msg)), + ConfigColor::DarkBlue => LuaValue::String(env.create_string("darkblue").expect(msg)), + ConfigColor::Magenta => LuaValue::String(env.create_string("magenta").expect(msg)), + ConfigColor::DarkMagenta => { + LuaValue::String(env.create_string("darkmagenta").expect(msg)) + } + ConfigColor::Cyan => LuaValue::String(env.create_string("cyan").expect(msg)), + ConfigColor::DarkCyan => LuaValue::String(env.create_string("darkcyan").expect(msg)), + ConfigColor::White => LuaValue::String(env.create_string("white").expect(msg)), + ConfigColor::Grey => LuaValue::String(env.create_string("grey").expect(msg)), + ConfigColor::Transparent => { + LuaValue::String(env.create_string("transparent").expect(msg)) + } + } + } + + pub fn to_color(&self) -> Result { + Ok(match self { + ConfigColor::Hex(hex) => { + let (r, g, b) = self.hex_to_rgb(hex)?; + Color::Rgb { r, g, b } + } + ConfigColor::Rgb(r, g, b) => Color::Rgb { + r: *r, + g: *g, + b: *b, + }, + ConfigColor::Black => Color::Black, + ConfigColor::DarkGrey => Color::DarkGrey, + ConfigColor::Red => Color::Red, + ConfigColor::DarkRed => Color::DarkRed, + ConfigColor::Green => Color::Green, + ConfigColor::DarkGreen => Color::DarkGreen, + ConfigColor::Yellow => Color::Yellow, + ConfigColor::DarkYellow => Color::DarkYellow, + ConfigColor::Blue => Color::Blue, + ConfigColor::DarkBlue => Color::DarkBlue, + ConfigColor::Magenta => Color::Magenta, + ConfigColor::DarkMagenta => Color::DarkMagenta, + ConfigColor::Cyan => Color::Cyan, + ConfigColor::DarkCyan => Color::DarkCyan, + ConfigColor::White => Color::White, + ConfigColor::Grey => Color::Grey, + ConfigColor::Transparent => Color::Reset, + }) + } + + fn hex_to_rgb(&self, hex: &str) -> Result<(u8, u8, u8)> { + // Remove the leading '#' if present + let hex = hex.trim_start_matches('#'); + + // Ensure the hex code is exactly 6 characters long + if hex.len() != 6 { + panic!("Invalid hex code used in configuration file - ensure they are of length 6"); + } + + // Parse the hex string into the RGB components + let mut tri: Vec = vec![]; + for i in 0..3 { + let section = &hex[(i * 2)..(i * 2 + 2)]; + if let Ok(val) = u8::from_str_radix(section, 16) { + tri.insert(0, val) + } else { + panic!("Invalid hex code used in configuration file - ensure all digits are between 0 and F"); + } + } + Ok((tri[0], tri[1], tri[2])) + } +} diff --git a/src/config/editor.rs b/src/config/editor.rs new file mode 100644 index 00000000..fea79b43 --- /dev/null +++ b/src/config/editor.rs @@ -0,0 +1,443 @@ +use crate::cli::VERSION; +use crate::editor::Editor; +use crate::ui::Feedback; +use kaolinite::Loc; +use mlua::prelude::*; + +impl LuaUserData for Editor { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("pasting", |_, editor| Ok(editor.paste_flag)); + fields.add_field_method_get("cursor", |_, editor| { + let loc = editor.doc().char_loc(); + Ok(LuaLoc { + x: loc.x, + y: loc.y + 1, + }) + }); + fields.add_field_method_get("document_name", |_, editor| { + let name = editor.doc().file_name.clone(); + Ok(name) + }); + fields.add_field_method_get("document_length", |_, editor| { + let len = editor.doc().len_lines(); + Ok(len) + }); + 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.doc.len())); + // DEPRECIATED + fields.add_field_method_get("help_visible", |_, _| Ok(false)); + fields.add_field_method_get("document_type", |_, editor| { + let ext = editor + .doc() + .file_name + .as_ref() + .and_then(|name| Some(name.split('.').last().unwrap_or(""))) + .unwrap_or(""); + let file_type = kaolinite::utils::filetype(ext); + Ok(file_type) + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Reload the configuration file + methods.add_method_mut("reload_config", |lua, editor, ()| { + if editor + .load_config(editor.config_path.clone(), &lua) + .is_err() + { + editor.feedback = Feedback::Error("Failed to reload config".to_string()); + } + Ok(()) + }); + // Display messages + methods.add_method_mut("display_error", |_, editor, message: String| { + editor.feedback = Feedback::Error(message); + Ok(()) + }); + methods.add_method_mut("display_warning", |_, editor, message: String| { + editor.feedback = Feedback::Warning(message); + Ok(()) + }); + methods.add_method_mut("display_info", |_, editor, message: String| { + editor.feedback = Feedback::Info(message); + Ok(()) + }); + // Prompt the user + methods.add_method_mut("prompt", |_, editor, question: String| { + Ok(editor + .prompt(question) + .unwrap_or_else(|_| "error".to_string())) + }); + // Edit commands (relative) + methods.add_method_mut("insert", |_, editor, text: String| { + for ch in text.chars() { + if let Err(err) = editor.character(ch) { + editor.feedback = Feedback::Error(err.to_string()); + } + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("remove", |_, editor, ()| { + if let Err(err) = editor.backspace() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("insert_line", |_, editor, ()| { + if let Err(err) = editor.enter() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("remove_line", |_, editor, ()| { + if let Err(err) = editor.delete_line() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + // Cursor moving + methods.add_method_mut("move_to", |_, editor, (x, y): (usize, usize)| { + let y = y.saturating_sub(1); + editor.doc_mut().move_to(&Loc { x, y }); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_up", |_, editor, ()| { + editor.up(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_down", |_, editor, ()| { + editor.down(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_left", |_, editor, ()| { + editor.left(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_right", |_, editor, ()| { + editor.right(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("select_up", |_, editor, ()| { + editor.select_up(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("select_down", |_, editor, ()| { + editor.select_down(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("select_left", |_, editor, ()| { + editor.select_left(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("select_right", |_, editor, ()| { + editor.select_right(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("select_all", |_, editor, ()| { + editor.select_all(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("cut", |_, editor, ()| { + if let Err(err) = editor.cut() { + editor.feedback = Feedback::Error(err.to_string()); + } else { + editor.feedback = Feedback::Info("Text cut to clipboard".to_owned()); + } + Ok(()) + }); + methods.add_method_mut("copy", |_, editor, ()| { + if let Err(err) = editor.copy() { + editor.feedback = Feedback::Error(err.to_string()); + } else { + editor.feedback = Feedback::Info("Text copied to clipboard".to_owned()); + } + Ok(()) + }); + methods.add_method_mut("move_home", |_, editor, ()| { + editor.doc_mut().move_home(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_end", |_, editor, ()| { + editor.doc_mut().move_end(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_page_up", |_, editor, ()| { + editor.doc_mut().move_page_up(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_page_down", |_, editor, ()| { + editor.doc_mut().move_page_down(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_top", |_, editor, ()| { + editor.doc_mut().move_top(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_bottom", |_, editor, ()| { + editor.doc_mut().move_bottom(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_previous_word", |_, editor, ()| { + editor.prev_word(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("move_next_word", |_, editor, ()| { + editor.next_word(); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut( + "insert_at", + |_, editor, (text, x, y): (String, usize, usize)| { + let y = y.saturating_sub(1); + let location = editor.doc_mut().char_loc(); + editor.doc_mut().move_to(&Loc { x, y }); + for ch in text.chars() { + if let Err(err) = editor.character(ch) { + editor.feedback = Feedback::Error(err.to_string()); + } + } + editor.doc_mut().move_to(&location); + update_highlighter(editor); + Ok(()) + }, + ); + methods.add_method_mut("remove_at", |_, editor, (x, y): (usize, usize)| { + let y = y.saturating_sub(1); + let location = editor.doc_mut().char_loc(); + editor.doc_mut().move_to(&Loc { x, y }); + if let Err(err) = editor.delete() { + editor.feedback = Feedback::Error(err.to_string()); + } + editor.doc_mut().move_to(&location); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("insert_line_at", |_, editor, (text, y): (String, usize)| { + let y = y.saturating_sub(1); + let location = editor.doc_mut().char_loc(); + if y < editor.doc().len_lines() { + editor.doc_mut().move_to_y(y); + editor.doc_mut().move_home(); + if let Err(err) = editor.enter() { + editor.feedback = Feedback::Error(err.to_string()); + } + editor.up(); + } else { + editor.doc_mut().move_bottom(); + if let Err(err) = editor.enter() { + editor.feedback = Feedback::Error(err.to_string()); + } + } + for ch in text.chars() { + if let Err(err) = editor.character(ch) { + editor.feedback = Feedback::Error(err.to_string()); + } + } + editor.doc_mut().move_to(&location); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("remove_line_at", |_, editor, y: usize| { + let y = y.saturating_sub(1); + let location = editor.doc_mut().char_loc(); + editor.doc_mut().move_to_y(y); + if let Err(err) = editor.delete_line() { + editor.feedback = Feedback::Error(err.to_string()); + } + editor.doc_mut().move_to(&location); + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("open_command_line", |_, editor, ()| { + match editor.prompt("Command") { + Ok(command) => { + editor.command = Some(command); + } + Err(err) => { + editor.feedback = Feedback::Error(err.to_string()); + } + } + Ok(()) + }); + methods.add_method_mut("previous_tab", |_, editor, ()| { + editor.prev(); + Ok(()) + }); + methods.add_method_mut("next_tab", |_, editor, ()| { + editor.next(); + Ok(()) + }); + methods.add_method_mut("new", |_, editor, ()| { + if let Err(err) = editor.new_document() { + editor.feedback = Feedback::Error(err.to_string()); + } + Ok(()) + }); + methods.add_method_mut("open", |_, editor, ()| { + if let Err(err) = editor.open_document() { + editor.feedback = Feedback::Error(err.to_string()); + } + Ok(()) + }); + methods.add_method_mut("save", |_, editor, ()| { + if let Err(err) = editor.save() { + editor.feedback = Feedback::Error(err.to_string()); + } + Ok(()) + }); + methods.add_method_mut("save_as", |_, editor, ()| { + if let Err(err) = editor.save_as() { + editor.feedback = Feedback::Error(err.to_string()); + } + Ok(()) + }); + methods.add_method_mut("save_all", |_, editor, ()| { + if let Err(err) = editor.save_all() { + editor.feedback = Feedback::Error(err.to_string()); + } + Ok(()) + }); + methods.add_method_mut("quit", |_, editor, ()| { + if let Err(err) = editor.quit() { + editor.feedback = Feedback::Error(err.to_string()); + } + Ok(()) + }); + methods.add_method_mut("undo", |_, editor, ()| { + if let Err(err) = editor.undo() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("redo", |_, editor, ()| { + if let Err(err) = editor.redo() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("search", |_, editor, ()| { + if let Err(err) = editor.search() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method_mut("replace", |_, editor, ()| { + if let Err(err) = editor.replace() { + editor.feedback = Feedback::Error(err.to_string()); + } + update_highlighter(editor); + Ok(()) + }); + methods.add_method("get_character", |_, editor, ()| { + let loc = editor.doc().char_loc(); + let ch = editor + .doc() + .line(loc.y) + .unwrap_or_else(|| "".to_string()) + .chars() + .nth(loc.x) + .and_then(|ch| Some(ch.to_string())) + .unwrap_or_else(|| "".to_string()); + Ok(ch) + }); + methods.add_method_mut("get_character_at", |_, editor, (x, y): (usize, usize)| { + editor.doc_mut().load_to(y); + let y = y.saturating_sub(1); + let ch = editor + .doc() + .line(y) + .unwrap_or_else(|| "".to_string()) + .chars() + .nth(x) + .and_then(|ch| Some(ch.to_string())) + .unwrap_or_else(|| "".to_string()); + update_highlighter(editor); + Ok(ch) + }); + methods.add_method("get_line", |_, editor, ()| { + let loc = editor.doc().char_loc(); + let line = editor.doc().line(loc.y).unwrap_or_else(|| "".to_string()); + Ok(line) + }); + methods.add_method_mut("get_line_at", |_, editor, y: usize| { + editor.doc_mut().load_to(y); + let y = y.saturating_sub(1); + let line = editor.doc().line(y).unwrap_or_else(|| "".to_string()); + update_highlighter(editor); + Ok(line) + }); + methods.add_method_mut("move_to_document", |_, editor, id: usize| { + editor.ptr = id; + Ok(()) + }); + methods.add_method_mut("move_previous_match", |_, editor, query: String| { + editor.prev_match(&query); + update_highlighter(editor); + Ok(()) + }); + // DEPRECIATED + methods.add_method_mut("hide_help_message", |_, _, ()| Ok(())); + // DEPRECIATED + methods.add_method_mut("show_help_message", |_, _, ()| Ok(())); + methods.add_method_mut("set_read_only", |_, editor, status: bool| { + editor.doc_mut().read_only = status; + Ok(()) + }); + methods.add_method_mut("set_file_type", |_, editor, ext: String| { + let mut highlighter = editor + .config + .syntax_highlighting + .borrow() + .get_highlighter(&ext); + highlighter.run(&editor.doc().lines); + editor.highlighter[editor.ptr] = highlighter; + Ok(()) + }); + } +} + +pub struct LuaLoc { + x: usize, + y: usize, +} + +impl IntoLua<'_> for LuaLoc { + fn into_lua(self, lua: &Lua) -> std::result::Result, LuaError> { + let table = lua.create_table()?; + table.set("x", self.x)?; + table.set("y", self.y)?; + Ok(LuaValue::Table(table)) + } +} + +fn update_highlighter(editor: &mut Editor) { + if let Err(err) = editor.update_highlighter() { + editor.feedback = Feedback::Error(err.to_string()); + } +} diff --git a/src/config/highlighting.rs b/src/config/highlighting.rs new file mode 100644 index 00000000..ba7b257e --- /dev/null +++ b/src/config/highlighting.rs @@ -0,0 +1,138 @@ +use crate::error::{OxError, Result}; +use mlua::prelude::*; +use std::collections::HashMap; +use synoptic::{from_extension, Highlighter}; + +use super::{Color, ConfigColor}; + +/// For storing configuration information related to syntax highlighting +#[derive(Debug)] +pub struct SyntaxHighlighting { + pub theme: HashMap, + pub user_rules: HashMap, +} + +impl Default for SyntaxHighlighting { + fn default() -> Self { + Self { + theme: HashMap::default(), + user_rules: HashMap::default(), + } + } +} + +impl SyntaxHighlighting { + /// Get a colour from the theme + pub fn get_theme(&self, name: &str) -> Result { + if let Some(col) = self.theme.get(name) { + col.to_color() + } else { + Err(OxError::Config(format!( + "{} has not been given a colour in the theme", + name + ))) + } + } + + /// Get a highlighter given a file extension + pub fn get_highlighter(&self, ext: &str) -> Highlighter { + self.user_rules + .get(ext) + .and_then(|h| Some(h.clone())) + .unwrap_or_else(|| from_extension(ext, 4).unwrap_or_else(|| Highlighter::new(4))) + } +} + +impl LuaUserData for SyntaxHighlighting { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut( + "keywords", + |lua, _, (name, pattern): (String, Vec)| { + let table = lua.create_table()?; + table.set("kind", "keyword")?; + table.set("name", name)?; + table.set("pattern", format!("({})", pattern.join("|")))?; + Ok(table) + }, + ); + methods.add_method_mut("keyword", |lua, _, (name, pattern): (String, String)| { + let table = lua.create_table()?; + table.set("kind", "keyword")?; + table.set("name", name)?; + table.set("pattern", pattern)?; + Ok(table) + }); + methods.add_method_mut( + "bounded", + |lua, _, (name, start, end, escape): (String, String, String, bool)| { + let table = lua.create_table()?; + table.set("kind", "bounded")?; + table.set("name", name)?; + table.set("start", start)?; + table.set("end", end)?; + table.set("escape", escape.to_string())?; + Ok(table) + }, + ); + type BoundedInterpArgs = (String, String, String, String, String, bool); + methods.add_method_mut( + "bounded_interpolation", + |lua, _, (name, start, end, i_start, i_end, escape): BoundedInterpArgs| { + let table = lua.create_table()?; + table.set("kind", "bounded_interpolation")?; + table.set("name", name)?; + table.set("start", start)?; + table.set("end", end)?; + table.set("i_start", i_start)?; + table.set("i_end", i_end)?; + table.set("escape", escape.to_string())?; + Ok(table) + }, + ); + methods.add_method_mut( + "new", + |_, syntax_highlighting, (extensions, rules): (LuaTable, LuaTable)| { + // Make note of the highlighter + for ext_idx in 1..(extensions.len()? + 1) { + // Create highlighter + let mut highlighter = Highlighter::new(4); + // Add rules one by one + for rule_idx in 1..(rules.len()? + 1) { + // Get rule + let rule = rules.get::>(rule_idx)?; + // Find type of rule and attatch it to the highlighter + match rule["kind"].as_str() { + "keyword" => { + highlighter.keyword(rule["name"].clone(), &rule["pattern"]) + } + "bounded" => highlighter.bounded( + rule["name"].clone(), + rule["start"].clone(), + rule["end"].clone(), + rule["escape"] == "true", + ), + "bounded_interpolation" => highlighter.bounded_interp( + rule["name"].clone(), + rule["start"].clone(), + rule["end"].clone(), + rule["i_start"].clone(), + rule["i_end"].clone(), + rule["escape"] == "true", + ), + _ => unreachable!(), + } + } + let ext = extensions.get::(ext_idx)?; + syntax_highlighting.user_rules.insert(ext, highlighter); + } + Ok(()) + }, + ); + methods.add_method_mut("set", |_, syntax_highlighting, (name, value)| { + syntax_highlighting + .theme + .insert(name, ConfigColor::from_lua(value)); + Ok(()) + }); + } +} diff --git a/src/config/interface.rs b/src/config/interface.rs new file mode 100644 index 00000000..a067e70d --- /dev/null +++ b/src/config/interface.rs @@ -0,0 +1,396 @@ +use crate::cli::VERSION; +use crate::editor::Editor; +use crate::error::Result; +use crossterm::style::SetForegroundColor as Fg; +use kaolinite::searching::Searcher; +use kaolinite::utils::{filetype, get_absolute_path, get_file_ext, get_file_name, icon}; +use kaolinite::Document; +use mlua::prelude::*; + +use super::{issue_warning, Colors}; + +/// For storing general configuration related to the terminal functionality +#[derive(Debug)] +pub struct TerminalConfig { + pub mouse_enabled: bool, +} + +impl Default for TerminalConfig { + fn default() -> Self { + Self { + mouse_enabled: true, + } + } +} + +impl LuaUserData for TerminalConfig { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("mouse_enabled", |_, this| Ok(this.mouse_enabled)); + fields.add_field_method_set("mouse_enabled", |_, this, value| { + this.mouse_enabled = value; + Ok(()) + }); + } +} + +/// For storing configuration information related to line numbers +#[derive(Debug)] +pub struct LineNumbers { + pub enabled: bool, + pub padding_left: usize, + pub padding_right: usize, +} + +impl Default for LineNumbers { + fn default() -> Self { + Self { + enabled: true, + padding_left: 1, + padding_right: 1, + } + } +} + +impl LuaUserData for LineNumbers { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); + fields.add_field_method_set("enabled", |_, this, value| { + this.enabled = value; + Ok(()) + }); + fields.add_field_method_get("padding_left", |_, this| Ok(this.padding_left)); + fields.add_field_method_set("padding_left", |_, this, value| { + this.padding_left = value; + Ok(()) + }); + fields.add_field_method_get("padding_right", |_, this| Ok(this.padding_right)); + fields.add_field_method_set("padding_right", |_, this, value| { + this.padding_right = value; + Ok(()) + }); + } +} + +/// For storing configuration information related to the greeting message +#[derive(Debug)] +pub struct GreetingMessage { + pub enabled: bool, + pub format: String, +} + +impl Default for GreetingMessage { + fn default() -> Self { + Self { + enabled: true, + format: "".to_string(), + } + } +} + +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(); + let mut result = self.format.clone(); + result = result.replace("{version}", &VERSION).to_string(); + result = result.replace("{highlight_start}", &highlight).to_string(); + result = result.replace("{highlight_end}", &editor_fg).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) { + let name = m + .text + .chars() + .skip(1) + .take(m.text.chars().count().saturating_sub(2)) + .collect::(); + if let Ok(func) = lua.globals().get::(name) { + if let Ok(r) = func.call::<(), LuaString>(()) { + result = result.replace(&m.text, r.to_str().unwrap_or("")); + } else { + break; + } + } else { + break; + } + } + Ok(result) + } +} + +impl LuaUserData for GreetingMessage { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); + fields.add_field_method_set("enabled", |_, this, value| { + this.enabled = value; + Ok(()) + }); + fields.add_field_method_get("format", |_, this| Ok(this.format.clone())); + fields.add_field_method_set("format", |_, this, value| { + this.format = value; + Ok(()) + }); + } +} + +/// For storing configuration information related to the help message +#[derive(Debug)] +pub struct HelpMessage { + pub enabled: bool, + pub format: String, +} + +impl Default for HelpMessage { + fn default() -> Self { + Self { + enabled: true, + format: "".to_string(), + } + } +} + +impl HelpMessage { + /// Take the configuration information and render the help 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(); + let mut result = self.format.clone(); + result = result.replace("{version}", &VERSION).to_string(); + result = result.replace("{highlight_start}", &highlight).to_string(); + result = result.replace("{highlight_end}", &editor_fg).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) { + let name = m + .text + .chars() + .skip(1) + .take(m.text.chars().count().saturating_sub(2)) + .collect::(); + if let Ok(func) = lua.globals().get::(name) { + if let Ok(r) = func.call::<(), LuaString>(()) { + result = result.replace(&m.text, r.to_str().unwrap_or("")); + } else { + break; + } + } else { + break; + } + } + Ok(result.split('\n').map(|l| l.to_string()).collect()) + } +} + +impl LuaUserData for HelpMessage { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); + fields.add_field_method_set("enabled", |_, this, value| { + this.enabled = value; + Ok(()) + }); + fields.add_field_method_get("format", |_, this| Ok(this.format.clone())); + fields.add_field_method_set("format", |_, this, value| { + this.format = value; + Ok(()) + }); + } +} + +/// For storing configuration information related to the status line +#[derive(Debug)] +pub struct TabLine { + pub enabled: bool, + pub format: String, +} + +impl Default for TabLine { + fn default() -> Self { + Self { + enabled: true, + format: " {file_name}{modified} ".to_string(), + } + } +} + +impl TabLine { + pub fn render(&self, document: &Document) -> String { + let path = document + .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 icon = icon(&filetype(&file_extension).unwrap_or_else(|| "".to_string())); + let modified = if document.modified { "[+]" } else { "" }; + let mut result = self.format.clone(); + result = result + .replace("{file_extension}", &file_extension) + .to_string(); + result = result.replace("{file_name}", &file_name).to_string(); + result = result + .replace("{absolute_path}", &absolute_path) + .to_string(); + result = result.replace("{path}", &path).to_string(); + result = result.replace("{modified}", &modified).to_string(); + result = result.replace("{icon}", &icon).to_string(); + result + } +} + +impl LuaUserData for TabLine { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("enabled", |_, this| Ok(this.enabled)); + fields.add_field_method_set("enabled", |_, this, value| { + this.enabled = value; + Ok(()) + }); + fields.add_field_method_get("format", |_, this| Ok(this.format.clone())); + fields.add_field_method_set("format", |_, this, value| { + this.format = value; + Ok(()) + }); + } +} + +/// For storing configuration information related to the status line +#[derive(Debug)] +pub struct StatusLine { + pub parts: Vec, + pub alignment: StatusAlign, +} + +impl Default for StatusLine { + fn default() -> Self { + Self { + parts: vec![], + alignment: StatusAlign::Between, + } + } +} + +impl StatusLine { + pub fn render(&self, editor: &Editor, lua: &Lua, w: usize) -> String { + let mut result = vec![]; + let path = editor + .doc() + .file_name + .to_owned() + .unwrap_or_else(|| "[No Name]".to_string()); + let file_extension = get_file_ext(&path).unwrap_or_else(|| "".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 = filetype(&file_extension).unwrap_or_else(|| { + if file_extension.is_empty() { + "Unknown".to_string() + } else { + file_extension.to_string() + } + }); + let icon = icon(&filetype(&file_extension).unwrap_or_else(|| "".to_string())); + let modified = if editor.doc().modified { "[+]" } 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(); + + for part in &self.parts { + let mut part = part.clone(); + part = part.replace("{file_name}", &file_name).to_string(); + part = part + .replace("{file_extension}", &file_extension) + .to_string(); + part = part.replace("{icon}", &icon).to_string(); + part = part.replace("{path}", &path).to_string(); + part = part.replace("{absolute_path}", &absolute_path).to_string(); + part = part.replace("{modified}", &modified).to_string(); + part = part.replace("{file_type}", &file_type).to_string(); + part = part.replace("{cursor_y}", &cursor_y).to_string(); + part = part.replace("{cursor_x}", &cursor_x).to_string(); + part = part.replace("{line_count}", &line_count).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(&part) { + let name = m + .text + .chars() + .skip(1) + .take(m.text.chars().count().saturating_sub(2)) + .collect::(); + if let Ok(func) = lua.globals().get::(name) { + if let Ok(r) = func.call::<(), LuaString>(()) { + part = part.replace(&m.text, r.to_str().unwrap_or("")); + } else { + break; + } + } else { + break; + } + } + result.push(part); + } + let status: Vec<&str> = result.iter().map(|s| s.as_str()).collect(); + match self.alignment { + StatusAlign::Between => alinio::align::between(status.as_slice(), w), + StatusAlign::Around => alinio::align::around(status.as_slice(), w), + } + .unwrap_or_else(|| "".to_string()) + } +} + +impl LuaUserData for StatusLine { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("clear", |_, status_line, ()| { + status_line.parts.clear(); + Ok(()) + }); + methods.add_method_mut("add_part", |_, status_line, part| { + status_line.parts.push(part); + Ok(()) + }); + } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("alignment", |_, this| { + let alignment: String = this.alignment.clone().into(); + Ok(alignment) + }); + fields.add_field_method_set("alignment", |_, this, value| { + this.alignment = StatusAlign::from_string(value); + Ok(()) + }); + } +} + +#[derive(Debug, Clone)] +pub enum StatusAlign { + Around, + Between, +} + +impl StatusAlign { + pub fn from_string(string: String) -> Self { + match string.as_str() { + "around" => Self::Around, + "between" => Self::Between, + _ => { + issue_warning( + "\ + Invalid status line alignment used in configuration file - \ + make sure value is either 'around' or 'between' (defaulting to 'between')", + ); + Self::Between + } + } + } +} + +impl Into for StatusAlign { + fn into(self) -> String { + match self { + Self::Around => "around", + Self::Between => "between", + } + .to_string() + } +} diff --git a/src/config/keys.rs b/src/config/keys.rs new file mode 100644 index 00000000..636c8535 --- /dev/null +++ b/src/config/keys.rs @@ -0,0 +1,111 @@ +use crossterm::event::{KeyCode as KCode, KeyModifiers as KMod, MediaKeyCode, ModifierKeyCode}; + +/// This contains the code for running code after a key binding is pressed +pub fn run_key(key: &str) -> String { + format!( + " + globalevent = (global_event_mapping[\"*\"] or {{}}) + for _, f in ipairs(globalevent) do + f() + end + key = (global_event_mapping[\"{key}\"] or error(\"key not bound\")) + for _, f in ipairs(key) do + f() + end + " + ) +} + +/// This contains the code for running code before a key binding is fully processed +pub fn run_key_before(key: &str) -> String { + format!( + " + globalevent = (global_event_mapping[\"before:*\"] or {{}}) + for _, f in ipairs(globalevent) do + f() + end + key = (global_event_mapping[\"before:{key}\"] or {{}}) + for _, f in ipairs(key) do + f() + end + " + ) +} + +/// Converts a key taken from a crossterm event into string format +pub fn key_to_string(modifiers: KMod, key: KCode) -> String { + let mut result = "".to_string(); + // Deal with modifiers + if modifiers.contains(KMod::CONTROL) { + result += "ctrl_"; + } + if modifiers.contains(KMod::ALT) { + result += "alt_"; + } + if modifiers.contains(KMod::SHIFT) { + result += "shift_"; + } + result += &match key { + KCode::Char('\\') => "\\\\".to_string(), + KCode::Char('"') => "\\\"".to_string(), + KCode::Backspace => "backspace".to_string(), + KCode::Enter => "enter".to_string(), + KCode::Left => "left".to_string(), + KCode::Right => "right".to_string(), + KCode::Up => "up".to_string(), + KCode::Down => "down".to_string(), + KCode::Home => "home".to_string(), + KCode::End => "end".to_string(), + KCode::PageUp => "pageup".to_string(), + KCode::PageDown => "pagedown".to_string(), + KCode::Tab => "tab".to_string(), + KCode::BackTab => "backtab".to_string(), + KCode::Delete => "delete".to_string(), + KCode::Insert => "insert".to_string(), + KCode::F(num) => format!("f{num}"), + KCode::Char(ch) => format!("{}", ch.to_lowercase()), + KCode::Null => "null".to_string(), + KCode::Esc => "esc".to_string(), + KCode::CapsLock => "capslock".to_string(), + KCode::ScrollLock => "scrolllock".to_string(), + KCode::NumLock => "numlock".to_string(), + KCode::PrintScreen => "printscreen".to_string(), + KCode::Pause => "pause".to_string(), + KCode::Menu => "menu".to_string(), + KCode::KeypadBegin => "keypadbegin".to_string(), + KCode::Media(key) => match key { + MediaKeyCode::Play => "play", + MediaKeyCode::Pause => "pause", + MediaKeyCode::PlayPause => "playpause", + MediaKeyCode::Reverse => "reverse", + MediaKeyCode::Stop => "stop", + MediaKeyCode::FastForward => "fastforward", + MediaKeyCode::TrackNext => "next", + MediaKeyCode::TrackPrevious => "previous", + MediaKeyCode::Record => "record", + MediaKeyCode::Rewind => "rewind", + MediaKeyCode::LowerVolume => "lowervolume", + MediaKeyCode::RaiseVolume => "raisevolume", + MediaKeyCode::MuteVolume => "mutevolume", + } + .to_string(), + KCode::Modifier(key) => match key { + ModifierKeyCode::LeftShift => "lshift", + ModifierKeyCode::LeftControl => "lctrl", + ModifierKeyCode::LeftAlt => "lalt", + ModifierKeyCode::LeftSuper => "lsuper", + ModifierKeyCode::LeftHyper => "lhyper", + ModifierKeyCode::LeftMeta => "lmeta", + ModifierKeyCode::RightControl => "rctrl", + ModifierKeyCode::RightAlt => "ralt", + ModifierKeyCode::RightSuper => "rsuper", + ModifierKeyCode::RightHyper => "rhyper", + ModifierKeyCode::RightMeta => "rmeta", + ModifierKeyCode::RightShift => "rshift", + ModifierKeyCode::IsoLevel3Shift => "iso3shift", + ModifierKeyCode::IsoLevel5Shift => "iso5shift", + } + .to_string(), + }; + return result; +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 00000000..f47e5e69 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,197 @@ +use crate::error::{OxError, Result}; +use crossterm::style::Color; +use mlua::prelude::*; +use std::collections::HashMap; +use std::{cell::RefCell, rc::Rc}; + +mod colors; +mod editor; +mod highlighting; +mod interface; +mod keys; + +pub use colors::{Colors, ConfigColor}; +pub use highlighting::SyntaxHighlighting; +pub use interface::{ + GreetingMessage, HelpMessage, LineNumbers, StatusLine, TabLine, TerminalConfig, +}; +pub use keys::{key_to_string, run_key, run_key_before}; + +// Issue a warning to the user +fn issue_warning(msg: &str) { + eprintln!("[WARNING] {}", msg); +} + +/// This contains the default configuration lua file +const DEFAULT_CONFIG: &str = include_str!("../../config/.oxrc"); + +/// Default plug-in code to use +const PAIRS: &str = include_str!("../../plugins/pairs.lua"); +const AUTOINDENT: &str = include_str!("../../plugins/autoindent.lua"); + +/// This contains the code for setting up plug-in infrastructure +pub const PLUGIN_BOOTSTRAP: &str = include_str!("../plugin/bootstrap.lua"); + +/// This contains the code for running the plugins +pub const PLUGIN_RUN: &str = include_str!("../plugin/run.lua"); + +/// The struct that holds all the configuration information +#[derive(Debug)] +pub struct Config { + pub syntax_highlighting: Rc>, + pub line_numbers: Rc>, + pub colors: Rc>, + pub status_line: Rc>, + pub tab_line: Rc>, + pub greeting_message: Rc>, + pub help_message: Rc>, + pub terminal: Rc>, + pub document: Rc>, +} + +impl Config { + /// Take a lua instance, inject all the configuration tables and return a default config struct + pub fn new(lua: &Lua) -> Result { + // Set up structs to populate (the default values will be thrown away) + let syntax_highlighting = Rc::new(RefCell::new(SyntaxHighlighting::default())); + let line_numbers = Rc::new(RefCell::new(LineNumbers::default())); + let greeting_message = Rc::new(RefCell::new(GreetingMessage::default())); + let help_message = Rc::new(RefCell::new(HelpMessage::default())); + let colors = Rc::new(RefCell::new(Colors::default())); + let status_line = Rc::new(RefCell::new(StatusLine::default())); + let tab_line = Rc::new(RefCell::new(TabLine::default())); + let terminal = Rc::new(RefCell::new(TerminalConfig::default())); + let document = Rc::new(RefCell::new(DocumentConfig::default())); + + // Push in configuration globals + lua.globals().set("syntax", syntax_highlighting.clone())?; + lua.globals().set("line_numbers", line_numbers.clone())?; + lua.globals() + .set("greeting_message", greeting_message.clone())?; + lua.globals().set("help_message", help_message.clone())?; + lua.globals().set("status_line", status_line.clone())?; + lua.globals().set("tab_line", tab_line.clone())?; + lua.globals().set("colors", colors.clone())?; + lua.globals().set("terminal", terminal.clone())?; + lua.globals().set("document", document.clone())?; + + Ok(Config { + syntax_highlighting, + line_numbers, + greeting_message, + help_message, + tab_line, + status_line, + colors, + terminal, + document, + }) + } + + /// Actually take the configuration file, open it and interpret it + pub fn read(&mut self, path: String, lua: &Lua) -> Result<()> { + // Load the default config to start with + lua.load(DEFAULT_CONFIG).exec()?; + // Reset plugin status based on built-in configuration file + lua.load("plugins = {}").exec()?; + lua.load("builtins = {}").exec()?; + + // Judge pre-user config state + let status_parts = self.status_line.borrow().parts.len(); + + // Attempt to read config file from home directory + let mut user_provided_config = false; + if let Ok(path) = shellexpand::full(&path) { + if let Ok(config) = std::fs::read_to_string(path.to_string()) { + // Update configuration with user-defined values + lua.load(config).exec()?; + user_provided_config = true; + } + } + + // Remove any default values if necessary + if self.status_line.borrow().parts.len() > status_parts { + self.status_line.borrow_mut().parts.drain(0..status_parts); + } + + // Determine whether or not to load built-in plugins + let mut builtins: HashMap<&str, &str> = HashMap::default(); + builtins.insert("pairs.lua", PAIRS); + builtins.insert("autoindent.lua", AUTOINDENT); + for (name, code) in builtins.iter() { + if self.load_bi(name, user_provided_config, &lua) { + lua.load(*code).exec()?; + } + } + + if user_provided_config { + Ok(()) + } else { + Err(OxError::Config("Not Found".to_string())) + } + } + + /// Decide whether to load a built-in plugin + pub fn load_bi(&self, name: &str, user_provided_config: bool, lua: &Lua) -> bool { + if !user_provided_config { + // Load when the user hasn't provided a configuration file + true + } else { + // Get list of user-loaded plug-ins + let plugins: Vec = lua + .globals() + .get::<_, LuaTable>("builtins") + .unwrap() + .sequence_values() + .filter_map(std::result::Result::ok) + .collect(); + // If the user wants to load the plug-in but it isn't available + if let Some(idx) = plugins.iter().position(|p| p.ends_with(name)) { + // User wants the plug-in + let path = &plugins[idx]; + // true if plug-in isn't avilable + !std::path::Path::new(path).exists() + } else { + // User doesn't want the plug-in + false + } + } + } +} + +#[derive(Debug)] +pub struct DocumentConfig { + pub tab_width: usize, + pub undo_period: usize, + pub wrap_cursor: bool, +} + +impl Default for DocumentConfig { + fn default() -> Self { + Self { + tab_width: 4, + undo_period: 10, + wrap_cursor: true, + } + } +} + +impl LuaUserData for DocumentConfig { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("tab_width", |_, document| Ok(document.tab_width)); + fields.add_field_method_set("tab_width", |_, this, value| { + this.tab_width = value; + Ok(()) + }); + fields.add_field_method_get("undo_period", |_, document| Ok(document.undo_period)); + fields.add_field_method_set("undo_period", |_, this, value| { + this.undo_period = value; + Ok(()) + }); + fields.add_field_method_get("wrap_cursor", |_, document| Ok(document.wrap_cursor)); + fields.add_field_method_set("wrap_cursor", |_, this, value| { + this.wrap_cursor = value; + Ok(()) + }); + } +} From 68970d226db974314598c2874ad01c417a847dc1 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:15:09 +0100 Subject: [PATCH 10/31] added bracketed paste to speed up pasting and avoid plug-in side effects --- src/config/editor.rs | 1 - src/editor/mod.rs | 16 ++++++++++------ src/ui.rs | 23 ++++++++++++++++++----- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index fea79b43..8b67a6be 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -6,7 +6,6 @@ use mlua::prelude::*; impl LuaUserData for Editor { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("pasting", |_, editor| Ok(editor.paste_flag)); fields.add_field_method_get("cursor", |_, editor| { let loc = editor.doc().char_loc(); Ok(LuaLoc { diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 26099bb7..b8e7c1f2 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -43,8 +43,6 @@ pub struct Editor { push_down: usize, /// Used to cache the location of the configuration file pub config_path: String, - /// This is a handy place to figure out if the user is currently pasting something or not - pub paste_flag: bool, } impl Editor { @@ -65,7 +63,6 @@ impl Editor { last_active: Instant::now(), push_down: 1, config_path: "~/.oxrc".to_string(), - paste_flag: false, }) } @@ -297,6 +294,7 @@ impl Editor { CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, CEvent::Resize(w, h) => self.handle_resize(w, h), CEvent::Mouse(mouse_event) => self.handle_mouse_event(mouse_event), + CEvent::Paste(text) => self.handle_paste(text)?, _ => (), } Ok(()) @@ -307,11 +305,10 @@ impl Editor { // Check period of inactivity let end = Instant::now(); let inactivity = end.duration_since(self.last_active).as_millis() as usize; + // Commit if over user-defined period of inactivity if inactivity > self.config.document.borrow().undo_period * 1000 { self.doc_mut().commit(); } - // Predict whether the user is currently pasting text (based on rapid activity) - self.paste_flag = inactivity < 5; // Register this activity self.last_active = Instant::now(); // Editing - these key bindings can't be modified (only added to)! @@ -324,7 +321,6 @@ impl Editor { (KMod::NONE, KCode::Enter) => self.enter()?, _ => (), } - // Check user-defined key combinations (includes defaults if not modified) Ok(()) } @@ -337,4 +333,12 @@ impl Editor { let max = self.doc().offset.x + self.doc().size.h; self.doc_mut().load_to(max + 1); } + + /// Handle paste + pub fn handle_paste(&mut self, text: String) -> Result<()> { + for ch in text.chars() { + self.character(ch)?; + } + Ok(()) + } } diff --git a/src/ui.rs b/src/ui.rs index 7ccd0ae2..1e04619c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,8 +4,8 @@ use base64::prelude::*; use crossterm::{ cursor::{Hide, MoveTo, Show}, event::{ - DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags, - PushKeyboardEnhancementFlags, + DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, + KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }, execute, style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, @@ -95,14 +95,22 @@ impl Terminal { pub fn start(&mut self) -> Result<()> { std::panic::set_hook(Box::new(|e| { terminal::disable_raw_mode().unwrap(); - execute!(stdout(), LeaveAlternateScreen, Show, DisableMouseCapture).unwrap(); + execute!( + stdout(), + LeaveAlternateScreen, + Show, + DisableMouseCapture, + EnableBracketedPaste + ) + .unwrap(); eprintln!("{}", e); })); execute!( self.stdout, EnterAlternateScreen, Clear(ClType::All), - DisableLineWrap + DisableLineWrap, + EnableBracketedPaste, )?; if self.config.borrow().mouse_enabled { execute!(self.stdout, EnableMouseCapture)?; @@ -119,7 +127,12 @@ impl Terminal { pub fn end(&mut self) -> Result<()> { self.show_cursor()?; terminal::disable_raw_mode()?; - execute!(self.stdout, LeaveAlternateScreen, EnableLineWrap)?; + execute!( + self.stdout, + LeaveAlternateScreen, + EnableLineWrap, + DisableBracketedPaste + )?; if self.config.borrow().mouse_enabled { execute!(self.stdout, DisableMouseCapture)?; } From c47928d6b1a2aa96a06d8ec062cc0c110ef1d6e5 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:48:26 +0100 Subject: [PATCH 11/31] ran clippy for cleaner and better code quality --- kaolinite/src/document.rs | 167 +++++++++++++++------------ kaolinite/src/event.rs | 15 +-- kaolinite/src/lib.rs | 4 +- kaolinite/src/utils.rs | 30 ++--- kaolinite/tests/test.rs | 24 ++-- src/cli.rs | 41 +++---- src/config/colors.rs | 225 ++++++++++++++++++------------------- src/config/editor.rs | 105 ++++++++--------- src/config/highlighting.rs | 41 +++---- src/config/interface.rs | 61 +++++----- src/config/keys.rs | 4 +- src/config/mod.rs | 45 ++++---- src/editor/editing.rs | 12 +- src/editor/interface.rs | 26 ++--- src/editor/mod.rs | 46 ++++---- src/editor/mouse.rs | 17 ++- src/editor/scanning.rs | 14 +-- src/main.rs | 92 +++++++-------- src/ui.rs | 34 +++--- test.sh | 2 +- 20 files changed, 497 insertions(+), 508 deletions(-) diff --git a/kaolinite/src/document.rs b/kaolinite/src/document.rs index d8cf966d..ae3afd3f 100644 --- a/kaolinite/src/document.rs +++ b/kaolinite/src/document.rs @@ -10,6 +10,19 @@ use std::fs::File; use std::io::{BufReader, BufWriter}; use std::ops::{Range, RangeBounds}; +/// A document info struct to store information about the file it represents +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct DocumentInfo { + /// Whether or not the document can be edited + pub read_only: bool, + /// Flag for an EOL + pub eol: bool, + /// true if the file has been modified since saving, false otherwise + pub modified: bool, + /// Contains the number of lines buffered into the document + pub loaded_to: usize, +} + /// A document struct manages a file. /// It has tools to read, write and traverse a document. /// By default, it uses file buffering so it can open almost immediately. @@ -21,10 +34,10 @@ pub struct Document { pub file_name: Option, /// The rope of the document to facilitate reading and writing to disk pub file: Rope, - /// Contains the number of lines buffered into the document - pub loaded_to: usize, /// Cache of all the loaded lines in this document pub lines: Vec, + /// Stores information about the underlying file + pub info: DocumentInfo, /// Stores the locations of double width characters pub dbl_map: CharMap, /// Stores the locations of tab characters @@ -39,42 +52,39 @@ pub struct Document { pub char_ptr: usize, /// Manages events, for the purpose of undo and redo pub undo_mgmt: UndoMgmt, - /// true if the file has been modified since saving, false otherwise - pub modified: bool, - /// The number of spaces a tab should be rendered as - pub tab_width: usize, - /// Whether or not the document can be edited - pub read_only: bool, /// Storage of the old cursor x position (to snap back to) pub old_cursor: usize, /// Flag for if the editor is currently in a redo action pub in_redo: bool, - /// Flag for an EOL - pub eol: bool, + /// The number of spaces a tab should be rendered as + pub tab_width: usize, } impl Document { /// Creates a new, empty document with no file name. #[cfg(not(tarpaulin_include))] + #[must_use] pub fn new(size: Size) -> Self { let mut this = Self { file: Rope::from_str("\n"), - lines: vec!["".to_string()], + lines: vec![String::new()], dbl_map: CharMap::default(), tab_map: CharMap::default(), - loaded_to: 1, file_name: None, cursor: Cursor::default(), offset: Loc::default(), size, char_ptr: 0, undo_mgmt: UndoMgmt::default(), - modified: false, tab_width: 4, - read_only: false, old_cursor: 0, in_redo: false, - eol: false, + info: DocumentInfo { + loaded_to: 1, + eol: false, + read_only: false, + modified: false, + }, }; this.undo_mgmt.undo.push(this.take_snapshot()); this.undo_mgmt.saved(); @@ -91,24 +101,26 @@ impl Document { let file_name = file_name.into(); let file = Rope::from_reader(BufReader::new(File::open(&file_name)?))?; let mut this = Self { - eol: !file - .line(file.len_lines().saturating_sub(1)) - .to_string() - .is_empty(), + info: DocumentInfo { + loaded_to: 0, + eol: !file + .line(file.len_lines().saturating_sub(1)) + .to_string() + .is_empty(), + read_only: false, + modified: false, + }, file, lines: vec![], dbl_map: CharMap::default(), tab_map: CharMap::default(), - loaded_to: 0, file_name: Some(file_name), cursor: Cursor::default(), offset: Loc::default(), size, char_ptr: 0, undo_mgmt: UndoMgmt::default(), - modified: false, tab_width: 4, - read_only: false, old_cursor: 0, in_redo: false, }; @@ -127,18 +139,16 @@ impl Document { /// Returns an error if the file fails to write, due to permissions /// or character set issues. pub fn save(&mut self) -> Result<()> { - if !self.read_only { - if let Some(file_name) = &self.file_name { - self.file - .write_to(BufWriter::new(File::create(file_name)?))?; - self.undo_mgmt.saved(); - self.modified = false; - Ok(()) - } else { - Err(Error::NoFileName) - } - } else { + if self.info.read_only { Err(Error::ReadOnlyFile) + } else if let Some(file_name) = &self.file_name { + self.file + .write_to(BufWriter::new(File::create(file_name)?))?; + self.undo_mgmt.saved(); + self.info.modified = false; + Ok(()) + } else { + Err(Error::NoFileName) } } @@ -147,12 +157,12 @@ impl Document { /// Returns an error if the file fails to write, due to permissions /// or character set issues. pub fn save_as(&self, file_name: &str) -> Result<()> { - if !self.read_only { + if self.info.read_only { + Err(Error::ReadOnlyFile) + } else { self.file .write_to(BufWriter::new(File::create(file_name)?))?; Ok(()) - } else { - Err(Error::ReadOnlyFile) } } @@ -161,7 +171,7 @@ impl Document { /// # Errors /// Will return an error if the event was unable to be completed. pub fn exe(&mut self, ev: Event) -> Result<()> { - if !self.read_only { + if !self.info.read_only { self.undo_mgmt.set_dirty(); self.forth(ev)?; } @@ -175,10 +185,10 @@ impl Document { pub fn undo(&mut self) -> Result<()> { if let Some(s) = self.undo_mgmt.undo(self.take_snapshot()) { self.apply_snapshot(s); - self.modified = true; + self.info.modified = true; } if self.undo_mgmt.at_file() { - self.modified = false; + self.info.modified = false; } Ok(()) } @@ -189,10 +199,10 @@ impl Document { pub fn redo(&mut self) -> Result<()> { if let Some(s) = self.undo_mgmt.redo() { self.apply_snapshot(s); - self.modified = true; + self.info.modified = true; } if self.undo_mgmt.at_file() { - self.modified = false; + self.info.modified = false; } Ok(()) } @@ -212,6 +222,7 @@ impl Document { } /// Takes a loc and converts it into a char index for ropey + #[must_use] pub fn loc_to_file_pos(&self, loc: &Loc) -> usize { self.file.line_to_char(loc.y) + loc.x } @@ -221,7 +232,7 @@ impl Document { /// Returns an error if location is out of range. pub fn insert(&mut self, loc: &Loc, st: &str) -> Result<()> { self.out_of_range(loc.x, loc.y)?; - self.modified = true; + self.info.modified = true; // Move cursor to location self.move_to(loc); // Update rope @@ -252,15 +263,16 @@ impl Document { } /// Deletes a character at a location whilst checking for tab spaces + /// + /// # Errors + /// This code will error if the location is invalid pub fn delete_with_tab(&mut self, loc: &Loc, st: &str) -> Result<()> { // Check for tab spaces - let boundaries = tab_boundaries_backward( - &self.line(loc.y).unwrap_or_else(|| "".to_string()), - self.tab_width, - ); + let boundaries = + tab_boundaries_backward(&self.line(loc.y).unwrap_or_default(), self.tab_width); if boundaries.contains(&loc.x.saturating_add(1)) && !self.in_redo { // Register other delete actions to delete the whole tab - let mut loc_copy = loc.clone(); + let mut loc_copy = *loc; self.delete(loc.x..=loc.x + st.chars().count(), loc.y)?; for _ in 1..self.tab_width { loc_copy.x = loc_copy.x.saturating_sub(1); @@ -285,7 +297,7 @@ impl Document { // Extract range information let (mut start, mut end) = get_range(&x, line_start, line_end); self.valid_range(start, end, y)?; - self.modified = true; + self.info.modified = true; self.move_to(&Loc::at(start, y)); start += line_start; end += line_start; @@ -316,12 +328,10 @@ impl Document { /// # Errors /// Returns an error if location is out of range. pub fn insert_line(&mut self, loc: usize, contents: String) -> Result<()> { - if !self.lines.is_empty() { - if !(self.len_lines() == 0 && loc == 0) { - self.out_of_range(0, loc.saturating_sub(1))?; - } + if !(self.lines.is_empty() || self.len_lines() == 0 && loc == 0) { + self.out_of_range(0, loc.saturating_sub(1))?; } - self.modified = true; + self.info.modified = true; // Update unicode and tab map self.dbl_map.shift_down(loc); self.tab_map.shift_down(loc); @@ -334,7 +344,7 @@ impl Document { // Update rope let char_idx = self.file.line_to_char(loc); self.file.insert(char_idx, &(contents + "\n")); - self.loaded_to += 1; + self.info.loaded_to += 1; // Goto line self.move_to_y(loc); self.old_cursor = self.loc().x; @@ -349,7 +359,7 @@ impl Document { // Update tab & unicode map self.dbl_map.delete(loc); self.tab_map.delete(loc); - self.modified = true; + self.info.modified = true; // Shift down other line numbers in the hashmap self.dbl_map.shift_up(loc); self.tab_map.shift_up(loc); @@ -359,7 +369,7 @@ impl Document { let idx_start = self.file.line_to_char(loc); let idx_end = self.file.line_to_char(loc + 1); self.file.remove(idx_start..idx_end); - self.loaded_to = self.loaded_to.saturating_sub(1); + self.info.loaded_to = self.info.loaded_to.saturating_sub(1); // Goto line self.move_to_y(loc); self.old_cursor = self.loc().x; @@ -372,7 +382,7 @@ impl Document { /// Returns an error if location is out of range. pub fn split_down(&mut self, loc: &Loc) -> Result<()> { self.out_of_range(loc.x, loc.y)?; - self.modified = true; + self.info.modified = true; // Gather context let line = self.line(loc.y).ok_or(Error::OutOfRange)?; let rhs: String = line.chars().skip(loc.x).collect(); @@ -389,7 +399,7 @@ impl Document { /// Returns an error if location is out of range. pub fn splice_up(&mut self, y: usize) -> Result<()> { self.out_of_range(0, y + 1)?; - self.modified = true; + self.info.modified = true; // Gather context let length = self.line(y).ok_or(Error::OutOfRange)?.chars().count(); let below = self.line(y + 1).ok_or(Error::OutOfRange)?; @@ -481,7 +491,7 @@ impl Document { return Status::StartOfLine; } // Determine the width of the character to traverse - let line = self.line(self.loc().y).unwrap_or_else(|| "".to_string()); + let line = self.line(self.loc().y).unwrap_or_default(); let boundaries = tab_boundaries_backward(&line, self.tab_width); let width = if boundaries.contains(&self.char_ptr) { // Push the character pointer up @@ -513,7 +523,7 @@ impl Document { /// Select with the cursor right pub fn select_right(&mut self) -> Status { // Return if already on end of line - let line = self.line(self.loc().y).unwrap_or_else(|| "".to_string()); + let line = self.line(self.loc().y).unwrap_or_default(); let width = width(&line, self.tab_width); if width == self.loc().x { return Status::EndOfLine; @@ -560,7 +570,7 @@ impl Document { /// Select to the end of the line pub fn select_end(&mut self) { - let line = self.line(self.loc().y).unwrap_or_else(|| "".to_string()); + let line = self.line(self.loc().y).unwrap_or_default(); let length = line.chars().count(); self.select_to_x(length); self.old_cursor = self.loc().x; @@ -627,7 +637,7 @@ impl Document { /// Moves to the next word in the document pub fn move_next_word(&mut self) -> Status { let Loc { x, y } = self.char_loc(); - let line = self.line(y).unwrap_or_else(|| "".to_string()); + let line = self.line(y).unwrap_or_default(); if x == line.chars().count() && y != self.len_lines() { return Status::EndOfLine; } @@ -736,14 +746,14 @@ impl Document { /// Function to select to a specific x position pub fn select_to_x(&mut self, x: usize) { - let line = self.line(self.loc().y).unwrap_or_else(|| "".to_string()); + let line = self.line(self.loc().y).unwrap_or_default(); // If we're already at this x coordinate, just exit if self.char_ptr == x { return; } // If the move position is out of bounds, move to the end of the line if line.chars().count() < x { - let line = self.line(self.loc().y).unwrap_or_else(|| "".to_string()); + let line = self.line(self.loc().y).unwrap_or_default(); let length = line.chars().count(); self.select_to_x(length); return; @@ -800,6 +810,10 @@ impl Document { /// Determines if specified coordinates are out of range of the document. /// # Errors /// Returns an error when the given coordinates are out of range. + /// # Panics + /// When you try using this function on a location that has not yet been loaded into buffer + /// If you see this error, you should double check that you have used `Document::load_to` + /// enough pub fn out_of_range(&self, x: usize, y: usize) -> Result<()> { let msg = "Did you forget to use load_to?"; if y >= self.len_lines() || x > self.line(y).expect(msg).chars().count() { @@ -821,6 +835,7 @@ impl Document { } /// Calculate the character index from the display index on a certain line + #[must_use] pub fn character_idx(&self, loc: &Loc) -> usize { let mut idx = loc.x; // Account for double width characters @@ -903,9 +918,9 @@ impl Document { to = len_lines; } // Only act if there are lines we haven't loaded yet - if to > self.loaded_to { + if to > self.info.loaded_to { // For each line, run through each character and make note of any double width characters - for i in self.loaded_to..to { + for i in self.info.loaded_to..to { let line: String = self.file.line(i).chars().collect(); // Add to char maps let (dbl_map, tab_map) = form_map(&line, self.tab_width); @@ -916,7 +931,7 @@ impl Document { .push(line.trim_end_matches(&['\n', '\r']).to_string()); } // Store new loaded point - self.loaded_to = to; + self.info.loaded_to = to; } } @@ -936,7 +951,7 @@ impl Document { /// Returns the number of lines in the document #[must_use] pub fn len_lines(&self) -> usize { - self.file.len_lines().saturating_sub(1) + if self.eol { 1 } else { 0 } + self.file.len_lines().saturating_sub(1) + usize::from(self.info.eol) } /// Evaluate the line number text for a specific line @@ -1004,6 +1019,7 @@ impl Document { } /// If the cursor is within the viewport, this will return where it is relatively + #[must_use] pub fn cursor_loc_in_screen(&self) -> Option { if self.cursor.loc.x < self.offset.x { return None; @@ -1018,15 +1034,17 @@ impl Document { if result.x > self.size.w || result.y > self.size.h { return None; } - return Some(result); + Some(result) } /// Returns true if there is no active selection and vice versa + #[must_use] pub fn is_selection_empty(&self) -> bool { self.cursor.loc == self.cursor.selection_end } /// Will return the bounds of the current active selection + #[must_use] pub fn selection_loc_bound(&self) -> (Loc, Loc) { let mut left = self.cursor.loc; let mut right = self.cursor.selection_end; @@ -1040,15 +1058,17 @@ impl Document { } /// Returns true if the provided location is within the current active selection + #[must_use] pub fn is_loc_selected(&self, loc: Loc) -> bool { let (left, right) = self.selection_loc_bound(); left <= loc && loc < right } /// Will return the current active selection as a range over file characters + #[must_use] pub fn selection_range(&self) -> Range { - let mut cursor = self.cursor.loc.clone(); - let mut selection_end = self.cursor.selection_end.clone(); + let mut cursor = self.cursor.loc; + let mut selection_end = self.cursor.selection_end; cursor.x = self.character_idx(&cursor); selection_end.x = self.character_idx(&selection_end); let mut left = self.loc_to_file_pos(&cursor); @@ -1060,6 +1080,7 @@ impl Document { } /// Will return the text contained within the current selection + #[must_use] pub fn selection_text(&self) -> String { self.file.slice(self.selection_range()).to_string() } @@ -1070,7 +1091,7 @@ impl Document { } pub fn reload_lines(&mut self) { - let to = std::mem::take(&mut self.loaded_to); + let to = std::mem::take(&mut self.info.loaded_to); self.lines.clear(); self.load_to(to); } @@ -1084,7 +1105,7 @@ impl Document { self.char_ptr = self.character_idx(&self.cursor.loc); self.cancel_selection(); self.bring_cursor_in_viewport(); - self.modified = true; + self.info.modified = true; } } diff --git a/kaolinite/src/event.rs b/kaolinite/src/event.rs index 28d396ad..96131c35 100644 --- a/kaolinite/src/event.rs +++ b/kaolinite/src/event.rs @@ -39,12 +39,11 @@ impl Event { #[must_use] pub fn loc(self) -> Loc { match self { - Event::Insert(loc, _) => loc, - Event::Delete(loc, _) => loc, - Event::InsertLine(loc, _) => Loc { x: 0, y: loc }, - Event::DeleteLine(loc, _) => Loc { x: 0, y: loc }, - Event::SplitDown(loc) => loc, - Event::SpliceUp(loc) => loc, + Event::Insert(loc, _) + | Event::Delete(loc, _) + | Event::SplitDown(loc) + | Event::SpliceUp(loc) => loc, + Event::InsertLine(loc, _) | Event::DeleteLine(loc, _) => Loc { x: 0, y: loc }, } } } @@ -96,6 +95,7 @@ pub struct UndoMgmt { } impl Document { + #[must_use] pub fn take_snapshot(&self) -> Snapshot { Snapshot { content: self.file.clone(), @@ -153,10 +153,11 @@ impl UndoMgmt { /// On file save, mark where the document is to match it on the disk pub fn saved(&mut self) { - self.on_disk = self.undo.len() + self.on_disk = self.undo.len(); } /// Determine if the state of the document is currently that of what is on the disk + #[must_use] pub fn at_file(&self) -> bool { self.undo.len() == self.on_disk } diff --git a/kaolinite/src/lib.rs b/kaolinite/src/lib.rs index d59c7ae5..3a543a83 100644 --- a/kaolinite/src/lib.rs +++ b/kaolinite/src/lib.rs @@ -1,7 +1,7 @@ //! # Kaolinite //! > Kaolinite is an advanced library that handles the backend of a terminal text editor. You can -//! feel free to make your own terminal text editor using kaolinite, or see the reference -//! implementation found under the directory `examples/cactus`. +//! > feel free to make your own terminal text editor using kaolinite, or see the reference +//! > implementation found under the directory `examples/cactus`. //! //! It'll handle things like //! - Opening and saving files diff --git a/kaolinite/src/utils.rs b/kaolinite/src/utils.rs index 6a364292..83f762d1 100644 --- a/kaolinite/src/utils.rs +++ b/kaolinite/src/utils.rs @@ -30,7 +30,7 @@ impl Loc { /// Shorthand to produce a location #[must_use] pub fn at(x: usize, y: usize) -> Self { - Self { x, y } + Self { y, x } } } @@ -56,7 +56,7 @@ impl Size { pub fn trim(string: &str, start: usize, length: usize, tab_width: usize) -> String { let string = string.replace('\t', &" ".repeat(tab_width)); if start >= string.width() { - return "".to_string(); + return String::new(); } let desired_length = string.width().saturating_sub(start); let mut chars: String = string; @@ -64,13 +64,13 @@ pub fn trim(string: &str, start: usize, length: usize, tab_width: usize) -> Stri chars = chars.chars().skip(1).collect(); } if chars.width() < desired_length { - chars = format!(" {}", chars); + chars = format!(" {chars}"); } while chars.width() > length { chars.pop(); } if chars.width() < length && desired_length > length { - chars = format!("{} ", chars); + chars = format!("{chars} "); } chars } @@ -151,7 +151,7 @@ pub fn get_file_name(path: &str) -> Option { let p = Path::new(path); p.file_name() .and_then(|name| name.to_str()) - .map(|s| s.to_string()) + .map(std::string::ToString::to_string) } /// Will get the file name from a file @@ -160,7 +160,7 @@ pub fn get_file_ext(path: &str) -> Option { let p = Path::new(path); p.extension() .and_then(|name| name.to_str()) - .map(|s| s.to_string()) + .map(std::string::ToString::to_string) } /// Determine the filetype from the extension @@ -298,14 +298,14 @@ pub fn icon(language: &str) -> String { "Assembly" => " ", "Batch" => "󰆍 ", "Brainfuck" => " ", - "C" => " ", - "CMake" => " ", + "C" | "C Header" => " ", + "CMake" | "Makefile" => " ", "Java" => " ", "Clojure" => " ", "CoffeeScript" => " ", "Crystal" => " ", "Cuda" => " ", - "C++" => " ", + "C++" | "C++ Header" => " ", "C#" => " ", "CSS" => " ", "CSV" => " ", @@ -325,11 +325,9 @@ pub fn icon(language: &str) -> String { "Gnuplot" => " ", "Go" => "", "Groovy" => " ", - "C Header" => " ", "Haml" => "", "Handlebars" => "󰅩 ", "Haskell" => " ", - "C++ Header" => " ", "HTML" => " ", "INI" => " ", "Arduino" => " ", @@ -346,7 +344,6 @@ pub fn icon(language: &str) -> String { "Matlab" => " ", "Objective-C" => " ", "OCaml" => " ", - "Makefile" => " ", "Markdown" => " ", "Nix" => " ", "NumPy" => "󰘨 ", @@ -354,16 +351,14 @@ pub fn icon(language: &str) -> String { "Perl" => " ", "PowerShell" => "󰨊 ", "Prolog" => " ", - "Python" => " ", - "Cython" => " ", + "Python" | "Cython" => " ", "R" => " ", "reStructuredText" => "󰊄", "Ruby" => " ", "Rust" => " ", - "Shell" => " ", - "SCSS" => " ", + "Shell" | "Zsh" => " ", + "SCSS" | "Sass" => " ", "SQL" => " ", - "Sass" => " ", "Scala" => "", "Scheme" => "", "Swift" => " ", @@ -375,7 +370,6 @@ pub fn icon(language: &str) -> String { "Visual Basic" => "󰯁 ", "Vue" => " ", "XML" => "󰗀 ", - "Zsh" => " ", _ => "󰈙 ", } .to_string() diff --git a/kaolinite/tests/test.rs b/kaolinite/tests/test.rs index 434efcc4..19f6b2e9 100644 --- a/kaolinite/tests/test.rs +++ b/kaolinite/tests/test.rs @@ -307,11 +307,11 @@ fn document_disks() { let mut doc = Document::new(Size::is(100, 10)); assert!(doc.save().is_err()); let mut doc = Document::new(Size::is(100, 10)); - doc.read_only = true; + doc.info.read_only = true; assert!(doc.save().is_err()); // Save as assert!(doc.save_as("tests/data/ghost.txt").is_err()); - doc.read_only = false; + doc.info.read_only = false; assert!(doc.save_as("tests/data/ghost.txt").is_ok()); // Clean up and verify ghost exists let result = std::fs::read_to_string("tests/data/ghost.txt").unwrap(); @@ -368,19 +368,19 @@ fn document_undo_redo() { doc.load_to(100); assert!(doc.undo_mgmt.undo(doc.take_snapshot()).is_none()); assert!(doc.redo().is_ok()); - assert!(!doc.modified); + assert!(!doc.info.modified); doc.exe(Event::InsertLine(0, st!("hello你bye好hello"))); doc.exe(Event::Delete(Loc { x: 0, y: 2 }, st!("\t"))); doc.exe(Event::Insert(Loc { x: 3, y: 2 }, st!("a"))); doc.commit(); - assert!(doc.modified); + assert!(doc.info.modified); assert!(doc.undo().is_ok()); - assert!(!doc.modified); + assert!(!doc.info.modified); assert_eq!(doc.line(0), Some(st!(" 你好"))); assert_eq!(doc.line(1), Some(st!("\thello"))); assert_eq!(doc.line(2), Some(st!(" hello"))); assert!(doc.redo().is_ok()); - assert!(doc.modified); + assert!(doc.info.modified); assert_eq!(doc.line(0), Some(st!("hello你bye好hello"))); assert_eq!(doc.line(2), Some(st!("helalo"))); } @@ -401,7 +401,7 @@ fn document_moving() { (1 + loaded).saturating_sub(9) } ); - assert!(doc.loaded_to >= loaded); + assert!(doc.info.loaded_to >= loaded); } assert_eq!(doc.move_down(), Status::EndOfFile); // Check moving up @@ -507,7 +507,7 @@ fn document_moving() { } ); assert_eq!(doc.old_cursor, 0); - assert_eq!(doc.loaded_to, doc.len_lines() + 1); + assert_eq!(doc.info.loaded_to, doc.len_lines() + 1); doc.move_top(); assert_eq!(doc.char_loc(), Loc { x: 0, y: 0 }); assert_eq!(doc.old_cursor, 0); @@ -520,7 +520,7 @@ fn document_moving() { } ); assert_eq!(doc.old_cursor, 0); - assert_eq!(doc.loaded_to, doc.len_lines() + 1); + assert_eq!(doc.info.loaded_to, doc.len_lines() + 1); doc.select_top(); assert_eq!(doc.char_loc(), Loc { x: 0, y: 0 }); assert_eq!(doc.old_cursor, 0); @@ -606,12 +606,12 @@ fn document_scrolling() { assert_eq!(doc.offset.y, 0); doc.scroll_down(); assert_eq!(doc.offset.y, 1); - assert_eq!(doc.loaded_to, 11); + assert_eq!(doc.info.loaded_to, 11); // Scrolling up assert_eq!(doc.offset.y, 1); doc.scroll_up(); assert_eq!(doc.offset.y, 0); - assert_eq!(doc.loaded_to, 11); + assert_eq!(doc.info.loaded_to, 11); } #[test] @@ -725,7 +725,7 @@ fn document_searching() { text: st!("world") }) ); - assert_eq!(doc.loaded_to, 5); + assert_eq!(doc.info.loaded_to, 5); doc.move_to(&Loc { x: 2, y: 2 }); assert_eq!( doc.next_match("hello", 0), diff --git a/src/cli.rs b/src/cli.rs index 917397b6..3a45bb19 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -30,23 +30,24 @@ EXAMPLES: "; /// Read from the standard input -pub fn get_stdin() -> Option { - let input = io::stdin() - .lock() - .lines() - .fold("".to_string(), |acc, line| { - acc + &line.unwrap_or_else(|_| "".to_string()) + "\n" - }); - - return Some(input); +pub fn get_stdin() -> String { + io::stdin().lock().lines().fold(String::new(), |acc, line| { + acc + &line.unwrap_or_else(|_| String::new()) + "\n" + }) } -/// Struct to help with starting ox -pub struct CommandLineInterface { +/// Flags for command line interface +#[allow(clippy::struct_excessive_bools)] +pub struct CommandLineInterfaceFlags { pub help: bool, pub version: bool, pub read_only: bool, pub stdin: bool, +} + +/// Struct to help with starting ox +pub struct CommandLineInterface { + pub flags: CommandLineInterfaceFlags, pub file_type: Option, pub config_path: String, pub to_open: Vec, @@ -63,10 +64,12 @@ impl CommandLineInterface { let config: Key = ["-c", "--config"].into(); Self { - help: j.contains(["-h", "--help"]), - version: j.contains(["-v", "--version"]), - read_only: j.contains(["-r", "--readonly"]), - stdin: j.contains("--stdin"), + flags: CommandLineInterfaceFlags { + help: j.contains(["-h", "--help"]), + version: j.contains(["-v", "--version"]), + read_only: j.contains(["-r", "--readonly"]), + stdin: j.contains("--stdin"), + }, file_type: j.option_arg::(filetype.clone()), config_path: j .option_arg::(config.clone()) @@ -77,11 +80,11 @@ impl CommandLineInterface { /// Handle options that won't need to start the editor pub fn basic_options(&self) { - if self.help { - println!("{}", HELP); + if self.flags.help { + println!("{HELP}"); std::process::exit(0); - } else if self.version { - println!("{}", VERSION); + } else if self.flags.version { + println!("{VERSION}"); std::process::exit(0); } } diff --git a/src/config/colors.rs b/src/config/colors.rs index 1c0fbf0b..404cbee8 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -1,71 +1,72 @@ -use crate::error::Result; -use crossterm::style::Color; +use crate::error::{OxError, Result}; +use crossterm::style::Color as CColor; use mlua::prelude::*; use super::issue_warning; #[derive(Debug)] pub struct Colors { - pub editor_bg: ConfigColor, - pub editor_fg: ConfigColor, + pub editor_bg: Color, + pub editor_fg: Color, - pub status_bg: ConfigColor, - pub status_fg: ConfigColor, + pub status_bg: Color, + pub status_fg: Color, - pub highlight: ConfigColor, + pub highlight: Color, - pub line_number_fg: ConfigColor, - pub line_number_bg: ConfigColor, + pub line_number_fg: Color, + pub line_number_bg: Color, - pub tab_active_fg: ConfigColor, - pub tab_active_bg: ConfigColor, - pub tab_inactive_fg: ConfigColor, - pub tab_inactive_bg: ConfigColor, + pub tab_active_fg: Color, + pub tab_active_bg: Color, + pub tab_inactive_fg: Color, + pub tab_inactive_bg: Color, - pub info_bg: ConfigColor, - pub info_fg: ConfigColor, - pub warning_bg: ConfigColor, - pub warning_fg: ConfigColor, - pub error_bg: ConfigColor, - pub error_fg: ConfigColor, + pub info_bg: Color, + pub info_fg: Color, + pub warning_bg: Color, + pub warning_fg: Color, + pub error_bg: Color, + pub error_fg: Color, - pub selection_fg: ConfigColor, - pub selection_bg: ConfigColor, + pub selection_fg: Color, + pub selection_bg: Color, } impl Default for Colors { fn default() -> Self { Self { - editor_bg: ConfigColor::Black, - editor_fg: ConfigColor::Black, + editor_bg: Color::Black, + editor_fg: Color::Black, - status_bg: ConfigColor::Black, - status_fg: ConfigColor::Black, + status_bg: Color::Black, + status_fg: Color::Black, - highlight: ConfigColor::Black, + highlight: Color::Black, - line_number_fg: ConfigColor::Black, - line_number_bg: ConfigColor::Black, + line_number_fg: Color::Black, + line_number_bg: Color::Black, - tab_active_fg: ConfigColor::Black, - tab_active_bg: ConfigColor::Black, - tab_inactive_fg: ConfigColor::Black, - tab_inactive_bg: ConfigColor::Black, + tab_active_fg: Color::Black, + tab_active_bg: Color::Black, + tab_inactive_fg: Color::Black, + tab_inactive_bg: Color::Black, - info_bg: ConfigColor::Black, - info_fg: ConfigColor::Black, - warning_bg: ConfigColor::Black, - warning_fg: ConfigColor::Black, - error_bg: ConfigColor::Black, - error_fg: ConfigColor::Black, + info_bg: Color::Black, + info_fg: Color::Black, + warning_bg: Color::Black, + warning_fg: Color::Black, + error_bg: Color::Black, + error_fg: Color::Black, - selection_fg: ConfigColor::White, - selection_bg: ConfigColor::Blue, + selection_fg: Color::White, + selection_bg: Color::Blue, } } } impl LuaUserData for Colors { + #[allow(clippy::too_many_lines)] fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { fields.add_field_method_get("editor_bg", |env, this| Ok(this.editor_bg.to_lua(env))); fields.add_field_method_get("editor_fg", |env, this| Ok(this.editor_fg.to_lua(env))); @@ -103,86 +104,86 @@ impl LuaUserData for Colors { Ok(this.selection_bg.to_lua(env)) }); fields.add_field_method_set("editor_bg", |_, this, value| { - this.editor_bg = ConfigColor::from_lua(value); + this.editor_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("editor_fg", |_, this, value| { - this.editor_fg = ConfigColor::from_lua(value); + this.editor_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("status_bg", |_, this, value| { - this.status_bg = ConfigColor::from_lua(value); + this.status_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("status_fg", |_, this, value| { - this.status_fg = ConfigColor::from_lua(value); + this.status_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("highlight", |_, this, value| { - this.highlight = ConfigColor::from_lua(value); + this.highlight = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("line_number_bg", |_, this, value| { - this.line_number_bg = ConfigColor::from_lua(value); + this.line_number_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("line_number_fg", |_, this, value| { - this.line_number_fg = ConfigColor::from_lua(value); + this.line_number_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("tab_active_fg", |_, this, value| { - this.tab_active_fg = ConfigColor::from_lua(value); + this.tab_active_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("tab_active_bg", |_, this, value| { - this.tab_active_bg = ConfigColor::from_lua(value); + this.tab_active_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("tab_inactive_fg", |_, this, value| { - this.tab_inactive_fg = ConfigColor::from_lua(value); + this.tab_inactive_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("tab_inactive_bg", |_, this, value| { - this.tab_inactive_bg = ConfigColor::from_lua(value); + this.tab_inactive_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("error_bg", |_, this, value| { - this.error_bg = ConfigColor::from_lua(value); + this.error_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("error_fg", |_, this, value| { - this.error_fg = ConfigColor::from_lua(value); + this.error_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("warning_bg", |_, this, value| { - this.warning_bg = ConfigColor::from_lua(value); + this.warning_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("warning_fg", |_, this, value| { - this.warning_fg = ConfigColor::from_lua(value); + this.warning_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("info_bg", |_, this, value| { - this.info_bg = ConfigColor::from_lua(value); + this.info_bg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("info_fg", |_, this, value| { - this.info_fg = ConfigColor::from_lua(value); + this.info_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("selection_fg", |_, this, value| { - this.selection_fg = ConfigColor::from_lua(value); + this.selection_fg = Color::from_lua(value); Ok(()) }); fields.add_field_method_set("selection_bg", |_, this, value| { - this.selection_bg = ConfigColor::from_lua(value); + this.selection_bg = Color::from_lua(value); Ok(()) }); } } #[derive(Debug)] -pub enum ConfigColor { +pub enum Color { Rgb(u8, u8, u8), Hex(String), Black, @@ -204,8 +205,8 @@ pub enum ConfigColor { Transparent, } -impl ConfigColor { - pub fn from_lua<'a>(value: LuaValue<'a>) -> Self { +impl Color { + pub fn from_lua(value: LuaValue<'_>) -> Self { match value { LuaValue::String(string) => match string.to_str().unwrap_or("transparent") { "black" => Self::Black, @@ -235,7 +236,7 @@ impl ConfigColor { let mut tri: Vec = vec![]; for _ in 0..3 { if let Ok(val) = table.pop() { - tri.insert(0, val) + tri.insert(0, val); } else { issue_warning("Invalid RGB sequence provided - please check your numerical values are between 0 and 255"); tri.insert(0, 255); @@ -253,11 +254,11 @@ impl ConfigColor { pub fn to_lua<'a>(&self, env: &'a Lua) -> LuaValue<'a> { let msg = "Failed to create lua string"; match self { - ConfigColor::Hex(hex) => { + Color::Hex(hex) => { let string = env.create_string(hex).expect(msg); LuaValue::String(string) } - ConfigColor::Rgb(r, g, b) => { + Color::Rgb(r, g, b) => { // Create lua table let table = env.create_table().expect("Failed to create lua table"); let _ = table.push(*r as isize); @@ -265,70 +266,65 @@ impl ConfigColor { let _ = table.push(*b as isize); LuaValue::Table(table) } - ConfigColor::Black => LuaValue::String(env.create_string("black").expect(msg)), - ConfigColor::DarkGrey => LuaValue::String(env.create_string("darkgrey").expect(msg)), - ConfigColor::Red => LuaValue::String(env.create_string("red").expect(msg)), - ConfigColor::DarkRed => LuaValue::String(env.create_string("darkred").expect(msg)), - ConfigColor::Green => LuaValue::String(env.create_string("green").expect(msg)), - ConfigColor::DarkGreen => LuaValue::String(env.create_string("darkgreen").expect(msg)), - ConfigColor::Yellow => LuaValue::String(env.create_string("yellow").expect(msg)), - ConfigColor::DarkYellow => { - LuaValue::String(env.create_string("darkyellow").expect(msg)) - } - ConfigColor::Blue => LuaValue::String(env.create_string("blue").expect(msg)), - ConfigColor::DarkBlue => LuaValue::String(env.create_string("darkblue").expect(msg)), - ConfigColor::Magenta => LuaValue::String(env.create_string("magenta").expect(msg)), - ConfigColor::DarkMagenta => { - LuaValue::String(env.create_string("darkmagenta").expect(msg)) - } - ConfigColor::Cyan => LuaValue::String(env.create_string("cyan").expect(msg)), - ConfigColor::DarkCyan => LuaValue::String(env.create_string("darkcyan").expect(msg)), - ConfigColor::White => LuaValue::String(env.create_string("white").expect(msg)), - ConfigColor::Grey => LuaValue::String(env.create_string("grey").expect(msg)), - ConfigColor::Transparent => { - LuaValue::String(env.create_string("transparent").expect(msg)) - } + Color::Black => LuaValue::String(env.create_string("black").expect(msg)), + Color::DarkGrey => LuaValue::String(env.create_string("darkgrey").expect(msg)), + Color::Red => LuaValue::String(env.create_string("red").expect(msg)), + Color::DarkRed => LuaValue::String(env.create_string("darkred").expect(msg)), + Color::Green => LuaValue::String(env.create_string("green").expect(msg)), + Color::DarkGreen => LuaValue::String(env.create_string("darkgreen").expect(msg)), + Color::Yellow => LuaValue::String(env.create_string("yellow").expect(msg)), + Color::DarkYellow => LuaValue::String(env.create_string("darkyellow").expect(msg)), + Color::Blue => LuaValue::String(env.create_string("blue").expect(msg)), + Color::DarkBlue => LuaValue::String(env.create_string("darkblue").expect(msg)), + Color::Magenta => LuaValue::String(env.create_string("magenta").expect(msg)), + Color::DarkMagenta => LuaValue::String(env.create_string("darkmagenta").expect(msg)), + Color::Cyan => LuaValue::String(env.create_string("cyan").expect(msg)), + Color::DarkCyan => LuaValue::String(env.create_string("darkcyan").expect(msg)), + Color::White => LuaValue::String(env.create_string("white").expect(msg)), + Color::Grey => LuaValue::String(env.create_string("grey").expect(msg)), + Color::Transparent => LuaValue::String(env.create_string("transparent").expect(msg)), } } - pub fn to_color(&self) -> Result { + pub fn to_color(&self) -> Result { Ok(match self { - ConfigColor::Hex(hex) => { - let (r, g, b) = self.hex_to_rgb(hex)?; - Color::Rgb { r, g, b } + Color::Hex(hex) => { + let (r, g, b) = Self::hex_to_rgb(hex)?; + CColor::Rgb { r, g, b } } - ConfigColor::Rgb(r, g, b) => Color::Rgb { + Color::Rgb(r, g, b) => CColor::Rgb { r: *r, g: *g, b: *b, }, - ConfigColor::Black => Color::Black, - ConfigColor::DarkGrey => Color::DarkGrey, - ConfigColor::Red => Color::Red, - ConfigColor::DarkRed => Color::DarkRed, - ConfigColor::Green => Color::Green, - ConfigColor::DarkGreen => Color::DarkGreen, - ConfigColor::Yellow => Color::Yellow, - ConfigColor::DarkYellow => Color::DarkYellow, - ConfigColor::Blue => Color::Blue, - ConfigColor::DarkBlue => Color::DarkBlue, - ConfigColor::Magenta => Color::Magenta, - ConfigColor::DarkMagenta => Color::DarkMagenta, - ConfigColor::Cyan => Color::Cyan, - ConfigColor::DarkCyan => Color::DarkCyan, - ConfigColor::White => Color::White, - ConfigColor::Grey => Color::Grey, - ConfigColor::Transparent => Color::Reset, + Color::Black => CColor::Black, + Color::DarkGrey => CColor::DarkGrey, + Color::Red => CColor::Red, + Color::DarkRed => CColor::DarkRed, + Color::Green => CColor::Green, + Color::DarkGreen => CColor::DarkGreen, + Color::Yellow => CColor::Yellow, + Color::DarkYellow => CColor::DarkYellow, + Color::Blue => CColor::Blue, + Color::DarkBlue => CColor::DarkBlue, + Color::Magenta => CColor::Magenta, + Color::DarkMagenta => CColor::DarkMagenta, + Color::Cyan => CColor::Cyan, + Color::DarkCyan => CColor::DarkCyan, + Color::White => CColor::White, + Color::Grey => CColor::Grey, + Color::Transparent => CColor::Reset, }) } - fn hex_to_rgb(&self, hex: &str) -> Result<(u8, u8, u8)> { + fn hex_to_rgb(hex: &str) -> Result<(u8, u8, u8)> { // Remove the leading '#' if present let hex = hex.trim_start_matches('#'); // Ensure the hex code is exactly 6 characters long if hex.len() != 6 { - panic!("Invalid hex code used in configuration file - ensure they are of length 6"); + let msg = "Invalid hex code used in configuration file - ensure they are of length 6"; + return Err(OxError::Config(msg.to_string())); } // Parse the hex string into the RGB components @@ -336,9 +332,10 @@ impl ConfigColor { for i in 0..3 { let section = &hex[(i * 2)..(i * 2 + 2)]; if let Ok(val) = u8::from_str_radix(section, 16) { - tri.insert(0, val) + tri.insert(0, val); } else { - panic!("Invalid hex code used in configuration file - ensure all digits are between 0 and F"); + let msg = "Invalid hex code used in configuration file - ensure all digits are between 0 and F"; + return Err(OxError::Config(msg.to_string())); } } Ok((tri[0], tri[1], tri[2])) diff --git a/src/config/editor.rs b/src/config/editor.rs index 8b67a6be..a47d2849 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -31,20 +31,18 @@ impl LuaUserData for Editor { .doc() .file_name .as_ref() - .and_then(|name| Some(name.split('.').last().unwrap_or(""))) - .unwrap_or(""); + .map_or("", |name| name.split('.').last().unwrap_or("")); let file_type = kaolinite::utils::filetype(ext); Ok(file_type) }); } + #[allow(clippy::too_many_lines)] fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { // Reload the configuration file methods.add_method_mut("reload_config", |lua, editor, ()| { - if editor - .load_config(editor.config_path.clone(), &lua) - .is_err() - { + let path = editor.config_path.clone(); + if editor.load_config(&path, lua).is_err() { editor.feedback = Feedback::Error("Failed to reload config".to_string()); } Ok(()) @@ -75,80 +73,80 @@ impl LuaUserData for Editor { editor.feedback = Feedback::Error(err.to_string()); } } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("remove", |_, editor, ()| { if let Err(err) = editor.backspace() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("insert_line", |_, editor, ()| { if let Err(err) = editor.enter() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("remove_line", |_, editor, ()| { if let Err(err) = editor.delete_line() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); // Cursor moving methods.add_method_mut("move_to", |_, editor, (x, y): (usize, usize)| { let y = y.saturating_sub(1); - editor.doc_mut().move_to(&Loc { x, y }); - update_highlighter(editor); + editor.doc_mut().move_to(&Loc { y, x }); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_up", |_, editor, ()| { editor.up(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_down", |_, editor, ()| { editor.down(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_left", |_, editor, ()| { editor.left(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_right", |_, editor, ()| { editor.right(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("select_up", |_, editor, ()| { editor.select_up(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("select_down", |_, editor, ()| { editor.select_down(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("select_left", |_, editor, ()| { editor.select_left(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("select_right", |_, editor, ()| { editor.select_right(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("select_all", |_, editor, ()| { editor.select_all(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("cut", |_, editor, ()| { @@ -169,42 +167,42 @@ impl LuaUserData for Editor { }); methods.add_method_mut("move_home", |_, editor, ()| { editor.doc_mut().move_home(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_end", |_, editor, ()| { editor.doc_mut().move_end(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_page_up", |_, editor, ()| { editor.doc_mut().move_page_up(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_page_down", |_, editor, ()| { editor.doc_mut().move_page_down(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_top", |_, editor, ()| { editor.doc_mut().move_top(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_bottom", |_, editor, ()| { editor.doc_mut().move_bottom(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_previous_word", |_, editor, ()| { editor.prev_word(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_next_word", |_, editor, ()| { editor.next_word(); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut( @@ -212,26 +210,26 @@ impl LuaUserData for Editor { |_, editor, (text, x, y): (String, usize, usize)| { let y = y.saturating_sub(1); let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to(&Loc { x, y }); + editor.doc_mut().move_to(&Loc { y, x }); for ch in text.chars() { if let Err(err) = editor.character(ch) { editor.feedback = Feedback::Error(err.to_string()); } } editor.doc_mut().move_to(&location); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }, ); methods.add_method_mut("remove_at", |_, editor, (x, y): (usize, usize)| { let y = y.saturating_sub(1); let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to(&Loc { x, y }); + editor.doc_mut().move_to(&Loc { y, x }); if let Err(err) = editor.delete() { editor.feedback = Feedback::Error(err.to_string()); } editor.doc_mut().move_to(&location); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("insert_line_at", |_, editor, (text, y): (String, usize)| { @@ -256,7 +254,7 @@ impl LuaUserData for Editor { } } editor.doc_mut().move_to(&location); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("remove_line_at", |_, editor, y: usize| { @@ -267,7 +265,7 @@ impl LuaUserData for Editor { editor.feedback = Feedback::Error(err.to_string()); } editor.doc_mut().move_to(&location); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("open_command_line", |_, editor, ()| { @@ -329,28 +327,28 @@ impl LuaUserData for Editor { if let Err(err) = editor.undo() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("redo", |_, editor, ()| { if let Err(err) = editor.redo() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("search", |_, editor, ()| { if let Err(err) = editor.search() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method_mut("replace", |_, editor, ()| { if let Err(err) = editor.replace() { editor.feedback = Feedback::Error(err.to_string()); } - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); methods.add_method("get_character", |_, editor, ()| { @@ -358,11 +356,11 @@ impl LuaUserData for Editor { let ch = editor .doc() .line(loc.y) - .unwrap_or_else(|| "".to_string()) + .unwrap_or_default() .chars() .nth(loc.x) - .and_then(|ch| Some(ch.to_string())) - .unwrap_or_else(|| "".to_string()); + .map(|ch| ch.to_string()) + .unwrap_or_default(); Ok(ch) }); methods.add_method_mut("get_character_at", |_, editor, (x, y): (usize, usize)| { @@ -371,24 +369,23 @@ impl LuaUserData for Editor { let ch = editor .doc() .line(y) - .unwrap_or_else(|| "".to_string()) + .unwrap_or_default() .chars() .nth(x) - .and_then(|ch| Some(ch.to_string())) - .unwrap_or_else(|| "".to_string()); - update_highlighter(editor); + .map_or_else(String::new, |ch| ch.to_string()); + editor.update_highlighter(); Ok(ch) }); methods.add_method("get_line", |_, editor, ()| { let loc = editor.doc().char_loc(); - let line = editor.doc().line(loc.y).unwrap_or_else(|| "".to_string()); + let line = editor.doc().line(loc.y).unwrap_or_default(); Ok(line) }); methods.add_method_mut("get_line_at", |_, editor, y: usize| { editor.doc_mut().load_to(y); let y = y.saturating_sub(1); - let line = editor.doc().line(y).unwrap_or_else(|| "".to_string()); - update_highlighter(editor); + let line = editor.doc().line(y).unwrap_or_default(); + editor.update_highlighter(); Ok(line) }); methods.add_method_mut("move_to_document", |_, editor, id: usize| { @@ -397,7 +394,7 @@ impl LuaUserData for Editor { }); methods.add_method_mut("move_previous_match", |_, editor, query: String| { editor.prev_match(&query); - update_highlighter(editor); + editor.update_highlighter(); Ok(()) }); // DEPRECIATED @@ -405,7 +402,7 @@ impl LuaUserData for Editor { // DEPRECIATED methods.add_method_mut("show_help_message", |_, _, ()| Ok(())); methods.add_method_mut("set_read_only", |_, editor, status: bool| { - editor.doc_mut().read_only = status; + editor.doc_mut().info.read_only = status; Ok(()) }); methods.add_method_mut("set_file_type", |_, editor, ext: String| { @@ -434,9 +431,3 @@ impl IntoLua<'_> for LuaLoc { Ok(LuaValue::Table(table)) } } - -fn update_highlighter(editor: &mut Editor) { - if let Err(err) = editor.update_highlighter() { - editor.feedback = Feedback::Error(err.to_string()); - } -} diff --git a/src/config/highlighting.rs b/src/config/highlighting.rs index ba7b257e..361f98d5 100644 --- a/src/config/highlighting.rs +++ b/src/config/highlighting.rs @@ -1,45 +1,39 @@ use crate::error::{OxError, Result}; +use crossterm::style::Color as CColor; use mlua::prelude::*; use std::collections::HashMap; use synoptic::{from_extension, Highlighter}; -use super::{Color, ConfigColor}; +use super::Color; + +type BoundedInterpArgs = (String, String, String, String, String, bool); /// For storing configuration information related to syntax highlighting -#[derive(Debug)] +#[derive(Debug, Default)] +#[allow(clippy::module_name_repetitions)] pub struct SyntaxHighlighting { - pub theme: HashMap, + pub theme: HashMap, pub user_rules: HashMap, } -impl Default for SyntaxHighlighting { - fn default() -> Self { - Self { - theme: HashMap::default(), - user_rules: HashMap::default(), - } - } -} - impl SyntaxHighlighting { /// Get a colour from the theme - pub fn get_theme(&self, name: &str) -> Result { + pub fn get_theme(&self, name: &str) -> Result { if let Some(col) = self.theme.get(name) { col.to_color() } else { Err(OxError::Config(format!( - "{} has not been given a colour in the theme", - name + "{name} has not been given a colour in the theme", ))) } } /// Get a highlighter given a file extension pub fn get_highlighter(&self, ext: &str) -> Highlighter { - self.user_rules - .get(ext) - .and_then(|h| Some(h.clone())) - .unwrap_or_else(|| from_extension(ext, 4).unwrap_or_else(|| Highlighter::new(4))) + self.user_rules.get(ext).map_or_else( + || from_extension(ext, 4).unwrap_or_else(|| Highlighter::new(4)), + std::clone::Clone::clone, + ) } } @@ -74,7 +68,6 @@ impl LuaUserData for SyntaxHighlighting { Ok(table) }, ); - type BoundedInterpArgs = (String, String, String, String, String, bool); methods.add_method_mut( "bounded_interpolation", |lua, _, (name, start, end, i_start, i_end, escape): BoundedInterpArgs| { @@ -93,17 +86,17 @@ impl LuaUserData for SyntaxHighlighting { "new", |_, syntax_highlighting, (extensions, rules): (LuaTable, LuaTable)| { // Make note of the highlighter - for ext_idx in 1..(extensions.len()? + 1) { + for ext_idx in 1..=(extensions.len()?) { // Create highlighter let mut highlighter = Highlighter::new(4); // Add rules one by one - for rule_idx in 1..(rules.len()? + 1) { + for rule_idx in 1..=(rules.len()?) { // Get rule let rule = rules.get::>(rule_idx)?; // Find type of rule and attatch it to the highlighter match rule["kind"].as_str() { "keyword" => { - highlighter.keyword(rule["name"].clone(), &rule["pattern"]) + highlighter.keyword(rule["name"].clone(), &rule["pattern"]); } "bounded" => highlighter.bounded( rule["name"].clone(), @@ -131,7 +124,7 @@ impl LuaUserData for SyntaxHighlighting { methods.add_method_mut("set", |_, syntax_highlighting, (name, value)| { syntax_highlighting .theme - .insert(name, ConfigColor::from_lua(value)); + .insert(name, Color::from_lua(value)); Ok(()) }); } diff --git a/src/config/interface.rs b/src/config/interface.rs index a067e70d..622068a7 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -11,11 +11,11 @@ use super::{issue_warning, Colors}; /// For storing general configuration related to the terminal functionality #[derive(Debug)] -pub struct TerminalConfig { +pub struct Terminal { pub mouse_enabled: bool, } -impl Default for TerminalConfig { +impl Default for Terminal { fn default() -> Self { Self { mouse_enabled: true, @@ -23,7 +23,7 @@ impl Default for TerminalConfig { } } -impl LuaUserData for TerminalConfig { +impl LuaUserData for Terminal { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { fields.add_field_method_get("mouse_enabled", |_, this| Ok(this.mouse_enabled)); fields.add_field_method_set("mouse_enabled", |_, this, value| { @@ -82,7 +82,7 @@ impl Default for GreetingMessage { fn default() -> Self { Self { enabled: true, - format: "".to_string(), + format: String::new(), } } } @@ -93,7 +93,7 @@ impl GreetingMessage { let highlight = Fg(colors.highlight.to_color()?).to_string(); let editor_fg = Fg(colors.editor_fg.to_color()?).to_string(); let mut result = self.format.clone(); - result = result.replace("{version}", &VERSION).to_string(); + result = result.replace("{version}", VERSION).to_string(); result = result.replace("{highlight_start}", &highlight).to_string(); result = result.replace("{highlight_end}", &editor_fg).to_string(); // Find functions to call and substitute in @@ -145,7 +145,7 @@ impl Default for HelpMessage { fn default() -> Self { Self { enabled: true, - format: "".to_string(), + format: String::new(), } } } @@ -156,7 +156,7 @@ impl HelpMessage { let highlight = Fg(colors.highlight.to_color()?).to_string(); let editor_fg = Fg(colors.editor_fg.to_color()?).to_string(); let mut result = self.format.clone(); - result = result.replace("{version}", &VERSION).to_string(); + result = result.replace("{version}", VERSION).to_string(); result = result.replace("{highlight_start}", &highlight).to_string(); result = result.replace("{highlight_end}", &editor_fg).to_string(); // Find functions to call and substitute in @@ -178,7 +178,10 @@ impl HelpMessage { break; } } - Ok(result.split('\n').map(|l| l.to_string()).collect()) + Ok(result + .split('\n') + .map(std::string::ToString::to_string) + .collect()) } } @@ -222,8 +225,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 = icon(&filetype(&file_extension).unwrap_or_else(|| "".to_string())); - let modified = if document.modified { "[+]" } else { "" }; + let icon = icon(&filetype(&file_extension).unwrap_or_default()); + let modified = if document.info.modified { "[+]" } else { "" }; let mut result = self.format.clone(); result = result .replace("{file_extension}", &file_extension) @@ -233,7 +236,7 @@ impl TabLine { .replace("{absolute_path}", &absolute_path) .to_string(); result = result.replace("{path}", &path).to_string(); - result = result.replace("{modified}", &modified).to_string(); + result = result.replace("{modified}", modified).to_string(); result = result.replace("{icon}", &icon).to_string(); result } @@ -276,9 +279,9 @@ impl StatusLine { let path = editor .doc() .file_name - .to_owned() + .clone() .unwrap_or_else(|| "[No Name]".to_string()); - let file_extension = get_file_ext(&path).unwrap_or_else(|| "".to_string()); + let file_extension = get_file_ext(&path).unwrap_or_default(); 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 = filetype(&file_extension).unwrap_or_else(|| { @@ -288,8 +291,12 @@ impl StatusLine { file_extension.to_string() } }); - let icon = icon(&filetype(&file_extension).unwrap_or_else(|| "".to_string())); - let modified = if editor.doc().modified { "[+]" } else { "" }; + let icon = icon(&filetype(&file_extension).unwrap_or_default()); + let modified = if editor.doc().info.modified { + "[+]" + } 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(); @@ -303,7 +310,7 @@ impl StatusLine { part = part.replace("{icon}", &icon).to_string(); part = part.replace("{path}", &path).to_string(); part = part.replace("{absolute_path}", &absolute_path).to_string(); - part = part.replace("{modified}", &modified).to_string(); + part = part.replace("{modified}", modified).to_string(); part = part.replace("{file_type}", &file_type).to_string(); part = part.replace("{cursor_y}", &cursor_y).to_string(); part = part.replace("{cursor_x}", &cursor_x).to_string(); @@ -329,12 +336,12 @@ impl StatusLine { } result.push(part); } - let status: Vec<&str> = result.iter().map(|s| s.as_str()).collect(); + let status: Vec<&str> = result.iter().map(String::as_str).collect(); match self.alignment { StatusAlign::Between => alinio::align::between(status.as_slice(), w), StatusAlign::Around => alinio::align::around(status.as_slice(), w), } - .unwrap_or_else(|| "".to_string()) + .unwrap_or_else(String::new) } } @@ -355,8 +362,8 @@ impl LuaUserData for StatusLine { let alignment: String = this.alignment.clone().into(); Ok(alignment) }); - fields.add_field_method_set("alignment", |_, this, value| { - this.alignment = StatusAlign::from_string(value); + fields.add_field_method_set("alignment", |_, this, value: String| { + this.alignment = StatusAlign::from_string(&value); Ok(()) }); } @@ -369,8 +376,8 @@ pub enum StatusAlign { } impl StatusAlign { - pub fn from_string(string: String) -> Self { - match string.as_str() { + pub fn from_string(string: &str) -> Self { + match string { "around" => Self::Around, "between" => Self::Between, _ => { @@ -385,11 +392,11 @@ impl StatusAlign { } } -impl Into for StatusAlign { - fn into(self) -> String { - match self { - Self::Around => "around", - Self::Between => "between", +impl From for String { + fn from(val: StatusAlign) -> Self { + match val { + StatusAlign::Around => "around", + StatusAlign::Between => "between", } .to_string() } diff --git a/src/config/keys.rs b/src/config/keys.rs index 636c8535..76fb42b7 100644 --- a/src/config/keys.rs +++ b/src/config/keys.rs @@ -34,7 +34,7 @@ pub fn run_key_before(key: &str) -> String { /// Converts a key taken from a crossterm event into string format pub fn key_to_string(modifiers: KMod, key: KCode) -> String { - let mut result = "".to_string(); + let mut result = String::new(); // Deal with modifiers if modifiers.contains(KMod::CONTROL) { result += "ctrl_"; @@ -107,5 +107,5 @@ pub fn key_to_string(modifiers: KMod, key: KCode) -> String { } .to_string(), }; - return result; + result } diff --git a/src/config/mod.rs b/src/config/mod.rs index f47e5e69..f21b349b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,5 +1,4 @@ use crate::error::{OxError, Result}; -use crossterm::style::Color; use mlua::prelude::*; use std::collections::HashMap; use std::{cell::RefCell, rc::Rc}; @@ -10,16 +9,14 @@ mod highlighting; mod interface; mod keys; -pub use colors::{Colors, ConfigColor}; +pub use colors::{Color, Colors}; pub use highlighting::SyntaxHighlighting; -pub use interface::{ - GreetingMessage, HelpMessage, LineNumbers, StatusLine, TabLine, TerminalConfig, -}; +pub use interface::{GreetingMessage, HelpMessage, LineNumbers, StatusLine, TabLine, Terminal}; pub use keys::{key_to_string, run_key, run_key_before}; // Issue a warning to the user fn issue_warning(msg: &str) { - eprintln!("[WARNING] {}", msg); + eprintln!("[WARNING] {msg}"); } /// This contains the default configuration lua file @@ -45,8 +42,8 @@ pub struct Config { pub tab_line: Rc>, pub greeting_message: Rc>, pub help_message: Rc>, - pub terminal: Rc>, - pub document: Rc>, + pub terminal: Rc>, + pub document: Rc>, } impl Config { @@ -60,8 +57,8 @@ impl Config { let colors = Rc::new(RefCell::new(Colors::default())); let status_line = Rc::new(RefCell::new(StatusLine::default())); let tab_line = Rc::new(RefCell::new(TabLine::default())); - let terminal = Rc::new(RefCell::new(TerminalConfig::default())); - let document = Rc::new(RefCell::new(DocumentConfig::default())); + let terminal = Rc::new(RefCell::new(Terminal::default())); + let document = Rc::new(RefCell::new(Document::default())); // Push in configuration globals lua.globals().set("syntax", syntax_highlighting.clone())?; @@ -78,18 +75,18 @@ impl Config { Ok(Config { syntax_highlighting, line_numbers, + colors, + status_line, + tab_line, greeting_message, help_message, - tab_line, - status_line, - colors, terminal, document, }) } /// Actually take the configuration file, open it and interpret it - pub fn read(&mut self, path: String, lua: &Lua) -> Result<()> { + pub fn read(&mut self, path: &str, lua: &Lua) -> Result<()> { // Load the default config to start with lua.load(DEFAULT_CONFIG).exec()?; // Reset plugin status based on built-in configuration file @@ -118,8 +115,8 @@ impl Config { let mut builtins: HashMap<&str, &str> = HashMap::default(); builtins.insert("pairs.lua", PAIRS); builtins.insert("autoindent.lua", AUTOINDENT); - for (name, code) in builtins.iter() { - if self.load_bi(name, user_provided_config, &lua) { + for (name, code) in &builtins { + if Self::load_bi(name, user_provided_config, lua) { lua.load(*code).exec()?; } } @@ -132,11 +129,8 @@ impl Config { } /// Decide whether to load a built-in plugin - pub fn load_bi(&self, name: &str, user_provided_config: bool, lua: &Lua) -> bool { - if !user_provided_config { - // Load when the user hasn't provided a configuration file - true - } else { + pub fn load_bi(name: &str, user_provided_config: bool, lua: &Lua) -> bool { + if user_provided_config { // Get list of user-loaded plug-ins let plugins: Vec = lua .globals() @@ -155,18 +149,21 @@ impl Config { // User doesn't want the plug-in false } + } else { + // Load when the user hasn't provided a configuration file + true } } } #[derive(Debug)] -pub struct DocumentConfig { +pub struct Document { pub tab_width: usize, pub undo_period: usize, pub wrap_cursor: bool, } -impl Default for DocumentConfig { +impl Default for Document { fn default() -> Self { Self { tab_width: 4, @@ -176,7 +173,7 @@ impl Default for DocumentConfig { } } -impl LuaUserData for DocumentConfig { +impl LuaUserData for Document { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { fields.add_field_method_get("tab_width", |_, document| Ok(document.tab_width)); fields.add_field_method_set("tab_width", |_, this, value| { diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 862ab44c..458a8336 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -40,7 +40,10 @@ impl Editor { // When the return key is pressed, we want to commit to the undo/redo stack self.doc_mut().commit(); // Perform the changes - if self.doc().loc().y != self.doc().len_lines() { + if self.doc().loc().y == self.doc().len_lines() { + // Enter pressed on the empty line at the bottom of the document + self.new_row()?; + } else { // Enter pressed in the start, middle or end of the line let loc = self.doc().char_loc(); self.exe(Event::SplitDown(loc))?; @@ -48,9 +51,6 @@ impl Editor { self.highlighter[self.ptr].insert_line(loc.y + 1, line); let line = &self.doc[self.ptr].lines[loc.y]; self.highlighter[self.ptr].edit(loc.y, line); - } else { - // Enter pressed on the empty line at the bottom of the document - self.new_row()?; } Ok(()) } @@ -114,8 +114,8 @@ impl Editor { /// Insert a new row at the end of the document if the cursor is on it fn new_row(&mut self) -> Result<()> { if self.doc().loc().y == self.doc().len_lines() { - self.exe(Event::InsertLine(self.doc().loc().y, "".to_string()))?; - self.highlighter().append(&"".to_string()); + self.exe(Event::InsertLine(self.doc().loc().y, String::new()))?; + self.highlighter().append(&String::new()); } Ok(()) } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index d9dd0885..cf7ff164 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -24,7 +24,7 @@ impl Editor { 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) as usize; + self.doc_mut().size.w = w.saturating_sub(max); // Render the tab line let tab_enabled = self.config.tab_line.borrow().enabled; if tab_enabled { @@ -33,7 +33,7 @@ impl Editor { // Run through each line of the terminal, rendering the correct line self.render_document(w, h)?; // Leave last line for status line - self.render_status_line(&lua, w, h)?; + self.render_status_line(lua, w, h)?; // Render greeting or help message if applicable if self.greet { self.render_greeting(lua, w, h)?; @@ -53,9 +53,8 @@ impl Editor { /// Render the lines of the document pub fn render_document(&mut self, _w: usize, h: usize) -> Result<()> { - for y in 0..(h as u16) { - self.terminal - .goto(0, y as usize + self.push_down as usize)?; + for y in 0..u16::try_from(h).unwrap_or(0) { + self.terminal.goto(0, y as usize + self.push_down)?; // Start background colour write!( self.terminal.stdout, @@ -99,7 +98,7 @@ impl Editor { match colour { // Success, write token Ok(col) => { - write!(self.terminal.stdout, "{}", Fg(col),)?; + write!(self.terminal.stdout, "{}", Fg(col))?; } // Failure, show error message and don't highlight this token Err(err) => { @@ -143,7 +142,7 @@ impl Editor { /// Render the tab line at the top of the document pub fn render_tab_line(&mut self, w: usize) -> Result<()> { - self.terminal.goto(0 as usize, 0 as usize)?; + self.terminal.goto(0_usize, 0_usize)?; write!( self.terminal.stdout, "{}{}", @@ -182,7 +181,7 @@ impl Editor { Bg(self.config.colors.borrow().status_bg.to_color()?), Fg(self.config.colors.borrow().status_fg.to_color()?), SetAttribute(Attribute::Bold), - self.config.status_line.borrow().render(&self, &lua, w), + self.config.status_line.borrow().render(self, lua, w), SetAttribute(Attribute::Reset), Fg(self.config.colors.borrow().editor_fg.to_color()?), Bg(self.config.colors.borrow().editor_bg.to_color()?), @@ -222,7 +221,7 @@ impl Editor { write!( self.terminal.stdout, "{}", - alinio::align::center(&line, w.saturating_sub(4)).unwrap_or_else(|| "".to_string()), + alinio::align::center(line, w.saturating_sub(4)).unwrap_or_default(), )?; } Ok(()) @@ -308,13 +307,9 @@ impl Editor { } /// Append any missed lines to the syntax highlighter - pub fn update_highlighter(&mut self) -> Result<()> { + pub fn update_highlighter(&mut self) { if self.active { - let actual = self - .doc - .get(self.ptr) - .and_then(|d| Some(d.loaded_to)) - .unwrap_or(0); + let actual = self.doc.get(self.ptr).map_or(0, |d| d.info.loaded_to); let percieved = self.highlighter().line_ref.len(); if percieved < actual { let diff = actual.saturating_sub(percieved); @@ -324,7 +319,6 @@ impl Editor { } } } - Ok(()) } /// Returns a highlighter at a certain index diff --git a/src/editor/mod.rs b/src/editor/mod.rs index b8e7c1f2..4883c54d 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -101,29 +101,29 @@ impl Editor { // If no documents were provided, create a new empty document if self.doc.is_empty() { self.blank()?; - self.greet = true && self.config.greeting_message.borrow().enabled; + self.greet = self.config.greeting_message.borrow().enabled; } Ok(()) } /// Function to open a document into the editor - pub fn open(&mut self, file_name: String) -> Result<()> { + pub fn open(&mut self, file_name: &str) -> Result<()> { let mut size = size()?; size.h = size.h.saturating_sub(1 + self.push_down); - let mut doc = Document::open(size, file_name.clone())?; + let mut doc = Document::open(size, file_name)?; doc.set_tab_width(self.config.document.borrow().tab_width); // Load all the lines within viewport into the document doc.load_to(size.h); // Update in the syntax highlighter let mut ext = file_name.split('.').last().unwrap_or(""); if ext == "oxrc" { - ext = "lua" + ext = "lua"; } let mut highlighter = self .config .syntax_highlighting .borrow() - .get_highlighter(&ext); + .get_highlighter(ext); highlighter.run(&doc.lines); self.highlighter.push(highlighter); doc.undo_mgmt.saved(); @@ -135,26 +135,26 @@ impl Editor { /// Function to ask the user for a file to open pub fn open_document(&mut self) -> Result<()> { let path = self.prompt("File to open")?; - self.open(path)?; + self.open(&path)?; self.ptr = self.doc.len().saturating_sub(1); Ok(()) } /// Function to try opening a document, and if it doesn't exist, create it pub fn open_or_new(&mut self, file_name: String) -> Result<()> { - let file = self.open(file_name.clone()); + let file = self.open(&file_name); if let Err(OxError::Kaolinite(KError::Io(ref os))) = file { if os.kind() == ErrorKind::NotFound { self.blank()?; let binding = file_name.clone(); let ext = binding.split('.').last().unwrap_or(""); self.doc.last_mut().unwrap().file_name = Some(file_name); - self.doc.last_mut().unwrap().modified = true; + self.doc.last_mut().unwrap().info.modified = true; let highlighter = self .config .syntax_highlighting .borrow() - .get_highlighter(&ext); + .get_highlighter(ext); *self.highlighter.last_mut().unwrap() = highlighter; self.highlighter .last_mut() @@ -190,9 +190,9 @@ impl Editor { .config .syntax_highlighting .borrow() - .get_highlighter(&ext); + .get_highlighter(ext); self.doc_mut().file_name = Some(file_name.clone()); - self.doc_mut().modified = false; + self.doc_mut().info.modified = false; } // Commit events to event manager (for undo / redo) self.doc_mut().commit(); @@ -203,12 +203,12 @@ impl Editor { /// Save all the open documents to the disk pub fn save_all(&mut self) -> Result<()> { - for doc in self.doc.iter_mut() { + for doc in &mut self.doc { doc.save()?; // Commit events to event manager (for undo / redo) doc.commit(); } - self.feedback = Feedback::Info(format!("Saved all documents")); + self.feedback = Feedback::Info("Saved all documents".to_string()); Ok(()) } @@ -218,7 +218,7 @@ impl Editor { // If there are still documents open, only close the requested document if self.active { let msg = "This document isn't saved, press Ctrl + Q to force quit or Esc to cancel"; - if !self.doc().modified || self.confirm(msg)? { + if !self.doc().info.modified || self.confirm(msg)? { self.doc.remove(self.ptr); self.highlighter.remove(self.ptr); self.prev(); @@ -263,8 +263,8 @@ impl Editor { } /// Load the configuration values - pub fn load_config(&mut self, path: String, lua: &Lua) -> Result<()> { - self.config_path = path.clone(); + pub fn load_config(&mut self, path: &str, lua: &Lua) -> Result<()> { + self.config_path = path.to_string(); let result = self.config.read(path, lua); // Display any warnings if the user configuration couldn't be found if let Err(OxError::Config(msg)) = result { @@ -273,14 +273,10 @@ impl Editor { self.feedback = Feedback::Warning(warn); } } else { - result? + result?; }; // Calculate the correct push down based on config - self.push_down = if self.config.tab_line.borrow().enabled { - 1 - } else { - 0 - }; + self.push_down = usize::from(self.config.tab_line.borrow().enabled); Ok(()) } @@ -294,7 +290,7 @@ impl Editor { CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, CEvent::Resize(w, h) => self.handle_resize(w, h), CEvent::Mouse(mouse_event) => self.handle_mouse_event(mouse_event), - CEvent::Paste(text) => self.handle_paste(text)?, + CEvent::Paste(text) => self.handle_paste(&text)?, _ => (), } Ok(()) @@ -328,14 +324,14 @@ impl Editor { 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(max as u16) as usize; + 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; let max = self.doc().offset.x + self.doc().size.h; self.doc_mut().load_to(max + 1); } /// Handle paste - pub fn handle_paste(&mut self, text: String) -> Result<()> { + pub fn handle_paste(&mut self, text: &str) -> Result<()> { for ch in text.chars() { self.character(ch)?; } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index f7c00d41..90cb770a 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -12,12 +12,12 @@ enum MouseLocation { impl Editor { fn find_mouse_location(&mut self, event: MouseEvent) -> MouseLocation { let tab_enabled = self.config.tab_line.borrow().enabled; - let tab = if tab_enabled { 1 } else { 0 }; + let tab = usize::from(tab_enabled); if event.row == 0 && tab_enabled { let mut c = event.column + 2; for (i, doc) in self.doc.iter().enumerate() { let header_len = self.config.tab_line.borrow().render(doc).len() + 1; - c = c.saturating_sub(header_len as u16); + c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); if c == 0 { return MouseLocation::Tabs(i); } @@ -55,15 +55,12 @@ impl Editor { MouseLocation::Tabs(_) | MouseLocation::Out => (), }, MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { - match self.find_mouse_location(event) { - MouseLocation::File(_) => { - if event.kind == MouseEventKind::ScrollDown { - self.doc_mut().scroll_down(); - } else { - self.doc_mut().scroll_up(); - } + if let MouseLocation::File(_) = self.find_mouse_location(event) { + if event.kind == MouseEventKind::ScrollDown { + self.doc_mut().scroll_down(); + } else { + self.doc_mut().scroll_up(); } - _ => (), } } MouseEventKind::ScrollLeft => { diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index e3acac52..b40a9be1 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -47,7 +47,7 @@ impl Editor { _ => (), } } - self.update_highlighter()?; + self.update_highlighter(); } Ok(()) } @@ -57,7 +57,7 @@ impl Editor { let mtch = self.doc_mut().next_match(target, 1)?; self.doc_mut().move_to(&mtch.loc); // Update highlighting - self.update_highlighter().ok()?; + self.update_highlighter(); Some(mtch.text) } @@ -66,7 +66,7 @@ impl Editor { let mtch = self.doc_mut().prev_match(target)?; self.doc_mut().move_to(&mtch.loc); // Update highlighting - self.update_highlighter().ok()?; + self.update_highlighter(); Some(mtch.text) } @@ -90,7 +90,7 @@ impl Editor { // Exit if there are no matches in the document return Ok(()); } - self.update_highlighter()?; + self.update_highlighter(); // Enter into the replace menu while !done { // Render just the document part @@ -128,7 +128,7 @@ impl Editor { } } // Update syntax highlighter if necessary - self.update_highlighter()?; + self.update_highlighter(); } Ok(()) } @@ -142,7 +142,7 @@ impl Editor { self.doc_mut().replace(loc, text, into)?; self.doc_mut().move_to(&loc); // Update syntax highlighter - self.update_highlighter()?; + self.update_highlighter(); self.highlighter[self.ptr].edit(loc.y, &self.doc[self.ptr].lines[loc.y]); Ok(()) } @@ -155,7 +155,7 @@ impl Editor { self.doc_mut().move_to(&Loc::at(0, 0)); while let Some(mtch) = self.doc_mut().next_match(target, 1) { drop(self.doc_mut().replace(mtch.loc, &mtch.text, into)); - drop(self.update_highlighter()); + self.update_highlighter(); self.highlighter[self.ptr].edit(mtch.loc.y, &self.doc[self.ptr].lines[mtch.loc.y]); } } diff --git a/src/main.rs b/src/main.rs index 3eaac89b..40a4df4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![warn(clippy::all, clippy::pedantic)] + mod cli; mod config; mod editor; @@ -25,20 +27,20 @@ fn main() { // Handle help and version options cli.basic_options(); - let result = run(cli); + let result = run(&cli); if let Err(err) = result { - panic!("{:?}", err); + panic!("{err:?}"); } } -fn run(cli: CommandLineInterface) -> Result<()> { +fn run(cli: &CommandLineInterface) -> Result<()> { // Create lua interpreter let lua = Lua::new(); // Create editor let editor = match Editor::new(&lua) { Ok(editor) => editor, - Err(error) => panic!("Editor failed to start: {:?}", error), + Err(error) => panic!("Editor failed to start: {error:?}"), }; // Push editor into lua @@ -49,7 +51,7 @@ fn run(cli: CommandLineInterface) -> Result<()> { lua.load(PLUGIN_BOOTSTRAP).exec()?; if editor .borrow_mut() - .load_config(cli.config_path, &lua) + .load_config(&cli.config_path, &lua) .is_err() { editor.borrow_mut().feedback = @@ -61,8 +63,8 @@ fn run(cli: CommandLineInterface) -> Result<()> { // Open the file editor.borrow_mut().open_or_new(file.to_string())?; // Set read only if applicable - if cli.read_only { - editor.borrow_mut().get_doc(c).read_only = true; + if cli.flags.read_only { + editor.borrow_mut().get_doc(c).info.read_only = true; } // Set highlighter if applicable if let Some(ref ext) = cli.file_type { @@ -71,27 +73,26 @@ fn run(cli: CommandLineInterface) -> Result<()> { .config .syntax_highlighting .borrow() - .get_highlighter(&ext); + .get_highlighter(ext); highlighter.run(&editor.borrow_mut().get_doc(c).lines); *editor.borrow_mut().get_highlighter(c) = highlighter; } } // Handle stdin if applicable - if cli.stdin { - if let Some(stdin) = cli::get_stdin() { - editor.borrow_mut().blank()?; - let this_doc = editor.borrow_mut().doc_len().saturating_sub(1); - let mut holder = editor.borrow_mut(); - let doc = holder.get_doc(this_doc); - doc.exe(Event::Insert(Loc { x: 0, y: 0 }, stdin))?; - doc.load_to(doc.size.h); - let lines = doc.lines.clone(); - let hl = holder.get_highlighter(this_doc); - hl.run(&lines); - if cli.read_only { - editor.borrow_mut().get_doc(this_doc).read_only = true; - } + if cli.flags.stdin { + let stdin = cli::get_stdin(); + editor.borrow_mut().blank()?; + let this_doc = editor.borrow_mut().doc_len().saturating_sub(1); + let mut holder = editor.borrow_mut(); + let doc = holder.get_doc(this_doc); + doc.exe(Event::Insert(Loc { x: 0, y: 0 }, stdin))?; + doc.load_to(doc.size.h); + let lines = doc.lines.clone(); + let hl = holder.get_highlighter(this_doc); + hl.run(&lines); + if cli.flags.read_only { + editor.borrow_mut().get_doc(this_doc).info.read_only = true; } } @@ -99,7 +100,7 @@ fn run(cli: CommandLineInterface) -> Result<()> { editor.borrow_mut().new_if_empty()?; // Run plug-ins - handle_lua_error(&editor, "", lua.load(PLUGIN_RUN).exec())?; + handle_lua_error(&editor, "", lua.load(PLUGIN_RUN).exec()); // Run the editor and handle errors if applicable editor.borrow_mut().init()?; @@ -114,7 +115,7 @@ fn run(cli: CommandLineInterface) -> Result<()> { let key_str = key_to_string(key.modifiers, key.code); let code = run_key_before(&key_str); let result = lua.load(&code).exec(); - handle_lua_error(&editor, &key_str, result)?; + handle_lua_error(&editor, &key_str, result); } // Actually handle editor event (errors included) @@ -127,16 +128,16 @@ fn run(cli: CommandLineInterface) -> Result<()> { let key_str = key_to_string(key.modifiers, key.code); let code = run_key(&key_str); let result = lua.load(&code).exec(); - handle_lua_error(&editor, &key_str, result)?; + handle_lua_error(&editor, &key_str, result); } - editor.borrow_mut().update_highlighter()?; + editor.borrow_mut().update_highlighter(); editor.borrow_mut().greet = false; // Check for any commands to run let command = editor.borrow().command.clone(); if let Some(command) = command { - run_editor_command(&editor, command, &lua)?; + run_editor_command(&editor, &command, &lua); } editor.borrow_mut().command = None; } @@ -146,21 +147,17 @@ fn run(cli: CommandLineInterface) -> Result<()> { Ok(()) } -fn handle_lua_error( - editor: &Rc>, - key_str: &str, - error: RResult<(), mlua::Error>, -) -> Result<()> { +fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult<(), mlua::Error>) { match error { // All good - Ok(_) => (), + Ok(()) => (), // Handle a runtime error Err(RuntimeError(msg)) => { - let msg = msg.split('\n').nth(0).unwrap_or("No Message Text"); + let msg = msg.split('\n').next().unwrap_or("No Message Text"); if msg.ends_with("key not bound") { // Key was not bound, issue a warning would be helpful - let key_str = key_str.replace(" ", "space"); - if key_str.contains(&"_") && key_str != "_" && !key_str.starts_with("shift") { + let key_str = key_str.replace(' ', "space"); + if key_str.contains('_') && key_str != "_" && !key_str.starts_with("shift") { editor.borrow_mut().feedback = Feedback::Warning(format!("The key {key_str} is not bound")); } @@ -170,29 +167,24 @@ fn handle_lua_error( Feedback::Error(format!("The command '{key_str}' is not defined")); } else { // Some other runtime error - editor.borrow_mut().feedback = Feedback::Error(format!("{msg}")); + editor.borrow_mut().feedback = Feedback::Error(msg.to_string()); } } // Other miscellaneous error Err(err) => { editor.borrow_mut().feedback = - Feedback::Error(format!("Failed to run Lua code: {err:?}")) + Feedback::Error(format!("Failed to run Lua code: {err:?}")); } } - Ok(()) } // Run a command in the editor -fn run_editor_command(editor: &Rc>, cmd: String, lua: &Lua) -> Result<()> { - let cmd = cmd.replace("'", "\\'").to_string(); - match cmd.split(' ').collect::>().as_slice() { - [subcmd, arguments @ ..] => { - let arguments = arguments.join("', '"); - let code = - format!("(commands['{subcmd}'] or error('command not found'))({{'{arguments}'}})"); - handle_lua_error(editor, subcmd, lua.load(code).exec())?; - } - _ => (), +fn run_editor_command(editor: &Rc>, cmd: &str, lua: &Lua) { + let cmd = cmd.replace('\'', "\\'").to_string(); + if let [subcmd, arguments @ ..] = cmd.split(' ').collect::>().as_slice() { + let arguments = arguments.join("', '"); + let code = + format!("(commands['{subcmd}'] or error('command not found'))({{'{arguments}'}})"); + handle_lua_error(editor, subcmd, lua.load(code).exec()); } - Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index 1e04619c..2670d1b9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,4 @@ -use crate::config::{Colors, TerminalConfig}; +use crate::config::{Colors, Terminal as TerminalConfig}; use crate::error::Result; use base64::prelude::*; use crossterm::{ @@ -55,24 +55,24 @@ impl Feedback { Fg(colors.error_fg.to_color()?), Bg(colors.error_bg.to_color()?) ), - Self::None => "".to_string(), + Self::None => String::new(), }; - let empty = "".to_string(); + let empty = String::new(); let msg = match self { - Self::Info(msg) => msg, - Self::Warning(msg) => msg, - Self::Error(msg) => msg, + Self::Info(msg) | Self::Warning(msg) | Self::Error(msg) => msg, Self::None => &empty, }; - let end_fg = Fg(colors.editor_fg.to_color()?).to_string(); - let end_bg = Bg(colors.editor_bg.to_color()?).to_string(); + let end = format!( + "{}{}", + Bg(colors.editor_bg.to_color()?), + Fg(colors.editor_fg.to_color()?), + ); Ok(format!( - "{}{}{}{}{}{}", + "{}{}{}{}{}", SetAttribute(Attribute::Bold), start, - alinio::align::center(&msg, w).unwrap_or_else(|| "".to_string()), - end_bg, - end_fg, + alinio::align::center(msg, w).unwrap_or_default(), + end, SetAttribute(Attribute::Reset) )) } @@ -103,7 +103,7 @@ impl Terminal { EnableBracketedPaste ) .unwrap(); - eprintln!("{}", e); + eprintln!("{e}"); })); execute!( self.stdout, @@ -152,7 +152,13 @@ impl Terminal { pub fn goto>(&mut self, x: Num, y: Num) -> Result<()> { let x: usize = x.into(); let y: usize = y.into(); - execute!(self.stdout, MoveTo(x as u16, y as u16))?; + execute!( + self.stdout, + MoveTo( + u16::try_from(x).unwrap_or(u16::MAX), + u16::try_from(y).unwrap_or(u16::MAX) + ) + )?; Ok(()) } diff --git a/test.sh b/test.sh index 979ce194..2bdf3b6e 100644 --- a/test.sh +++ b/test.sh @@ -1 +1 @@ -cargo tarpaulin -o Html --ignore-tests --skip-clean --workspace --exclude-files src/* src/editor/* kaolinite/examples/cactus/src/* +cargo tarpaulin -o Html --ignore-tests --skip-clean --workspace --exclude-files src/* src/editor/* src/config/* kaolinite/examples/cactus/src/* From 2b0d8b69594e8eee4051ab0be7793db3a651b749 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 00:02:13 +0100 Subject: [PATCH 12/31] added in fuzz test to detect panics --- kaolinite/tests/test.rs | 99 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/kaolinite/tests/test.rs b/kaolinite/tests/test.rs index 19f6b2e9..c5b2ab0a 100644 --- a/kaolinite/tests/test.rs +++ b/kaolinite/tests/test.rs @@ -805,6 +805,105 @@ fn file_paths() { assert_eq!(get_file_ext("src/document.rs"), Some(st!("rs"))); } +#[test] +fn fuzz() { + for _ in 0..20 { + println!("--"); + let size = Size { w: 10, h: 8 }; + let mut doc = Document::open(size, "tests/data/unicode.txt").unwrap(); + doc.load_to(100); + println!("{} | {}", doc.loc().x, doc.char_ptr); + for _ in 0..500 { + let e = rand::random::() % 25; + println!("{}", e); + match e { + 0 => doc.forth(Event::Insert(doc.char_loc(), 'a'.to_string())), + 1 => doc.forth(Event::Insert(doc.char_loc(), 'b'.to_string())), + 2 => doc.forth(Event::Insert(doc.char_loc(), '在'.to_string())), + 3 => doc.forth(Event::Delete( + Loc { + x: doc.char_ptr.saturating_sub(1), + y: doc.char_loc().y, + }, + ' '.to_string(), + )), + 4 => doc.forth(Event::InsertLine(doc.loc().y, "surpri在se".to_string())), + 5 => doc.forth(Event::DeleteLine(doc.loc().y, "".to_string())), + 6 => doc.forth(Event::SplitDown(doc.char_loc())), + 7 => doc.forth(Event::SpliceUp(Loc { + x: 0, + y: doc.loc().y, + })), + 8 => { + doc.move_left(); + Ok(()) + } + 9 => { + doc.move_right(); + Ok(()) + } + 10 => { + doc.move_up(); + Ok(()) + } + 11 => { + doc.move_down(); + Ok(()) + } + 12 => { + doc.move_end(); + Ok(()) + } + 13 => { + doc.move_home(); + Ok(()) + } + 14 => { + doc.move_top(); + Ok(()) + } + 15 => { + doc.move_bottom(); + Ok(()) + } + 16 => { + doc.move_page_up(); + Ok(()) + } + 17 => { + doc.move_page_down(); + Ok(()) + } + 18 => { + doc.move_prev_word(); + Ok(()) + } + 19 => { + doc.move_next_word(); + Ok(()) + } + 20 => { + doc.replace_all("a", "c"); + Ok(()) + } + 21 => { + doc.commit(); + Ok(()) + } + 22 => { + doc.commit(); + Ok(()) + } + 23 => doc.undo(), + 24 => doc.redo(), + _ => Ok(()), + }; + println!("{} | {}", doc.loc().x, doc.char_ptr); + doc.load_to(doc.len_lines() + 10); + } + } +} + /* Template: From 0f7a176378c192ab61ac60894f3c8349aba7802d Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:53:17 +0100 Subject: [PATCH 13/31] Cleaned up plug-ins and improved plug-in error reporting --- plugins/autoindent.lua | 50 +++++++++--------- plugins/pairs.lua | 114 ++++++++++++++--------------------------- src/main.rs | 24 ++++++++- 3 files changed, 85 insertions(+), 103 deletions(-) diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index cdbec970..5f6396a0 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,35 +1,10 @@ --[[ Auto Indent v0.6 -You will be able to press return at the start of a block and have -Ox automatically indent for you. - -By default, it will indent whenever you press the enter key with -the character to the left of the cursor being an opening bracket or -other syntax that indicates a block has started e.g. ":" in python +Helps you when programming by guessing where indentation should go +and then automatically applying these guesses as you program ]]-- -event_mapping["enter"] = function() - -- Indent where appropriate - if autoindent:causes_indent(editor.cursor.y - 1) then - local new_level = autoindent:get_indent(editor.cursor.y) + 1 - autoindent:set_indent(editor.cursor.y, new_level) - end - -- Give newly created line a boost to match it up relatively with the line before it - local added_level = autoindent:get_indent(editor.cursor.y) + autoindent:get_indent(editor.cursor.y - 1) - autoindent:set_indent(editor.cursor.y, added_level) - -- Handle the case where enter is pressed, creating a multi-line block that requires neatening up - autoindent:disperse_block() -end - -event_mapping["*"] = function() - -- Dedent where appropriate - if autoindent:causes_dedent(editor.cursor.y) then - local new_level = autoindent:get_indent(editor.cursor.y) - 1 - autoindent:set_indent(editor.cursor.y, new_level) - end -end - autoindent = {} -- Determine if a line starts with a certain string @@ -179,3 +154,24 @@ function autoindent:disperse_block() editor:move_to(old_cursor.x, old_cursor.y) end end + +event_mapping["enter"] = function() + -- Indent where appropriate + if autoindent:causes_indent(editor.cursor.y - 1) then + local new_level = autoindent:get_indent(editor.cursor.y) + 1 + autoindent:set_indent(editor.cursor.y, new_level) + end + -- Give newly created line a boost to match it up relatively with the line before it + local added_level = autoindent:get_indent(editor.cursor.y) + autoindent:get_indent(editor.cursor.y - 1) + autoindent:set_indent(editor.cursor.y, added_level) + -- Handle the case where enter is pressed, creating a multi-line block that requires neatening up + autoindent:disperse_block() +end + +event_mapping["*"] = function() + -- Dedent where appropriate + if autoindent:causes_dedent(editor.cursor.y) then + local new_level = autoindent:get_indent(editor.cursor.y) - 1 + autoindent:set_indent(editor.cursor.y, new_level) + end +end diff --git a/plugins/pairs.lua b/plugins/pairs.lua index 9361829b..84dd64b0 100644 --- a/plugins/pairs.lua +++ b/plugins/pairs.lua @@ -1,56 +1,58 @@ --[[ -Bracket Pairs v0.4 +Bracket Pairs v0.5 -This will automatically insert a closing bracket or quote -when you type an opening one +Automatically insert and delete brackets and quotes where appropriate +Also helps when you want to pad out brackets and quotes with whitespace ]]-- +autopairs = {} + -- The following pairs are in the form [start of pair][end of pair] -pairings = { +autopairs.pairings = { -- Bracket pairs "()", "[]", "{}", -- Quote pairs '""', "''", "``", - -- Other pairs you wish to define can be added below... } -just_paired = { x = nil, y = nil } -was_pasting = editor.pasting -line_cache = { y = editor.cursor.y, line = editor:get_line() } +autopairs.just_paired = { x = nil, y = nil } -event_mapping["*"] = function() - -- If the editor is pasting, try to determine the first character of the paste - if editor.pasting and not was_pasting then - local first_paste = editor:get_character_at(editor.cursor.x - 2, editor.cursor.y) - local between_pasting = false - for _, str in ipairs(pairings) do - if string.sub(str, 1, 1) == first_paste then - between_pasting = true - end - end - if between_pasting then - -- Fix rogue paste - editor:remove_at(editor.cursor.x, editor.cursor.y) +-- Determine whether we are currently inside a pair +function autopairs:in_pair() + local first = editor:get_character_at(editor.cursor.x - 1, editor.cursor.y) + local second = editor:get_character_at(editor.cursor.x, editor.cursor.y) + local potential_pair = first .. second + for _, v in ipairs(autopairs.pairings) do + if v == potential_pair then + return true end end - was_pasting = editor.pasting - local changed_line = line_cache.y ~= editor.cursor.y; - local potential_backspace = not changed_line and string.len(line_cache.line) - 1 == string.len(editor:get_line()); - if changed_line or not potential_backspace then - line_cache = { y = editor.cursor.y, line = editor:get_line() } + return false +end + +-- Automatically delete end pair if user deletes corresponding start pair +event_mapping["before:backspace"] = function() + if autopairs:in_pair() then + editor:remove_at(editor.cursor.x, editor.cursor.y) + end +end + +-- Automatically insert an extra space if the user presses space between pairs +event_mapping["before:space"] = function() + if autopairs:in_pair() then + editor:insert(" ") + editor:move_left() end end -- Link up pairs to event mapping -for i, str in ipairs(pairings) do +for i, str in ipairs(autopairs.pairings) do local start_pair = string.sub(str, 1, 1) local end_pair = string.sub(str, 2, 2) -- Determine which implementation to use if start_pair == end_pair then -- Handle hybrid start_pair and end_pair event_mapping[start_pair] = function() - -- Return if the user is currently pasting text - if editor.pasting then return end -- Check if there is a matching start pair local at_char = ' ' if editor.cursor.x > 1 then @@ -58,79 +60,41 @@ for i, str in ipairs(pairings) do end local potential_dupe = at_char == start_pair -- Check if we're at the site of the last pairing - local at_immediate_pair_x = just_paired.x == editor.cursor.x - 1 - local at_immediate_pair_y = just_paired.y == editor.cursor.y + local at_immediate_pair_x = autopairs.just_paired.x == editor.cursor.x - 1 + local at_immediate_pair_y = autopairs.just_paired.y == editor.cursor.y local at_immediate_pair = at_immediate_pair_x and at_immediate_pair_y if potential_dupe and at_immediate_pair then -- User just tried to add a closing pair despite us doing it for them! -- Undo it for them editor:remove_at(editor.cursor.x - 1, editor.cursor.y) - just_paired = { x = nil, y = nil } - line_cache = { y = editor.cursor.y, line = editor:get_line() } + autopairs.just_paired = { x = nil, y = nil } else - just_paired = editor.cursor + autopairs.just_paired = editor.cursor editor:insert(end_pair) editor:move_left() - line_cache = { y = editor.cursor.y, line = editor:get_line() } end end else -- Handle traditional pairs event_mapping[end_pair] = function() - -- Return if the user is currently pasting text - if editor.pasting then return end -- Check if there is a matching start pair local at_char = editor:get_character_at(editor.cursor.x - 2, editor.cursor.y) local potential_dupe = at_char == start_pair -- Check if we're at the site of the last pairing - local at_immediate_pair_x = just_paired.x == editor.cursor.x - 1 - local at_immediate_pair_y = just_paired.y == editor.cursor.y + local at_immediate_pair_x = autopairs.just_paired.x == editor.cursor.x - 1 + local at_immediate_pair_y = autopairs.just_paired.y == editor.cursor.y local at_immediate_pair = at_immediate_pair_x and at_immediate_pair_y if potential_dupe and at_immediate_pair then -- User just tried to add a closing pair despite us doing it for them! -- Undo it for them editor:remove_at(editor.cursor.x - 1, editor.cursor.y) - just_paired = { x = nil, y = nil } - line_cache = { y = editor.cursor.y, line = editor:get_line() } + autopairs.just_paired = { x = nil, y = nil } end end event_mapping[start_pair] = function() - -- Return if the user is currently pasting text - if editor.pasting then return end - just_paired = editor.cursor + autopairs.just_paired = editor.cursor editor:insert(end_pair) editor:move_left() - line_cache = { y = editor.cursor.y, line = editor:get_line() } - end - end -end - -function includes(array, value) - for _, v in ipairs(array) do - if v == value then - return true -- Value found end end - return false -- Value not found -end - --- Automatically delete pairs -event_mapping["backspace"] = function() - local old_line = line_cache.line - local potential_pair = string.sub(old_line, editor.cursor.x + 1, editor.cursor.x + 2) - if includes(pairings, potential_pair) then - editor:remove_at(editor.cursor.x, editor.cursor.y) - line_cache = { y = editor.cursor.y, line = editor:get_line() } - end -end - --- Space out pairs when pressing space between pairs -event_mapping["space"] = function() - local first = editor:get_character_at(editor.cursor.x - 2, editor.cursor.y) - local second = editor:get_character_at(editor.cursor.x, editor.cursor.y) - local potential_pair = first .. second - if includes(pairings, potential_pair) then - editor:insert(" ") - editor:move_left() - end end diff --git a/src/main.rs b/src/main.rs index 40a4df4e..e975baba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use crossterm::event::Event as CEvent; use editor::Editor; use error::Result; use kaolinite::event::Event; +use kaolinite::searching::Searcher; use kaolinite::Loc; use mlua::Error::RuntimeError; use mlua::Lua; @@ -153,7 +154,28 @@ fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult< Ok(()) => (), // Handle a runtime error Err(RuntimeError(msg)) => { - let msg = msg.split('\n').next().unwrap_or("No Message Text"); + let msg = msg.split('\n').collect::>(); + // Extract description + let description = msg.get(0).unwrap_or(&"No Message Text"); + // See if there is any additional error location information + let mut error_line_finder = Searcher::new(r"^\s*(.+:\d+):.*$"); + let location_line = msg + .iter() + .skip(1) + .position(|line| error_line_finder.lfind(line).is_some()); + let msg = if let Some(trace) = location_line { + // There is additional line info, attach it + let location = msg[trace + 1] + .to_string() + .trim() + .split(':') + .take(2) + .collect::>() + .join(" on line "); + format!("{location}: {description}") + } else { + description.to_string() + }; if msg.ends_with("key not bound") { // Key was not bound, issue a warning would be helpful let key_str = key_str.replace(' ', "space"); From 1643f2ebc28c934a87424e1c617a3f47db25180f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:00:49 +0100 Subject: [PATCH 14/31] Further improved error reporting by avoiding unnecessary location information --- src/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index e975baba..d855fd1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,13 +156,18 @@ fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult< Err(RuntimeError(msg)) => { let msg = msg.split('\n').collect::>(); // Extract description - let description = msg.get(0).unwrap_or(&"No Message Text"); + let description = msg.first().unwrap_or(&"No Message Text"); // See if there is any additional error location information let mut error_line_finder = Searcher::new(r"^\s*(.+:\d+):.*$"); - let location_line = msg + let mut location_line = msg .iter() .skip(1) .position(|line| error_line_finder.lfind(line).is_some()); + // Don't attach additional location if description already includes it + if error_line_finder.lfind(description).is_some() { + location_line = None; + } + // Put together the message (attaching location if not already provided) let msg = if let Some(trace) = location_line { // There is additional line info, attach it let location = msg[trace + 1] @@ -174,7 +179,7 @@ fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult< .join(" on line "); format!("{location}: {description}") } else { - description.to_string() + (*description).to_string() }; if msg.ends_with("key not bound") { // Key was not bound, issue a warning would be helpful From 56515900ac8b688f79af1fa1e5e2e29269ca1072 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:29:57 +0100 Subject: [PATCH 15/31] Better configuration file error reporting --- src/config/editor.rs | 2 +- src/editor/mod.rs | 24 ++++++++++++++---------- src/main.rs | 28 +++++++++++++++++++--------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index a47d2849..bef8e4f9 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -42,7 +42,7 @@ impl LuaUserData for Editor { // Reload the configuration file methods.add_method_mut("reload_config", |lua, editor, ()| { let path = editor.config_path.clone(); - if editor.load_config(&path, lua).is_err() { + if editor.load_config(&path, lua).is_some() { editor.feedback = Feedback::Error("Failed to reload config".to_string()); } Ok(()) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 4883c54d..9e33c368 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -4,7 +4,7 @@ use crate::ui::{size, Feedback, Terminal}; use crossterm::event::{Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}; use kaolinite::event::Error as KError; use kaolinite::Document; -use mlua::Lua; +use mlua::{Error as LuaError, Lua}; use std::io::ErrorKind; use std::time::Instant; use synoptic::Highlighter; @@ -263,21 +263,25 @@ impl Editor { } /// Load the configuration values - pub fn load_config(&mut self, path: &str, lua: &Lua) -> Result<()> { + pub fn load_config(&mut self, path: &str, lua: &Lua) -> Option { self.config_path = path.to_string(); let result = self.config.read(path, lua); // Display any warnings if the user configuration couldn't be found - if let Err(OxError::Config(msg)) = result { - if msg == "Not Found" { - let warn = "No configuration file found, using default configuration".to_string(); - self.feedback = Feedback::Warning(warn); + match result { + Ok(_) => (), + Err(OxError::Config(msg)) => { + if msg == "Not Found" { + let warn = + "No configuration file found, using default configuration".to_string(); + self.feedback = Feedback::Warning(warn); + } } - } else { - result?; - }; + Err(OxError::Lua(err)) => return Some(err), + _ => unreachable!(), + } // Calculate the correct push down based on config self.push_down = usize::from(self.config.tab_line.borrow().enabled); - Ok(()) + None } /// Handle event diff --git a/src/main.rs b/src/main.rs index d855fd1f..b9baca51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use error::Result; use kaolinite::event::Event; use kaolinite::searching::Searcher; use kaolinite::Loc; -use mlua::Error::RuntimeError; +use mlua::Error::{RuntimeError, SyntaxError}; use mlua::Lua; use std::cell::RefCell; use std::rc::Rc; @@ -50,14 +50,11 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // Load config and initialise lua.load(PLUGIN_BOOTSTRAP).exec()?; - if editor - .borrow_mut() - .load_config(&cli.config_path, &lua) - .is_err() - { - editor.borrow_mut().feedback = - Feedback::Error("Failed to load configuration file".to_string()); - } + let result = editor.borrow_mut().load_config(&cli.config_path, &lua); + if let Some(err) = result { + // Handle error if available + handle_lua_error(&editor, "configuration", Err(err)); + }; // Open files user has asked to open for (c, file) in cli.to_open.iter().enumerate() { @@ -197,6 +194,19 @@ fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult< editor.borrow_mut().feedback = Feedback::Error(msg.to_string()); } } + // Handle a syntax error + Err(SyntaxError { message, .. }) => { + if key_str == "configuration" { + let mut message = message.rsplit(':').take(2).collect::>(); + message.reverse(); + let message = message.join(":"); + editor.borrow_mut().feedback = + Feedback::Error(format!("Syntax Error in config file on line {message}")); + } else { + editor.borrow_mut().feedback = + Feedback::Error(format!("Syntax Error: {message:?}")); + } + } // Other miscellaneous error Err(err) => { editor.borrow_mut().feedback = From 7a1d98f396fdaeacb2a9bae4c345977a1e0b4a16 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:44:03 +0100 Subject: [PATCH 16/31] Fixed strange backspace behaviour --- src/editor/editing.rs | 2 +- src/editor/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 458a8336..97b86a1b 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -78,7 +78,7 @@ impl Editor { self.exe(Event::SpliceUp(loc))?; let line = &self.doc[self.ptr].lines[loc.y]; self.highlighter[self.ptr].edit(loc.y, line); - } else { + } else if !(c == 0 && on_first_line) { // Backspace was pressed in the middle of the line, delete the character c = c.saturating_sub(1); if let Some(line) = self.doc().line(self.doc().loc().y) { diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 9e33c368..e1aec201 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -268,7 +268,7 @@ impl Editor { let result = self.config.read(path, lua); // Display any warnings if the user configuration couldn't be found match result { - Ok(_) => (), + Ok(()) => (), Err(OxError::Config(msg)) => { if msg == "Not Found" { let warn = From a4ff28ba76a5bc59ee0c2727e191c4bbfd6a4829 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:47:53 +0100 Subject: [PATCH 17/31] Fixed hex codes giving the incorrect colour --- src/config/colors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/colors.rs b/src/config/colors.rs index 404cbee8..f3e24262 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -332,7 +332,7 @@ impl Color { for i in 0..3 { let section = &hex[(i * 2)..(i * 2 + 2)]; if let Ok(val) = u8::from_str_radix(section, 16) { - tri.insert(0, val); + tri.push(val); } else { let msg = "Invalid hex code used in configuration file - ensure all digits are between 0 and F"; return Err(OxError::Config(msg.to_string())); From 478e071b3e8265b08ca9f93b506d2b8321878524 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 13:00:35 +0100 Subject: [PATCH 18/31] Added ANSI colour code support to configuration file --- src/config/colors.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/config/colors.rs b/src/config/colors.rs index f3e24262..7864dafe 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -186,6 +186,7 @@ impl LuaUserData for Colors { pub enum Color { Rgb(u8, u8, u8), Hex(String), + Ansi(u8), Black, DarkGrey, Red, @@ -244,6 +245,17 @@ impl Color { } Self::Rgb(tri[0], tri[1], tri[2]) } + LuaValue::Integer(number) => { + if (0..=255).contains(&number) { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + Self::Ansi(number as u8) + } else { + issue_warning( + "ANSI colour codes must be between 0-255 inclusively, defaulting to black", + ); + Self::Ansi(0) + } + } _ => { issue_warning("Invalid data type used for colour in configuration file"); Self::Transparent @@ -266,6 +278,7 @@ impl Color { let _ = table.push(*b as isize); LuaValue::Table(table) } + Color::Ansi(code) => LuaValue::Integer(i64::from(*code)), Color::Black => LuaValue::String(env.create_string("black").expect(msg)), Color::DarkGrey => LuaValue::String(env.create_string("darkgrey").expect(msg)), Color::Red => LuaValue::String(env.create_string("red").expect(msg)), @@ -297,6 +310,7 @@ impl Color { g: *g, b: *b, }, + Color::Ansi(code) => CColor::AnsiValue(*code), Color::Black => CColor::Black, Color::DarkGrey => CColor::DarkGrey, Color::Red => CColor::Red, From a44b10e7a10fee5dc2fd27d0c9c868035fc513de Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:49:02 +0100 Subject: [PATCH 19/31] Added networking to plug-in api --- src/plugin/bootstrap.lua | 3 +++ src/plugin/networking.lua | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/plugin/networking.lua diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 3b88dbc1..c6d54cf2 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -14,6 +14,9 @@ plugins = {} builtins = {} plugin_issues = false +-- Import plug-in api components +http = require('src/plugin/networking') + function load_plugin(base) path_cross = base path_unix = home .. "/.config/ox/" .. base diff --git a/src/plugin/networking.lua b/src/plugin/networking.lua new file mode 100644 index 00000000..040749d4 --- /dev/null +++ b/src/plugin/networking.lua @@ -0,0 +1,35 @@ +-- Networking library (for plug-ins to use) +-- Requires curl to be installed + +http = {} + +function http.get(url) + -- Using curl for the request + local handle = io.popen("curl -s -X GET '" .. url .. "'") + local result = handle:read("*a") + handle:close() + return result +end + +function http.post(url, data) + local handle = io.popen("curl -s -X POST -d '" .. data .. "' '" .. url .. "'") + local result = handle:read("*a") + handle:close() + return result +end + +function http.put(url, data) + local handle = io.popen("curl -s -X PUT -d '" .. data .. "' '" .. url .. "'") + local result = handle:read("*a") + handle:close() + return result +end + +function http.delete(url) + local handle = io.popen("curl -s -X DELETE '" .. url .. "'") + local result = handle:read("*a") + handle:close() + return result +end + +return http From c926506bffcc338f1b94ec4d9f715f0ad392563f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 18:01:28 +0100 Subject: [PATCH 20/31] Made networking library crossplatform --- src/plugin/networking.lua | 69 +++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/plugin/networking.lua b/src/plugin/networking.lua index 040749d4..4e909f7b 100644 --- a/src/plugin/networking.lua +++ b/src/plugin/networking.lua @@ -1,35 +1,70 @@ -- Networking library (for plug-ins to use) --- Requires curl to be installed +-- Uses curl on unix based systems and powershell on windows -http = {} +http = { + backend = package.config:sub(1,1) == '\\' and 'powershell' or 'curl', +} -function http.get(url) - -- Using curl for the request - local handle = io.popen("curl -s -X GET '" .. url .. "'") +local function execute(cmd) + local handle = io.popen(cmd) local result = handle:read("*a") handle:close() return result end +function http.get(url) + -- Using curl for the request + local cmd + if http.backend == 'curl' then + cmd = "curl -s -X GET '" .. url .. "'" + else + cmd = table.concat({ + 'powershell -Command "Invoke-WebRequest -Uri \'', url, + '\' -UseBasicParsing | Select-Object -ExpandProperty Content"' + }) + end + return execute(cmd) +end + function http.post(url, data) - local handle = io.popen("curl -s -X POST -d '" .. data .. "' '" .. url .. "'") - local result = handle:read("*a") - handle:close() - return result + local cmd + if http.backend == 'curl' then + cmd = "curl -s -X POST -d '" .. data .. "' '" .. url .. "'" + else + cmd = table.concat({ + 'powershell -Command "Invoke-WebRequest -Uri \'', url, + '\' -Method POST -Body \'', data, + '\' -UseBasicParsing | Select-Object -ExpandProperty Content"' + }) + end + return execute(cmd) end function http.put(url, data) - local handle = io.popen("curl -s -X PUT -d '" .. data .. "' '" .. url .. "'") - local result = handle:read("*a") - handle:close() - return result + local cmd + if http.backend == 'curl' then + cmd = "curl -s -X PUT -d '" .. data .. "' '" .. url .. "'" + else + cmd = table.concat({ + 'powershell -Command "Invoke-WebRequest -Uri \'', url, + '\' -Method PUT -Body \'', data, + '\' -UseBasicParsing | Select-Object -ExpandProperty Content"' + }) + end + return execute(cmd) end function http.delete(url) - local handle = io.popen("curl -s -X DELETE '" .. url .. "'") - local result = handle:read("*a") - handle:close() - return result + local cmd + if http.backend == 'curl' then + cmd = "curl -s -X DELETE '" .. url .. "'" + else + cmd = table.concat({ + 'powershell -Command "Invoke-WebRequest -Uri \'', url, + '\' -Method DELETE -UseBasicParsing | Select-Object -ExpandProperty Content"' + }) + end + return execute(cmd) end return http From 55514fe72249dea6e52502fda646ae3e0eba1b8f Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 18:28:48 +0100 Subject: [PATCH 21/31] Version bump and added update notification plug-in --- Cargo.lock | 2 +- Cargo.toml | 2 +- plugins/update_notification.lua | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 plugins/update_notification.lua diff --git a/Cargo.lock b/Cargo.lock index f0873067..811f4789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.6.2" +version = "0.6.3" dependencies = [ "alinio", "base64", diff --git a/Cargo.toml b/Cargo.toml index fd1e39d3..5f3c78bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["cactus"] [package] name = "ox" -version = "0.6.2" +version = "0.6.3" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A Rust powered text editor." diff --git a/plugins/update_notification.lua b/plugins/update_notification.lua new file mode 100644 index 00000000..d1c4833e --- /dev/null +++ b/plugins/update_notification.lua @@ -0,0 +1,9 @@ +-- Get the contents of the latest Cargo.toml +local cargo_latest = http.get("https://raw.githubusercontent.com/curlpipe/ox/refs/heads/master/Cargo.toml") +-- Extract the version from the build file +local version = cargo_latest:match("version%s*=%s*\"(%d+.%d+.%d+)\"") +-- Display it to the user +if version ~= editor.version and version ~= nil then + editor:display_warning("Update to " .. version .. " is available (you have " .. editor.version .. ")") +end + From 27447b44a414253457c20b9da21b021356765533 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:01:15 +0100 Subject: [PATCH 22/31] Added rerender function to editor --- config/.oxrc | 2 ++ src/config/editor.rs | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 00bb3535..2047f9e4 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -313,3 +313,5 @@ syntax:set("list", {86, 217, 178}) -- Quotes in various markup languages e.g. _ -- Import plugins (must be at the bottom of this file) load_plugin("pairs.lua") load_plugin("autoindent.lua") +--load_plugin("pomodoro.lua") +--load_plugin("update_notification.lua") diff --git a/src/config/editor.rs b/src/config/editor.rs index bef8e4f9..ae6aa642 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -24,8 +24,6 @@ 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.doc.len())); - // DEPRECIATED - fields.add_field_method_get("help_visible", |_, _| Ok(false)); fields.add_field_method_get("document_type", |_, editor| { let ext = editor .doc() @@ -397,10 +395,6 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); - // DEPRECIATED - methods.add_method_mut("hide_help_message", |_, _, ()| Ok(())); - // DEPRECIATED - methods.add_method_mut("show_help_message", |_, _, ()| Ok(())); methods.add_method_mut("set_read_only", |_, editor, status: bool| { editor.doc_mut().info.read_only = status; Ok(()) @@ -415,6 +409,11 @@ impl LuaUserData for Editor { editor.highlighter[editor.ptr] = highlighter; Ok(()) }); + methods.add_method_mut("rerender", |lua, editor, ()| { + // If you can't render the editor, you're pretty much done for anyway + let _ = editor.render(lua); + Ok(()) + }); } } From 30057e829198f5dc3a389a9dbfd76a406ff5d569 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 20:52:45 +0100 Subject: [PATCH 23/31] Added concurrency API to plug-ins --- src/config/editor.rs | 2 ++ src/config/mod.rs | 40 ++++++++++++++++++++++++++++++++ src/config/tasks.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 14 +++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/config/tasks.rs diff --git a/src/config/editor.rs b/src/config/editor.rs index ae6aa642..cc413116 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -410,6 +410,8 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("rerender", |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 _ = editor.render(lua); Ok(()) diff --git a/src/config/mod.rs b/src/config/mod.rs index f21b349b..dabd4263 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,17 +2,20 @@ use crate::error::{OxError, Result}; use mlua::prelude::*; use std::collections::HashMap; use std::{cell::RefCell, rc::Rc}; +use std::sync::{Arc, Mutex}; mod colors; mod editor; mod highlighting; mod interface; mod keys; +mod tasks; pub use colors::{Color, Colors}; pub use highlighting::SyntaxHighlighting; pub use interface::{GreetingMessage, HelpMessage, LineNumbers, StatusLine, TabLine, Terminal}; pub use keys::{key_to_string, run_key, run_key_before}; +pub use tasks::TaskManager; // Issue a warning to the user fn issue_warning(msg: &str) { @@ -44,6 +47,7 @@ pub struct Config { pub help_message: Rc>, pub terminal: Rc>, pub document: Rc>, + pub task_manager: Arc>, } impl Config { @@ -60,6 +64,16 @@ impl Config { let terminal = Rc::new(RefCell::new(Terminal::default())); let document = Rc::new(RefCell::new(Document::default())); + // Set up the task manager + let task_manager = Arc::new(Mutex::new(TaskManager::default())); + let task_manager_clone = Arc::clone(&task_manager); + std::thread::spawn(move || { + loop { + task_manager_clone.lock().unwrap().cycle(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + // Push in configuration globals lua.globals().set("syntax", syntax_highlighting.clone())?; lua.globals().set("line_numbers", line_numbers.clone())?; @@ -72,6 +86,31 @@ impl Config { lua.globals().set("terminal", terminal.clone())?; lua.globals().set("document", document.clone())?; + // Define task list + let task_manager_clone = Arc::clone(&task_manager); + let get_task_list = lua.create_function(move |_, ()| { + Ok(format!("{:?}", task_manager_clone.lock().unwrap().execution_list())) + })?; + lua.globals().set("get_task_list", get_task_list)?; + + // Provide a function "after" to run a function after n seconds + let task_manager_clone = Arc::clone(&task_manager); + let after = lua.create_function(move |_, args: (isize, String)| { + let (delay, target) = args; + task_manager_clone.lock().unwrap().attach(delay, target, false); + Ok(()) + })?; + lua.globals().set("after", after)?; + + // Provide a function "every" to run a function every n seconds + let task_manager_clone = Arc::clone(&task_manager); + let every = lua.create_function(move |_, args: (isize, String)| { + let (delay, target) = args; + task_manager_clone.lock().unwrap().attach(delay, target, true); + Ok(()) + })?; + lua.globals().set("every", every)?; + Ok(Config { syntax_highlighting, line_numbers, @@ -82,6 +121,7 @@ impl Config { help_message, terminal, document, + task_manager, }) } diff --git a/src/config/tasks.rs b/src/config/tasks.rs new file mode 100644 index 00000000..c1f8dd53 --- /dev/null +++ b/src/config/tasks.rs @@ -0,0 +1,55 @@ +#[derive(Default, Debug)] +pub struct Task { + repeat: bool, + delay: isize, + remaining: isize, + target: String, +} + +/// A struct in charge of executing functions concurrently +#[derive(Default, Debug)] +pub struct TaskManager { + pub tasks: Vec, + pub to_execute: Vec, +} + +impl TaskManager { + /// Thread to run and keep track of which tasks to execute + pub fn cycle(&mut self) { + for task in &mut self.tasks { + // Decrement remaining time + if task.remaining < 0 { + task.remaining = task.remaining.saturating_sub(1); + } + // Check if activation is required + if task.remaining == 0 { + self.to_execute.push(task.target.clone()); + // Check whether to repeat or not + if task.repeat { + // Re-load the task + task.remaining = task.delay; + } else { + // Condemn the task to decrementing forever + task.remaining = -1; + } + } + } + } + + /// Obtain a list of functions to execute (and remove them from the execution list) + pub fn execution_list(&mut self) -> Vec { + let mut new = vec![]; + std::mem::swap(&mut self.to_execute, &mut new); + new + } + + /// Define a new task + pub fn attach(&mut self, delay: isize, target: String, repeat: bool) { + self.tasks.push(Task { + remaining: delay, + delay, + target, + repeat, + }); + } +} diff --git a/src/main.rs b/src/main.rs index b9baca51..2d1d26eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,6 +105,20 @@ fn run(cli: &CommandLineInterface) -> Result<()> { while editor.borrow().active { // Render and wait for event editor.borrow_mut().render(&lua)?; + + // While waiting for an event to come along, service the task manager + while let Ok(false) = crossterm::event::poll(std::time::Duration::from_millis(100)) { + let exec = editor.borrow_mut().config.task_manager.lock().unwrap().execution_list(); + for task in exec { + if let Ok(target) = lua.globals().get::<_, mlua::Function>(task.clone()) { + // Run the code + handle_lua_error(&editor, "task", target.call(())); + } else { + editor.borrow_mut().feedback = Feedback::Warning(format!("Function '{task}' was not found")); + } + } + } + let event = crossterm::event::read()?; editor.borrow_mut().feedback = Feedback::None; From 0ae6e3ddd2c50e4ef366ddc6e19697d73900a345 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 20:53:36 +0100 Subject: [PATCH 24/31] rustfmt --- src/config/mod.rs | 25 ++++++++++++++++--------- src/main.rs | 11 +++++++++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index dabd4263..6e3ec403 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,8 @@ use crate::error::{OxError, Result}; use mlua::prelude::*; use std::collections::HashMap; -use std::{cell::RefCell, rc::Rc}; use std::sync::{Arc, Mutex}; +use std::{cell::RefCell, rc::Rc}; mod colors; mod editor; @@ -67,11 +67,9 @@ impl Config { // Set up the task manager let task_manager = Arc::new(Mutex::new(TaskManager::default())); let task_manager_clone = Arc::clone(&task_manager); - std::thread::spawn(move || { - loop { - task_manager_clone.lock().unwrap().cycle(); - std::thread::sleep(std::time::Duration::from_secs(1)); - } + std::thread::spawn(move || loop { + task_manager_clone.lock().unwrap().cycle(); + std::thread::sleep(std::time::Duration::from_secs(1)); }); // Push in configuration globals @@ -89,7 +87,10 @@ impl Config { // Define task list let task_manager_clone = Arc::clone(&task_manager); let get_task_list = lua.create_function(move |_, ()| { - Ok(format!("{:?}", task_manager_clone.lock().unwrap().execution_list())) + Ok(format!( + "{:?}", + task_manager_clone.lock().unwrap().execution_list() + )) })?; lua.globals().set("get_task_list", get_task_list)?; @@ -97,7 +98,10 @@ impl Config { let task_manager_clone = Arc::clone(&task_manager); let after = lua.create_function(move |_, args: (isize, String)| { let (delay, target) = args; - task_manager_clone.lock().unwrap().attach(delay, target, false); + task_manager_clone + .lock() + .unwrap() + .attach(delay, target, false); Ok(()) })?; lua.globals().set("after", after)?; @@ -106,7 +110,10 @@ impl Config { let task_manager_clone = Arc::clone(&task_manager); let every = lua.create_function(move |_, args: (isize, String)| { let (delay, target) = args; - task_manager_clone.lock().unwrap().attach(delay, target, true); + task_manager_clone + .lock() + .unwrap() + .attach(delay, target, true); Ok(()) })?; lua.globals().set("every", every)?; diff --git a/src/main.rs b/src/main.rs index 2d1d26eb..90765d69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,13 +108,20 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // While waiting for an event to come along, service the task manager while let Ok(false) = crossterm::event::poll(std::time::Duration::from_millis(100)) { - let exec = editor.borrow_mut().config.task_manager.lock().unwrap().execution_list(); + let exec = editor + .borrow_mut() + .config + .task_manager + .lock() + .unwrap() + .execution_list(); for task in exec { if let Ok(target) = lua.globals().get::<_, mlua::Function>(task.clone()) { // Run the code handle_lua_error(&editor, "task", target.call(())); } else { - editor.borrow_mut().feedback = Feedback::Warning(format!("Function '{task}' was not found")); + editor.borrow_mut().feedback = + Feedback::Warning(format!("Function '{task}' was not found")); } } } From 3db3fc9bfb40c4d80c47e4dbbe0bc120b7a76c2e Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:28:05 +0100 Subject: [PATCH 25/31] added more specific rerender functions --- src/config/editor.rs | 30 +++++++++++++++++++++++++++++- src/config/tasks.rs | 2 +- src/editor/mod.rs | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index cc413116..4e85445a 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -1,7 +1,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; -use kaolinite::Loc; +use kaolinite::{Loc, Size}; use mlua::prelude::*; impl LuaUserData for Editor { @@ -416,6 +416,34 @@ impl LuaUserData for Editor { let _ = editor.render(lua); Ok(()) }); + methods.add_method_mut("rerender_feedback_line", |_, editor, ()| { + // If you can't render the editor, you're pretty much done for anyway + let Size { w, mut h } = crate::ui::size().unwrap_or(Size { w: 0, h: 0 }); + h = h.saturating_sub(1 + editor.push_down); + let _ = editor.terminal.hide_cursor(); + let _ = editor.render_feedback_line(w, h); + // Apply render and restore cursor + let max = editor.dent(); + if let Some(Loc { x, y }) = editor.doc().cursor_loc_in_screen() { + let _ = editor.terminal.goto(x + max, y + editor.push_down); + } + let _ = editor.terminal.show_cursor(); + Ok(()) + }); + methods.add_method_mut("rerender_status_line", |lua, editor, ()| { + // If you can't render the editor, you're pretty much done for anyway + let Size { w, mut h } = crate::ui::size().unwrap_or(Size { w: 0, h: 0 }); + h = h.saturating_sub(1 + editor.push_down); + let _ = editor.terminal.hide_cursor(); + let _ = editor.render_status_line(lua, w, h); + // Apply render and restore cursor + let max = editor.dent(); + if let Some(Loc { x, y }) = editor.doc().cursor_loc_in_screen() { + let _ = editor.terminal.goto(x + max, y + editor.push_down); + } + let _ = editor.terminal.show_cursor(); + Ok(()) + }); } } diff --git a/src/config/tasks.rs b/src/config/tasks.rs index c1f8dd53..f10296c8 100644 --- a/src/config/tasks.rs +++ b/src/config/tasks.rs @@ -18,7 +18,7 @@ impl TaskManager { pub fn cycle(&mut self) { for task in &mut self.tasks { // Decrement remaining time - if task.remaining < 0 { + if task.remaining > 0 { task.remaining = task.remaining.saturating_sub(1); } // Check if activation is required diff --git a/src/editor/mod.rs b/src/editor/mod.rs index e1aec201..1e17243b 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -40,7 +40,7 @@ pub struct Editor { /// Will store the last time the editor was interacted with (to track inactivity) pub last_active: Instant, /// Used for storing amount to push document down - push_down: usize, + pub push_down: usize, /// Used to cache the location of the configuration file pub config_path: String, } From f50576537706c56c50c35af4e397299b7e2a5437 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:34:45 +0100 Subject: [PATCH 26/31] massively sped up rendering --- src/config/editor.rs | 2 ++ src/ui.rs | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index 4e85445a..1ab6d72e 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -428,6 +428,7 @@ impl LuaUserData for Editor { let _ = editor.terminal.goto(x + max, y + editor.push_down); } let _ = editor.terminal.show_cursor(); + let _ = editor.terminal.flush(); Ok(()) }); methods.add_method_mut("rerender_status_line", |lua, editor, ()| { @@ -442,6 +443,7 @@ impl LuaUserData for Editor { let _ = editor.terminal.goto(x + max, y + editor.push_down); } let _ = editor.terminal.show_cursor(); + let _ = editor.terminal.flush(); Ok(()) }); } diff --git a/src/ui.rs b/src/ui.rs index 2670d1b9..42133eb0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -8,6 +8,7 @@ use crossterm::{ KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }, execute, + queue, style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, terminal::{ self, Clear, ClearType as ClType, DisableLineWrap, EnableLineWrap, EnterAlternateScreen, @@ -140,19 +141,19 @@ impl Terminal { } pub fn show_cursor(&mut self) -> Result<()> { - execute!(self.stdout, Show)?; + queue!(self.stdout, Show)?; Ok(()) } pub fn hide_cursor(&mut self) -> Result<()> { - execute!(self.stdout, Hide)?; + queue!(self.stdout, Hide)?; Ok(()) } pub fn goto>(&mut self, x: Num, y: Num) -> Result<()> { let x: usize = x.into(); let y: usize = y.into(); - execute!( + queue!( self.stdout, MoveTo( u16::try_from(x).unwrap_or(u16::MAX), @@ -163,7 +164,7 @@ impl Terminal { } pub fn clear_current_line(&mut self) -> Result<()> { - execute!(self.stdout, Clear(ClType::CurrentLine))?; + queue!(self.stdout, Clear(ClType::CurrentLine))?; Ok(()) } From f93184c023156373fdfccf83c558ea2039029384 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:35:33 +0100 Subject: [PATCH 27/31] new pomodoro plug-in that re-renders itself on the status line when active --- plugins/pomodoro.lua | 8 ++++++++ src/ui.rs | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/pomodoro.lua b/plugins/pomodoro.lua index c11d43b3..4c0c442a 100644 --- a/plugins/pomodoro.lua +++ b/plugins/pomodoro.lua @@ -57,3 +57,11 @@ commands["pomodoro"] = function(arguments) editor:display_info("Stopped pomodoro timer") end end + +-- Force rerender of the status line every second whilst the timer is active +function pomodoro_refresh() + if pomodoro.current ~= "none" then + editor:rerender_status_line() + end +end +every(1, "pomodoro_refresh") diff --git a/src/ui.rs b/src/ui.rs index 42133eb0..926c6ce3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -7,8 +7,7 @@ use crossterm::{ DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }, - execute, - queue, + execute, queue, style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, terminal::{ self, Clear, ClearType as ClType, DisableLineWrap, EnableLineWrap, EnterAlternateScreen, From 81a0872f1e94939e51eb40e01fb4815fd3f92f19 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:07:53 +0100 Subject: [PATCH 28/31] added plugin management system --- plugins/pomodoro.lua | 10 +- src/config/editor.rs | 203 +++++++++++++++++-------------- src/config/mod.rs | 3 + src/main.rs | 7 +- src/plugin/plugin_manager.lua | 219 ++++++++++++++++++++++++++++++++++ 5 files changed, 351 insertions(+), 91 deletions(-) create mode 100644 src/plugin/plugin_manager.lua diff --git a/plugins/pomodoro.lua b/plugins/pomodoro.lua index 4c0c442a..16dc6825 100644 --- a/plugins/pomodoro.lua +++ b/plugins/pomodoro.lua @@ -17,8 +17,8 @@ function dec2mmss(decimal_seconds) return string.format("%02d:%02d", minutes, seconds) end --- Define a function to display the countdown in the status line -function pomodoro_show() +-- Helper function to work out how long the timer has left +function pomodoro_left() local current = os.date("*t") local elapsed = os.time(current) - os.time(pomodoro.started) local minutes = 0 @@ -27,6 +27,12 @@ function pomodoro_show() elseif pomodoro.current == "rest" then minutes = pomodoro.rest_time * 60 - elapsed end + return minutes +end + +-- Define a function to display the countdown in the status line +function pomodoro_show() + local minutes = pomodoro_left() if minutes < 0 then if pomodoro.current == "work" then pomodoro.current = "rest" diff --git a/src/config/editor.rs b/src/config/editor.rs index 1ab6d72e..accbcd6b 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -2,6 +2,7 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; use kaolinite::{Loc, Size}; +use crate::{PLUGIN_BOOTSTRAP, PLUGIN_RUN, PLUGIN_MANAGER}; use mlua::prelude::*; impl LuaUserData for Editor { @@ -45,6 +46,20 @@ impl LuaUserData for Editor { } Ok(()) }); + methods.add_method_mut("reload_plugins", |lua, editor, ()| { + // Provide plug-in bootstrap + let _ = lua.load(PLUGIN_BOOTSTRAP).exec(); + // Reload the configuration file + let path = editor.config_path.clone(); + if editor.load_config(&path, lua).is_some() { + editor.feedback = Feedback::Error("Failed to reload config".to_string()); + } + // Run plug-ins + let _ = lua.load(PLUGIN_RUN).exec(); + // Attach plugin manager + let _ = lua.load(PLUGIN_MANAGER).exec(); + Ok(()) + }); // Display messages methods.add_method_mut("display_error", |_, editor, message: String| { editor.feedback = Feedback::Error(message); @@ -122,87 +137,89 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("select_up", |_, editor, ()| { - editor.select_up(); + methods.add_method_mut("move_home", |_, editor, ()| { + editor.doc_mut().move_home(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("select_down", |_, editor, ()| { - editor.select_down(); + methods.add_method_mut("move_end", |_, editor, ()| { + editor.doc_mut().move_end(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("select_left", |_, editor, ()| { - editor.select_left(); + methods.add_method_mut("move_page_up", |_, editor, ()| { + editor.doc_mut().move_page_up(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("select_right", |_, editor, ()| { - editor.select_right(); + methods.add_method_mut("move_page_down", |_, editor, ()| { + editor.doc_mut().move_page_down(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("select_all", |_, editor, ()| { - editor.select_all(); + methods.add_method_mut("move_top", |_, editor, ()| { + editor.doc_mut().move_top(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("cut", |_, editor, ()| { - if let Err(err) = editor.cut() { - editor.feedback = Feedback::Error(err.to_string()); - } else { - editor.feedback = Feedback::Info("Text cut to clipboard".to_owned()); - } + methods.add_method_mut("move_bottom", |_, editor, ()| { + editor.doc_mut().move_bottom(); + editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("copy", |_, editor, ()| { - if let Err(err) = editor.copy() { - editor.feedback = Feedback::Error(err.to_string()); - } else { - editor.feedback = Feedback::Info("Text copied to clipboard".to_owned()); - } + methods.add_method_mut("move_previous_word", |_, editor, ()| { + editor.prev_word(); + editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_home", |_, editor, ()| { - editor.doc_mut().move_home(); + methods.add_method_mut("move_next_word", |_, editor, ()| { + editor.next_word(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_end", |_, editor, ()| { - editor.doc_mut().move_end(); + // Cursor selection and clipboard + methods.add_method_mut("select_up", |_, editor, ()| { + editor.select_up(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_page_up", |_, editor, ()| { - editor.doc_mut().move_page_up(); + methods.add_method_mut("select_down", |_, editor, ()| { + editor.select_down(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_page_down", |_, editor, ()| { - editor.doc_mut().move_page_down(); + methods.add_method_mut("select_left", |_, editor, ()| { + editor.select_left(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_top", |_, editor, ()| { - editor.doc_mut().move_top(); + methods.add_method_mut("select_right", |_, editor, ()| { + editor.select_right(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_bottom", |_, editor, ()| { - editor.doc_mut().move_bottom(); + methods.add_method_mut("select_all", |_, editor, ()| { + editor.select_all(); editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("move_previous_word", |_, editor, ()| { - editor.prev_word(); - editor.update_highlighter(); + methods.add_method_mut("cut", |_, editor, ()| { + if let Err(err) = editor.cut() { + editor.feedback = Feedback::Error(err.to_string()); + } else { + editor.feedback = Feedback::Info("Text cut to clipboard".to_owned()); + } Ok(()) }); - methods.add_method_mut("move_next_word", |_, editor, ()| { - editor.next_word(); - editor.update_highlighter(); + methods.add_method_mut("copy", |_, editor, ()| { + if let Err(err) = editor.copy() { + editor.feedback = Feedback::Error(err.to_string()); + } else { + editor.feedback = Feedback::Info("Text copied to clipboard".to_owned()); + } Ok(()) }); + // Document editing methods.add_method_mut( "insert_at", |_, editor, (text, x, y): (String, usize, usize)| { @@ -266,17 +283,44 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); - methods.add_method_mut("open_command_line", |_, editor, ()| { - match editor.prompt("Command") { - Ok(command) => { - editor.command = Some(command); - } - Err(err) => { - editor.feedback = Feedback::Error(err.to_string()); - } - } - Ok(()) + methods.add_method("get_character", |_, editor, ()| { + let loc = editor.doc().char_loc(); + let ch = editor + .doc() + .line(loc.y) + .unwrap_or_default() + .chars() + .nth(loc.x) + .map(|ch| ch.to_string()) + .unwrap_or_default(); + Ok(ch) + }); + methods.add_method_mut("get_character_at", |_, editor, (x, y): (usize, usize)| { + editor.doc_mut().load_to(y); + let y = y.saturating_sub(1); + let ch = editor + .doc() + .line(y) + .unwrap_or_default() + .chars() + .nth(x) + .map_or_else(String::new, |ch| ch.to_string()); + editor.update_highlighter(); + Ok(ch) + }); + methods.add_method("get_line", |_, editor, ()| { + let loc = editor.doc().char_loc(); + let line = editor.doc().line(loc.y).unwrap_or_default(); + Ok(line) }); + methods.add_method_mut("get_line_at", |_, editor, y: usize| { + editor.doc_mut().load_to(y); + let y = y.saturating_sub(1); + let line = editor.doc().line(y).unwrap_or_default(); + editor.update_highlighter(); + Ok(line) + }); + // Document management methods.add_method_mut("previous_tab", |_, editor, ()| { editor.prev(); Ok(()) @@ -285,6 +329,10 @@ impl LuaUserData for Editor { editor.next(); Ok(()) }); + methods.add_method_mut("move_to_document", |_, editor, id: usize| { + editor.ptr = id; + Ok(()) + }); methods.add_method_mut("new", |_, editor, ()| { if let Err(err) = editor.new_document() { editor.feedback = Feedback::Error(err.to_string()); @@ -335,6 +383,7 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); + // Searching and replacing methods.add_method_mut("search", |_, editor, ()| { if let Err(err) = editor.search() { editor.feedback = Feedback::Error(err.to_string()); @@ -349,45 +398,9 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); - methods.add_method("get_character", |_, editor, ()| { - let loc = editor.doc().char_loc(); - let ch = editor - .doc() - .line(loc.y) - .unwrap_or_default() - .chars() - .nth(loc.x) - .map(|ch| ch.to_string()) - .unwrap_or_default(); - Ok(ch) - }); - methods.add_method_mut("get_character_at", |_, editor, (x, y): (usize, usize)| { - editor.doc_mut().load_to(y); - let y = y.saturating_sub(1); - let ch = editor - .doc() - .line(y) - .unwrap_or_default() - .chars() - .nth(x) - .map_or_else(String::new, |ch| ch.to_string()); - editor.update_highlighter(); - Ok(ch) - }); - methods.add_method("get_line", |_, editor, ()| { - let loc = editor.doc().char_loc(); - let line = editor.doc().line(loc.y).unwrap_or_default(); - Ok(line) - }); - methods.add_method_mut("get_line_at", |_, editor, y: usize| { - editor.doc_mut().load_to(y); - let y = y.saturating_sub(1); - let line = editor.doc().line(y).unwrap_or_default(); + methods.add_method_mut("move_next_match", |_, editor, query: String| { + editor.next_match(&query); editor.update_highlighter(); - Ok(line) - }); - methods.add_method_mut("move_to_document", |_, editor, id: usize| { - editor.ptr = id; Ok(()) }); methods.add_method_mut("move_previous_match", |_, editor, query: String| { @@ -395,6 +408,7 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); + // Document state modification methods.add_method_mut("set_read_only", |_, editor, status: bool| { editor.doc_mut().info.read_only = status; Ok(()) @@ -409,6 +423,7 @@ impl LuaUserData for Editor { editor.highlighter[editor.ptr] = highlighter; Ok(()) }); + // Rerendering methods.add_method_mut("rerender", |lua, editor, ()| { // Force a re-render editor.needs_rerender = true; @@ -446,6 +461,18 @@ impl LuaUserData for Editor { let _ = editor.terminal.flush(); Ok(()) }); + // Miscellaneous + methods.add_method_mut("open_command_line", |_, editor, ()| { + match editor.prompt("Command") { + Ok(command) => { + editor.command = Some(command); + } + Err(err) => { + editor.feedback = Feedback::Error(err.to_string()); + } + } + Ok(()) + }); } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6e3ec403..c59b0c54 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -35,6 +35,9 @@ pub const PLUGIN_BOOTSTRAP: &str = include_str!("../plugin/bootstrap.lua"); /// This contains the code for running the plugins pub const PLUGIN_RUN: &str = include_str!("../plugin/run.lua"); +/// This contains the code for running the plugins +pub const PLUGIN_MANAGER: &str = include_str!("../plugin/plugin_manager.lua"); + /// The struct that holds all the configuration information #[derive(Debug)] pub struct Config { diff --git a/src/main.rs b/src/main.rs index 90765d69..fb5faf14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,9 @@ mod error; mod ui; use cli::CommandLineInterface; -use config::{key_to_string, run_key, run_key_before, PLUGIN_BOOTSTRAP, PLUGIN_RUN}; +use config::{ + key_to_string, run_key, run_key_before, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_RUN, +}; use crossterm::event::Event as CEvent; use editor::Editor; use error::Result; @@ -100,6 +102,9 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // Run plug-ins handle_lua_error(&editor, "", lua.load(PLUGIN_RUN).exec()); + // Add in the plugin manager + handle_lua_error(&editor, "", lua.load(PLUGIN_MANAGER).exec()); + // Run the editor and handle errors if applicable editor.borrow_mut().init()?; while editor.borrow().active { diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua new file mode 100644 index 00000000..4b4c0031 --- /dev/null +++ b/src/plugin/plugin_manager.lua @@ -0,0 +1,219 @@ +-- Plug-in management system + +plugin_manager = {} + +-- Install a plug-in +function plugin_manager:install(plugin) + -- Check if downloaded / in config + local downloaded = self:plugin_downloaded(plugin) + local in_config = self:plugin_in_config(plugin) + local do_download = false + local do_enabling = false + if downloaded and in_config then + -- Already installed + local resp = editor:prompt("Plug-in is already installed, would you like to update it? (y/n)") + if resp == "y" then + do_download = true + else + return false + end + elseif not downloaded and not in_config then + -- No evidence of plug-in on system, get installing + do_download = true + do_enabling = true + elseif not downloaded and in_config then + -- Somehow, the user has it enabled, but it isn't downloaded + local resp = editor:prompt("Plugin already enabled, start download? (y/n)") + if resp == "y" then + do_download = true + else + return false + end + elseif downloaded and not in_config then + -- The user has managed to download it, but they haven't enabled it + local resp = editor:prompt("Plugin already downloaded, enable plug-in? (y/n)") + if resp == "y" then + do_enabling = true + else + return false + end + end + -- Do the installing + if do_download then + local result = plugin_manager:download_plugin(plugin) + if result ~= nil then + editor:display_error(result) + return true + end + end + if do_enabling then + local result = plugin_manager:append_to_config(plugin) + if result ~= nil then + editor:display_error(result) + return true + end + end + -- Reload configuration file and plugins just to be safe + editor:reload_plugins() + editor:display_info("Plugin was installed successfully") + return true +end + +-- Uninstall a plug-in +function plugin_manager:uninstall(plugin) + -- Check if downloaded / in config + local downloaded = self:plugin_downloaded(plugin) + local in_config = self:plugin_in_config(plugin) + if not downloaded and not in_config then + editor:display_error("Plugin does not exist") + return + end + if downloaded then + local result = plugin_manager:remove_plugin(plugin) + if result ~= nil then + editor:display_error(result) + return + end + end + if in_config then + local result = plugin_manager:remove_from_config(plugin) + if result ~= nil then + editor:display_error(result) + return + end + end + -- Reload configuration file and plugins just to be safe + editor:reload_plugins() + editor:display_info("Plugin was uninstalled successfully") +end + +-- Get the status of the plug-ins including how many are installed and which ones +function plugin_manager:status() + local count = 0 + local list = "" + for _, v in ipairs(plugins) do + count = count + 1 + list = list .. v:match("^.+[\\/](.+).lua$") .. " " + end + editor:display_info(tostring(count) .. " plug-ins installed: " .. list) +end + +-- Verify whether or not a plug-in is downloaded +function plugin_manager:plugin_downloaded(plugin) + local base = plugin .. ".lua" + local path_cross = base + local path_unix = home .. "/.config/ox/" .. base + local path_win = home .. "/ox/" .. base + local installed = file_exists(path_cross) or file_exists(path_unix) or file_exists(path_win) + -- Return true if plug-ins are built in + local is_autoindent = base == "autoindent.lua" + local is_pairs = base == "pairs.lua" + return installed or is_pairs or is_autoindent +end + +-- Download a plug-in from the ox repository +function plugin_manager:download_plugin(plugin) + -- Download the plug-in code + local url = "https://raw.githubusercontent.com/curlpipe/ox/refs/heads/master/plugins/" .. plugin .. ".lua" + local resp = http.get(url) + if resp == "404: Not Found" then + return "Plug-in not found in repository" + end + -- Find the path to download it to + local path = package.config:sub(1,1) == '\\' and home .. "/ox" or home .. "/.config/ox" + path = path .. "/" .. plugin .. ".lua" + -- Write it to a file + file = io.open(path, "w") + if not file then + return "Failed to write to " .. path + end + file:write(resp) + file:close() + return nil +end + +-- Remove a plug-in from the configuration directory +function plugin_manager:remove_plugin(plugin) + -- Obtain the path + local path = package.config:sub(1,1) == '\\' and home .. "/ox" or home .. "/.config/ox" + path = path .. "/" .. plugin .. ".lua" + -- Remove the file + local success, err = os.remove(path) + if not success then + return "Failed to delete the plug-in: " .. err + else + return nil + end +end + +-- Verify whether the plug-in is being imported in the configuration file +function plugin_manager:plugin_in_config(plugin) + -- Find the configuration file path + local path = home .. "/.oxrc" + -- Open the document + local file = io.open(path, "r") + if not file then return false end + -- Check each line to see whether it is being loaded + for line in file:lines() do + local pattern1 = '^load_plugin%("' .. plugin .. '.lua"%)' + local pattern2 = "^load_plugin%('" .. plugin .. ".lua'%)" + if line:match(pattern1) or line:match(pattern2) then + file:close() + return true + end + end + file:close() + return false +end + +-- Append the plug-in import code to the configuration file so it is loaded +function plugin_manager:append_to_config(plugin) + local path = home .. "/.oxrc" + local file = io.open(path, "a") + if not file then + return "Failed to open configuration file" + end + file:write('load_plugin("' .. plugin .. '.lua")\n') + file:close() + return nil +end + +-- Remove plug-in import code from the configuration file +function plugin_manager:remove_from_config(plugin) + -- Find the configuration file path + local path = home .. "/.oxrc" + -- Open the configuration file + local file = io.open(path, "r") + if not file then + return "Failed to open configuration file" + end + local lines = {} + for line in file:lines() do + table.insert(lines, line) + end + file:close() + -- Run through each line and only write back the non-offending lines + local file = io.open(path, "w") + for _, line in ipairs(lines) do + local pattern1 = '^load_plugin%("' .. plugin .. '.lua"%)' + local pattern2 = "^load_plugin%('" .. plugin .. ".lua'%)" + if not line:match(pattern1) and not line:match(pattern2) then + file:write(line .. "\n") + end + end + file:close() + return nil +end + +commands["plugin"] = function(arguments) + if arguments[1] == "install" then + local result = plugin_manager:install(arguments[2]) + if not result then + editor:display_info("Plug-in installation cancelled") + end + elseif arguments[1] == "uninstall" then + plugin_manager:uninstall(arguments[2]) + elseif arguments[1] == "status" then + plugin_manager:status() + end +end From 5a24bd3d18f54a9d0508b79dad7ec2a6871f0b69 Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:09:20 +0100 Subject: [PATCH 29/31] rustfmt --- src/config/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/editor.rs b/src/config/editor.rs index accbcd6b..c0cf9f6b 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -1,8 +1,8 @@ use crate::cli::VERSION; use crate::editor::Editor; use crate::ui::Feedback; +use crate::{PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, PLUGIN_RUN}; use kaolinite::{Loc, Size}; -use crate::{PLUGIN_BOOTSTRAP, PLUGIN_RUN, PLUGIN_MANAGER}; use mlua::prelude::*; impl LuaUserData for Editor { From 9cae4fa0f3fa0fcae528101d66dde298bbbd6ebb Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:49:45 +0100 Subject: [PATCH 30/31] added config level preference for using spaces over tabs --- config/.oxrc | 1 + src/config/mod.rs | 34 ++++++++++++++++++++++++++++++++++ src/editor/mod.rs | 17 +++++++++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/config/.oxrc b/config/.oxrc index 2047f9e4..520c1cc0 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -191,6 +191,7 @@ commands = { -- Configure Documents -- document.tab_width = 4 +document.indentation = "tabs" document.undo_period = 10 document.wrap_cursor = true diff --git a/src/config/mod.rs b/src/config/mod.rs index c59b0c54..c8091b2c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -206,9 +206,35 @@ impl Config { } } +#[derive(Debug, PartialEq)] +pub enum Indentation { + Tabs, + Spaces, +} + +impl ToString for Indentation { + fn to_string(&self) -> String { + match self { + Self::Tabs => "tabs", + Self::Spaces => "spaces", + } + .to_string() + } +} + +impl From for Indentation { + fn from(s: String) -> Self { + match s.as_str() { + "spaces" => Self::Spaces, + _ => Self::Tabs, + } + } +} + #[derive(Debug)] pub struct Document { pub tab_width: usize, + pub indentation: Indentation, pub undo_period: usize, pub wrap_cursor: bool, } @@ -217,6 +243,7 @@ impl Default for Document { fn default() -> Self { Self { tab_width: 4, + indentation: Indentation::Tabs, undo_period: 10, wrap_cursor: true, } @@ -230,6 +257,13 @@ impl LuaUserData for Document { this.tab_width = value; Ok(()) }); + fields.add_field_method_get("indentation", |_, document| { + Ok(document.indentation.to_string()) + }); + fields.add_field_method_set("indentation", |_, this, value: String| { + this.indentation = value.into(); + Ok(()) + }); fields.add_field_method_get("undo_period", |_, document| Ok(document.undo_period)); fields.add_field_method_set("undo_period", |_, this, value| { this.undo_period = value; diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 1e17243b..624b5d31 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::config::{Config, Indentation}; use crate::error::{OxError, Result}; use crate::ui::{size, Feedback, Terminal}; use crossterm::event::{Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEventKind}; @@ -315,7 +315,7 @@ impl Editor { match (modifiers, code) { // Core key bindings (non-configurable behaviour) (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, - (KMod::NONE, KCode::Tab) => self.character('\t')?, + (KMod::NONE, KCode::Tab) => self.handle_tab()?, (KMod::NONE, KCode::Backspace) => self.backspace()?, (KMod::NONE, KCode::Delete) => self.delete()?, (KMod::NONE, KCode::Enter) => self.enter()?, @@ -341,4 +341,17 @@ impl Editor { } Ok(()) } + + /// Handle tab character being inserted + pub fn handle_tab(&mut self) -> Result<()> { + if self.config.document.borrow().indentation == Indentation::Tabs { + self.character('\t')?; + } else { + let tab_width = self.config.document.borrow().tab_width; + for _ in 0..tab_width { + self.character(' ')?; + } + } + Ok(()) + } } From 61b73188e860f53fe17827ddb529d2956134f26b Mon Sep 17 00:00:00 2001 From: Luke <11898833+curlpipe@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:55:20 +0100 Subject: [PATCH 31/31] clippy --- src/config/mod.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index c8091b2c..ab68e0e0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,7 +2,11 @@ use crate::error::{OxError, Result}; use mlua::prelude::*; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::RefCell, + fmt::{Display, Error, Formatter}, + rc::Rc, +}; mod colors; mod editor; @@ -212,13 +216,16 @@ pub enum Indentation { Spaces, } -impl ToString for Indentation { - fn to_string(&self) -> String { - match self { - Self::Tabs => "tabs", - Self::Spaces => "spaces", - } - .to_string() +impl Display for Indentation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { + write!( + f, + "{}", + match self { + Self::Tabs => "tabs", + Self::Spaces => "spaces", + } + ) } }