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",
},
}