diff --git a/Cargo.lock b/Cargo.lock index 1eb1fbfe..f0873067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libredox" @@ -306,7 +306,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.6.1" +version = "0.6.2" dependencies = [ "alinio", "base64", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "ppv-lite86" @@ -413,9 +413,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags", ] @@ -591,9 +591,9 @@ dependencies = [ [[package]] name = "synoptic" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374eba7bc5f97fbd1ddd40f7e071ce4366cb7859cc108c8f2e393ddc51fd08a" +checksum = "ae9709e6bef1ab11de1bf1d091cd6e60468f35d54b72e83005a6396a80c96c15" dependencies = [ "if_chain", "regex", @@ -602,18 +602,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7b4902b9..fd1e39d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["cactus"] [package] name = "ox" -version = "0.6.1" +version = "0.6.2" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A Rust powered text editor." diff --git a/config/.oxrc b/config/.oxrc index 7ef8a8e3..649e2d95 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -117,6 +117,8 @@ event_mapping = { -- delete old copy and reposition cursor editor:remove_line_at(y + 1) editor:move_up() + -- correct indentation level + autoindent:fix_indent() end, ["alt_down"] = function() -- current line information @@ -127,6 +129,8 @@ event_mapping = { -- delete old copy and reposition cursor editor:remove_line_at(y) editor:move_down() + -- correct indentation level + autoindent:fix_indent() end, ["ctrl_w"] = function() y = editor.cursor.y @@ -291,5 +295,5 @@ syntax:set("block", {40, 198, 232}) -- Quotes in various markup languages e.g. _ syntax:set("list", {86, 217, 178}) -- Quotes in various markup languages e.g. _ in markdown -- Import plugins (must be at the bottom of this file) --- load_plugin("pairs.lua") --- load_plugin("autoindent.lua") +load_plugin("pairs.lua") +load_plugin("autoindent.lua") diff --git a/kaolinite/src/document.rs b/kaolinite/src/document.rs index 7f8973da..5ed2bf52 100644 --- a/kaolinite/src/document.rs +++ b/kaolinite/src/document.rs @@ -128,11 +128,11 @@ impl Document { /// or character set issues. pub fn save(&mut self) -> Result<()> { if !self.read_only { - self.undo_mgmt.saved(); - self.modified = false; 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) diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index 5924ec08..33a27ba9 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.1 +Auto Indent v0.4 You will be able to press return at the start of a block and have Ox automatically indent for you. @@ -9,20 +9,21 @@ 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 ends(suffix) - return suffix == "" or sline:sub(-#suffix) == suffix - end local function starts(prefix) return sline:sub(1, #prefix) == prefix + end + local function ends(suffix) + return suffix == "" or sline:sub(-#suffix) == suffix end -- Work out how indented the line was - indents = #(line:match("^\t+") or "") + #(line:match("^ +") or "") / 4 + 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 @@ -33,7 +34,7 @@ event_mapping["enter"] = function() if ends("do") then indents = indents + 1 end elseif editor.document_type == "Lua" then func = ends(")") and (starts("function") or starts("local function")) - if ends("do") or ends("then") or func then indents = indents + 1 end + if ends("else") or ends("do") or ends("then") or func then indents = indents + 1 end elseif editor.document_type == "Haskell" then if ends("where") or ends("let") or ends("do") then indents = indents + 1 end elseif editor.document_type == "Shell" then @@ -43,8 +44,96 @@ event_mapping["enter"] = function() 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) + end +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 --- Helper function to check string endings -local function ends_with(str, ending) +event_mapping["*"] = function() + line = editor:get_line() + local function ends(suffix) + return line:match("^%s*" .. suffix .. "$") ~= nil + end + 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 + elseif editor.document_type == "Python" then + if ends("else") or ends("elif") or ends("except") or ends("finally") then do_dedent() 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 + elseif editor.document_type == "Lua" then + if ends("end") or ends("else") or ends("elseif") or ends("until") then do_dedent() end + elseif editor.document_type == "Haskell" then + if ends("else") or ends("in") then do_dedent() end + end +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 + local new_line = nil + if indent_change > 0 then + -- Insert indentation + if line:match("\t") ~= nil then + -- Insert Tabs + new_line = string.rep("\t", indent_change) .. line + else + -- Insert Spaces + new_line = string.rep(" ", indent_change * document.tab_width) .. line + end + elseif indent_change < 0 then + -- Remove indentation + if line:match("\t") ~= nil then + -- Remove Tabs + new_line = line:gsub("\t", "", -indent_change) + else + -- Remove Spaces + 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) end diff --git a/plugins/pairs.lua b/plugins/pairs.lua index 5104ba5d..63ad9630 100644 --- a/plugins/pairs.lua +++ b/plugins/pairs.lua @@ -1,37 +1,124 @@ --[[ -Bracket Pairs v0.1 +Bracket Pairs v0.2 This will automatically insert a closing bracket or quote when you type an opening one ]]-- -event_mapping["("] = function() - editor:insert(")") - editor:move_left() -end +-- The following pairs are in the form [start of pair][end of pair] +pairings = { + -- Bracket pairs + "()", "[]", "{}", + -- Quote pairs + '""', "''", "``", + -- Other pairs you wish to define can be added below... +} -event_mapping["["] = function() - editor:insert("]") - editor:move_left() -end +just_paired = { x = nil, y = nil } +was_pasting = editor.pasting +line_cache = { y = editor.cursor.y, line = editor:get_line() } -event_mapping["{"] = function() - editor:insert("}") - editor:move_left() +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) + 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() } + end end -event_mapping["\""] = function() - editor:insert("\"") - editor:move_left() +-- Link up pairs to event mapping +for i, str in ipairs(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 + at_char = editor:get_character_at(editor.cursor.x - 2, editor.cursor.y) + 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 = 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() } + else + 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 = 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 } + 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 + editor:insert(end_pair) + editor:move_left() + line_cache = { y = editor.cursor.y, line = editor:get_line() } + end + end end -event_mapping["'"] = function() - editor:insert("'") - editor:move_left() +-- Automatically delete pairs +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 -event_mapping["`"] = function() - editor:insert("`") - editor:move_left() +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 - diff --git a/src/config.rs b/src/config.rs index b09efcf3..2c0f340e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,80 +22,29 @@ fn issue_warning(msg: &str) { /// 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 = r#" -home = os.getenv("HOME") or os.getenv("USERPROFILE") - -function file_exists(file_path) - local file = io.open(file_path, "r") - if file then - file:close() - return true - else - return false - end -end - -plugins = {} -plugin_issues = false - -function load_plugin(base) - path_cross = base - path_unix = home .. "/.config/ox/" .. base - path_win = home .. "/ox/" .. base - if file_exists(path_cross) then - path = path_cross - elseif file_exists(path_unix) then - path = path_unix - elseif file_exists(path_win) then - path = file_win - else - print("[WARNING] Failed to load plugin " .. base) - plugin_issues = true - end - plugins[#plugins + 1] = path -end -"#; +pub const PLUGIN_BOOTSTRAP: &str = include_str!("plugin/bootstrap.lua"); /// This contains the code for running the plugins -pub const PLUGIN_RUN: &str = " -global_event_mapping = {} - -function merge_event_mapping() - for key, f in pairs(event_mapping) do - if global_event_mapping[key] ~= nil then - table.insert(global_event_mapping[key], f) - else - global_event_mapping[key] = {f,} - end - end - event_mapping = {} -end - -for c, path in ipairs(plugins) do - merge_event_mapping() - dofile(path) -end -merge_event_mapping() - -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)\") - print(\"https://github.com/curlpipe/ox\") - print(\"\") - print(\"Alternatively, you may silence these warnings\\nby removing the load_plugin() lines in your configuration file\\nfor the missing plug-ins that are listed above\") -end -"; +pub const PLUGIN_RUN: &str = include_str!("plugin/run.lua"); /// This contains the code for handling a key binding 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 - " + " ) } @@ -152,20 +101,21 @@ impl Config { 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()?; - } else { - return Err(OxError::Config("Not Found".to_string())); + user_provided_config = true; } - } else { - return Err(OxError::Config("Not Found".to_string())); } // Remove any default values if necessary @@ -173,7 +123,47 @@ impl Config { self.status_line.borrow_mut().parts.drain(0..status_parts); } - Ok(()) + // 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 + } + } } } @@ -1070,6 +1060,9 @@ impl LuaUserData for DocumentConfig { 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.rs b/src/editor.rs index 5e2f112f..b5de8c99 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -46,6 +46,8 @@ 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 { @@ -67,6 +69,7 @@ impl Editor { last_active: Instant::now(), push_down: 1, config_path: "~/.oxrc".to_string(), + paste_flag: false, }) } @@ -248,15 +251,19 @@ impl Editor { }; match event { CEvent::Key(key) => { - // Check period of inactivity and commit events (for undo/redo) if over 10secs + // Check period of inactivity let end = Instant::now(); - let inactivity = end.duration_since(self.last_active).as_secs() as usize; - if inactivity > self.config.document.borrow().undo_period { + 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()?, diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua new file mode 100644 index 00000000..3b88dbc1 --- /dev/null +++ b/src/plugin/bootstrap.lua @@ -0,0 +1,40 @@ +home = os.getenv("HOME") or os.getenv("USERPROFILE") + +function file_exists(file_path) + local file = io.open(file_path, "r") + if file then + file:close() + return true + else + return false + end +end + +plugins = {} +builtins = {} +plugin_issues = false + +function load_plugin(base) + path_cross = base + path_unix = home .. "/.config/ox/" .. base + path_win = home .. "/ox/" .. base + if file_exists(path_cross) then + path = path_cross + elseif file_exists(path_unix) then + path = path_unix + elseif file_exists(path_win) then + path = file_win + else + -- 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 + -- Issue warning if plug-in is builtin + print("[WARNING] Failed to load plugin " .. base) + plugin_issues = true + else + table.insert(builtins, base) + end + end + plugins[#plugins + 1] = path +end diff --git a/src/plugin/run.lua b/src/plugin/run.lua new file mode 100644 index 00000000..f0167245 --- /dev/null +++ b/src/plugin/run.lua @@ -0,0 +1,26 @@ +global_event_mapping = {} + +function merge_event_mapping() + for key, f in pairs(event_mapping) do + if global_event_mapping[key] ~= nil then + table.insert(global_event_mapping[key], f) + else + global_event_mapping[key] = {f,} + end + end + event_mapping = {} +end + +for c, path in ipairs(plugins) do + merge_event_mapping() + dofile(path) +end +merge_event_mapping() + +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)") + print("https://github.com/curlpipe/ox") + print("") + print("Alternatively, you may silence these warnings\nby removing the load_plugin() lines in your configuration file\nfor the missing plug-ins that are listed above") +end