diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a34d753 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# Ignore .md files, because 2 spaces at end-of-line has meaning +[*.md] +trim_trailing_whitespace = false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..46d51c4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.4.0] - 2018-10-22 + +### Fixed + +- Issues with Lua's `io.popen` on some systems by using Micro's built-in `RunShellCommand` instead, [thanks to @scottbilas](https://github.com/NicolaiSoeborg/filemanager-plugin/pull/38) + +### Added + +- Adds the option `filemanager-openonstart` to allow auto-opening the file tree when Micro is started (default OFF) + +### Changed + +- Update README's option's documentation + +## [3.3.1] - 2018-10-03 + +### Changed + +- Performance improvement by removing unnecessary refresh of the opened file, [thanks to @jackwilsdon](https://github.com/NicolaiSoeborg/filemanager-plugin/pull/37) + +## [3.3.0] - 2018-09-13 + +### Added + +- The ability to sort folders above files, [thanks to @cbrown1](https://github.com/NicolaiSoeborg/filemanager-plugin/pull/33) + +### Fixed + +- The displayed filenames are now correctly only showing their "basename" on Windows + +## [3.2.0] - 2018-02-15 + +### Added + +- The ability to go to parent directory with left arrow (when not minimizing). Thanks @avently +- The ability to jump to the `..` as a "parent directory". Thanks @avently + +## [3.1.2] - 2018-02-07 + +### Fixed + +- The minimum Micro version, which was incorrectly set to v1.4.0. Ref [issue #28](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/28) + +## [3.1.1] - 2018-02-04 + +### Fixed + +Ref https://github.com/zyedidia/micro/issues/992 for both of these fixes. + +- The syntax parser not loading correctly (mostly block comments) on opened files. **Requires Micro >= v1.4.0** +- An errant tab being inserted into the newly opened file. + +## [3.1.0] - 2018-01-30 + +### Added + +- The ability to hide dotfiles using the `filemanager-showdotfiles` option. +- The ability to hide files ignored in your VCS (aka `.gitignore`'d) using the `filemanager-showignored` option. Only works with Git at the moment. +- This `CHANGELOG.md` + +### Fixed + +- A bug with the `rm` command that caused weird, undefined behaviour to contents within the same dir as the file/dir deleted. +- Issue [#24](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/24) + +## [3.0.0] - 2018-01-10 + +### Fixed + +- Issues [#13](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/13), [#14](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/14), [#15](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/15), [#19](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/19), [#20](https://github.com/NicolaiSoeborg/filemanager-plugin/issues/20) +- The broken syntax highlighting + +### Added + +- Directory expansion/compression below itself for viewing more akin to a file tree. +- The `rm` command, which deletes the file/directory under the cursor. +- The `touch` command, which creates a file with the passed filename. +- The `mkdir` command, which creates a directory with the passed filename. +- An API, of sorts, for the user to rebind their keys to if they dislike the defaults. +- An [editorconfig](http://editorconfig.org/) file. + +### Changed + +- The view that it spawns in to read-only, which requires Micro version >= 1.3.5 +- The functionality of some keybindings (when in the view) so they work safetly, or at all, with the plugin. +- From the `enter` key to `tab` for opening/going into files/dirs (a side-effect of using the read-only setting) + +### Removed + +- The ability to use a lot of keybindings that would otherwise mess with the view, and have no benifit. +- The pointless `.gitignore` file. + +[unreleased]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.4.0...HEAD +[3.4.0]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.3.1...v3.4.0 +[3.3.1]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.3.0...v3.3.1 +[3.3.0]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.2.0...v3.3.0 +[3.2.0]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.1.2...v3.2.0 +[3.1.2]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.1.1...v3.1.2 +[3.1.1]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.1.0...v3.1.1 +[3.1.0]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v3.0.0...v3.1.0 +[3.0.0]: https://github.com/NicolaiSoeborg/filemanager-plugin/compare/v2.1.1...v3.0.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ae6fb2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Nicolai Søborg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2944c05..79d983d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ -# Filetree Plugin +# Filemanager Plugin -This is still a work in progress... +A simple plugin that allows for easy navigation of a file tree. + +![Example picture](./example.jpg?raw=true "Example") + +**Installation:** run `plugin install filemanager` and restart Micro. + +## Basics + +The top line always has the current directory's path to show you where you are.\ +The `..` near the top is used to move back a directory, from your current position. + +All directories have a `/` added to the end of it, and are syntax-highlighted as a `special` character.\ +If the directory is expanded, there will be a `+` to the left of it. If it is collapsed there will be a `-` instead. + +**NOTE:** If you change files without using the plugin, it can't know what you did. The only fix is to close and open the tree. + +### Options + +| Option | Purpose | Default | +| :--------------------------- | :----------------------------------------------------------- | :------ | +| `filemanager-showdotfiles` | Show dotfiles (hidden if false) | `true` | +| `filemanager-showignored` | Show gitignore'd files (hidden if false) | `true` | +| `filemanager-compressparent` | Collapse the parent dir when left is pressed on a child file | `true` | +| `filemanager-foldersfirst` | Sorts folders above any files | `true` | +| `filemanager-openonstart` | Automatically open the file tree when starting Micro | `false` | + +### Commands and Keybindings + +The keybindings below are the equivalent to Micro's defaults, and not actually set by the plugin. If you've changed any of those keybindings, then that key is used instead. + +If you want to [keybind](https://github.com/zyedidia/micro/blob/master/runtime/help/keybindings.md#rebinding-keys) any of the operations/commands, bind to the labeled API in the table below. + +| Command | Keybinding(s) | What it does | API for `bindings.json` | +| :------- | :------------------------- | :------------------------------------------------------------------------------------------ | :------------------------------------ | +| `tree` | - | Open/close the tree | `filemanager.toggle_tree` | +| - | Tab & MouseLeft | Open a file, or go into the directory. Goes back a dir if on `..` | `filemanager.try_open_at_cursor` | +| - | | Expand directory in tree listing | `filemanager.uncompress_at_cursor` | +| - | | Collapse directory listing | `filemanager.compress_at_cursor` | +| - | Shift ⬆ | Go to the target's parent directory | `filemanager.goto_parent_dir` | +| - | Alt Shift { | Jump to the previous directory in the view | `filemanager.goto_next_dir` | +| - | Alt Shift } | Jump to the next directory in the view | `filemanager.goto_prev_dir` | +| `rm` | - | Prompt to delete the target file/directory your cursor is on | `filemanager.prompt_delete_at_cursor` | +| `rename` | - | Rename the file/directory your cursor is on, using the passed name | `filemanager.rename_at_cursor` | +| `touch` | - | Make a new file under/into the file/directory your cursor is on, using the passed name | `filemanager.new_file` | +| `mkdir` | - | Make a new directory under/into the file/directory your cursor is on, using the passed name | `filemanager.new_dir` | + +#### Notes + +- `rename`, `touch`, and `mkdir` require a name to be passed when calling.\ + Example: `rename newnamehere`, `touch filenamehere`, `mkdir dirnamehere`.\ + If the passed name already exists in the current dir, it will cancel instead of overwriting (for safety). + +- The Ctrl w keybinding is to switch which buffer your cursor is on.\ + This isn't specific to the plugin, it's just part of Micro, but many people seem to not know this. diff --git a/example.jpg b/example.jpg new file mode 100644 index 0000000..ab5bf1c Binary files /dev/null and b/example.jpg differ diff --git a/filemanager.lua b/filemanager.lua new file mode 100644 index 0000000..2ada8e0 --- /dev/null +++ b/filemanager.lua @@ -0,0 +1,1359 @@ +VERSION = "3.4.0" + +-- Let the user disable showing of dotfiles like ".editorconfig" or ".DS_STORE" +if GetOption("filemanager-showdotfiles") == nil then + AddOption("filemanager-showdotfiles", true) +end + +-- Let the user disable showing files ignored by the VCS (i.e. gitignored) +if GetOption("filemanager-showignored") == nil then + AddOption("filemanager-showignored", true) +end + +-- Let the user disable going to parent directory via left arrow key when file selected (not directory) +if GetOption("filemanager-compressparent") == nil then + AddOption("filemanager-compressparent", true) +end + +-- Let the user choose to list sub-folders first when listing the contents of a folder +if GetOption("filemanager-foldersfirst") == nil then + AddOption("filemanager-foldersfirst", true) +end + +-- 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 +if GetOption("filemanager-openonstart") == nil then + AddOption("filemanager-openonstart", false) +end + +-- Clear out all stuff in Micro's messenger +local function clear_messenger() + messenger:Reset() + messenger:Clear() +end + +-- Holds the CurView() we're manipulating +local tree_view = nil +-- Keeps track of the current working directory +local current_dir = WorkingDirectory() +-- 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 + messenger: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 + messenger:AddLog("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 + messenger: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 = JoinPaths(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 = GetOption("filemanager-showdotfiles") + local show_ignored = GetOption("filemanager-showignored") + local folders_first = GetOption("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(JoinPaths(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.Buf.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.Buf.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 JoinPaths(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.Buf.Cursor.Loc.Y = last_y + end + elseif tree_view.Buf.Cursor.Loc.Y < 2 then + -- Put the cursor on the ".." if it's above it + tree_view.Buf.Cursor.Loc.Y = 2 + end + + -- Puts the cursor back in bounds (if it isn't) for safety + tree_view.Buf.Cursor:Relocate() + + -- Makes sure the cursor is visible (if it isn't) + -- (false) means no callback + tree_view:Center(false) + + -- Highlight the current line where the cursor is + tree_view.Buf.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.Width < 30 then + tree_view.Width = 30 + end + + -- Delete everything in the view/buffer + tree_view.Buf:remove(tree_view.Buf:Start(), tree_view.Buf:End()) + + -- Insert the top 3 things that are always there + -- Current dir + tree_view.Buf:insert(Loc(0, 0), current_dir .. "\n") + -- An ASCII separator + tree_view.Buf:insert(Loc(0, 1), repeat_str("─", tree_view.Width) .. "\n") + -- The ".." and use a newline if there are things in the current dir + tree_view.Buf:insert(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:insert(Loc(0, i + 2), display_content) + end + + -- Resizes all views after messing with ours + tabs[curTab + 1]:Resize() +end + +-- Moves the cursor to the ".." in tree_view +local function move_cursor_top() + -- 2 is the position of the ".." + tree_view.Buf.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.Buf.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 GetOption("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.Width > (30 + highest_visible_indent) then + -- Shave off some width + tree_view.Width = 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 + messenger:Error("You can't delete that") + -- Exit early if there's nothing to delete + return + end + + local yes_del, no_del = + messenger:YesNoPrompt( + "Do you want to delete the " .. (scanlist[y].dirmsg ~= "" and "dir" or "file") .. ' "' .. scanlist[y].abspath .. '"? ' + ) + + if yes_del and not no_del 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 + messenger: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 + messenger:Error("Failed deleting file/dir: ", remove_log) + end + else + messenger:Message("Nothing was deleted") + 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.Width = 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 = DirectoryName(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 + messenger:Message("Filemanager opened ", scanlist[y].abspath) + -- Opens the absolute path in new vertical view + CurView():VSplitIndex(NewBufferFromFile(scanlist[y].abspath), 1) + -- Resizes all views after opening a file + tabs[curTab + 1]:Resize() + end + else + messenger: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.Width = tree_view.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 CurView() ~= tree_view then + messenger:Message("Rename only works with the cursor in the tree!") + return + end + + -- Safety check they actually passed a name + if new_name == nil then + messenger: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 + messenger: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 + messenger:AddLog("Rename log: ", log_out) + end + + -- Check if the rename worked + if not path_exists(new_path) then + messenger: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 CurView() ~= tree_view then + messenger: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 + messenger: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 = JoinPaths(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 = JoinPaths(current_dir, filedir_name) + end + + -- Check if the name is already taken by a file/dir + if path_exists(filedir_path) then + messenger: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) + messenger:AddLog("Filemanager created directory: " .. filedir_path) + else + -- Creates the file + golib_os.Create(filedir_path) + messenger:AddLog("Filemanager created file: " .. filedir_path) + end + + -- If the file we tried to make doesn't exist, fail + if not path_exists(filedir_path) then + messenger: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.Buf.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.Buf.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) + CurView():VSplitIndex(NewBuffer("", "filemanager"), 0) + -- Save the new view so we can access it later + tree_view = CurView() + + -- Set the width of tree_view to 30% & lock it + tree_view.Width = 30 + tree_view.LockWidth = true + -- Set the type to unsavable (A "vtScratch" ViewType) + tree_view.Type.Kind = 2 + tree_view.Type.Readonly = true + tree_view.Type.Scratch = 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) + SetLocalOption("softwrap", "true", tree_view) + -- No line numbering + SetLocalOption("ruler", "false", tree_view) + -- Is this needed with new non-savable settings from being "vtLog"? + SetLocalOption("autosave", "false", tree_view) + -- Don't show the statusline to differentiate the view from normal views + SetLocalOption("statusline", "false", tree_view) + SetLocalOption("scrollbar", "false", tree_view) + + -- Fill the scanlist, and then print its contents to tree_view + update_current_dir(WorkingDirectory()) +end + +-- close_tree will close the tree plugin view and release memory. +local function close_tree() + if tree_view ~= nil then + tree_view:Quit(false) + 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 CurView() == tree_view then + uncompress_target(get_safe_y()) + end +end + +function compress_at_cursor() + if CurView() == 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 CurView() ~= 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.Buf.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 CurView() ~= 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.Buf.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 CurView() ~= 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.Buf.Cursor:UpN(cur_y - scanlist[cur_y].owner) + select_line() + end +end + +function try_open_at_cursor() + if CurView() ~= tree_view or scanlist_is_empty() then + return + end + + try_open_at_y(tree_view.Buf.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.Buf.Cursor.Loc.Y < 2 then + -- If it went past the "..", move back onto it + tree_view.Buf.Cursor:DownN(2 - tree_view.Buf.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.Buf.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.Buf.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.Buf.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.Buf.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 +-- 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 = WorkingDirectory() +end + +-- Update the current dir when using "cd" +function onCommandMode(view) + local new_dir = WorkingDirectory() + -- 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 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 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 + +-- Open/close the tree view +MakeCommand("tree", "filemanager.toggle_tree", 0) +-- Rename the file/dir under the cursor +MakeCommand("rename", "filemanager.rename_at_cursor", 0) +-- Create a new file +MakeCommand("touch", "filemanager.new_file", 0) +-- Create a new dir +MakeCommand("mkdir", "filemanager.new_dir", 0) +-- Delete a file/dir, and anything contained in it if it's a dir +MakeCommand("rm", "filemanager.prompt_delete_at_cursor", 0) +-- 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 +AddRuntimeFile("filemanager", "syntax", "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 GetOption("filemanager-openonstart") == true 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 + CurView():NextSplit(false) + else + -- Log error so they can fix it + messenger.AddLog( + "Warning: filemanager-openonstart was enabled, but somehow the tree was already open so the option was ignored." + ) + end +end diff --git a/filetree.lua b/filetree.lua deleted file mode 100644 index 16f3f11..0000000 --- a/filetree.lua +++ /dev/null @@ -1,95 +0,0 @@ -VERSION = "1.1.2" - -treeView = nil -cwd = "." - -function OpenTree() - local origNum = CurView().Num - CurView():VSplitIndex(NewBuffer("", ""), 0) - CurView().Width = 30 - CurView().LockWidth = true - tabs[curTab+1]:Resize() - - treeView = CurView() - RefreshTree() - SetFocus(origNum) -end - -function SetFocus(num) - tabs[curTab+1].CurView = num + 1 -end - -function RefreshTree() - treeView.Buf:remove(treeView.Buf:Start(), treeView.Buf:End()) - treeView.Buf:Insert(Loc(0,0), table.concat(scandir(cwd), "\n")) - treeView.Buf.Settings["softwrap"] = false - treeView.Buf.Settings["autosave"] = false - treeView.Buf.IsModified = false -end - --- When user press enter -function preInsertNewline(view) - if view == treeView then - local selected = view.Buf:Line(view.Cursor.Loc.Y) - if isDir(selected) then - cwd = cwd .. "/" .. selected - RefreshTree() - else - -- TODO: NewBuffer calls NewBufferFromString - -- ... so manually read file content: - local filename = cwd .. "/" .. selected - local filehandle = io.open(filename, "r") - if not filehandle then - TermMessage("Can't open file:", filename) - return false - end - local filecontent = filehandle:read("*all") - CurView():VSplitIndex(NewBuffer(filecontent, filename), 0) - tabs[curTab+1]:Resize() - SetFocus(CurView().Num - 1) -- set focus on opened tab - end - return false - end - return true -end - ---[[ allows for deleting files -function preDelete(view) - if view == treeView then - messenger:YesNoPrompt("Do you want to delete ...?") - return false - end - return true -end -]]-- - --- don't prompt to save tree view -function preQuit(view) - if view == treeView then - view.Buf.IsModified = false - end - return true -end - -function scandir(directory) - local i, t, popen = 0, {}, io.popen - local pfile = popen('ls -aF "'..directory..'"') - for filename in pfile:lines() do - i = i + 1 - t[i] = filename - end - pfile:close() - return t -end - -function isDir(path) - local pfile = io.popen('ls -adl "' .. cwd .. "/" .. path .. '"') - local status = false - if pfile:read(1) == "d" then - status = true - end - pfile:close() - return status -end - -MakeCommand("tree", "filetree.OpenTree", 0) diff --git a/repo.json b/repo.json new file mode 100644 index 0000000..6d142f1 --- /dev/null +++ b/repo.json @@ -0,0 +1,30 @@ +[ + { + "Name": "filemanager", + "Description": "File manager for Micro", + "Tags": ["filetree", "filemanager", "file", "manager"], + "Versions": [ + { + "Version": "2.1.1", + "Url": "https://github.com/NicolaiSoeborg/filemanager-plugin/archive/v2.1.1.zip", + "Require": { + "micro": ">=1.3.2" + } + }, + { + "Version": "3.1.0", + "Url": "https://github.com/NicolaiSoeborg/filemanager-plugin/archive/v3.1.0.zip", + "Require": { + "micro": ">=1.3.5" + } + }, + { + "Version": "3.4.0", + "Url": "https://github.com/NicolaiSoeborg/filemanager-plugin/archive/v3.4.0.zip", + "Require": { + "micro": ">=1.4.1" + } + } + ] + } +] diff --git a/syntax.yaml b/syntax.yaml new file mode 100644 index 0000000..da001cf --- /dev/null +++ b/syntax.yaml @@ -0,0 +1,9 @@ +filetype: filemanager + +detect: + filename: "^filemanager$" + +rules: + # The "..", the separator line thing, and directories + # Optionally, add this below to highlight the ascii line: "^─*$" + - special: "^\\.\\.$|\\-\\s.*|\\+\\s.*"