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