├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .python-version ├── Context.sublime-menu ├── Default.sublime-commands ├── Default.sublime-keymap ├── FileManager.py ├── FileManager.sublime-settings ├── LICENSE ├── Main.sublime-menu ├── README.md ├── Side Bar.sublime-menu ├── Tab Context.sublime-menu ├── commands ├── __init__.py ├── copy.py ├── create.py ├── create_from_selection.py ├── delete.py ├── duplicate.py ├── editto.py ├── find_in_files.py ├── fmcommand.py ├── move.py ├── open_all.py ├── open_in_browser.py ├── open_in_explorer.py ├── open_terminal.py └── rename.py ├── dependencies.json ├── libs ├── input_for_path.py ├── pathhelper.py ├── send2trash │ ├── LICENSE │ ├── __init__.py │ ├── compat.py │ ├── exceptions.py │ ├── plat_osx.py │ ├── plat_other.py │ └── plat_win.py └── sublimefunctions.py ├── messages.json ├── messages ├── 1.4.5.txt └── install.txt ├── mkdocs.yml ├── prevent_default.py ├── requirements.txt ├── tests └── test_path_helper.py └── todo /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/ export-ignore 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Set up Python 11 | uses: actions/setup-python@v1 12 | with: 13 | python-version: 3.7 14 | - name: Install black 15 | run: pip install black 16 | - name: Check formating 17 | run: black --check --exclude "/(\.git(hub)?|\.mypy_cache|\.nox|\.tox|\.venv|libs/send2trash)/" . 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | test.py 3 | __pycache__/ 4 | Thumbs.db 5 | .DS_STORE 6 | *sublime-project 7 | *sublime-workspace -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "fm_create_file_from_selection" 4 | } 5 | ] 6 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "File Manager: New File", 4 | "command": "fm_create" 5 | }, 6 | { 7 | "caption": "File Manager: Move", 8 | "command": "fm_move" 9 | }, 10 | { 11 | "caption": "File Manager: Duplicate", 12 | "command": "fm_duplicate" 13 | }, 14 | { 15 | "caption": "File Manager: Delete", 16 | "command": "fm_delete" 17 | }, 18 | { 19 | "caption": "File Manager: Open Containing Folder…", 20 | "command": "fm_open_in_explorer", 21 | }, 22 | { 23 | "caption": "File Manager: Open In Browser", 24 | "command": "fm_open_in_browser" 25 | }, 26 | { 27 | "caption": "File Manager: Open Terminal", 28 | "command": "fm_open_terminal" 29 | }, 30 | { 31 | "caption": "File Manager: Find In Files", 32 | "command": "fm_find_in_files" 33 | }, 34 | { 35 | "caption": "File Manager: Copy Name", 36 | "command": "fm_copy", 37 | "args": { 38 | "which": "name" 39 | } 40 | }, 41 | { 42 | "caption": "File Manager: Copy Absolute Path", 43 | "command": "fm_copy", 44 | "args": { 45 | "which": "absolute path" 46 | } 47 | }, 48 | { 49 | "caption": "File Manager: Copy Relative Path", 50 | "command": "fm_copy", 51 | "args": { 52 | "which": "relative path" 53 | } 54 | }, 55 | { 56 | "caption": "File Manager: Copy Path From Root", 57 | "command": "fm_copy", 58 | "args": { 59 | "which": "path from root" 60 | } 61 | }, 62 | { 63 | "caption": "File Manager: Create Template", 64 | "command": "fm_create", 65 | "args": { 66 | "paths": ["${packages}/User/.FileManager/"], 67 | "initial_text": "template." 68 | } 69 | }, 70 | { 71 | "caption": "File Manager: List Templates", 72 | "command": "fm_create", 73 | "args": { 74 | "paths": ["${packages}/User/.FileManager/"], 75 | "initial_text": "", 76 | "start_with_browser": true, 77 | "no_browser_action": true 78 | } 79 | }, 80 | { 81 | "caption": "Preferences: File Manager Settings", 82 | "command": "edit_settings", 83 | "args": { 84 | "base_file": "${packages}/FileManager/FileManager.sublime-settings", 85 | "default": "// Your settings for FileManager. See the default file to see the different options. \n{\n\t$0\n}\n" 86 | } 87 | }, 88 | { 89 | "caption": "File Manager: Create File From Selection", 90 | "command": "fm_create_file_from_selection" 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+n"], 4 | "command": "fm_create", 5 | "context": [ 6 | { "key": "package_setting.FileManager.create_keybinding_enabled" } 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /FileManager.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import sys 3 | 4 | import sublime 5 | import sublime_plugin 6 | 7 | # Clear module cache to force reloading all modules of this package. 8 | prefix = __package__ + "." # don't clear the base package 9 | for module_name in [ 10 | module_name 11 | for module_name in sys.modules 12 | if module_name.startswith(prefix) and module_name != __name__ 13 | ]: 14 | del sys.modules[module_name] 15 | prefix = None 16 | 17 | from .commands.copy import FmCopyCommand 18 | from .commands.create import FmCreaterCommand, FmCreateCommand 19 | from .commands.create_from_selection import FmCreateFileFromSelectionCommand 20 | from .commands.delete import FmDeleteCommand 21 | from .commands.duplicate import FmDuplicateCommand 22 | from .commands.editto import FmEditToTheLeftCommand, FmEditToTheRightCommand 23 | from .commands.find_in_files import FmFindInFilesCommand 24 | from .commands.move import FmMoveCommand 25 | from .commands.open_all import FmOpenAllCommand 26 | from .commands.open_in_browser import FmOpenInBrowserCommand 27 | from .commands.open_in_explorer import FmOpenInExplorerCommand 28 | from .commands.open_terminal import FmOpenTerminalCommand 29 | from .commands.rename import FmRenameCommand, FmRenamePathCommand 30 | 31 | 32 | def plugin_loaded(): 33 | settings = sublime.load_settings("FileManager.sublime-settings") 34 | # this use to be a supported setting, but we dropped it. (see #27) 35 | if settings.get("auto_close_empty_groups") is not None: 36 | # we could remove the setting automatically, and install the 37 | # package if it was set to true, but it'd be an extra source 38 | # of bugs, and it doesn't take that much effort (it's a one 39 | # time thing, so it doesn't need to be automated) 40 | sublime.error_message( 41 | "FileManager\n\n" 42 | "auto_close_empty_groups is set, but this setting is no longer " 43 | "supported.\n\n" 44 | "Auto closing empty groups (in the layout) use to be a feature " 45 | "of FileManager, but it has now moved to it's own package.\n\n" 46 | "If you still want this behaviour, you can install " 47 | "AutoCloseEmptyGroup, it's available on package control.\n\n" 48 | "To disable this warning, unset the setting " 49 | "auto_close_empty_groups in FileManager.sublime-settings (search " 50 | "for Preferences: FileManager Settings in the command palette)" 51 | ) 52 | 53 | 54 | class FmEditReplace(sublime_plugin.TextCommand): 55 | def run(self, edit, **kwargs): 56 | kwargs.get("view", self.view).replace( 57 | edit, sublime.Region(*kwargs["region"]), kwargs["text"] 58 | ) 59 | 60 | 61 | class FmListener(sublime_plugin.EventListener): 62 | def on_load(self, view): 63 | settings = view.settings() 64 | snippet = settings.get("fm_insert_snippet_on_load", None) 65 | if snippet: 66 | view.run_command("insert_snippet", {"contents": snippet}) 67 | settings.erase("fm_insert_snippet_on_load") 68 | if sublime.load_settings("FileManager.sublime-settings").get( 69 | "save_after_creating" 70 | ): 71 | view.run_command("save") 72 | if settings.get("fm_reveal_in_sidebar"): 73 | view.window().run_command("reveal_in_side_bar") 74 | 75 | def on_text_command(self, view, command, args): 76 | if ( 77 | command not in ["undo", "unindent"] 78 | or view.name() != "FileManager::input-for-path" 79 | ): 80 | return 81 | 82 | settings = view.settings() 83 | 84 | if command == "unindent": 85 | index = settings.get("completions_index") 86 | settings.set("go_backwards", True) 87 | view.run_command("insert", {"characters": "\t"}) 88 | return 89 | 90 | # command_history: (command, args, times) 91 | first = view.command_history(0) 92 | if first[0] != "fm_edit_replace" or first[2] != 1: 93 | return 94 | 95 | second = view.command_history(-1) 96 | if (second[0] != "reindent") and not ( 97 | second[0] == "insert" and second[1] == {"characters": "\t"} 98 | ): 99 | return 100 | 101 | settings.set("ran_undo", True) 102 | view.run_command("undo") 103 | 104 | index = settings.get("completions_index") 105 | if index == 0 or index is None: 106 | settings.erase("completions") 107 | settings.erase("completions_index") 108 | else: 109 | settings.set("completions_index", index - 1) 110 | -------------------------------------------------------------------------------- /FileManager.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | 3 | // when deleting files/folders, the user will be asked for 4 | // confirmation, and then the items will be sent to the trash. 5 | // set to false to remove the confirmation dialog, and send the 6 | // items to the trash straight away. 7 | "ask_for_confirmation_on_delete": true, 8 | 9 | 10 | // auto complete with files 11 | "complete_with_files_too": true, 12 | 13 | // only relevant if complete_with_files_too is true 14 | // choose folder over file if folder and file are available for 15 | // completion. Null means that it will be selected by alphabetic order 16 | // valid values: "files", "folders", "alphabetic" 17 | "pick_first": "folders", 18 | 19 | // define if the auto completion case sensitive 20 | "case_sensitive": false, 21 | 22 | // recommended to be a char that cannot be in a file name 23 | "index_folder_separator": ">", 24 | 25 | // which folder to pick for reference by default 26 | // (create folder from it) 27 | "default_index": 0, 28 | 29 | // valid value 30 | // false: disable the log 31 | // "user": display a user friendly path. eg: ~/Desktop/ (working on window) 32 | // "computer": display a computer friendly path: C:\User\\Desktop\ 33 | "log_in_status_bar": "computer", 34 | 35 | // terminals 36 | // if there is only one, it will directly open it 37 | // otherwise, it will open a quick panel with all 38 | // the name listed 39 | // example for cmder: 40 | 41 | // { "name": "Cmder", "cmd": ["C:/cmder/cmder.exe", "/SINGLE", "$cwd"] } 42 | 43 | // $cwd will be replaced by the current working directory 44 | 45 | "terminals": [ 46 | { 47 | "name": "CMD", 48 | "cmd": ["cmd"], 49 | "platform": "windows" 50 | }, 51 | { 52 | "name": "Terminal", 53 | "cmd": ["open", "-a", "Terminal", "$cwd"], 54 | "platform": "osx" 55 | }, 56 | { 57 | "name": "iTerm", 58 | "cmd": ["open", "-a", "iTerm", "$cwd"], 59 | "platform": "osx" 60 | }, 61 | { 62 | "name": "GNOME Terminal", 63 | "cmd": ["gnome-terminal"], 64 | "platform": "linux" 65 | } 66 | ], 67 | 68 | // If set to true, all the command that are disabled (in grey) 69 | // will be hidden 70 | "menu_without_distraction": true, 71 | 72 | // auto refresh the side bar when you run any action that might affect it 73 | // by default, sublime text would do it by itself, but if this is 74 | // set to true, then it will be explicitly refreshed 75 | "explicitly_refresh_sidebar": false, 76 | 77 | // if true, each time you create/rename/duplicate etc a file, it will be revealed 78 | // in the sidebar 79 | "reveal_in_sidebar": false, 80 | 81 | // Save after creating a file (because a snippet can be inserted) 82 | "save_after_creating": false, 83 | 84 | "aliases": { 85 | "st": "$packages", 86 | "des": "~/Desktop", 87 | "here": "$file_path" 88 | }, 89 | 90 | // See https://math2001.github.io/FileManager/aliases/#watch-out-for-infinite-loops 91 | "open_help_on_alias_infinite_loop": true, 92 | 93 | // Once again, to improve your speed, if there is commands 94 | // you never use, you can super easily hide them. 95 | 96 | // You can hide/show every command, and it will have no impact 97 | // on the other ones. 98 | 99 | "show_create_command": true, 100 | "show_copy_command": true, 101 | "show_delete_command": true, 102 | "show_duplicate_command": true, 103 | "show_edit_to_the_left_command": true, 104 | "show_edit_to_the_right_command": true, 105 | "show_find_in_files_command": true, 106 | "show_move_command": true, 107 | "show_open_in_explorer_command": true, 108 | "show_open_in_browser_command": true, 109 | "show_open_terminal_command": true, 110 | "show_rename_command": true, 111 | "show_rename_path_command": true, 112 | "show_create_from_selection_command": true, 113 | "show_open_all_command": true, 114 | 115 | // Set to false to disable the default alt+n key binding 116 | "create_keybinding_enabled": true 117 | 118 | } 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017-2019 Mathieu PATUREL 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": [ 5 | { 6 | "id": "package-settings", 7 | "children": [ 8 | { 9 | "caption": "FileManager", 10 | "command": "edit_settings", 11 | "args": { 12 | "base_file": "${packages}/FileManager/FileManager.sublime-settings", 13 | "default": "// Your settings for FileManager. See the default file to see the different options. \n{\n\t$0\n}\n" 14 | } 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Manager 2 | 3 | File Manager is a plugin for Sublime Text that is suppose to replace SideBarEnhancement and AdvancedNewFile. 4 | 5 | Why? Because those to plugin basically do the same thing: *They manage files from sublime text*. 6 | 7 | With this package, you can create, rename, move, duplicate and delete files or folders. You can also copy there relative/absolute path, or their name. 8 | 9 | 10 | 11 | - ["Spirit"](#spirit) 12 | - [The idea is to make you save time, not to propose you features you're never going to use.](#the-idea-is-to-make-you-save-time-not-to-propose-you-features-youre-never-going-to-use) 13 | - [Docs](#docs) 14 | - [Installation](#installation) 15 | - [Using package control](#using-package-control) 16 | - [Using the command line](#using-the-command-line) 17 | - [How to open the `README`](#how-to-open-the-readme) 18 | - [Contributing](#contributing) 19 | 20 | 21 | 22 | ## "Spirit" 23 | 24 | #### The idea is to make you save time, not to propose you features you're never going to use. 25 | 26 | > This package has as main goal to be 100% optimized. 27 | 28 | So, for example, there is an **auto completion** system (based on the folders/files, both, you choose) on every input that is showed by FileManager. Just press tab to cycle through the auto completion. 29 | 30 | > There shouldn't be 2 commands when 1 can do the job. 31 | 32 | FileManager doesn't have a command `create_new_file` and `create_new_folder`. Just `fm_create`. It opens up an input, and the last character you type in is a `/` (or a `\`), it creates a folder instead of a file. 33 | 34 | ## Docs 35 | 36 | Although they're a fair bit of information in there, the docs are still a work in progress. Here they are: [math2001.github.io/FileManager](https://math2001.github.io/FileManager). **Go have a quick look, you won't regret it** :smile: 37 | 38 | ## Installation 39 | 40 | #### Using package control 41 | 42 | 1. Open up the command palette: ctrl+shift+p 43 | 2. Search for `Package Control: Install Package` 44 | 3. Search for `FileManager` 45 | 4. Hit enter :wink: 46 | 47 | #### Using the command line 48 | 49 | If you want to contribute to this package, first thanks, and second, you should download this using `git` so that you can propose your changes. 50 | 51 | ```bash 52 | cd "%APPDATA%\Sublime Text 3\Packages" # on Windows 53 | cd ~/Library/Application\ Support/Sublime\ Text\ 3 # on Mac 54 | cd ~/.config/sublime-text-3 # on Linux 55 | 56 | git clone "https://github.com/math2001/FileManager" 57 | ``` 58 | 59 | ## How to open the [`README`](https://github.com/math2001/FileManager/blob/master/README.md) 60 | 61 | To open their README, some of the package add a command in the menus, others in the command palette, or other nowhere. None of those options are really good, especially the last one on ST3 because the packages are compressed. But, fortunately, there is plugin that exists and will **solve this problem for us** (and he has a really cute name, don't you think?): [ReadmePlease](https://packagecontrol.io/packages/ReadmePlease). :tada: 62 | 63 | ## Contributing 64 | 65 | You want to contribute? Great! There's two different things you can contribute 66 | to: 67 | 68 | 1. the package itself 69 | 2. the docs 70 | 71 | If you want to contribute to the *package*, then you're at the right place. 72 | Otherwise, please go have a look at [the contributing part of the docs][0] 73 | 74 | First, whatever you want to do, please raise an issue. Then, if you feel in a 75 | hacky mood, go ahead and code it: 76 | 77 | - create a branch: `my-feature-name` 78 | - don't hesitate to change stuff in the `.tasks` file. 79 | - Push and PR 80 | 81 | Note: This plugin is only working on Sublime Text 3. 82 | 83 | [0]: https://math2001.github.io/FileManager/contributing/ 84 | -------------------------------------------------------------------------------- /Side Bar.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "New…", 4 | "command": "fm_create", 5 | "mnemonic": "N", 6 | "args": { 7 | "paths": [] 8 | } 9 | }, 10 | { 11 | "caption": "Open All", 12 | "command": "fm_open_all", 13 | "mnemonic": "A", 14 | "args": { 15 | "files": [] 16 | } 17 | }, 18 | { 19 | "caption": "Edit to Right", 20 | "command": "fm_edit_to_the_right", 21 | "mnemonic": "R", 22 | "args": { 23 | "files": [] 24 | } 25 | }, 26 | { 27 | "caption": "Edit to Left", 28 | "command": "fm_edit_to_the_left", 29 | "mnemonic": "L", 30 | "args": { 31 | "files": [] 32 | } 33 | }, 34 | { 35 | "caption": "Rename…", 36 | "mnemonic": "R", 37 | "command": "fm_rename_path", 38 | "args": { 39 | "paths": [] 40 | } 41 | }, 42 | { 43 | "caption": "Move…", 44 | "command": "fm_move", 45 | "mnemonic": "M", 46 | "args": { 47 | "paths": [] 48 | } 49 | }, 50 | { 51 | "caption": "Duplicate…", 52 | "command": "fm_duplicate", 53 | "mnemonic": "D", 54 | "args": { 55 | "paths": [] 56 | } 57 | }, 58 | { 59 | "caption": "Delete…", 60 | "command": "fm_delete", 61 | "mnemonic": "e", 62 | "args": { 63 | "paths": [] 64 | } 65 | }, 66 | { 67 | "caption": "-" 68 | }, 69 | { 70 | "mnemonic": "C", 71 | "caption": "Copy…", 72 | "children": [ 73 | { 74 | "caption": "Name", 75 | "command": "fm_copy", 76 | "args": { 77 | "which": "name", 78 | "paths": [] 79 | } 80 | }, 81 | { 82 | "caption": "Absolute Path", 83 | "command": "fm_copy", 84 | "args": { 85 | "which": "absolute path", 86 | "paths": [] 87 | } 88 | }, 89 | { 90 | "caption": "Path From Root", 91 | "command": "fm_copy", 92 | "args": { 93 | "which": "path from root", 94 | "paths": [] 95 | } 96 | }, 97 | { 98 | "caption": "Relative Path", 99 | "command": "fm_copy", 100 | "args": { 101 | "which": "relative path", 102 | "paths": [] 103 | } 104 | } 105 | ] 106 | }, 107 | { 108 | "caption": "-" 109 | }, 110 | { 111 | "caption": "Open in Explorer", 112 | "command": "fm_open_in_explorer", 113 | "mnemonic": "O", 114 | "args": { 115 | "visible_on_platforms": ["windows", "linux"], 116 | "paths": [] 117 | } 118 | }, 119 | { 120 | "caption": "Open in Finder", 121 | "command": "fm_open_in_explorer", 122 | "mnemonic": "O", 123 | "args": { 124 | "visible_on_platforms": ["osx"], 125 | "paths": [] 126 | } 127 | }, 128 | { 129 | "mnemonic": "T", 130 | "caption": "Open Terminal Here", 131 | "command": "fm_open_terminal", 132 | "args": { 133 | "paths": [] 134 | } 135 | }, 136 | { 137 | "caption": "Open in Browser", 138 | "mnemonic": "B", 139 | "command": "fm_open_in_browser", 140 | "args": { 141 | "paths": [] 142 | } 143 | }, 144 | { 145 | "caption": "-", 146 | "id": "folder_commands", 147 | }, 148 | { 149 | "mnemonic": "F", 150 | "caption": "Find in Files", 151 | "command": "fm_find_in_files", 152 | "args": { 153 | "paths": [] 154 | } 155 | }, 156 | ] 157 | -------------------------------------------------------------------------------- /Tab Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "caption": "-", "id": "file" }, 3 | { "caption": "Open In Browser", "command": "fm_open_in_browser" }, 4 | { "caption": "-", "id": "file_manager" }, 5 | { "caption": "Rename...", "command": "fm_rename_path" }, 6 | { "caption": "Move...", "command": "fm_move" }, 7 | { "caption": "Duplicate...", "command": "fm_duplicate" }, 8 | { "caption": "Delete", "command": "fm_delete" }, 9 | { 10 | "caption": "Copy…", 11 | "mnemonic": "C", 12 | "children": [ 13 | { 14 | "caption": "Name", 15 | "command": "fm_copy", 16 | "args": { "which": "name" } 17 | }, 18 | { 19 | "caption": "Absolute Path", 20 | "command": "fm_copy", 21 | "args": { "which": "absolute path" } 22 | }, 23 | { 24 | "caption": "Path From Root", 25 | "command": "fm_copy", 26 | "args": { "which": "path from root" } 27 | }, 28 | { 29 | "caption": "Relative Path", 30 | "command": "fm_copy", 31 | "args": { "which": "relative path" } 32 | } 33 | ] 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /commands/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | """ Seperate the commands because I'm sick of this huge file """ 4 | -------------------------------------------------------------------------------- /commands/copy.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | import sublime 5 | 6 | from .fmcommand import FmWindowCommand 7 | 8 | 9 | class FmCopyCommand(FmWindowCommand): 10 | def run(self, which, paths=None): 11 | text = [] 12 | folders = self.window.folders() 13 | 14 | for path in paths or [self.window.active_view().file_name()]: 15 | if which == "name": 16 | text.append(os.path.basename(path)) 17 | elif which == "absolute path": 18 | text.append(os.path.abspath(path)) 19 | elif which == "relative path": 20 | for folder in folders: 21 | if folder in path: 22 | text.append(os.path.relpath(path, folder)) 23 | break 24 | elif which == "path from root": 25 | for folder in folders: 26 | if folder in path: 27 | norm_path = os.path.relpath(path, folder) 28 | text.append("/" + norm_path.replace(os.path.sep, "/")) 29 | break 30 | 31 | sublime.set_clipboard("\n".join(text)) 32 | -------------------------------------------------------------------------------- /commands/create.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | import sublime 5 | 6 | from ..libs.input_for_path import InputForPath 7 | from ..libs.sublimefunctions import ( 8 | get_template, 9 | refresh_sidebar, 10 | transform_aliases, 11 | ) 12 | from ..libs.pathhelper import user_friendly 13 | from .fmcommand import FmWindowCommand 14 | 15 | 16 | class FmCreaterCommand(FmWindowCommand): 17 | """Create folder(s)/files that might be required and the 18 | final ones if it doesn't exists. Finaly, opens the file""" 19 | 20 | def run(self, abspath, input_path): 21 | input_path = user_friendly(input_path) 22 | if input_path[-1] == "/": 23 | return os.makedirs(abspath, exist_ok=True) 24 | if not os.path.isfile(abspath): 25 | os.makedirs(os.path.dirname(abspath), exist_ok=True) 26 | with open(abspath, "w"): 27 | pass 28 | template = get_template(abspath) 29 | else: 30 | template = None 31 | 32 | settings = self.window.open_file(abspath).settings() 33 | if template: 34 | settings.set("fm_insert_snippet_on_load", template) 35 | 36 | refresh_sidebar(settings, self.window) 37 | 38 | if self.settings.get("reveal_in_sidebar"): 39 | settings.set("fm_reveal_in_sidebar", True) 40 | sublime.set_timeout( 41 | lambda: self.window.run_command("reveal_in_side_bar"), 500 42 | ) 43 | 44 | 45 | class FmCreateCommand(FmWindowCommand): 46 | def run( 47 | self, 48 | paths=None, 49 | initial_text="", 50 | start_with_browser=False, 51 | no_browser_action=False, 52 | ): 53 | view = self.window.active_view() 54 | 55 | self.index_folder_separator = self.settings.get("index_folder_" + "separator") 56 | self.default_index = self.settings.get("default_index") 57 | 58 | self.folders = self.window.folders() 59 | 60 | self.know_where_to_create_from = paths is not None 61 | 62 | if paths is not None: 63 | # creating from the sidebar 64 | create_from = paths[0].replace("${packages}", sublime.packages_path()) 65 | 66 | create_from = transform_aliases(self.window, create_from) 67 | 68 | # you can right-click on a file, and run `New...` 69 | if os.path.isfile(create_from): 70 | create_from = os.path.dirname(create_from) 71 | elif self.folders: 72 | # it is going to be interactive, so it'll be 73 | # understood from the input itself 74 | create_from = None 75 | elif view.file_name() is not None: 76 | create_from = os.path.dirname(view.file_name()) 77 | self.know_where_to_create_from = True 78 | else: 79 | # from home 80 | create_from = "~" 81 | 82 | self.input = InputForPath( 83 | caption="New: ", 84 | initial_text=initial_text, 85 | on_done=self.on_done, 86 | on_change=self.on_change, 87 | on_cancel=None, 88 | create_from=create_from, 89 | with_files=self.settings.get("complete_with_files_too"), 90 | pick_first=self.settings.get("pick_first"), 91 | case_sensitive=self.settings.get("case_sensitive"), 92 | log_in_status_bar=self.settings.get("log_in_status_bar"), 93 | log_template="Creating at {0}", 94 | start_with_browser=start_with_browser, 95 | no_browser_action=no_browser_action, 96 | ) 97 | 98 | def on_change(self, input_path, path_to_create_choosed_from_browsing): 99 | if path_to_create_choosed_from_browsing: 100 | # The user has browsed, we let InputForPath select the path 101 | return 102 | if self.know_where_to_create_from: 103 | return 104 | elif self.folders: 105 | splited_input = input_path.split(self.index_folder_separator, 1) 106 | if len(splited_input) == 1: 107 | index = self.default_index 108 | else: 109 | try: 110 | index = int(splited_input[0]) 111 | except ValueError: 112 | return None, input_path 113 | 114 | return self.folders[index], splited_input[-1] 115 | return "~", input_path 116 | 117 | def is_enabled(self, paths=None): 118 | return paths is None or len(paths) == 1 119 | 120 | def on_done(self, abspath, input_path): 121 | self.window.run_command( 122 | "fm_creater", {"abspath": abspath, "input_path": input_path} 123 | ) 124 | -------------------------------------------------------------------------------- /commands/create_from_selection.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | from re import compile as re_comp 4 | 5 | import sublime 6 | import sublime_plugin 7 | 8 | from ..libs.pathhelper import computer_friendly, user_friendly 9 | 10 | """ This command has been inspired at 90% by the open_url_context command 11 | AND the vintage open_file_under_selection. Thanks John!""" 12 | 13 | 14 | def is_legal_path_char(c): 15 | # XXX make this platform-specific? 16 | return c not in ' \n"|*<>{}[]()' 17 | 18 | 19 | def move_until(view, stop_char, increment, start): 20 | char = view.substr(start) 21 | while is_legal_path_char(char) and start >= 0: 22 | start += increment 23 | char = view.substr(start) 24 | return start 25 | 26 | 27 | class FmCreateFileFromSelectionCommand(sublime_plugin.TextCommand): 28 | CONTEXT_MAX_LENGTH = 50 29 | MATCH_SOURCE_ATTR = re_comp(r"(src|href) *= *$") 30 | MATCH_JS_REQUIRE = re_comp(r"require\(\s*$") 31 | MATCH_RUBY_REQUIRE = re_comp(r"require_relative\s*\(?\s*$") 32 | 33 | @property 34 | def settings(cls): 35 | try: 36 | return cls.settings_ 37 | except AttributeError: 38 | cls.settings_ = sublime.load_settings("FileManager.sublime-settings") 39 | return cls.settings_ 40 | 41 | def run(self, edit, event): 42 | base_path, input_path = self.get_path(event) 43 | abspath = computer_friendly(os.path.join(base_path, input_path)) 44 | sublime.run_command( 45 | "fm_creater", {"abspath": abspath, "input_path": input_path} 46 | ) 47 | 48 | def want_event(self): 49 | return True 50 | 51 | def get_path(self, event, for_context_menu=False): 52 | """ 53 | @return (base_path: str, relative_path: str) 54 | """ 55 | file_name = None 56 | region = self.view.sel()[0] 57 | if not region.empty(): 58 | file_name = self.view.substr(region) 59 | if "\n" in file_name: 60 | return 61 | else: 62 | syntax = self.view.settings().get("syntax").lower() 63 | call_pos = self.view.window_to_text((event["x"], event["y"])) 64 | current_line = self.view.line(call_pos) 65 | if "html" in syntax: 66 | region = self.view.extract_scope(call_pos) 67 | text = self.view.substr(sublime.Region(0, self.view.size())) 68 | text = text[: region.begin()] 69 | if self.MATCH_SOURCE_ATTR.search(text): 70 | file_name = self.view.substr(region)[:-1] 71 | # removes the " at the end, I guess this is due to the 72 | # PHP syntax definition 73 | else: 74 | return 75 | elif "python" in syntax: 76 | current_line = self.view.substr(current_line) 77 | if current_line.startswith("from ."): 78 | current_line = current_line[6:] 79 | index = current_line.find(" import") 80 | if index < 0: 81 | return 82 | current_line = current_line[:index].replace(".", "/") 83 | if current_line.startswith("/"): 84 | current_line = ".." + current_line 85 | file_name = current_line + ".py" 86 | elif current_line.startswith("import "): 87 | file_name = current_line[7:].replace(".", "/") + ".py" 88 | else: 89 | return 90 | elif "php" in syntax: 91 | current_line = self.view.substr(current_line) 92 | if not ( 93 | current_line.startswith("include ") 94 | or current_line.startswith("require ") 95 | ): 96 | return 97 | file_name = self.view.substr(self.view.extract_scope(call_pos)) 98 | elif "javascript" in syntax: 99 | # for now, it only supports require 100 | region = self.view.extract_scope(call_pos) 101 | text = self.view.substr(sublime.Region(0, self.view.size())) 102 | text = text[: region.begin()] 103 | if self.MATCH_JS_REQUIRE.search(text) is None: 104 | return 105 | # [1:-1] removes the quotes 106 | file_name = self.view.substr(region)[1:-1] 107 | if not file_name.endswith(".js"): 108 | file_name += ".js" 109 | elif "ruby" in syntax: 110 | region = self.view.extract_scope(call_pos) 111 | text = self.view.substr(sublime.Region(0, self.view.size())) 112 | text = text[: region.begin()] 113 | if self.MATCH_RUBY_REQUIRE.search(text) is None: 114 | return 115 | # [1:-1] removes the quotes 116 | file_name = self.view.substr(region)[1:-1] 117 | if not file_name.endswith(".rb"): 118 | file_name += ".rb" 119 | else: 120 | # unknown syntax 121 | return 122 | 123 | if file_name[0] in ('"', "'"): 124 | file_name = file_name[1:] 125 | if file_name[-1] in ('"', "'"): 126 | file_name = file_name[:-1] 127 | 128 | return os.path.dirname(self.view.file_name()), file_name 129 | 130 | def description(self, event): 131 | base, file_name = self.get_path(event, True) 132 | keyword = "Open" if os.path.isfile(os.path.join(base, file_name)) else "Create" 133 | while file_name.startswith("../"): 134 | file_name = file_name[3:] 135 | base = os.path.dirname(base) 136 | base, file_name = user_friendly(base), user_friendly(file_name) 137 | 138 | if len(base) + len(file_name) > self.CONTEXT_MAX_LENGTH: 139 | path = base[: self.CONTEXT_MAX_LENGTH - len(file_name) - 4] 140 | path += ".../" + file_name 141 | else: 142 | path = base + "/" + file_name 143 | return keyword + " " + path 144 | 145 | def is_visible(self, event=None): 146 | if event is None: 147 | return False 148 | return ( 149 | self.settings.get("show_create_from_selection_command") is True 150 | and self.view.file_name() is not None 151 | and self.get_path(event) is not None 152 | ) 153 | -------------------------------------------------------------------------------- /commands/delete.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | import sublime 5 | 6 | from ..libs.sublimefunctions import refresh_sidebar 7 | from ..libs.send2trash import send2trash 8 | from .fmcommand import FmWindowCommand 9 | 10 | 11 | class FmDeleteCommand(FmWindowCommand): 12 | def run(self, paths=None): 13 | self.paths = paths or [self.window.active_view().file_name()] 14 | 15 | if self.settings.get("ask_for_confirmation_on_delete"): 16 | paths_to_display = [ 17 | [ 18 | "Confirm", 19 | "Send {0} items to trash".format(len(self.paths)) 20 | if len(self.paths) > 1 21 | else "Send item to trash", 22 | ], 23 | [ 24 | "Cancel All", 25 | "Select an individual item to remove it from the deletion list", 26 | ], 27 | ] 28 | paths_to_display.extend( 29 | [os.path.basename(path), path] for path in self.paths 30 | ) 31 | 32 | self.window.show_quick_panel(paths_to_display, self.delete) 33 | 34 | else: 35 | # index 0 is like clicking on the first option of the panel 36 | # ie. confirming the deletion 37 | self.delete(index=0) 38 | 39 | def delete(self, index): 40 | if index == 0: 41 | for path in self.paths: 42 | for window in sublime.windows(): 43 | view = window.find_open_file(path) 44 | while view is not None: 45 | view.set_scratch(True) 46 | view.close() 47 | view = window.find_open_file(path) 48 | 49 | try: 50 | send2trash(path) 51 | except OSError as e: 52 | sublime.error_message("Unable to send to trash: {}".format(e)) 53 | raise OSError("Unable to send {0!r} to trash: {1}".format(path, e)) 54 | 55 | refresh_sidebar(self.settings, self.window) 56 | 57 | elif index > 1: 58 | self.paths.pop(index - 2) 59 | if self.paths: 60 | self.run(self.paths) 61 | -------------------------------------------------------------------------------- /commands/duplicate.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | import shutil 4 | 5 | import sublime 6 | 7 | from ..libs.input_for_path import InputForPath 8 | from ..libs.sublimefunctions import refresh_sidebar, yes_no_cancel_panel 9 | from ..libs.pathhelper import user_friendly 10 | from ..libs.send2trash import send2trash 11 | from .fmcommand import FmWindowCommand 12 | 13 | 14 | class FmDuplicateCommand(FmWindowCommand): 15 | def run(self, paths=None): 16 | if paths is None: 17 | self.origin = self.window.active_view().file_name() 18 | else: 19 | self.origin = paths[0] 20 | 21 | initial_path = user_friendly(self.origin) 22 | 23 | self.input = InputForPath( 24 | caption="Duplicate to: ", 25 | initial_text=initial_path, 26 | on_done=self.duplicate, 27 | on_change=None, 28 | on_cancel=None, 29 | create_from="", 30 | with_files=False, 31 | pick_first=self.settings.get("pick_first"), 32 | case_sensitive=self.settings.get("case_sensitive"), 33 | log_in_status_bar=self.settings.get("log_in_status_bar"), 34 | log_template="Duplicating at {0}", 35 | ) 36 | 37 | head = len(os.path.dirname(initial_path)) + 1 38 | filename = len(os.path.splitext(os.path.basename(initial_path))[0]) 39 | self.input.input.view.selection.clear() 40 | self.input.input.view.selection.add(sublime.Region(head, head + filename)) 41 | 42 | def duplicate(self, dst, input_path): 43 | user_friendly_path = user_friendly(dst) 44 | 45 | if os.path.abspath(self.origin) == os.path.abspath(dst): 46 | sublime.error_message("Destination is the same with the source.") 47 | return 48 | 49 | # remove right trailing slashes, because os.path.dirname('foo/bar/') 50 | # returns foo/bar rather foo/ 51 | dst = dst.rstrip("/") 52 | 53 | os.makedirs(os.path.dirname(dst), exist_ok=True) 54 | 55 | if os.path.isdir(self.origin): 56 | if not os.path.exists(dst): 57 | shutil.copytree(self.origin, dst) 58 | else: 59 | sublime.error_message("This path already exists!") 60 | raise ValueError( 61 | "Cannot move the directory {0!r} because it already exists " 62 | "{1!r}".format(self.origin, dst) 63 | ) 64 | else: 65 | if not os.path.exists(dst): 66 | shutil.copy2(self.origin, dst) 67 | self.window.open_file(dst) 68 | else: 69 | 70 | def overwrite(): 71 | try: 72 | send2trash(dst) 73 | except OSError as e: 74 | sublime.error_message("Unable to send to trash: {}".format(e)) 75 | raise OSError( 76 | "Unable to send to the trash the item {0}".format(e) 77 | ) 78 | 79 | shutil.copy2(self.origin, dst) 80 | self.window.open_file(dst) 81 | 82 | def open_file(): 83 | return self.window.open_file(dst) 84 | 85 | yes_no_cancel_panel( 86 | message=[ 87 | "This file already exists. Overwrite?", 88 | user_friendly_path, 89 | ], 90 | yes=overwrite, 91 | no=open_file, 92 | cancel=None, 93 | yes_text=[ 94 | "Yes. Overwrite", 95 | user_friendly_path, 96 | "will be sent " "to the trash, and then written", 97 | ], 98 | no_text=["Just open the target file", user_friendly_path], 99 | cancel_text=["No, don't do anything"], 100 | ) 101 | 102 | refresh_sidebar(self.settings, self.window) 103 | return 104 | 105 | def is_enabled(self, paths=None): 106 | return paths is None or len(paths) == 1 107 | -------------------------------------------------------------------------------- /commands/editto.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from .fmcommand import FmWindowCommand 3 | 4 | 5 | class FmEditToTheRightCommand(FmWindowCommand): 6 | def run(self, files=None): 7 | self.window.set_layout( 8 | { 9 | "cols": [0.0, 0.5, 1.0], 10 | "rows": [0.0, 1.0], 11 | "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], 12 | } 13 | ) 14 | for file in files or [self.window.active_view().file_name()]: 15 | self.window.set_view_index(self.window.open_file(file), 1, 0) 16 | 17 | self.window.focus_group(1) 18 | 19 | def is_enabled(self, files=None): 20 | return (files is None or len(files) >= 1) and self.window.active_group() != 1 21 | 22 | 23 | class FmEditToTheLeftCommand(FmWindowCommand): 24 | def run(self, files=None): 25 | self.window.set_layout( 26 | { 27 | "cols": [0.0, 0.5, 1.0], 28 | "rows": [0.0, 1.0], 29 | "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], 30 | } 31 | ) 32 | for file in files or [self.window.active_view().file_name()]: 33 | self.window.set_view_index(self.window.open_file(file), 0, 0) 34 | 35 | self.window.focus_group(0) 36 | 37 | def is_enabled(self, files=None): 38 | return (files is None or len(files) >= 1) and self.window.active_group() != 0 39 | -------------------------------------------------------------------------------- /commands/find_in_files.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | from .fmcommand import FmWindowCommand 5 | 6 | 7 | class FmFindInFilesCommand(FmWindowCommand): 8 | def run(self, paths=None): 9 | valid_paths = set() 10 | for path in paths or self.windows.active_view().file_name(): 11 | if os.path.isfile(path): 12 | valid_paths.add(os.path.dirname(path)) 13 | else: 14 | valid_paths.add(path) 15 | 16 | self.window.run_command( 17 | "show_panel", {"panel": "find_in_files", "where": ", ".join(valid_paths)} 18 | ) 19 | -------------------------------------------------------------------------------- /commands/fmcommand.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import sublime 3 | import sublime_plugin 4 | 5 | 6 | class FmWindowCommand(sublime_plugin.WindowCommand): 7 | @property 8 | def settings(cls): 9 | try: 10 | return cls.settings_ 11 | except AttributeError: 12 | cls.settings_ = sublime.load_settings("FileManager.sublime-settings") 13 | return cls.settings_ 14 | 15 | def is_visible(self, *args, **kwargs): 16 | name = "show_{}_command".format(self.name().replace("fm_", "")) 17 | show = self.settings.get(name) 18 | if show is None: 19 | # this should never happen, this is an error 20 | # we could nag the user to get him to report that issue, 21 | # but that's going to make this plugin really painful to use 22 | # So, I just print something to the console, and hope someone 23 | # sees and reports it 24 | print( 25 | "FileManager: No setting available for the command {!r}. This is an internal error, please report it".format( 26 | type(self).__name__ 27 | ) 28 | ) 29 | show = True 30 | 31 | return bool( 32 | show 33 | and ( 34 | not self.settings.get("menu_without_distraction") 35 | or self.is_enabled(*args, **kwargs) 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /commands/move.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | import sublime 5 | 6 | from ..libs.input_for_path import InputForPath 7 | from ..libs.pathhelper import commonpath, user_friendly 8 | from ..libs.sublimefunctions import refresh_sidebar 9 | from .fmcommand import FmWindowCommand 10 | 11 | 12 | class FmMoveCommand(FmWindowCommand): 13 | def run(self, paths=None): 14 | self.origins = paths or [self.window.active_view().file_name()] 15 | 16 | if len(self.origins) > 1: 17 | initial_text = commonpath(self.origins) 18 | else: 19 | initial_text = os.path.dirname(self.origins[0]) 20 | initial_text = user_friendly(initial_text) + "/" 21 | 22 | InputForPath( 23 | caption="Move to", 24 | initial_text=initial_text, 25 | on_done=self.move, 26 | on_change=None, 27 | on_cancel=None, 28 | create_from="", 29 | with_files=self.settings.get("complete_with_files_too"), 30 | pick_first=self.settings.get("pick_first"), 31 | case_sensitive=self.settings.get("case_sensitive"), 32 | log_in_status_bar=self.settings.get("log_in_status_bar"), 33 | log_template="Moving at {0}", 34 | browser_action={"title": "Move here", "func": self.move}, 35 | browser_index=0, 36 | ) 37 | 38 | def move(self, path, input_path): 39 | os.makedirs(path, exist_ok=True) 40 | for origin in self.origins: 41 | view = self.window.find_open_file(origin) 42 | new_name = os.path.join(path, os.path.basename(origin)) 43 | try: 44 | os.rename(origin, new_name) 45 | except Exception as e: 46 | sublime.error_message( 47 | "An error occured while moving the file " "{}".format(e) 48 | ) 49 | raise OSError( 50 | "An error occured while moving the file {0!r} " 51 | "to {1!r}".format(origin, new_name) 52 | ) 53 | if view: 54 | view.retarget(new_name) 55 | 56 | refresh_sidebar(self.settings, self.window) 57 | -------------------------------------------------------------------------------- /commands/open_all.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | from .fmcommand import FmWindowCommand 3 | 4 | 5 | class FmOpenAllCommand(FmWindowCommand): 6 | def run(self, files=[]): 7 | for file in files: 8 | self.window.open_file(file) 9 | 10 | def is_enabled(self, files=[]): 11 | return len(files) > 1 12 | -------------------------------------------------------------------------------- /commands/open_in_browser.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | import sublime 5 | 6 | from .fmcommand import FmWindowCommand 7 | 8 | 9 | class FmOpenInBrowserCommand(FmWindowCommand): 10 | def run(self, paths=None, *args, **kwargs): 11 | folders = self.window.folders() 12 | 13 | view = self.window.active_view() 14 | url = view.settings().get("url") 15 | if url is not None: 16 | url = url.strip("/") 17 | 18 | for path in paths or [view.file_name()]: 19 | if url is None: 20 | self.open_url("file:///" + path) 21 | else: 22 | for folder in folders: 23 | if folder in path: 24 | if os.path.splitext(os.path.basename(path))[0] == "index": 25 | path = os.path.dirname(path) 26 | self.open_url(url + path.replace(folder, "")) 27 | break 28 | else: 29 | self.open_url("file:///" + path) 30 | 31 | def open_url(self, url): 32 | sublime.run_command("open_url", {"url": url}) 33 | -------------------------------------------------------------------------------- /commands/open_in_explorer.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | 4 | import sublime 5 | from .fmcommand import FmWindowCommand 6 | 7 | 8 | class FmOpenInExplorerCommand(FmWindowCommand): 9 | def run(self, paths=None): 10 | # visible_on_platforms is just used by is_visible 11 | for path in paths or [self.window.active_view().file_name()]: 12 | if os.path.isdir(path): 13 | self.window.run_command("open_dir", {"dir": path}) 14 | else: 15 | dirname, basename = os.path.split(path) 16 | self.window.run_command( 17 | "open_dir", 18 | {"dir": dirname, "file": basename}, 19 | ) 20 | 21 | def is_visible(self, visible_on_platforms=None, paths=None): 22 | return super().is_visible() and ( 23 | visible_on_platforms is None or sublime.platform() in visible_on_platforms 24 | ) 25 | -------------------------------------------------------------------------------- /commands/open_terminal.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | import subprocess 4 | 5 | import sublime 6 | 7 | from ..libs.pathhelper import user_friendly 8 | from .fmcommand import FmWindowCommand 9 | 10 | 11 | class FmOpenTerminalCommand(FmWindowCommand): 12 | def run(self, paths=None): 13 | current_platform = sublime.platform() 14 | self.terminals = [ 15 | terminal 16 | for terminal in self.settings.get("terminals") 17 | if self.is_available(terminal, current_platform) 18 | ] 19 | 20 | if paths is not None: 21 | cwd = paths[0] 22 | elif self.window.folders() != []: 23 | cwd = self.window.folders()[0] 24 | else: 25 | cwd = self.window.active_view().file_name() 26 | 27 | def open_terminal_callback(index): 28 | if index == -1: 29 | return 30 | self.open_terminal( 31 | self.terminals[index]["cmd"], cwd, self.terminals[index]["name"] 32 | ) 33 | 34 | if len(self.terminals) == 1: 35 | open_terminal_callback(0) 36 | else: 37 | self.window.show_quick_panel( 38 | [term_infos["name"] for term_infos in self.terminals], 39 | open_terminal_callback, 40 | ) 41 | 42 | def is_enabled(self, paths=None): 43 | return paths is None or len(paths) == 1 44 | 45 | def is_available(self, terminal, current_platform): 46 | try: 47 | terminal["platform"] 48 | except KeyError: 49 | return True 50 | if not isinstance(terminal["platform"], str): 51 | return False 52 | 53 | platforms = terminal["platform"].lower().split(" ") 54 | return current_platform in platforms 55 | 56 | def open_terminal(self, cmd, cwd, name): 57 | if os.path.isfile(cwd): 58 | cwd = os.path.dirname(cwd) 59 | 60 | for j, bit in enumerate(cmd): 61 | cmd[j] = bit.replace("$cwd", cwd) 62 | sublime.status_message('Opening "{0}" at {1}'.format(name, user_friendly(cwd))) 63 | return subprocess.Popen(cmd, cwd=cwd) 64 | -------------------------------------------------------------------------------- /commands/rename.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | import uuid 4 | 5 | import sublime 6 | 7 | from ..libs.input_for_path import InputForPath 8 | from ..libs.pathhelper import user_friendly 9 | from ..libs.sublimefunctions import yes_no_cancel_panel, refresh_sidebar 10 | from ..libs.send2trash import send2trash 11 | from .fmcommand import FmWindowCommand 12 | 13 | 14 | class FmRenamePathCommand(FmWindowCommand): 15 | def run(self, paths=None): 16 | self.window.run_command( 17 | "rename_path", {"paths": paths or [self.window.active_view().file_name()]} 18 | ) 19 | 20 | 21 | class FmRenameCommand(FmWindowCommand): 22 | def run(self, paths=None): 23 | print( 24 | "fm_rename has been deprecated. It doesn't provide any more useful feature " 25 | "than the default command. You should use rename_path (for a " 26 | "file or a folder) or rename_file (for a file) instead." 27 | ) 28 | sublime.status_message("fm_rename has been deprecated (see console)") 29 | 30 | if paths is None: 31 | self.origin = self.window.active_view().file_name() 32 | else: 33 | self.origin = paths[0] 34 | 35 | filename, ext = os.path.splitext(os.path.basename(self.origin)) 36 | 37 | self.input = InputForPath( 38 | caption="Rename to: ", 39 | initial_text="{0}{1}".format(filename, ext), 40 | on_done=self.rename, 41 | on_change=None, 42 | on_cancel=None, 43 | create_from=os.path.dirname(self.origin), 44 | with_files=self.settings.get("complete_with_files_too"), 45 | pick_first=self.settings.get("pick_first"), 46 | case_sensitive=self.settings.get("case_sensitive"), 47 | log_in_status_bar=self.settings.get("log_in_status_bar"), 48 | log_template="Renaming to {0}", 49 | ) 50 | self.input.input.view.selection.clear() 51 | self.input.input.view.selection.add(sublime.Region(0, len(filename))) 52 | 53 | def rename(self, dst, input_dst): 54 | def is_windows_same_filesystem_name(): 55 | return ( 56 | sublime.platform() == "windows" and self.origin.lower() == dst.lower() 57 | ) 58 | 59 | def rename(): 60 | dst_dir = os.path.dirname(dst) 61 | os.makedirs(dst_dir, exist_ok=True) 62 | 63 | if is_windows_same_filesystem_name() and self.origin != dst: 64 | dst_tmp = os.path.join(dst_dir, str(uuid.uuid4())) 65 | os.rename(self.origin, dst_tmp) 66 | os.rename(dst_tmp, dst) 67 | else: 68 | os.rename(self.origin, dst) 69 | 70 | view = self.window.find_open_file(self.origin) 71 | if view: 72 | view.retarget(dst) 73 | 74 | if os.path.exists(dst) and not is_windows_same_filesystem_name(): 75 | 76 | def open_file(self): 77 | return self.window.open_file(dst) 78 | 79 | def overwrite(): 80 | try: 81 | send2trash(dst) 82 | except OSError as e: 83 | sublime.error_message("Unable to send to trash: {}".format(e)) 84 | raise OSError( 85 | "Unable to send the item {0!r} to the trash! Error {1!r}".format( 86 | dst, e 87 | ) 88 | ) 89 | 90 | rename() 91 | 92 | user_friendly_path = user_friendly(dst) 93 | return yes_no_cancel_panel( 94 | message=["This file already exists. Overwrite?", user_friendly_path], 95 | yes=overwrite, 96 | no=open_file, 97 | cancel=None, 98 | yes_text=[ 99 | "Yes. Overwrite", 100 | user_friendly_path, 101 | "will be sent to the trash, and then written", 102 | ], 103 | no_text=["Just open the target file", user_friendly_path], 104 | cancel_text=["No, don't do anything"], 105 | ) 106 | 107 | rename() 108 | refresh_sidebar(self.settings, self.window) 109 | 110 | def is_enabled(self, paths=None): 111 | return paths is None or len(paths) == 1 112 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": { 3 | "*": [ 4 | "package_setting_context" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/input_for_path.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sublime 4 | 5 | from .pathhelper import computer_friendly, user_friendly 6 | from .sublimefunctions import sm, transform_aliases 7 | 8 | 9 | def set_status(view, key, value): 10 | if view: 11 | view.set_status(key, value) 12 | else: 13 | sm(value) 14 | 15 | 16 | def get_entire_text(view): 17 | return view.substr(sublime.Region(0, view.size())) 18 | 19 | 20 | def StdClass(name="Unknown"): 21 | # add the str() function because of the unicode in Python 2 22 | return type(str(name).title(), (), {}) 23 | 24 | 25 | class InputForPath(object): 26 | STATUS_KEY = "input_for_path" 27 | 28 | def __init__( 29 | self, 30 | caption, 31 | initial_text, 32 | on_done, 33 | on_change, 34 | on_cancel, 35 | create_from, 36 | with_files, 37 | pick_first, 38 | case_sensitive, 39 | log_in_status_bar, 40 | log_template, 41 | browser_action={}, 42 | start_with_browser=False, 43 | no_browser_action=False, 44 | browser_index=None, 45 | ): 46 | self.user_on_done = on_done 47 | self.user_on_change = on_change 48 | self.user_on_cancel = on_cancel 49 | self.caption = caption 50 | self.initial_text = initial_text 51 | self.log_template = log_template 52 | self.browser_action = browser_action 53 | self.no_browser_action = no_browser_action 54 | self.browser_index = browser_index 55 | 56 | self.create_from = create_from 57 | if self.create_from: 58 | self.create_from = computer_friendly(self.create_from) 59 | if not os.path.isdir(self.create_from): 60 | if os.path.exists(self.create_from): 61 | sublime.error_message( 62 | "This path exists, but doesn't seem to be a directory. " 63 | " Please report this (see link in the console)" 64 | ) 65 | raise ValueError( 66 | "This path exists, but doesn't seem to be a directory. " 67 | "Here's the path {0}. Please report this bug here: " 68 | "https://github.com/math2001/FileManager/issues".format( 69 | self.create_from 70 | ) 71 | ) 72 | sublime.error_message( 73 | "The path `create_from` should exists. {0!r} does not " 74 | " exists.".format(self.create_from) 75 | ) 76 | raise ValueError( 77 | "The path create from does not exists ({0!r})".format( 78 | self.create_from 79 | ) 80 | ) 81 | 82 | self.browser = StdClass("Browser") 83 | self.browser.path = self.create_from 84 | self.browser.items = [] 85 | 86 | self.with_files = with_files 87 | self.pick_first = pick_first 88 | self.case_sensitive = case_sensitive 89 | 90 | self.path_to_create_choosed_from_browsing = False 91 | 92 | self.window = sublime.active_window() 93 | self.view = self.window.active_view() 94 | 95 | self.log_in_status_bar = log_in_status_bar 96 | 97 | if start_with_browser: 98 | self.browsing_on_done() 99 | else: 100 | self.create_input() 101 | 102 | def create_input(self): 103 | self.prev_input_path = None 104 | 105 | self.input = StdClass("input") 106 | self.input.view = self.window.show_input_panel( 107 | self.caption, 108 | self.initial_text, 109 | self.input_on_done, 110 | self.input_on_change, 111 | self.input_on_cancel, 112 | ) 113 | self.input.view.set_name("FileManager::input-for-path") 114 | self.input.settings = self.input.view.settings() 115 | self.input.settings.set("tab_completion", False) 116 | 117 | def __get_completion_for( 118 | self, abspath, with_files, pick_first, case_sensitive, can_add_slash 119 | ): 120 | """Return a string and list: the prefix, and the list 121 | of available completion in the right order""" 122 | 123 | def sort_in_two_list(items, key): 124 | first, second = [], [] 125 | for item in items: 126 | first_list, item = key(item) 127 | if first_list: 128 | first.append(item) 129 | else: 130 | second.append(item) 131 | return first, second 132 | 133 | abspath = computer_friendly(abspath) 134 | 135 | if abspath.endswith(os.path.sep): 136 | prefix = "" 137 | load_items_from = abspath 138 | else: 139 | load_items_from = os.path.dirname(abspath) 140 | prefix = os.path.basename(abspath) 141 | 142 | items = sorted(os.listdir(load_items_from)) 143 | items_with_right_prefix = [] 144 | 145 | if not case_sensitive: 146 | prefix = prefix.lower() 147 | 148 | for i, item in enumerate(items): 149 | if not case_sensitive: 150 | item = item.lower() 151 | if item.startswith(prefix): 152 | # I add items[i] because it's case is never changed 153 | items_with_right_prefix.append( 154 | [items[i], os.path.isdir(os.path.join(load_items_from, items[i]))] 155 | ) 156 | 157 | folders, files = sort_in_two_list( 158 | items_with_right_prefix, lambda item: [item[1], item[0]] 159 | ) 160 | if can_add_slash: 161 | folders = [folder + "/" for folder in folders] 162 | 163 | if with_files: 164 | if pick_first == "folders": 165 | return prefix, folders + files 166 | elif pick_first == "files": 167 | return prefix, files + folders 168 | elif pick_first == "alphabetic": 169 | return prefix, sorted(files + folders) 170 | else: 171 | sublime.error_message( 172 | "The keyword {0!r} to define the order of completions is " 173 | "not valid. See the default settings.".format(pick_first) 174 | ) 175 | raise ValueError( 176 | "The keyword {0!r} to define the order of completions is " 177 | "not valid. See the default settings.".format(pick_first) 178 | ) 179 | else: 180 | return prefix, folders 181 | 182 | def input_on_change(self, input_path): 183 | self.input_path = user_friendly(input_path) 184 | 185 | self.input_path = transform_aliases(self.window, self.input_path) 186 | # get changed inputs and create_from from the on_change user function 187 | if self.user_on_change: 188 | new_values = self.user_on_change( 189 | self.input_path, self.path_to_create_choosed_from_browsing 190 | ) 191 | if new_values is not None: 192 | create_from, self.input_path = new_values 193 | if create_from is not None: 194 | self.create_from = computer_friendly(create_from) 195 | 196 | def reset_settings(): 197 | self.input.settings.erase("completions") 198 | self.input.settings.erase("completions_index") 199 | 200 | def replace_with_completion(completions, index, prefix=None): 201 | # replace the previous completion 202 | # with the new one (completions[index+1]) 203 | region = [self.input.view.sel()[0].begin()] 204 | # -1 because of the \t 205 | region.append( 206 | region[0] 207 | - len(prefix if prefix is not None else completions[index]) 208 | - 1 209 | ) 210 | if self.input.settings.get("go_backwards") is True: 211 | index -= 1 212 | self.input.settings.erase("go_backwards") 213 | else: 214 | index += 1 215 | self.input.settings.set("completions_index", index) 216 | # Running fm_edit_replace will trigger this function 217 | # and because it is not going to find any \t 218 | # it's going to erase the settings 219 | # Adding this will prevent this behaviour 220 | self.input.settings.set("just_completed", True) 221 | self.input.view.run_command( 222 | "fm_edit_replace", {"region": region, "text": completions[index]} 223 | ) 224 | self.prev_input_path = self.input.view.substr( 225 | sublime.Region(0, self.input.view.size()) 226 | ) 227 | 228 | if self.log_in_status_bar: 229 | path = computer_friendly( 230 | os.path.normpath( 231 | os.path.join(self.create_from, computer_friendly(self.input_path)) 232 | ) 233 | ) 234 | if self.input_path != "" and self.input_path[-1] == "/": 235 | path += os.path.sep 236 | if self.log_in_status_bar == "user": 237 | path = user_friendly(path) 238 | set_status(self.view, self.STATUS_KEY, self.log_template.format(path)) 239 | 240 | if not hasattr(self.input, "settings"): 241 | return 242 | 243 | if self.input.settings.get("ran_undo", False) is True: 244 | return self.input.settings.erase("ran_undo") 245 | 246 | completions = self.input.settings.get("completions", None) 247 | index = self.input.settings.get("completions_index", None) 248 | 249 | if index == 0 and len(completions) == 1: 250 | reset_settings() 251 | return 252 | 253 | if completions is not None and index is not None: 254 | # check if the user typed something after the completion 255 | text = input_path 256 | if text[-1] == "\t": 257 | text = text[:-1] 258 | if not text.endswith(tuple(completions)): 259 | return reset_settings() 260 | if "\t" in input_path: 261 | # there is still some completions available 262 | if len(completions) - 1 > index: 263 | return replace_with_completion(completions, index) 264 | if "\t" in input_path: 265 | before, after = self.input_path.split("\t", 1) 266 | prefix, completions = self.__get_completion_for( 267 | abspath=computer_friendly(os.path.join(self.create_from, before)), 268 | with_files=self.with_files, 269 | pick_first=self.pick_first, 270 | case_sensitive=self.case_sensitive, 271 | can_add_slash=after == "" or after[0] != "/", 272 | ) 273 | 274 | if not completions: 275 | return 276 | 277 | self.input.settings.set("completions", completions) 278 | self.input.settings.set("completions_index", -1) 279 | 280 | replace_with_completion(completions, -1, prefix) 281 | 282 | def input_on_done(self, input_path): 283 | if self.log_in_status_bar: 284 | set_status(self.view, self.STATUS_KEY, "") 285 | # use the one returned by the on change function 286 | input_path = self.input_path 287 | computer_path = computer_friendly(os.path.join(self.create_from, input_path)) 288 | # open browser 289 | if os.path.isdir(computer_path): 290 | self.browser.path = computer_path 291 | return self.browsing_on_done() 292 | else: 293 | self.user_on_done(computer_path, input_path) 294 | 295 | def input_on_cancel(self): 296 | active_view = self.window.active_view() 297 | if active_view.file_name() in [ 298 | os.path.join(self.browser.path, item) for item in self.browser.items 299 | ]: 300 | active_view.close() 301 | set_status(self.view, self.STATUS_KEY, "") 302 | if self.user_on_cancel: 303 | self.user_on_cancel() 304 | 305 | def open_in_transient(self, index): 306 | if self.no_browser_action is False and index < 2: 307 | return 308 | if not os.path.isfile( 309 | os.path.join(self.browser.path, self.browser.items[index]) 310 | ): 311 | return 312 | self.window.open_file( 313 | os.path.join(self.browser.path, self.browser.items[index]), 314 | sublime.TRANSIENT, 315 | ) 316 | 317 | def browsing_on_done(self, index=None): 318 | if index == -1: 319 | return self.input_on_cancel() 320 | 321 | if self.no_browser_action is False and index == 0: 322 | # create from the position in the browser 323 | self.create_from = self.browser.path 324 | self.path_to_create_choosed_from_browsing = True 325 | if self.browser_action.get("func", None) is None: 326 | return self.create_input() 327 | else: 328 | return self.browser_action["func"](self.create_from, None) 329 | elif (self.no_browser_action is True and index == 0) or ( 330 | index == 1 and self.no_browser_action is False 331 | ): 332 | self.browser.path = os.path.normpath(os.path.join(self.browser.path, "..")) 333 | elif index is not None: 334 | self.browser.path = os.path.join( 335 | self.browser.path, self.browser.items[index] 336 | ) 337 | 338 | if os.path.isfile(self.browser.path): 339 | set_status(self.view, self.STATUS_KEY, "") 340 | return self.window.open_file(self.browser.path) 341 | 342 | folders, files = [], [] 343 | for item in os.listdir(self.browser.path): 344 | if os.path.isdir(os.path.join(self.browser.path, item)): 345 | folders.append(item + "/") 346 | else: 347 | files.append(item) 348 | 349 | if self.no_browser_action: 350 | self.browser.items = ["[cmd] .."] + folders + files 351 | elif self.browser_action.get("title", None) is not None: 352 | self.browser.items = ( 353 | ["[cmd] " + self.browser_action["title"], "[cmd] .."] + folders + files 354 | ) 355 | else: 356 | self.browser.items = ( 357 | ["[cmd] Create from here", "[cmd] .."] + folders + files 358 | ) 359 | 360 | set_status( 361 | self.view, 362 | self.STATUS_KEY, 363 | "Browsing at: {0}".format(user_friendly(self.browser.path)), 364 | ) 365 | if self.browser_index is not None: 366 | index = self.browser_index 367 | elif self.no_browser_action: 368 | index = 0 369 | elif len(folders) + len(files) == 0: 370 | # browser actions are enabled, but there just aren't any folder or 371 | # files 372 | index = 0 373 | else: 374 | index = 2 375 | 376 | self.window.show_quick_panel( 377 | self.browser.items, 378 | self.browsing_on_done, 379 | sublime.KEEP_OPEN_ON_FOCUS_LOST, 380 | index, 381 | self.open_in_transient, 382 | ) 383 | -------------------------------------------------------------------------------- /libs/pathhelper.py: -------------------------------------------------------------------------------- 1 | import genericpath 2 | import os 3 | 4 | 5 | def user_friendly(path): 6 | path = computer_friendly(path) 7 | return path.replace(os.path.expanduser("~"), "~").replace(os.path.sep, "/") 8 | 9 | 10 | def computer_friendly(path): 11 | """Also makes sure the path is valid""" 12 | if "~" in path: 13 | path = path[path.rfind("~") :] 14 | if ":" in path: 15 | path = path[path.rfind(":") - 1 :] 16 | path = path.replace("~", os.path.expanduser("~")) 17 | path = path.replace("/", os.path.sep) 18 | return path 19 | 20 | 21 | def commonpath(paths): 22 | """Given a sequence of path names, returns the longest common sub-path.""" 23 | 24 | if not paths: 25 | raise ValueError("commonpath() arg is an empty sequence") 26 | 27 | sep = os.sep 28 | altsep = "/" 29 | curdir = "." 30 | 31 | try: 32 | drivesplits = [ 33 | os.path.splitdrive(p.replace(altsep, sep).lower()) for p in paths 34 | ] 35 | split_paths = [p.split(sep) for d, p in drivesplits] 36 | 37 | try: 38 | (isabs,) = set(p[:1] == sep for d, p in drivesplits) 39 | except ValueError: 40 | raise ValueError("Can't mix absolute and relative paths") 41 | 42 | # Check that all drive letters or UNC paths match. The check is made 43 | # only now otherwise type errors for mixing strings and bytes would not 44 | # be caught. 45 | if len(set(d for d, p in drivesplits)) != 1: 46 | raise ValueError("Paths don't have the same drive") 47 | 48 | drive, path = os.path.splitdrive(paths[0].replace(altsep, sep)) 49 | common = path.split(sep) 50 | common = [c for c in common if c and c != curdir] 51 | 52 | split_paths = [[c for c in s if c and c != curdir] for s in split_paths] 53 | s1 = min(split_paths) 54 | s2 = max(split_paths) 55 | for i, c in enumerate(s1): 56 | if c != s2[i]: 57 | common = common[:i] 58 | break 59 | else: 60 | common = common[: len(s1)] 61 | 62 | prefix = drive + sep if isabs else drive 63 | return prefix + sep.join(common) 64 | except (TypeError, AttributeError): 65 | genericpath._check_arg_types("commonpath", *paths) 66 | raise 67 | -------------------------------------------------------------------------------- /libs/send2trash/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Virgil Dupras 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /libs/send2trash/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Hardcoded Software (http://www.hardcoded.net) 2 | 3 | # This software is licensed under the "BSD" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.hardcoded.net/licenses/bsd_license 6 | 7 | import sys 8 | 9 | from .exceptions import TrashPermissionError 10 | 11 | if sys.platform == 'darwin': 12 | from .plat_osx import send2trash 13 | elif sys.platform == 'win32': 14 | from .plat_win import send2trash 15 | else: 16 | try: 17 | # If we can use gio, let's use it 18 | from .plat_gio import send2trash 19 | except ImportError: 20 | # Oh well, let's fallback to our own Freedesktop trash implementation 21 | from .plat_other import send2trash 22 | -------------------------------------------------------------------------------- /libs/send2trash/compat.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Virgil Dupras 2 | 3 | # This software is licensed under the "BSD" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.hardcoded.net/licenses/bsd_license 6 | 7 | import sys 8 | import os 9 | 10 | PY3 = sys.version_info[0] >= 3 11 | if PY3: 12 | text_type = str 13 | binary_type = bytes 14 | if os.supports_bytes_environ: 15 | # environb will be unset under Windows, but then again we're not supposed to use it. 16 | environb = os.environb 17 | else: 18 | text_type = unicode 19 | binary_type = str 20 | environb = os.environ 21 | -------------------------------------------------------------------------------- /libs/send2trash/exceptions.py: -------------------------------------------------------------------------------- 1 | import errno 2 | from .compat import PY3 3 | 4 | if PY3: 5 | _permission_error = PermissionError 6 | else: 7 | _permission_error = OSError 8 | 9 | class TrashPermissionError(_permission_error): 10 | """A permission error specific to a trash directory. 11 | 12 | Raising this error indicates that permissions prevent us efficiently 13 | trashing a file, although we might still have permission to delete it. 14 | This is *not* used when permissions prevent removing the file itself: 15 | that will be raised as a regular PermissionError (OSError on Python 2). 16 | 17 | Application code that catches this may try to simply delete the file, 18 | or prompt the user to decide, or (on Freedesktop platforms), move it to 19 | 'home trash' as a fallback. This last option probably involves copying the 20 | data between partitions, devices, or network drives, so we don't do it as 21 | a fallback. 22 | """ 23 | def __init__(self, filename): 24 | _permission_error.__init__(self, errno.EACCES, "Permission denied", 25 | filename) 26 | -------------------------------------------------------------------------------- /libs/send2trash/plat_osx.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Virgil Dupras 2 | 3 | # This software is licensed under the "BSD" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.hardcoded.net/licenses/bsd_license 6 | 7 | from __future__ import unicode_literals 8 | 9 | from ctypes import cdll, byref, Structure, c_char, c_char_p 10 | from ctypes.util import find_library 11 | 12 | from .compat import binary_type 13 | 14 | Foundation = cdll.LoadLibrary(find_library('Foundation')) 15 | CoreServices = cdll.LoadLibrary(find_library('CoreServices')) 16 | 17 | GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString 18 | GetMacOSStatusCommentString.restype = c_char_p 19 | FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions 20 | FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync 21 | 22 | kFSPathMakeRefDefaultOptions = 0 23 | kFSPathMakeRefDoNotFollowLeafSymlink = 0x01 24 | 25 | kFSFileOperationDefaultOptions = 0 26 | kFSFileOperationOverwrite = 0x01 27 | kFSFileOperationSkipSourcePermissionErrors = 0x02 28 | kFSFileOperationDoNotMoveAcrossVolumes = 0x04 29 | kFSFileOperationSkipPreflight = 0x08 30 | 31 | class FSRef(Structure): 32 | _fields_ = [('hidden', c_char * 80)] 33 | 34 | def check_op_result(op_result): 35 | if op_result: 36 | msg = GetMacOSStatusCommentString(op_result).decode('utf-8') 37 | raise OSError(msg) 38 | 39 | def send2trash(path): 40 | if not isinstance(path, binary_type): 41 | path = path.encode('utf-8') 42 | fp = FSRef() 43 | opts = kFSPathMakeRefDoNotFollowLeafSymlink 44 | op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) 45 | check_op_result(op_result) 46 | opts = kFSFileOperationDefaultOptions 47 | op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) 48 | check_op_result(op_result) 49 | -------------------------------------------------------------------------------- /libs/send2trash/plat_other.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Virgil Dupras 2 | 3 | # This software is licensed under the "BSD" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.hardcoded.net/licenses/bsd_license 6 | 7 | # This is a reimplementation of plat_other.py with reference to the 8 | # freedesktop.org trash specification: 9 | # [1] http://www.freedesktop.org/wiki/Specifications/trash-spec 10 | # [2] http://www.ramendik.ru/docs/trashspec.html 11 | # See also: 12 | # [3] http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 13 | # 14 | # For external volumes this implementation will raise an exception if it can't 15 | # find or create the user's trash directory. 16 | 17 | from __future__ import unicode_literals 18 | 19 | import errno 20 | import sys 21 | import os 22 | import os.path as op 23 | from datetime import datetime 24 | import stat 25 | try: 26 | from urllib.parse import quote 27 | except ImportError: 28 | # Python 2 29 | from urllib import quote 30 | 31 | from .compat import text_type, environb 32 | from .exceptions import TrashPermissionError 33 | 34 | try: 35 | fsencode = os.fsencode # Python 3 36 | fsdecode = os.fsdecode 37 | except AttributeError: 38 | def fsencode(u): # Python 2 39 | return u.encode(sys.getfilesystemencoding()) 40 | def fsdecode(b): 41 | return b.decode(sys.getfilesystemencoding()) 42 | # The Python 3 versions are a bit smarter, handling surrogate escapes, 43 | # but these should work in most cases. 44 | 45 | FILES_DIR = b'files' 46 | INFO_DIR = b'info' 47 | INFO_SUFFIX = b'.trashinfo' 48 | 49 | # Default of ~/.local/share [3] 50 | XDG_DATA_HOME = op.expanduser(environb.get(b'XDG_DATA_HOME', b'~/.local/share')) 51 | HOMETRASH_B = op.join(XDG_DATA_HOME, b'Trash') 52 | HOMETRASH = fsdecode(HOMETRASH_B) 53 | 54 | uid = os.getuid() 55 | TOPDIR_TRASH = b'.Trash' 56 | TOPDIR_FALLBACK = b'.Trash-' + text_type(uid).encode('ascii') 57 | 58 | def is_parent(parent, path): 59 | path = op.realpath(path) # In case it's a symlink 60 | if isinstance(path, text_type): 61 | path = fsencode(path) 62 | parent = op.realpath(parent) 63 | if isinstance(parent, text_type): 64 | parent = fsencode(parent) 65 | return path.startswith(parent) 66 | 67 | def format_date(date): 68 | return date.strftime("%Y-%m-%dT%H:%M:%S") 69 | 70 | def info_for(src, topdir): 71 | # ...it MUST not include a ".." directory, and for files not "under" that 72 | # directory, absolute pathnames must be used. [2] 73 | if topdir is None or not is_parent(topdir, src): 74 | src = op.abspath(src) 75 | else: 76 | src = op.relpath(src, topdir) 77 | 78 | info = "[Trash Info]\n" 79 | info += "Path=" + quote(src) + "\n" 80 | info += "DeletionDate=" + format_date(datetime.now()) + "\n" 81 | return info 82 | 83 | def check_create(dir): 84 | # use 0700 for paths [3] 85 | if not op.exists(dir): 86 | os.makedirs(dir, 0o700) 87 | 88 | def trash_move(src, dst, topdir=None): 89 | filename = op.basename(src) 90 | filespath = op.join(dst, FILES_DIR) 91 | infopath = op.join(dst, INFO_DIR) 92 | base_name, ext = op.splitext(filename) 93 | 94 | counter = 0 95 | destname = filename 96 | while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)): 97 | counter += 1 98 | destname = base_name + b' ' + text_type(counter).encode('ascii') + ext 99 | 100 | check_create(filespath) 101 | check_create(infopath) 102 | 103 | os.rename(src, op.join(filespath, destname)) 104 | f = open(op.join(infopath, destname + INFO_SUFFIX), 'w') 105 | f.write(info_for(src, topdir)) 106 | f.close() 107 | 108 | def find_mount_point(path): 109 | # Even if something's wrong, "/" is a mount point, so the loop will exit. 110 | # Use realpath in case it's a symlink 111 | path = op.realpath(path) # Required to avoid infinite loop 112 | while not op.ismount(path): 113 | path = op.split(path)[0] 114 | return path 115 | 116 | def find_ext_volume_global_trash(volume_root): 117 | # from [2] Trash directories (1) check for a .Trash dir with the right 118 | # permissions set. 119 | trash_dir = op.join(volume_root, TOPDIR_TRASH) 120 | if not op.exists(trash_dir): 121 | return None 122 | 123 | mode = os.lstat(trash_dir).st_mode 124 | # vol/.Trash must be a directory, cannot be a symlink, and must have the 125 | # sticky bit set. 126 | if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX): 127 | return None 128 | 129 | trash_dir = op.join(trash_dir, text_type(uid).encode('ascii')) 130 | try: 131 | check_create(trash_dir) 132 | except OSError: 133 | return None 134 | return trash_dir 135 | 136 | def find_ext_volume_fallback_trash(volume_root): 137 | # from [2] Trash directories (1) create a .Trash-$uid dir. 138 | trash_dir = op.join(volume_root, TOPDIR_FALLBACK) 139 | # Try to make the directory, if we lack permission, raise TrashPermissionError 140 | try: 141 | check_create(trash_dir) 142 | except OSError as e: 143 | if e.errno == errno.EACCES: 144 | raise TrashPermissionError(e.filename) 145 | raise 146 | return trash_dir 147 | 148 | def find_ext_volume_trash(volume_root): 149 | trash_dir = find_ext_volume_global_trash(volume_root) 150 | if trash_dir is None: 151 | trash_dir = find_ext_volume_fallback_trash(volume_root) 152 | return trash_dir 153 | 154 | # Pull this out so it's easy to stub (to avoid stubbing lstat itself) 155 | def get_dev(path): 156 | return os.lstat(path).st_dev 157 | 158 | def send2trash(path): 159 | if isinstance(path, text_type): 160 | path_b = fsencode(path) 161 | elif isinstance(path, bytes): 162 | path_b = path 163 | elif hasattr(path, '__fspath__'): 164 | # Python 3.6 PathLike protocol 165 | return send2trash(path.__fspath__()) 166 | else: 167 | raise TypeError('str, bytes or PathLike expected, not %r' % type(path)) 168 | 169 | if not op.exists(path_b): 170 | raise OSError("File not found: %s" % path) 171 | # ...should check whether the user has the necessary permissions to delete 172 | # it, before starting the trashing operation itself. [2] 173 | if not os.access(path_b, os.W_OK): 174 | raise OSError("Permission denied: %s" % path) 175 | # if the file to be trashed is on the same device as HOMETRASH we 176 | # want to move it there. 177 | path_dev = get_dev(path_b) 178 | 179 | # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the 180 | # home directory, and these paths will be created further on if needed. 181 | trash_dev = get_dev(op.expanduser(b'~')) 182 | 183 | if path_dev == trash_dev: 184 | topdir = XDG_DATA_HOME 185 | dest_trash = HOMETRASH_B 186 | else: 187 | topdir = find_mount_point(path_b) 188 | trash_dev = get_dev(topdir) 189 | if trash_dev != path_dev: 190 | raise OSError("Couldn't find mount point for %s" % path) 191 | dest_trash = find_ext_volume_trash(topdir) 192 | trash_move(path_b, dest_trash, topdir) 193 | -------------------------------------------------------------------------------- /libs/send2trash/plat_win.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Virgil Dupras 2 | 3 | # This software is licensed under the "BSD" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.hardcoded.net/licenses/bsd_license 6 | 7 | from __future__ import unicode_literals 8 | 9 | from ctypes import (windll, Structure, byref, c_uint, 10 | create_unicode_buffer, addressof) 11 | from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL 12 | import os.path as op 13 | 14 | from .compat import text_type 15 | 16 | kernel32 = windll.kernel32 17 | GetShortPathNameW = kernel32.GetShortPathNameW 18 | 19 | shell32 = windll.shell32 20 | SHFileOperationW = shell32.SHFileOperationW 21 | 22 | 23 | class SHFILEOPSTRUCTW(Structure): 24 | _fields_ = [ 25 | ("hwnd", HWND), 26 | ("wFunc", UINT), 27 | ("pFrom", LPCWSTR), 28 | ("pTo", LPCWSTR), 29 | ("fFlags", c_uint), 30 | ("fAnyOperationsAborted", BOOL), 31 | ("hNameMappings", c_uint), 32 | ("lpszProgressTitle", LPCWSTR), 33 | ] 34 | 35 | 36 | FO_MOVE = 1 37 | FO_COPY = 2 38 | FO_DELETE = 3 39 | FO_RENAME = 4 40 | 41 | FOF_MULTIDESTFILES = 1 42 | FOF_SILENT = 4 43 | FOF_NOCONFIRMATION = 16 44 | FOF_ALLOWUNDO = 64 45 | FOF_NOERRORUI = 1024 46 | 47 | 48 | def get_short_path_name(long_name): 49 | if not long_name.startswith('\\\\?\\'): 50 | long_name = '\\\\?\\' + long_name 51 | buf_size = GetShortPathNameW(long_name, None, 0) 52 | output = create_unicode_buffer(buf_size) 53 | GetShortPathNameW(long_name, output, buf_size) 54 | return output.value[4:] # Remove '\\?\' for SHFileOperationW 55 | 56 | 57 | def send2trash(path): 58 | if not isinstance(path, text_type): 59 | path = text_type(path, 'mbcs') 60 | if not op.isabs(path): 61 | path = op.abspath(path) 62 | path = get_short_path_name(path) 63 | fileop = SHFILEOPSTRUCTW() 64 | fileop.hwnd = 0 65 | fileop.wFunc = FO_DELETE 66 | # FIX: https://github.com/hsoft/send2trash/issues/17 67 | # Starting in python 3.6.3 it is no longer possible to use: 68 | # LPCWSTR(path + '\0') directly as embedded null characters are no longer 69 | # allowed in strings 70 | # Workaround 71 | # - create buffer of c_wchar[] (LPCWSTR is based on this type) 72 | # - buffer is two c_wchar characters longer (double null terminator) 73 | # - cast the address of the buffer to a LPCWSTR 74 | # NOTE: based on how python allocates memory for these types they should 75 | # always be zero, if this is ever not true we can go back to explicitly 76 | # setting the last two characters to null using buffer[index] = '\0'. 77 | buffer = create_unicode_buffer(path, len(path)+2) 78 | fileop.pFrom = LPCWSTR(addressof(buffer)) 79 | fileop.pTo = None 80 | fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT 81 | fileop.fAnyOperationsAborted = 0 82 | fileop.hNameMappings = 0 83 | fileop.lpszProgressTitle = None 84 | result = SHFileOperationW(byref(fileop)) 85 | if result: 86 | raise WindowsError(None, None, path, result) 87 | -------------------------------------------------------------------------------- /libs/sublimefunctions.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sublime 4 | 5 | TEMPLATE_FOLDER = None 6 | 7 | 8 | def md(*t, **kwargs): 9 | sublime.message_dialog(kwargs.get("sep", "\n").join([str(el) for el in t])) 10 | 11 | 12 | def sm(*t, **kwargs): 13 | sublime.status_message(kwargs.get("sep", " ").join([str(el) for el in t])) 14 | 15 | 16 | def em(*t, **kwargs): 17 | sublime.error_message(kwargs.get("sep", " ").join([str(el) for el in t])) 18 | 19 | 20 | def isST3(): 21 | return int(sublime.version()) > 3000 22 | 23 | 24 | def get_settings(): 25 | return sublime.load_settings("FileManager.sublime-settings") 26 | 27 | 28 | def refresh_sidebar(settings=None, window=None): 29 | if window is None: 30 | window = sublime.active_window() 31 | if settings is None: 32 | settings = window.active_view().settings() 33 | if settings.get("explicitly_refresh_sidebar") is True: 34 | window.run_command("refresh_folder_list") 35 | 36 | 37 | def get_template(created_file): 38 | """Return the right template for the create file""" 39 | global TEMPLATE_FOLDER 40 | 41 | if TEMPLATE_FOLDER is None: 42 | TEMPLATE_FOLDER = os.path.join(sublime.packages_path(), "User", ".FileManager") 43 | os.makedirs(TEMPLATE_FOLDER, exist_ok=True) 44 | 45 | template_files = os.listdir(TEMPLATE_FOLDER) 46 | for item in template_files: 47 | if ( 48 | os.path.splitext(item)[0] == "template" 49 | and os.path.splitext(item)[1] == os.path.splitext(created_file)[1] 50 | ): 51 | with open(os.path.join(TEMPLATE_FOLDER, item)) as fp: 52 | return fp.read() 53 | return "" 54 | 55 | 56 | def yes_no_cancel_panel( 57 | message, 58 | yes, 59 | no, 60 | cancel, 61 | yes_text="Yes", 62 | no_text="No", 63 | cancel_text="Cancel", 64 | **kwargs 65 | ): 66 | loc = locals() 67 | if isinstance(message, list): 68 | message.append("Do not select this item") 69 | else: 70 | message = [message, "Do not select this item"] 71 | items = [message, yes_text, no_text, cancel_text] 72 | 73 | def get_max(item): 74 | return len(item) 75 | 76 | maxi = len(max(items, key=get_max)) 77 | for i, item in enumerate(items): 78 | while len(items[i]) < maxi: 79 | items[i].append("") 80 | 81 | def on_done(index): 82 | if index in [-1, 3] and cancel: 83 | return cancel(*kwargs.get("args", []), **kwargs.get("kwargs", {})) 84 | elif index == 1 and yes: 85 | return yes(*kwargs.get("args", []), **kwargs.get("kwargs", {})) 86 | elif index == 2 and no: 87 | return no(*kwargs.get("args", []), **kwargs.get("kwargs", {})) 88 | elif index == 0: 89 | return yes_no_cancel_panel(**loc) 90 | 91 | window = sublime.active_window() 92 | window.show_quick_panel(items, on_done, 0, 1) 93 | 94 | 95 | def transform_aliases(window, string): 96 | """Transform aliases using the settings and the default variables 97 | It's recursive, so you can use aliases *in* your aliases' values 98 | """ 99 | 100 | vars = window.extract_variables() 101 | vars.update(get_settings().get("aliases")) 102 | 103 | def has_unescaped_dollar(string): 104 | start = 0 105 | while True: 106 | index = string.find("$", start) 107 | if index < 0: 108 | return False 109 | elif string[index - 1] == "\\": 110 | start = index + 1 111 | else: 112 | return True 113 | 114 | string = string.replace("$$", "\\$") 115 | 116 | inifinite_loop_counter = 0 117 | while has_unescaped_dollar(string): 118 | inifinite_loop_counter += 1 119 | if inifinite_loop_counter > 100: 120 | sublime.error_message( 121 | "Infinite loop: you better check your " 122 | "aliases, they're calling each other " 123 | "over and over again." 124 | ) 125 | if get_settings().get("open_help_on_alias_infinite_loop", True) is True: 126 | sublime.run_command( 127 | "open_url", 128 | { 129 | "url": "https://github.com/math2001/ " 130 | "FileManager/wiki/Aliases " 131 | "#watch-out-for-infinite-loops" 132 | }, 133 | ) 134 | return string 135 | string = sublime.expand_variables(string, vars) 136 | 137 | return string 138 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt" 3 | } 4 | -------------------------------------------------------------------------------- /messages/1.4.5.txt: -------------------------------------------------------------------------------- 1 | ______ _ _ ___ ___ 2 | | ___(_) | | \/ | 3 | | |_ _| | ___| . . | __ _ _ __ __ _ __ _ ___ _ __ 4 | | _| | | |/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__| 5 | | | | | | __/ | | | (_| | | | | (_| | (_| | __/ | 6 | \_| |_|_|\___\_| |_/\__,_|_| |_|\__,_|\__, |\___|_| 7 | __/ | 8 | |___/ 9 | 10 | Support the 'platform' key for terminals in your settings. Have a look at the 11 | documentation for more information: 12 | 13 | https://math2001.github.io/FileManager/commands/#open-terminal-here 14 | 15 | That's it! 16 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | ______ _ _ ___ ___ 2 | | ___(_) | | \/ | 3 | | |_ _| | ___| . . | __ _ _ __ __ _ __ _ ___ _ __ 4 | | _| | | |/ _ \ |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__| 5 | | | | | | __/ | | | (_| | | | | (_| | (_| | __/ | 6 | \_| |_|_|\___\_| |_/\__,_|_| |_|\__,_|\__, |\___|_| 7 | __/ | 8 | |___/ 9 | 10 | Thanks for installing FileManger! I hope you'll like using it! 11 | 12 | Presentation: 13 | ~~~~~~~~~~~~~ 14 | 15 | FileManager is a package that propose you different *optimized* commands for 16 | managing your files from Sublime Text, such as creating, opening, renaming, 17 | moving, duplicating, etc... 18 | 19 | First, you can have a look around at the options it added to your sidebar's 20 | context menu and the command palette, but you really should have a look at the 21 | GitHub's wiki really get 100% of FileManager. 22 | 23 | Quick Start Tips: 24 | ~~~~~~~~~~~~~~~~~ 25 | 26 | - When you're creating something (pressed alt+n for example), if your path ends 27 | up with a '/', it'll create a folder instead of a file. 28 | - Try to create a folder that already exists ;) 29 | - Have a quick look in here: 'Preferences → Packages Settings → FileManager' 30 | 31 | Say thanks: 32 | ~~~~~~~~~~~ 33 | 34 | Just letting me know you're enjoying this package's a great way to say thanks! 35 | 36 | To do so, you can star the GitHub repository, or send me a tweet 37 | (@_math2001) 38 | 39 | Troubles? 40 | ~~~~~~~~~ 41 | 42 | If you have any trouble with FileManager, don't hesitate to let me know by 43 | raising an issue here: 44 | 45 | https://github.com/math2001/FileManager/issues 46 | 47 | Credits: 48 | ~~~~~~~~ 49 | 50 | The ASCII FileManager has been generated using the ASCII Decorator. 51 | 52 | https://github.com/viisual/ASCII-Decorator 53 | 54 | Tip of the day: You can bind commands to modifiers-free key combination: 55 | 56 | { 57 | "keys": ["-", ">"], 58 | "command": "insert_snippet", 59 | "args": { 60 | "contents": "→ " 61 | } 62 | } 63 | 64 | Once set, each time you'll type '->', it'll insert this '→ ' 65 | 66 | If you don't know how to set up shortcuts, don't worry, it's 67 | explained here: 68 | 69 | http://docs.sublimetext.info/en/latest/reference/key_bindings.html 70 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: FileManager 2 | theme: material 3 | repo_name: math2001/FileManager 4 | repo_url: https://github.com/math2001/FileManager 5 | site_description: Sublime Text 3 Plugin FileManager's documentation 6 | site_author: math2001 7 | 8 | pages: 9 | - Home: index.md 10 | - Getting stated: getting-started.md 11 | - Commands: commands.md 12 | - References: 13 | - Auto Completion: references/auto-completion.md 14 | - Settings: references/settings.md 15 | - Templates: references/templates.md 16 | - Type of path: references/type-of-path.md 17 | - Aliases: references/aliases.md 18 | - Contributing: contributing.md 19 | - License: license.md 20 | - About: about.md 21 | 22 | markdown_extensions: 23 | - toc(permalink=true) 24 | - pymdownx.arithmatex 25 | - pymdownx.betterem(smart_enable=all) 26 | - pymdownx.caret 27 | - pymdownx.critic 28 | - pymdownx.emoji: 29 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 30 | - pymdownx.inlinehilite 31 | - pymdownx.magiclink 32 | - pymdownx.mark 33 | - pymdownx.smartsymbols 34 | - pymdownx.superfences 35 | - pymdownx.tasklist(custom_checkbox=true) 36 | - pymdownx.tilde 37 | - admonition 38 | - codehilite 39 | 40 | extra: 41 | logo: imgs/FileManager-opposite.svg 42 | palette: 43 | primary: red 44 | accent: Deep Orange 45 | social: 46 | - type: github 47 | link: https://github.com/math2001 48 | - type: twitter 49 | link: https://twitter.com/_math2001 50 | -------------------------------------------------------------------------------- /prevent_default.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | from Default.side_bar import RenamePathCommand 3 | 4 | 5 | class NewFileAtCommand(sublime_plugin.WindowCommand): 6 | def is_visible(self): 7 | return False 8 | 9 | def is_enabled(self): 10 | return False 11 | 12 | 13 | class DeleteFileCommand(sublime_plugin.WindowCommand): 14 | def is_visible(self): 15 | return False 16 | 17 | def is_enabled(self): 18 | return False 19 | 20 | 21 | class NewFolderCommand(sublime_plugin.WindowCommand): 22 | def is_visible(self): 23 | return False 24 | 25 | def is_enabled(self): 26 | return False 27 | 28 | 29 | class DeleteFolderCommand(sublime_plugin.WindowCommand): 30 | def is_visible(self): 31 | return False 32 | 33 | def is_enabled(self): 34 | return False 35 | 36 | 37 | class RenamePathCommand(RenamePathCommand): 38 | def is_visible(self): 39 | return False 40 | 41 | 42 | class FindInFolderCommand(sublime_plugin.WindowCommand): 43 | def is_visible(self): 44 | return False 45 | 46 | def is_enabled(self): 47 | return False 48 | 49 | 50 | class OpenContainingFolderCommand(sublime_plugin.WindowCommand): 51 | def is_visible(self): 52 | return False 53 | 54 | def is_enabled(self): 55 | return False 56 | 57 | 58 | class OpenInBrowserCommand(sublime_plugin.TextCommand): 59 | def is_visible(self): 60 | return False 61 | 62 | def is_enabled(self): 63 | return False 64 | 65 | 66 | class CopyPathCommand(sublime_plugin.TextCommand): 67 | def is_visible(self): 68 | return False 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.2.3 2 | mkdocs-material==1.0.2 3 | Pygments==2.7.4 4 | pymdown-extensions==1.6.1 5 | -------------------------------------------------------------------------------- /tests/test_path_helper.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from FileManager.libs.pathhelper import computer_friendly, user_friendly 7 | 8 | 9 | class PathHelperTest(unittest.TestCase): 10 | def test_computer_friendly(self): 11 | home = os.path.expanduser("~") 12 | tests = [ 13 | ("~", home), 14 | ("~/", home + os.path.sep), 15 | ("~/hello/world", os.path.sep.join([home, "hello", "world"])), 16 | ( 17 | "~/hello/world/", 18 | os.path.sep.join([home, "hello", "world"]) + os.path.sep, 19 | ), 20 | ("C:/hello/~/hi", os.path.sep.join([home, "hi"])), 21 | ("C:/hello/~/hi/~/yep", os.path.sep.join([home, "yep"])), 22 | ("C:/hello/~/hi/C:/hello/yep", os.path.sep.join(["C:", "hello", "yep"])), 23 | ("/hello/C:/hi/~/hey", os.path.sep.join([home, "hey"])), 24 | ("\\\\shared\\folder", "\\\\shared\\folder"), 25 | ( 26 | "C:/courses/sublime text 3/", 27 | os.path.sep.join(["C:", "courses", "sublime text 3", ""]), 28 | ), 29 | ] 30 | for base, result in tests: 31 | if result is None: 32 | result = base 33 | self.assertEqual(computer_friendly(base), result) 34 | 35 | def test_user_friendly(self): 36 | home = os.path.expanduser("~") 37 | tests = [ 38 | (home, "~"), 39 | ("C:/courses/sublime text 3/", None), 40 | ("C:/courses/sublime text 3/", None), 41 | ] 42 | 43 | for base, result in tests: 44 | if result is None: 45 | result = base 46 | self.assertEqual(user_friendly(base), result) 47 | -------------------------------------------------------------------------------- /todo: -------------------------------------------------------------------------------- 1 | Plugin: 2 | 3 | ☐ move/rename: save cursor position 4 | ☐ settings per command (move: complete with files) 5 | ☐ log in status bar: path to file exists, files already exists, etc 6 | ☐ specify snippet **file** to use as a template 7 | ☐ fix status message bar when creating 8 | ☐ open_in_browser: url per folder 9 | ☐ platform for opening terminal 10 | ☐ cycle through the auto completion 11 | ☐ add global settings: `fm_use_terminal` 12 | ☐ improve duplicating 13 | ☐ support symbol in browser (@) 14 | 15 | add settings: 16 | ☐ ignore patterns in auto completion (maybe use settings for the side bar) 17 | 18 | Docs: 19 | ☐ show the gifs. 20 | ☐ specify infos when duplicating folder 21 | ☐ add license 22 | ☐ talk about `show_` settings 23 | 24 | ___________________ 25 | Archive: 26 | ✔ browser: open in transient mode when 'hovering' @done Mon 23 Jan 2017 at 10:55 @project(Plugin) 27 | ✔ autocompletion: shift+tab -> prev completion @done Fri 06 Jan 2017 at 14:32 @project(Plugin) 28 | ✘ autocompletion: complete with aliases too @cancelled Fri 06 Jan 2017 at 11:13 @project(Plugin) 29 | ✔ Fix case in Sidebar::copy @done Thu 05 Jan 2017 at 19:22 @project(Plugin) 30 | ✔ create_from_selection: support single quotes @done Thu 05 Jan 2017 at 19:20 @project(Plugin) 31 | --------------------------------------------------------------------------------