Dot_Files/.config/micro/plug/filemanager/filemanager.lua
2022-01-08 15:24:29 -06:00

1367 lines
41 KiB
Lua

VERSION = "3.5.0"
local micro = import("micro")
local config = import("micro/config")
local shell = import("micro/shell")
local buffer = import("micro/buffer")
local os = import("os")
local filepath = import("path/filepath")
-- Clear out all stuff in Micro's messenger
local function clear_messenger()
-- messenger:Reset()
-- messenger:Clear()
end
-- Holds the micro.CurPane() we're manipulating
local tree_view = nil
-- Keeps track of the current working directory
local current_dir = os.Getwd()
-- Keep track of current highest visible indent to resize width appropriately
local highest_visible_indent = 0
-- Holds a table of paths -- objects from new_listobj() calls
local scanlist = {}
-- Get a new object used when adding to scanlist
local function new_listobj(p, d, o, i)
return {
["abspath"] = p,
["dirmsg"] = d,
["owner"] = o,
["indent"] = i,
-- Since decreasing/increasing is common, we include these with the object
["decrease_owner"] = function(self, minus_num)
self.owner = self.owner - minus_num
end,
["increase_owner"] = function(self, plus_num)
self.owner = self.owner + plus_num
end
}
end
-- Repeats a string x times, then returns it concatenated into one string
local function repeat_str(str, len)
-- Do NOT try to concat in a loop, it freezes micro...
-- instead, use a temporary table to hold values
local string_table = {}
for i = 1, len do
string_table[i] = str
end
-- Return the single string of repeated characters
return table.concat(string_table)
end
-- A check for if a path is a dir
local function is_dir(path)
-- Used for checking if dir
local golib_os = import("os")
-- Returns a FileInfo on the current file/path
local file_info, stat_error = golib_os.Stat(path)
-- Wrap in nil check for file/dirs without read permissions
if file_info ~= nil then
-- Returns true/false if it's a dir
return file_info:IsDir()
else
-- Couldn't stat the file/dir, usually because no read permissions
micro.InfoBar():Error("Error checking if is dir: ", stat_error)
-- Nil since we can't read the path
return nil
end
end
-- Returns a list of files (in the target dir) that are ignored by the VCS system (if exists)
-- aka this returns a list of gitignored files (but for whatever VCS is found)
local function get_ignored_files(tar_dir)
-- True/false if the target dir returns a non-fatal error when checked with 'git status'
local function has_git()
local git_rp_results = RunShellCommand('git -C "' .. tar_dir .. '" rev-parse --is-inside-work-tree')
return git_rp_results:match("^true%s*$")
end
local readout_results = {}
-- TODO: Support more than just Git, such as Mercurial or SVN
if has_git() then
-- If the dir is a git dir, get all ignored in the dir
local git_ls_results =
RunShellCommand('git -C "' .. tar_dir .. '" ls-files . --ignored --exclude-standard --others --directory')
-- Cut off the newline that is at the end of each result
for split_results in string.gmatch(git_ls_results, "([^\r\n]+)") do
-- git ls-files adds a trailing slash if it's a dir, so we remove it (if it is one)
readout_results[#readout_results + 1] =
(string.sub(split_results, -1) == "/" and string.sub(split_results, 1, -2) or split_results)
end
end
-- Make sure we return a table
return readout_results
end
-- Returns the basename of a path (aka a name without leading path)
local function get_basename(path)
if path == nil then
micro.Log("Bad path passed to get_basename")
return nil
else
-- Get Go's path lib for a basename callback
local golib_path = import("filepath")
return golib_path.Base(path)
end
end
-- Returns true/false if the file is a dotfile
local function is_dotfile(file_name)
-- Check if the filename starts with a dot
if string.sub(file_name, 1, 1) == "." then
return true
else
return false
end
end
-- Structures the output of the scanned directory content to be used in the scanlist table
-- This is useful for both initial creation of the tree, and when nesting with uncompress_target()
local function get_scanlist(dir, ownership, indent_n)
local golib_ioutil = import("ioutil")
-- Gets a list of all the files in the current dir
local dir_scan, scan_error = golib_ioutil.ReadDir(dir)
-- dir_scan will be nil if the directory is read-protected (no permissions)
if dir_scan == nil then
micro.InfoBar():Error("Error scanning dir: ", scan_error)
return nil
end
-- The list of files to be returned (and eventually put in the view)
local results = {}
local files = {}
local function get_results_object(file_name)
local abs_path = filepath.Join(dir, file_name)
-- Use "+" for dir's, "" for files
local dirmsg = (is_dir(abs_path) and "+" or "")
return new_listobj(abs_path, dirmsg, ownership, indent_n)
end
-- Save so we don't have to rerun GetOption a bunch
local show_dotfiles = config.GetGlobalOption("filemanager.showdotfiles")
local show_ignored = config.GetGlobalOption("filemanager.showignored")
local folders_first = config.GetGlobalOption("filemanager.foldersfirst")
-- The list of VCS-ignored files (if any)
-- Only bother getting ignored files if we're not showing ignored
local ignored_files = (not show_ignored and get_ignored_files(dir) or {})
-- True/false if the file is an ignored file
local function is_ignored_file(filename)
for i = 1, #ignored_files do
if ignored_files[i] == filename then
return true
end
end
return false
end
-- Hold the current scan's filename in most of the loops below
local filename
for i = 1, #dir_scan do
local showfile = true
filename = dir_scan[i]:Name()
-- If we should not show dotfiles, and this is a dotfile, don't show
if not show_dotfiles and is_dotfile(filename) then
showfile = false
end
-- If we should not show ignored files, and this is an ignored file, don't show
if not show_ignored and is_ignored_file(filename) then
showfile = false
end
if showfile then
-- This file is good to show, proceed
if folders_first and not is_dir(filepath.Join(dir, filename)) then
-- If folders_first and this is a file, add it to (temporary) files
files[#files + 1] = get_results_object(filename)
else
-- Otherwise, add to results
results[#results + 1] = get_results_object(filename)
end
end
end
if #files > 0 then
-- Append any files to results, now that all folders have been added
-- files will be > 0 only if folders_first and there are files
for i = 1, #files do
results[#results + 1] = files[i]
end
end
-- Return the list of scanned files
return results
end
-- A short "get y" for when acting on the scanlist
-- Needed since we don't store the first 3 visible indicies in scanlist
local function get_safe_y(optional_y)
-- Default to 0 so we can check against and see if it's bad
local y = 0
-- Make the passed y optional
if optional_y == nil then
-- Default to cursor's Y loc if nothing was passed, instead of declaring another y
optional_y = tree_view.Cursor.Loc.Y
end
-- 0/1/2 would be the top "dir, separator, .." so check if it's past
if optional_y > 2 then
-- -2 to conform to our scanlist, since zero-based Go index & Lua's one-based
y = tree_view.Cursor.Loc.Y - 2
end
return y
end
-- Joins the target dir's leading path to the passed name
local function dirname_and_join(path, join_name)
-- The leading path to the dir we're in
local leading_path = DirectoryName(path)
-- Joins with OS-specific slashes
return filepath.Join(leading_path, join_name)
end
-- Hightlights the line when you move the cursor up/down
local function select_line(last_y)
-- Make last_y optional
if last_y ~= nil then
-- Don't let them move past ".." by checking the result first
if last_y > 1 then
-- If the last position was valid, move back to it
tree_view.Cursor.Loc.Y = last_y
end
elseif tree_view.Cursor.Loc.Y < 2 then
-- Put the cursor on the ".." if it's above it
tree_view.Cursor.Loc.Y = 2
end
-- Puts the cursor back in bounds (if it isn't) for safety
tree_view.Cursor:Relocate()
-- Makes sure the cursor is visible (if it isn't)
-- (false) means no callback
tree_view:Center()
-- Highlight the current line where the cursor is
tree_view.Cursor:SelectLine()
end
-- Simple true/false if scanlist is currently empty
local function scanlist_is_empty()
if next(scanlist) == nil then
return true
else
return false
end
end
local function refresh_view()
clear_messenger()
-- If it's less than 30, just use 30 for width. Don't want it too small
if tree_view:GetView().Width < 30 then
tree_view:ResizePane(30)
end
-- Delete everything in the view/buffer
tree_view.Buf.EventHandler:Remove(tree_view.Buf:Start(), tree_view.Buf:End())
-- Insert the top 3 things that are always there
-- Current dir
tree_view.Buf.EventHandler:Insert(buffer.Loc(0, 0), current_dir .. "\n")
-- An ASCII separator
tree_view.Buf.EventHandler:Insert(buffer.Loc(0, 1), repeat_str("", tree_view:GetView().Width) .. "\n")
-- The ".." and use a newline if there are things in the current dir
tree_view.Buf.EventHandler:Insert(buffer.Loc(0, 2), (#scanlist > 0 and "..\n" or ".."))
-- Holds the current basename of the path (purely for display)
local display_content
-- NOTE: might want to not do all these concats in the loop, it can get slow
for i = 1, #scanlist do
-- The first 3 indicies are the dir/separator/"..", so skip them
if scanlist[i].dirmsg ~= "" then
-- Add the + or - to the left to signify if it's compressed or not
-- Add a forward slash to the right to signify it's a dir
display_content = scanlist[i].dirmsg .. " " .. get_basename(scanlist[i].abspath) .. "/"
else
-- Use the basename from the full path for display
-- Two spaces to align with any directories, instead of being "off"
display_content = " " .. get_basename(scanlist[i].abspath)
end
if scanlist[i].owner > 0 then
-- Add a space and repeat it * the indent number
display_content = repeat_str(" ", 2 * scanlist[i].indent) .. display_content
end
-- Newlines are needed for all inserts except the last
-- If you insert a newline on the last, it leaves a blank spot at the bottom
if i < #scanlist then
display_content = display_content .. "\n"
end
-- Insert line-by-line to avoid out-of-bounds on big folders
-- +2 so we skip the 0/1/2 positions that hold the top dir/separator/..
tree_view.Buf.EventHandler:Insert(buffer.Loc(0, i + 2), display_content)
end
-- Resizes all views after messing with ours
tree_view:Tab():Resize()
end
-- Moves the cursor to the ".." in tree_view
local function move_cursor_top()
-- 2 is the position of the ".."
tree_view.Cursor.Loc.Y = 2
-- select the line after moving
select_line()
end
local function refresh_and_select()
-- Save the cursor position before messing with the view..
-- because changing contents in the view causes the Y loc to move
local last_y = tree_view.Cursor.Loc.Y
-- Actually refresh
refresh_view()
-- Moves the cursor back to it's original position
select_line(last_y)
end
-- Find everything nested under the target, and remove it from the scanlist
local function compress_target(y, delete_y)
-- Can't compress the top stuff, or if there's nothing there, so exit early
if y == 0 or scanlist_is_empty() then
return
end
-- Check if the target is a dir, since files don't have anything to compress
-- Also make sure it's actually an uncompressed dir by checking the gutter message
if scanlist[y].dirmsg == "-" then
local target_index, delete_index
-- Add the original target y to stuff to delete
local delete_under = {[1] = y}
local new_table = {}
local del_count = 0
-- Loop through the whole table, looking for nested content, or stuff with ownership == y...
-- and delete matches. y+1 because we want to start under y, without actually touching y itself.
for i = 1, #scanlist do
delete_index = false
-- Don't run on y, since we don't always delete y
if i ~= y then
-- On each loop, check if the ownership matches
for x = 1, #delete_under do
-- Check for something belonging to a thing to delete
if scanlist[i].owner == delete_under[x] then
-- Delete the target if it has an ownership to our delete target
delete_index = true
-- Keep count of total deleted (can't use #delete_under because it's for deleted dir count)
del_count = del_count + 1
-- Check if an uncompressed dir
if scanlist[i].dirmsg == "-" then
-- Add the index to stuff to delete, since it holds nested content
delete_under[#delete_under + 1] = i
end
-- See if we're on the "deepest" nested content
if scanlist[i].indent == highest_visible_indent and scanlist[i].indent > 0 then
-- Save the lower indent, since we're minimizing/deleting nested dirs
highest_visible_indent = highest_visible_indent - 1
end
-- Nothing else to do, so break this inner loop
break
end
end
end
if not delete_index then
-- Save the index in our new table
new_table[#new_table + 1] = scanlist[i]
end
end
scanlist = new_table
if del_count > 0 then
-- Ownership adjusting since we're deleting an index
for i = y + 1, #scanlist do
-- Don't touch root file/dirs
if scanlist[i].owner > y then
-- Minus ownership, on everything below i, the number deleted
scanlist[i]:decrease_owner(del_count)
end
end
end
-- If not deleting, then update the gutter message to be + to signify compressed
if not delete_y then
-- Update the dir message
scanlist[y].dirmsg = "+"
end
elseif config.GetGlobalOption("filemanager.compressparent") and not delete_y then
goto_parent_dir()
-- Prevent a pointless refresh of the view
return
end
-- Put outside check above because we call this to delete targets as well
if delete_y then
local second_table = {}
-- Quickly remove y
for i = 1, #scanlist do
if i == y then
-- Reduce everything's ownership by 1 after y
for x = i + 1, #scanlist do
-- Don't touch root file/dirs
if scanlist[x].owner > y then
-- Minus 1 since we're just deleting y
scanlist[x]:decrease_owner(1)
end
end
else
-- Put everything but y into the temporary table
second_table[#second_table + 1] = scanlist[i]
end
end
-- Put everything (but y) back into scanlist, with adjusted ownership values
scanlist = second_table
end
if tree_view:GetView().Width > (30 + highest_visible_indent) then
-- Shave off some width
tree_view:ResizePane(30 + highest_visible_indent)
end
refresh_and_select()
end
-- Prompts the user for deletion of a file/dir when triggered
-- Not local so Micro can access it
function prompt_delete_at_cursor()
local y = get_safe_y()
-- Don't let them delete the top 3 index dir/separator/..
if y == 0 or scanlist_is_empty() then
micro.InfoBar():Error("You can't delete that")
-- Exit early if there's nothing to delete
return
end
micro.InfoBar():YNPrompt("Do you want to delete the " .. (scanlist[y].dirmsg ~= "" and "dir" or "file") .. ' "' .. scanlist[y].abspath .. '"? ', function(yes, canceled)
if yes and not canceled then
-- Use Go's os.Remove to delete the file
local go_os = import("os")
-- Delete the target (if its a dir then the children too)
local remove_log = go_os.RemoveAll(scanlist[y].abspath)
if remove_log == nil then
micro.InfoBar():Message("Filemanager deleted: ", scanlist[y].abspath)
-- Remove the target (and all nested) from scanlist[y + 1]
-- true to delete y
compress_target(get_safe_y(), true)
else
micro.InfoBar():Error("Failed deleting file/dir: ", remove_log)
end
else
micro.InfoBar():Message("Nothing was deleted")
end
end)
end
-- Changes the current dir in the top of the tree..
-- then scans that dir, and prints it to the view
local function update_current_dir(path)
-- Clear the highest since this is a full refresh
highest_visible_indent = 0
-- Set the width back to 30
tree_view:ResizePane(30)
-- Update the current dir to the new path
current_dir = path
-- Get the current working dir's files into our list of files
-- 0 ownership because this is a scan of the base dir
-- 0 indent because this is the base dir
local scan_results = get_scanlist(path, 0, 0)
-- Safety check with not-nil
if scan_results ~= nil then
-- Put in the new scan stuff
scanlist = scan_results
else
-- If nil, just empty it
scanlist = {}
end
refresh_view()
-- Since we're going into a new dir, move cursor to the ".." by default
move_cursor_top()
end
-- (Tries to) go back one "step" from the current directory
local function go_back_dir()
-- Use Micro's dirname to get everything but the current dir's path
local one_back_dir = filepath.Dir(current_dir)
-- Try opening, assuming they aren't at "root", by checking if it matches last dir
if one_back_dir ~= current_dir then
-- If DirectoryName returns different, then they can move back..
-- so we update the current dir and refresh
update_current_dir(one_back_dir)
end
end
-- Tries to open the current index
-- If it's the top dir indicator, or separator, nothing happens
-- If it's ".." then it tries to go back a dir
-- If it's a dir then it moves into the dir and refreshes
-- If it's actually a file, open it in a new vsplit
-- THIS EXPECTS ZERO-BASED Y
local function try_open_at_y(y)
-- 2 is the zero-based index of ".."
if y == 2 then
go_back_dir()
elseif y > 2 and not scanlist_is_empty() then
-- -2 to conform to our scanlist "missing" first 3 indicies
y = y - 2
if scanlist[y].dirmsg ~= "" then
-- if passed path is a directory, update the current dir to be one deeper..
update_current_dir(scanlist[y].abspath)
else
-- If it's a file, then open it
micro.InfoBar():Message("Filemanager opened ", scanlist[y].abspath)
-- Opens the absolute path in new vertical view
micro.CurPane():VSplitIndex(buffer.NewBufferFromFile(scanlist[y].abspath), true)
-- Resizes all views after opening a file
-- tabs[curTab + 1]:Resize()
end
else
micro.InfoBar():Error("Can't open that")
end
end
-- Opens the dir's contents nested under itself
local function uncompress_target(y)
-- Exit early if on the top 3 non-list items
if y == 0 or scanlist_is_empty() then
return
end
-- Only uncompress if it's a dir and it's not already uncompressed
if scanlist[y].dirmsg == "+" then
-- Get a new scanlist with results from the scan in the target dir
local scan_results = get_scanlist(scanlist[y].abspath, y, scanlist[y].indent + 1)
-- Don't run any of this if there's nothing in the dir we scanned, pointless
if scan_results ~= nil then
-- Will hold all the old values + new scan results
local new_table = {}
-- By not inserting in-place, some unexpected results can be avoided
-- Also, table.insert actually moves values up (???) instead of down
for i = 1, #scanlist do
-- Put the current val into our new table
new_table[#new_table + 1] = scanlist[i]
if i == y then
-- Fill in the scan results under y
for x = 1, #scan_results do
new_table[#new_table + 1] = scan_results[x]
end
-- Basically "moving down" everything below y, so ownership needs to increase on everything
for inner_i = y + 1, #scanlist do
-- When root not pushed by inserting, don't change its ownership
-- This also has a dual-purpose to make it not effect root file/dirs
-- since y is always >= 3
if scanlist[inner_i].owner > y then
-- Increase each indicies ownership by the number of scan results inserted
scanlist[inner_i]:increase_owner(#scan_results)
end
end
end
end
-- Update our scanlist with the new values
scanlist = new_table
end
-- Change to minus to signify it's uncompressed
scanlist[y].dirmsg = "-"
-- Check if we actually need to resize, or if we're nesting at the same indent
-- Also check if there's anything in the dir, as we don't need to expand on an empty dir
if scan_results ~= nil then
if scanlist[y].indent > highest_visible_indent and #scan_results >= 1 then
-- Save the new highest indent
highest_visible_indent = scanlist[y].indent
-- Increase the width to fit the new nested content
tree_view:ResizePane(tree_view:GetView().Width + scanlist[y].indent)
end
end
refresh_and_select()
end
end
-- Stat a path to check if it exists, returning true/false
local function path_exists(path)
local go_os = import("os")
-- Stat the file/dir path we created
-- file_stat should be non-nil, and stat_err should be nil on success
local file_stat, stat_err = go_os.Stat(path)
-- Check if what we tried to create exists
if stat_err ~= nil then
-- true/false if the file/dir exists
return go_os.IsExist(stat_err)
elseif file_stat ~= nil then
-- Assume it exists if no errors
return true
end
return false
end
-- Prompts for a new name, then renames the file/dir at the cursor's position
-- Not local so Micro can use it
function rename_at_cursor(new_name)
if micro.CurPane() ~= tree_view then
micro.InfoBar():Message("Rename only works with the cursor in the tree!")
return
end
-- Safety check they actually passed a name
if new_name == nil then
micro.InfoBar():Error('When using "rename" you need to input a new name')
return
end
-- +1 since Go uses zero-based indices
local y = get_safe_y()
-- Check if they're trying to rename the top stuff
if y == 0 then
-- Error since they tried to rename the top stuff
micro.InfoBar():Message("You can't rename that!")
return
end
-- The old file/dir's path
local old_path = scanlist[y].abspath
-- Join the path into their supplied rename, so that we have an absolute path
local new_path = dirname_and_join(old_path, new_name)
-- Use Go's os package for renaming the file/dir
local golib_os = import("os")
-- Actually rename the file
local log_out = golib_os.Rename(old_path, new_path)
-- Output the log, if any, of the rename
if log_out ~= nil then
micro.Log("Rename log: ", log_out)
end
-- Check if the rename worked
if not path_exists(new_path) then
micro.InfoBar():Error("Path doesn't exist after rename!")
return
end
-- NOTE: doesn't alphabetically sort after refresh, but it probably should
-- Replace the old path with the new path
scanlist[y].abspath = new_path
-- Refresh the tree with our new name
refresh_and_select()
end
-- Prompts the user for the file/dir name, then creates the file/dir using Go's os package
local function create_filedir(filedir_name, make_dir)
if micro.CurPane() ~= tree_view then
micro.InfoBar():Message("You can't create a file/dir if your cursor isn't in the tree!")
return
end
-- Safety check they passed a name
if filedir_name == nil then
micro.InfoBar():Error('You need to input a name when using "touch" or "mkdir"!')
return
end
-- The target they're trying to create on top of/in/at/whatever
local y = get_safe_y()
-- Holds the path passed to Go for the eventual new file/dir
local filedir_path
-- A true/false if scanlist is empty
local scanlist_empty = scanlist_is_empty()
-- Check there's actually anything in the list, and that they're not on the ".."
if not scanlist_empty and y ~= 0 then
-- If they're inserting on a folder, don't strip its path
if scanlist[y].dirmsg ~= "" then
-- Join our new file/dir onto the dir
filedir_path = filepath.Join(scanlist[y].abspath, filedir_name)
else
-- The current index is a file, so strip its name and join ours onto it
filedir_path = dirname_and_join(scanlist[y].abspath, filedir_name)
end
else
-- if nothing in the list, or cursor is on top of "..", use the current dir
filedir_path = filepath.Join(current_dir, filedir_name)
end
-- Check if the name is already taken by a file/dir
if path_exists(filedir_path) then
micro.InfoBar():Error("You can't create a file/dir with a pre-existing name")
return
end
-- Use Go's os package for creating the files
local golib_os = import("os")
-- Create the dir or file
if make_dir then
-- Creates the dir
golib_os.Mkdir(filedir_path, golib_os.ModePerm)
micro.Log("Filemanager created directory: " .. filedir_path)
else
-- Creates the file
golib_os.Create(filedir_path)
micro.Log("Filemanager created file: " .. filedir_path)
end
-- If the file we tried to make doesn't exist, fail
if not path_exists(filedir_path) then
micro.InfoBar():Error("The file/dir creation failed")
return
end
-- Creates a sort of default object, to be modified below
-- If creating a dir, use a "+"
local new_filedir = new_listobj(filedir_path, (make_dir and "+" or ""), 0, 0)
-- Refresh with our new value(s)
local last_y
-- Only insert to scanlist if not created into a compressed dir, since it'd be hidden if it was
-- Wrap the below checks so a y=0 doesn't break something
if not scanlist_empty and y ~= 0 then
-- +1 so it's highlighting the new file/dir
last_y = tree_view.Cursor.Loc.Y + 1
-- Only actually add the object to the list if it's not created on an uncompressed folder
if scanlist[y].dirmsg == "+" then
-- Exit early, since it was created into an uncompressed folder
return
elseif scanlist[y].dirmsg == "-" then
-- Check if created on top of an uncompressed folder
-- Change ownership to the folder it was created on top of..
-- otherwise, the ownership would be incorrect
new_filedir.owner = y
-- We insert under the folder, so increment the indent
new_filedir.indent = scanlist[y].indent + 1
else
-- This triggers if the cursor is on top of a file...
-- so we copy the properties of it
new_filedir.owner = scanlist[y].owner
new_filedir.indent = scanlist[y].indent
end
-- A temporary table for adding our new object, and manipulation
local new_table = {}
-- Insert the new file/dir, and update ownership of everything below it
for i = 1, #scanlist do
-- Don't use i as index, as it will be off by one on the next pass after below "i == y"
new_table[#new_table + 1] = scanlist[i]
if i == y then
-- Insert our new file/dir (below the last item)
new_table[#new_table + 1] = new_filedir
-- Increase ownership of everything below it, since we're inserting
-- Basically "moving down" everything below y, so ownership needs to increase on everything
for inner_i = y + 1, #scanlist do
-- When root not pushed by inserting, don't change its ownership
-- This also has a dual-purpose to make it not effect root file/dirs
-- since y is always >= 3
if scanlist[inner_i].owner > y then
-- Increase each indicies ownership by 1 since we're only inserting 1 file/dir
scanlist[inner_i]:increase_owner(1)
end
end
end
end
-- Update the scanlist with the new object & updated ownerships
scanlist = new_table
else
-- The scanlist is empty (or cursor is on ".."), so we add on our new file/dir at the bottom
scanlist[#scanlist + 1] = new_filedir
-- Add current position so it takes into account where we are
last_y = #scanlist + tree_view.Cursor.Loc.Y
end
refresh_view()
select_line(last_y)
end
-- Triggered with "touch filename"
function new_file(input_name)
-- False because not a dir
create_filedir(input_name, false)
end
-- Triggered with "mkdir dirname"
function new_dir(input_name)
-- True because dir
create_filedir(input_name, true)
end
-- open_tree setup's the view
local function open_tree()
-- Open a new Vsplit (on the very left)
micro.CurPane():VSplitIndex(buffer.NewBuffer("", "filemanager"), false)
-- Save the new view so we can access it later
tree_view = micro.CurPane()
-- Set the width of tree_view to 30% & lock it
tree_view:ResizePane(30)
-- Set the type to unsavable
-- tree_view.Buf.Type = buffer.BTLog
tree_view.Buf.Type.Scratch = true
tree_view.Buf.Type.Readonly = true
-- Set the various display settings, but only on our view (by using SetLocalOption instead of SetOption)
-- NOTE: Micro requires the true/false to be a string
-- Softwrap long strings (the file/dir paths)
tree_view.Buf:SetOptionNative("softwrap", true)
-- No line numbering
tree_view.Buf:SetOptionNative("ruler", false)
-- Is this needed with new non-savable settings from being "vtLog"?
tree_view.Buf:SetOptionNative("autosave", false)
-- Don't show the statusline to differentiate the view from normal views
tree_view.Buf:SetOptionNative("statusformatr", "")
tree_view.Buf:SetOptionNative("statusformatl", "filemanager")
tree_view.Buf:SetOptionNative("scrollbar", false)
-- Fill the scanlist, and then print its contents to tree_view
update_current_dir(os.Getwd())
end
-- close_tree will close the tree plugin view and release memory.
local function close_tree()
if tree_view ~= nil then
tree_view:Quit()
tree_view = nil
clear_messenger()
end
end
-- toggle_tree will toggle the tree view visible (create) and hide (delete).
function toggle_tree()
if tree_view == nil then
open_tree()
else
close_tree()
end
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Functions exposed specifically for the user to bind
-- Some are used in callbacks as well
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function uncompress_at_cursor()
if micro.CurPane() == tree_view then
uncompress_target(get_safe_y())
end
end
function compress_at_cursor()
if micro.CurPane() == tree_view then
-- False to not delete y
compress_target(get_safe_y(), false)
end
end
-- Goes up 1 visible directory (if any)
-- Not local so it can be bound
function goto_prev_dir()
if micro.CurPane() ~= tree_view or scanlist_is_empty() then
return
end
local cur_y = get_safe_y()
-- If they try to run it on the ".." do nothing
if cur_y ~= 0 then
local move_count = 0
for i = cur_y - 1, 1, -1 do
move_count = move_count + 1
-- If a dir, stop counting
if scanlist[i].dirmsg ~= "" then
-- Jump to its parent (the ownership)
tree_view.Cursor:UpN(move_count)
select_line()
break
end
end
end
end
-- Goes down 1 visible directory (if any)
-- Not local so it can be bound
function goto_next_dir()
if micro.CurPane() ~= tree_view or scanlist_is_empty() then
return
end
local cur_y = get_safe_y()
local move_count = 0
-- If they try to goto_next on "..", pretends the cursor is valid
if cur_y == 0 then
cur_y = 1
move_count = 1
end
-- Only do anything if it's even possible for there to be another dir
if cur_y < #scanlist then
for i = cur_y + 1, #scanlist do
move_count = move_count + 1
-- If a dir, stop counting
if scanlist[i].dirmsg ~= "" then
-- Jump to its parent (the ownership)
tree_view.Cursor:DownN(move_count)
select_line()
break
end
end
end
end
-- Goes to the parent directory (if any)
-- Not local so it can be keybound
function goto_parent_dir()
if micro.CurPane() ~= tree_view or scanlist_is_empty() then
return
end
local cur_y = get_safe_y()
-- Check if the cursor is even in a valid location for jumping to the owner
if cur_y > 0 then
-- Jump to its parent (the ownership)
tree_view.Cursor:UpN(cur_y - scanlist[cur_y].owner)
select_line()
end
end
function try_open_at_cursor()
if micro.CurPane() ~= tree_view or scanlist_is_empty() then
return
end
try_open_at_y(tree_view.Cursor.Loc.Y)
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Shorthand functions for actions to reduce repeat code
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Used to fail certain actions that we shouldn't allow on the tree_view
local function false_if_tree(view)
if view == tree_view then
return false
end
end
-- Select the line at the cursor
local function selectline_if_tree(view)
if view == tree_view then
select_line()
end
end
-- Move the cursor to the top, but don't allow the action
local function aftermove_if_tree(view)
if view == tree_view then
if tree_view.Cursor.Loc.Y < 2 then
-- If it went past the "..", move back onto it
tree_view.Cursor:DownN(2 - tree_view.Cursor.Loc.Y)
end
select_line()
end
end
local function clearselection_if_tree(view)
if view == tree_view then
-- Clear the selection when doing a find, so it doesn't copy the current line
tree_view.Cursor:ResetSelection()
end
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- All the events for certain Micro keys go below here
-- Other than things we flat-out fail
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Close current
function preQuit(view)
if view == tree_view then
-- A fake quit function
close_tree()
-- Don't actually "quit", otherwise it closes everything without saving for some reason
return false
end
end
-- Close all
function preQuitAll(view)
close_tree()
end
-- FIXME: Workaround for the weird 2-index movement on cursordown
function preCursorDown(view)
if view == tree_view then
tree_view.Cursor:Down()
select_line()
-- Don't actually go down, as it moves 2 indicies for some reason
return false
end
end
-- Up
function onCursorUp(view)
selectline_if_tree(view)
end
-- Alt-Shift-{
-- Go to target's parent directory (if exists)
function preParagraphPrevious(view)
if view == tree_view then
goto_prev_dir()
-- Don't actually do the action
return false
end
end
-- Alt-Shift-}
-- Go to next dir (if exists)
function preParagraphNext(view)
if view == tree_view then
goto_next_dir()
-- Don't actually do the action
return false
end
end
-- PageUp
function onCursorPageUp(view)
aftermove_if_tree(view)
end
-- Ctrl-Up
function onCursorStart(view)
aftermove_if_tree(view)
end
-- PageDown
function onCursorPageDown(view)
selectline_if_tree(view)
end
-- Ctrl-Down
function onCursorEnd(view)
selectline_if_tree(view)
end
function onNextSplit(view)
selectline_if_tree(view)
end
function onPreviousSplit(view)
selectline_if_tree(view)
end
-- On click, open at the click's y
function preMousePress(view, event)
if view == tree_view then
local x, y = event:Position()
-- Fixes the y because softwrap messes with it
local new_x, new_y = tree_view:GetMouseClickLocation(x, y)
-- Try to open whatever is at the click's y index
-- Will go into/back dirs based on what's clicked, nothing gets expanded
try_open_at_y(new_y)
-- Don't actually allow the mousepress to trigger, so we avoid highlighting stuff
return false
end
end
-- Up
function preCursorUp(view)
if view == tree_view then
-- Disallow selecting past the ".." in the tree
if tree_view.Cursor.Loc.Y == 2 then
return false
end
end
end
-- Left
function preCursorLeft(view)
if view == tree_view then
-- +1 because of Go's zero-based index
-- False to not delete y
compress_target(get_safe_y(), false)
-- Don't actually move the cursor, as it messes with selection
return false
end
end
-- Right
function preCursorRight(view)
if view == tree_view then
-- +1 because of Go's zero-based index
uncompress_target(get_safe_y())
-- Don't actually move the cursor, as it messes with selection
return false
end
end
-- Workaround for tab getting inserted into opened files
-- Ref https://github.com/zyedidia/micro/issues/992
local tab_pressed = false
-- Tab
function preIndentSelection(view)
if view == tree_view then
tab_pressed = true
-- Open the file
-- Using tab instead of enter, since enter won't work with Readonly
try_open_at_y(tree_view.Cursor.Loc.Y)
-- Don't actually insert a tab
return false
end
end
-- Workaround for tab getting inserted into opened files
-- Ref https://github.com/zyedidia/micro/issues/992
function preInsertTab(view)
if tab_pressed then
tab_pressed = false
return false
end
end
function preInsertNewline(view)
if view == tree_view then
return false
end
return true
end
-- CtrlL
function onJumpLine(view)
-- Highlight the line after jumping to it
-- Also moves you to index 3 (2 in zero-base) if you went to the first 2 lines
aftermove_if_tree(view)
end
-- ShiftUp
function preSelectUp(view)
if view == tree_view then
-- Go to the file/dir's parent dir (if any)
goto_parent_dir()
-- Don't actually selectup
return false
end
end
-- CtrlF
function preFind(view)
-- Since something is always selected, clear before a find
-- Prevents copying the selection into the find input
clearselection_if_tree(view)
end
-- FIXME: doesn't work for whatever reason
function onFind(view)
-- Select the whole line after a find, instead of just the input txt
selectline_if_tree(view)
end
-- CtrlN after CtrlF
function onFindNext(view)
selectline_if_tree(view)
end
-- CtrlP after CtrlF
function onFindPrevious(view)
selectline_if_tree(view)
end
-- NOTE: This is a workaround for "cd" not having its own callback
local precmd_dir
function preCommandMode(view)
precmd_dir = os.Getwd()
end
-- Update the current dir when using "cd"
function onCommandMode(view)
local new_dir = os.Getwd()
-- Only do anything if the tree is open, and they didn't cd to nothing
if tree_view ~= nil and new_dir ~= precmd_dir and new_dir ~= current_dir then
update_current_dir(new_dir)
end
end
------------------------------------------------------------------
-- Fail a bunch of useless actions
-- Some of these need to be removed (read-only makes some useless)
------------------------------------------------------------------
function preStartOfLine(view)
return false_if_tree(view)
end
function preStartOfText(view)
return false_if_tree(view)
end
function preEndOfLine(view)
return false_if_tree(view)
end
function preMoveLinesDown(view)
return false_if_tree(view)
end
function preMoveLinesUp(view)
return false_if_tree(view)
end
function preWordRight(view)
return false_if_tree(view)
end
function preWordLeft(view)
return false_if_tree(view)
end
function preSelectDown(view)
return false_if_tree(view)
end
function preSelectLeft(view)
return false_if_tree(view)
end
function preSelectRight(view)
return false_if_tree(view)
end
function preSelectWordRight(view)
return false_if_tree(view)
end
function preSelectWordLeft(view)
return false_if_tree(view)
end
function preSelectToStartOfLine(view)
return false_if_tree(view)
end
function preSelectToStartOfText(view)
return false_if_tree(view)
end
function preSelectToEndOfLine(view)
return false_if_tree(view)
end
function preSelectToStart(view)
return false_if_tree(view)
end
function preSelectToEnd(view)
return false_if_tree(view)
end
function preDeleteWordLeft(view)
return false_if_tree(view)
end
function preDeleteWordRight(view)
return false_if_tree(view)
end
function preOutdentSelection(view)
return false_if_tree(view)
end
function preOutdentLine(view)
return false_if_tree(view)
end
function preSave(view)
return false_if_tree(view)
end
function preCut(view)
return false_if_tree(view)
end
function preCutLine(view)
return false_if_tree(view)
end
function preDuplicateLine(view)
return false_if_tree(view)
end
function prePaste(view)
return false_if_tree(view)
end
function prePastePrimary(view)
return false_if_tree(view)
end
function preMouseMultiCursor(view)
return false_if_tree(view)
end
function preSpawnMultiCursor(view)
return false_if_tree(view)
end
function preSelectAll(view)
return false_if_tree(view)
end
function init()
-- Let the user disable showing of dotfiles like ".editorconfig" or ".DS_STORE"
config.RegisterCommonOption("filemanager", "showdotfiles", true)
-- Let the user disable showing files ignored by the VCS (i.e. gitignored)
config.RegisterCommonOption("filemanager", "showignored", true)
-- Let the user disable going to parent directory via left arrow key when file selected (not directory)
config.RegisterCommonOption("filemanager", "compressparent", true)
-- Let the user choose to list sub-folders first when listing the contents of a folder
config.RegisterCommonOption("filemanager", "foldersfirst", true)
-- Lets the user have the filetree auto-open any time Micro is opened
-- false by default, as it's a rather noticable user-facing change
config.RegisterCommonOption("filemanager", "openonstart", false)
-- Open/close the tree view
config.MakeCommand("tree", toggle_tree, config.NoComplete)
-- Rename the file/dir under the cursor
config.MakeCommand("rename", rename_at_cursor, config.NoComplete)
-- Create a new file
config.MakeCommand("touch", new_file, config.NoComplete)
-- Create a new dir
config.MakeCommand("mkdir", new_dir, config.NoComplete)
-- Delete a file/dir, and anything contained in it if it's a dir
config.MakeCommand("rm", prompt_delete_at_cursor, config.NoComplete)
-- Adds colors to the ".." and any dir's in the tree view via syntax highlighting
-- TODO: Change it to work with git, based on untracked/changed/added/whatever
config.AddRuntimeFile("filemanager", config.RTSyntax, "syntax.yaml")
-- NOTE: This must be below the syntax load command or coloring won't work
-- Just auto-open if the option is enabled
-- This will run when the plugin first loads
if config.GetGlobalOption("filemanager.openonstart") then
-- Check for safety on the off-chance someone's init.lua breaks this
if tree_view == nil then
open_tree()
-- Puts the cursor back in the empty view that initially spawns
-- This is so the cursor isn't sitting in the tree view at startup
micro.CurPane():NextSplit()
else
-- Log error so they can fix it
micro.Log(
"Warning: filemanager.openonstart was enabled, but somehow the tree was already open so the option was ignored."
)
end
end
end