VERSION = "3.5.1"

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 = shell.ExecCommand('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 =
			shell.ExecCommand('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 = filepath.Dir(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 filepath.Dir 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(bp, args)

	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 #args < 1 then
		micro.InfoBar():Error('When using "rename" you need to input a new name')
		return
	end

	local new_name = args[1]

	-- +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(bp, args)

	-- Safety check they actually passed a name
	if #args < 1 then
		micro.InfoBar():Error('When using "touch" you need to input a file name')
		return
	end

	local file_name = args[1]

	-- False because not a dir
	create_filedir(file_name, false)
end

-- Triggered with "mkdir dirname"
function new_dir(bp, args)

	-- Safety check they actually passed a name
	if #args < 1 then
		micro.InfoBar():Error('When using "mkdir" you need to input a dir name')
		return
	end

	local dir_name = args[1]

	-- True because dir
	create_filedir(dir_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
