diff --git a/Cargo.lock b/Cargo.lock index b97821ce..951b35e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.6.8" +version = "0.6.9" dependencies = [ "alinio", "base64", diff --git a/Cargo.toml b/Cargo.toml index 775bc1d8..023e7e96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["cactus"] [package] name = "ox" -version = "0.6.8" +version = "0.6.9" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A Rust powered text editor." diff --git a/README.md b/README.md index 566ecc66..4ef8bbd1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - +

@@ -8,11 +8,11 @@

Ox editor

- Ox is a code editor that runs in your terminal. + The simple but flexible text editor

- - + +

@@ -28,7 +28,7 @@ Ox is an independent text editor that can be used to write everything from text If you're looking for a text editor that... 1. :feather: Is lightweight and efficient 2. :wrench: Can be configured to your heart's content -3. :package: Has features out of the box, including +3. :package: Has features out of the box, including - syntax highlighting - undo and redo - search and replace @@ -40,22 +40,22 @@ If you're looking for a text editor that... It runs in your terminal as a text-user-interface, just like vim, nano and micro, however, it is not based on any existing editors and has been built from the ground up. -It is mainly used on linux systems, but macOS and Windows users (via WSL) are free to give it a go. +It is mainly designed on linux systems, but macOS and Windows users (via WSL) are free to give it a go. Work is currently underway to get it working perfectly on all systems. ## Selling Points ### Lightweight and Efficient -- :feather: Ox is lightweight, with the precompiled binary taking up roughly 4mb in storage space. +- :feather: Ox is lightweight, with the precompiled binary taking up roughly 5mb in storage space. - :knot: It uses a `rope` data structure which allows incremental editing, file reading and file writing, which will speed up performance, particularly on huge files. -- :crab: It was built in Rust, which is a quick lower level language that has a strong reputation in the performance department. +- :crab: It was built in Rust, which is a quick lower-level language that has a strong reputation in the performance department. ### Strong configurability - :electric_plug: Plug-In system where you can write your own plug-ins or integrate other people's - :wrench: A wide number of options for configuration with everything from colours to the status line to syntax highlighting being open to customisation - :moon: Ox uses Lua as a configuration language for familiarity when scripting and configuring -- 🤝 A configuration assistant to quickly get Ox set up for you from the get-go +- :handshake: A configuration assistant to quickly get Ox set up for you from the get-go ### Out of the box features @@ -161,6 +161,8 @@ ox This will open up an empty document. +However, if you've just downloaded Ox, the configuration assistant will automatically start up and help you configure the editor initially. + If you wish to open a file straight from the command line, you can run ```sh ox /path/to/file @@ -228,6 +230,8 @@ We've covered most keyboard shortcuts, but there are some other features you mig Ox features a configuration system that allows the editor to be modified and personalised. +By default, you will be greeted by a configuration assistant when first starting Ox, when no configuration file is in place. This will help you generate a configuration file. + By default, Ox will look for a file here: `$XDG_CONFIG_HOME/.oxrc` or `~/.oxrc`. On Windows, Ox will try to look here `C:/Users/user/ox/.oxrc` (where `user` is the user name of your account) @@ -236,18 +240,16 @@ Ox's configuration language is [Lua](https://lua.org). For reference, there is a default config in the `config` folder in the repository. You can either download it and place it in the default config directory or create your own using the example ones as a reference. -If you don't have a config file or don't want to mess around with it, don't worry, Ox has default settings it will use. - ## Documentation If you've been through the quick start guide above, but are looking for more detail, you can find in-depth documentation on the [wiki page](https://github.com/curlpipe/ox/wiki/) -This will take you step-by-step in great detail through 5 different stages: +This will take you step-by-step in great detail through 6 different stages: 1. **Installation** - advice and how-tos on installation -2. **Starting** - using the command line interface -3. **Using** - editing a document and controlling the editor -4. **Configuring** - writing plug-ins, changing the layout, adding to and changing the syntax highlighting +2. **Configuring** - changing the layout, adding to and changing the syntax highlighting +3. **General Editing** - editing a document and controlling the editor +4. **Command Line** - using the command line interface 5. **Plugins** - installing or uninstalling community plug-ins and writing or distributing your own plug-ins 6. **Roadmap** - planned features diff --git a/config/.oxrc b/config/.oxrc index 4cc188d8..8b37a91f 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -49,6 +49,12 @@ event_mapping = { ["pagedown"] = function() editor:move_page_down() end, + ["esc"] = function() + editor:cancel_selection() + end, + ["alt_v"] = function() + editor:cursor_to_viewport() + end, ["ctrl_g"] = function() local line = editor:prompt("Go to line") editor:move_to(0, tonumber(line)) @@ -116,30 +122,54 @@ event_mapping = { editor:open_command_line() end, ["alt_up"] = function() - -- current line information - local line = editor:get_line() local cursor = editor.cursor - -- insert a new line - editor:insert_line_at(line, cursor.y - 1) - -- delete old copy and reposition cursor - editor:remove_line_at(cursor.y + 1) - -- restore cursor position - editor:move_to(cursor.x, cursor.y - 1) - -- correct indentation level - autoindent:fix_indent() + local select = editor.selection + local single = select.x == cursor.x and select.y == cursor.y + if single then + -- move single line + editor:move_line_up() + autoindent:fix_indent() + else + -- move an entire selection + if cursor.y > select.y then + for line = select.y, cursor.y do + editor:move_to(cursor.x, line) + editor:move_line_up() + end + else + for line = cursor.y, select.y do + editor:move_to(cursor.x, line) + editor:move_line_up() + end + end + editor:move_to(cursor.x, cursor.y - 1) + editor:select_to(select.x, select.y - 1) + end end, ["alt_down"] = function() - -- current line information - local line = editor:get_line() local cursor = editor.cursor - -- insert a new line - editor:insert_line_at(line, cursor.y + 2) - -- delete old copy and reposition cursor - editor:remove_line_at(cursor.y) - -- restore cursor position - editor:move_to(cursor.x, cursor.y + 1) - -- correct indentation level - autoindent:fix_indent() + local select = editor.selection + local single = select.x == cursor.x and select.y == cursor.y + if single then + -- move single line + editor:move_line_down() + autoindent:fix_indent() + else + -- move an entire selection + if cursor.y > select.y then + for line = cursor.y, select.y, -1 do + editor:move_to(cursor.x, line) + editor:move_line_down() + end + else + for line = select.y, cursor.y, -1 do + editor:move_to(cursor.x, line) + editor:move_line_down() + end + end + editor:move_to(cursor.x, cursor.y + 1) + editor:select_to(select.x, select.y + 1) + end end, ["ctrl_w"] = function() editor:remove_word() @@ -309,5 +339,4 @@ syntax:set("deletion", {255, 100, 100}) -- Lists 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") +load_plugin("quickcomment.lua") diff --git a/kaolinite/src/document.rs b/kaolinite/src/document.rs index 2ac2ae7d..2f774ff3 100644 --- a/kaolinite/src/document.rs +++ b/kaolinite/src/document.rs @@ -262,7 +262,7 @@ impl Document { self.file.insert(idx, st); // Update cache let line: String = self.file.line(loc.y).chars().collect(); - self.lines[loc.y] = line.trim_end_matches(&['\n', '\r']).to_string(); + self.lines[loc.y] = line.trim_end_matches(['\n', '\r']).to_string(); // Update unicode map let dbl_start = self.dbl_map.shift_insertion(loc, st, self.tab_width); let tab_start = self.tab_map.shift_insertion(loc, st, self.tab_width); @@ -341,7 +341,7 @@ impl Document { self.file.remove(start..end); // Update cache let line: String = self.file.line(y).chars().collect(); - self.lines[y] = line.trim_end_matches(&['\n', '\r']).to_string(); + self.lines[y] = line.trim_end_matches(['\n', '\r']).to_string(); self.old_cursor = self.loc().x; Ok(()) } @@ -432,6 +432,36 @@ impl Document { Ok(()) } + /// Swap a line upwards + /// # Errors + /// When out of bounds + pub fn swap_line_up(&mut self) -> Result<()> { + let cursor = self.char_loc(); + let line = self.line(cursor.y).ok_or(Error::OutOfRange)?; + self.insert_line(cursor.y.saturating_sub(1), line)?; + self.delete_line(cursor.y + 1)?; + self.move_to(&Loc { + x: cursor.x, + y: cursor.y.saturating_sub(1), + }); + Ok(()) + } + + /// Swap a line downwards + /// # Errors + /// When out of bounds + pub fn swap_line_down(&mut self) -> Result<()> { + let cursor = self.char_loc(); + let line = self.line(cursor.y).ok_or(Error::OutOfRange)?; + self.insert_line(cursor.y + 2, line)?; + self.delete_line(cursor.y)?; + self.move_to(&Loc { + x: cursor.x, + y: cursor.y + 1, + }); + Ok(()) + } + /// Cancels the current selection pub fn cancel_selection(&mut self) { self.cursor.selection_end = self.cursor.loc; @@ -953,6 +983,8 @@ impl Document { // Bounds checking if self.loc().y != y && y <= self.len_lines() { self.cursor.loc.y = y; + } else if y > self.len_lines() { + self.cursor.loc.y = self.len_lines(); } // Snap to end of line self.fix_dangling_cursor(); @@ -1140,7 +1172,7 @@ impl Document { self.tab_map.insert(i, tab_map); // Cache this line self.lines - .push(line.trim_end_matches(&['\n', '\r']).to_string()); + .push(line.trim_end_matches(['\n', '\r']).to_string()); } // Store new loaded point self.info.loaded_to = to; @@ -1297,17 +1329,21 @@ impl Document { self.file.slice(self.selection_range()).to_string() } + /// Commit a change to the undo management system pub fn commit(&mut self) { let s = self.take_snapshot(); + self.undo_mgmt.backpatch_cursor(&self.cursor); self.undo_mgmt.commit(s); } + /// Completely reload the file pub fn reload_lines(&mut self) { let to = std::mem::take(&mut self.info.loaded_to); self.lines.clear(); self.load_to(to); } + /// Delete the currently selected text pub fn remove_selection(&mut self) { self.file.remove(self.selection_range()); self.reload_lines(); diff --git a/kaolinite/src/event.rs b/kaolinite/src/event.rs index 485f0751..99a28e2f 100644 --- a/kaolinite/src/event.rs +++ b/kaolinite/src/event.rs @@ -189,4 +189,11 @@ impl UndoMgmt { pub fn at_file(&self) -> bool { self.undo.len() == self.on_disk } + + /// Change the cursor position of the previous snapshot + pub fn backpatch_cursor(&mut self, cursor: &Cursor) { + if let Some(snapshot) = self.undo.last_mut() { + snapshot.cursor = *cursor; + } + } } diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index ba61ae9a..0d4b0bd3 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.10 +Auto Indent v0.11 Helps you when programming by guessing where indentation should go and then automatically applying these guesses as you program @@ -112,6 +112,7 @@ function autoindent:set_indent(y, new_indent) editor:insert_line_at(new_line, y) editor:remove_line_at(y + 1) -- Place the cursor at a sensible position + if x < 0 then x = 0 end editor:move_to(x, y) end @@ -192,8 +193,73 @@ for i = 32, 126 do end end +function dedent_amount(y) + local tabs = editor:get_line_at(y):match("^\t") ~= nil + if tabs then + return 1 + else + return document.tab_width + end +end + +-- Shortcut to indent a selection +event_mapping["ctrl_tab"] = function() + local cursor = editor.cursor + local select = editor.selection + if cursor.y == select.y then + -- Single line is selected + local level = autoindent:get_indent(cursor.y) + autoindent:set_indent(cursor.y, level + 1) + else + -- Multiple lines selected + if cursor.y > select.y then + for line = select.y, cursor.y do + editor:move_to(0, line) + local indent = autoindent:get_indent(line) + autoindent:set_indent(line, indent + 1) + end + else + for line = cursor.y, select.y do + editor:move_to(0, line) + local indent = autoindent:get_indent(line) + autoindent:set_indent(line, indent + 1) + end + end + local cursor_tabs = dedent_amount(cursor.y) + local select_tabs = dedent_amount(select.y) + editor:move_to(cursor.x + cursor_tabs, cursor.y) + editor:select_to(select.x + select_tabs, select.y) + end + editor:cursor_snap() +end + -- Shortcut to dedent a line event_mapping["shift_tab"] = function() - local level = autoindent:get_indent(editor.cursor.y) - autoindent:set_indent(editor.cursor.y, level - 1) + local cursor = editor.cursor + local select = editor.selection + if cursor.x == select.x and cursor.y == select.y then + -- Dedent a single line + local level = autoindent:get_indent(editor.cursor.y) + autoindent:set_indent(editor.cursor.y, level - 1) + else + -- Dedent a group of lines + if cursor.y > select.y then + for line = select.y, cursor.y do + editor:move_to(0, line) + local indent = autoindent:get_indent(line) + autoindent:set_indent(line, indent - 1) + end + else + for line = cursor.y, select.y do + editor:move_to(0, line) + local indent = autoindent:get_indent(line) + autoindent:set_indent(line, indent - 1) + end + end + local cursor_tabs = dedent_amount(cursor.y) + local select_tabs = dedent_amount(select.y) + editor:move_to(cursor.x - cursor_tabs, cursor.y) + editor:select_to(select.x - select_tabs, select.y) + end + editor:cursor_snap() end diff --git a/plugins/emmet.lua b/plugins/emmet.lua index a5fdc4a7..7e83894d 100644 --- a/plugins/emmet.lua +++ b/plugins/emmet.lua @@ -1,5 +1,5 @@ --[[ -Emmet v0.2 +Emmet v0.3 Implementation of Emmet for Ox for rapid web development ]]-- @@ -85,10 +85,11 @@ def place_cursor(expansion): img_match = find_cursor_index(r']*src="()"[^>]*>', 'src') input_match = find_cursor_index(r']*type="()"[^>]*>', 'type') label_match = find_cursor_index(r']*for="()"[^>]*>', 'for') + form_match = find_cursor_index(r']*action="()"[^>]*>', 'action') empty_tag_match = re.search(r"<([a-zA-Z0-9]+)([^>]*)>", expansion) if empty_tag_match is not None: empty_tag_match = empty_tag_match.end(2) + 1 - alone_tags = [a_match, img_match, input_match, label_match, empty_tag_match] + alone_tags = [a_match, img_match, input_match, label_match, form_match, empty_tag_match] try: best_alone = min(filter(lambda x: x is not None, alone_tags)) return best_alone diff --git a/plugins/git.lua b/plugins/git.lua index 2848659b..8c3a68f4 100644 --- a/plugins/git.lua +++ b/plugins/git.lua @@ -1,5 +1,5 @@ --[[ -Git v0.3 +Git v0.4 A plug-in for git integration that provides features to: - Choose which files to add to a commit @@ -87,6 +87,7 @@ function git:diff_all() end function git_branch() + git:refresh_status() local branch = shell:output("git rev-parse --abbrev-ref HEAD") if branch == "" or branch:match("fatal") then return "N/A" @@ -96,7 +97,6 @@ function git_branch() end function git_status(tab) - git:refresh_status() for file, state in pairs(git.status) do if file == tab then if state ~= nil then @@ -111,6 +111,14 @@ function git_status(tab) end end +function git_init() + git:refresh_status() + editor:rerender() +end + +-- Initial status grab +after(0, "git_init") + -- Export the git command commands["git"] = function(args) -- Check if git is installed @@ -185,5 +193,7 @@ commands["git"] = function(args) editor:display_error("Failed to checkout branch '" .. branch .. "'") end end + -- Refresh state after a git command + git:refresh_status() end end diff --git a/plugins/quickcomment.lua b/plugins/quickcomment.lua new file mode 100644 index 00000000..6f64d202 --- /dev/null +++ b/plugins/quickcomment.lua @@ -0,0 +1,77 @@ +--[[ +Quickcomment v0.1 + +A plug-in to help you comment and uncomment lines quickly +]]-- + +quickcomment = {} + +function quickcomment:comment(y) + local line = editor:get_line_at(y) + -- Find start of line + local _, index = line:find("%S") + index = index or 0 + -- Select a comment depending on the language + local comment_start = self:comment_start() .. " " + -- Insert the character + local old_x = editor.cursor.x + editor:move_to(index - 1, y) + editor:insert(comment_start) + editor:move_to(old_x + #comment_start, y) +end + +function quickcomment:uncomment(y) + local comment_start = self:comment_start() .. " " + local line = editor:get_line_at(y) + local old_x = editor.cursor.x + if self:is_commented(y) then + local index = line:find(comment_start) + if index ~= nil then + for i = 0, #comment_start - 1 do + editor:remove_at(index - 1, y) + end + else + comment_start = self:comment_start() + local index = line:find(comment_start) + for i = 0, #comment_start - 1 do + editor:remove_at(index - 1, y) + end + end + editor:move_to(old_x - #comment_start, y) + end +end + +function quickcomment:is_commented(y) + local comment_start = self:comment_start() + local line = editor:get_line_at(y) + local _, index = line:find("%S") + index = index or 0 + return string.sub(line, index, index + #comment_start - 1) == comment_start +end + +function quickcomment:comment_start() + if editor.document_type == "Shell" then + comment_start = "#" + elseif editor.document_type == "Python" then + comment_start = "#" + elseif editor.document_type == "Ruby" then + comment_start = "#" + elseif editor.document_type == "Lua" then + comment_start = "--" + elseif editor.document_type == "Haskell" then + comment_start = "--" + else + comment_start = "//" + end + return comment_start +end + +event_mapping["alt_c"] = function() + if quickcomment:is_commented(editor.cursor.y) then + quickcomment:uncomment(editor.cursor.y) + else + quickcomment:comment(editor.cursor.y) + end + -- Avoid weird behaviour with cursor moving up and down + editor:cursor_snap() +end diff --git a/src/cli.rs b/src/cli.rs index 2d589d5d..2a510e2a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -79,7 +79,7 @@ impl CommandLineInterface { config_path: j .option_arg::(config.clone()) .unwrap_or_else(|| "~/.oxrc".to_string()), - to_open: j.finish(), + to_open: j.finish().into_iter().filter(|o| o != "--").collect(), } } diff --git a/src/config/assistant.rs b/src/config/assistant.rs index a0211b4a..1fb058fb 100644 --- a/src/config/assistant.rs +++ b/src/config/assistant.rs @@ -11,7 +11,6 @@ use mlua::prelude::*; use std::cell::RefCell; use std::io::{stdout, Write}; use std::rc::Rc; -//use std::collections::HashMap; pub const TROPICAL: &str = include_str!("../themes/tropical.lua"); pub const GALAXY: &str = include_str!("../themes/galaxy.lua"); @@ -62,6 +61,7 @@ Ox has an ecosystem of plug-ins that you can make use of, they are as follows: ──────────────────── Code Helpers ──────────────────── AutoIndent - A plug-in that will insert and remove code indentation automatically Pairs - A plug-in that will insert end brackets and end quotes automatically +QuickComment - A plug-in that will help you quickly comment and uncomment lines of code ─────────────────── Web Development ────────────────── Emmet - A neat language to help you write HTML quickly - requires python and the py-emmet module @@ -124,6 +124,7 @@ impl Theme { pub enum Plugin { AutoIndent, Pairs, + QuickComment, DiscordRPC, Emmet, Git, @@ -144,6 +145,7 @@ impl Plugin { match self { Self::AutoIndent => "autoindent", Self::Pairs => "pairs", + Self::QuickComment => "quickcomment", Self::DiscordRPC => "discord_rpc", Self::Emmet => "emmet", Self::Git => "git", @@ -192,7 +194,7 @@ impl Default for Assistant { scroll_sensitivity: 2, cursor_wrap: true, // Plug-ins - plugins: vec![Plugin::AutoIndent, Plugin::Pairs], + plugins: vec![Plugin::AutoIndent, Plugin::Pairs, Plugin::QuickComment], // Misc icons: false, } @@ -490,6 +492,7 @@ impl Assistant { &[ "autoindent", "pairs", + "quickcomment", "emmet", "live_html", "discord_rpc", @@ -505,6 +508,7 @@ impl Assistant { let plugin = match adding.as_str() { "autoindent" => Plugin::AutoIndent, "pairs" => Plugin::Pairs, + "quickcomment" => Plugin::QuickComment, "emmet" => Plugin::Emmet, "live_html" => Plugin::LiveHTML, "discord_rpc" => Plugin::DiscordRPC, @@ -765,7 +769,7 @@ impl Assistant { for plugin in &self.plugins { result += &plugin.to_config(); if plugin == &Plugin::Git { - result += "git = { icons = true }"; + result += "git = { icons = true }\n"; } } // Ready to go diff --git a/src/config/editor.rs b/src/config/editor.rs index 6cf4c72e..0a12d501 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -10,19 +10,40 @@ use mlua::prelude::*; impl LuaUserData for Editor { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { fields.add_field_method_get("cursor", |_, editor| { - let loc = editor.doc().char_loc(); - Ok(LuaLoc { - x: loc.x, - y: loc.y + 1, - }) + if let Some(doc) = editor.try_doc() { + let loc = doc.char_loc(); + Ok(Some(LuaLoc { + x: loc.x, + y: loc.y + 1, + })) + } else { + Ok(None) + } + }); + fields.add_field_method_get("selection", |_, editor| { + if let Some(doc) = editor.try_doc() { + let loc = doc.cursor.selection_end; + Ok(Some(LuaLoc { + x: editor.doc().character_idx(&loc), + y: loc.y + 1, + })) + } else { + Ok(None) + } }); fields.add_field_method_get("document_name", |_, editor| { - let name = editor.doc().file_name.clone(); - Ok(name) + if let Some(doc) = editor.try_doc() { + Ok(Some(doc.file_name.clone())) + } else { + Ok(None) + } }); fields.add_field_method_get("document_length", |_, editor| { - let len = editor.doc().len_lines(); - Ok(len) + if let Some(doc) = editor.try_doc() { + Ok(Some(doc.len_lines())) + } else { + Ok(None) + } }); fields.add_field_method_get("version", |_, _| Ok(VERSION)); fields.add_field_method_get("current_document_id", |_, editor| Ok(editor.ptr)); @@ -34,16 +55,31 @@ impl LuaUserData for Editor { .map_or("Unknown".to_string(), |t| t.name)) }); fields.add_field_method_get("file_name", |_, editor| { - let name = get_file_name(&editor.doc().file_name.clone().unwrap_or_default()); - Ok(name) + if let Some(doc) = editor.try_doc() { + Ok(Some(get_file_name( + &doc.file_name.clone().unwrap_or_default(), + ))) + } else { + Ok(None) + } }); fields.add_field_method_get("file_extension", |_, editor| { - let name = get_file_ext(&editor.doc().file_name.clone().unwrap_or_default()); - Ok(name) + if let Some(doc) = editor.try_doc() { + Ok(Some(get_file_ext( + &doc.file_name.clone().unwrap_or_default(), + ))) + } else { + Ok(None) + } }); fields.add_field_method_get("file_path", |_, editor| { - let name = get_absolute_path(&editor.doc().file_name.clone().unwrap_or_default()); - Ok(name) + if let Some(doc) = editor.try_doc() { + Ok(Some(get_absolute_path( + &doc.file_name.clone().unwrap_or_default(), + ))) + } else { + Ok(None) + } }); } @@ -206,6 +242,24 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); + methods.add_method_mut("cursor_snap", |_, editor, ()| { + editor.doc_mut().old_cursor = editor.doc().loc().x; + Ok(()) + }); + methods.add_method_mut("move_line_up", |_, editor, ()| { + let _ = editor.doc_mut().swap_line_up(); + editor.hl_edit(editor.doc().loc().y); + editor.hl_edit(editor.doc().loc().y + 1); + editor.update_highlighter(); + Ok(()) + }); + methods.add_method_mut("move_line_down", |_, editor, ()| { + let _ = editor.doc_mut().swap_line_down(); + editor.hl_edit(editor.doc().loc().y.saturating_sub(1)); + editor.hl_edit(editor.doc().loc().y); + editor.update_highlighter(); + Ok(()) + }); // Cursor selection and clipboard methods.add_method_mut("select_up", |_, editor, ()| { editor.select_up(); @@ -232,6 +286,20 @@ impl LuaUserData for Editor { editor.update_highlighter(); Ok(()) }); + methods.add_method_mut("select_to", |_, editor, (x, y): (usize, usize)| { + let y = y.saturating_sub(1); + editor.doc_mut().select_to(&Loc { y, x }); + editor.update_highlighter(); + Ok(()) + }); + methods.add_method_mut("cancel_selection", |_, editor, ()| { + editor.doc_mut().cancel_selection(); + Ok(()) + }); + methods.add_method_mut("cursor_to_viewport", |_, editor, ()| { + editor.doc_mut().bring_cursor_in_viewport(); + Ok(()) + }); methods.add_method_mut("cut", |_, editor, ()| { editor.plugin_active = true; if let Err(err) = editor.cut() { @@ -449,11 +517,13 @@ impl LuaUserData for Editor { }); methods.add_method_mut("move_next_match", |_, editor, query: String| { editor.next_match(&query); + editor.doc_mut().cancel_selection(); editor.update_highlighter(); Ok(()) }); methods.add_method_mut("move_previous_match", |_, editor, query: String| { editor.prev_match(&query); + editor.doc_mut().cancel_selection(); editor.update_highlighter(); Ok(()) }); diff --git a/src/config/interface.rs b/src/config/interface.rs index df876da0..43a1a303 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -7,6 +7,7 @@ use crossterm::style::SetForegroundColor as Fg; use kaolinite::searching::Searcher; use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; use mlua::prelude::*; +use std::result::Result as RResult; use super::{issue_warning, Colors}; @@ -316,7 +317,7 @@ impl Default for StatusLine { impl StatusLine { /// Take the configuration information and render the status line - pub fn render(&self, editor: &Editor, lua: &Lua, w: usize) -> String { + pub fn render(&self, editor: &Editor, lua: &Lua, w: usize) -> RResult { let file = &editor.files[editor.ptr]; let mut result = vec![]; let path = editor @@ -365,11 +366,8 @@ impl StatusLine { .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; - } + let r = func.call::(absolute_path.clone())?; + part = part.replace(&m.text, r.to_str().unwrap_or("")); } else { break; } @@ -377,11 +375,11 @@ impl StatusLine { result.push(part); } let status: Vec<&str> = result.iter().map(String::as_str).collect(); - match self.alignment { + Ok(match self.alignment { StatusAlign::Between => alinio::align::between(status.as_slice(), w), StatusAlign::Around => alinio::align::around(status.as_slice(), w), } - .unwrap_or_else(String::new) + .unwrap_or_else(String::new)) } } diff --git a/src/config/keys.rs b/src/config/keys.rs index 511df373..243ee129 100644 --- a/src/config/keys.rs +++ b/src/config/keys.rs @@ -1,5 +1,7 @@ +use crate::error::OxError; /// For dealing with keys in the configuration file use crossterm::event::{KeyCode as KCode, KeyModifiers as KMod, MediaKeyCode, ModifierKeyCode}; +use mlua::prelude::*; /// This contains the code for running code after a key binding is pressed pub fn run_key(key: &str) -> String { @@ -33,6 +35,19 @@ pub fn run_key_before(key: &str) -> String { ) } +/// This contains code for getting event listeners +pub fn get_listeners<'a>(name: &'a str, lua: &'a Lua) -> Result>, OxError> { + let mut result = vec![]; + let listeners: LuaTable = lua + .load(format!("(global_event_mapping[\"{name}\"] or {{}})")) + .eval()?; + for listener in listeners.pairs::() { + let (_, lua_func) = listener?; + result.push(lua_func); + } + Ok(result) +} + /// Converts a key taken from a crossterm event into string format pub fn key_to_string(modifiers: KMod, key: KCode) -> String { let mut result = String::new(); diff --git a/src/config/mod.rs b/src/config/mod.rs index 3578f22a..6f11999a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -22,7 +22,7 @@ pub use assistant::Assistant; 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 keys::{get_listeners, key_to_string, run_key, run_key_before}; pub use tasks::TaskManager; /// Issue a warning to the user @@ -36,6 +36,7 @@ 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"); +const QUICKCOMMENT: &str = include_str!("../../plugins/quickcomment.lua"); /// This contains the code for setting up plug-in infrastructure pub const PLUGIN_BOOTSTRAP: &str = include_str!("../plugin/bootstrap.lua"); @@ -150,14 +151,14 @@ impl Config { pub fn read(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 - lua.load("plugins = {}").exec()?; - lua.load("builtins = {}").exec()?; // Attempt to read config file from home directory let user_provided = Self::get_user_provided_config(path); let mut user_provided_config = false; if let Some(config) = user_provided { + // Reset plugin status based on built-in configuration file + lua.load("plugins = {}").exec()?; + lua.load("builtins = {}").exec()?; // Load in user-defined configuration file lua.load(config).exec()?; user_provided_config = true; @@ -167,6 +168,7 @@ impl Config { let mut builtins: HashMap<&str, &str> = HashMap::default(); builtins.insert("pairs.lua", PAIRS); builtins.insert("autoindent.lua", AUTOINDENT); + builtins.insert("quickcomment.lua", QUICKCOMMENT); for (name, code) in &builtins { if Self::load_bi(name, user_provided_config, lua) { lua.load(*code).exec()?; @@ -194,7 +196,7 @@ impl Config { /// Decide whether to load a built-in plugin pub fn load_bi(name: &str, user_provided_config: bool, lua: &Lua) -> bool { if user_provided_config { - // Get list of user-loaded plug-ins + // Get list of requested built-in plugins let plugins: Vec = lua .globals() .get::<_, LuaTable>("builtins") @@ -213,8 +215,13 @@ impl Config { false } } else { - // Load when the user hasn't provided a configuration file - true + // User hasn't provided configuration file, check for local copy + !lua.globals() + .get::<_, LuaTable>("plugins") + .unwrap() + .sequence_values() + .filter_map(std::result::Result::ok) + .any(|p: String| p.ends_with(name)) } } } diff --git a/src/editor/editing.rs b/src/editor/editing.rs index fa3ef3b8..649683e5 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -179,4 +179,10 @@ impl Editor { self.reload_highlight(); Ok(()) } + + /// Shortcut to help rehighlight a line + pub fn hl_edit(&mut self, y: usize) { + let line = self.doc().line(y).unwrap_or_default(); + self.highlighter().edit(y, &line); + } } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index c44b29f8..586fe005 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,7 +1,7 @@ -/// Functions for rendering the UI -use crate::display; use crate::error::{OxError, Result}; use crate::ui::{size, Feedback}; +/// Functions for rendering the UI +use crate::{display, handle_lua_error}; use crossterm::{ event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, queue, @@ -163,13 +163,37 @@ impl Editor { let idx = y.saturating_sub(start); let line = message .get(idx as usize) - .map_or(" ".repeat(max_width), |s| s.to_string()); + .map_or(" ".repeat(max_width), std::string::ToString::to_string); display!(self, line, " ".repeat(max_width)); } } Ok(()) } + /// Get list of tabs + pub fn get_tab_parts(&mut self, lua: &Lua, w: usize) -> (Vec, usize, usize) { + let mut headers: Vec = vec![]; + let mut idx = 0; + let mut length = 0; + let mut offset = 0; + let tab_line = self.config.tab_line.borrow(); + for (c, file) in self.files.iter().enumerate() { + let render = tab_line.render(lua, file, &mut self.feedback); + length += width(&render, 4) + 1; + headers.push(render); + if c == self.ptr { + idx = headers.len().saturating_sub(1); + } + while c == self.ptr && length > w { + headers.remove(0); + length = length.saturating_sub(width(&headers[0], 4) + 1); + idx = headers.len().saturating_sub(1); + offset += 1; + } + } + (headers, idx, offset) + } + /// Render the tab line at the top of the document #[allow(clippy::similar_names)] pub fn render_tab_line(&mut self, lua: &Lua, w: usize) -> Result<()> { @@ -178,29 +202,23 @@ impl Editor { let tab_inactive_fg = Fg(self.config.colors.borrow().tab_inactive_fg.to_color()?); let tab_active_bg = Bg(self.config.colors.borrow().tab_active_bg.to_color()?); let tab_active_fg = Fg(self.config.colors.borrow().tab_active_fg.to_color()?); + let (tabs, idx, _) = self.get_tab_parts(lua, w); display!(self, tab_inactive_fg, tab_inactive_bg); - for (c, file) in self.files.iter().enumerate() { - let document_header = - self.config - .tab_line - .borrow() - .render(lua, file, &mut self.feedback); - if c == self.ptr { - // Representing the document we're currently looking at + for (c, header) in tabs.iter().enumerate() { + if c == idx { display!( self, tab_active_bg, tab_active_fg, SetAttribute(Attribute::Bold), - document_header, + header, SetAttribute(Attribute::Reset), tab_inactive_fg, tab_inactive_bg, "│" ); } else { - // Other document that is currently open - display!(self, document_header, "│"); + display!(self, header, "│"); } } display!(self, " ".to_string().repeat(w)); @@ -215,17 +233,33 @@ impl Editor { let editor_fg = Fg(self.config.colors.borrow().editor_fg.to_color()?); let status_bg = Bg(self.config.colors.borrow().status_bg.to_color()?); let status_fg = Fg(self.config.colors.borrow().status_fg.to_color()?); - let content = self.config.status_line.borrow().render(self, lua, w); - display!( - self, - status_bg, - status_fg, - SetAttribute(Attribute::Bold), - content, - SetAttribute(Attribute::Reset), - editor_fg, - editor_bg - ); + match self.config.status_line.borrow().render(self, lua, w) { + Ok(content) => { + display!( + self, + status_bg, + status_fg, + SetAttribute(Attribute::Bold), + content, + SetAttribute(Attribute::Reset), + editor_fg, + editor_bg + ); + } + Err(lua_error) => { + display!( + self, + status_bg, + status_fg, + SetAttribute(Attribute::Bold), + " ".repeat(w), + SetAttribute(Attribute::Reset), + editor_fg, + editor_bg + ); + handle_lua_error("status_line", Err(lua_error), &mut self.feedback); + } + } Ok(()) } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index c9c1f2e6..4ffa4f36 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -6,7 +6,7 @@ use crossterm::event::{ Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEvent, MouseEventKind, }; use kaolinite::event::Error as KError; -use kaolinite::utils::get_absolute_path; +use kaolinite::utils::{get_absolute_path, get_file_name}; use kaolinite::Document; use mlua::{Error as LuaError, Lua}; use std::env; @@ -133,6 +133,12 @@ impl Editor { /// Function to open a document into the editor pub fn open(&mut self, file_name: &str) -> Result<()> { + if let Some(idx) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) { + self.ptr = idx; + return Err(OxError::AlreadyOpen( + get_file_name(file_name).unwrap_or_default(), + )); + } let mut size = size()?; size.h = size.h.saturating_sub(1 + self.push_down); let mut doc = Document::open(size, file_name)?; @@ -207,6 +213,18 @@ impl Editor { } } + /// Determine if a file is already open + pub fn already_open(&mut self, abs_path: &str) -> Option { + for (ptr, file) in self.files.iter().enumerate() { + let file_path = file.doc.file_name.as_ref(); + let file_path = file_path.map(|f| get_absolute_path(f).unwrap_or_default()); + if file_path == Some(abs_path.to_string()) { + return Some(ptr); + } + } + None + } + /// save the document to the disk pub fn save(&mut self) -> Result<()> { // Commit events to event manager (for undo / redo) @@ -300,6 +318,11 @@ impl Editor { } } + /// Try to get a document + pub fn try_doc(&self) -> Option<&Document> { + self.files.get(self.ptr).map(|file| &file.doc) + } + /// Returns a document at a certain index pub fn get_doc(&mut self, idx: usize) -> &mut Document { &mut self.files.get_mut(idx).unwrap().doc @@ -394,6 +417,7 @@ impl Editor { /// Handle paste pub fn handle_paste(&mut self, text: &str) -> Result<()> { + // Apply paste for ch in text.chars() { self.character(ch)?; } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index 4738f569..dbfbeb6c 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -1,3 +1,4 @@ +use crate::ui::size; /// For handling mouse events use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; use kaolinite::{utils::width, Loc}; @@ -22,17 +23,13 @@ impl Editor { let tab_enabled = self.config.tab_line.borrow().enabled; let tab = usize::from(tab_enabled); if event.row == 0 && tab_enabled { + let (tabs, _, offset) = self.get_tab_parts(lua, size().map_or(0, |s| s.w)); let mut c = event.column + 2; - for (i, file) in self.files.iter().enumerate() { - let header = self - .config - .tab_line - .borrow() - .render(lua, file, &mut self.feedback); - let header_len = width(&header, self.config.document.borrow().tab_width) + 1; + for (i, header) in tabs.iter().enumerate() { + let header_len = width(header, 4) + 1; c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); if c == 0 { - return MouseLocation::Tabs(i); + return MouseLocation::Tabs(i + offset); } } MouseLocation::Out diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 9bb1a55f..3768c410 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -1,10 +1,11 @@ +use crate::display; /// Functions for searching and replacing -use crate::error::Result; +use crate::error::{OxError, Result}; use crate::ui::size; use crossterm::{ event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, queue, - style::Print, + style::{Attribute, Print, SetAttribute, SetBackgroundColor as Bg}, }; use kaolinite::utils::{Loc, Size}; use mlua::Lua; @@ -14,12 +15,65 @@ use super::Editor; impl Editor { /// Use search feature pub fn search(&mut self, lua: &Lua) -> Result<()> { + let cache = self.doc().char_loc(); // Prompt for a search term - let target = self.prompt("Search")?; + let mut target = String::new(); + let mut done = false; + while !done { + let Size { w, h } = size()?; + // Render prompt message + self.terminal.prepare_line(h)?; + self.terminal.show_cursor()?; + let editor_bg = Bg(self.config.colors.borrow().editor_bg.to_color()?); + display!( + self, + editor_bg, + "Search: ", + target.clone(), + "│", + " ".to_string().repeat(w) + ); + self.terminal.hide_cursor()?; + self.render_document(lua, w, h.saturating_sub(2))?; + // Move back to correct cursor position + if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { + let max = self.dent(); + self.terminal.goto(x + max, y + 1)?; + self.terminal.show_cursor()?; + } else { + self.terminal.hide_cursor()?; + } + self.terminal.flush()?; + 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, + // Cancel operation + (KMod::NONE, KCode::Esc) => { + self.doc_mut().move_to(&cache); + self.doc_mut().cancel_selection(); + return Err(OxError::Cancelled); + } + // Remove from the input string if the user presses backspace + (KMod::NONE, KCode::Backspace) => { + target.pop(); + self.doc_mut().move_to(&cache); + self.next_match(&target); + } + // Add to the input string if the user presses a character + (KMod::NONE | KMod::SHIFT, KCode::Char(c)) => { + target.push(c); + self.doc_mut().move_to(&cache); + self.next_match(&target); + } + _ => (), + } + } + } + + // Main body of the search feature 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 @@ -29,7 +83,7 @@ impl Editor { self.terminal.goto(0, h)?; queue!( self.terminal.stdout, - Print("[<-]: Search previous | [->]: Search next") + Print("[<-]: Search previous | [->]: Search next | [Enter] Finish | [Esc] Cancel") )?; // Move back to correct cursor position if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { @@ -44,7 +98,11 @@ impl Editor { 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, + (KMod::NONE, KCode::Enter) => done = true, + (KMod::NONE, KCode::Esc) => { + self.doc_mut().move_to(&cache); + 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 @@ -54,13 +112,19 @@ impl Editor { } self.update_highlighter(); } + self.doc_mut().cancel_selection(); 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); + // Select match + self.doc_mut().cancel_selection(); + let mut move_to = mtch.loc; + move_to.x += mtch.text.chars().count(); + self.doc_mut().move_to(&move_to); + self.doc_mut().select_to(&mtch.loc); // Update highlighting self.update_highlighter(); Some(mtch.text) @@ -70,6 +134,12 @@ impl Editor { pub fn prev_match(&mut self, target: &str) -> Option { let mtch = self.doc_mut().prev_match(target)?; self.doc_mut().move_to(&mtch.loc); + // Select match + self.doc_mut().cancel_selection(); + let mut move_to = mtch.loc; + move_to.x += mtch.text.chars().count(); + self.doc_mut().move_to(&move_to); + self.doc_mut().select_to(&mtch.loc); // Update highlighting self.update_highlighter(); Some(mtch.text) @@ -105,7 +175,9 @@ impl Editor { self.terminal.goto(0, h)?; queue!( self.terminal.stdout, - Print("[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All") + Print( + "[<-] Previous | [->] Next | [Enter] Replace | [Tab] Replace All | [Esc] Exit" + ) )?; // Move back to correct cursor location if let Some(Loc { x, y }) = self.doc().cursor_loc_in_screen() { @@ -135,6 +207,7 @@ impl Editor { // Update syntax highlighter if necessary self.update_highlighter(); } + self.doc_mut().cancel_selection(); Ok(()) } diff --git a/src/error.rs b/src/error.rs index e4438c96..408b3a61 100644 --- a/src/error.rs +++ b/src/error.rs @@ -32,6 +32,9 @@ quick_error! { Cancelled { display("Operation Cancelled") } + AlreadyOpen(file: String) { + display("File '{}' is already open", file) + } None } } diff --git a/src/main.rs b/src/main.rs index 57bdea63..e3bd2d67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,21 +8,23 @@ mod ui; use cli::CommandLineInterface; use config::{ - key_to_string, run_key, run_key_before, Assistant, Config, PLUGIN_BOOTSTRAP, PLUGIN_MANAGER, - PLUGIN_NETWORKING, PLUGIN_RUN, + get_listeners, key_to_string, run_key, run_key_before, Assistant, Config, PLUGIN_BOOTSTRAP, + PLUGIN_MANAGER, PLUGIN_NETWORKING, PLUGIN_RUN, }; use crossterm::event::Event as CEvent; use editor::{Editor, FileTypes}; -use error::Result; -use kaolinite::event::Event; +use error::{OxError, Result}; +use kaolinite::event::{Error as KError, Event}; use kaolinite::searching::Searcher; +use kaolinite::utils::file_or_dir; use kaolinite::Loc; use mlua::Error::{RuntimeError, SyntaxError}; use mlua::{FromLua, Lua, Value}; use std::cell::RefCell; +use std::io::ErrorKind; use std::rc::Rc; use std::result::Result as RResult; -use ui::Feedback; +use ui::{fatal_error, Feedback}; /// Entry point - grabs command line arguments and runs the editor fn main() { @@ -64,18 +66,26 @@ fn run(cli: &CommandLineInterface) -> Result<()> { lua.globals().set("editor", editor.clone())?; // Inject the networking library for plug-ins to use - handle_lua_error(&editor, "", lua.load(PLUGIN_NETWORKING).exec()); + handle_lua_error( + "", + lua.load(PLUGIN_NETWORKING).exec(), + &mut editor.borrow_mut().feedback, + ); // Load config and initialise lua.load(PLUGIN_BOOTSTRAP).exec()?; 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)); + handle_lua_error("configuration", Err(err), &mut editor.borrow_mut().feedback); }; // Run plug-ins - handle_lua_error(&editor, "", lua.load(PLUGIN_RUN).exec()); + handle_lua_error( + "", + lua.load(PLUGIN_RUN).exec(), + &mut editor.borrow_mut().feedback, + ); // Load in the file types let file_types = lua @@ -86,9 +96,13 @@ fn run(cli: &CommandLineInterface) -> Result<()> { editor.borrow_mut().config.document.borrow_mut().file_types = file_types; // Open files user has asked to open + let cwd = std::env::current_dir()?; for (c, file) in cli.to_open.iter().enumerate() { + // Reset cwd + let _ = std::env::set_current_dir(&cwd); // Open the file - editor.borrow_mut().open_or_new(file.to_string())?; + let result = editor.borrow_mut().open_or_new(file.to_string()); + handle_file_opening(&editor, result, file); // Set read only if applicable if cli.flags.read_only { editor.borrow_mut().get_doc(c).info.read_only = true; @@ -138,7 +152,11 @@ fn run(cli: &CommandLineInterface) -> Result<()> { editor.borrow_mut().new_if_empty()?; // Add in the plugin manager - handle_lua_error(&editor, "", lua.load(PLUGIN_MANAGER).exec()); + handle_lua_error( + "", + lua.load(PLUGIN_MANAGER).exec(), + &mut editor.borrow_mut().feedback, + ); // Run the editor and handle errors if applicable editor.borrow().update_cwd(); @@ -159,7 +177,7 @@ fn run(cli: &CommandLineInterface) -> Result<()> { for task in exec { if let Ok(target) = lua.globals().get::<_, mlua::Function>(task.clone()) { // Run the code - handle_lua_error(&editor, "task", target.call(())); + handle_lua_error("task", target.call(()), &mut editor.borrow_mut().feedback); } else { editor.borrow_mut().feedback = Feedback::Warning(format!("Function '{task}' was not found")); @@ -175,7 +193,19 @@ 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(&key_str, result, &mut editor.borrow_mut().feedback); + } + + // Handle paste event (before event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("before:paste", &lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut editor.borrow_mut().feedback, + ); + } } // Actually handle editor event (errors included) @@ -183,12 +213,24 @@ fn run(cli: &CommandLineInterface) -> Result<()> { editor.borrow_mut().feedback = Feedback::Error(format!("{err:?}")); } + // Handle paste event (after event) + if let CEvent::Paste(ref paste_text) = event { + let listeners = get_listeners("paste", &lua)?; + for listener in listeners { + handle_lua_error( + "paste", + listener.call(paste_text.clone()), + &mut editor.borrow_mut().feedback, + ); + } + } + // Handle plug-in after key press mappings (if no errors occured) if let CEvent::Key(key) = event { let key_str = key_to_string(key.modifiers, key.code); let code = run_key(&key_str); let result = lua.load(&code).exec(); - handle_lua_error(&editor, &key_str, result); + handle_lua_error(&key_str, result, &mut editor.borrow_mut().feedback); } editor.borrow_mut().update_highlighter(); @@ -204,14 +246,14 @@ fn run(cli: &CommandLineInterface) -> Result<()> { // Run any plugin cleanup operations let result = lua.load(run_key("exit")).exec(); - handle_lua_error(&editor, "exit", result); + handle_lua_error("exit", result, &mut editor.borrow_mut().feedback); editor.borrow_mut().terminal.end()?; Ok(()) } /// Handle a lua error, showing the user an informative error -fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult<(), mlua::Error>) { +fn handle_lua_error(key_str: &str, error: RResult<(), mlua::Error>, feedback: &mut Feedback) { match error { // All good Ok(()) => (), @@ -248,16 +290,14 @@ fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult< // 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")); + *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")); + *feedback = Feedback::Error(format!("The command '{key_str}' is not defined")); } else { // Some other runtime error - editor.borrow_mut().feedback = Feedback::Error(msg.to_string()); + *feedback = Feedback::Error(msg.to_string()); } } // Handle a syntax error @@ -266,18 +306,54 @@ fn handle_lua_error(editor: &Rc>, key_str: &str, error: RResult< let mut message = message.rsplit(':').take(2).collect::>(); message.reverse(); let message = message.join(":"); - editor.borrow_mut().feedback = + *feedback = Feedback::Error(format!("Syntax Error in config file on line {message}")); } else { - editor.borrow_mut().feedback = - Feedback::Error(format!("Syntax Error: {message:?}")); + *feedback = Feedback::Error(format!("Syntax Error: {message:?}")); } } // Other miscellaneous error Err(err) => { - editor.borrow_mut().feedback = - Feedback::Error(format!("Failed to run Lua code: {err:?}")); + *feedback = Feedback::Error(format!("Failed to run Lua code: {err:?}")); + } + } +} + +/// Handle opening files +fn handle_file_opening(editor: &Rc>, result: Result<()>, name: &str) { + // TEMPORARY WORK-AROUND: Delete after Rust 1.83 + if file_or_dir(name) == "directory" { + fatal_error(&format!("'{name}' is a directory, not a file")); + } + match result { + Ok(()) => (), + Err(OxError::AlreadyOpen(_)) => { + let len = editor.borrow().files.len().saturating_sub(1); + editor.borrow_mut().ptr = len; + } + Err(OxError::Kaolinite(kerr)) => { + match kerr { + KError::Io(ioerr) => match ioerr.kind() { + ErrorKind::NotFound => fatal_error(&format!("File '{name}' not found")), + ErrorKind::PermissionDenied => { + fatal_error(&format!("Permission to read file '{name}' denied")); + } + /* + // NOTE: Uncomment when Rust 1.83 becomes stable (io_error_more will be stabilised) + ErrorKind::IsADirectory => + fatal_error(&format!("'{name}' is a directory, not a file")), + ErrorKind::ReadOnlyFilesystem => + fatal_error(&format!("You are on a read only file system")), + ErrorKind::ResourceBusy => + fatal_error(&format!("The resource '{name}' is busy")), + */ + ErrorKind::OutOfMemory => fatal_error("You are out of memory"), + kind => fatal_error(&format!("I/O error occured: {kind:?}")), + }, + _ => fatal_error(&format!("Backend error opening '{name}': {kerr:?}")), + } } + result => fatal_error(&format!("Error opening file '{name}': {result:?}")), } } @@ -288,6 +364,10 @@ fn run_editor_command(editor: &Rc>, cmd: &str, lua: &Lua) { let arguments = arguments.join("', '"); let code = format!("(commands['{subcmd}'] or error('command not found'))({{'{arguments}'}})"); - handle_lua_error(editor, subcmd, lua.load(code).exec()); + handle_lua_error( + subcmd, + lua.load(code).exec(), + &mut editor.borrow_mut().feedback, + ); } } diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index e5d4db6e..a741d871 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -36,7 +36,8 @@ function load_plugin(base) -- Prevent warning if plug-in is built-in local is_autoindent = base:match("autoindent.lua$") ~= nil local is_pairs = base:match("pairs.lua$") ~= nil - if not is_pairs and not is_autoindent then + local is_quickcomment = base:match("quickcomment.lua$") ~= nil + if not is_pairs and not is_autoindent and not is_quickcomment then -- Issue warning if plug-in is builtin print("[WARNING] Failed to load plugin " .. base) plugin_issues = true diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index fe5ca70a..2ecd6e61 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -108,7 +108,8 @@ function plugin_manager:plugin_is_builtin(plugin) local base = plugin .. ".lua" local is_autoindent = base == "autoindent.lua" local is_pairs = base == "pairs.lua" - return is_autoindent or is_pairs + local is_quickcomment = base == "quickcomment.lua" + return is_autoindent or is_pairs or is_quickcomment end -- Verify whether or not a plug-in is downloaded diff --git a/src/ui.rs b/src/ui.rs index 7b0a09f1..456402cb 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use crossterm::{ KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }, execute, queue, - style::{Attribute, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, + style::{Attribute, Color, SetAttribute, SetBackgroundColor as Bg, SetForegroundColor as Fg}, terminal::{ self, Clear, ClearType as ClType, DisableLineWrap, EnableLineWrap, EnterAlternateScreen, LeaveAlternateScreen, @@ -40,7 +40,20 @@ pub fn size() -> Result { }) } +/// Fatal Error +pub fn fatal_error(msg: &str) { + eprintln!( + "{}{}[Error]{}{} {msg}", + SetAttribute(Attribute::Bold), + Fg(Color::Red), + Fg(Color::Reset), + SetAttribute(Attribute::Reset) + ); + std::process::exit(1); +} + /// Represents different status messages +#[derive(Debug)] pub enum Feedback { Info(String), Warning(String),