diff --git a/Cargo.lock b/Cargo.lock index 34c53396..957611b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,9 +56,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "shlex", ] @@ -169,12 +169,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "home" version = "0.5.9" @@ -216,9 +210,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.166" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libredox" @@ -279,11 +273,10 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "log", "wasi", @@ -292,9 +285,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae9546e4a268c309804e8bbb7526e31cbfdedca7cd60ac1b987d0b212e0d876" +checksum = "9ea43c3ffac2d0798bd7128815212dd78c98316b299b7a902dabef13dc7b6b8d" dependencies = [ "bstr", "either", @@ -306,9 +299,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa6bf1a64f06848749b7e7727417f4ec2121599e2a10ef0a8a3888b0e9a5a0d" +checksum = "63a11d485edf0f3f04a508615d36c7d50d299cf61a7ee6d3e2530651e0a31771" dependencies = [ "cc", "cfg-if", @@ -340,7 +333,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ox" -version = "0.7.3" +version = "0.7.4" dependencies = [ "alinio", "base64", @@ -500,9 +493,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" @@ -608,9 +601,9 @@ checksum = "cc0db74f9ee706e039d031a560bd7d110c7022f016051b3d33eeff9583e3e67a" [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 667bb5d3..8f6f41f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [package] name = "ox" -version = "0.7.3" +version = "0.7.4" edition = "2021" authors = ["Curlpipe <11898833+curlpipe@users.noreply.github.com>"] description = "A simple but flexible text editor." diff --git a/README.md b/README.md index f840f713..9a46f418 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ The simple but flexible text editor

- - +

+ ![Build Status](https://img.shields.io/github/forks/curlpipe/ox.svg?style=for-the-badge) ![Build Status](https://img.shields.io/github/stars/curlpipe/ox.svg?style=for-the-badge) ![License](https://img.shields.io/github/license/curlpipe/ox.svg?style=for-the-badge) @@ -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 useful features out of the box +3. :package: Has useful features out of the box and a library of plug-ins for everything else ...then Ox is right up your street @@ -46,8 +46,12 @@ It works best on linux, but macOS and Windows are also supported. ### 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 +- :electric_plug: Plug-In system where you can write your own plug-ins or choose from pre-existing ones + - πŸ’¬ Discord RPC + - πŸ“— Git integration with diffs, stats and more + - πŸ•ΈοΈ Handy web development tools such as Emmet and live HTML viewer + - ⏲️ Productivity tools such as a pomodoro timer and todo list tracker +- :wrench: A wide number of options for configuration including colours, key bindings and behaviours - :moon: Ox uses Lua as a configuration language for familiarity when scripting and configuring - :handshake: A configuration assistant to quickly get Ox set up for you from the get-go @@ -62,6 +66,7 @@ It works best on linux, but macOS and Windows are also supported. - :writing_hand: Convenient shortcuts when writing code - :crossed_swords: Multi-editing features such as multiple cursors and recordable macros - :window: Splits to view multiple documents on the same screen at the same time +- :file_cabinet: File tree to view, open, create, delete, copy and move files ### Robustness diff --git a/assets/showcase.gif b/assets/showcase.gif new file mode 100644 index 00000000..8e17171c Binary files /dev/null and b/assets/showcase.gif differ diff --git a/config/.oxrc b/config/.oxrc index 542eedbe..f72ce6f8 100644 --- a/config/.oxrc +++ b/config/.oxrc @@ -276,6 +276,23 @@ event_mapping = { editor:macro_record_stop() editor:display_info("Macro recorded") end, + -- Splits + ["ctrl_alt_left"] = function() + editor:focus_split_left() + end, + ["ctrl_alt_right"] = function() + editor:focus_split_right() + end, + ["ctrl_alt_down"] = function() + editor:focus_split_down() + end, + ["ctrl_alt_up"] = function() + editor:focus_split_up() + end, + -- File Tree + ["ctrl_space"] = function() + editor:toggle_file_tree() + end, } -- Define user-defined commands @@ -401,6 +418,22 @@ colors.error_bg = {41, 41, 61} colors.selection_fg = {255, 255, 255} colors.selection_bg = {59, 59, 130} +colors.file_tree_bg = {41, 41, 61} +colors.file_tree_fg = {255, 255, 255} +colors.file_tree_selection_fg = {255, 255, 255} +colors.file_tree_selection_bg = {59, 59, 130} + +colors.file_tree_red = {240, 104, 89} +colors.file_tree_orange = {240, 142, 89} +colors.file_tree_yellow = {240, 237, 89} +colors.file_tree_green = {89, 240, 169} +colors.file_tree_lightblue = {89, 225, 240} +colors.file_tree_darkblue = {89, 149, 240} +colors.file_tree_purple = {139, 89, 240} +colors.file_tree_pink = {215, 89, 240} +colors.file_tree_brown = {158, 94, 94} +colors.file_tree_grey = {150, 144, 201} + -- Configure Line Numbers -- line_numbers.enabled = true line_numbers.padding_left = 1 @@ -410,6 +443,12 @@ line_numbers.padding_right = 1 terminal.mouse_enabled = true terminal.scroll_amount = 4 +-- Configure File Tree -- +file_tree.width = 30 +file_tree.move_focus_to_file = true +file_tree.icons = false +file_tree.language_icons = true + -- Configure Tab Line -- tab_line.enabled = true tab_line.separators = true diff --git a/kaolinite/src/document/disk.rs b/kaolinite/src/document/disk.rs index 1b46fe94..cd6b692f 100644 --- a/kaolinite/src/document/disk.rs +++ b/kaolinite/src/document/disk.rs @@ -53,8 +53,11 @@ impl Document { /// disk errors. #[cfg(not(tarpaulin_include))] pub fn open>(size: Size, file_name: S) -> Result { + // Try to find the absolute path and load it into the reader let file_name = file_name.into(); - let file = load_rope_from_reader(BufReader::new(File::open(&file_name)?)); + let full_path = std::fs::canonicalize(&file_name)?; + let file = load_rope_from_reader(BufReader::new(File::open(&full_path)?)); + // Find the string representation of the absolute path let file_name = get_absolute_path(&file_name); Ok(Self { info: DocumentInfo { diff --git a/plugins/autoindent.lua b/plugins/autoindent.lua index 62416de8..3ddd1645 100644 --- a/plugins/autoindent.lua +++ b/plugins/autoindent.lua @@ -1,5 +1,5 @@ --[[ -Auto Indent v0.12 +Auto Indent v0.13 Helps you when programming by guessing where indentation should go and then automatically applying these guesses as you program @@ -119,6 +119,7 @@ end -- Get how indented a line is at a certain y index function autoindent:get_indent(y) + if y == nil then return nil end local line = editor:get_line_at(y) return #(line:match("^\t+") or "") + #(line:match("^ +") or "") / document.tab_width end @@ -161,16 +162,18 @@ function autoindent:disperse_block() end event_mapping["enter"] = function() - -- Indent where appropriate - if autoindent:causes_indent(editor.cursor.y - 1) then - local new_level = autoindent:get_indent(editor.cursor.y) + 1 - autoindent:set_indent(editor.cursor.y, new_level) + if editor.cursor ~= nil then + -- Indent where appropriate + if autoindent:causes_indent(editor.cursor.y - 1) then + local new_level = autoindent:get_indent(editor.cursor.y) + 1 + autoindent:set_indent(editor.cursor.y, new_level) + end + -- Give newly created line a boost to match it up relatively with the line before it + local added_level = autoindent:get_indent(editor.cursor.y) + autoindent:get_indent(editor.cursor.y - 1) + autoindent:set_indent(editor.cursor.y, added_level) + -- Handle the case where enter is pressed, creating a multi-line block that requires neatening up + autoindent:disperse_block() end - -- Give newly created line a boost to match it up relatively with the line before it - local added_level = autoindent:get_indent(editor.cursor.y) + autoindent:get_indent(editor.cursor.y - 1) - autoindent:set_indent(editor.cursor.y, added_level) - -- Handle the case where enter is pressed, creating a multi-line block that requires neatening up - autoindent:disperse_block() end -- For each ascii characters and punctuation @@ -181,14 +184,18 @@ for i = 32, 126 do 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) + if editor.cursor ~= nil then + was_dedenting = autoindent:causes_dedent(editor.cursor.y) + end 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) + if editor.cursor ~= nil then + 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 @@ -205,62 +212,66 @@ 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 + if editor.cursor ~= nil then + 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 - for line = cursor.y, select.y do - editor:move_to(0, line) - local indent = autoindent:get_indent(line) - autoindent:set_indent(line, indent + 1) + -- 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 - 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) + editor:cursor_snap() end - editor:cursor_snap() end -- Shortcut to dedent a line event_mapping["shift_tab"] = function() - 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 + if editor.cursor ~= nil then + 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 - for line = cursor.y, select.y do - editor:move_to(0, line) - local indent = autoindent:get_indent(line) - autoindent:set_indent(line, indent - 1) + -- 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 - 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) + editor:cursor_snap() end - editor:cursor_snap() end diff --git a/plugins/pairs.lua b/plugins/pairs.lua index 499d9eee..3d5820e3 100644 --- a/plugins/pairs.lua +++ b/plugins/pairs.lua @@ -1,5 +1,5 @@ --[[ -Bracket Pairs v0.6 +Bracket Pairs v0.7 Automatically insert and delete brackets and quotes where appropriate Also helps when you want to pad out brackets and quotes with whitespace @@ -19,6 +19,7 @@ autopairs.just_paired = { x = nil, y = nil } -- Determine whether we are currently inside a pair function autopairs:in_pair() + if editor.cursor == nil then return false end -- Get first candidate for a pair local first if editor.cursor.x == 0 then @@ -61,6 +62,7 @@ for i, str in ipairs(autopairs.pairings) do if start_pair == end_pair then -- Handle hybrid start_pair and end_pair event_mapping[start_pair] = function() + if editor.cursor == nil then return end -- Check if there is a matching start pair local at_char = ' ' if editor.cursor.x > 1 then @@ -85,6 +87,7 @@ for i, str in ipairs(autopairs.pairings) do else -- Handle traditional pairs event_mapping[end_pair] = function() + if editor.cursor == nil 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 @@ -100,6 +103,7 @@ for i, str in ipairs(autopairs.pairings) do end end event_mapping[start_pair] = function() + if editor.cursor == nil then return end autopairs.just_paired = editor.cursor editor:insert(end_pair) editor:move_left() diff --git a/plugins/themes/default16.lua b/plugins/themes/default16.lua index 7058b2b9..12771158 100644 --- a/plugins/themes/default16.lua +++ b/plugins/themes/default16.lua @@ -48,6 +48,22 @@ colors.error_fg = red colors.selection_bg = darkgrey colors.selection_fg = cyan +colors.file_tree_bg = black +colors.file_tree_fg = white +colors.file_tree_selection_bg = darkgrey +colors.file_tree_selection_fg = cyan + +colors.file_tree_red = red +colors.file_tree_orange = darkyellow +colors.file_tree_yellow = yellow +colors.file_tree_green = green +colors.file_tree_lightblue = blue +colors.file_tree_darkblue = darkblue +colors.file_tree_purple = darkmagenta +colors.file_tree_pink = magenta +colors.file_tree_brown = darkred +colors.file_tree_grey = grey + -- Configure Syntax Highlighting Colours -- syntax:set("string", green) -- Strings, bright green syntax:set("comment", darkgrey) -- Comments, light purple/gray diff --git a/plugins/themes/galaxy.lua b/plugins/themes/galaxy.lua index 0b568f3b..e1da9e5e 100644 --- a/plugins/themes/galaxy.lua +++ b/plugins/themes/galaxy.lua @@ -44,6 +44,22 @@ colors.error_fg = red colors.selection_bg = grey1 colors.selection_fg = lightblue +colors.file_tree_bg = black +colors.file_tree_fg = white +colors.file_tree_selection_bg = purple +colors.file_tree_selection_fg = black + +colors.file_tree_red = {247, 156, 156} +colors.file_tree_orange = {247, 165, 156} +colors.file_tree_yellow = {247, 226, 156} +colors.file_tree_green = {191, 247, 156} +colors.file_tree_lightblue = {156, 214, 247} +colors.file_tree_darkblue = {156, 163, 247} +colors.file_tree_purple = {197, 156, 247} +colors.file_tree_pink = {246, 156, 247} +colors.file_tree_brown = {163, 118, 118} +colors.file_tree_grey = {160, 157, 191} + -- Configure Syntax Highlighting Colours -- syntax:set("string", green) -- Strings, bright green syntax:set("comment", grey3) -- Comments, light purple/gray diff --git a/plugins/themes/omni.lua b/plugins/themes/omni.lua index 9ba09f4f..7a1f6130 100644 --- a/plugins/themes/omni.lua +++ b/plugins/themes/omni.lua @@ -44,6 +44,22 @@ colors.error_fg = red colors.selection_bg = selection colors.selection_fg = foreground +colors.file_tree_bg = background +colors.file_tree_fg = foreground +colors.file_tree_selection_bg = pink +colors.file_tree_selection_fg = background + +colors.file_tree_red = {255, 128, 128} +colors.file_tree_orange = {255, 155, 128} +colors.file_tree_yellow = {255, 204, 128} +colors.file_tree_green = {196, 255, 128} +colors.file_tree_lightblue = {128, 236, 255} +colors.file_tree_darkblue = {128, 147, 255} +colors.file_tree_purple = {204, 128, 255} +colors.file_tree_pink = {255, 128, 200} +colors.file_tree_brown = {163, 108, 108} +colors.file_tree_grey = {155, 153, 176} + -- Configure Syntax Highlighting Colours -- syntax:set("string", yellow) -- Strings, fresh green syntax:set("comment", comment) -- Comments, muted and subtle diff --git a/plugins/themes/transparent.lua b/plugins/themes/transparent.lua index c67ec8ce..91961880 100644 --- a/plugins/themes/transparent.lua +++ b/plugins/themes/transparent.lua @@ -9,3 +9,7 @@ colors.split_fg = 15 colors.info_bg = 'transparent' colors.warning_bg = 'transparent' colors.error_bg = 'transparent' + +colors.file_tree_bg = 'transparent' +colors.file_tree_selection_fg = {35, 240, 144} +colors.file_tree_selection_bg = 'transparent' diff --git a/plugins/themes/tropical.lua b/plugins/themes/tropical.lua index aaa9b182..a4033fa2 100644 --- a/plugins/themes/tropical.lua +++ b/plugins/themes/tropical.lua @@ -44,6 +44,22 @@ colors.error_fg = red colors.selection_bg = grey1 colors.selection_fg = lightblue +colors.file_tree_bg = black +colors.file_tree_fg = white +colors.file_tree_selection_bg = lightblue +colors.file_tree_selection_fg = black + +colors.file_tree_red = {245, 127, 127} +colors.file_tree_orange = {245, 169, 127} +colors.file_tree_yellow = {245, 217, 127} +colors.file_tree_green = {165, 245, 127} +colors.file_tree_lightblue = {127, 227, 245} +colors.file_tree_darkblue = {127, 145, 245} +colors.file_tree_purple = {190, 127, 245} +colors.file_tree_pink = {245, 127, 217} +colors.file_tree_brown = {163, 116, 116} +colors.file_tree_grey = {191, 190, 196} + -- Configure Syntax Highlighting Colours -- syntax:set("string", lightblue) -- Strings, bright green syntax:set("comment", grey3) -- Comments, light purple/gray diff --git a/plugins/todo.lua b/plugins/todo.lua index 207dada1..f95c9661 100644 --- a/plugins/todo.lua +++ b/plugins/todo.lua @@ -1,5 +1,5 @@ --[[ -Todo Lists v0.2 +Todo Lists v0.3 This plug-in will provide todo list functionality on files with the extension .todo You can mark todos as done / not done by using the Ctrl + Enter key combination @@ -12,6 +12,7 @@ file_types["Todo"] = { extensions = {"todo"}, files = {".todo.md", ".todo"}, modelines = {}, + color = "grey", } -- Add syntax highlighting to .todo files (done todos are comments) diff --git a/src/config/assistant.rs b/src/config/assistant.rs index 2ff3565b..f5494ab0 100644 --- a/src/config/assistant.rs +++ b/src/config/assistant.rs @@ -176,6 +176,8 @@ pub struct Assistant { pub tab_line: bool, pub tab_line_sep: bool, pub greeting_message: bool, + pub file_tree_icons: bool, + pub file_tree_language_icons: bool, pub plugins: Vec, } @@ -195,6 +197,9 @@ impl Default for Assistant { tab_line_sep: true, // Greeting Message greeting_message: true, + // File Tree + file_tree_icons: false, + file_tree_language_icons: true, // Mouse and Cursor Behaviour mouse: true, scroll_sensitivity: 4, @@ -229,6 +234,8 @@ impl Assistant { Self::ask_mouse_cursor(&mut result)?; // Icons Self::ask_icons(&mut result)?; + // File tree + Self::ask_file_tree(&mut result)?; // Plug-Ins Self::ask_plugins(&mut result)?; // Create the configuration file (and print it) @@ -456,6 +463,28 @@ impl Assistant { Ok(()) } + pub fn ask_file_tree(result: &mut Self) -> Result<()> { + if result.icons { + let orange = Fg(Color::Ansi(202).to_color()?); + let yellow = Fg(Color::Ansi(220).to_color()?); + let green = Fg(Color::Ansi(34).to_color()?); + let reset = Fg(Color::Transparent.to_color()?); + println!("πŸ–Ή file1.txt \nπŸ–Ή file2.txt \nπŸ–Ή file3.txt \n"); + result.file_tree_icons = + Self::confirmation("Would you like icons in the file tree?", false); + if result.file_tree_icons { + println!( + "\n {green}🎡{reset} file1.mp3 \n {orange}{{}}{reset} file2.css \n {yellow}{reset} file3.html \n" + ); + result.file_tree_language_icons = Self::confirmation( + "Would you like the tab line to have language specific icons?", + true, + ); + } + } + Ok(()) + } + pub fn ask_line_numbers(result: &mut Self) -> Result<()> { let red = Fg(Color::Ansi(196).to_color()?); let orange = Fg(Color::Ansi(202).to_color()?); @@ -690,6 +719,7 @@ impl Assistant { } /// Turn the configuration assistant details into a lua file + #[allow(clippy::too_many_lines)] pub fn to_config(&self) -> String { let mut result = String::new(); let (sections, fields) = self.diff(); @@ -779,6 +809,19 @@ impl Assistant { result += "\n-- Greeting Message Configuration --\n"; result += &format!("greeting_message.enabled = {}\n", self.greeting_message); } + // Configuration of file tree + if sections.contains(&"file_tree") { + result += "\n-- File Tree Configuration --\n"; + if fields.contains(&"file_tree_icons") { + result += &format!("file_tree.icons = {}\n", self.file_tree_icons); + } + if fields.contains(&"file_tree_language_icons") { + result += &format!( + "file_tree.language_icons = {}\n", + self.file_tree_language_icons + ); + } + } // Configuration of mouse and cursor behaviour if sections.contains(&"cursors") { result += "\n-- Cursor Configuration --\n"; @@ -820,6 +863,14 @@ impl Assistant { ("mouse", self.mouse != def.mouse), ("cursor_wrap", self.cursor_wrap != def.cursor_wrap), ("icons", self.icons != def.icons), + ( + "file_tree_icons", + self.file_tree_icons != def.file_tree_icons, + ), + ( + "file_tree_language_icons", + self.file_tree_language_icons != def.file_tree_language_icons, + ), ( "line_number_padding", self.line_number_padding != def.line_number_padding, @@ -865,6 +916,10 @@ impl Assistant { || fields.contains(&"scroll_sensitivity") || fields.contains(&"cursor_wrap"), ), + ( + "file_tree", + fields.contains(&"file_tree_icons") || fields.contains(&"file_tree_language_icons"), + ), ]; let sections = sections .iter() diff --git a/src/config/colors.rs b/src/config/colors.rs index 4f909abb..0159e250 100644 --- a/src/config/colors.rs +++ b/src/config/colors.rs @@ -6,7 +6,7 @@ use mlua::prelude::*; use super::issue_warning; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Colors { pub editor_bg: Color, pub editor_fg: Color, @@ -36,6 +36,22 @@ pub struct Colors { pub selection_fg: Color, pub selection_bg: Color, + + pub file_tree_fg: Color, + pub file_tree_bg: Color, + pub file_tree_selection_fg: Color, + pub file_tree_selection_bg: Color, + + pub file_tree_red: Color, + pub file_tree_orange: Color, + pub file_tree_yellow: Color, + pub file_tree_green: Color, + pub file_tree_lightblue: Color, + pub file_tree_darkblue: Color, + pub file_tree_purple: Color, + pub file_tree_pink: Color, + pub file_tree_brown: Color, + pub file_tree_grey: Color, } impl Default for Colors { @@ -67,8 +83,24 @@ impl Default for Colors { error_bg: Color::Rgb(41, 41, 61), error_fg: Color::Rgb(255, 100, 100), - selection_fg: Color::Rgb(41, 41, 61), - selection_bg: Color::Rgb(41, 41, 61), + selection_fg: Color::Rgb(255, 255, 255), + selection_bg: Color::Rgb(59, 59, 130), + + file_tree_bg: Color::Rgb(41, 41, 61), + file_tree_fg: Color::Rgb(255, 255, 255), + file_tree_selection_bg: Color::Rgb(59, 59, 130), + file_tree_selection_fg: Color::Rgb(255, 255, 255), + + file_tree_red: Color::Rgb(240, 56, 36), + file_tree_orange: Color::Rgb(240, 107, 36), + file_tree_yellow: Color::Rgb(240, 236, 36), + file_tree_green: Color::Rgb(35, 240, 144), + file_tree_lightblue: Color::Rgb(36, 219, 240), + file_tree_darkblue: Color::Rgb(36, 117, 240), + file_tree_purple: Color::Rgb(104, 36, 240), + file_tree_pink: Color::Rgb(206, 36, 240), + file_tree_brown: Color::Rgb(158, 94, 94), + file_tree_grey: Color::Rgb(150, 144, 201), } } } @@ -197,10 +229,106 @@ impl LuaUserData for Colors { this.selection_bg = Color::from_lua(value); Ok(()) }); + fields.add_field_method_set("file_tree_bg", |_, this, value| { + this.file_tree_bg = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_fg", |_, this, value| { + this.file_tree_fg = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_selection_bg", |_, this, value| { + this.file_tree_selection_bg = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_selection_fg", |_, this, value| { + this.file_tree_selection_fg = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_red", |_, this, value| { + this.file_tree_red = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_red", |_, this, value| { + this.file_tree_red = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_orange", |_, this, value| { + this.file_tree_orange = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_orange", |_, this, value| { + this.file_tree_orange = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_yellow", |_, this, value| { + this.file_tree_yellow = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_yellow", |_, this, value| { + this.file_tree_yellow = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_green", |_, this, value| { + this.file_tree_green = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_green", |_, this, value| { + this.file_tree_green = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_lightblue", |_, this, value| { + this.file_tree_lightblue = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_lightblue", |_, this, value| { + this.file_tree_lightblue = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_darkblue", |_, this, value| { + this.file_tree_darkblue = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_darkblue", |_, this, value| { + this.file_tree_darkblue = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_purple", |_, this, value| { + this.file_tree_purple = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_purple", |_, this, value| { + this.file_tree_purple = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_pink", |_, this, value| { + this.file_tree_pink = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_pink", |_, this, value| { + this.file_tree_pink = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_brown", |_, this, value| { + this.file_tree_brown = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_brown", |_, this, value| { + this.file_tree_brown = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_grey", |_, this, value| { + this.file_tree_grey = Color::from_lua(value); + Ok(()) + }); + fields.add_field_method_set("file_tree_grey", |_, this, value| { + this.file_tree_grey = Color::from_lua(value); + Ok(()) + }); } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Color { Rgb(u8, u8, u8), Hex(String), diff --git a/src/config/editor.rs b/src/config/editor.rs index 95222945..979a9f13 100644 --- a/src/config/editor.rs +++ b/src/config/editor.rs @@ -24,7 +24,7 @@ impl LuaUserData for Editor { if let Some(doc) = editor.try_doc() { let loc = doc.cursor.selection_end; Ok(Some(LuaLoc { - x: editor.doc().character_idx(&loc), + x: doc.character_idx(&loc), y: loc.y + 1, })) } else { @@ -189,16 +189,21 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("remove_word", |_, editor, ()| { - let _ = editor.doc_mut().delete_word(); - editor.update_highlighter(); - editor.hl_edit(editor.doc().loc().y); + if let Some(doc) = editor.try_doc_mut() { + let _ = doc.delete_word(); + let y = doc.loc().y; + editor.update_highlighter(); + editor.hl_edit(y); + } Ok(()) }); // Cursor moving methods.add_method_mut("move_to", |_, editor, (x, y): (usize, usize)| { - let y = y.saturating_sub(1); - editor.doc_mut().move_to(&Loc { y, x }); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + let y = y.saturating_sub(1); + doc.move_to(&Loc { y, x }); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_up", |_, editor, ()| { @@ -222,33 +227,45 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("move_home", |_, editor, ()| { - editor.doc_mut().move_home(); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_home(); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_end", |_, editor, ()| { - editor.doc_mut().move_end(); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_end(); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_page_up", |_, editor, ()| { - editor.doc_mut().move_page_up(); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_page_up(); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_page_down", |_, editor, ()| { - editor.doc_mut().move_page_down(); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_page_down(); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_top", |_, editor, ()| { - editor.doc_mut().move_top(); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_top(); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_bottom", |_, editor, ()| { - editor.doc_mut().move_bottom(); - editor.update_highlighter(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_bottom(); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("move_previous_word", |_, editor, ()| { @@ -262,21 +279,29 @@ impl LuaUserData for Editor { Ok(()) }); methods.add_method_mut("cursor_snap", |_, editor, ()| { - editor.doc_mut().old_cursor = editor.doc().loc().x; + if let Some(doc) = editor.try_doc_mut() { + doc.old_cursor = 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(); + if let Some(doc) = editor.try_doc_mut() { + let _ = doc.swap_line_up(); + let y = doc.loc().y; + editor.hl_edit(y); + editor.hl_edit(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(); + if let Some(doc) = editor.try_doc_mut() { + let _ = doc.swap_line_down(); + let y = doc.loc().y; + editor.hl_edit(y.saturating_sub(1)); + editor.hl_edit(y); + editor.update_highlighter(); + } Ok(()) }); // Cursor selection and clipboard @@ -306,17 +331,23 @@ impl LuaUserData for Editor { 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(); + if let Some(doc) = editor.try_doc_mut() { + let y = y.saturating_sub(1); + doc.select_to(&Loc { y, x }); + editor.update_highlighter(); + } Ok(()) }); methods.add_method_mut("cancel_selection", |_, editor, ()| { - editor.doc_mut().cancel_selection(); + if let Some(doc) = editor.try_doc_mut() { + doc.cancel_selection(); + } Ok(()) }); methods.add_method_mut("cursor_to_viewport", |_, editor, ()| { - editor.doc_mut().bring_cursor_in_viewport(); + if let Some(doc) = editor.try_doc_mut() { + doc.bring_cursor_in_viewport(); + } Ok(()) }); methods.add_method_mut("cut", |_, editor, ()| { @@ -341,72 +372,90 @@ impl LuaUserData for Editor { methods.add_method_mut( "insert_at", |_, editor, (text, x, y): (String, usize, usize)| { - editor.plugin_active = true; - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to(&Loc { y, x }); - for ch in text.chars() { - if let Err(err) = editor.character(ch) { - editor.feedback = Feedback::Error(err.to_string()); + if editor.try_doc().is_some() { + editor.plugin_active = true; + let y = y.saturating_sub(1); + let location = editor.try_doc().unwrap().char_loc(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_to(&Loc { y, x }); + } + for ch in text.chars() { + if let Err(err) = editor.character(ch) { + editor.feedback = Feedback::Error(err.to_string()); + } } + if let Some(doc) = editor.try_doc_mut() { + doc.move_to(&location); + } + editor.update_highlighter(); + editor.plugin_active = false; } - editor.doc_mut().move_to(&location); - editor.update_highlighter(); - editor.plugin_active = false; Ok(()) }, ); methods.add_method_mut("remove_at", |_, editor, (x, y): (usize, usize)| { - editor.plugin_active = true; - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to(&Loc { y, x }); - if let Err(err) = editor.delete() { - editor.feedback = Feedback::Error(err.to_string()); - } - editor.doc_mut().move_to(&location); - editor.update_highlighter(); - editor.plugin_active = false; - Ok(()) - }); - methods.add_method_mut("insert_line_at", |_, editor, (text, y): (String, usize)| { - editor.plugin_active = true; - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - if y < editor.doc().len_lines() { - editor.doc_mut().move_to_y(y); - editor.doc_mut().move_home(); - if let Err(err) = editor.enter() { - editor.feedback = Feedback::Error(err.to_string()); + if editor.try_doc().is_some() { + editor.plugin_active = true; + let y = y.saturating_sub(1); + let location = editor.try_doc().unwrap().char_loc(); + if let Some(doc) = editor.try_doc_mut() { + doc.move_to(&Loc { y, x }); } - editor.up(); - } else { - editor.doc_mut().move_bottom(); - if let Err(err) = editor.enter() { + if let Err(err) = editor.delete() { editor.feedback = Feedback::Error(err.to_string()); } + if let Some(doc) = editor.try_doc_mut() { + doc.move_to(&location); + } + editor.update_highlighter(); + editor.plugin_active = false; } - for ch in text.chars() { - if let Err(err) = editor.character(ch) { - editor.feedback = Feedback::Error(err.to_string()); + Ok(()) + }); + methods.add_method_mut("insert_line_at", |_, editor, (text, y): (String, usize)| { + if editor.try_doc().is_some() { + editor.plugin_active = true; + let y = y.saturating_sub(1); + let location = editor.try_doc().unwrap().char_loc(); + if let Some(doc) = editor.try_doc_mut() { + if y < doc.len_lines() { + doc.move_to_y(y); + doc.move_home(); + if let Err(err) = editor.enter() { + editor.feedback = Feedback::Error(err.to_string()); + } + editor.up(); + } else { + doc.move_bottom(); + if let Err(err) = editor.enter() { + editor.feedback = Feedback::Error(err.to_string()); + } + } + for ch in text.chars() { + if let Err(err) = editor.character(ch) { + editor.feedback = Feedback::Error(err.to_string()); + } + } } + editor.try_doc_mut().unwrap().move_to(&location); + editor.update_highlighter(); + editor.plugin_active = false; } - editor.doc_mut().move_to(&location); - editor.update_highlighter(); - editor.plugin_active = false; Ok(()) }); methods.add_method_mut("remove_line_at", |_, editor, y: usize| { - editor.plugin_active = true; - let y = y.saturating_sub(1); - let location = editor.doc_mut().char_loc(); - editor.doc_mut().move_to_y(y); - if let Err(err) = editor.delete_line() { - editor.feedback = Feedback::Error(err.to_string()); + if editor.try_doc().is_some() { + editor.plugin_active = true; + let y = y.saturating_sub(1); + let location = editor.try_doc().unwrap().char_loc(); + editor.try_doc_mut().unwrap().move_to_y(y); + if let Err(err) = editor.delete_line() { + editor.feedback = Feedback::Error(err.to_string()); + } + editor.try_doc_mut().unwrap().move_to(&location); + editor.update_highlighter(); + editor.plugin_active = false; } - editor.doc_mut().move_to(&location); - editor.update_highlighter(); - editor.plugin_active = false; Ok(()) }); methods.add_method_mut("get", |_, editor, ()| { @@ -545,6 +594,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_up(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.update_cwd(); Ok(true) } else { Ok(false) @@ -555,6 +605,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_down(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.update_cwd(); Ok(true) } else { Ok(false) @@ -565,6 +616,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_left(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.update_cwd(); Ok(true) } else { Ok(false) @@ -575,6 +627,7 @@ impl LuaUserData for Editor { editor.ptr = editor .files .open_right(editor.ptr.clone(), FileLayout::Atom(vec![fc], 0)); + editor.update_cwd(); Ok(true) } else { Ok(false) @@ -604,18 +657,32 @@ impl LuaUserData for Editor { ); methods.add_method_mut("focus_split_up", |_, editor, ()| { editor.ptr = FileLayout::move_up(editor.ptr.clone(), &editor.render_cache.span); + editor.update_cwd(); Ok(()) }); methods.add_method_mut("focus_split_down", |_, editor, ()| { editor.ptr = FileLayout::move_down(editor.ptr.clone(), &editor.render_cache.span); + editor.update_cwd(); Ok(()) }); methods.add_method_mut("focus_split_left", |_, editor, ()| { - editor.ptr = FileLayout::move_left(editor.ptr.clone(), &editor.render_cache.span); + let new_ptr = FileLayout::move_left(editor.ptr.clone(), &editor.render_cache.span); + let in_file_tree = matches!( + editor.files.get_raw(new_ptr.clone()), + Some(FileLayout::FileTree) + ); + if in_file_tree { + // We just entered into a file tree, cache where we were (minus the file tree itself) + editor.old_ptr = editor.ptr.clone(); + editor.old_ptr.pop(); + } + editor.ptr = new_ptr; + editor.update_cwd(); Ok(()) }); methods.add_method_mut("focus_split_right", |_, editor, ()| { editor.ptr = FileLayout::move_right(editor.ptr.clone(), &editor.render_cache.span); + editor.update_cwd(); Ok(()) }); // Searching and replacing @@ -639,32 +706,40 @@ impl LuaUserData for Editor { }); methods.add_method_mut("move_next_match", |_, editor, query: String| { editor.next_match(&query); - editor.doc_mut().cancel_selection(); + if let Some(doc) = editor.try_doc_mut() { + doc.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(); + if let Some(doc) = editor.try_doc_mut() { + doc.cancel_selection(); + } editor.update_highlighter(); Ok(()) }); // Document state modification methods.add_method_mut("set_read_only", |_, editor, status: bool| { - editor.doc_mut().info.read_only = status; + if let Some(doc) = editor.try_doc_mut() { + doc.info.read_only = status; + } Ok(()) }); methods.add_method_mut("set_file_type", |_, editor, name: String| { - let doc = config!(editor.config, document); - if let Some(file_type) = doc.file_types.get_name(&name) { - let mut highlighter = file_type.get_highlighter(&editor.config, 4); - highlighter.run(&editor.doc().lines); - if let Some(file) = editor.files.get_mut(editor.ptr.clone()) { - file.highlighter = highlighter; - file.file_type = Some(file_type); + if let Some(actual_doc) = editor.try_doc() { + let doc = config!(editor.config, document); + if let Some(file_type) = doc.file_types.get_name(&name) { + let mut highlighter = file_type.get_highlighter(&editor.config, 4); + highlighter.run(&actual_doc.lines); + if let Some(file) = editor.files.get_mut(editor.ptr.clone()) { + file.highlighter = highlighter; + file.file_type = Some(file_type); + } + } else { + editor.feedback = Feedback::Error(format!("Invalid file type: {name}")); } - } else { - editor.feedback = Feedback::Error(format!("Invalid file type: {name}")); } Ok(()) }); @@ -690,6 +765,11 @@ impl LuaUserData for Editor { let _ = editor.render(lua); Ok(()) }); + // File Tree + methods.add_method_mut("toggle_file_tree", |_, editor, ()| { + editor.toggle_file_tree(); + Ok(()) + }); // Miscellaneous methods.add_method_mut("open_command_line", |_, editor, ()| { match editor.prompt("Command") { diff --git a/src/config/filetree.rs b/src/config/filetree.rs new file mode 100644 index 00000000..44f0e75e --- /dev/null +++ b/src/config/filetree.rs @@ -0,0 +1,46 @@ +/// Related to file tree configuration options +use mlua::prelude::*; + +#[derive(Debug)] +pub struct FileTree { + pub width: usize, + pub move_focus_to_file: bool, + pub icons: bool, + pub language_icons: bool, +} + +impl Default for FileTree { + fn default() -> Self { + Self { + width: 30, + move_focus_to_file: true, + icons: false, + language_icons: true, + } + } +} + +impl LuaUserData for FileTree { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("width", |_, this| Ok(this.width)); + fields.add_field_method_set("width", |_, this, value| { + this.width = value; + Ok(()) + }); + fields.add_field_method_get("move_focus_to_file", |_, this| Ok(this.move_focus_to_file)); + fields.add_field_method_set("move_focus_to_file", |_, this, value| { + this.move_focus_to_file = value; + Ok(()) + }); + fields.add_field_method_get("icons", |_, this| Ok(this.icons)); + fields.add_field_method_set("icons", |_, this, value| { + this.icons = value; + Ok(()) + }); + fields.add_field_method_get("language_icons", |_, this| Ok(this.language_icons)); + fields.add_field_method_set("language_icons", |_, this, value| { + this.language_icons = value; + Ok(()) + }); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 380d8315..56a379ea 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex}; mod assistant; mod colors; mod editor; +mod filetree; mod highlighting; mod interface; mod keys; @@ -15,6 +16,7 @@ mod tasks; pub use assistant::Assistant; pub use colors::{Color, Colors}; +pub use filetree::FileTree; pub use highlighting::SyntaxHighlighting; pub use interface::{GreetingMessage, HelpMessage, LineNumbers, StatusLine, TabLine, Terminal}; pub use keys::{get_listeners, key_to_string, run_key, run_key_before}; @@ -82,6 +84,9 @@ macro_rules! config { .borrow::<$crate::config::HelpMessage>() .unwrap() }; + ($cfg:expr, file_tree) => { + $cfg.file_tree.borrow::<$crate::config::FileTree>().unwrap() + }; ($cfg:expr, terminal) => { $cfg.terminal.borrow::<$crate::config::Terminal>().unwrap() }; @@ -97,6 +102,7 @@ pub struct Config { pub tab_line: LuaAnyUserData, pub greeting_message: LuaAnyUserData, pub help_message: LuaAnyUserData, + pub file_tree: LuaAnyUserData, pub terminal: LuaAnyUserData, pub document: LuaAnyUserData, pub task_manager: Arc>, @@ -113,6 +119,7 @@ impl Config { let colors = lua.create_userdata(Colors::default())?; let status_line = lua.create_userdata(StatusLine::default())?; let tab_line = lua.create_userdata(TabLine::default())?; + let file_tree = lua.create_userdata(FileTree::default())?; let terminal = lua.create_userdata(Terminal::default())?; let document = lua.create_userdata(Document::default())?; @@ -132,6 +139,7 @@ impl Config { lua.globals().set("help_message", help_message.clone())?; lua.globals().set("status_line", status_line.clone())?; lua.globals().set("tab_line", tab_line.clone())?; + lua.globals().set("file_tree", file_tree.clone())?; lua.globals().set("colors", colors.clone())?; lua.globals().set("terminal", terminal.clone())?; lua.globals().set("document", document.clone())?; @@ -178,6 +186,7 @@ impl Config { tab_line, greeting_message, help_message, + file_tree, terminal, document, task_manager, @@ -367,12 +376,14 @@ impl FromLua for FileTypes { .pairs::() .filter_map(|val| if let Ok((_, v)) = val { Some(v) } else { None }) .collect::>(); + let color = info.get::("color")?; result.push(FileType { name, icon, files, extensions, modelines, + color, }); } } diff --git a/src/editor/cursor.rs b/src/editor/cursor.rs index 246279f4..e742b4a9 100644 --- a/src/editor/cursor.rs +++ b/src/editor/cursor.rs @@ -9,91 +9,113 @@ use super::Editor; impl Editor { /// Move the cursor up pub fn select_up(&mut self) { - self.doc_mut().select_up(); + if let Some(doc) = self.try_doc_mut() { + doc.select_up(); + } } /// Move the cursor down pub fn select_down(&mut self) { - self.doc_mut().select_down(); + if let Some(doc) = self.try_doc_mut() { + doc.select_down(); + } } /// Move the cursor left pub fn select_left(&mut self) { - let status = self.doc_mut().select_left(); - // Cursor wrapping if cursor hits the start of the line let wrapping = config!(self.config, document).wrap_cursor; - if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { - self.doc_mut().select_up(); - self.doc_mut().select_end(); + if let Some(doc) = self.try_doc_mut() { + let status = doc.select_left(); + // Cursor wrapping if cursor hits the start of the line + if status == Status::StartOfLine && doc.loc().y != 0 && wrapping { + doc.select_up(); + doc.select_end(); + } } } /// Move the cursor right pub fn select_right(&mut self) { - let status = self.doc_mut().select_right(); - // Cursor wrapping if cursor hits the end of a line let wrapping = config!(self.config, document).wrap_cursor; - if status == Status::EndOfLine && wrapping { - self.doc_mut().select_down(); - self.doc_mut().select_home(); + if let Some(doc) = self.try_doc_mut() { + let status = doc.select_right(); + // Cursor wrapping if cursor hits the end of a line + if status == Status::EndOfLine && wrapping { + doc.select_down(); + doc.select_home(); + } } } /// Select the whole document pub fn select_all(&mut self) { - self.doc_mut().move_top(); - self.doc_mut().select_bottom(); + if let Some(doc) = self.try_doc_mut() { + doc.move_top(); + doc.select_bottom(); + } } /// Move the cursor up pub fn up(&mut self) { - self.doc_mut().move_up(); + if let Some(doc) = self.try_doc_mut() { + doc.move_up(); + } } /// Move the cursor down pub fn down(&mut self) { - self.doc_mut().move_down(); + if let Some(doc) = self.try_doc_mut() { + doc.move_down(); + } } /// Move the cursor left pub fn left(&mut self) { - let status = self.doc_mut().move_left(); - // Cursor wrapping if cursor hits the start of the line let wrapping = config!(self.config, document).wrap_cursor; - if status == Status::StartOfLine && self.doc().loc().y != 0 && wrapping { - self.doc_mut().move_up(); - self.doc_mut().move_end(); + if let Some(doc) = self.try_doc_mut() { + let status = doc.move_left(); + // Cursor wrapping if cursor hits the start of the line + if status == Status::StartOfLine && doc.loc().y != 0 && wrapping { + doc.move_up(); + doc.move_end(); + } } } /// Move the cursor right pub fn right(&mut self) { - let status = self.doc_mut().move_right(); - // Cursor wrapping if cursor hits the end of a line let wrapping = config!(self.config, document).wrap_cursor; - if status == Status::EndOfLine && wrapping { - self.doc_mut().move_down(); - self.doc_mut().move_home(); + if let Some(doc) = self.try_doc_mut() { + let status = doc.move_right(); + // Cursor wrapping if cursor hits the end of a line + if status == Status::EndOfLine && wrapping { + doc.move_down(); + doc.move_home(); + } } } /// Move the cursor to the previous word in the line pub fn prev_word(&mut self) { - let status = self.doc_mut().move_prev_word(); let wrapping = config!(self.config, document).wrap_cursor; - if status == Status::StartOfLine && wrapping { - self.doc_mut().move_up(); - self.doc_mut().move_end(); + if let Some(doc) = self.try_doc_mut() { + let status = doc.move_prev_word(); + if status == Status::StartOfLine && wrapping { + doc.move_up(); + doc.move_end(); + } } } /// Move the cursor to the next word in the line pub fn next_word(&mut self) { - let status = self.doc_mut().move_next_word(); let wrapping = config!(self.config, document).wrap_cursor; - if status == Status::EndOfLine && wrapping { - self.doc_mut().move_down(); - self.doc_mut().move_home(); + if let Some(doc) = self.try_doc_mut() { + let status = doc.move_next_word(); + if status == Status::EndOfLine && wrapping { + doc.move_down(); + doc.move_home(); + } } } } @@ -105,12 +127,14 @@ pub fn handle_multiple_cursors( lua: &Lua, original_loc: &Loc, ) -> Result<()> { + if ged!(&editor).try_doc().is_none() { + return Ok(()); + } let mut original_loc = *original_loc; // Cache the state of the document - let mut cursor = ged!(&editor).doc().cursor; - // For each secondary cursor, replay the key event + let mut cursor = ged!(&editor).try_doc().unwrap().cursor; + let mut secondary_cursors = ged!(&editor).try_doc().unwrap().secondary_cursors.clone(); ged!(mut &editor).macro_man.playing = true; - let mut secondary_cursors = ged!(&editor).doc().secondary_cursors.clone(); // Prevent interference adjust_other_cursors( &mut secondary_cursors, @@ -124,12 +148,15 @@ pub fn handle_multiple_cursors( while ptr < secondary_cursors.len() { // Move to the secondary cursor position let sec_cursor = secondary_cursors[ptr]; - ged!(mut &editor).doc_mut().move_to(&sec_cursor); + ged!(mut &editor) + .try_doc_mut() + .unwrap() + .move_to(&sec_cursor); // Replay the event - let old_loc = ged!(&editor).doc().char_loc(); + let old_loc = ged!(&editor).try_doc().unwrap().char_loc(); handle_event(editor, event, lua)?; // Prevent any interference - let char_loc = ged!(&editor).doc().char_loc(); + let char_loc = ged!(&editor).try_doc().unwrap().char_loc(); cursor.loc = adjust_other_cursors( &mut secondary_cursors, &old_loc, @@ -142,15 +169,15 @@ pub fn handle_multiple_cursors( // Move to the next secondary cursor ptr += 1; } - ged!(mut &editor).doc_mut().secondary_cursors = secondary_cursors; + ged!(mut &editor).try_doc_mut().unwrap().secondary_cursors = secondary_cursors; ged!(mut &editor).macro_man.playing = false; // Restore back to the state of the document beforehand // TODO: calculate char_ptr and old_cursor too - ged!(mut &editor).doc_mut().cursor = cursor; - let char_ptr = ged!(&editor).doc().character_idx(&cursor.loc); - ged!(mut &editor).doc_mut().char_ptr = char_ptr; - ged!(mut &editor).doc_mut().old_cursor = cursor.loc.x; - ged!(mut &editor).doc_mut().cancel_selection(); + ged!(mut &editor).try_doc_mut().unwrap().cursor = cursor; + let char_ptr = ged!(&editor).try_doc().unwrap().character_idx(&cursor.loc); + ged!(mut &editor).try_doc_mut().unwrap().char_ptr = char_ptr; + ged!(mut &editor).try_doc_mut().unwrap().old_cursor = cursor.loc.x; + ged!(mut &editor).try_doc_mut().unwrap().cancel_selection(); Ok(()) } diff --git a/src/editor/documents.rs b/src/editor/documents.rs index 4cea45bc..9551c016 100644 --- a/src/editor/documents.rs +++ b/src/editor/documents.rs @@ -19,6 +19,8 @@ pub enum FileLayout { Atom(Vec, usize), /// Placeholder for an empty file split None, + /// Representing a file tree + FileTree, } impl Default for FileLayout { @@ -38,8 +40,10 @@ impl FileLayout { pub fn span(&self, idx: Vec, size: Size, at: Loc) -> Span { match self { Self::None => vec![], - // Atom: stretches from starting position through to end of it's container - Self::Atom(_, _) => vec![(idx, at.y..at.y + size.h, at.x..at.x + size.w)], + // Atom and file trees: stretch from starting position through to end of their containers + Self::Atom(_, _) | Self::FileTree => { + vec![(idx, at.y..at.y + size.h, at.x..at.x + size.w)] + } // SideBySide: distributes available container space to each sub-layout Self::SideBySide(layouts) => { let mut result = vec![]; @@ -153,17 +157,27 @@ impl FileLayout { /// Work out how many files are currently open pub fn len(&self) -> usize { match self { - Self::None => 0, + Self::None | Self::FileTree => 0, Self::Atom(containers, _) => containers.len(), Self::SideBySide(layouts) => layouts.iter().map(|(layout, _)| layout.len()).sum(), Self::TopToBottom(layouts) => layouts.iter().map(|(layout, _)| layout.len()).sum(), } } + /// Work out how many atoms are currently open + pub fn n_atoms(&self) -> usize { + match self { + Self::None | Self::FileTree => 0, + Self::Atom(_, _) => 1, + Self::SideBySide(layouts) => layouts.iter().map(|(layout, _)| layout.n_atoms()).sum(), + Self::TopToBottom(layouts) => layouts.iter().map(|(layout, _)| layout.n_atoms()).sum(), + } + } + /// Find a file container location from it's path pub fn find(&self, idx: Vec, path: &str) -> Option<(Vec, usize)> { match self { - Self::None => None, + Self::None | Self::FileTree => None, Self::Atom(containers, _) => { // Scan this atom for any documents for (ptr, container) in containers.iter().enumerate() { @@ -193,7 +207,7 @@ impl FileLayout { /// Get the `FileLayout` at a certain index pub fn get_raw(&self, mut idx: Vec) -> Option<&FileLayout> { match self { - Self::None | Self::Atom(_, _) => Some(self), + Self::None | Self::Atom(_, _) | Self::FileTree => Some(self), Self::SideBySide(layouts) => { if idx.is_empty() { Some(self) @@ -219,7 +233,7 @@ impl FileLayout { Some(self) } else { match self { - Self::None | Self::Atom(_, _) => Some(self), + Self::None | Self::Atom(_, _) | Self::FileTree => Some(self), Self::SideBySide(layouts) => { let subidx = idx.remove(0); layouts.get_mut(subidx)?.0.get_raw_mut(idx) @@ -235,7 +249,7 @@ impl FileLayout { /// Get the `FileLayout` at a certain index pub fn set(&mut self, mut idx: Vec, fl: FileLayout) { match self { - Self::None | Self::Atom(_, _) => *self = fl, + Self::None | Self::Atom(_, _) | Self::FileTree => *self = fl, Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { if idx.is_empty() { *self = fl; @@ -250,7 +264,7 @@ impl FileLayout { /// Given an index, find the file containers in the tree pub fn get_atom(&self, mut idx: Vec) -> Option<(&[FileContainer], usize)> { match self { - Self::None => None, + Self::None | Self::FileTree => None, Self::Atom(containers, ptr) => Some((containers, *ptr)), Self::SideBySide(layouts) => { let subidx = idx.remove(0); @@ -269,7 +283,7 @@ impl FileLayout { mut idx: Vec, ) -> Option<(&mut Vec, &mut usize)> { match self { - Self::None => None, + Self::None | Self::FileTree => None, Self::Atom(ref mut containers, ref mut ptr) => Some((containers, ptr)), Self::SideBySide(layouts) => { let subidx = idx.remove(0); @@ -302,7 +316,7 @@ impl FileLayout { /// In the currently active atom, move to a different document pub fn move_to(&mut self, mut idx: Vec, ptr: usize) { match self { - Self::None => (), + Self::None | Self::FileTree => (), Self::Atom(_, ref mut old_ptr) => *old_ptr = ptr, Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { let subidx = idx.remove(0); @@ -320,6 +334,24 @@ impl FileLayout { } } + /// Remove any empty atoms + pub fn clean_up_multis(&mut self, mut idx: Vec) -> Vec { + // Continue checking for redundant sidebyside / toptobottom + while let Some(redundant_idx) = self.redundant_multis(vec![]) { + let multi = self.get_raw_mut(redundant_idx.clone()); + if let Some(layout) = multi { + if let Self::SideBySide(layouts) | Self::TopToBottom(layouts) = layout { + *layout = layouts[0].0.clone(); + if idx.starts_with(&redundant_idx) { + idx.remove(redundant_idx.len()); + return idx; + } + } + } + } + idx + } + /// Remove a certain index from this tree #[allow(clippy::cast_precision_loss)] pub fn remove(&mut self, at: Vec) { @@ -329,7 +361,7 @@ impl FileLayout { // Determine behaviour based on parent if let Some(parent) = self.get_raw_mut(at_parent) { match parent { - Self::None | Self::Atom(_, _) => unreachable!(), + Self::None | Self::Atom(_, _) | Self::FileTree => unreachable!(), Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { // Get the proportion of what we're removing let removed_prop = layouts[within_parent].1; @@ -353,7 +385,7 @@ impl FileLayout { /// Traverse the tree and return a list of indices to empty atoms pub fn empty_atoms(&self, at: Vec) -> Option> { match self { - Self::None => None, + Self::None | Self::FileTree => None, Self::Atom(fcs, _) => { if fcs.is_empty() { Some(at) @@ -378,6 +410,27 @@ impl FileLayout { } } + /// Traverse the tree and return a list of indices to redundant sidebyside/toptobottom + pub fn redundant_multis(&self, at: Vec) -> Option> { + match self { + Self::None | Self::FileTree | Self::Atom(_, _) => None, + Self::SideBySide(layouts) | Self::TopToBottom(layouts) => { + if layouts.len() == 1 { + Some(at) + } else { + for (c, layout) in layouts.iter().enumerate() { + let mut idx = at.clone(); + idx.push(c); + if let Some(result) = layout.0.redundant_multis(idx) { + return Some(result); + } + } + None + } + } + } + } + /// Find a new pointer position when something is removed pub fn new_pointer_position(&self, old: &[usize]) -> Vec { // Zoom out until a sidebyside or toptobottom is found @@ -407,6 +460,7 @@ impl FileLayout { new_ptr.push(0); Self::TopToBottom(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } + Self::FileTree => return at, }; self.set(at, new_fl); } @@ -423,6 +477,7 @@ impl FileLayout { new_ptr.push(1); Self::TopToBottom(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } + Self::FileTree => return at, }; self.set(at, new_fl); } @@ -439,6 +494,7 @@ impl FileLayout { new_ptr.push(0); Self::SideBySide(vec![(fl, 0.5), (old_fl.clone(), 0.5)]) } + Self::FileTree => return at, }; self.set(at, new_fl); } @@ -455,6 +511,7 @@ impl FileLayout { new_ptr.push(1); Self::SideBySide(vec![(old_fl.clone(), 0.5), (fl, 0.5)]) } + Self::FileTree => return at, }; self.set(at, new_fl); } diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 32ea09d9..3391b741 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -8,44 +8,51 @@ use super::Editor; impl Editor { /// Execute an edit event pub fn exe(&mut self, ev: Event) -> Result<()> { - let multi_cursors = self.doc().secondary_cursors.len() > 0; - if !(self.plugin_active || self.pasting || self.macro_man.playing || multi_cursors) { - let last_ev = self.doc().event_mgmt.last_event.as_ref(); - // If last event is present and the same as this one, commit - let event_type_differs = last_ev.map(|e1| e1.same_type(&ev)) != Some(true); - // If last event is present and on a different line from the previous, commit - let event_on_different_line = last_ev.map(|e| e.loc().y == ev.loc().y) != Some(true); - // Commit if necessary - if event_type_differs || event_on_different_line { - self.doc_mut().commit(); + if self.try_doc().is_some() { + let multi_cursors = !self.try_doc().unwrap().secondary_cursors.is_empty(); + if !(self.plugin_active || self.pasting || self.macro_man.playing || multi_cursors) { + let last_ev = self.try_doc().unwrap().event_mgmt.last_event.as_ref(); + // If last event is present and the same as this one, commit + let event_type_differs = last_ev.map(|e1| e1.same_type(&ev)) != Some(true); + // If last event is present and on a different line from the previous, commit + let event_on_different_line = + last_ev.map(|e| e.loc().y == ev.loc().y) != Some(true); + // Commit if necessary + if event_type_differs || event_on_different_line { + self.try_doc_mut().unwrap().commit(); + } + } else if self.try_doc().unwrap().event_mgmt.history.is_empty() { + // If there is no initial commit and a plug-in changes things without commiting + // It can cause the initial state of the document to be lost + // This condition makes sure there is a copy to go back to if this is the case + self.try_doc_mut().unwrap().commit(); } - } else if self.doc().event_mgmt.history.is_empty() { - // If there is no initial commit and a plug-in changes things without commiting - // It can cause the initial state of the document to be lost - // This condition makes sure there is a copy to go back to if this is the case - self.doc_mut().commit(); + self.try_doc_mut().unwrap().exe(ev)?; } - self.doc_mut().exe(ev)?; Ok(()) } /// Insert a character into the document, creating a new row if editing /// on the last line of the document pub fn character(&mut self, ch: char) -> Result<()> { - if !self.doc().is_selection_empty() && !self.doc().info.read_only { - self.doc_mut().remove_selection(); - self.reload_highlight(); - } - self.new_row()?; - // Handle the character insertion - if ch == '\n' { - self.enter()?; - } else { - let loc = self.doc().char_loc(); - self.exe(Event::Insert(loc, ch.to_string()))?; - if let Some(file) = self.files.get_mut(self.ptr.clone()) { - if !file.doc.info.read_only { - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + if self.try_doc().is_some() { + let doc = self.try_doc().unwrap(); + if !doc.is_selection_empty() && !doc.info.read_only { + self.try_doc_mut().unwrap().remove_selection(); + self.reload_highlight(); + } + self.new_row()?; + // Handle the character insertion + if ch == '\n' { + self.enter()?; + } else { + let doc = self.try_doc().unwrap(); + let loc = doc.char_loc(); + self.exe(Event::Insert(loc, ch.to_string()))?; + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + if !file.doc.info.read_only { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } } } } @@ -54,20 +61,22 @@ impl Editor { /// Handle the return key pub fn enter(&mut self) -> Result<()> { - // Perform the changes - if self.doc().loc().y == self.doc().len_lines() { - // Enter pressed on the empty line at the bottom of the document - self.new_row()?; - } else { - // Enter pressed in the start, middle or end of the line - let loc = self.doc().char_loc(); - self.exe(Event::SplitDown(loc))?; - if let Some(file) = self.files.get_mut(self.ptr.clone()) { - if !file.doc.info.read_only { - let line = &file.doc.lines[loc.y + 1]; - file.highlighter.insert_line(loc.y + 1, line); - let line = &file.doc.lines[loc.y]; - file.highlighter.edit(loc.y, line); + if let Some(doc) = self.try_doc_mut() { + // Perform the changes + if doc.loc().y == doc.len_lines() { + // Enter pressed on the empty line at the bottom of the document + self.new_row()?; + } else { + // Enter pressed in the start, middle or end of the line + let loc = doc.char_loc(); + self.exe(Event::SplitDown(loc))?; + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + if !file.doc.info.read_only { + let line = &file.doc.lines[loc.y + 1]; + file.highlighter.insert_line(loc.y + 1, line); + let line = &file.doc.lines[loc.y]; + file.highlighter.edit(loc.y, line); + } } } } @@ -76,46 +85,51 @@ impl Editor { /// Handle the backspace key pub fn backspace(&mut self) -> Result<()> { - if !self.doc().is_selection_empty() && !self.doc().info.read_only { - // Removing a selection is significant and worth an undo commit - self.doc_mut().commit(); - self.doc_mut().remove_selection(); - self.reload_highlight(); - return Ok(()); - } - let mut c = self.doc().char_ptr; - let on_first_line = self.doc().loc().y == 0; - let out_of_range = self.doc().out_of_range(0, self.doc().loc().y).is_err(); - if c == 0 && !on_first_line && !out_of_range { - // Backspace was pressed on the start of the line, move line to the top - self.new_row()?; - let mut loc = self.doc().char_loc(); - let file = self.files.get_mut(self.ptr.clone()).unwrap(); - if !file.doc.info.read_only { - self.highlighter().remove_line(loc.y); + if self.try_doc().is_some() { + let doc = self.try_doc().unwrap(); + if !doc.is_selection_empty() && !doc.info.read_only { + // Removing a selection is significant and worth an undo commit + let doc = self.try_doc_mut().unwrap(); + doc.commit(); + doc.remove_selection(); + self.reload_highlight(); + return Ok(()); } - loc.y = loc.y.saturating_sub(1); - let file = self.files.get_mut(self.ptr.clone()).unwrap(); - loc.x = file.doc.line(loc.y).unwrap().chars().count(); - self.exe(Event::SpliceUp(loc))?; - let file = self.files.get_mut(self.ptr.clone()).unwrap(); - let line = &file.doc.lines[loc.y]; - if !file.doc.info.read_only { - file.highlighter.edit(loc.y, line); - } - } else if !(c == 0 && on_first_line) { - // Backspace was pressed in the middle of the line, delete the character - c = c.saturating_sub(1); - if let Some(line) = self.doc().line(self.doc().loc().y) { - if let Some(ch) = line.chars().nth(c) { - let loc = Loc { - x: c, - y: self.doc().loc().y, - }; - self.exe(Event::Delete(loc, ch.to_string()))?; - let file = self.files.get_mut(self.ptr.clone()).unwrap(); - if !file.doc.info.read_only { - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + let doc = self.try_doc().unwrap(); + let mut c = doc.char_ptr; + let on_first_line = doc.loc().y == 0; + let out_of_range = doc.out_of_range(0, doc.loc().y).is_err(); + if c == 0 && !on_first_line && !out_of_range { + // Backspace was pressed on the start of the line, move line to the top + self.new_row()?; + let mut loc = self.try_doc().unwrap().char_loc(); + let file = self.files.get_mut(self.ptr.clone()).unwrap(); + if !file.doc.info.read_only { + self.highlighter().remove_line(loc.y); + } + loc.y = loc.y.saturating_sub(1); + let file = self.files.get_mut(self.ptr.clone()).unwrap(); + loc.x = file.doc.line(loc.y).unwrap().chars().count(); + self.exe(Event::SpliceUp(loc))?; + let file = self.files.get_mut(self.ptr.clone()).unwrap(); + let line = &file.doc.lines[loc.y]; + if !file.doc.info.read_only { + file.highlighter.edit(loc.y, line); + } + } else if !(c == 0 && on_first_line) { + // Backspace was pressed in the middle of the line, delete the character + c = c.saturating_sub(1); + if let Some(line) = doc.line(doc.loc().y) { + if let Some(ch) = line.chars().nth(c) { + let loc = Loc { + x: c, + y: doc.loc().y, + }; + self.exe(Event::Delete(loc, ch.to_string()))?; + let file = self.files.get_mut(self.ptr.clone()).unwrap(); + if !file.doc.info.read_only { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } } } } @@ -125,17 +139,19 @@ impl Editor { /// Delete the character in place pub fn delete(&mut self) -> Result<()> { - let c = self.doc().char_ptr; - if let Some(line) = self.doc().line(self.doc().loc().y) { - if let Some(ch) = line.chars().nth(c) { - let loc = Loc { - x: c, - y: self.doc().loc().y, - }; - self.exe(Event::Delete(loc, ch.to_string()))?; - if let Some(file) = self.files.get_mut(self.ptr.clone()) { - if !file.doc.info.read_only { - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + if let Some(doc) = self.try_doc() { + let c = doc.char_ptr; + if let Some(line) = doc.line(doc.loc().y) { + if let Some(ch) = line.chars().nth(c) { + let loc = Loc { + x: c, + y: doc.loc().y, + }; + self.exe(Event::Delete(loc, ch.to_string()))?; + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + if !file.doc.info.read_only { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } } } } @@ -145,10 +161,14 @@ impl Editor { /// Insert a new row at the end of the document if the cursor is on it fn new_row(&mut self) -> Result<()> { - if self.doc().loc().y == self.doc().len_lines() { - self.exe(Event::InsertLine(self.doc().loc().y, String::new()))?; - if !self.doc().info.read_only { - self.highlighter().append(""); + if self.try_doc().is_some() { + let doc = self.try_doc().unwrap(); + if doc.loc().y == doc.len_lines() { + self.exe(Event::InsertLine(doc.loc().y, String::new()))?; + let doc = self.try_doc().unwrap(); + if !doc.info.read_only { + self.highlighter().append(""); + } } } Ok(()) @@ -156,13 +176,17 @@ impl Editor { /// Delete the current line pub fn delete_line(&mut self) -> Result<()> { - // Delete the line - if self.doc().loc().y < self.doc().len_lines() { - let y = self.doc().loc().y; - let line = self.doc().line(y).unwrap(); - self.exe(Event::DeleteLine(y, line))?; - if !self.doc().info.read_only { - self.highlighter().remove_line(y); + if self.try_doc().is_some() { + // Delete the line + let doc = self.try_doc().unwrap(); + if doc.loc().y < doc.len_lines() { + let y = doc.loc().y; + let line = doc.line(y).unwrap(); + self.exe(Event::DeleteLine(y, line))?; + let doc = self.try_doc().unwrap(); + if !doc.info.read_only { + self.highlighter().remove_line(y); + } } } Ok(()) @@ -170,35 +194,47 @@ impl Editor { /// Perform redo action pub fn redo(&mut self) -> Result<()> { - let result = Ok(self.doc_mut().redo()?); - self.reload_highlight(); - result + if let Some(doc) = self.try_doc_mut() { + doc.redo()?; + self.reload_highlight(); + } + Ok(()) } /// Perform undo action pub fn undo(&mut self) -> Result<()> { - let result = Ok(self.doc_mut().undo()?); - self.reload_highlight(); - result + if let Some(doc) = self.try_doc_mut() { + doc.undo()?; + self.reload_highlight(); + } + Ok(()) } /// Copy the selected text pub fn copy(&mut self) -> Result<()> { - let selected_text = self.doc().selection_text(); - self.terminal.copy(&selected_text) + if let Some(doc) = self.try_doc() { + let selected_text = doc.selection_text(); + self.terminal.copy(&selected_text) + } else { + Ok(()) + } } /// Cut the selected text pub fn cut(&mut self) -> Result<()> { - self.copy()?; - self.doc_mut().remove_selection(); - self.reload_highlight(); + if self.try_doc().is_some() { + self.copy()?; + self.try_doc_mut().unwrap().remove_selection(); + 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); + if let Some(doc) = self.try_doc() { + let line = doc.line(y).unwrap_or_default(); + self.highlighter().edit(y, &line); + } } } diff --git a/src/editor/filetree.rs b/src/editor/filetree.rs new file mode 100644 index 00000000..b73dd23a --- /dev/null +++ b/src/editor/filetree.rs @@ -0,0 +1,662 @@ +/// Utilities for handling the file tree +use crate::config::FileTree as CfgFT; +use crate::editor::FileLayout; +use crate::ui::size; +use crate::{config, Editor, Feedback, FileTypes, OxError, Result}; +use kaolinite::utils::{file_or_dir, get_cwd, get_file_name}; +use std::path::{Path, PathBuf}; + +/// How parts of a file tree are stored +/// (Padding, Icon, Icon Color, File Name) +pub type FTParts = Vec<(usize, String, Option, String)>; + +/// The backend of a file tree - stores the structure of the files and directories +#[derive(Debug, Clone)] +pub enum FileTree { + /// Represents a file + File { path: String }, + /// Represents a directory + Dir { + path: String, + /// NOTE: when files is None, it means it has been unexpanded + /// directories lazily expand, only when the user requests them to be opened + files: Option>, + }, +} + +impl FileTree { + /// Build a file tree from a directory + pub fn build(dir: &str) -> Result { + // Ensure we have the absolute path + let root = std::fs::canonicalize(dir)?; + let mut result = Self::build_shallow(&root)?; + result.sort(); + Ok(result) + } + + /// Expands into a directory + fn build_shallow(path: &PathBuf) -> Result { + if path.is_file() { + Ok(Self::File { + path: Self::path_to_string(path), + }) + } else if path.is_dir() { + let mut files = vec![]; + for entry in std::fs::read_dir(path)? { + let entry = entry?; + if entry.path().is_file() { + files.push(Self::File { + path: Self::path_to_string(&entry.path()), + }); + } else if entry.path().is_dir() { + files.push(Self::Dir { + path: Self::path_to_string(&entry.path()), + files: None, + }); + } + } + Ok(Self::Dir { + path: Self::path_to_string(path), + files: Some(files), + }) + } else { + Err(OxError::InvalidPath) + } + } + + /// Takes a path and turns it into a string + fn path_to_string(path: &Path) -> String { + let mut path = path.to_string_lossy().to_string(); + if path.starts_with("\\\\?\\") { + path = path[4..].to_string(); + } + path + } + + /// Search for and retrieve a mutable reference to a node + pub fn get_mut(&mut self, needle: &str) -> Option<&mut Self> { + match self { + Self::File { path } => { + if needle == path { + // Match found! + Some(self) + } else { + // No match + None + } + } + Self::Dir { path, .. } => { + if needle == path { + // This directory is what we're searching for + Some(self) + } else if let Self::Dir { + files: Some(files), .. + } = self + { + // Not directly what we're looking for, let's go deeper + for file in files { + if let Some(result) = file.get_mut(needle) { + // Found it! Return upwards + return Some(result); + } + } + // None of the files match up + None + } else { + // Dead end + None + } + } + } + } + + /// Expand a directory downwards + pub fn expand(&mut self) { + if let Self::Dir { path, .. } = self { + // Expand this directory + if let Ok(root) = std::fs::canonicalize(path) { + if let Ok(mut expanded) = Self::build_shallow(&root) { + expanded.sort(); + *self = expanded; + } + } + } + } + + /// Sort a file tree to have directories and files separated and ordered alphabetically + fn sort(&mut self) { + match self { + Self::File { .. } => (), + Self::Dir { files, .. } => { + // Sort child directories + if let Some(files) = files { + for file in files.iter_mut() { + file.sort(); + } + + // Sort this directory + files.sort_by(|a, b| { + let a_is_hidden = a.is_hidden(); + let b_is_hidden = b.is_hidden(); + let a_is_dir = matches!(a, FileTree::Dir { .. }); + let b_is_dir = matches!(b, FileTree::Dir { .. }); + + // Directories come first + match (a_is_hidden, b_is_hidden) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => { + // If both are the same hidden status, directories come first + match (a_is_dir, b_is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => { + // If both are the same type, compare by path + let a_path = match a { + FileTree::File { path } + | FileTree::Dir { path, .. } => path, + }; + let b_path = match b { + FileTree::File { path } + | FileTree::Dir { path, .. } => path, + }; + a_path.cmp(b_path) + } + } + } + } + }); + } + } + } + } + + /// Work out if this node is hidden or not + fn is_hidden(&self) -> bool { + let path = match self { + Self::File { path } | Self::Dir { path, .. } => path, + }; + get_file_name(path).is_some_and(|name| name.starts_with('.')) + } + + /// Get a language-related icon for this node + fn lang_icon(&self, fts: &FileTypes, config: &CfgFT) -> Option<(String, String)> { + if config.language_icons { + let path = match self { + Self::File { path } | Self::Dir { path, .. } => path, + }; + if let Some(ft) = fts.identify_from_path(path) { + return Some((ft.icon, ft.color)); + } + } + None + } + + /// Get the appropriate icon + fn icon(&self, fts: &FileTypes, config: &CfgFT) -> (String, Option) { + let is_file = match self { + Self::File { .. } => true, + Self::Dir { .. } => false, + }; + let is_expanded = match self { + Self::File { .. } => false, + Self::Dir { files, .. } => files.is_some(), + }; + let is_hidden = self.is_hidden(); + match (self.lang_icon(fts, config), is_file, is_hidden, is_expanded) { + // Language specific icons + (Some((icon, colour)), _, _, _) => (icon + " ", Some(colour)), + // Closed folders + (_, false, false, false) => ("σ°‰– ".to_string(), None), + (_, false, true, false) => ("󱞞 ".to_string(), None), + // Opened folders + (_, false, _, true) => ("󰷏 ".to_string(), None), + // Files + (_, true, false, _) => ("󰈀 ".to_string(), None), + (_, true, true, _) => ("σ°˜“ ".to_string(), None), + } + } + + /// Work out if this node is selected + pub fn is_selected(&self, selection: &str) -> bool { + match self { + Self::File { path } | Self::Dir { path, .. } => path == selection, + } + } + + /// Display this file tree + pub fn display(&self, sel: &str, fts: &FileTypes, cfg: &CfgFT) -> (FTParts, Option) { + let mut result = self.display_recursive(sel, fts, cfg); + result + .0 + .insert(0, (0, "σ°‰– ".to_string(), None, "..".to_string())); + if sel == ".." { + result.1 = Some(0); + } else if let Some(ref mut at) = result.1 { + *at += 1; + } + result + } + + /// Display this file tree (recursive) + pub fn display_recursive( + &self, + sel: &str, + fts: &FileTypes, + cfg: &CfgFT, + ) -> (FTParts, Option) { + let icons = cfg.icons; + match self { + Self::File { path } => { + let (icon, icon_color) = if icons { + self.icon(fts, cfg) + } else { + (String::new(), None) + }; + let file_name = get_file_name(path).unwrap_or(path.to_string()); + ( + vec![(0, icon, icon_color, file_name)], + if self.is_selected(sel) { Some(0) } else { None }, + ) + } + Self::Dir { path, files } => { + let mut result = vec![]; + let mut at = None; + // Write self + let (icon, icon_color) = if icons { + self.icon(fts, cfg) + } else { + (String::new(), None) + }; + let file_name = get_file_name(path).unwrap_or(path.to_string()); + result.push((0, icon, icon_color, file_name)); + if self.is_selected(sel) { + at = Some(result.len().saturating_sub(1)); + } + // Write child nodes + if let Some(files) = files { + for file in files { + let (sub_display, sub_at) = file.display_recursive(sel, fts, cfg); + for (c, s) in sub_display.iter().enumerate() { + let mut s = s.clone(); + s.0 += 1; + result.push(s); + if let Some(sub_at) = sub_at { + if sub_at == c { + at = Some(result.len().saturating_sub(1)); + } + } + } + } + } + (result, at) + } + } + } + + /// Find the file path at a certain index + pub fn flatten(&self) -> Vec { + let mut result = self.flatten_recursive(); + result.insert(0, "..".to_string()); + result + } + + /// Find the file path at a certain index (recursive) + pub fn flatten_recursive(&self) -> Vec { + match self { + Self::File { path } => vec![path.to_string()], + Self::Dir { path, files } => { + let mut result = vec![]; + result.push(path.to_string()); + if let Some(files) = files { + for file in files { + result.append(&mut file.flatten_recursive()); + } + } + result + } + } + } + + /// Expand this file tree upwards towards parent + pub fn open_parent(&self) -> Result { + if let Self::Dir { path, files } = self { + let parent_path = format!("{path}/.."); + let mut parent = Self::build(&parent_path)?; + if let Some(Self::Dir { files: child, .. }) = parent.get_mut(path) { + child.clone_from(&files.clone()); + } + Ok(parent) + } else { + Err(OxError::InvalidPath) + } + } + + /// Get all directories that have been expanded + pub fn get_expanded(&mut self) -> Vec { + let mut result = vec![]; + match self { + Self::File { .. } => (), + Self::Dir { files, path } => { + if let Some(files) = files { + // Pre-order traversal - very important this remains! + result.push(path.clone()); + for file in files { + result.append(&mut file.get_expanded()); + } + } + } + } + result + } + + /// Refresh all open directories + pub fn refresh(&mut self) { + if let Self::Dir { + files: Some(_), + path, + } = self + { + // Rebuild the tree + if let Ok(mut result) = Self::build(path) { + // Re expand the tree + let expanded = self.get_expanded(); + for dir in expanded { + if let Some(dir) = result.get_mut(&dir) { + dir.expand(); + } + } + *self = result; + } + } + } +} + +impl Editor { + /// Open the file tree + #[allow(clippy::cast_precision_loss)] + pub fn open_file_tree(&mut self) { + if !self.file_tree_is_open() { + // Calculate display proportions + let total_width = size().map(|s| s.w as f64).unwrap_or(1.0); + let width = config!(self.config, file_tree).width as f64 / total_width; + let other = 1.0 - width as f64; + // Set up file tree values + self.old_ptr = self.ptr.clone(); + if let Some(cwd) = get_cwd() { + if let Ok(ft) = FileTree::build(&cwd) { + self.file_tree = Some(ft); + self.file_tree_selection = Some(cwd); + } + } + // Wrap existing file layout in new file layout + self.files = FileLayout::SideBySide(vec![ + (FileLayout::FileTree, width), + (self.files.clone(), other), + ]); + self.ptr = vec![0]; + } + } + + /// Close the file tree + pub fn close_file_tree(&mut self) { + if let Some(FileLayout::SideBySide(layouts)) = self.files.get_raw(vec![]) { + let in_file_tree = matches!( + self.files.get_raw(self.ptr.clone()), + Some(FileLayout::FileTree) + ); + // Locate where the file tree is + let ftp = layouts + .iter() + .position(|(l, _)| matches!(l, FileLayout::FileTree)); + if let Some(at) = ftp { + // Delete the file tree + self.files.remove(vec![at]); + // Clear up any leftovers sidebyside + if let FileLayout::SideBySide(layouts) = &self.files { + if layouts.len() == 1 { + // Remove leftover + self.files = layouts[0].0.clone(); + } + } + // Reset pointer back to what it used to be IF we're in the file tree + if in_file_tree { + self.ptr = self.old_ptr.clone(); + } else if !self.ptr.is_empty() { + // If we're outside the file tree + // just take the existing pointer and remove file tree aspect + self.ptr.remove(0); + } + } + } + } + + /// Toggle the file tree + pub fn toggle_file_tree(&mut self) { + if self.file_tree_is_open() { + self.close_file_tree(); + } else { + self.open_file_tree(); + } + } + + /// Determine whether the file tree is open + pub fn file_tree_is_open(&self) -> bool { + if let Some(FileLayout::SideBySide(layouts)) = self.files.get_raw(vec![]) { + layouts + .iter() + .any(|(layout, _)| matches!(layout, FileLayout::FileTree)) + } else { + false + } + } + + /// Move file tree selection upwards + pub fn file_tree_select_up(&mut self) { + if let Some(ref mut fts) = self.render_cache.file_tree_selection { + // Move up a file (in the render cache) + *fts = fts.saturating_sub(1); + // Move up a file (in the backend) + let flat = self + .file_tree + .as_ref() + .map(FileTree::flatten) + .unwrap_or_default(); + let new_path = flat.get(*fts); + self.file_tree_selection = new_path.cloned(); + } + } + + /// Move file tree selection upwards + pub fn file_tree_select_down(&mut self) { + if let Some(ref mut fts) = self.render_cache.file_tree_selection { + let flat = self + .file_tree + .as_ref() + .map(FileTree::flatten) + .unwrap_or_default(); + if *fts + 1 < flat.len() { + // Move up a file (in the render cache) + *fts += 1; + // Move up a file (in the backend) + let new_path = flat.get(*fts); + self.file_tree_selection = new_path.cloned(); + } + } + } + + /// Open a certain file / directory in a file tree + pub fn file_tree_open_node(&mut self) -> Result<()> { + if let Some(file_name) = &self.file_tree_selection.clone() { + if file_name == ".." { + self.file_tree_open_parent()?; + } else { + match file_or_dir(file_name) { + "file" => self.file_tree_open_file()?, + "directory" => self.file_tree_toggle_dir(), + _ => (), + } + } + } + Ok(()) + } + + /// Open a file from the file tree + pub fn file_tree_open_file(&mut self) -> Result<()> { + // Work out how to behave when opening files + let move_focus = config!(self.config, file_tree).move_focus_to_file; + if let Some(file_name) = &self.file_tree_selection.clone() { + // Restore to old pointer to open + let ptr_cache = self.ptr.clone(); + let mut temp = self.old_ptr.clone(); + temp.insert(0, 1); + self.ptr = temp; + // Perform open operation + self.open(file_name)?; + self.next(); + self.update_cwd(); + // If we don't want to move focus, then move focus back to the file tree + if !move_focus { + self.ptr = ptr_cache; + } + } + Ok(()) + } + + /// Toggle a directory to expand or contract + pub fn file_tree_toggle_dir(&mut self) { + if let Some(ref mut file_tree) = &mut self.file_tree { + if let Some(file_name) = self.file_tree_selection.as_ref() { + if let Some(node) = file_tree.get_mut(file_name) { + if let FileTree::Dir { files, .. } = node { + if files.is_some() { + // Clear expansion if already expanded + *files = None; + } else { + // Expand if not already expanded + node.expand(); + } + } + } + } + } + } + + /// Expand this tree up to the parent + pub fn file_tree_open_parent(&mut self) -> Result<()> { + if let Some(ref mut file_tree) = &mut self.file_tree { + self.file_tree = Some(file_tree.open_parent()?); + } + Ok(()) + } + + /// Expand this tree up to the parent + pub fn file_tree_move_into(&mut self) { + if let Some(ref mut file_tree) = &mut self.file_tree { + if let Some(file_name) = self.file_tree_selection.as_ref() { + if let Some(node) = file_tree.get_mut(file_name) { + if let FileTree::Dir { files, .. } = node { + if files.is_none() { + // Expand if not already expanded + node.expand(); + } + *file_tree = node.clone(); + } + } + } + } + } + + /// Move to the top of the file tree + pub fn file_tree_move_to_top(&mut self) { + if let Some(ref mut file_tree) = &mut self.file_tree { + self.file_tree_selection = file_tree + .flatten() + .first() + .map(std::string::ToString::to_string); + } + } + + /// Move to the bottom of the file tree + pub fn file_tree_move_to_bottom(&mut self) { + if let Some(ref mut file_tree) = &mut self.file_tree { + self.file_tree_selection = file_tree + .flatten() + .last() + .map(std::string::ToString::to_string); + } + } + + /// Create a new file / folder + pub fn file_tree_new(&mut self) -> Result<()> { + let path = self.path_prompt()?; + if path.ends_with(std::path::MAIN_SEPARATOR) { + std::fs::create_dir_all(path)?; + self.file_tree_refresh(); + self.feedback = Feedback::Info("Folder created".to_string()); + } else { + std::fs::File::create(path)?; + self.file_tree_refresh(); + self.feedback = Feedback::Info("File created".to_string()); + } + Ok(()) + } + + /// Delete a file + pub fn file_tree_delete(&mut self) -> Result<()> { + if let Some(file_name) = &self.file_tree_selection.clone() { + let prompt = + self.prompt(format!("Are you sure you wish to delete {file_name} (y/n)"))?; + if prompt == "y" { + if file_or_dir(file_name) == "file" { + std::fs::remove_file(file_name)?; + self.file_tree_refresh(); + self.file_tree_select_up(); + self.feedback = Feedback::Info("File deleted".to_string()); + } else { + self.feedback = Feedback::Error( + "Folders can't be deleted in Ox: too dangerous".to_string(), + ); + } + } + } + Ok(()) + } + + /// Copy a file + pub fn file_tree_copy(&mut self) -> Result<()> { + if let Some(old_file) = &self.file_tree_selection.clone() { + let path = self.path_prompt()?; + if file_or_dir(old_file) == "file" { + std::fs::copy(old_file, path)?; + self.file_tree_refresh(); + self.feedback = Feedback::Info("File copied".to_string()); + } else { + self.feedback = Feedback::Error("Not a file".to_string()); + } + } + Ok(()) + } + + /// Move (or rename) a file / folder + pub fn file_tree_move(&mut self) -> Result<()> { + if let Some(old_file) = &self.file_tree_selection.clone() { + let path = self.path_prompt()?; + std::fs::rename(old_file, path.clone())?; + self.file_tree_refresh(); + if file_or_dir(&path) == "file" { + self.feedback = Feedback::Info("File moved".to_string()); + } else if file_or_dir(&path) == "directory" { + self.feedback = Feedback::Info("Folder moved".to_string()); + } + } + Ok(()) + } + + /// Refresh the file tree + pub fn file_tree_refresh(&mut self) { + if let Some(ref mut file_tree) = self.file_tree { + file_tree.refresh(); + } + } +} diff --git a/src/editor/filetypes.rs b/src/editor/filetypes.rs index 98ca4011..feacf8e9 100644 --- a/src/editor/filetypes.rs +++ b/src/editor/filetypes.rs @@ -1,5 +1,5 @@ -use crate::config; /// Tools for managing and identifying file types +use crate::config; use crate::editor::Config; use kaolinite::utils::get_file_name; use kaolinite::Document; @@ -33,6 +33,19 @@ impl FileTypes { None } + pub fn identify_from_path(&self, path: &str) -> Option { + if let Some(e) = Path::new(&path).extension() { + let file_name = get_file_name(path).unwrap_or_default(); + let extension = e.to_str().unwrap_or_default().to_string(); + for t in &self.types { + if t.fits(&extension, &file_name, "") { + return Some(t.clone()); + } + } + } + None + } + pub fn get_name(&self, name: &str) -> Option { self.types.iter().find(|t| t.name == name).cloned() } @@ -51,6 +64,8 @@ pub struct FileType { pub extensions: Vec, /// The modelines that files of this type have pub modelines: Vec, + /// The colour associated with this file type + pub color: String, } impl Default for FileType { @@ -61,6 +76,7 @@ impl Default for FileType { files: vec![], extensions: vec![], modelines: vec![], + color: "grey".to_string(), } } } diff --git a/src/editor/interface.rs b/src/editor/interface.rs index 3600499d..9031bfad 100644 --- a/src/editor/interface.rs +++ b/src/editor/interface.rs @@ -1,6 +1,6 @@ -use crate::config::SyntaxHighlighting as SH; /// Functions for rendering the UI -use crate::editor::FileLayout; +use crate::config::SyntaxHighlighting as SH; +use crate::editor::{FTParts, FileLayout}; use crate::error::{OxError, Result}; use crate::events::wait_for_event_hog; use crate::ui::{key_event, size, Feedback}; @@ -24,6 +24,8 @@ pub struct RenderCache { pub help_message: Vec<(bool, String)>, pub help_message_width: usize, pub help_message_span: Range, + pub file_tree: FTParts, + pub file_tree_selection: Option, } impl Editor { @@ -51,6 +53,18 @@ impl Editor { let help_start = (size.h / 2).saturating_sub(help_length / 2) + 1; let help_end = help_start + help_length; self.render_cache.help_message_span = help_start..help_end + 1; + // Calculate file tree display representation + let fts = &config!(self.config, document).file_types; + let ft_config = &config!(self.config, file_tree); + if let Some(file_tree) = self.file_tree.as_ref() { + let (files, sel) = file_tree.display( + self.file_tree_selection.as_ref().unwrap_or(&String::new()), + fts, + ft_config, + ); + self.render_cache.file_tree = files; + self.render_cache.file_tree_selection = sel; + } } /// Render a specific line @@ -65,6 +79,10 @@ impl Editor { let mut accounted_for = 0; // Render each component of this line for (c, (fc, rows, range)) in fcs.iter().enumerate() { + let in_file_tree = matches!( + self.files.get_raw(fc.to_owned()), + Some(FileLayout::FileTree) + ); // Check if we have encountered an area of discontinuity in the line if range.start != accounted_for { // Discontinuity detected, fill with vertical bar! @@ -93,7 +111,10 @@ impl Editor { let length = range.end.saturating_sub(range.start); let height = rows.end.saturating_sub(rows.start); let rel_y = y.saturating_sub(rows.start); - if y == rows.start && tab_line_enabled { + if in_file_tree { + // Part of file tree! + result += &self.render_file_tree(y, length)?; + } else if y == rows.start && tab_line_enabled { // Tab line result += &self.render_tab_line(fc, lua, length)?; } else if y == rows.end.saturating_sub(1) { @@ -201,13 +222,19 @@ impl Editor { /// Function to calculate the cursor's position on screen pub fn cursor_position(&self) -> Option { - let Loc { x, y } = self.doc().cursor_loc_in_screen()?; - for (ptr, rows, cols) in &self.render_cache.span { - if ptr == &self.ptr { - return Some(Loc { - x: cols.start + x + self.dent(), - y: rows.start + y + self.push_down, - }); + let in_file_tree = matches!( + self.files.get_raw(self.ptr.clone()), + Some(FileLayout::FileTree) + ); + if !in_file_tree { + let Loc { x, y } = self.try_doc().unwrap().cursor_loc_in_screen()?; + for (ptr, rows, cols) in &self.render_cache.span { + if ptr == &self.ptr { + return Some(Loc { + x: cols.start + x + self.dent(), + y: rows.start + y + self.push_down, + }); + } } } None @@ -238,7 +265,7 @@ impl Editor { // Refuse to render help message on splits - awkward edge case let help_message_here = config!(self.config, help_message).enabled && self.render_cache.help_message_span.contains(&y) - && self.files.len() == 1; + && self.files.n_atoms() == 1; // Render short of the help message let mut total_width = if help_message_here { self.render_cache.help_message_width @@ -522,6 +549,61 @@ impl Editor { Ok(content) } + /// Render a line in the file tree + #[allow(clippy::similar_names)] + fn render_file_tree(&mut self, y: usize, length: usize) -> Result { + let selected = self.render_cache.file_tree_selection == Some(y); + let ft_bg = Bg(config!(self.config, colors).file_tree_bg.to_color()?); + let ft_fg = Fg(config!(self.config, colors).file_tree_fg.to_color()?); + let ft_selection_bg = Bg(config!(self.config, colors) + .file_tree_selection_bg + .to_color()?); + let ft_selection_fg = Fg(config!(self.config, colors) + .file_tree_selection_fg + .to_color()?); + let ft_colors = config!(self.config, colors); + // Perform the rendering + let mut total_length = 0; + let line = self.render_cache.file_tree.get(y); + let mut line = if let Some((padding, icon, icon_colour, name)) = line { + total_length = padding * 2 + width(icon, 4) + width(name, 4); + if let (Some(colour), false) = (icon_colour, selected) { + let colour = Fg(match colour.as_str() { + "red" => ft_colors.file_tree_red.to_color()?, + "orange" => ft_colors.file_tree_orange.to_color()?, + "yellow" => ft_colors.file_tree_yellow.to_color()?, + "green" => ft_colors.file_tree_green.to_color()?, + "lightblue" => ft_colors.file_tree_lightblue.to_color()?, + "darkblue" => ft_colors.file_tree_darkblue.to_color()?, + "purple" => ft_colors.file_tree_purple.to_color()?, + "pink" => ft_colors.file_tree_pink.to_color()?, + "brown" => ft_colors.file_tree_brown.to_color()?, + "grey" => ft_colors.file_tree_grey.to_color()?, + _ => Color::White, + }); + format!("{}{colour}{icon}{ft_fg}{name}", " ".repeat(*padding)) + } else { + format!("{}{icon}{name}", " ".repeat(*padding)) + } + } else { + String::new() + }; + while total_length > length { + if let Some(ch) = line.pop() { + total_length -= width_char(&ch, 4); + } else { + break; + } + } + line += &" ".repeat(length.saturating_sub(total_length)); + // Return result + if selected { + Ok(format!("{ft_selection_bg}{ft_selection_fg}{line}")) + } else { + Ok(format!("{ft_bg}{ft_fg}{line}")) + } + } + /// Display a prompt in the document pub fn prompt>(&mut self, prompt: S) -> Result { let prompt = prompt.into(); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 076c6456..7a733cc8 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -7,7 +7,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, get_file_name}; +use kaolinite::utils::{file_or_dir, get_absolute_path, get_file_name}; use kaolinite::{Document, Loc}; use mlua::{Error as LuaError, Lua}; use std::env; @@ -19,6 +19,7 @@ use synoptic::Highlighter; mod cursor; mod documents; mod editing; +mod filetree; mod filetypes; mod interface; mod macros; @@ -27,6 +28,7 @@ mod scanning; pub use cursor::{allowed_by_multi_cursor, handle_multiple_cursors}; pub use documents::{FileContainer, FileLayout}; +pub use filetree::{FTParts, FileTree}; pub use filetypes::{FileType, FileTypes}; pub use interface::RenderCache; pub use macros::MacroMan; @@ -70,6 +72,12 @@ pub struct Editor { pub macro_man: MacroMan, /// Render cache pub render_cache: RenderCache, + /// For storing the current file tree value + pub file_tree: Option, + /// The selected file in the file tree + pub file_tree_selection: Option, + /// For caching a pointer to go back to when in a file tree + pub old_ptr: Vec, } impl Editor { @@ -95,6 +103,9 @@ impl Editor { alt_click_state: None, macro_man: MacroMan::default(), render_cache: RenderCache::default(), + file_tree: None, + file_tree_selection: None, + old_ptr: vec![], }) } @@ -168,6 +179,13 @@ impl Editor { /// Function to create a file container pub fn open_fc(&mut self, file_name: &str) -> Result { + // Reject the opening of directories + if file_or_dir(file_name) != "file" { + return Err(OxError::Kaolinite(KError::Io(std::io::Error::new( + std::io::ErrorKind::IsADirectory, + "This is a directory, not a file", + )))); + } // Check if a file is already opened if let Some((idx, ptr)) = self.already_open(&get_absolute_path(file_name).unwrap_or_default()) @@ -252,45 +270,50 @@ impl Editor { /// save the document to the disk pub fn save(&mut self) -> Result<()> { - // Perform the save - self.doc_mut().save()?; - // All done - self.feedback = Feedback::Info("Document saved successfully".to_string()); + if let Some(doc) = self.try_doc_mut() { + // Perform the save + doc.save()?; + // All done + self.feedback = Feedback::Info("Document saved successfully".to_string()); + } Ok(()) } /// save the document to the disk at a specified path pub fn save_as(&mut self) -> Result<()> { - let file_name = self.prompt("Save as")?; - self.doc_mut().save_as(&file_name)?; - if self.doc().file_name.is_none() { - if let Some((files, _)) = self.files.get_atom_mut(self.ptr.clone()) { + if self.try_doc().is_some() { + let file_name = self.prompt("Save as")?; + self.try_doc_mut().unwrap().save_as(&file_name)?; + // If this file is currently unnamed, give it a name, syntax highlighting and a type + if self.try_doc().unwrap().file_name.is_none() { let tab_width = config!(self.config, document).tab_width; - let file = files.last_mut().unwrap(); - // Set the file name - file.doc.file_name = Some(file_name.clone()); - // Update the file type - file.file_type = config!(self.config, document) - .file_types - .identify(&mut file.doc); - // Reattach an appropriate highlighter - let highlighter = file - .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); - // Set up to date with disk - file.doc.event_mgmt.force_not_with_disk = false; - file.doc.event_mgmt.disk_write(&file.doc.take_snapshot()); + if let Some((files, ptr)) = self.files.get_atom_mut(self.ptr.clone()) { + let file = files.get_mut(*ptr).unwrap(); + // Set the file name + file.doc.file_name = Some(file_name.clone()); + // Update the file type + file.file_type = config!(self.config, document) + .file_types + .identify(&mut file.doc); + // Reattach an appropriate highlighter + let highlighter = file + .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); + // Set up to date with disk + file.doc.event_mgmt.force_not_with_disk = false; + file.doc.event_mgmt.disk_write(&file.doc.take_snapshot()); + } } + // Commit events to event manager (for undo / redo) + self.try_doc_mut().unwrap().commit(); + // All done + self.feedback = Feedback::Info(format!("Document saved as {file_name} successfully")); } - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // All done - self.feedback = Feedback::Info(format!("Document saved as {file_name} successfully")); Ok(()) } @@ -311,6 +334,7 @@ impl Editor { pub fn quit(&mut self) -> Result<()> { // Get the atom we're currently at if let Some((fcs, ptr)) = self.files.get_atom(self.ptr.clone()) { + let last_file = fcs.len() == 1; // Remove the file that is currently open and selected let msg = "This document isn't saved, press Ctrl + Q to force quit or Esc to cancel"; let doc = &fcs[ptr].doc; @@ -319,13 +343,18 @@ impl Editor { fcs.remove(*ptr); self.prev(); } - // Clean up the file structure - self.files.clean_up(); - // Find a new pointer position - self.ptr = self.files.new_pointer_position(&self.ptr); + // Perform cleanup / pointer reassignment if this atom is now empty + if last_file { + // Clean up the file structure + self.files.clean_up(); + // Find a new pointer position + self.ptr = self.files.new_pointer_position(&self.ptr); + // Clean up the redundant sidebyside/toptobottom + self.ptr = self.files.clean_up_multis(self.ptr.clone()); + } } // If there are no longer any active atoms, quit the entire editor - self.active = !matches!(self.files, FileLayout::None); + self.active = !matches!(self.files, FileLayout::None | FileLayout::FileTree); Ok(()) } @@ -351,10 +380,12 @@ impl Editor { /// Updates the current working directory of the editor pub fn update_cwd(&self) { - if let Some(name) = get_absolute_path(&self.doc().file_name.clone().unwrap_or_default()) { - let file = Path::new(&name); - if let Some(cwd) = file.parent() { - let _ = env::set_current_dir(cwd); + if let Some(doc) = self.try_doc() { + if let Some(name) = &doc.file_name { + let file = Path::new(&name); + if let Some(cwd) = file.parent() { + let _ = env::set_current_dir(cwd); + } } } } @@ -376,16 +407,6 @@ impl Editor { &mut self.files.get_atom_mut(self.ptr.clone()).unwrap().0[idx].doc } - /// Gets a reference to the current document - pub fn doc(&self) -> &Document { - &self.files.get(self.ptr.clone()).unwrap().doc - } - - /// Gets a mutable reference to the current document - pub fn doc_mut(&mut self) -> &mut Document { - &mut self.files.get_mut(self.ptr.clone()).unwrap().doc - } - /// Gets the number of documents currently open pub fn doc_len(&mut self) -> usize { self.files.get_atom(self.ptr.clone()).unwrap().0.len() @@ -426,7 +447,7 @@ impl Editor { match event { CEvent::Key(key) => self.handle_key_event(key.modifiers, key.code)?, CEvent::Resize(_, _) => self.handle_resize(lua)?, - CEvent::Mouse(mouse_event) => self.handle_mouse_event(lua, mouse_event), + CEvent::Mouse(mouse_event) => self.handle_mouse_event(lua, mouse_event)?, CEvent::Paste(text) => self.handle_paste(&text)?, _ => (), } @@ -435,24 +456,46 @@ impl Editor { /// Handle key event pub fn handle_key_event(&mut self, modifiers: KMod, code: KCode) -> Result<()> { - // Check period of inactivity - let end = Instant::now(); - let inactivity = end.duration_since(self.last_active).as_millis() as usize; - // Commit if over user-defined period of inactivity - if inactivity > config!(self.config, document).undo_period * 1000 { - self.doc_mut().commit(); - } - // Register this activity - self.last_active = Instant::now(); - // Editing - these key bindings can't be modified (only added to)! - match (modifiers, code) { - // Core key bindings (non-configurable behaviour) - (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, - (KMod::NONE, KCode::Tab) => self.handle_tab()?, - (KMod::NONE, KCode::Backspace) => self.backspace()?, - (KMod::NONE, KCode::Delete) => self.delete()?, - (KMod::NONE, KCode::Enter) => self.enter()?, - _ => (), + let in_file_tree = matches!( + self.files.get_raw(self.ptr.clone()), + Some(FileLayout::FileTree) + ); + if in_file_tree { + // File tree key behaviour + match (modifiers, code) { + (KMod::NONE, KCode::Up) => self.file_tree_select_up(), + (KMod::NONE, KCode::Down) => self.file_tree_select_down(), + (KMod::NONE, KCode::Enter) => self.file_tree_open_node()?, + (KMod::CONTROL, KCode::Up) => self.file_tree_move_to_top(), + (KMod::CONTROL, KCode::Down) => self.file_tree_move_to_bottom(), + (KMod::CONTROL, KCode::Enter) => self.file_tree_move_into(), + (KMod::NONE, KCode::Char('n')) => self.file_tree_new()?, + (KMod::NONE, KCode::Char('d')) => self.file_tree_delete()?, + (KMod::NONE, KCode::Char('m')) => self.file_tree_move()?, + (KMod::NONE, KCode::Char('c')) => self.file_tree_copy()?, + _ => (), + } + } else { + // Non file tree behaviour + // Check period of inactivity + let end = Instant::now(); + let inactivity = end.duration_since(self.last_active).as_millis() as usize; + // Commit if over user-defined period of inactivity + if inactivity > config!(self.config, document).undo_period * 1000 { + self.try_doc_mut().unwrap().commit(); + } + // Register this activity + self.last_active = Instant::now(); + // Editing - these key bindings can't be modified (only added to)! + match (modifiers, code) { + // Core key bindings (non-configurable behaviour) + (KMod::SHIFT | KMod::NONE, KCode::Char(ch)) => self.character(ch)?, + (KMod::NONE, KCode::Tab) => self.handle_tab()?, + (KMod::NONE, KCode::Backspace) => self.backspace()?, + (KMod::NONE, KCode::Delete) => self.delete()?, + (KMod::NONE, KCode::Enter) => self.enter()?, + _ => (), + } } Ok(()) } @@ -466,23 +509,25 @@ impl Editor { /// Handle paste pub fn handle_paste(&mut self, text: &str) -> Result<()> { - // If we're playing back a macro, use the last text the user copied - // (to prevent hard-coded pasting) - let text = if self.macro_man.playing { - self.terminal.last_copy.to_string() - } else { - text.to_string() - }; - // Save state before paste - self.doc_mut().commit(); - // Apply paste - self.pasting = true; - for ch in text.chars() { - self.character(ch)?; + if self.try_doc().is_some() { + // If we're playing back a macro, use the last text the user copied + // (to prevent hard-coded pasting) + let text = if self.macro_man.playing { + self.terminal.last_copy.to_string() + } else { + text.to_string() + }; + // Save state before paste + self.try_doc_mut().unwrap().commit(); + // Apply paste + self.pasting = true; + for ch in text.chars() { + self.character(ch)?; + } + self.pasting = false; + // Save state after paste + self.try_doc_mut().unwrap().commit(); } - self.pasting = false; - // Save state after paste - self.doc_mut().commit(); Ok(()) } diff --git a/src/editor/mouse.rs b/src/editor/mouse.rs index 16041df5..d3c7a55e 100644 --- a/src/editor/mouse.rs +++ b/src/editor/mouse.rs @@ -1,5 +1,5 @@ /// For handling mouse events -use crate::config; +use crate::{config, Result}; use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use kaolinite::{utils::width, Loc}; use mlua::Lua; @@ -13,6 +13,8 @@ enum MouseLocation { File(Vec, Loc), /// Where the mouse has clicked on a tab Tabs(Vec, usize), + /// Where the mouse has clicked in the file tree + FileTree(usize), /// Mouse has clicked nothing of importance Out, } @@ -34,49 +36,53 @@ impl Editor { if let Some((idx, rows, cols)) = at_idx { let idx = idx.clone(); // Calculate the current dent in this split - let doc_idx = self.files.get_atom(idx.clone()).unwrap().1; - let dent = self.dent_for(&idx, doc_idx); - // Split that user clicked in located - adjust event location - let clicked = Loc { - x: col.saturating_sub(cols.start), - y: row.saturating_sub(rows.start), - }; - // Work out where the user clicked - if clicked.y == 0 && tab_enabled { - // Clicked on tab line - let (tabs, _, offset) = - self.get_tab_parts(&idx, lua, cols.end.saturating_sub(cols.start)); - // Try to work out which tab we clicked on - let mut c = u16::try_from(clicked.x).unwrap_or(u16::MAX) + 2; - for (i, header) in tabs.iter().enumerate() { - let header_len = width(header, 4) + 1; - c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); - if c == 0 { - // This tab was clicked on - return MouseLocation::Tabs(idx.clone(), i + offset); + if let Some((_, doc_idx)) = self.files.get_atom(idx.clone()) { + let dent = self.dent_for(&idx, doc_idx); + // Split that user clicked in located - adjust event location + let clicked = Loc { + x: col.saturating_sub(cols.start), + y: row.saturating_sub(rows.start), + }; + // Work out where the user clicked + if clicked.y == 0 && tab_enabled { + // Clicked on tab line + let (tabs, _, offset) = + self.get_tab_parts(&idx, lua, cols.end.saturating_sub(cols.start)); + // Try to work out which tab we clicked on + let mut c = u16::try_from(clicked.x).unwrap_or(u16::MAX) + 2; + for (i, header) in tabs.iter().enumerate() { + let header_len = width(header, 4) + 1; + c = c.saturating_sub(u16::try_from(header_len).unwrap_or(u16::MAX)); + if c == 0 { + // This tab was clicked on + return MouseLocation::Tabs(idx.clone(), i + offset); + } } + // Did not click on a tab + MouseLocation::Out + } else if clicked.y == rows.end.saturating_sub(1) { + // Clicked on status line + MouseLocation::Out + } else if clicked.x < dent { + // Clicked on line numbers + MouseLocation::Out + } else if let Some((fcs, ptr)) = self.files.get_atom(idx.clone()) { + // Clicked on document + let offset = fcs[ptr].doc.offset; + MouseLocation::File( + idx.clone(), + Loc { + x: clicked.x.saturating_sub(dent) + offset.x, + y: clicked.y.saturating_sub(tab) + offset.y, + }, + ) + } else { + // We can't seem to get the atom for some reason, just default to Out + MouseLocation::Out } - // Did not click on a tab - MouseLocation::Out - } else if clicked.y == rows.end.saturating_sub(1) { - // Clicked on status line - MouseLocation::Out - } else if clicked.x < dent { - // Clicked on line numbers - MouseLocation::Out - } else if let Some((fcs, ptr)) = self.files.get_atom(idx.clone()) { - // Clicked on document - let offset = fcs[ptr].doc.offset; - MouseLocation::File( - idx.clone(), - Loc { - x: clicked.x.saturating_sub(dent) + offset.x, - y: clicked.y.saturating_sub(tab) + offset.y, - }, - ) } else { - // We can't seem to get the atom for some reason, just default to Out - MouseLocation::Out + // Pretty sure we just clicked on the file tree (split with no atom!) + MouseLocation::FileTree(row) } } else { MouseLocation::Out @@ -85,54 +91,77 @@ impl Editor { /// Handles a mouse event (dragging / clicking) #[allow(clippy::too_many_lines)] - pub fn handle_mouse_event(&mut self, lua: &Lua, event: MouseEvent) { + pub fn handle_mouse_event(&mut self, lua: &Lua, event: MouseEvent) -> Result<()> { match event.modifiers { KeyModifiers::NONE => match event.kind { // Single click MouseEventKind::Down(MouseButton::Left) => { + let location = self.find_mouse_location(lua, event); + let clicked_in_ft = matches!(location, MouseLocation::FileTree(_)); // 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 { + // If the user quickly clicked twice in the same location (outside the file tree) + if short_period && same_location && !clicked_in_ft { self.handle_double_click(lua, event); - return; + return Ok(()); } } - match self.find_mouse_location(lua, event) { + match location { MouseLocation::File(idx, mut loc) => { + self.cache_old_ptr(&idx); self.ptr.clone_from(&idx); - self.doc_mut().clear_cursors(); - loc.x = self.doc_mut().character_idx(&loc); - self.doc_mut().move_to(&loc); - self.doc_mut().old_cursor = self.doc().loc().x; + self.update_cwd(); + if let Some(doc) = self.try_doc_mut() { + doc.clear_cursors(); + loc.x = doc.character_idx(&loc); + doc.move_to(&loc); + doc.old_cursor = doc.loc().x; + } } MouseLocation::Tabs(idx, i) => { self.files.move_to(idx.clone(), i); + self.cache_old_ptr(&idx); self.ptr.clone_from(&idx); self.update_cwd(); } + MouseLocation::FileTree(y) => { + // Handle the click + if let Some(ft) = &self.file_tree { + // Move selection to where we clicked + if let Some(item) = ft.flatten().get(y) { + self.file_tree_selection = Some(item.to_string()); + // Toggle the node + self.file_tree_open_node()?; + } + } + } MouseLocation::Out => (), } } MouseEventKind::Down(MouseButton::Right) => { // Select the current line if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { + self.cache_old_ptr(&idx); self.ptr.clone_from(&idx); - self.doc_mut().select_line_at(loc.y); - let line = self.doc().line(loc.y).unwrap_or_default(); - self.alt_click_state = Some(( - Loc { - x: 0, - y: self.doc().loc().y, - }, - Loc { - x: line.chars().count(), - y: self.doc().loc().y, - }, - )); + self.update_cwd(); + if let Some(doc) = self.try_doc_mut() { + doc.select_line_at(loc.y); + let line = doc.line(loc.y).unwrap_or_default(); + self.alt_click_state = Some(( + Loc { + x: 0, + y: doc.loc().y, + }, + Loc { + x: line.chars().count(), + y: doc.loc().y, + }, + )); + } } } MouseEventKind::Up(MouseButton::Right) => { @@ -149,70 +178,99 @@ impl Editor { MouseEventKind::Drag(MouseButton::Left) => { match self.find_mouse_location(lua, event) { MouseLocation::File(idx, mut loc) => { - self.ptr.clone_from(&idx); - loc.x = self.doc_mut().character_idx(&loc); - if let Some((dbl_start, dbl_end)) = self.alt_click_state { - if loc.x > self.doc().cursor.selection_end.x { - // Find boundary of next word - let next = self.doc().next_word_close(loc); - self.doc_mut().move_to(&dbl_start); - self.doc_mut().select_to(&Loc { x: next, y: loc.y }); + if self.try_doc().is_some() { + self.cache_old_ptr(&idx); + self.ptr.clone_from(&idx); + self.update_cwd(); + let doc = self.try_doc().unwrap(); + loc.x = doc.character_idx(&loc); + if let Some((dbl_start, dbl_end)) = self.alt_click_state { + let doc = self.try_doc().unwrap(); + if loc.x > doc.cursor.selection_end.x { + // Find boundary of next word + let next = doc.next_word_close(loc); + let doc = self.try_doc_mut().unwrap(); + doc.move_to(&dbl_start); + doc.select_to(&Loc { x: next, y: loc.y }); + } else { + // Find boundary of previous word + let next = doc.prev_word_close(loc); + let doc = self.try_doc_mut().unwrap(); + doc.move_to(&dbl_end); + doc.select_to(&Loc { x: next, y: loc.y }); + } } else { - // Find boundary of previous word - let next = self.doc().prev_word_close(loc); - self.doc_mut().move_to(&dbl_end); - self.doc_mut().select_to(&Loc { x: next, y: loc.y }); + let doc = self.try_doc_mut().unwrap(); + doc.select_to(&loc); } - } else { - self.doc_mut().select_to(&loc); } } - MouseLocation::Tabs(_, _) | MouseLocation::Out => (), + MouseLocation::Tabs(_, _) + | MouseLocation::Out + | MouseLocation::FileTree(_) => (), } } MouseEventKind::Drag(MouseButton::Right) => { match self.find_mouse_location(lua, event) { MouseLocation::File(idx, mut loc) => { - self.ptr.clone_from(&idx); - loc.x = self.doc_mut().character_idx(&loc); - if let Some((line_start, line_end)) = self.alt_click_state { - if loc.y > self.doc().cursor.selection_end.y { - let line = self.doc().line(loc.y).unwrap_or_default(); - self.doc_mut().move_to(&line_start); - self.doc_mut().select_to(&Loc { - x: line.chars().count(), - y: loc.y, - }); + if self.try_doc().is_some() { + self.cache_old_ptr(&idx); + self.ptr.clone_from(&idx); + self.update_cwd(); + let doc = self.try_doc_mut().unwrap(); + loc.x = doc.character_idx(&loc); + if let Some((line_start, line_end)) = self.alt_click_state { + let doc = self.try_doc().unwrap(); + if loc.y > doc.cursor.selection_end.y { + let line = doc.line(loc.y).unwrap_or_default(); + let doc = self.try_doc_mut().unwrap(); + doc.move_to(&line_start); + doc.select_to(&Loc { + x: line.chars().count(), + y: loc.y, + }); + } else { + let doc = self.try_doc_mut().unwrap(); + doc.move_to(&line_end); + doc.select_to(&Loc { x: 0, y: loc.y }); + } } else { - self.doc_mut().move_to(&line_end); - self.doc_mut().select_to(&Loc { x: 0, y: loc.y }); + self.try_doc_mut().unwrap().select_to(&loc); } - } else { - self.doc_mut().select_to(&loc); } } - MouseLocation::Tabs(_, _) | MouseLocation::Out => (), + MouseLocation::Tabs(_, _) + | MouseLocation::Out + | MouseLocation::FileTree(_) => (), } } // Mouse scroll behaviour MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { + let scroll_amount = config!(self.config, terminal).scroll_amount; if let MouseLocation::File(idx, _) = self.find_mouse_location(lua, event) { + self.cache_old_ptr(&idx); self.ptr.clone_from(&idx); - let scroll_amount = config!(self.config, terminal).scroll_amount; - for _ in 0..scroll_amount { - if event.kind == MouseEventKind::ScrollDown { - self.doc_mut().scroll_down(); - } else { - self.doc_mut().scroll_up(); + self.update_cwd(); + if let Some(doc) = self.try_doc_mut() { + for _ in 0..scroll_amount { + if event.kind == MouseEventKind::ScrollDown { + doc.scroll_down(); + } else { + doc.scroll_up(); + } } } } } MouseEventKind::ScrollLeft => { - self.doc_mut().move_left(); + if let Some(doc) = self.try_doc_mut() { + doc.move_left(); + } } MouseEventKind::ScrollRight => { - self.doc_mut().move_right(); + if let Some(doc) = self.try_doc_mut() { + doc.move_right(); + } } _ => (), }, @@ -220,27 +278,44 @@ impl Editor { KeyModifiers::CONTROL => { if let MouseEventKind::Down(MouseButton::Left) = event.kind { if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { + self.cache_old_ptr(&idx); self.ptr.clone_from(&idx); - self.doc_mut().new_cursor(loc); - self.doc_mut().commit(); + self.update_cwd(); + if let Some(doc) = self.try_doc_mut() { + doc.new_cursor(loc); + doc.commit(); + } } } } _ => (), } + Ok(()) } /// Handle a double-click event pub fn handle_double_click(&mut self, lua: &Lua, event: MouseEvent) { // Select the current word if let MouseLocation::File(idx, loc) = self.find_mouse_location(lua, event) { + self.cache_old_ptr(&idx); self.ptr.clone_from(&idx); - self.doc_mut().select_word_at(&loc); - let mut selection = self.doc().cursor.selection_end; - let mut cursor = self.doc().cursor.loc; - selection.x = self.doc().character_idx(&selection); - cursor.x = self.doc().character_idx(&cursor); - self.alt_click_state = Some((selection, cursor)); + self.update_cwd(); + if let Some(doc) = self.try_doc_mut() { + doc.select_word_at(&loc); + let mut selection = doc.cursor.selection_end; + let mut cursor = doc.cursor.loc; + selection.x = doc.character_idx(&selection); + cursor.x = doc.character_idx(&cursor); + self.alt_click_state = Some((selection, cursor)); + } + } + } + + /// Cache the old ptr + fn cache_old_ptr(&mut self, idx: &Vec) { + self.old_ptr.clone_from(idx); + if self.file_tree_is_open() && !self.old_ptr.is_empty() { + self.old_ptr.remove(0); } } } diff --git a/src/editor/scanning.rs b/src/editor/scanning.rs index 09c9716e..0e6b455a 100644 --- a/src/editor/scanning.rs +++ b/src/editor/scanning.rs @@ -15,8 +15,13 @@ use super::Editor; impl Editor { /// Use search feature pub fn search(&mut self, lua: &Lua) -> Result<()> { + // Block any non-documents from activating search + if self.try_doc().is_none() { + return Ok(()); + } + // Gather data let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); - let cache = self.doc().char_loc(); + let cache = self.try_doc().unwrap().char_loc(); // Prompt for a search term let mut target = String::new(); let mut done = false; @@ -51,20 +56,20 @@ impl Editor { (KMod::NONE, KCode::Enter) => done = true, // Cancel operation (KMod::NONE, KCode::Esc) => { - self.doc_mut().move_to(&cache); - self.doc_mut().cancel_selection(); + self.try_doc_mut().unwrap().move_to(&cache); + self.try_doc_mut().unwrap().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.try_doc_mut().unwrap().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.try_doc_mut().unwrap().move_to(&cache); self.next_match(&target); } _ => (), @@ -104,7 +109,7 @@ impl Editor { // On return or escape key, exit menu (KMod::NONE, KCode::Enter) => done = true, (KMod::NONE, KCode::Esc) => { - self.doc_mut().move_to(&cache); + self.try_doc_mut().unwrap().move_to(&cache); done = true; } // On left key, move to the previous match in the document @@ -116,41 +121,54 @@ impl Editor { } self.update_highlighter(); } - self.doc_mut().cancel_selection(); + self.try_doc_mut().unwrap().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)?; - // 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) + if let Some(doc) = self.try_doc_mut() { + let mtch = doc.next_match(target, 1)?; + // Select match + doc.cancel_selection(); + let mut move_to = mtch.loc; + move_to.x += mtch.text.chars().count(); + doc.move_to(&move_to); + doc.select_to(&mtch.loc); + // Update highlighting + self.update_highlighter(); + Some(mtch.text) + } else { + None + } } /// Move to the previous match pub fn prev_match(&mut self, target: &str) -> Option { - let mtch = self.doc_mut().prev_match(target)?; - self.doc_mut().move_to(&mtch.loc); - // 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) + if let Some(doc) = self.try_doc_mut() { + let mtch = doc.prev_match(target)?; + doc.move_to(&mtch.loc); + // Select match + doc.cancel_selection(); + let mut move_to = mtch.loc; + move_to.x += mtch.text.chars().count(); + doc.move_to(&move_to); + doc.select_to(&mtch.loc); + // Update highlighting + self.update_highlighter(); + Some(mtch.text) + } else { + None + } } /// Use replace feature pub fn replace(&mut self, lua: &Lua) -> Result<()> { + // Block any non-documents from activating replace + if self.try_doc().is_none() { + return Ok(()); + } + // Gather data let editor_bg = Bg(config!(self.config, colors).editor_bg.to_color()?); // Request replace information let target = self.prompt("Replace")?; @@ -215,38 +233,46 @@ impl Editor { // Update syntax highlighter if necessary self.update_highlighter(); } - self.doc_mut().cancel_selection(); + self.try_doc_mut().unwrap().cancel_selection(); Ok(()) } /// Replace an instance in a document fn do_replace(&mut self, into: &str, text: &str) -> Result<()> { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // Do the replacement - let loc = self.doc().char_loc(); - self.doc_mut().replace(loc, text, into)?; - self.doc_mut().move_to(&loc); - // Update syntax highlighter - self.update_highlighter(); - if let Some(file) = self.files.get_mut(self.ptr.clone()) { - file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + if let Some(doc) = self.try_doc_mut() { + // Commit events to event manager (for undo / redo) + doc.commit(); + // Do the replacement + let loc = doc.char_loc(); + doc.replace(loc, text, into)?; + doc.move_to(&loc); + // Update syntax highlighter + self.update_highlighter(); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + file.highlighter.edit(loc.y, &file.doc.lines[loc.y]); + } } Ok(()) } /// Replace all instances in a document fn do_replace_all(&mut self, target: &str, into: &str) { - // Commit events to event manager (for undo / redo) - self.doc_mut().commit(); - // Replace everything top to bottom - self.doc_mut().move_to(&Loc::at(0, 0)); - while let Some(mtch) = self.doc_mut().next_match(target, 1) { - drop(self.doc_mut().replace(mtch.loc, &mtch.text, into)); - self.update_highlighter(); - if let Some(file) = self.files.get_mut(self.ptr.clone()) { - file.highlighter - .edit(mtch.loc.y, &file.doc.lines[mtch.loc.y]); + if self.try_doc().is_some() { + // Commit events to event manager (for undo / redo) + self.try_doc_mut().unwrap().commit(); + // Replace everything top to bottom + self.try_doc_mut().unwrap().move_to(&Loc::at(0, 0)); + while let Some(mtch) = self.try_doc_mut().unwrap().next_match(target, 1) { + drop( + self.try_doc_mut() + .unwrap() + .replace(mtch.loc, &mtch.text, into), + ); + self.update_highlighter(); + if let Some(file) = self.files.get_mut(self.ptr.clone()) { + file.highlighter + .edit(mtch.loc.y, &file.doc.lines[mtch.loc.y]); + } } } } diff --git a/src/error.rs b/src/error.rs index e1f9010c..659d60e6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,7 @@ error_set! { AlreadyOpen { file: String, }, + InvalidPath, // None, <--- Needed??? }; } diff --git a/src/main.rs b/src/main.rs index 8a5728cd..79025a2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use events::wait_for_event; use kaolinite::event::{Error as KError, Event}; use kaolinite::searching::Searcher; use kaolinite::utils::{file_or_dir, get_cwd}; -use kaolinite::Loc; +use kaolinite::{Document, Loc}; use mlua::Error::{RuntimeError, SyntaxError}; use mlua::{AnyUserData, FromLua, Lua, Value}; use std::io::ErrorKind; @@ -184,7 +184,10 @@ fn run(cli: &CommandLineInterface) -> Result<()> { let event = wait_for_event(&editor, &lua)?; // Handle the event - let original_loc = ged!(&editor).doc().char_loc(); + let original_loc = ged!(&editor) + .try_doc() + .map(Document::char_loc) + .unwrap_or_default(); handle_event(&editor, &event, &lua)?; // Handle multi cursors @@ -243,8 +246,19 @@ fn handle_event(editor: &AnyUserData, event: &CEvent, lua: &Lua) -> Result<()> { } // Actually handle editor event (errors included) - if let Err(err) = ged!(mut &editor).handle_event(lua, event.clone()) { - ged!(mut &editor).feedback = Feedback::Error(format!("{err:?}")); + let event_result = ged!(mut &editor).handle_event(lua, event.clone()); + if let Err(err) = event_result { + // Nicely display error to user + match err { + OxError::Lua(err) => { + handle_lua_error("event", Err(err), &mut ged!(mut &editor).feedback); + } + OxError::AlreadyOpen { file } => { + ged!(mut &editor).feedback = + Feedback::Error(format!("File '{file}' is already open")); + } + _ => ged!(mut &editor).feedback = Feedback::Error(format!("{err:?}")), + } } // Handle paste event (after event) diff --git a/src/plugin/bootstrap.lua b/src/plugin/bootstrap.lua index 7b280abb..7bf00eae 100644 --- a/src/plugin/bootstrap.lua +++ b/src/plugin/bootstrap.lua @@ -159,528 +159,626 @@ function shell:kill(pid) end -- Add types for built-in file type detection +-- Colours are in the format of a string of: +-- red +-- orange +-- yellow +-- green +-- dark blue +-- light blue +-- purple +-- pink +-- brown +-- gray file_types = { ["ABAP"] = { icon = "σ°…© ", files = {}, extensions = {"abap"}, modelines = {}, + color = "darkblue", }, ["Ada"] = { icon = "", files = {}, extensions = {"ada"}, modelines = {}, + color = "green", }, ["AutoHotkey"] = { icon = "ο„œ ", files = {}, extensions = {"ahk", "ahkl"}, modelines = {}, + color = "green", }, ["AppleScript"] = { icon = "ξœ‘", files = {}, extensions = {"applescript", "scpt"}, modelines = {}, + color = "grey", }, ["Arc"] = { icon = "σ°…© ", files = {}, extensions = {"arc"}, modelines = {}, + color = "pink", }, ["ASP"] = { icon = "σ°…© ", files = {}, extensions = {"asp", "asax", "ascx", "ashx", "asmx", "aspx", "axd"}, modelines = {}, + color = "lightblue", }, ["ActionScript"] = { icon = "σ°‘· ", files = {}, extensions = {"as"}, modelines = {}, + color = "orange", }, ["AGS Script"] = { icon = "σ°…© ", files = {}, extensions = {"asc", "ash"}, modelines = {}, + color = "purple", }, ["Assembly"] = { icon = " ", files = {}, extensions = {"asm", "nasm"}, modelines = {}, + color = "grey", }, ["Awk"] = { icon = "σ°…© ", files = {}, extensions = {"awk", "auk", "gawk", "mawk", "nawk"}, modelines = {"#!\\s*/usr/bin/(env )?awk"}, + color = "red", }, ["Batch"] = { icon = "󰆍 ", files = {}, extensions = {"bat", "cmd"}, modelines = {}, + color = "grey", }, ["Brainfuck"] = { icon = " ", files = {}, extensions = {"b", "bf"}, modelines = {}, + color = "yellow", }, ["C"] = { icon = " ", files = {}, extensions = {"c"}, modelines = {}, + color = "lightblue", }, ["CMake"] = { icon = "ξ™³ ", files = {}, extensions = {"cmake"}, modelines = {}, + color = "green", }, ["Cobol"] = { icon = "σ°…© ", files = {}, extensions = {"cbl", "cobol", "cob"}, modelines = {}, + color = "purple", }, ["Java"] = { icon = "ξ‰– ", files = {}, extensions = {"class", "java"}, modelines = {}, + color = "orange", }, ["Clojure"] = { icon = "ξ™‚ ", files = {}, extensions = {"clj", "cl2", "cljs", "cljx", "cljc"}, modelines = {}, + color = "lightblue", }, ["CoffeeScript"] = { icon = "ξ˜› ", files = {}, extensions = {"coffee"}, modelines = {}, + color = "brown", }, ["Crystal"] = { icon = " ", files = {}, extensions = {"cr"}, modelines = {}, + color = "grey", }, ["Cuda"] = { icon = "ξ°™ ", files = {}, extensions = {"cu", "cuh"}, modelines = {}, + color = "green", }, ["C++"] = { icon = " ", files = {}, extensions = {"cpp", "cxx"}, modelines = {}, + color = "darkblue", }, ["C#"] = { icon = "ξ™ˆ ", files = {}, extensions = {"cs", "cshtml", "csx"}, modelines = {}, + color = "purple", }, ["CSS"] = { icon = " ", files = {}, extensions = {"css"}, modelines = {}, + color = "purple", }, ["CSV"] = { icon = "ξ™Š ", files = {}, extensions = {"csv"}, modelines = {}, + color = "grey", }, ["D"] = { icon = "ξ™‘ ", files = {}, extensions = {"d", "di"}, modelines = {}, + color = "red", }, ["Dart"] = { icon = "ξ™Œ ", files = {}, extensions = {"dart"}, modelines = {}, + color = "lightblue", }, ["Diff"] = { icon = "ο“’ ", files = {}, extensions = {"diff", "patch"}, modelines = {}, + color = "green", }, ["Dockerfile"] = { icon = " ", files = {}, extensions = {"dockerfile"}, modelines = {}, + color = "lightblue", }, ["Elixir"] = { icon = " ", files = {}, extensions = {"ex", "exs"}, modelines = {}, + color = "purple", }, ["Elm"] = { icon = " ", files = {}, extensions = {"elm"}, modelines = {}, + color = "lightblue", }, ["Emacs Lisp"] = { icon = " ", files = {}, extensions = {"el"}, modelines = {}, + color = "purple", }, ["ERB"] = { icon = "σ°…© ", files = {}, extensions = {"erb"}, modelines = {}, + color = "red", }, ["Erlang"] = { icon = " ", files = {}, extensions = {"erl", "es"}, modelines = {}, + color = "red", }, ["F#"] = { icon = "ξ™š ", files = {}, extensions = {"fs", "fsi", "fsx"}, modelines = {}, + color = "lightblue", }, ["FORTRAN"] = { icon = "󱈚 ", files = {}, extensions = {"f", "f90", "fpp", "for"}, modelines = {}, + color = "purple", }, ["Fish"] = { icon = " ", files = {}, extensions = {"fish"}, modelines = {"#!\\s*/usr/bin/(env )?fish"}, + color = "orange", }, ["Forth"] = { icon = "σ°…© ", files = {}, extensions = {"fth"}, modelines = {}, + color = "red", }, ["ANTLR"] = { icon = "σ°…© ", files = {}, extensions = {"g4"}, modelines = {}, + color = "red", }, ["GDScript"] = { icon = "ξ™Ÿ ", files = {}, extensions = {"gd"}, modelines = {}, + color = "darkblue", }, ["GLSL"] = { icon = "ο€Ύ ", files = {}, extensions = {"glsl", "vert", "shader", "geo", "fshader", "vrx", "vsh", "vshader", "frag"}, modelines = {}, + color = "lightblue", }, ["Gnuplot"] = { icon = " ", files = {}, extensions = {"gnu", "gp", "plot"}, modelines = {}, + color = "grey", }, ["Go"] = { icon = "", files = {}, extensions = {"go"}, modelines = {}, + color = "lightblue", }, ["Groovy"] = { icon = " ", files = {}, extensions = {"groovy", "gvy"}, modelines = {}, + color = "lightblue", }, ["HLSL"] = { icon = "σ°…© ", files = {}, extensions = {"hlsl"}, modelines = {}, + color = "darkblue", }, ["C Header"] = { icon = " ", files = {}, extensions = {"h"}, modelines = {}, + color = "lightblue", }, ["Haml"] = { icon = "", files = {}, extensions = {"haml"}, modelines = {}, + color = "yellow", }, ["Handlebars"] = { icon = "σ°…© ", files = {}, extensions = {"handlebars", "hbs"}, modelines = {}, + color = "brown", }, ["Haskell"] = { icon = " ", files = {}, extensions = {"hs"}, modelines = {}, + color = "purple", }, ["C++ Header"] = { icon = " ", files = {}, extensions = {"hpp"}, modelines = {}, + color = "darkblue", }, ["HTML"] = { icon = " ", files = {}, extensions = {"html", "htm", "xhtml"}, modelines = {}, + color = "orange", }, ["INI"] = { icon = " ", files = {}, extensions = {"ini", "cfg"}, modelines = {}, + color = "grey", }, ["Arduino"] = { icon = " ", files = {}, extensions = {"ino"}, modelines = {}, + color = "lightblue", }, ["J"] = { icon = "ξ™­ ", files = {}, extensions = {"ijs"}, modelines = {}, + color = "lightblue", }, ["JSON"] = { icon = "ξ˜‹ ", files = {}, extensions = {"json"}, modelines = {}, + color = "grey", }, ["JSX"] = { icon = "ξ˜₯ ", files = {}, extensions = {"jsx"}, modelines = {}, + color = "lightblue", }, ["JavaScript"] = { icon = " ", files = {}, extensions = {"js"}, modelines = {"#!\\s*/usr/bin/(env )?node"}, + color = "yellow", }, ["Julia"] = { icon = " ", files = {}, extensions = {"jl"}, modelines = {}, + color = "lightblue", }, ["Kotlin"] = { icon = " ", files = {}, extensions = {"kt", "ktm", "kts"}, modelines = {}, + color = "purple", }, ["LLVM"] = { icon = "σ°…© ", files = {}, extensions = {"ll"}, modelines = {}, + color = "lightblue", }, ["Lex"] = { icon = "σ°…© ", files = {}, extensions = {"l", "lex"}, modelines = {}, + color = "grey", }, ["Lua"] = { icon = " ", files = {".oxrc"}, extensions = {"lua"}, modelines = {"#!\\s*/usr/bin/(env )?lua"}, + color = "darkblue", }, ["LiveScript"] = { icon = "ξ™± ", files = {}, extensions = {"ls"}, modelines = {}, + color = "lightblue", }, ["LOLCODE"] = { icon = "σ°…© ", files = {}, extensions = {"lol"}, modelines = {}, + color = "red", }, ["Common Lisp"] = { icon = " ", files = {}, extensions = {"lisp", "asd", "lsp"}, modelines = {}, + color = "grey", }, ["Log file"] = { icon = "ο“­ ", files = {}, extensions = {"log"}, modelines = {}, + color = "grey", }, ["M4"] = { icon = "σ°…© ", files = {}, extensions = {"m4"}, modelines = {}, + color = "darkblue", }, ["Groff"] = { icon = "σ°…© ", files = {}, extensions = {"man", "roff"}, modelines = {}, + color = "grey", }, ["Matlab"] = { icon = " ", files = {}, extensions = {"matlab"}, modelines = {}, + color = "orange", }, ["Objective-C"] = { icon = " ", files = {}, extensions = {"m"}, modelines = {}, + color = "lightblue", }, ["OCaml"] = { icon = "ξ™Ί ", files = {}, extensions = {"ml"}, modelines = {}, + color = "orange", }, ["Makefile"] = { icon = "ξ™³ ", files = {"Makefile"}, extensions = {"mk", "mak"}, modelines = {}, + color = "grey", }, ["Markdown"] = { icon = "ο’Š ", files = {}, extensions = {"md", "markdown"}, modelines = {}, + color = "grey", }, ["Nix"] = { icon = "οŒ“ ", files = {}, extensions = {"nix"}, modelines = {}, + color = "lightblue", }, ["NumPy"] = { icon = "󰘨 ", files = {}, extensions = {"numpy"}, modelines = {}, + color = "darkblue", }, ["OpenCL"] = { icon = "σ°…© ", files = {}, extensions = {"opencl", "cl"}, modelines = {}, + color = "green", }, ["PHP"] = { icon = "󰌟 ", files = {}, extensions = {"php"}, modelines = {"#!\\s*/usr/bin/(env )?php"}, + color = "lightblue", }, ["Pascal"] = { icon = "σ°…© ", files = {}, extensions = {"pas"}, modelines = {}, + color = "darkblue", }, ["Perl"] = { icon = "ξ™Ύ ", files = {}, extensions = {"pl"}, modelines = {"#!\\s*/usr/bin/(env )?perl"}, + color = "lightblue", }, ["PowerShell"] = { icon = "󰨊 ", files = {}, extensions = {"psl"}, modelines = {}, + color = "lightblue", }, ["Prolog"] = { icon = "ξš… ", files = {}, extensions = {"pro"}, modelines = {}, + color = "orange", }, ["Python"] = { icon = "ξ˜† ", files = {}, extensions = {"py", "pyw"}, modelines = {"#!\\s*/usr/bin/(env )?python3?"}, + color = "lightblue", }, ["Cython"] = { icon = "ξ˜† ", files = {}, extensions = {"pyx", "pxd", "pxi"}, modelines = {}, + color = "darkblue", }, ["R"] = { icon = " ", files = {}, extensions = {"r"}, modelines = {}, + color = "darkblue", }, ["reStructuredText"] = { icon = "σ°Š„", files = {}, extensions = {"rst"}, modelines = {}, + color = "grey", }, ["Racket"] = { icon = "σ°…© ", files = {}, extensions = {"rkt"}, modelines = {}, + color = "red", }, ["Ruby"] = { icon = " ", files = {}, extensions = {"rb", "ruby"}, modelines = {"#!\\s*/usr/bin/(env )?ruby"}, + color = "red", }, ["Rust"] = { icon = "ξš‹ ", files = {}, extensions = {"rs"}, modelines = {"#!\\s*/usr/bin/(env )?rust"}, + color = "orange", }, ["Shell"] = { icon = "ξž• ", @@ -690,131 +788,153 @@ file_types = { "#!\\s*/bin/(sh|bash)", "#!\\s*/usr/bin/env bash", }, + color = "grey", }, ["SCSS"] = { icon = "ξ˜ƒ ", files = {}, extensions = {"scss"}, modelines = {}, + color = "pink", }, ["SQL"] = { icon = " ", files = {}, extensions = {"sql"}, modelines = {}, + color = "lightblue", }, ["Sass"] = { icon = "ξ˜ƒ ", files = {}, extensions = {"sass"}, modelines = {}, + color = "pink", }, ["Scala"] = { icon = "", files = {}, extensions = {"scala"}, modelines = {}, + color = "red", }, ["Scheme"] = { icon = "", files = {}, extensions = {"scm"}, modelines = {}, + color = "grey", }, ["Smalltalk"] = { icon = "σ°…© ", files = {}, extensions = {"st"}, modelines = {}, + color = "lightblue", }, ["Swift"] = { icon = "ξš™ ", files = {}, extensions = {"swift"}, modelines = {}, + color = "orange", }, ["TOML"] = { icon = " ", files = {}, extensions = {"toml"}, modelines = {}, + color = "orange", }, ["Tcl"] = { icon = "σ°…© ", files = {}, extensions = {"tcl"}, modelines = {"#!\\s*/usr/bin/(env )?tcl"}, + color = "red", }, ["TeX"] = { icon = "ξš› ", files = {}, extensions = {"tex"}, modelines = {}, + color = "grey", }, ["TypeScript"] = { icon = " ", files = {}, extensions = {"ts", "tsx"}, modelines = {}, + color = "darkblue", }, ["Plain Text"] = { icon = "ξ™Ž ", files = {}, extensions = {"txt"}, modelines = {}, + color = "grey", }, ["Vala"] = { icon = " ", files = {}, extensions = {"vala"}, modelines = {}, + color = "purple", }, ["Visual Basic"] = { icon = "󰯁 ", files = {}, extensions = {"vb", "vbs"}, modelines = {}, + color = "purple", }, ["Vue"] = { icon = " ", files = {}, extensions = {"vue"}, modelines = {}, + color = "green", }, ["Logos"] = { icon = "σ°…© ", files = {}, extensions = {"xm", "x", "xi"}, modelines = {}, + color = "red", }, ["XML"] = { icon = "σ°…© ", files = {}, extensions = {"xml"}, modelines = {}, + color = "lightblue", }, ["Yacc"] = { icon = "σ°…© ", files = {}, extensions = {"y", "yacc"}, modelines = {}, + color = "grey", }, ["Yaml"] = { icon = "σ°…© ", files = {}, extensions = {"yaml", "yml"}, modelines = {}, + color = "red", }, ["Bison"] = { icon = "σ°…© ", files = {}, extensions = {"yxx"}, modelines = {}, + color = "grey", }, ["Zsh"] = { icon = "ξž• ", files = {}, extensions = {"zsh"}, modelines = {}, + color = "orange", }, }