diff --git a/Cargo.lock b/Cargo.lock index 35fa6ab7..02b5c89f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.6.5" +version = "0.6.6" dependencies = [ "alinio", "base64", diff --git a/Cargo.toml b/Cargo.toml index ff0d0981..72381b8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["cactus"] [package] name = "ox" -version = "0.6.5" +version = "0.6.6" 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 520c1cc0..40061a29 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -180,8 +180,8 @@ commands = { end end, ["filetype"] = function(arguments) - ext = arguments[1] - editor:set_file_type(ext) + local file_type_name = arguments[1] + editor:set_file_type(file_type_name) end, ["reload"] = function(arguments) editor:reload_config() diff --git a/kaolinite/src/document.rs b/kaolinite/src/document.rs index dc93f636..920d33cf 100644 --- a/kaolinite/src/document.rs +++ b/kaolinite/src/document.rs @@ -767,10 +767,6 @@ 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_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_default(); @@ -810,6 +806,43 @@ impl Document { self.load_to(self.offset.y + self.size.h); } + /// Select a word at a location + pub fn select_word_at(&mut self, loc: &Loc) { + let y = loc.y; + let x = self.character_idx(loc); + let re = format!("(\t| {{{}}}|^|\\W| )", self.tab_width); + let start = if let Some(mut mtch) = self.prev_match(&re) { + let len = mtch.text.chars().count(); + let same = mtch.loc.x + len == x; + if !same { + mtch.loc.x += len; + } + self.move_to(&mtch.loc); + if same && self.loc().x != 0 { + self.move_prev_word(); + } + mtch.loc.x + } else { + 0 + }; + let re = format!("(\t| {{{}}}|\\W|$|^ +| )", self.tab_width); + let end = if let Some(mtch) = self.next_match(&re, 0) { + mtch.loc.x + } else { + self.line(y).unwrap_or_default().chars().count() + }; + self.move_to(&Loc { x: start, y }); + self.select_to(&Loc { x: end, y }); + self.old_cursor = self.loc().x; + } + + /// Select a line at a location + pub fn select_line_at(&mut self, y: usize) { + let len = self.line(y).unwrap_or_default().chars().count(); + self.move_to(&Loc { x: 0, y }); + self.select_to(&Loc { x: len, y }); + } + /// Brings the cursor into the viewport so it can be seen pub fn bring_cursor_in_viewport(&mut self) { if self.offset.y > self.cursor.loc.y { diff --git a/kaolinite/tests/test.rs b/kaolinite/tests/test.rs index 73bed97a..a6701d06 100644 --- a/kaolinite/tests/test.rs +++ b/kaolinite/tests/test.rs @@ -605,6 +605,27 @@ fn document_selection() { assert!(!doc.is_loc_selected(Loc { x: 0, y: 0 })); assert!(!doc.is_loc_selected(Loc { x: 2, y: 0 })); assert!(!doc.is_loc_selected(Loc { x: 3, y: 0 })); + doc.select_line_at(1); + assert_eq!( + doc.selection_loc_bound(), + (Loc { x: 0, y: 1 }, Loc { x: 31, y: 1 }) + ); + doc.remove_selection(); + doc.exe(Event::InsertLine(1, "hello there world".to_string())); + doc.exe(Event::InsertLine(2, "hello".to_string())); + doc.move_to(&Loc { x: 8, y: 1 }); + doc.select_word_at(&Loc { x: 8, y: 1 }); + assert_eq!( + doc.selection_loc_bound(), + (Loc { x: 6, y: 1 }, Loc { x: 11, y: 1 }) + ); + doc.remove_selection(); + doc.move_to(&Loc { x: 0, y: 2 }); + doc.select_word_at(&Loc { x: 0, y: 2 }); + assert_eq!( + doc.selection_loc_bound(), + (Loc { x: 0, y: 2 }, Loc { x: 5, y: 2 }) + ); } #[test] diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index ad62f37f..0fe520c4 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.7 +Auto Indent v0.8 Helps you when programming by guessing where indentation should go and then automatically applying these guesses as you program @@ -170,10 +170,23 @@ event_mapping["enter"] = function() 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) +-- For each ascii characters and punctuation +was_dedenting = false +for i = 32, 126 do + local char = string.char(i) + -- ... excluding the global event binding + if char ~= "*" then + -- Keep track of whether the line was previously dedenting beforehand + event_mapping["before:" .. char] = function() + was_dedenting = autoindent:causes_dedent(editor.cursor.y) + end + -- Trigger dedent checking + event_mapping[char] = function() + -- Dedent where appropriate + if autoindent:causes_dedent(editor.cursor.y) and not was_dedenting then + local new_level = autoindent:get_indent(editor.cursor.y) - 1 + autoindent:set_indent(editor.cursor.y, new_level) + end + end end end diff --git a/src/cli.rs b/src/cli.rs index 3a45bb19..69ab78ad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,12 +12,12 @@ Ox: A lightweight and flexible text editor USAGE: ox [options] [files] OPTIONS: - --help, -h : Show this help message - --version, -v : Show the version number - --config [path], -c [path] : Specify the configuration file - --readonly, -r : Prevent opened files from writing - --filetype [ext], -f [ext] : Set the file type of files opened - --stdin : Reads file from the stdin + --help, -h : Show this help message + --version, -v : Show the version number + --config [path], -c [path] : Specify the configuration file + --readonly, -r : Prevent opened files from writing + --filetype [name], -f [name] : Set the file type of files opened + --stdin : Reads file from the stdin EXAMPLES: ox @@ -25,7 +25,7 @@ EXAMPLES: ox test.txt test2.txt ox /home/user/docs/test.txt ox -c config.lua test.txt - ox -r -c ~/.config/.oxrc -f lua my_file.lua + ox -r -c ~/.config/.oxrc -f Lua my_file.lua tree | ox -r --stdin\ "; diff --git a/src/config/editor.rs b/src/config/editor.rs index db9a4938..b73c6d93 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -24,7 +24,7 @@ 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())); + fields.add_field_method_get("document_count", |_, editor| Ok(editor.files.len())); fields.add_field_method_get("document_type", |_, editor| { let ext = editor .doc() @@ -437,14 +437,15 @@ impl LuaUserData for Editor { editor.doc_mut().info.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; + methods.add_method_mut("set_file_type", |_, editor, name: String| { + if let Some(file_type) = editor.config.document.borrow().file_types.get_name(&name) { + let mut highlighter = file_type.get_highlighter(&editor.config, 4); + highlighter.run(&editor.doc().lines); + editor.files[editor.ptr].highlighter = highlighter; + editor.files[editor.ptr].file_type = Some(file_type); + } else { + editor.feedback = Feedback::Error(format!("Invalid file type: {name}")); + } Ok(()) }); // Rerendering diff --git a/src/config/highlighting.rs b/src/config/highlighting.rs index 361f98d5..2981a121 100644 --- a/src/config/highlighting.rs +++ b/src/config/highlighting.rs @@ -2,7 +2,7 @@ use crate::error::{OxError, Result}; use crossterm::style::Color as CColor; use mlua::prelude::*; use std::collections::HashMap; -use synoptic::{from_extension, Highlighter}; +use synoptic::Highlighter; use super::Color; @@ -27,14 +27,6 @@ impl SyntaxHighlighting { ))) } } - - /// Get a highlighter given a file extension - pub fn get_highlighter(&self, ext: &str) -> Highlighter { - self.user_rules.get(ext).map_or_else( - || from_extension(ext, 4).unwrap_or_else(|| Highlighter::new(4)), - std::clone::Clone::clone, - ) - } } impl LuaUserData for SyntaxHighlighting { @@ -84,40 +76,36 @@ impl LuaUserData for SyntaxHighlighting { ); methods.add_method_mut( "new", - |_, syntax_highlighting, (extensions, rules): (LuaTable, LuaTable)| { - // Make note of the highlighter - 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()?) { - // 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!(), + |_, syntax_highlighting, (name, rules): (String, LuaTable)| { + // Create highlighter + let mut highlighter = Highlighter::new(4); + // Add rules one by one + 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"]); } + "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); } + syntax_highlighting.user_rules.insert(name, highlighter); Ok(()) }, ); diff --git a/src/config/interface.rs b/src/config/interface.rs index 6d6cf1ab..5d642d8e 100644 --- a/src/config/interface.rs +++ b/src/config/interface.rs @@ -1,10 +1,9 @@ use crate::cli::VERSION; -use crate::editor::{which_extension, Editor}; +use crate::editor::{Editor, FileContainer}; use crate::error::Result; use crossterm::style::SetForegroundColor as Fg; use kaolinite::searching::Searcher; -use kaolinite::utils::{filetype, get_absolute_path, get_file_name, icon}; -use kaolinite::Document; +use kaolinite::utils::{get_absolute_path, get_file_ext, get_file_name}; use mlua::prelude::*; use super::{issue_warning, Colors}; @@ -218,16 +217,17 @@ impl Default for TabLine { impl TabLine { /// Take the configuration information and render the tab line - pub fn render(&self, document: &Document) -> String { - let path = document + pub fn render(&self, file: &FileContainer) -> String { + let path = file + .doc .file_name .clone() .unwrap_or_else(|| "[No Name]".to_string()); - let file_extension = which_extension(document).unwrap_or_else(|| "Unknown".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_default()); - let modified = if document.info.modified { "[+]" } else { "" }; + let icon = file.file_type.clone().map_or("󰈙 ".to_string(), |t| t.icon); + let modified = if file.doc.info.modified { "[+]" } else { "" }; let mut result = self.format.clone(); result = result .replace("{file_extension}", &file_extension) @@ -277,23 +277,21 @@ 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 { + let file = &editor.files[editor.ptr]; let mut result = vec![]; let path = editor .doc() .file_name .clone() .unwrap_or_else(|| "[No Name]".to_string()); - let file_extension = which_extension(editor.doc()).unwrap_or_else(|| "Unknown".to_string()); + let file_extension = get_file_ext(&path).unwrap_or_else(|| "Unknown".to_string()); let absolute_path = get_absolute_path(&path).unwrap_or_else(|| "[No Name]".to_string()); let file_name = get_file_name(&path).unwrap_or_else(|| "[No Name]".to_string()); - let file_type = 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_default()); + let file_type = file + .file_type + .clone() + .map_or("Unknown".to_string(), |t| t.name); + let icon = file.file_type.clone().map_or("󰈙 ".to_string(), |t| t.icon); let modified = if editor.doc().info.modified { "[+]" } else { diff --git a/src/config/mod.rs b/src/config/mod.rs index 33553fb8..de3c5580 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,3 +1,4 @@ +use crate::editor::{FileType, FileTypes}; use crate::error::{OxError, Result}; use mlua::prelude::*; use std::collections::HashMap; @@ -178,6 +179,15 @@ impl Config { } } + // Load in the file types + let file_types = lua + .globals() + .get("file_types") + .unwrap_or(LuaValue::Table(lua.create_table()?)); + let file_types = FileTypes::from_lua(file_types, lua).unwrap_or_default(); + self.document.borrow_mut().file_types = file_types; + + // Return result if user_provided_config { Ok(()) } else { @@ -248,6 +258,7 @@ pub struct Document { pub indentation: Indentation, pub undo_period: usize, pub wrap_cursor: bool, + pub file_types: FileTypes, } impl Default for Document { @@ -257,6 +268,7 @@ impl Default for Document { indentation: Indentation::Tabs, undo_period: 10, wrap_cursor: true, + file_types: FileTypes::default(), } } } @@ -287,3 +299,38 @@ impl LuaUserData for Document { }); } } + +impl FromLua<'_> for FileTypes { + fn from_lua(value: LuaValue<'_>, _: &Lua) -> std::result::Result { + let mut result = vec![]; + if let LuaValue::Table(table) = value { + for i in table.pairs::() { + let (name, info) = i?; + let icon = info.get::<_, String>("icon")?; + let extensions = info + .get::<_, LuaTable>("extensions")? + .pairs::() + .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) + .collect::>(); + let files = info + .get::<_, LuaTable>("files")? + .pairs::() + .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) + .collect::>(); + let modelines = info + .get::<_, LuaTable>("modelines")? + .pairs::() + .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) + .collect::>(); + result.push(FileType { + name, + icon, + files, + extensions, + modelines, + }); + } + } + Ok(Self { types: result }) + } +} diff --git a/src/editor/documents.rs b/src/editor/documents.rs new file mode 100644 index 00000000..6d17d8e8 --- /dev/null +++ b/src/editor/documents.rs @@ -0,0 +1,9 @@ +use crate::editor::FileType; +use kaolinite::Document; +use synoptic::Highlighter; + +pub struct FileContainer { + pub doc: Document, + pub highlighter: Highlighter, + pub file_type: Option, +} diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 82cab9f5..acbef333 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -28,7 +28,8 @@ impl Editor { } 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]); + let file = &mut self.files[self.ptr]; + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); } Ok(()) } @@ -43,10 +44,11 @@ impl Editor { // Enter pressed in the start, middle or end of the line let loc = self.doc().char_loc(); self.exe(Event::SplitDown(loc))?; - let 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); + let file = &mut self.files[self.ptr]; + let line = &file.doc.lines[loc.y + 1]; + file.highlighter.insert_line(loc.y + 1, line); + let line = &file.doc.lines[loc.y]; + file.highlighter.edit(loc.y, line); } Ok(()) } @@ -70,10 +72,12 @@ impl Editor { 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(); + let file = &mut self.files[self.ptr]; + loc.x = file.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); + let file = &mut self.files[self.ptr]; + let line = &file.doc.lines[loc.y]; + file.highlighter.edit(loc.y, line); } else if !(c == 0 && on_first_line) { // Backspace was pressed in the middle of the line, delete the character c = c.saturating_sub(1); @@ -84,7 +88,8 @@ impl Editor { 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]); + let file = &mut self.files[self.ptr]; + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); } } } @@ -101,7 +106,8 @@ impl Editor { 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]); + let file = &mut self.files[self.ptr]; + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); } } Ok(()) diff --git a/src/editor/filetypes.rs b/src/editor/filetypes.rs new file mode 100644 index 00000000..b621f4e5 --- /dev/null +++ b/src/editor/filetypes.rs @@ -0,0 +1,101 @@ +use crate::editor::Config; +use kaolinite::utils::get_file_name; +use kaolinite::Document; +use std::path::Path; +use synoptic::{from_extension, Highlighter, Regex}; + +/// A struct to store different file types and provide utilities for finding the correct one +#[derive(Default, Debug, Clone)] +pub struct FileTypes { + /// The file types available + pub types: Vec, +} + +impl FileTypes { + pub fn identify(&self, doc: &mut Document) -> Option { + for t in &self.types { + let mut extension = String::new(); + let mut file_name = String::new(); + if let Some(f) = &doc.file_name { + file_name = get_file_name(f).unwrap_or_default(); + if let Some(e) = Path::new(&f).extension() { + extension = e.to_str().unwrap_or_default().to_string(); + } + } + doc.load_to(1); + let first_line = doc.line(0).unwrap_or_default(); + if t.fits(&extension, &file_name, &first_line) { + return Some(t.clone()); + } + } + None + } + + pub fn get_name(&self, name: &str) -> Option { + self.types.iter().find(|t| t.name == name).cloned() + } +} + +/// An struct to represent the characteristics of a file type +#[derive(Debug, Clone)] +pub struct FileType { + /// The name of the file type + pub name: String, + /// The icon representing the file type + pub icon: String, + /// The file names that files of this type exhibit + pub files: Vec, + /// The extensions that files of this type have + pub extensions: Vec, + /// The modelines that files of this type have + pub modelines: Vec, +} + +impl Default for FileType { + fn default() -> Self { + FileType { + name: "Unknown".to_string(), + icon: "󰈙 ".to_string(), + files: vec![], + extensions: vec![], + modelines: vec![], + } + } +} + +impl FileType { + /// Determine whether a file fits with this file type + pub fn fits(&self, extension: &String, file_name: &String, first_line: &str) -> bool { + let mut modelines = false; + for modeline in &self.modelines { + if let Ok(re) = Regex::new(&format!("^{modeline}\\s*$")) { + if re.is_match(first_line) { + modelines = true; + break; + } + } + } + self.extensions.contains(extension) || self.files.contains(file_name) || modelines + } + + /// Identify the correct highlighter to use + pub fn get_highlighter(&self, config: &Config, tab_width: usize) -> Highlighter { + if let Some(highlighter) = config + .syntax_highlighting + .borrow() + .user_rules + .get(&self.name) + { + // The user has defined their own syntax highlighter for this file type + highlighter.clone() + } else { + // The user hasn't defined their own syntax highlighter, use synoptic builtins + for ext in &self.extensions { + if let Some(h) = from_extension(ext, tab_width) { + return h; + } + } + Highlighter::new(tab_width) + } + } +} diff --git a/src/editor/interface.rs b/src/editor/interface.rs index f3a5a1ca..fe475790 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,5 +1,5 @@ use crate::display; -use crate::error::Result; +use crate::error::{OxError, Result}; use crate::ui::{size, Feedback}; use crossterm::{ event::{read, Event as CEvent, KeyCode as KCode, KeyModifiers as KMod}, @@ -90,38 +90,40 @@ impl Editor { let tokens = trim(&tokens, self.doc().offset.x); let mut x_pos = self.doc().offset.x; for token in tokens { - let text = match token { + // Find out the text (and colour of that text) + let (text, colour) = match token { + // Non-highlighted text 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 { + let colour = match colour { // Success, write token - Ok(col) => { - display!(self, Fg(col)); - } + Ok(col) => Fg(col), // Failure, show error message and don't highlight this token Err(err) => { self.feedback = Feedback::Error(err.to_string()); + editor_fg } - } - text + }; + (text, colour) } - TokOpt::None(text) => text, + // Highlighted text + TokOpt::None(text) => (text, editor_fg), }; + // Do the rendering 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 { display!(self, selection_bg, selection_fg); } else { - display!(self, editor_bg); + display!(self, editor_bg, colour); } display!(self, c); x_pos += 1; } - display!(self, editor_fg); } // Pad out the line (to remove any junk left over from previous render) + display!(self, editor_fg, editor_bg); let tab_width = self.config.document.borrow().tab_width; let line_width = width(&line, tab_width); let pad_amount = w.saturating_sub(self.dent()).saturating_sub(line_width) + 1; @@ -144,8 +146,8 @@ impl Editor { 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()?); display!(self, tab_inactive_fg, tab_inactive_bg); - for (c, document) in self.doc.iter().enumerate() { - let document_header = self.config.tab_line.borrow().render(document); + for (c, file) in self.files.iter().enumerate() { + let document_header = self.config.tab_line.borrow().render(file); if c == self.ptr { // Representing the document we're currently looking at display!( @@ -250,6 +252,8 @@ impl Editor { 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) => return Err(OxError::Cancelled), // Remove from the input string if the user presses backspace (KMod::NONE, KCode::Backspace) => { input.pop(); @@ -267,19 +271,31 @@ impl Editor { /// Prompt for selecting a file pub fn path_prompt(&mut self) -> Result { let mut input = get_cwd().map(|s| s + "/").unwrap_or_default(); + let mut offset = 0; let mut done = false; + let mut old_suggestions = vec![]; // Enter into a menu that asks for a prompt while !done { - // Find the suggested file / folder + // Find the suggested files and folders let parent = if input.ends_with('/') { input.to_string() } else { get_parent(&input).unwrap_or_default() }; - let mut suggestion = list_dir(&parent) + let suggestions = list_dir(&parent) .unwrap_or_default() .iter() - .find(|p| p.starts_with(&input)) + .filter(|p| p.starts_with(&input)) + .cloned() + .collect::>(); + // Reset offset if we've changed suggestions / out of bounds + if suggestions != old_suggestions || offset >= suggestions.len() { + offset = 0; + } + old_suggestions.clone_from(&suggestions); + // Select suggestion + let mut suggestion = suggestions + .get(offset) .map(std::string::ToString::to_string) .unwrap_or(input.clone()); // Render prompt message @@ -307,6 +323,8 @@ impl Editor { match (key.modifiers, key.code) { // Exit the menu when the enter key is pressed (KMod::NONE, KCode::Enter) => done = true, + // Cancel when escape key is pressed + (KMod::NONE, KCode::Esc) => return Err(OxError::Cancelled), // Remove from the input string if the user presses backspace (KMod::NONE, KCode::Backspace) => { input.pop(); @@ -314,11 +332,19 @@ impl Editor { // Add to the input string if the user presses a character (KMod::NONE | KMod::SHIFT, KCode::Char(c)) => input.push(c), // Autocomplete path - (KMod::NONE, KCode::Right | KCode::Tab) => { + (KMod::NONE, KCode::Right) => { if file_or_dir(&suggestion) == "directory" { suggestion += "/"; } input = suggestion; + offset = 0; + } + // Cycle through suggestions + (KMod::SHIFT, KCode::BackTab) => offset = offset.saturating_sub(1), + (KMod::NONE, KCode::Tab) => { + if offset + 1 < suggestions.len() { + offset += 1; + } } _ => (), } @@ -366,13 +392,14 @@ impl Editor { /// Append any missed lines to the syntax highlighter pub fn update_highlighter(&mut self) { if self.active { - let actual = self.doc.get(self.ptr).map_or(0, |d| d.info.loaded_to); + let actual = self.files.get(self.ptr).map_or(0, |d| d.doc.info.loaded_to); let percieved = self.highlighter().line_ref.len(); if percieved < actual { let diff = actual.saturating_sub(percieved); for i in 0..diff { - let line = &self.doc[self.ptr].lines[percieved + i]; - self.highlighter[self.ptr].append(line); + let file = &mut self.files[self.ptr]; + let line = &file.doc.lines[percieved + i]; + file.highlighter.append(line); } } } @@ -380,17 +407,18 @@ impl Editor { /// Returns a highlighter at a certain index pub fn get_highlighter(&mut self, idx: usize) -> &mut Highlighter { - self.highlighter.get_mut(idx).unwrap() + &mut self.files.get_mut(idx).unwrap().highlighter } /// Gets a mutable reference to the current document pub fn highlighter(&mut self) -> &mut Highlighter { - self.highlighter.get_mut(self.ptr).unwrap() + &mut self.files.get_mut(self.ptr).unwrap().highlighter } /// Reload the whole document in the highlighter pub fn reload_highlight(&mut self) { - self.highlighter[self.ptr].run(&self.doc[self.ptr].lines); + let file = &mut self.files[self.ptr]; + file.highlighter.run(&file.doc.lines); } /// Work out how much to push the document to the right (to make way for line numbers) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 7ff47411..28e52e99 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,21 +1,27 @@ 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}; +use crossterm::event::{ + Event as CEvent, KeyCode as KCode, KeyModifiers as KMod, MouseEvent, MouseEventKind, +}; use kaolinite::event::Error as KError; use kaolinite::Document; use mlua::{Error as LuaError, Lua}; use std::io::ErrorKind; -use std::path::Path; use std::time::Instant; use synoptic::Highlighter; mod cursor; +mod documents; mod editing; +mod filetypes; mod interface; mod mouse; mod scanning; +pub use documents::FileContainer; +pub use filetypes::{FileType, FileTypes}; + /// For managing all editing and rendering of cactus #[allow(clippy::struct_excessive_bools)] pub struct Editor { @@ -26,9 +32,7 @@ pub struct Editor { /// 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, + pub files: Vec, /// Pointer to the document that is currently being edited pub ptr: usize, /// true if the editor is still running, false otherwise @@ -47,6 +51,8 @@ pub struct Editor { pub config_path: String, /// Flag to determine whether or not the editor is under control by a plug-in pub plugin_active: bool, + /// Stores the last click the user made (in order to detect double-click) + pub last_click: Option<(Instant, MouseEvent)>, } impl Editor { @@ -54,20 +60,20 @@ impl Editor { pub fn new(lua: &Lua) -> Result { let config = Config::new(lua)?; Ok(Self { - doc: vec![], + files: 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(), plugin_active: false, + last_click: None, }) } @@ -88,23 +94,26 @@ impl Editor { // 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); + self.files.push(FileContainer { + highlighter, + file_type: Some(FileType::default()), + 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); + self.ptr = self.files.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() { + if self.files.is_empty() { self.blank()?; self.greet = self.config.greeting_message.borrow().enabled; } @@ -116,21 +125,24 @@ impl Editor { let mut size = size()?; size.h = size.h.saturating_sub(1 + self.push_down); 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 + // Collect various data from the document + let tab_width = self.config.document.borrow().tab_width; + let file_type = self.config.document.borrow().file_types.identify(&mut doc); + // Set up the document + doc.set_tab_width(tab_width); doc.load_to(size.h); + doc.undo_mgmt.saved(); // Update in the syntax highlighter - let ext = which_extension(&doc); - let mut highlighter = self - .config - .syntax_highlighting - .borrow() - .get_highlighter(&ext.unwrap_or_default()); + let mut highlighter = file_type.as_ref().map_or(Highlighter::new(tab_width), |t| { + t.get_highlighter(&self.config, tab_width) + }); highlighter.run(&doc.lines); - self.highlighter.push(highlighter); - doc.undo_mgmt.saved(); - // Add document to documents - self.doc.push(doc); + // Add in the file + self.files.push(FileContainer { + doc, + highlighter, + file_type, + }); Ok(()) } @@ -138,7 +150,7 @@ impl Editor { pub fn open_document(&mut self) -> Result<()> { let path = self.path_prompt()?; self.open(&path)?; - self.ptr = self.doc.len().saturating_sub(1); + self.ptr = self.files.len().saturating_sub(1); Ok(()) } @@ -147,21 +159,28 @@ impl Editor { let file = self.open(&file_name); if let Err(OxError::Kaolinite(KError::Io(ref os))) = file { if os.kind() == ErrorKind::NotFound { + // Create a new document if not found 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().info.modified = true; - let highlighter = self + let file = self.files.last_mut().unwrap(); + file.doc.file_name = Some(file_name); + // Work out information for the document + let tab_width = self.config.document.borrow().tab_width; + let file_type = self .config - .syntax_highlighting + .document .borrow() - .get_highlighter(ext); - *self.highlighter.last_mut().unwrap() = highlighter; - self.highlighter - .last_mut() - .unwrap() - .run(&self.doc.last_mut().unwrap().lines); + .file_types + .identify(&mut file.doc); + // Set up the document + file.doc.info.modified = true; + file.doc.set_tab_width(tab_width); + // Attach the correct highlighter + let highlighter = file_type.clone().map_or(Highlighter::new(tab_width), |t| { + t.get_highlighter(&self.config, tab_width) + }); + file.highlighter = highlighter; + file.highlighter.run(&file.doc.lines); + file.file_type = file_type; Ok(()) } else { file @@ -187,14 +206,23 @@ impl Editor { let file_name = self.prompt("Save as")?; self.doc_mut().save_as(&file_name)?; if self.doc().file_name.is_none() { - let ext = which_extension(self.doc()); - self.highlighter[self.ptr] = self + // Get information about the document + let file = self.files.last_mut().unwrap(); + let tab_width = self.config.document.borrow().tab_width; + let file_type = self .config - .syntax_highlighting + .document .borrow() - .get_highlighter(&ext.unwrap_or_default()); - self.doc_mut().file_name = Some(file_name.clone()); - self.doc_mut().info.modified = false; + .file_types + .identify(&mut file.doc); + // Reattach an appropriate highlighter + let highlighter = file_type.map_or(Highlighter::new(tab_width), |t| { + t.get_highlighter(&self.config, tab_width) + }); + file.highlighter = highlighter; + file.highlighter.run(&file.doc.lines); + file.doc.file_name = Some(file_name.clone()); + file.doc.info.modified = false; } // Commit events to event manager (for undo / redo) self.doc_mut().commit(); @@ -205,10 +233,10 @@ impl Editor { /// Save all the open documents to the disk pub fn save_all(&mut self) -> Result<()> { - for doc in &mut self.doc { - doc.save()?; + for file in &mut self.files { + file.doc.save()?; // Commit events to event manager (for undo / redo) - doc.commit(); + file.doc.commit(); } self.feedback = Feedback::Info("Saved all documents".to_string()); Ok(()) @@ -216,23 +244,22 @@ impl Editor { /// Quit the editor pub fn quit(&mut self) -> Result<()> { - self.active = !self.doc.is_empty(); + self.active = !self.files.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().info.modified || self.confirm(msg)? { - self.doc.remove(self.ptr); - self.highlighter.remove(self.ptr); + self.files.remove(self.ptr); self.prev(); } } - self.active = !self.doc.is_empty(); + self.active = !self.files.is_empty(); Ok(()) } /// Move to the next document opened in the editor pub fn next(&mut self) { - if self.ptr + 1 < self.doc.len() { + if self.ptr + 1 < self.files.len() { self.ptr += 1; } } @@ -246,22 +273,22 @@ impl Editor { /// Returns a document at a certain index pub fn get_doc(&mut self, idx: usize) -> &mut Document { - self.doc.get_mut(idx).unwrap() + &mut self.files.get_mut(idx).unwrap().doc } /// Gets a reference to the current document pub fn doc(&self) -> &Document { - self.doc.get(self.ptr).unwrap() + &self.files.get(self.ptr).unwrap().doc } /// Gets a mutable reference to the current document pub fn doc_mut(&mut self) -> &mut Document { - self.doc.get_mut(self.ptr).unwrap() + &mut self.files.get_mut(self.ptr).unwrap().doc } /// Gets the number of documents currently open pub fn doc_len(&mut self) -> usize { - self.doc.len() + self.files.len() } /// Load the configuration values @@ -357,17 +384,3 @@ impl Editor { Ok(()) } } - -/// Takes a document, and tries to help determine the file type -pub fn which_extension(doc: &Document) -> Option { - let file_name = doc.file_name.clone().unwrap_or_default(); - let is_config = Path::new(&file_name) - .extension() - .map_or(false, |ext| ext.eq_ignore_ascii_case("oxrc")); - match (is_config, doc.get_file_type()) { - (true, _) => Some("lua"), - (false, Some(ext)) => Some(ext), - (_, None) => None, - } - .map(std::string::ToString::to_string) -} diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index c4934630..ce0417b4 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -1,5 +1,6 @@ use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; use kaolinite::Loc; +use std::time::{Duration, Instant}; use super::Editor; @@ -20,8 +21,8 @@ impl Editor { 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; + for (i, file) in self.files.iter().enumerate() { + let header_len = self.config.tab_line.borrow().render(file).len() + 1; c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); if c == 0 { return MouseLocation::Tabs(i); @@ -42,17 +43,44 @@ impl Editor { /// Handles a mouse event (dragging / clicking) pub fn handle_mouse_event(&mut self, event: MouseEvent) { match event.kind { - MouseEventKind::Down(MouseButton::Left) => match self.find_mouse_location(event) { - MouseLocation::File(mut loc) => { - loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().move_to(&loc); - self.doc_mut().old_cursor = self.doc().loc().x; + // Single click + MouseEventKind::Down(MouseButton::Left) => { + // Determine if there has been a click within 500ms + if let Some((time, last_event)) = self.last_click { + let now = Instant::now(); + let short_period = now.duration_since(time) <= Duration::from_millis(500); + let same_location = + last_event.column == event.column && last_event.row == event.row; + if short_period && same_location { + self.handle_double_click(event); + return; + } } - MouseLocation::Tabs(i) => { - self.ptr = i; + match self.find_mouse_location(event) { + MouseLocation::File(mut loc) => { + loc.x = self.doc_mut().character_idx(&loc); + self.doc_mut().move_to(&loc); + self.doc_mut().old_cursor = self.doc().loc().x; + } + MouseLocation::Tabs(i) => { + self.ptr = i; + } + MouseLocation::Out => (), } - MouseLocation::Out => (), - }, + } + MouseEventKind::Down(MouseButton::Right) => { + // Select the current line + if let MouseLocation::File(loc) = self.find_mouse_location(event) { + self.doc_mut().select_line_at(loc.y); + } + } + // Double click detection + MouseEventKind::Up(MouseButton::Left) => { + let now = Instant::now(); + // Register this click as having happened + self.last_click = Some((now, event)); + } + // Mouse drag MouseEventKind::Drag(MouseButton::Left) => match self.find_mouse_location(event) { MouseLocation::File(mut loc) => { loc.x = self.doc_mut().character_idx(&loc); @@ -60,6 +88,14 @@ impl Editor { } MouseLocation::Tabs(_) | MouseLocation::Out => (), }, + MouseEventKind::Drag(MouseButton::Right) => match self.find_mouse_location(event) { + MouseLocation::File(mut loc) => { + loc.x = self.doc_mut().character_idx(&loc); + self.doc_mut().select_to_y(loc.y); + } + MouseLocation::Tabs(_) | MouseLocation::Out => (), + }, + // Mouse scroll behaviour MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { if let MouseLocation::File(_) = self.find_mouse_location(event) { if event.kind == MouseEventKind::ScrollDown { @@ -78,4 +114,12 @@ impl Editor { _ => (), } } + + /// Handle a double-click event + pub fn handle_double_click(&mut self, event: MouseEvent) { + // Select the current word + if let MouseLocation::File(loc) = self.find_mouse_location(event) { + self.doc_mut().select_word_at(&loc); + } + } } diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index c30f3aa4..cd9effe8 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -146,7 +146,8 @@ impl Editor { 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]); + let file = &mut self.files[self.ptr]; + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); Ok(()) } @@ -159,7 +160,9 @@ impl Editor { while let Some(mtch) = self.doc_mut().next_match(target, 1) { drop(self.doc_mut().replace(mtch.loc, &mtch.text, into)); self.update_highlighter(); - self.highlighter[self.ptr].edit(mtch.loc.y, &self.doc[self.ptr].lines[mtch.loc.y]); + let file = &mut self.files[self.ptr]; + file.highlighter + .edit(mtch.loc.y, &file.doc.lines[mtch.loc.y]); } } } diff --git a/src/error.rs b/src/error.rs index 097487c0..d30a0d64 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,9 @@ quick_error! { from() display("Error in lua: {}", err) } + Cancelled { + display("Operation Cancelled") + } None } } diff --git a/src/main.rs b/src/main.rs index 391e789b..f440f0af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ fn main() { } /// Run the editor +#[allow(clippy::too_many_lines)] fn run(cli: &CommandLineInterface) -> Result<()> { // Create lua interpreter let lua = Lua::new(); @@ -73,15 +74,22 @@ fn run(cli: &CommandLineInterface) -> Result<()> { editor.borrow_mut().get_doc(c).info.read_only = true; } // Set highlighter if applicable - if let Some(ref ext) = cli.file_type { - let mut highlighter = editor - .borrow() + if let Some(ref file_type) = cli.file_type { + let tab_width = editor.borrow().config.document.borrow().tab_width; + let file_type = editor + .borrow_mut() .config - .syntax_highlighting + .document .borrow() - .get_highlighter(ext); + .file_types + .get_name(file_type) + .unwrap_or_default(); + let mut highlighter = file_type.get_highlighter(&editor.borrow().config, tab_width); highlighter.run(&editor.borrow_mut().get_doc(c).lines); - *editor.borrow_mut().get_highlighter(c) = highlighter; + let mut editor = editor.borrow_mut(); + let file = editor.files.get_mut(c).unwrap(); + file.highlighter = highlighter; + file.file_type = Some(file_type); } } @@ -172,7 +180,6 @@ fn run(cli: &CommandLineInterface) -> Result<()> { editor.borrow_mut().command = None; } - // Exit editor.borrow_mut().terminal.end()?; Ok(()) } diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 3b88dbc1..e0eedaeb 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -1,3 +1,4 @@ +-- Bootstrap plug-ins home = os.getenv("HOME") or os.getenv("USERPROFILE") function file_exists(file_path) @@ -25,6 +26,7 @@ function load_plugin(base) elseif file_exists(path_win) then path = file_win else + path = nil -- Prevent warning if plug-in is built-in local is_autoindent = base:match("autoindent.lua$") ~= nil local is_pairs = base:match("pairs.lua$") ~= nil @@ -36,5 +38,668 @@ function load_plugin(base) table.insert(builtins, base) end end - plugins[#plugins + 1] = path + if path ~= nil then + plugins[#plugins + 1] = path + end end + +-- Populate the document object with built-in file type detection +file_types = { + ["ABAP"] = { + icon = "󰅩 ", + files = {}, + extensions = {"abap"}, + modelines = {}, + }, + ["Ada"] = { + icon = "", + files = {}, + extensions = {"ada"}, + modelines = {}, + }, + ["AutoHotkey"] = { + icon = " ", + files = {}, + extensions = {"ahk", "ahkl"}, + modelines = {}, + }, + ["AppleScript"] = { + icon = "", + files = {}, + extensions = {"applescript", "scpt"}, + modelines = {}, + }, + ["Arc"] = { + icon = "󰅩 ", + files = {}, + extensions = {"arc"}, + modelines = {}, + }, + ["ASP"] = { + icon = "󰅩 ", + files = {}, + extensions = {"asp", "asax", "ascx", "ashx", "asmx", "aspx", "axd"}, + modelines = {}, + }, + ["ActionScript"] = { + icon = "󰑷 ", + files = {}, + extensions = {"as"}, + modelines = {}, + }, + ["AGS Script"] = { + icon = "󰅩 ", + files = {}, + extensions = {"asc", "ash"}, + modelines = {}, + }, + ["Assembly"] = { + icon = " ", + files = {}, + extensions = {"asm", "nasm"}, + modelines = {}, + }, + ["Awk"] = { + icon = "󰅩 ", + files = {}, + extensions = {"awk", "auk", "gawk", "mawk", "nawk"}, + modelines = {"#!\\s*/usr/bin/(env )?awk"}, + }, + ["Batch"] = { + icon = "󰆍 ", + files = {}, + extensions = {"bat", "cmd"}, + modelines = {}, + }, + ["Brainfuck"] = { + icon = " ", + files = {}, + extensions = {"b", "bf"}, + modelines = {}, + }, + ["C"] = { + icon = " ", + files = {}, + extensions = {"c"}, + modelines = {}, + }, + ["CMake"] = { + icon = " ", + files = {}, + extensions = {"cmake"}, + modelines = {}, + }, + ["Cobol"] = { + icon = "󰅩 ", + files = {}, + extensions = {"cbl", "cobol", "cob"}, + modelines = {}, + }, + ["Java"] = { + icon = " ", + files = {}, + extensions = {"class", "java"}, + modelines = {}, + }, + ["Clojure"] = { + icon = " ", + files = {}, + extensions = {"clj", "cl2", "cljs", "cljx", "cljc"}, + modelines = {}, + }, + ["CoffeeScript"] = { + icon = " ", + files = {}, + extensions = {"coffee"}, + modelines = {}, + }, + ["Crystal"] = { + icon = " ", + files = {}, + extensions = {"cr"}, + modelines = {}, + }, + ["Cuda"] = { + icon = " ", + files = {}, + extensions = {"cu", "cuh"}, + modelines = {}, + }, + ["C++"] = { + icon = " ", + files = {}, + extensions = {"cpp", "cxx"}, + modelines = {}, + }, + ["C#"] = { + icon = " ", + files = {}, + extensions = {"cs", "cshtml", "csx"}, + modelines = {}, + }, + ["CSS"] = { + icon = " ", + files = {}, + extensions = {"css"}, + modelines = {}, + }, + ["CSV"] = { + icon = " ", + files = {}, + extensions = {"csv"}, + modelines = {}, + }, + ["D"] = { + icon = " ", + files = {}, + extensions = {"d", "di"}, + modelines = {}, + }, + ["Dart"] = { + icon = " ", + files = {}, + extensions = {"dart"}, + modelines = {}, + }, + ["Diff"] = { + icon = " ", + files = {}, + extensions = {"diff", "patch"}, + modelines = {}, + }, + ["Dockerfile"] = { + icon = " ", + files = {}, + extensions = {"dockerfile"}, + modelines = {}, + }, + ["Elixr"] = { + icon = " ", + files = {}, + extensions = {"ex", "exs"}, + modelines = {}, + }, + ["Elm"] = { + icon = " ", + files = {}, + extensions = {"elm"}, + modelines = {}, + }, + ["Emacs Lisp"] = { + icon = " ", + files = {}, + extensions = {"el"}, + modelines = {}, + }, + ["ERB"] = { + icon = "󰅩 ", + files = {}, + extensions = {"erb"}, + modelines = {}, + }, + ["Erlang"] = { + icon = " ", + files = {}, + extensions = {"erl", "es"}, + modelines = {}, + }, + ["F#"] = { + icon = " ", + files = {}, + extensions = {"fs", "fsi", "fsx"}, + modelines = {}, + }, + ["FORTRAN"] = { + icon = "󱈚 ", + files = {}, + extensions = {"f", "f90", "fpp", "for"}, + modelines = {}, + }, + ["Fish"] = { + icon = " ", + files = {}, + extensions = {"fish"}, + modelines = {"#!\\s*/usr/bin/(env )?fish"}, + }, + ["Forth"] = { + icon = "󰅩 ", + files = {}, + extensions = {"fth"}, + modelines = {}, + }, + ["ANTLR"] = { + icon = "󰅩 ", + files = {}, + extensions = {"g4"}, + modelines = {}, + }, + ["GDScript"] = { + icon = " ", + files = {}, + extensions = {"gd"}, + modelines = {}, + }, + ["GLSL"] = { + icon = " ", + files = {}, + extensions = {"glsl", "vert", "shader", "geo", "fshader", "vrx", "vsh", "vshader", "frag"}, + modelines = {}, + }, + ["Gnuplot"] = { + icon = " ", + files = {}, + extensions = {"gnu", "gp", "plot"}, + modelines = {}, + }, + ["Go"] = { + icon = "", + files = {}, + extensions = {"go"}, + modelines = {}, + }, + ["Groovy"] = { + icon = " ", + files = {}, + extensions = {"groovy", "gvy"}, + modelines = {}, + }, + ["HLSL"] = { + icon = "󰅩 ", + files = {}, + extensions = {"hlsl"}, + modelines = {}, + }, + ["C Header"] = { + icon = " ", + files = {}, + extensions = {"h"}, + modelines = {}, + }, + ["Haml"] = { + icon = "", + files = {}, + extensions = {"haml"}, + modelines = {}, + }, + ["Handlebars"] = { + icon = "󰅩 ", + files = {}, + extensions = {"handlebars", "hbs"}, + modelines = {}, + }, + ["Haskell"] = { + icon = " ", + files = {}, + extensions = {"hs"}, + modelines = {}, + }, + ["C++ Header"] = { + icon = " ", + files = {}, + extensions = {"hpp"}, + modelines = {}, + }, + ["HTML"] = { + icon = " ", + files = {}, + extensions = {"html", "htm", "xhtml"}, + modelines = {}, + }, + ["INI"] = { + icon = " ", + files = {}, + extensions = {"ini", "cfg"}, + modelines = {}, + }, + ["Arduino"] = { + icon = " ", + files = {}, + extensions = {"ino"}, + modelines = {}, + }, + ["J"] = { + icon = " ", + files = {}, + extensions = {"ijs"}, + modelines = {}, + }, + ["JSON"] = { + icon = " ", + files = {}, + extensions = {"json"}, + modelines = {}, + }, + ["JSX"] = { + icon = " ", + files = {}, + extensions = {"jsx"}, + modelines = {}, + }, + ["JavaScript"] = { + icon = " ", + files = {}, + extensions = {"js"}, + modelines = {"#!\\s*/usr/bin/(env )?node"}, + }, + ["Julia"] = { + icon = " ", + files = {}, + extensions = {"jl"}, + modelines = {}, + }, + ["Kotlin"] = { + icon = " ", + files = {}, + extensions = {"kt", "ktm", "kts"}, + modelines = {}, + }, + ["LLVM"] = { + icon = "󰅩 ", + files = {}, + extensions = {"ll"}, + modelines = {}, + }, + ["Lex"] = { + icon = "󰅩 ", + files = {}, + extensions = {"l", "lex"}, + modelines = {}, + }, + ["Lua"] = { + icon = " ", + files = {".oxrc"}, + extensions = {"lua"}, + modelines = {"#!\\s*/usr/bin/(env )?lua"}, + }, + ["LiveScript"] = { + icon = " ", + files = {}, + extensions = {"ls"}, + modelines = {}, + }, + ["LOLCODE"] = { + icon = "󰅩 ", + files = {}, + extensions = {"lol"}, + modelines = {}, + }, + ["Common Lisp"] = { + icon = " ", + files = {}, + extensions = {"lisp", "asd", "lsp"}, + modelines = {}, + }, + ["Log file"] = { + icon = " ", + files = {}, + extensions = {"log"}, + modelines = {}, + }, + ["M4"] = { + icon = "󰅩 ", + files = {}, + extensions = {"m4"}, + modelines = {}, + }, + ["Groff"] = { + icon = "󰅩 ", + files = {}, + extensions = {"man", "roff"}, + modelines = {}, + }, + ["Matlab"] = { + icon = " ", + files = {}, + extensions = {"matlab"}, + modelines = {}, + }, + ["Objective-C"] = { + icon = " ", + files = {}, + extensions = {"m"}, + modelines = {}, + }, + ["OCaml"] = { + icon = " ", + files = {}, + extensions = {"ml"}, + modelines = {}, + }, + ["Makefile"] = { + icon = " ", + files = {}, + extensions = {"mk", "mak"}, + modelines = {}, + }, + ["Markdown"] = { + icon = " ", + files = {}, + extensions = {"md", "markdown"}, + modelines = {}, + }, + ["Nix"] = { + icon = " ", + files = {}, + extensions = {"nix"}, + modelines = {}, + }, + ["NumPy"] = { + icon = "󰘨 ", + files = {}, + extensions = {"numpy"}, + modelines = {}, + }, + ["OpenCL"] = { + icon = "󰅩 ", + files = {}, + extensions = {"opencl", "cl"}, + modelines = {}, + }, + ["PHP"] = { + icon = "󰌟 ", + files = {}, + extensions = {"php"}, + modelines = {"#!\\s*/usr/bin/(env )?php"}, + }, + ["Pascal"] = { + icon = "󰅩 ", + files = {}, + extensions = {"pas"}, + modelines = {}, + }, + ["Perl"] = { + icon = " ", + files = {}, + extensions = {"pl"}, + modelines = {"#!\\s*/usr/bin/(env )?perl"}, + }, + ["PowerShell"] = { + icon = "󰨊 ", + files = {}, + extensions = {"psl"}, + modelines = {}, + }, + ["Prolog"] = { + icon = " ", + files = {}, + extensions = {"pro"}, + modelines = {}, + }, + ["Python"] = { + icon = " ", + files = {}, + extensions = {"py", "pyw"}, + modelines = {"#!\\s*/usr/bin/(env )?python3?"}, + }, + ["Cython"] = { + icon = " ", + files = {}, + extensions = {"pyx", "pxd", "pxi"}, + modelines = {}, + }, + ["R"] = { + icon = " ", + files = {}, + extensions = {"r"}, + modelines = {}, + }, + ["reStructuredText"] = { + icon = "󰊄", + files = {}, + extensions = {"rst"}, + modelines = {}, + }, + ["Racket"] = { + icon = "󰅩 ", + files = {}, + extensions = {"rkt"}, + modelines = {}, + }, + ["Ruby"] = { + icon = " ", + files = {}, + extensions = {"rb", "ruby"}, + modelines = {"#!\\s*/usr/bin/(env )?ruby"}, + }, + ["Rust"] = { + icon = " ", + files = {}, + extensions = {"rs"}, + modelines = {"#!\\s*/usr/bin/(env )?rust"}, + }, + ["Shell"] = { + icon = " ", + files = {}, + extensions = {"sh"}, + modelines = { + "#!\\s*/bin/(sh|bash)", + "#!\\s*/usr/bin/env bash", + }, + }, + ["SCSS"] = { + icon = " ", + files = {}, + extensions = {"scss"}, + modelines = {}, + }, + ["SQL"] = { + icon = " ", + files = {}, + extensions = {"sql"}, + modelines = {}, + }, + ["Sass"] = { + icon = " ", + files = {}, + extensions = {"sass"}, + modelines = {}, + }, + ["Scala"] = { + icon = "", + files = {}, + extensions = {"scala"}, + modelines = {}, + }, + ["Scheme"] = { + icon = "", + files = {}, + extensions = {"scm"}, + modelines = {}, + }, + ["Smalltalk"] = { + icon = "󰅩 ", + files = {}, + extensions = {"st"}, + modelines = {}, + }, + ["Swift"] = { + icon = " ", + files = {}, + extensions = {"swift"}, + modelines = {}, + }, + ["TOML"] = { + icon = " ", + files = {}, + extensions = {"toml"}, + modelines = {}, + }, + ["Tcl"] = { + icon = "󰅩 ", + files = {}, + extensions = {"tcl"}, + modelines = {"#!\\s*/usr/bin/(env )?tcl"}, + }, + ["TeX"] = { + icon = " ", + files = {}, + extensions = {"tex"}, + modelines = {}, + }, + ["TypeScript"] = { + icon = " ", + files = {}, + extensions = {"ts", "tsx"}, + modelines = {}, + }, + ["Plain Text"] = { + icon = " ", + files = {}, + extensions = {"txt"}, + modelines = {}, + }, + ["Vala"] = { + icon = " ", + files = {}, + extensions = {"vala"}, + modelines = {}, + }, + ["Visual Basic"] = { + icon = "󰯁 ", + files = {}, + extensions = {"vb", "vbs"}, + modelines = {}, + }, + ["Vue"] = { + icon = " ", + files = {}, + extensions = {"vue"}, + modelines = {}, + }, + ["Logos"] = { + icon = "󰅩 ", + files = {}, + extensions = {"xm", "x", "xi"}, + modelines = {}, + }, + ["XML"] = { + icon = "󰅩 ", + files = {}, + extensions = {"xml"}, + modelines = {}, + }, + ["Yacc"] = { + icon = "󰅩 ", + files = {}, + extensions = {"y", "yacc"}, + modelines = {}, + }, + ["Yaml"] = { + icon = "󰅩 ", + files = {}, + extensions = {"yaml", "yml"}, + modelines = {}, + }, + ["Bison"] = { + icon = "󰅩 ", + files = {}, + extensions = {"yxx"}, + modelines = {}, + }, + ["Zsh"] = { + icon = " ", + files = {}, + extensions = {"zsh"}, + modelines = {}, + }, +} diff --git a/src/plugin/plugin_manager.lua b/src/plugin/plugin_manager.lua index 4b4c0031..6f142126 100644 --- a/src/plugin/plugin_manager.lua +++ b/src/plugin/plugin_manager.lua @@ -64,11 +64,12 @@ function plugin_manager:uninstall(plugin) -- Check if downloaded / in config local downloaded = self:plugin_downloaded(plugin) local in_config = self:plugin_in_config(plugin) + local is_builtin = self:plugin_is_builtin(plugin) if not downloaded and not in_config then - editor:display_error("Plugin does not exist") + editor:display_error("Plugin is not installed") return end - if downloaded then + if downloaded and not is_builtin then local result = plugin_manager:remove_plugin(plugin) if result ~= nil then editor:display_error(result) @@ -91,6 +92,10 @@ end function plugin_manager:status() local count = 0 local list = "" + for _, v in ipairs(builtins) do + count = count + 1 + list = list .. v:match("(.+).lua$") .. " " + end for _, v in ipairs(plugins) do count = count + 1 list = list .. v:match("^.+[\\/](.+).lua$") .. " " @@ -98,6 +103,14 @@ function plugin_manager:status() editor:display_info(tostring(count) .. " plug-ins installed: " .. list) end +-- Verify whether or not a plug-in is built-in +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 +end + -- Verify whether or not a plug-in is downloaded function plugin_manager:plugin_downloaded(plugin) local base = plugin .. ".lua" @@ -106,9 +119,8 @@ function plugin_manager:plugin_downloaded(plugin) 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 + local builtin = self:plugin_is_builtin(plugin) + return installed or builtin end -- Download a plug-in from the ox repository