├── .python-version
├── .gitattributes
├── messages.json
├── Context.sublime-menu
├── requirements.txt
├── .gitignore
├── commands
├── __init__.py
├── open_all.py
├── find_in_files.py
├── open_in_explorer.py
├── open_in_browser.py
├── copy.py
├── editto.py
├── fmcommand.py
├── open_terminal.py
├── move.py
├── delete.py
├── duplicate.py
├── rename.py
├── create.py
└── create_from_selection.py
├── dependencies.json
├── Default.sublime-keymap
├── .github
└── workflows
│ └── ci.yml
├── messages
├── 1.4.5.txt
└── install.txt
├── libs
├── send2trash
│ ├── compat.py
│ ├── __init__.py
│ ├── exceptions.py
│ ├── LICENSE
│ ├── plat_osx.py
│ ├── plat_win.py
│ └── plat_other.py
├── pathhelper.py
├── sublimefunctions.py
└── input_for_path.py
├── Main.sublime-menu
├── LICENSE
├── Tab Context.sublime-menu
├── todo
├── mkdocs.yml
├── prevent_default.py
├── tests
└── test_path_helper.py
├── Default.sublime-commands
├── README.md
├── Side Bar.sublime-menu
├── FileManager.sublime-settings
└── FileManager.py
/.python-version:
--------------------------------------------------------------------------------
1 | 3.8
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | docs/ export-ignore
2 |
--------------------------------------------------------------------------------
/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "install": "messages/install.txt"
3 | }
4 |
--------------------------------------------------------------------------------
/Context.sublime-menu:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "command": "fm_create_file_from_selection"
4 | }
5 | ]
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | site/
2 | test.py
3 | __pycache__/
4 | Thumbs.db
5 | .DS_STORE
6 | *sublime-project
7 | *sublime-workspace
--------------------------------------------------------------------------------
/commands/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | """ Seperate the commands because I'm sick of this huge file """
4 |
--------------------------------------------------------------------------------
/dependencies.json:
--------------------------------------------------------------------------------
1 | {
2 | "*": {
3 | "*": [
4 | "package_setting_context"
5 | ]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------