├── .python-version
├── docs
├── src
│ ├── markdown
│ │ ├── .snippets
│ │ │ ├── links.md
│ │ │ ├── refs.md
│ │ │ └── abbr.md
│ │ ├── about
│ │ │ ├── license.md
│ │ │ └── contributing.md
│ │ ├── index.md
│ │ ├── installation.md
│ │ └── usage.md
│ ├── requirements.txt
│ └── dictionary
│ │ └── en-custom.txt
└── theme
│ └── announce.html
├── .github
├── FUNDING.yml
├── labels.yml
└── workflows
│ ├── deploy.yml
│ └── build.yml
├── Default.sublime-commands
├── CHANGES.md
├── tox.ini
├── Tab Context.sublime-menu
├── Side Bar.sublime-menu
├── README.md
├── LICENSE.md
├── notify.py
├── Main.sublime-menu
├── fuzzy_file_nav.sublime-settings
├── Default (Linux).sublime-keymap
├── Default (Windows).sublime-keymap
├── Default (OSX).sublime-keymap
├── .pyspelling.yml
├── multiconf.py
└── fuzzy_file_nav.py
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13
2 |
--------------------------------------------------------------------------------
/docs/src/markdown/.snippets/links.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/src/markdown/about/license.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | --8<-- "LICENSE.md"
4 |
--------------------------------------------------------------------------------
/docs/src/markdown/.snippets/refs.md:
--------------------------------------------------------------------------------
1 | --8<--
2 | links.md
3 | abbr.md
4 | --8<--
5 |
--------------------------------------------------------------------------------
/docs/src/markdown/.snippets/abbr.md:
--------------------------------------------------------------------------------
1 | *[ST2]: Sublime Text 2
2 | *[ST3]: Sublime Text 3
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: facelessuser
2 | custom:
3 | - "https://www.paypal.me/facelessuser"
4 |
--------------------------------------------------------------------------------
/docs/src/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs_pymdownx_material_extras>=2.1
2 | mkdocs-git-revision-date-localized-plugin
3 | mkdocs-minify-plugin
4 | pyspelling
5 |
--------------------------------------------------------------------------------
/docs/src/dictionary/en-custom.txt:
--------------------------------------------------------------------------------
1 | Autocomplete
2 | Boundincode
3 | Cmd
4 | Ctrl
5 | FuzzyFileNav
6 | JSON
7 | MERCHANTABILITY
8 | MkDocs
9 | Multiconf
10 | NONINFRINGEMENT
11 | OSX
12 | SubNotify
13 | Sublime's
14 | Twemoji
15 | autocomplete
16 | autocompletion
17 | biermeester
18 | hostname
19 | installable
20 | macOS
21 | matthjes
22 | mkdocs
23 | multiconf
24 | os
25 | pre
26 | quodlibet
27 | requesters
28 | subfolder
29 | sublicense
30 | tox
31 |
--------------------------------------------------------------------------------
/Default.sublime-commands:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "caption": "Fuzzy Nav",
4 | "command": "fuzzy_file_nav"
5 | },
6 | {
7 | "caption": "Fuzzy Nav Here...",
8 | "command": "fuzzy_start_from_file",
9 | "args": {"paths": []}
10 | },
11 | {
12 | "caption": "Fuzzy Project Folders",
13 | "command": "fuzzy_project_folder_load"
14 | },
15 | {
16 | "caption": "Fuzzy BookMarks",
17 | "command": "fuzzy_bookmarks_load"
18 | }
19 | ]
20 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # FuzzyFileNav
2 |
3 | ## 2.1.0
4 |
5 | - **NEW**: Updates for Python 3.13 on ST 4201+.
6 |
7 | ## 2.0.1
8 |
9 | - **FIX**: Fix same file check.
10 |
11 | ## 2.0.0
12 |
13 | - **NEW**: Allow opening folder in new window.
14 | - **NEW**: Commands are available from the command palette.
15 | - **NEW**: Move context menu items to tab context menu as commands don't directly operate on view content.
16 | - **FIX**: Don't copy `.` and `..`.
17 | - **FIX**: Fix logging errors.
18 | - **FIX**: Fix bad plugin notification title.
19 | - **FIX**: Exclude `..` from completions.
20 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | skipsdist=True
3 | envlist =
4 | py39,py310,py311,py312,py313,lint
5 |
6 | [testenv]
7 | deps=
8 | pytest
9 | commands=
10 | py.test .
11 |
12 | [testenv:documents]
13 | deps=
14 | -rdocs/src/requirements.txt
15 | commands=
16 | "{envpython}" -m mkdocs build --clean --verbose --strict
17 | pyspelling
18 |
19 | [testenv:lint]
20 | deps=
21 | flake8
22 | flake8_docstrings
23 | pep8-naming
24 | flake8-mutable
25 | flake8-builtins
26 | commands=
27 | flake8 "{toxinidir}"
28 |
29 | [flake8]
30 | ignore=D202,D203,D401,W504,N818
31 | max-line-length=120
32 | exclude=site/*.py,.tox/*
33 |
--------------------------------------------------------------------------------
/docs/theme/announce.html:
--------------------------------------------------------------------------------
1 | Sponsorship
2 | is now available!
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/src/markdown/index.md:
--------------------------------------------------------------------------------
1 | # FuzzyFileNav
2 |
3 | ## Overview
4 |
5 | FuzzyFileNav is a simple plugin that allows for quick navigation of the file system from the quick panel. It also
6 | allows for deletion, copying, moving, creation of files and folders, and other actions.
7 |
8 | ## Credits
9 |
10 | * Thanks to quodlibet for helping come up with great ideas for the plugin during development.
11 |
12 | * Special thanks to [Boundincode](https://github.com/Boundincode) whose witty humor and quirky coding fueled the
13 | development of the plugin. (If only he was more humble...)
14 |
15 | * Thanks to biermeester and matthjes for their suggestions and ideas with platform/host specific settings.
16 |
--------------------------------------------------------------------------------
/Tab Context.sublime-menu:
--------------------------------------------------------------------------------
1 | [
2 | { "caption": "-" },
3 | {
4 | "caption": "Fuzzy Nav Here...",
5 | "command": "fuzzy_start_from_file"
6 | },
7 | {
8 | "caption": "Fuzzy File Nav",
9 | "children":
10 | [
11 | {
12 | "caption": "Fuzzy Nav",
13 | "command": "fuzzy_file_nav"
14 | },
15 | {
16 | "caption": "Fuzzy Project Folders",
17 | "command": "fuzzy_project_folder_load"
18 | },
19 | {
20 | "caption": "Fuzzy BookMarks",
21 | "command": "fuzzy_bookmarks_load"
22 | }
23 | ]
24 | },
25 | { "caption": "-"}
26 | ]
27 |
--------------------------------------------------------------------------------
/Side Bar.sublime-menu:
--------------------------------------------------------------------------------
1 | [
2 | { "caption": "-" },
3 | {
4 | "caption": "Fuzzy Nav Here...",
5 | "command": "fuzzy_start_from_file",
6 | "args": {"paths": []}
7 | },
8 | {
9 | "caption": "Fuzzy File Nav",
10 | "children":
11 | [
12 | {
13 | "caption": "Fuzzy Nav",
14 | "command": "fuzzy_file_nav"
15 | },
16 | {
17 | "caption": "Fuzzy Project Folders",
18 | "command": "fuzzy_project_folder_load"
19 | },
20 | {
21 | "caption": "Fuzzy BookMarks",
22 | "command": "fuzzy_bookmarks_load"
23 | }
24 | ]
25 | },
26 | { "caption": "-" }
27 | ]
28 |
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | template: 'facelessuser:master-labels:labels.yml:master'
2 |
3 | # Wildcard labels
4 |
5 | brace_expansion: true
6 | extended_glob: true
7 | minus_negate: false
8 |
9 | rules:
10 | - labels: ['C: infrastructure']
11 | patterns: ['*|!@(*.md|*.py|*.sublime-@(keymap|menu|settings|commands))', '.github/**']
12 |
13 | - labels: ['C: source']
14 | patterns: ['**/@(*.py|*.sublime-@(keymap|menu|settings|commands))|!tests']
15 |
16 | - labels: ['C: docs']
17 | patterns: ['**/*.md|docs/**|messages/**']
18 |
19 | - labels: ['C: tests']
20 | patterns: ['tests/**']
21 |
22 | - labels: ['C: settings']
23 | patterns: ['*.sublime-@(keymap|menu|settings|commands)']
24 |
25 | # Label management
26 |
27 | labels:
28 |
29 | - name: 'C: settings'
30 | color: subcategory
31 | description: Sublime settings files.
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Donate via PayPal][donate-image]][donate-link]
2 | [![Package Control Downloads][pc-image]][pc-link]
3 | ![License][license-image]
4 | # FuzzyFileNav
5 |
6 | FuzzyFileNav is a simple plugin that allows for quick navigation of the file system from the quick panel. It also
7 | allows for deletion, copying, moving, creation of files and folders, and other actions.
8 |
9 | # Documentation
10 |
11 | https://facelessuser.github.io/FuzzyFileNav/
12 |
13 | # License
14 |
15 | FuzzyFileNav is released under the MIT license.
16 |
17 | [pc-image]: https://img.shields.io/packagecontrol/dt/FuzzyFileNav.svg?labelColor=333333&logo=sublime%20text
18 | [pc-link]: https://packagecontrol.io/packages/FuzzyFileNav
19 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?labelColor=333333
20 | [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal
21 | [donate-link]: https://www.paypal.me/facelessuser
22 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'st3-*'
7 | - 'st4-*'
8 |
9 | jobs:
10 |
11 | documents:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | - name: Set up Python
20 | uses: actions/setup-python@v5
21 | with:
22 | python-version: 3.11
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip setuptools build
26 | python -m pip install -r docs/src/requirements.txt
27 | - name: Deploy documents
28 | run: |
29 | git config user.name facelessuser
30 | git config user.email "${{ secrets.GH_EMAIL }}"
31 | git remote add gh-token "https://${{ secrets.GH_TOKEN }}@github.com/facelessuser/FuzzyFileNav.git"
32 | git fetch gh-token && git fetch gh-token gh-pages:gh-pages
33 | python -m mkdocs gh-deploy -v --clean --remote-name gh-token
34 | git push gh-token gh-pages
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | FuzzyFileNav is released under the MIT license.
2 |
3 | Copyright (c) 2012 - 2025 Isaac Muse
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy oft his software and associated
6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8 | persons to whom the Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11 | Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
--------------------------------------------------------------------------------
/notify.py:
--------------------------------------------------------------------------------
1 | """
2 | Fuzzy File Navigation.
3 |
4 | Licensed under MIT
5 | Copyright (c) 2012 - 2015 Isaac Muse
6 | """
7 | import sublime
8 | try:
9 | from SubNotify.sub_notify import SubNotifyIsReadyCommand as Notify
10 | except Exception:
11 | class Notify(object):
12 | """Fallback SubNotify object."""
13 |
14 | @classmethod
15 | def is_ready(cls):
16 | """Return false to disable SubNotify."""
17 |
18 | return False
19 |
20 |
21 | def notify(msg):
22 | """Notify message."""
23 |
24 | settings = sublime.load_settings("fuzzy_file_nav.sublime-settings")
25 | if settings.get("use_sub_notify", False) and Notify.is_ready():
26 | sublime.run_command("sub_notify", {"title": "FuzzyFileNav", "msg": msg})
27 | else:
28 | sublime.status_message(msg)
29 |
30 |
31 | def error(msg):
32 | """Error message."""
33 |
34 | settings = sublime.load_settings("fuzzy_file_nav.sublime-settings")
35 | if settings.get("use_sub_notify", False) and Notify.is_ready():
36 | sublime.run_command("sub_notify", {"title": "FuzzyFileNav", "msg": msg, "level": "error"})
37 | else:
38 | sublime.error_message("FuzzyFileNav:\n%s" % msg)
39 |
--------------------------------------------------------------------------------
/docs/src/markdown/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | ## Package Control
4 |
5 | The recommended way to install FuzzyFileNav is via [Package Control](https://packagecontrol.io/). Package Control will
6 | install the correct branch on your system and keep it up to date.
7 |
8 | 1. Ensure Package Control is installed. Instructions are found [here](https://packagecontrol.io/installation).
9 |
10 | 2. In Sublime Text, press ++ctrl+shift+p++ (Win, Linux) or ++cmd+shift+p++ (OSX) to bring up the quick panel. It will
11 | show a list of installable plugins.
12 |
13 | 3. Start typing `FuzzyFileNav`; when you see it, select it.
14 |
15 | 4. Restart to be sure everything is loaded proper.
16 |
17 | 5. Enjoy!
18 |
19 | ## Git Cloning
20 |
21 | /// warning | Warning
22 | This is not the recommended way to install FuzzyFileNav for the casual user as it requires the user to know which
23 | branch to install, know how to use git, and **will not** get automatically updated.
24 |
25 | If you are forking for a pull request, this is the way to go, just replace the official repository with the link for
26 | your fork.
27 | ///
28 |
29 | 1. Quit Sublime Text.
30 |
31 | 2. Open a terminal:
32 |
33 | ```
34 | cd /path/to/Sublime Text 3/Packages
35 | git clone https://github.com/facelessuser/FuzzyFileNav.git FuzzyFileNav
36 | ```
37 |
38 | 3. Restart Sublime Text.
39 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | tags:
8 | - '**'
9 | pull_request:
10 | branches:
11 | - '**'
12 |
13 | jobs:
14 | tests:
15 |
16 | env:
17 | TOXENV: py311
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Set up Python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: 3.11
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip setuptools tox build
30 | - name: Tests
31 | run: |
32 | python -m tox
33 |
34 | lint:
35 |
36 | env:
37 | TOXENV: lint
38 |
39 | runs-on: ubuntu-latest
40 |
41 | steps:
42 | - uses: actions/checkout@v4
43 | - name: Set up Python
44 | uses: actions/setup-python@v5
45 | with:
46 | python-version: 3.11
47 | - name: Install dependencies
48 | run: |
49 | python -m pip install --upgrade pip setuptools tox build
50 | - name: Lint
51 | run: |
52 | python -m tox
53 |
54 | documents:
55 |
56 | env:
57 | TOXENV: documents
58 |
59 | runs-on: ubuntu-latest
60 |
61 | steps:
62 | - uses: actions/checkout@v4
63 | - name: Set up Python
64 | uses: actions/setup-python@v5
65 | with:
66 | python-version: 3.11
67 | - name: Install dependencies
68 | run: |
69 | python -m pip install --upgrade pip setuptools tox build
70 | - name: Install Aspell
71 | run: |
72 | sudo apt-get install aspell aspell-en
73 | - name: Build documents
74 | run: |
75 | python -m tox
76 |
--------------------------------------------------------------------------------
/Main.sublime-menu:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "caption": "Preferences",
4 | "mnemonic": "n",
5 | "id": "preferences",
6 | "children":
7 | [
8 | {
9 | "caption": "Package Settings",
10 | "mnemonic": "P",
11 | "id": "package-settings",
12 | "children":
13 | [
14 | {
15 | "caption": "FuzzyFileNav",
16 | "children":
17 | [
18 | {
19 | "caption": "Settings",
20 | "command": "edit_settings",
21 | "args": {
22 | "base_file": "${packages}/FuzzyFileNav/fuzzy_file_nav.sublime-settings",
23 | "default": "// Settings in here override those in \"FuzzyFileNav/fuzzy_file_nav.sublime-settings\"\n{\n\t$0\n}\n"
24 | }
25 | },
26 | {
27 | "caption": "Key Bindings",
28 | "command": "edit_settings",
29 | "args": {
30 | "base_file": "${packages}/FuzzyFileNav/Default (${platform}).sublime-keymap",
31 | "user_file": "${packages}/User/Default (${platform}).sublime-keymap",
32 | "default": "[\n\t$0\n]\n"
33 | }
34 | }
35 | ]
36 | }
37 | ]
38 | }
39 | ]
40 | }
41 | ]
42 |
--------------------------------------------------------------------------------
/fuzzy_file_nav.sublime-settings:
--------------------------------------------------------------------------------
1 | {
2 | // Location of home folder
3 | "home": "~",
4 |
5 | // Patterns of files/folders to exclude
6 | "regex_exclude": [".*\\.(DS_Store|svn|git)$"],
7 |
8 | // Keep panel open after a file is opened, deleted, created, etc. so
9 | // More files can be have actions performed on them.
10 | "keep_panel_open_after_action": true,
11 |
12 | // Actions that can ignore the keep panel open settings
13 | // Available actions: delete, open, saveas, mkfile, mkdir, paste
14 | "keep_panel_open_exceptions": [],
15 |
16 | // Controls whether system hidden files are shown in FuzzyFileNav.
17 | "show_system_hidden_files": true,
18 |
19 | // (fuzzy/windows/nix)
20 | // fuzzy - this will auto-complete with the selected index in the quick panel
21 | // windows - this will complete like a windows terminal would complete paths
22 | // nix - this will complete like a unix/linux terminal traditionally completes paths
23 | "completion_style": "fuzzy",
24 |
25 | // If the "FuzzyStartFromFileCommand" is run outside of a open buffer
26 | // or from a buffer that does not exist on disk, you can specify
27 | // its default action to do instead of starting navigation from
28 | // a file's location. Options are "bookmarks", "home", "root", "project".
29 | "start_from_here_default_action": "bookmarks",
30 |
31 | // Bookmarked paths
32 | "bookmarks": [
33 | {"name": "My Computer", "path": {"#multiconf#": [{"os:windows": ""}]}},
34 | {"name": "Root", "path": {"#multiconf#": [{"os:linux": "/"}, {"os:osx": "/"}]}}
35 | ],
36 |
37 | // Add your folders relative to the project file (if project file exists on disk)
38 | "add_folder_to_project_relative": false,
39 |
40 | // When adding folder to project, set "follow_symlinks" setting as true or false
41 | "add_folder_to_project_follow_symlink": true,
42 |
43 | // Use subnotify if available
44 | "use_sub_notify": true
45 | }
46 |
--------------------------------------------------------------------------------
/Default (Linux).sublime-keymap:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "keys": ["tab"],
4 | "command": "fuzzy_path_complete",
5 | "context": [{"key": "fuzzy_path_complete"}]
6 | },
7 | {
8 | "keys": ["shift+tab"],
9 | "command": "fuzzy_path_complete",
10 | "context": [{"key": "fuzzy_path_complete_back"}],
11 | "args": {"back": true}
12 | },
13 | {
14 | "keys": ["ctrl+h"],
15 | "command": "fuzzy_toggle_hidden",
16 | "context": [{"key": "fuzzy_toggle_hidden"}]
17 | },
18 | {
19 | "keys": ["ctrl+b"],
20 | "command": "fuzzy_bookmarks_load",
21 | "context": [{"key": "fuzzy_bookmarks_load"}]
22 | },
23 | {
24 | "keys": ["ctrl+d"],
25 | "command": "fuzzy_delete",
26 | "context": [{"key": "fuzzy_delete"}]
27 | },
28 | {
29 | "keys": ["ctrl+c"],
30 | "command": "fuzzy_clipboard",
31 | "context": [{"key": "fuzzy_copy"}],
32 | "args": {"action": "copy"}
33 | },
34 | {
35 | "keys": ["ctrl+x"],
36 | "command": "fuzzy_clipboard",
37 | "context": [{"key": "fuzzy_cut"}],
38 | "args": {"action": "cut"}
39 | },
40 | {
41 | "keys": ["ctrl+v"],
42 | "command": "fuzzy_clipboard",
43 | "context": [{"key": "fuzzy_paste"}],
44 | "args": {"action": "paste"}
45 | },
46 | {
47 | "keys": ["ctrl+n"],
48 | "command": "fuzzy_make_file",
49 | "context": [{"key": "fuzzy_make_file"}]
50 | },
51 | {
52 | "keys": ["ctrl+shift+n"],
53 | "command": "fuzzy_make_folder",
54 | "context": [{"key": "fuzzy_make_folder"}]
55 | },
56 | {
57 | "keys": ["ctrl+r"],
58 | "command": "fuzzy_reveal",
59 | "context": [{"key": "fuzzy_reveal"}]
60 | },
61 | {
62 | "keys": ["ctrl+s"],
63 | "command": "fuzzy_save_file",
64 | "context": [{"key": "fuzzy_save_as"}]
65 | },
66 | {
67 | "keys": ["ctrl+."],
68 | "command": "fuzzy_current_working_view",
69 | "context": [{"key": "fuzzy_cwv"}]
70 | },
71 | {
72 | "keys": ["ctrl+f"],
73 | "command": "fuzzy_search_folder",
74 | "context": [{"key": "fuzzy_search"}]
75 | },
76 | {
77 | "keys": ["ctrl+p"],
78 | "command": "fuzzy_open_folder",
79 | "context": [{"key": "fuzzy_open_folder"}]
80 | },
81 | {
82 | "keys": ["ctrl+shift+p"],
83 | "command": "fuzzy_open_folder",
84 | "context": [{"key": "fuzzy_open_folder"}],
85 | "args": {"new_window": true}
86 | },
87 | {
88 | "keys": ["right"],
89 | "command": "fuzzy_quick_open",
90 | "context": [{"key": "fuzzy_quick_open"}]
91 | }
92 | ]
93 |
--------------------------------------------------------------------------------
/Default (Windows).sublime-keymap:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "keys": ["tab"],
4 | "command": "fuzzy_path_complete",
5 | "context": [{"key": "fuzzy_path_complete"}]
6 | },
7 | {
8 | "keys": ["shift+tab"],
9 | "command": "fuzzy_path_complete",
10 | "context": [{"key": "fuzzy_path_complete_back"}],
11 | "args": {"back": true}
12 | },
13 | {
14 | "keys": ["ctrl+h"],
15 | "command": "fuzzy_toggle_hidden",
16 | "context": [{"key": "fuzzy_toggle_hidden"}]
17 | },
18 | {
19 | "keys": ["ctrl+b"],
20 | "command": "fuzzy_bookmarks_load",
21 | "context": [{"key": "fuzzy_bookmarks_load"}]
22 | },
23 | {
24 | "keys": ["ctrl+d"],
25 | "command": "fuzzy_delete",
26 | "context": [{"key": "fuzzy_delete"}]
27 | },
28 | {
29 | "keys": ["ctrl+c"],
30 | "command": "fuzzy_clipboard",
31 | "context": [{"key": "fuzzy_copy"}],
32 | "args": {"action": "copy"}
33 | },
34 | {
35 | "keys": ["ctrl+x"],
36 | "command": "fuzzy_clipboard",
37 | "context": [{"key": "fuzzy_cut"}],
38 | "args": {"action": "cut"}
39 | },
40 | {
41 | "keys": ["ctrl+v"],
42 | "command": "fuzzy_clipboard",
43 | "context": [{"key": "fuzzy_paste"}],
44 | "args": {"action": "paste"}
45 | },
46 | {
47 | "keys": ["ctrl+n"],
48 | "command": "fuzzy_make_file",
49 | "context": [{"key": "fuzzy_make_file"}]
50 | },
51 | {
52 | "keys": ["ctrl+shift+n"],
53 | "command": "fuzzy_make_folder",
54 | "context": [{"key": "fuzzy_make_folder"}]
55 | },
56 | {
57 | "keys": ["ctrl+r"],
58 | "command": "fuzzy_reveal",
59 | "context": [{"key": "fuzzy_reveal"}]
60 | },
61 | {
62 | "keys": ["ctrl+s"],
63 | "command": "fuzzy_save_file",
64 | "context": [{"key": "fuzzy_save_as"}]
65 | },
66 | {
67 | "keys": ["ctrl+."],
68 | "command": "fuzzy_current_working_view",
69 | "context": [{"key": "fuzzy_cwv"}]
70 | },
71 | {
72 | "keys": ["ctrl+f"],
73 | "command": "fuzzy_search_folder",
74 | "context": [{"key": "fuzzy_search"}]
75 | },
76 | {
77 | "keys": ["ctrl+p"],
78 | "command": "fuzzy_open_folder",
79 | "context": [{"key": "fuzzy_open_folder"}]
80 | },
81 | {
82 | "keys": ["ctrl+shift+p"],
83 | "command": "fuzzy_open_folder",
84 | "context": [{"key": "fuzzy_open_folder"}],
85 | "args": {"new_window": true}
86 | },
87 | {
88 | "keys": ["right"],
89 | "command": "fuzzy_quick_open",
90 | "context": [{"key": "fuzzy_quick_open"}]
91 | }
92 | ]
93 |
--------------------------------------------------------------------------------
/Default (OSX).sublime-keymap:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "keys": ["tab"],
4 | "command": "fuzzy_path_complete",
5 | "context": [{"key": "fuzzy_path_complete"}]
6 | },
7 | {
8 | "keys": ["shift+tab"],
9 | "command": "fuzzy_path_complete",
10 | "context": [{"key": "fuzzy_path_complete_back"}],
11 | "args": {"back": true}
12 | },
13 | {
14 | "keys": ["super+h"],
15 | "command": "fuzzy_toggle_hidden",
16 | "context": [{"key": "fuzzy_toggle_hidden"}]
17 | },
18 | {
19 | "keys": ["super+b"],
20 | "command": "fuzzy_bookmarks_load",
21 | "context": [{"key": "fuzzy_bookmarks_load"}]
22 | },
23 | {
24 | "keys": ["super+d"],
25 | "command": "fuzzy_delete",
26 | "context": [{"key": "fuzzy_delete"}]
27 | },
28 | {
29 | "keys": ["super+c"],
30 | "command": "fuzzy_clipboard",
31 | "context": [{"key": "fuzzy_copy"}],
32 | "args": {"action": "copy"}
33 | },
34 | {
35 | "keys": ["super+x"],
36 | "command": "fuzzy_clipboard",
37 | "context": [{"key": "fuzzy_cut"}],
38 | "args": {"action": "cut"}
39 | },
40 | {
41 | "keys": ["super+v"],
42 | "command": "fuzzy_clipboard",
43 | "context": [{"key": "fuzzy_paste"}],
44 | "args": {"action": "paste"}
45 | },
46 | {
47 | "keys": ["super+n"],
48 | "command": "fuzzy_make_file",
49 | "context": [{"key": "fuzzy_make_file"}]
50 | },
51 | {
52 | "keys": ["super+shift+n"],
53 | "command": "fuzzy_make_folder",
54 | "context": [{"key": "fuzzy_make_folder"}]
55 | },
56 | {
57 | "keys": ["super+r"],
58 | "command": "fuzzy_reveal",
59 | "context": [{"key": "fuzzy_reveal"}]
60 | },
61 | {
62 | "keys": ["super+s"],
63 | "command": "fuzzy_save_file",
64 | "context": [{"key": "fuzzy_save_as"}]
65 | },
66 | {
67 | "keys": ["super+."],
68 | "command": "fuzzy_current_working_view",
69 | "context": [{"key": "fuzzy_cwv"}]
70 | },
71 | {
72 | "keys": ["super+f"],
73 | "command": "fuzzy_search_folder",
74 | "context": [{"key": "fuzzy_search"}]
75 | },
76 | {
77 | "keys": ["super+p"],
78 | "command": "fuzzy_open_folder",
79 | "context": [{"key": "fuzzy_open_folder"}]
80 | },
81 | {
82 | "keys": ["super+shift+p"],
83 | "command": "fuzzy_open_folder",
84 | "context": [{"key": "fuzzy_open_folder"}],
85 | "args": {"new_window": true}
86 | },
87 | {
88 | "keys": ["right"],
89 | "command": "fuzzy_quick_open",
90 | "context": [{"key": "fuzzy_quick_open"}]
91 | }
92 | ]
93 |
--------------------------------------------------------------------------------
/.pyspelling.yml:
--------------------------------------------------------------------------------
1 | jobs: 8
2 |
3 | matrix:
4 | - name: settings
5 | sources:
6 | - '**/*.sublime-settings'
7 | aspell:
8 | lang: en
9 | dictionary:
10 | wordlists:
11 | - docs/src/dictionary/en-custom.txt
12 | output: build/dictionary/settings.dic
13 | pipeline:
14 | - pyspelling.filters.cpp:
15 | prefix: 'st'
16 | group_comments: true
17 | line_comments: false
18 | - pyspelling.filters.context:
19 | context_visible_first: true
20 | escapes: '\\[\\`]'
21 | delimiters:
22 | # Ignore multiline content between fences (fences can have 3 or more back ticks)
23 | # ```
24 | # content
25 | # ```
26 | - open: '(?s)^(?P *`{3,})$'
27 | close: '^(?P=open)$'
28 | # Ignore text between inline back ticks
29 | - open: '(?P`+)'
30 | close: '(?P=open)'
31 | - pyspelling.filters.url:
32 |
33 | - name: mkdocs
34 | sources:
35 | - site/**/*.html
36 | aspell:
37 | lang: en
38 | dictionary:
39 | wordlists:
40 | - docs/src/dictionary/en-custom.txt
41 | output: build/dictionary/mkdocs.dic
42 | pipeline:
43 | - pyspelling.filters.html:
44 | comments: false
45 | attributes:
46 | - title
47 | - alt
48 | ignores:
49 | - 'code, pre, a.magiclink, span.keys'
50 | - '.MathJax_Preview, .md-nav__link, .md-footer-custom-text, .md-source__repository, .headerlink, .md-icon'
51 | - '.md-social__link'
52 | - pyspelling.filters.url:
53 |
54 | - name: markdown
55 | sources:
56 | - '*.md|messages/*.md'
57 | aspell:
58 | lang: en
59 | dictionary:
60 | wordlists:
61 | - docs/src/dictionary/en-custom.txt
62 | output: build/dictionary/markdown.dic
63 | pipeline:
64 | - pyspelling.filters.markdown:
65 | - pyspelling.filters.html:
66 | comments: false
67 | attributes:
68 | - title
69 | - alt
70 | ignores:
71 | - :is(code, pre)
72 | - pyspelling.filters.url:
73 |
74 | - name: python
75 | sources:
76 | - '*.py|tests/**/*.py'
77 | aspell:
78 | lang: en
79 | dictionary:
80 | wordlists:
81 | - docs/src/dictionary/en-custom.txt
82 | output: build/dictionary/python.dic
83 | pipeline:
84 | - pyspelling.filters.python:
85 | group_comments: true
86 | - pyspelling.flow_control.wildcard:
87 | allow:
88 | - py-comment
89 | - pyspelling.filters.context:
90 | context_visible_first: true
91 | delimiters:
92 | # Ignore lint (noqa) and coverage (pragma) as well as shebang (#!)
93 | - open: '^(?: *(?:noqa\b|pragma: no cover)|!)'
94 | close: '$'
95 | # Ignore Python encoding string -*- encoding stuff -*-
96 | - open: '^ *-\*-'
97 | close: '-\*-$'
98 | - pyspelling.filters.context:
99 | context_visible_first: true
100 | escapes: '\\[\\`]'
101 | delimiters:
102 | # Ignore multiline content between fences (fences can have 3 or more back ticks)
103 | # ```
104 | # content
105 | # ```
106 | - open: '(?s)^(?P *`{3,})$'
107 | close: '^(?P=open)$'
108 | # Ignore text between inline back ticks
109 | - open: '(?P`+)'
110 | close: '(?P=open)'
111 | - pyspelling.filters.url:
112 |
--------------------------------------------------------------------------------
/docs/src/markdown/about/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing & Support
2 |
3 | ## Overview
4 |
5 | Sublime Versions | Description
6 | ---------------- | -----------
7 | ST2 | Supported on a separate branch, but not actively. Any further fixes or enhancements must come from the community. Issues for versions less than ST3 will not be addressed moving forward by me. Pull requests are welcome for back-porting features, enhancements, or fixes to the old branch, but the content of the pull **must** already exist on the main, actively developed branch. I will not allow an older branch to exceed the main branch in regards to functionality.
8 | ST3 | Fully supported and actively maintained.
9 |
10 | Contribution from the community is encouraged and can be done in a variety of ways:
11 |
12 | - Become a sponsor.
13 | - Bug reports.
14 | - Reviewing code.
15 | - Code patches via pull requests.
16 | - Documentation improvements via pull requests.
17 |
18 | ## Become a Sponsor :octicons-heart-fill-16:{: .heart-throb}
19 |
20 | Open source projects take time and money. Help support the project by becoming a sponsor. You can add your support at
21 | any tier you feel comfortable with. No amount is too little. We also accept one time contributions via PayPal.
22 |
23 | [:octicons-mark-github-16: GitHub Sponsors](https://github.com/sponsors/facelessuser){: .md-button .md-button--primary }
24 | [:fontawesome-brands-paypal: PayPal](https://www.paypal.me/facelessuser){ .md-button}
25 |
26 | ## Bug Reports
27 |
28 | 1. Please **read the documentation** and **search the issue tracker** to try to find the answer to your question
29 | **before** posting an issue.
30 |
31 | 2. When creating an issue on the repository, please provide as much info as possible:
32 |
33 | - Sublime Text build.
34 | - Operating system.
35 | - Errors in console.
36 | - Detailed description of the problem.
37 | - Examples for reproducing the error. You can post pictures, but if specific text or code is required to
38 | reproduce the issue, please provide the text in a plain text format for easy copy/paste.
39 |
40 | The more info provided the greater the chance someone will take the time to answer, implement, or fix the issue.
41 |
42 | 3. Be prepared to answer questions and provide additional information if required. Issues in which the creator refuses
43 | to respond to follow up questions will be marked as stale and closed.
44 |
45 | ## Reviewing Code
46 |
47 | Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss
48 | solutions to overcome weakness in the algorithm.
49 |
50 | ## Pull Requests
51 |
52 | Pull requests are welcome, and if you plan on contributing directly to the code, there are a couple of things to be
53 | mindful of.
54 |
55 | Continuous integration tests on are run on all pull requests and commits via Travis CI. When making a pull request, the
56 | tests will automatically be run, and the request must pass to be accepted. You can (and should) run these tests before
57 | pull requesting. If it is not possible to run these tests locally, they will be run when the pull request is made, but
58 | it is strongly suggested that requesters make an effort to verify before requesting to allow for a quick, smooth merge.
59 |
60 | Feel free to use a virtual environment if you are concerned about installing any of the Python packages. In the future,
61 | I may use tox, but as I currently only test on Python 3.3, I wanted to keep things simple.
62 |
63 | ### Running Validation Tests
64 |
65 | /// tip | Tip
66 | If you are running Sublime on a OSX or Linux/Unix system, you run all tests by by running the shell script (assuming
67 | you have installed your environment fulfills all requirements below):
68 |
69 | ```
70 | chmod +x run_tests.sh
71 | ./run_tests.sh
72 | ```
73 | ///
74 |
75 | There are a couple of dependencies that must be present before running the tests.
76 |
77 | 1. As ST3 is the only current, actively supported version, Python 3.3 must be used to validate the tests.
78 |
79 | 2. Unit tests are run with `pytest`. You can install `pytest` via:
80 |
81 | ```
82 | pip install pytest
83 | ```
84 |
85 | The tests should be run from the root folder of the plugin by using the following command:
86 |
87 | ```
88 | py.test .
89 | ```
90 |
91 | 3. Linting is performed on the entire project with with the libraries below. They can be installed with:
92 |
93 | ```
94 | pip install flake8
95 | pip install flake8-docstrings
96 | pip install flake8-mutable
97 | pip install flake8-builtins
98 | pip install pep8-naming
99 | ```
100 |
101 | Linting is performed with the following command:
102 |
103 | ```
104 | flake8 .
105 | ```
106 |
107 | ## Documentation Improvements
108 |
109 | A ton of time has been spent not only creating and supporting this plugin, but also spent making this documentation. If
110 | you feel it is still lacking, show your appreciation for the plugin by helping to improve the documentation. Help with
111 | documentation is always appreciated and can be done via pull requests. There shouldn't be any need to run validation
112 | tests if only updating documentation.
113 |
114 | You don't have to render the docs locally before pull requesting, but if you wish to, I currently use a combination of
115 | [mkdocs](http://www.mkdocs.org) with my own custom Python Markdown [extensions](https://github.com/facelessuser/pymdown-extensions)
116 | to render the docs. You can preview the docs if you install these two packages. The command for previewing the docs is
117 | `mkdocs serve` from the root directory.
118 |
--------------------------------------------------------------------------------
/multiconf.py:
--------------------------------------------------------------------------------
1 | r"""
2 | Multiconf allows the reading of platform and or host specific values from Sublime settings.
3 |
4 | Multiconf is a module that allows you to read platform and/or host
5 | specific configuration values to be used by Sublime Text 2 plugins.
6 |
7 | Using this module's `get` function, allows the user to replace any settings
8 | value in a '.settings' file with a dictionary containing multiple values.
9 |
10 | Multiconf does this by using a dictionary with a special identifier
11 | "#multiconf#" and a list of dictionaries identified by a qualifier of the form
12 |
13 | ":[;:]..."
14 |
15 | For example, the following setting
16 |
17 | ```
18 | "user_home": "/home"
19 | ```
20 |
21 | would result in `get("user_home")` returning the value "/home" but it could also
22 | be replaced with
23 |
24 | ```
25 | "user_home": {
26 | "#multiconf#": [
27 | {"os:windows": "C:\\Users"},
28 | {"os:linux;host:his_pc": "/home"},
29 | {"os:linux;host:her_pc": "/home/her/special"}
30 | ]
31 | }
32 | ```
33 |
34 | Now the same configuration file will provide different values depending on the
35 | machine it's on. On an MS Windows machine the value returned by `get` will be
36 | "C:\\Users", and on a Linux machine with the host name `his_pc` the value will be
37 | "/home".
38 |
39 | -----
40 |
41 | Thanks to: biermeester and matthjes for their ideas and contributions
42 |
43 | -----
44 |
45 | Licensed under MIT.
46 |
47 | Copyright (C) 2012 - 2017 Isaac Muse
48 |
49 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
50 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation
51 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
52 | to permit persons to whom the Software is furnished to do so, subject to the following conditions:
53 |
54 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of
55 | the Software.
56 |
57 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
58 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
59 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
60 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
61 | """
62 | import socket
63 | import sublime
64 | import re
65 |
66 | __version__ = "1.0"
67 |
68 | __CURRENT_HOSTNAME = socket.gethostname().lower()
69 |
70 | QUALIFIERS = r"""([A-Za-z\d_]*):([^;]*)(?:;|$)"""
71 |
72 |
73 | def get(settings_obj, key, default=None, callback=None):
74 | """
75 | Return a Sublime Text plugin setting value.
76 |
77 | Parameters:
78 | settings_obj - a sublime.Settings object or a dictionary containing
79 | settings
80 | key - the name of the setting
81 | default - the default value to return if the key value is not found.
82 | callback - a callback function that, if provided, will be called with
83 | the found and default values as parameters.
84 |
85 | """
86 |
87 | # Parameter validation
88 | if not isinstance(settings_obj, (dict, sublime.Settings)):
89 | raise AttributeError("Invalid settings object")
90 | if not isinstance(key, str):
91 | raise AttributeError("Invalid callback function")
92 | if callback is not None and not hasattr(callback, '__call__'):
93 | raise AttributeError("Invalid callback function")
94 |
95 | setting = settings_obj.get(key, default)
96 | final_val = None
97 |
98 | if isinstance(setting, dict) and "#multiconf#" in setting:
99 | reject_item = False
100 | for entry in setting["#multiconf#"]:
101 | reject_item = False if isinstance(entry, dict) and len(entry) else True
102 |
103 | k, v = entry.popitem()
104 |
105 | if reject_item:
106 | continue
107 |
108 | for qual in re.compile(QUALIFIERS).finditer(k):
109 | if Qualifications.exists(qual.group(1)):
110 | reject_item = not Qualifications.eval_qual(qual.group(1), qual.group(2))
111 | else:
112 | reject_item = True
113 | if reject_item:
114 | break
115 |
116 | if not reject_item:
117 | final_val = v
118 | break
119 |
120 | if reject_item:
121 | final_val = default
122 | else:
123 | final_val = setting
124 |
125 | return callback(final_val, default) if callback else final_val
126 |
127 |
128 | class QualException(Exception):
129 | """Qualification exception."""
130 |
131 | pass
132 |
133 |
134 | class Qualifications(object):
135 | """Qualifications."""
136 |
137 | __qualifiers = {}
138 |
139 | @classmethod
140 | def add_qual(cls, key, callback):
141 | """Add a qualifier."""
142 |
143 | if isinstance(key, str) and re.match(r"^[a-zA-Z][a-zA-Z\d_]*$", key) is None:
144 | raise QualException("'%s' is not a valid function name." % key)
145 | if not hasattr(callback, '__call__'):
146 | raise QualException("Bad function callback.")
147 | if key in cls.__qualifiers:
148 | raise QualException("'%s' qualifier already exists." % key)
149 |
150 | cls.__qualifiers[key] = callback
151 |
152 | @classmethod
153 | def exists(cls, key):
154 | """See if qualifier exists."""
155 |
156 | return (key in cls.__qualifiers)
157 |
158 | @classmethod
159 | def eval_qual(cls, key, value):
160 | """
161 | Evaluate the qualifier.
162 |
163 | See if key is in the qualifier list,
164 | and if so, test the value.
165 | """
166 |
167 | try:
168 | return cls.__qualifiers[key](value)
169 | except Exception:
170 | raise QualException("Failed to execute %s qualifier" % key)
171 |
172 |
173 | def _host_match(h):
174 | """Check if the host matches the input."""
175 |
176 | return (h.lower() == __CURRENT_HOSTNAME)
177 |
178 |
179 | def _os_match(os):
180 | """See if the OS platform matches the input."""
181 |
182 | return (os == sublime.platform())
183 |
184 |
185 | Qualifications.add_qual("host", _host_match)
186 | Qualifications.add_qual("os", _os_match)
187 |
--------------------------------------------------------------------------------
/docs/src/markdown/usage.md:
--------------------------------------------------------------------------------
1 | # User Guide
2 |
3 | ## General Usage
4 |
5 | By default, FuzzyFileNav has access enabled via the view context menu. The context menu gives you various fuzzy
6 | navigation methods. By selecting `Fuzzy Nav Here...`, you will begin navigating the parent folder of the view's file
7 | (if saved on disk). By selecting an option under `Fuzzy File Nav`, you can start with a list of folders from the
8 | project, bookmarks, or the root of the file system. If you prefer to initiate the commands from a shortcut, you can
9 | define your own; some suggestions are shown in [Suggested Accessibility Shortcuts](#suggested-accessibility-shortcuts).
10 | From the FuzzyFileNav panel, you can use shortcuts to copy, paste, delete, open, and various other file actions.
11 |
12 | ## Using the FuzzyFileNav Panel
13 |
14 | While a FuzzyFileNav navigation panel is open, a number of shortcuts will be activated that can apply different actions
15 | to a file or folder or aide in navigating the panel. Actions are only performed when the **full** name is typed into
16 | the panel. Using the path completion navigation shortcut will make this quick and easy.
17 |
18 | ### Autocomplete File Paths
19 |
20 | FuzzyFileNav can complete file paths in the quick panel when ++tab++ is pressed. Depending on the completion style,
21 | ++shift+tab++ can navigate through the completion options backwards. There are three styles of autocompletion that
22 | FuzzyFileNav supports.
23 |
24 | Autocomplete\ Style | Description
25 | ------------------- | -----------
26 | Sublime | Complete path with the selected index in the quick panel.
27 | Unix/Linux | Complete path like a Unix/Linux terminal traditionally completes paths. Will only complete the path if there is one possible option.
28 | Windows | Complete path like Windows completes paths in a command prompt. This allows you to cycle through options that start with the entered text.
29 |
30 | See the [completion_style](#completion_style) setting for more info on configuring the completion style.
31 |
32 | ### Navigating Folders
33 |
34 | When navigating folders in the quick panel, you start typing the folder's name, and you can press ++tab++ to complete
35 | the path (behavior may differ depending on [completion style setting](#autocomplete-file-paths)). You can descend into
36 | the folder by typing a `/` at the end (you can also use `\` on windows). The full path must be completed for "slash
37 | folder navigation". You can can also press ++enter++ and whatever folder is currently selected in the panel will be
38 | navigated to.
39 |
40 | /// tip | Tip
41 | Your file systems root can be accessed any time by typing '/'. You can also switch to windows drives by typing
42 | `c:\` etc.
43 |
44 | The home folder can be accessed any time by typing `~/` into the FuzzyFileNav quick panel.
45 | ///
46 |
47 | ### Actions
48 |
49 | Action | Windows\ &\ Linux | macOS
50 | --------------------------------------------------------- | ------------------------ | -----
51 | [Open](#open) | ++enter++\ or\ ++right++ | ++enter++\ or\ ++right++
52 | [Show/Hide\ hidden\ files](#show-hide-hidden-files) | ++ctrl+h++ | ++cmd+h++
53 | [Show\ Bookmarks](#show-bookmarks) | ++ctrl+b++ | ++cmd+b++
54 | [Delete](#delete) | ++ctrl+d++ | ++cmd+d++
55 | [Copy](#copy) | ++ctrl+c++ | ++cmd+c++
56 | [Cut](#cut) | ++ctrl+x++ | ++cmd+x++
57 | [Paste](#paste) | ++ctrl+v++ | ++cmd+v++
58 | [New\ file](#new-file) | ++ctrl+n++ | ++cmd+n++
59 | [New\ folder](#new-folder) | ++ctrl+shift+n++ | ++cmd+shift+n++
60 | [Save\ file\ as](#save-file-as) | ++ctrl+s++ | ++cmd+s++
61 | [Reveal](#reveal) | ++ctrl+r++ | ++cmd+r++
62 | [Search\ folder](#search-folder) | ++ctrl+f++ | ++cmd+f++
63 | [Add\ folder\ to\ project](#add-folder-to-project) | ++ctrl+p++ | ++cmd+p++
64 | [Add\ folder\ to\ new\ window](#add-folder-to-new-window) | ++ctrl+shift+p++ | ++cmd+shift+p++
65 | [Get\ Current\ Working\ View](#get-current-working-view) | ++ctrl+period++ | ++cmd+period++
66 |
67 | #### Open
68 |
69 | This action opens the selected file in the palette.
70 |
71 | #### Show/Hide hidden files
72 |
73 | Toggles the showing/hiding of hidden files defined by the system or that are hidden via the regular expression patterns
74 | in the settings file.
75 |
76 | #### Show Bookmarks
77 |
78 | Shows the FuzzyFileNav bookmarks panel.
79 |
80 | #### Delete
81 |
82 | Deletes the folder/file object currently typed in the FuzzyFileNav panel.
83 |
84 | #### Copy
85 |
86 | Copies the folder/file object currently typed file in the FuzzyFileNav panel. The copy will remain in the clipboard
87 | until a paste is performed or a new copy is initiated.
88 |
89 | #### Cut
90 |
91 | Cuts (moves) the folder/file object currently typed into the FuzzyFileNav panel. A Fuzzy File Paste must be performed
92 | to complete the cut (move). The cut will remain in the clipboard until a paste is performed or another copy or cut
93 | replaces it.
94 |
95 | #### Paste
96 |
97 | Pastes the folder/file object that is in the clipboard. The file/folder will be pasted into the currently opened folder
98 | in the FuzzyFileNav panel. To rename the folder/file object on paste, type the full name in the panel that should be
99 | used before pressing initiating the paste.
100 |
101 | #### New File
102 |
103 | Creates a new file in the currently opened folder in the FuzzyFileNav panel. The name that is typed into the panel is
104 | the name that will be used.
105 |
106 | #### New Folder
107 |
108 | Creates a new folder in the currently opened folder in the FuzzyFileNav panel. The name that is typed into the panel is
109 | the name that will be used.
110 |
111 | #### Save File as
112 |
113 | Saves the current focused view to the the currently opened folder in the FuzzyFileNav Panel. The name that is typed
114 | into the panel is the name of the file the view will be saved to. You will be prompted for file overwrite.
115 |
116 | #### Reveal
117 |
118 | Reveals the location of the file/folder name typed into the FuzzyFileNav panel in your file manager. Will use the
119 | current folder if a valid one is not typed into the panel.
120 |
121 | #### Search Folder
122 |
123 | This action will open a folder search panel with the current folder's path, or the path of a subfolder (if one is typed
124 | into the FuzzyFileNav panel) and pre-load that folder name into the `where` box. Any content in clipboard will be
125 | pre-loaded into the `Find` box.
126 |
127 | #### Add Folder to Project
128 |
129 | Adds the location of the folder name typed into the FuzzyFileNav panel into the current project. Will use the current
130 | folder if a valid one is not typed into the panel.
131 |
132 | ### Add Folder to New Window
133 |
134 | Open's a new window and adds the location of the folder name typed into the FuzzyFileNav panel into the current project.
135 | Will use the current folder if a valid one is not typed into the panel.
136 |
137 | #### Get Current Working View
138 |
139 | Gets the file name of the current working view and copies it into the FuzzyFileNav Panel.
140 |
141 | ## Settings
142 |
143 | There are various settings you can alter to enhance your experience with FuzzyFileNav.
144 |
145 | ### `bookmarks`
146 |
147 | When using the bookmark command, you can bring up a list of bookmarked folders. Bookmarks are defined in `bookmarks`
148 | setting as shown below. To add or change the bookmark list, just add, remove or modify an entry in the bookmark list.
149 | Each entry is a dictionary containing two keys: `name` and `path`. `name` is the name that will be displayed, path is
150 | the path to the folder.
151 |
152 | ```javascript
153 | // Bookmarked paths
154 | "bookmarks": [
155 | {"name": "My Computer", "path": ""},
156 | {"name": "Root", "path": "/"}
157 | ]
158 | ```
159 |
160 | /// tip | Tip
161 | If it is desired to have specific bookmarks show up only on a specific OS or a specific host, you can augment the
162 | `path` option using the notation below. For more information, please see[Platform/Computer Specific Settings](#platformcomputer-specific-settings).
163 |
164 | ```javascript
165 | // Bookmarked paths
166 | "bookmarks": [
167 | {"name": "My Computer", "path": {"#multiconf#": [{"os:windows": ""}]}},
168 | {"name": "Root", "path": {"#multiconf#": [{"os:linux": "/"}, {"os:osx": "/"}]}}
169 | ]
170 | ```
171 | ///
172 |
173 | ### `home`
174 |
175 | `home` is your home directory. By default it is `~` which expands to your user directory on your OS, but if you would
176 | like to modify it to be something else, this is the place!
177 |
178 | ```javascript
179 | // Location of home folder
180 | "home": "~",
181 | ```
182 |
183 | ### `regex_exclude`
184 |
185 | `regex_exclude` is an array of regular expression patterns that indicate which files and folders FuzzyFileNav should
186 | ignore.
187 |
188 | ```js
189 | // Patterns of files/folders to exclude
190 | "regex_exclude": [".*\\.(DS_Store|svn|git)$"],
191 | ```
192 |
193 | ### `keep_panel_open_after_action`
194 |
195 | Controls whether the quick panel should remain open after a file action (such as open) as performed.
196 |
197 | ```js
198 | // Keep panel open after a file is opened, deleted, created, etc. so
199 | // More files can be have actions performed on them.
200 | "keep_panel_open_after_action": true,
201 | ```
202 |
203 | ### `keep_panel_open_exceptions`
204 |
205 | Provides exceptions for the [keep_panel_open_after_action](#keep_panel_open_after_action) setting.
206 |
207 | ```js
208 | // Actions that can ignore the keep panel open settings
209 | // Available actions: delete, open, saveas, mkfile, mkdir, paste
210 | "keep_panel_open_exceptions": [],
211 | ```
212 |
213 | ### `show_system_hidden_files`
214 |
215 | Controls whether system hidden files are shown in FuzzyFileNav. How files are hidden vary on a given OS, but this should
216 | be able to show them.
217 |
218 | ```js
219 | // Controls whether system hidden files are shown in FuzzyFileNav.
220 | "show_system_hidden_files": true,
221 | ```
222 |
223 | ### `completion_style`
224 | Allows the changing of the completion style to one of three styles.
225 |
226 | ```js
227 | // (fuzzy/windows/nix)
228 | // fuzzy - this will auto-complete with the selected index in the quick panel
229 | // windows - this will complete like a windows terminal would complete paths
230 | // nix - this will complete like a unix/linux terminal traditionally completes paths
231 | "completion_style": "fuzzy",
232 | ```
233 |
234 | ### `start_from_here_default_action`
235 |
236 | There are times when the a FuzzyFileNav navigation command won't be fed a path. One example is when the
237 | `Fuzzy Nav Here...` command is run from a view that hasn't been saved to disk. This setting allows you to specify the
238 | fallback options to display.
239 |
240 | ```js
241 | // If the "FuzzyStartFromFileCommand" is run outside of an open buffer
242 | // or from a buffer that does not exist on disk, you can specify
243 | // its default action to do instead of starting navigation from
244 | // a file's location. Options are "bookmarks", "home", "root", "project".
245 | "start_from_here_default_action": "bookmarks",
246 | ```
247 |
248 | ### `add_folder_to_project_relative`
249 |
250 | When a FuzzyFileNav adds a folder to the project, this will be used to determined if the folder should be added as a
251 | path relative to the project file or not.
252 |
253 | ```js
254 | // Add your folders relative to the project file (if project file exists on disk)
255 | "add_folder_to_project_relative": false,
256 | ```
257 |
258 | ### `add_folder_to_project_follow_symlink`
259 |
260 | When a FuzzyFileNav adds a folder to the project, this will be used to determined if the folder should have the
261 | `follow_symlinks` option set.
262 |
263 | ```js
264 | // When adding folder to project, set "follow_symlinks" setting as true or false
265 | "add_folder_to_project_follow_symlink": true,
266 | ```
267 |
268 | ### `use_sub_notify`
269 |
270 | Enables use of [SubNotify](https://github.com/facelessuser/SubNotify) notifications.
271 |
272 | ```js
273 | // Use subnotify if available
274 | "use_sub_notify": true
275 | ```
276 |
277 | ## Suggested Accessibility Shortcuts
278 |
279 | You can create shortcuts to access FuzzyFileNav Quickly, some examples are shown below.
280 |
281 | For Windows/Linux:
282 |
283 | ```js
284 | [
285 | // Start from the parent folder of the current view's file
286 | { "keys": ["ctrl+o"], "command": "fuzzy_start_from_file" },
287 | // Show bookmarked folders
288 | { "keys": ["ctrl+shift+o"], "command": "fuzzy_bookmarks_load" }
289 | ]
290 | ```
291 |
292 | For macOS:
293 |
294 | ```js
295 | [
296 | // Start from the parent folder of the current view's file
297 | { "keys": ["super+o"], "command": "fuzzy_start_from_file" },
298 | // Show bookmarked folders
299 | { "keys": ["super+shift+o"], "command": "fuzzy_bookmarks_load" }
300 | ]
301 | ```
302 |
303 | ## Platform/Computer Specific Settings
304 |
305 | Currently, the `home` settings in the settings file, and the `path` setting in a bookmark entry can be configured to
306 | have multiple OS and/or host specific settings to help manage settings across different machines.
307 |
308 | The syntax to configure one of these settings for multiple OS and/or hostname:
309 |
310 | - The setting should be a key/value pair, where the key is `#multiconf#` and the value is an array of key/value entries
311 | whose keys describe the host and/or os qualifiers needed for the value to be used.
312 | - The key/value entries will have a key that represents one or more qualifiers, each of which must be separated with a
313 | `;`
314 | - Each qualifier consists of the qualifier type and a qualifier value to compare against. These will be separated by a
315 | `:`.
316 | - There are two supported qualifiers: `host` and `os`. `host` is the name of your PC. `os` is the platform and can be
317 | either `windows`, `linux`, or `osx`.
318 | - The key/value entries will have a value associated with the key, and can be of any type: string, number, array,
319 | dictionary, etc. This value is what will be returned if the qualifier is met.
320 |
321 | examples:
322 |
323 | ```js
324 | "home": {"#multiconf#": [{"os:windows": "c:\\Some\\Location"}, {"os:linux": "/Some/Linux/Location"}]},
325 | ```
326 |
327 | ```js
328 | "home": {
329 | "#multiconf#": [
330 | {"os:windows": "C:\\Users"},
331 | {"os:linux;host:his_pc": "/home"},
332 | {"os:linux;host:her_pc": "/home/her/special"}
333 | ]
334 | }
335 | ```
336 |
--------------------------------------------------------------------------------
/fuzzy_file_nav.py:
--------------------------------------------------------------------------------
1 | """
2 | Fuzzy File Navigation.
3 |
4 | Licensed under MIT
5 | Copyright (c) 2012 - 2015 Isaac Muse
6 | """
7 | import sublime
8 | import sublime_plugin
9 | import os
10 | import os.path as path
11 | import re
12 | import shutil
13 | import glob
14 | from FuzzyFileNav.multiconf import get as qualify_settings
15 | from FuzzyFileNav.notify import error, notify
16 | import platform
17 | if platform.system() == "Windows":
18 | import ctypes
19 |
20 | FUZZY_SETTINGS = "fuzzy_file_nav.sublime-settings"
21 | CMD_WIN = r"^(?:(?:(~)|(\.\.))(?:\\|/)|((?:[A-Za-z]{1}:)?(?:\\|/))|([\w\W]*(?:\\|/)))$"
22 | CMD_NIX = r"^(?:(?:(~)|(\.\.))/|(/)|([\w\W]*/))$"
23 | WIN_DRIVE = r"(^[A-Za-z]{1}:(?:\\|/))"
24 | PLATFORM = None
25 |
26 |
27 | def debug_log(s):
28 | """Debug log."""
29 | if sublime.load_settings(FUZZY_SETTINGS).get("debug", False):
30 | print("FuzzyFileNav: {}".format(s))
31 |
32 |
33 | def status_cwd():
34 | """Set the status bar to the current working directory."""
35 |
36 | if FuzzyFileNavCommand.status:
37 | sublime.run_command("fuzzy_get_cwd")
38 | sublime.set_timeout(status_cwd, 1000)
39 |
40 |
41 | def get_root_path():
42 | """
43 | Get the root path.
44 |
45 | Windows doesn't have a root, so just
46 | return an empty string to represent its root.
47 | """
48 |
49 | return "" if PLATFORM == "windows" else "/"
50 |
51 |
52 | def get_path_true_case(pth):
53 | """
54 | Get the true case for the given path.
55 |
56 | This is mainly for windows;
57 | OSX and Linux will just return the same path.
58 | """
59 | if PLATFORM == "windows":
60 | # http://stackoverflow.com/a/14742779
61 | true_path = None
62 | if path.exists(pth):
63 | glob_test = []
64 | parts = path.normpath(pth).split('\\')
65 | glob_test.append(parts[0].upper())
66 | for p in parts[1:]:
67 | glob_test.append("{}[{}]".format(p[:-1], p[-1]))
68 | results = glob.glob('\\'.join(glob_test))
69 | if results is not None:
70 | true_path = results[0]
71 | else:
72 | true_path = pth
73 | return true_path
74 |
75 |
76 | def expanduser(path, default):
77 | """Expand the path by converting ~ to the user path."""
78 |
79 | return os.path.expanduser(path) if path is not None else path
80 |
81 |
82 | def back_dir(cwd):
83 | """Step back a directory."""
84 |
85 | prev = path.dirname(cwd)
86 |
87 | # On windows, if you try and get the
88 | # directory name of a drive, you get the drive.
89 | # So if the previous directory is the same
90 | # as the current, back out of the drive and
91 | # list all drives.
92 | return get_root_path() if prev == cwd else prev
93 |
94 |
95 | def get_drives():
96 | """Search through valid drive names and see if they exist."""
97 |
98 | return [d + ":" for d in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if path.exists(d + ":")]
99 |
100 |
101 | def back_to_root(cwd):
102 | """Back to the root directory."""
103 |
104 | root = ""
105 | if PLATFORM != "windows":
106 | # Linux/Unix: just return root
107 | root = "/"
108 | else:
109 | # Windows: try and find root drive from path
110 | # otherwise return "" to signal fuzzy navigation to
111 | # list all drives.
112 | m = re.match(WIN_DRIVE, cwd)
113 | if m:
114 | root = m.group(1)
115 | return root
116 |
117 |
118 | class FuzzyEditGlobal(object):
119 | """Class containing global variables to store buffers and regions for editing."""
120 |
121 | bfr = None
122 | region = None
123 |
124 | @classmethod
125 | def clear(cls):
126 | """Clear buffer and region."""
127 |
128 | cls.bfr = None
129 | cls.region = None
130 |
131 |
132 | class FuzzyApplyEditsCommand(sublime_plugin.TextCommand):
133 | """Applies edits to a view."""
134 |
135 | def run(self, edit):
136 | """Run the command."""
137 |
138 | self.view.replace(edit, FuzzyEditGlobal.region, FuzzyEditGlobal.bfr)
139 |
140 |
141 | class FuzzyPanelText(object):
142 | """Object to track panel content."""
143 |
144 | content = ""
145 |
146 | @classmethod
147 | def set_content(cls, txt):
148 | """Set content."""
149 |
150 | cls.content = txt
151 |
152 | @classmethod
153 | def get_content(cls):
154 | """Return the content."""
155 | txt = cls.content
156 | return txt
157 |
158 | @classmethod
159 | def clear_content(cls):
160 | """Clear the content."""
161 |
162 | cls.content = ""
163 |
164 |
165 | class FuzzyEventListener(sublime_plugin.EventListener):
166 | """Listener that detects panel closes, shortcuts pressed in the panel, and panel content changes."""
167 |
168 | def on_activated(self, view):
169 | """Track when fuzzy panels are activated or deactivated."""
170 |
171 | # New window gained activation? Reset fuzzy command state
172 | if FuzzyFileNavCommand.active and view.window() and view.window().id() != FuzzyFileNavCommand.win_id:
173 | FuzzyFileNavCommand.reset()
174 | # View has not been assigned yet since fuzzy navigation panel appeared; assign it
175 | if (
176 | FuzzyFileNavCommand.active and
177 | (FuzzyFileNavCommand.view is None or FuzzyFileNavCommand.view.id() != view.id())
178 | ):
179 | FuzzyFileNavCommand.view = view
180 |
181 | def on_query_context(self, view, key, operator, operand, match_all):
182 | """Capture shortcuts in a `FuzzyNavPanel`."""
183 |
184 | active = FuzzyFileNavCommand.active is True
185 | if active and FuzzyFileNavCommand.view is not None and FuzzyFileNavCommand.view.id() == view.id():
186 | FuzzyPanelText.set_content(view.substr(view.line(view.sel()[0])))
187 | full_name = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
188 | empty = (FuzzyPanelText.get_content().strip() in ("", '.', '..'))
189 | # See if this is the auto-complete path command
190 | if key in [
191 | "fuzzy_path_complete", "fuzzy_path_complete_back", "fuzzy_toggle_hidden",
192 | "fuzzy_bookmarks_load", "fuzzy_get_cwd", "fuzzy_cwv"
193 | ]:
194 | return active
195 | elif key == "fuzzy_open_folder":
196 | if (
197 | (
198 | (not empty and path.exists(full_name) and path.isdir(full_name)) or
199 | (empty and path.exists(FuzzyFileNavCommand.cwd))
200 | )
201 | ):
202 | return active
203 | elif key in ["fuzzy_reveal", "fuzzy_search"]:
204 | if path.exists(FuzzyFileNavCommand.cwd):
205 | return active
206 | else:
207 | notify("{} does not exist!".format(FuzzyFileNavCommand.cwd))
208 | elif key in ["fuzzy_quick_open"]:
209 | sels = view.sel()
210 | if len(sels) == 1:
211 | if sels[0].a == sels[0].b and sels[0].a == view.size():
212 | return active
213 | else:
214 | pass
215 | elif key == "fuzzy_delete":
216 | if not empty and path.exists(full_name):
217 | return active
218 | elif not empty:
219 | notify("{} does not exist!".format(full_name))
220 | elif key in ["fuzzy_make_file", "fuzzy_make_folder"]:
221 | if not empty and not path.exists(full_name):
222 | return active
223 | elif not empty:
224 | notify("{} already exists!".format(full_name))
225 | elif key == "fuzzy_save_as":
226 | if not empty and (not path.exists(full_name) or not path.isdir(full_name)):
227 | return active
228 | elif not empty:
229 | notify("{} is a directory!".format(full_name))
230 | elif key == "fuzzy_copy":
231 | if not empty and path.exists(full_name):
232 | return active
233 | elif not empty:
234 | notify("{} does not exist!".format(full_name))
235 | elif key == "fuzzy_cut":
236 | if path.exists(FuzzyFileNavCommand.cwd):
237 | return active
238 | else:
239 | notify("{} does not exist!".format(FuzzyFileNavCommand.cwd))
240 | elif key == "fuzzy_paste":
241 | if path.exists(FuzzyFileNavCommand.cwd) and len(FuzzyClipboardCommand.clips):
242 | return active
243 | else:
244 | notify("{} does not exist!".format(FuzzyFileNavCommand.cwd))
245 | return False
246 |
247 | def on_modified(self, view):
248 | """Monitor content change in the panel and take actions accordingly."""
249 |
250 | if (
251 | FuzzyFileNavCommand.active and FuzzyFileNavCommand.view is not None and
252 | FuzzyFileNavCommand.view.id() == view.id()
253 | ):
254 | sel = view.sel()[0]
255 | win = view.window()
256 | line_text = view.substr(view.line(sel))
257 | FuzzyPathCompleteCommand.update_autocomplete(line_text)
258 | regex = CMD_WIN if PLATFORM == "windows" else CMD_NIX
259 | m = re.match(regex, line_text)
260 | if m:
261 | if m.group(1):
262 | # Go Home
263 | FuzzyFileNavCommand.fuzzy_reload = True
264 | home = qualify_settings(sublime.load_settings(FUZZY_SETTINGS), "home", "", expanduser)
265 | home = get_root_path() if not path.exists(home) or not path.isdir(home) else home
266 | win.run_command("hide_overlay")
267 | win.run_command("fuzzy_file_nav", {"start": home})
268 | elif m.group(2):
269 | # Back a directory
270 | FuzzyFileNavCommand.fuzzy_reload = True
271 | win.run_command("hide_overlay")
272 | win.run_command("fuzzy_file_nav", {"start": back_dir(FuzzyFileNavCommand.cwd)})
273 | elif m.group(3):
274 | # Go to root of drive/computer
275 | new_path = None
276 | if PLATFORM == "windows" and re.match(WIN_DRIVE, line_text):
277 | if path.exists(line_text):
278 | new_path = line_text.upper()
279 | else:
280 | new_path = back_to_root(FuzzyFileNavCommand.cwd)
281 | if new_path is not None:
282 | FuzzyFileNavCommand.fuzzy_reload = True
283 | win.run_command("hide_overlay")
284 | win.run_command("fuzzy_file_nav", {"start": new_path})
285 | elif m.group(4):
286 | # Load folder
287 | new_path = path.join(FuzzyFileNavCommand.cwd, m.group(4))
288 | if path.exists(new_path) and path.isdir(new_path):
289 | FuzzyFileNavCommand.fuzzy_reload = True
290 | win.run_command("hide_overlay")
291 | win.run_command("fuzzy_file_nav", {"start": new_path})
292 |
293 |
294 | class FuzzyOpenFolderCommand(sublime_plugin.WindowCommand):
295 | """Open folders in project."""
296 |
297 | def compare_relative(self, proj_folder, new_folder, proj_file):
298 | """Compare folder to relative path."""
299 |
300 | if proj_file is None:
301 | return self.compare_absolute(proj_folder, new_folder)
302 | return path.relpath(new_folder, path.dirname(proj_file)) == proj_folder
303 |
304 | def compare_absolute(self, proj_folder, new_folder):
305 | """Compare folder to absolute path."""
306 |
307 | return proj_folder == new_folder
308 |
309 | def compare(self, folders, new_folder, proj_file):
310 | """Check if folder exists in the project."""
311 |
312 | already_exists = False
313 | for folder in folders:
314 | if PLATFORM == "windows":
315 | if re.match(WIN_DRIVE, folder["path"]) is not None:
316 | already_exists = self.compare_absolute(folder["path"].lower(), new_folder.lower())
317 | else:
318 | already_exists = self.compare_relative(
319 | folder["path"].lower(), new_folder.lower(), proj_file.lower()
320 | )
321 | else:
322 | if folder["path"].startswith("/"):
323 | already_exists = self.compare_absolute(folder["path"], new_folder)
324 | else:
325 | already_exists = self.compare_relative(folder["path"], new_folder, proj_file)
326 | if already_exists:
327 | break
328 | return already_exists
329 |
330 | def run(self, new_window=False):
331 | """Run the command."""
332 |
333 | if new_window:
334 | sublime.run_command('new_window')
335 | window = sublime.active_window()
336 | else:
337 | window = self.window
338 |
339 | file_name = FuzzyPanelText.get_content()
340 | FuzzyPanelText.clear_content()
341 | proj_file = window.project_file_name()
342 | data = window.project_data()
343 | if data is None:
344 | data = {}
345 | new_folder = path.join(FuzzyFileNavCommand.cwd, file_name)
346 | if not path.exists(new_folder):
347 | return
348 | if not path.isdir(new_folder):
349 | new_folder = path.dirname(new_folder)
350 | if "folders" not in data:
351 | data["folders"] = []
352 | already_exists = self.compare(data["folders"], new_folder, proj_file)
353 |
354 | if not already_exists:
355 | true_path = get_path_true_case(new_folder)
356 | if true_path is not None:
357 | if (
358 | sublime.load_settings(FUZZY_SETTINGS).get("add_folder_to_project_relative", False) and
359 | proj_file is not None
360 | ):
361 | new_folder = path.relpath(new_folder, path.dirname(proj_file))
362 | follow_sym = sublime.load_settings(FUZZY_SETTINGS).get("add_folder_to_project_follow_symlink", True)
363 | data["folders"].append({'follow_symlinks': follow_sym, 'path': new_folder})
364 | window.set_project_data(data)
365 | else:
366 | error("Couldn't resolve case for path {}!".format(new_folder))
367 |
368 | if new_window:
369 | self.window.run_command("hide_overlay")
370 | FuzzyFileNavCommand.reset()
371 |
372 |
373 | class FuzzyProjectFolderLoadCommand(sublime_plugin.WindowCommand):
374 | """Load folder content in quick panel."""
375 |
376 | def run(self):
377 | """Run command."""
378 |
379 | if FuzzyFileNavCommand.active:
380 | self.window.run_command("hide_overlay")
381 | self.display = []
382 | proj_file = self.window.project_file_name()
383 | data = self.window.project_data()
384 | if data is None:
385 | data = {}
386 | if "folders" not in data:
387 | data["folders"] = []
388 | for folder in data["folders"]:
389 | if (
390 | (PLATFORM == "windows" and re.match(WIN_DRIVE, folder["path"]) is None and proj_file is not None) or
391 | (PLATFORM != "windows" and not folder["path"].startswith("/") and proj_file is not None)
392 | ):
393 | self.display.append(
394 | get_path_true_case(path.abspath(path.join(path.dirname(proj_file), folder["path"])))
395 | )
396 | else:
397 | self.display.append(get_path_true_case(folder["path"]))
398 |
399 | if len(self.display):
400 | self.window.show_quick_panel([path.basename(x) for x in self.display], self.check_selection)
401 |
402 | def check_selection(self, value):
403 | """Check the user's selection."""
404 |
405 | if value > -1:
406 | # Load fuzzy navigation with project folder
407 | self.window.run_command("fuzzy_file_nav", {"start": self.display[value]})
408 |
409 |
410 | class FuzzyCurrentWorkingViewCommand(sublime_plugin.TextCommand):
411 | """Insert current working directory into panel."""
412 |
413 | def run(self, edit):
414 | """Run command."""
415 |
416 | name = None
417 | win = sublime.active_window()
418 | if win is not None:
419 | cwv = win.active_view()
420 | if cwv is not None:
421 | file_name = cwv.file_name()
422 | if file_name is not None:
423 | name = path.basename(file_name)
424 | if name is not None:
425 | view = FuzzyFileNavCommand.view
426 | FuzzyEditGlobal.bfr = name
427 | FuzzyEditGlobal.region = sublime.Region(0, view.size())
428 | view.run_command("fuzzy_apply_edits")
429 | FuzzyEditGlobal.clear()
430 | sels = view.sel()
431 | sels.clear()
432 | sels.add(sublime.Region(view.size()))
433 |
434 |
435 | class FuzzyRevealCommand(sublime_plugin.WindowCommand):
436 | """Reveal the file/folder in file browser."""
437 |
438 | def run(self):
439 | """Run command."""
440 |
441 | file_name = FuzzyPanelText.get_content()
442 | FuzzyPanelText.clear_content()
443 | if path.exists(path.join(FuzzyFileNavCommand.cwd, file_name)):
444 | self.window.run_command("open_dir", {"dir": FuzzyFileNavCommand.cwd, "file": file_name})
445 | else:
446 | self.window.run_command("open_dir", {"dir": FuzzyFileNavCommand.cwd, "file": ""})
447 |
448 |
449 | class FuzzySearchFolderCommand(sublime_plugin.WindowCommand):
450 | """Initiate Sublime's folder search."""
451 |
452 | def run(self):
453 | """Run command."""
454 |
455 | file_name = FuzzyPanelText.get_content()
456 | FuzzyPanelText.clear_content()
457 | folder = path.join(FuzzyFileNavCommand.cwd, file_name)
458 | if path.exists(folder) and path.isdir(folder):
459 | self.window.run_command("show_panel", {"panel": "find_in_files", "where": folder})
460 | else:
461 | self.window.run_command("show_panel", {"panel": "find_in_files", "where": FuzzyFileNavCommand.cwd})
462 |
463 |
464 | class FuzzyClipboardCommand(sublime_plugin.WindowCommand):
465 | """Command to handle fuzzy cut/copy/paste actions."""
466 |
467 | clips = []
468 | action = None
469 |
470 | def run(self, action):
471 | """Run command."""
472 |
473 | self.cls = FuzzyClipboardCommand
474 | if action in ["cut", "copy"]:
475 | if len(self.cls.clips):
476 | self.cls.clear_entries()
477 | full_name = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
478 | FuzzyPanelText.clear_content()
479 | self.cls.add_entry(full_name)
480 | self.cls.set_action(action)
481 | FuzzyFileNavCommand.fuzzy_reload = True
482 | self.window.run_command("hide_overlay")
483 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
484 | elif action == "paste":
485 | self.paste()
486 |
487 | def paste(self):
488 | """Paste files."""
489 |
490 | errors = False
491 | self.to_path = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
492 | FuzzyPanelText.clear_content()
493 | move = (self.cls.action == "cut")
494 | self.from_path = self.cls.clips[0]
495 | self.cls.clear_entries()
496 | multi_file = (
497 | bool(sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_after_action", False)) and
498 | "paste" not in sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_exceptions", [])
499 | )
500 |
501 | if not multi_file:
502 | self.window.run_command("hide_overlay")
503 | FuzzyFileNavCommand.reset()
504 | else:
505 | FuzzyFileNavCommand.fuzzy_reload = True
506 |
507 | if path.exists(self.from_path):
508 | if path.isdir(self.from_path):
509 | self.action = shutil.copytree if not bool(move) else shutil.move
510 | errors = self.dir_copy()
511 | else:
512 | self.action = shutil.copyfile if not bool(move) else shutil.move
513 | errors = self.file_copy()
514 | if multi_file:
515 | if errors:
516 | FuzzyFileNavCommand.reset()
517 | else:
518 | self.window.run_command("hide_overlay")
519 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
520 |
521 | def dir_copy(self):
522 | """Handle directory copy."""
523 |
524 | errors = False
525 | try:
526 | if path.exists(self.to_path):
527 | if path.isdir(self.to_path):
528 | dest = path.join(self.to_path, path.basename(self.from_path))
529 | same = self.samefile(self.from_path, dest)
530 | if path.exists(dest) and not same:
531 | if sublime.ok_cancel_dialog('{} exists!\n\nOverwrite?'.format(dest)):
532 | if path.isdir(dest):
533 | shutil.rmtree(dest)
534 | else:
535 | os.remove(dest)
536 | else:
537 | return errors
538 | if not same:
539 | self.action(self.from_path, dest)
540 | else:
541 | errors = True
542 | error("{} already exists!".format(self.to_path))
543 | elif path.exists(path.dirname(self.to_path)):
544 | same = self.samefile(self.from_path, self.to_path)
545 | if path.exists(self.to_path) and not same:
546 | if sublime.ok_cancel_dialog('{} exists!\n\nOverwrite?'.format(self.to_path)):
547 | if path.isdir(self.to_path):
548 | shutil.rmtree(self.to_path)
549 | else:
550 | os.remove(self.to_path)
551 | else:
552 | return errors
553 | if not same:
554 | self.action(self.from_path, self.to_path)
555 | else:
556 | errors = True
557 | error("Cannot copy {}".format(self.from_path))
558 | except Exception:
559 | errors = True
560 | error("Cannot copy {}".format(self.from_path))
561 | return errors
562 |
563 | def samefile(self, a, b):
564 | """Check if files are the same."""
565 |
566 | if path.exists(a) and path.exists(b):
567 | return path.samefile(a, b)
568 |
569 | # One or both don't exist, so they can't be the same.
570 | return False
571 |
572 | def file_copy(self):
573 | """Handle file copy."""
574 |
575 | errors = False
576 | try:
577 | if path.exists(self.to_path):
578 | if path.isdir(self.to_path):
579 | file_name = path.join(self.to_path, path.basename(self.from_path))
580 | same = self.samefile(self.from_path, file_name)
581 | if path.exists(file_name) and not same:
582 | if not sublime.ok_cancel_dialog("{} exists!\n\nOverwrite file?".format(file_name)):
583 | return errors
584 | if not same:
585 | self.action(self.from_path, file_name)
586 | else:
587 | same = self.samefile(self.from_path, self.to_path)
588 | if not same and sublime.ok_cancel_dialog("{} exists!\n\nOverwrite file?".format(self.to_path)):
589 | self.action(self.from_path, self.to_path)
590 | elif path.exists(path.dirname(self.to_path)):
591 | same = self.samefile(self.from_path, self.to_path)
592 | if not same:
593 | self.action(self.from_path, self.to_path)
594 | else:
595 | errors = True
596 | error("Cannot copy {}".fromat(self.from_path))
597 | except Exception:
598 | errors = True
599 | error("Cannot copy {}".fromat(self.from_path))
600 | return errors
601 |
602 | @classmethod
603 | def add_entry(cls, entry):
604 | """Add entry to clip board."""
605 |
606 | if entry not in cls.clips:
607 | cls.clips.append(entry)
608 |
609 | @classmethod
610 | def set_action(cls, action):
611 | """Set the action."""
612 |
613 | cls.action = action
614 |
615 | @classmethod
616 | def clear_entries(cls):
617 | """Clear entries."""
618 |
619 | cls.clips = []
620 | cls.action = None
621 |
622 |
623 | class FuzzyDeleteCommand(sublime_plugin.WindowCommand):
624 | """Delete file/folder."""
625 |
626 | def run(self):
627 | """Run command."""
628 |
629 | errors = False
630 | full_name = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
631 | FuzzyPanelText.clear_content()
632 | multi_file = (
633 | bool(sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_after_action", False)) and
634 | "delete" not in sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_exceptions", [])
635 | )
636 |
637 | if not multi_file:
638 | self.window.run_command("hide_overlay")
639 | FuzzyFileNavCommand.reset()
640 | else:
641 | FuzzyFileNavCommand.fuzzy_reload = True
642 |
643 | if sublime.ok_cancel_dialog("Delete {}?\n\nWarning: this is permanent!".format(full_name)):
644 | try:
645 | if path.isdir(full_name):
646 | shutil.rmtree(full_name)
647 | else:
648 | os.remove(full_name)
649 | except Exception:
650 | errors = True
651 | error("Error deleting {}!".format(full_name))
652 |
653 | if multi_file:
654 | if errors:
655 | FuzzyFileNavCommand.reset()
656 | else:
657 | self.window.run_command("hide_overlay")
658 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
659 |
660 |
661 | class FuzzySaveFileCommand(sublime_plugin.WindowCommand):
662 | """Save file."""
663 |
664 | def save(self):
665 | """Perform save."""
666 |
667 | if self.view.is_loading():
668 | sublime.set_timeout(self.save, 100)
669 | else:
670 | FuzzyEditGlobal.bfr = self.bfr
671 | FuzzyEditGlobal.region = sublime.Region(0, self.view.size())
672 | self.view.run_command("fuzzy_apply_edits")
673 | FuzzyEditGlobal.clear()
674 | sels = self.view.sel()
675 | sels.clear()
676 | sels.add_all(self.current_sels)
677 | self.window.focus_view(self.view)
678 | self.view.set_viewport_position(self.position)
679 | self.view.run_command("save")
680 | if self.multi_file:
681 | self.window.run_command("hide_overlay")
682 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
683 |
684 | def run(self):
685 | """Run command."""
686 |
687 | full_name = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
688 | file_exists = path.exists(full_name)
689 | if file_exists:
690 | if not sublime.ok_cancel_dialog("{} exists!\n\nOverwrite file?".format(full_name)):
691 | return
692 |
693 | FuzzyPanelText.clear_content()
694 | self.multi_file = (
695 | bool(sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_after_action", False)) and
696 | "saveas" not in sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_exceptions", [])
697 | )
698 | active_view = self.window.active_view()
699 | if active_view is None:
700 | return
701 | self.bfr = active_view.substr(sublime.Region(0, active_view.size()))
702 | self.current_sels = [s for s in active_view.sel()]
703 | self.position = active_view.viewport_position()
704 | if not self.multi_file:
705 | self.window.run_command("hide_overlay")
706 | FuzzyFileNavCommand.reset()
707 | else:
708 | FuzzyFileNavCommand.fuzzy_reload = True
709 |
710 | try:
711 | if not file_exists:
712 | with open(full_name, "a"):
713 | pass
714 | active_view.set_scratch(True)
715 | self.window.run_command("close")
716 | self.view = self.window.open_file(full_name)
717 | sublime.set_timeout(self.save, 100)
718 | except Exception:
719 | error("Could not create {}!".format(full_name))
720 | if self.multi_file:
721 | FuzzyFileNavCommand.reset()
722 |
723 |
724 | class FuzzyMakeFileCommand(sublime_plugin.WindowCommand):
725 | """Create a file."""
726 |
727 | def run(self):
728 | """Run command."""
729 |
730 | errors = False
731 | full_name = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
732 | FuzzyPanelText.clear_content()
733 | multi_file = (
734 | bool(sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_after_action", False)) and
735 | "mkfile" not in sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_exceptions", [])
736 | )
737 | if not multi_file:
738 | self.window.run_command("hide_overlay")
739 | FuzzyFileNavCommand.reset()
740 | else:
741 | FuzzyFileNavCommand.fuzzy_reload = True
742 |
743 | try:
744 | with open(full_name, "a"):
745 | pass
746 | self.window.open_file(full_name)
747 | except Exception:
748 | errors = True
749 | error("Could not create {}!".format(full_name))
750 |
751 | if multi_file:
752 | if errors:
753 | FuzzyFileNavCommand.reset()
754 | else:
755 | self.window.run_command("hide_overlay")
756 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
757 |
758 |
759 | class FuzzyMakeFolderCommand(sublime_plugin.WindowCommand):
760 | """Create a folder."""
761 |
762 | def run(self):
763 | """Run command."""
764 |
765 | errors = False
766 | full_name = path.join(FuzzyFileNavCommand.cwd, FuzzyPanelText.get_content())
767 | FuzzyPanelText.clear_content()
768 | multi_file = (
769 | bool(sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_after_action", False)) and
770 | "mkdir" not in sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_exceptions", [])
771 | )
772 | if not multi_file:
773 | self.window.run_command("hide_overlay")
774 | FuzzyFileNavCommand.reset()
775 | else:
776 | FuzzyFileNavCommand.fuzzy_reload = True
777 |
778 | try:
779 | os.makedirs(full_name)
780 | except Exception:
781 | errors = True
782 | error("Could not create {}!".format(full_name))
783 |
784 | if multi_file:
785 | if errors:
786 | FuzzyFileNavCommand.reset()
787 | else:
788 | self.window.run_command("hide_overlay")
789 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
790 |
791 |
792 | class FuzzyBookmarksLoadCommand(sublime_plugin.WindowCommand):
793 | """Load bookmarks in panel."""
794 |
795 | def run(self):
796 | """Run command."""
797 |
798 | if FuzzyFileNavCommand.active:
799 | self.window.run_command("hide_overlay")
800 | self.display = []
801 | # Search through bookmarks
802 | bookmarks = sublime.load_settings(FUZZY_SETTINGS).get("bookmarks", [])
803 | for bm in bookmarks:
804 | # Only show bookmarks that are for this host and/or platform
805 | target = qualify_settings(bm, "path", None, expanduser)
806 | # Make sure bookmarks point to valid locations
807 | if (
808 | target is not None and
809 | ((path.exists(target) and path.isdir(target)) or (PLATFORM == "windows" and target == ""))
810 | ):
811 | self.display.append([bm.get("name", target), target])
812 | if len(self.display) > 0:
813 | # Display bookmarks if valid ones were found
814 | self.window.run_command("hide_overlay")
815 | FuzzyFileNavCommand.reset()
816 | self.window.show_quick_panel(self.display, self.check_selection)
817 |
818 | def check_selection(self, value):
819 | """Check the user's selection and navigate the folder."""
820 |
821 | if value > -1:
822 | # Load fuzzy navigation with bookmarked shortcut
823 | self.window.run_command("fuzzy_file_nav", {"start": self.display[value][1]})
824 |
825 |
826 | class FuzzyGetCwdCommand(sublime_plugin.ApplicationCommand):
827 | """Show the current working directory in the status bar."""
828 |
829 | def run(self):
830 | """Run command."""
831 |
832 | if FuzzyFileNavCommand.active:
833 | sublime.status_message("CWD: " + FuzzyFileNavCommand.cwd)
834 |
835 |
836 | class FuzzyToggleHiddenCommand(sublime_plugin.WindowCommand):
837 | """Toggle whether hidden files are shown or hidden."""
838 |
839 | def run(self, show=None):
840 | """Run command."""
841 |
842 | if FuzzyFileNavCommand.active:
843 | FuzzyFileNavCommand.fuzzy_reload = True
844 | if show is None:
845 | FuzzyFileNavCommand.hide_hidden = not FuzzyFileNavCommand.hide_hidden
846 | elif bool(show):
847 | FuzzyFileNavCommand.hide_hidden = True
848 | else:
849 | FuzzyFileNavCommand.hide_hidden = False
850 | self.window.run_command("hide_overlay")
851 | self.window.run_command("fuzzy_file_nav", {"start": FuzzyFileNavCommand.cwd})
852 |
853 |
854 | class FuzzyStartFromFileCommand(sublime_plugin.WindowCommand):
855 | """Start navigating from the folder, project, file system root, or bookmarks."""
856 |
857 | def run(self, paths=[]):
858 | """Run command."""
859 |
860 | if FuzzyFileNavCommand.active:
861 | self.window.run_command("hide_overlay")
862 | name = self.get_target(paths)
863 | actions = set(["home", "bookmarks", "root", "project"])
864 | # Check if you can retrieve a file name (means it exists on disk).
865 | if name is None:
866 | view = self.window.active_view()
867 | name = view.file_name() if view is not None else None
868 | if name:
869 | # Buffer/view has a file name, so it exists on disk; navigate its parent directory.
870 | self.window.run_command(
871 | "fuzzy_file_nav",
872 | {
873 | "start": path.dirname(name) if not path.isdir(name) else name
874 | }
875 | )
876 | else:
877 | action = sublime.load_settings(FUZZY_SETTINGS).get("start_from_here_default_action", "bookmarks")
878 | if action in actions:
879 | # Load special action
880 | debug_log("Load default action: {}".format(action))
881 | getattr(self, action)()
882 | else:
883 | # Invalid action; just load bookmarks
884 | self.window.run_command("fuzzy_bookmarks_load")
885 | debug_log("Load bookmarks")
886 |
887 | def get_target(self, paths=[]):
888 | """Get the target to navigate from."""
889 |
890 | target = None
891 | if len(paths) and path.exists(paths[0]):
892 | target = paths[0]
893 | return target
894 |
895 | def project(self):
896 | """Get folders from project."""
897 |
898 | data = self.window.project_data()
899 | if data is None:
900 | data = {}
901 | if "folders" not in data:
902 | data["folders"] = []
903 |
904 | if len(data["folders"]):
905 | self.window.run_command("fuzzy_project_folder_load")
906 | else:
907 | self.home()
908 |
909 | def home(self):
910 | """Navigate from home."""
911 |
912 | home = qualify_settings(sublime.load_settings(FUZZY_SETTINGS), "home", "", expanduser)
913 | home = get_root_path() if not path.exists(home) or not path.isdir(home) else home
914 | self.window.run_command("fuzzy_file_nav", {"start": home})
915 |
916 | def root(self):
917 | """Navigate from root of file system."""
918 |
919 | self.window.run_command("fuzzy_file_nav")
920 |
921 | def bookmarks(self):
922 | """Load bookmarks."""
923 |
924 | if len(sublime.load_settings(FUZZY_SETTINGS).get("bookmarks", [])):
925 | self.window.run_command("fuzzy_bookmarks_load")
926 | else:
927 | self.home()
928 |
929 |
930 | class FuzzyQuickOpenCommand(sublime_plugin.WindowCommand):
931 | """Mimic open file when the right arrow key is pressed (like sublime does)."""
932 |
933 | def run(self):
934 | """Run command."""
935 |
936 | hl_index = FuzzyPathCompleteCommand.hl_index
937 | if hl_index != -1 or hl_index < len(FuzzyFileNavCommand.files):
938 | file_path = path.join(FuzzyFileNavCommand.cwd, FuzzyFileNavCommand.files[hl_index])
939 | if not path.isdir(file_path):
940 | self.window.open_file(file_path)
941 |
942 |
943 | class FuzzyPathCompleteCommand(sublime_plugin.WindowCommand):
944 | """Complete a path when 'tab' is pressed."""
945 |
946 | last = None
947 | in_progress = False
948 | text = None
949 | hl_index = -1
950 |
951 | def run(self, back=False):
952 | """Run command."""
953 | cls = FuzzyPathCompleteCommand
954 | view = FuzzyFileNavCommand.view
955 | settings = sublime.load_settings(FUZZY_SETTINGS)
956 | completion_style = settings.get("completion_style", "fuzzy")
957 | if view is not None:
958 | if completion_style == "fuzzy":
959 | self.sublime_completion(cls, view)
960 | else:
961 | nix_path_complete = completion_style == "nix"
962 | self.terminal_completion(cls, view, back, nix_path_complete)
963 |
964 | def sublime_completion(self, cls, view):
965 | """Sublime fuzzy completion."""
966 |
967 | if cls.hl_index > 0 and cls.hl_index < len(FuzzyFileNavCommand.files):
968 | FuzzyEditGlobal.bfr = FuzzyFileNavCommand.files[cls.hl_index]
969 | if path.isdir(path.join(FuzzyFileNavCommand.cwd, FuzzyEditGlobal.bfr)):
970 | FuzzyEditGlobal.bfr = FuzzyEditGlobal.bfr[0:len(FuzzyEditGlobal.bfr) - 1]
971 | FuzzyEditGlobal.region = sublime.Region(0, view.size())
972 | view.run_command("fuzzy_apply_edits")
973 | FuzzyEditGlobal.clear()
974 | sels = view.sel()
975 | sels.clear()
976 | sels.add(sublime.Region(view.size()))
977 |
978 | def terminal_completion(self, cls, view, back, nix_path_complete):
979 | """Terminal style completion."""
980 |
981 | complete = []
982 | case_insensitive = PLATFORM == "windows" or not nix_path_complete
983 | sel = view.sel()[0]
984 |
985 | if cls.text is None:
986 | cls.text = view.substr(view.line(sel))
987 | debug_log("completion text - " + cls.text)
988 | current = cls.text.lower() if case_insensitive else cls.text
989 | for item in FuzzyFileNavCommand.files:
990 | # Windows is case insensitive
991 | if item == '..':
992 | continue
993 | i = item.lower() if case_insensitive else item
994 | # See if current input matches the beginning of some of the entries
995 | if i.startswith(current):
996 | if path.isdir(path.join(FuzzyFileNavCommand.cwd, item)):
997 | item = item[0:len(item) - 1]
998 | complete.append(item)
999 |
1000 | complete_len = len(complete)
1001 |
1002 | if nix_path_complete and complete_len:
1003 | self.nix_common_chars(current, complete, case_insensitive)
1004 | complete_len = len(complete)
1005 |
1006 | # If only one entry matches, auto-complete it
1007 | if (nix_path_complete and complete_len == 1) or (not nix_path_complete and complete_len):
1008 | if nix_path_complete:
1009 | cls.last = 0
1010 | else:
1011 | last = cls.last
1012 | if back:
1013 | cls.last = complete_len - 1 if last is None or last < 1 else last - 1
1014 | else:
1015 | cls.last = 0 if last is None or last >= complete_len - 1 else last + 1
1016 | cls.in_progress = True
1017 | FuzzyEditGlobal.bfr = complete[cls.last]
1018 | FuzzyEditGlobal.region = sublime.Region(0, view.size())
1019 | view.run_command("fuzzy_apply_edits")
1020 | FuzzyEditGlobal.clear()
1021 | sels = view.sel()
1022 | sels.clear()
1023 | sels.add(sublime.Region(view.size()))
1024 | else:
1025 | cls.last = None
1026 | cls.text = None
1027 |
1028 | def nix_common_chars(self, current_complete, clist, case_insensitive):
1029 | """Resolve entries using their common start."""
1030 |
1031 | common = current_complete
1032 | while True:
1033 | match = True
1034 | cmn_len = len(current_complete)
1035 | if len(clist[0]) > cmn_len:
1036 | common += clist[0][cmn_len].lower() if case_insensitive else clist[0][cmn_len]
1037 | cmn_len += 1
1038 | else:
1039 | break
1040 | for item in clist:
1041 | value = item.lower() if case_insensitive else item
1042 | if not value.startswith(common):
1043 | match = False
1044 | break
1045 | if not match:
1046 | break
1047 | else:
1048 | current_complete = common
1049 | selection = clist[0][0:len(current_complete)]
1050 | del clist[:]
1051 | clist.append(selection)
1052 |
1053 | @classmethod
1054 | def update_autocomplete(cls, text):
1055 | """Update autocomplete."""
1056 |
1057 | if text != cls.text and cls.last is not None:
1058 | if not cls.in_progress:
1059 | cls.text = None
1060 | cls.last = None
1061 | else:
1062 | cls.in_progress = False
1063 |
1064 | @classmethod
1065 | def reset_autocomplete(cls):
1066 | """Reset autocomplete variables."""
1067 | cls.last = None
1068 | cls.in_progress = False
1069 | cls.text = None
1070 | cls.hl_index = -1
1071 | cls.hl_last = -1
1072 |
1073 |
1074 | class FuzzyFileNavCommand(sublime_plugin.WindowCommand):
1075 | """Navigate through folders."""
1076 |
1077 | active = False
1078 | win_id = None
1079 | view = None
1080 | fuzzy_reload = False
1081 | hide_hidden = False
1082 | cwd = ""
1083 | status = False
1084 |
1085 | @classmethod
1086 | def reset(cls):
1087 | """Reset variables."""
1088 |
1089 | cls.active = False
1090 | cls.win_id = None
1091 | cls.view = None
1092 | cls.status = False
1093 | cls.hide_hidden = not bool(sublime.load_settings(FUZZY_SETTINGS).get("show_system_hidden_files", False))
1094 | # `FuzzyClipboardCommand.clear_entries()`
1095 |
1096 | @classmethod
1097 | def set_hidden(cls, value):
1098 | """Set hiding hidden file option."""
1099 |
1100 | cls.hide_hidden = value
1101 |
1102 | def run(self, start=None):
1103 | """Run command."""
1104 |
1105 | if FuzzyFileNavCommand.active:
1106 | self.window.run_command("hide_overlay")
1107 | self.cls = FuzzyFileNavCommand
1108 | previous = self.cls.cwd
1109 | self.cls.active = True
1110 | self.cls.win_id = self.window.id()
1111 | self.regex_exclude = sublime.load_settings(FUZZY_SETTINGS).get("regex_exclude", [])
1112 | FuzzyPathCompleteCommand.reset_autocomplete()
1113 |
1114 | debug_log("start - {}".format(start if start is not None else "None"))
1115 |
1116 | # Check if a start destination has been given
1117 | # and ensure it is valid.
1118 | directory = get_root_path() if start is None or not path.exists(start) or not path.isdir(start) else start
1119 | self.cls.cwd = directory if PLATFORM == "windows" and directory == "" else path.normpath(directory)
1120 |
1121 | debug_log("cwd - {}".format(self.cls.cwd))
1122 |
1123 | # Get and display options.
1124 | try:
1125 | self.display_files(self.cls.cwd)
1126 | except Exception:
1127 | if self.cls.fuzzy_reload:
1128 | # Reloading, so fuzzy panel must be up, so preserve previous state
1129 | self.cls.fuzzy_reload = False
1130 | self.cls.cwd = previous
1131 | else:
1132 | # Not reloading, so go ahead and reset the state
1133 | self.cls.reset()
1134 | notify("{} is not accessible!".format(self.cls.cwd))
1135 |
1136 | def get_files(self, cwd):
1137 | """Get files, folders, or window's drives."""
1138 |
1139 | # Get files/drives (windows).
1140 | files = get_drives() if PLATFORM == "windows" and cwd == "" else os.listdir(cwd)
1141 | folders = []
1142 | documents = []
1143 | for f in files:
1144 | valid = True
1145 | full_path = path.join(cwd, f)
1146 |
1147 | # Check exclusion to omit files.
1148 | if self.hide_hidden:
1149 | if valid:
1150 | if not PLATFORM == "windows":
1151 | if f.startswith('.') and f != "..":
1152 | valid = False
1153 | else:
1154 | attrs = ctypes.windll.kernel32.GetFileAttributesW(full_path)
1155 | if attrs != -1 and bool(attrs & 2):
1156 | valid = False
1157 |
1158 | if valid:
1159 | for regex in self.regex_exclude:
1160 | if re.match(regex, f):
1161 | valid = False
1162 |
1163 | # Store file/folder info.
1164 | if valid:
1165 | if not path.isdir(full_path):
1166 | documents.append(f)
1167 | else:
1168 | folders.append(f + ("\\" if PLATFORM == "windows" else "/"))
1169 | return [".."] + sorted(folders) + sorted(documents)
1170 |
1171 | def on_highlight(self, value):
1172 | """Get index of highlighted file."""
1173 |
1174 | FuzzyPathCompleteCommand.hl_index = value
1175 |
1176 | def display_files(self, cwd, index=-1):
1177 | """Display files in folder."""
1178 |
1179 | # Get the folders children
1180 | self.cls.status = True
1181 | status_cwd()
1182 | self.cls.files = self.get_files(cwd)
1183 |
1184 | # Make sure panel is down before loading a new one.
1185 | self.cls.view = None
1186 | sublime.set_timeout(
1187 | lambda: self.window.show_quick_panel(
1188 | self.cls.files, self.check_selection, 0, index, on_highlight=self.on_highlight
1189 | ),
1190 | 0
1191 | )
1192 |
1193 | def check_selection(self, selection):
1194 | """Check the users selection and navigate to directory or open file."""
1195 |
1196 | debug_log("Process selection")
1197 | if selection > -1:
1198 | self.cls.fuzzy_reload = False
1199 | # The first selection is the "go up a directory" option.
1200 | directory = back_dir(self.cls.cwd) if selection == 0 else path.join(self.cls.cwd, self.cls.files[selection])
1201 | self.cls.cwd = directory if PLATFORM == "windows" and directory == "" else path.normpath(directory)
1202 |
1203 | # Check if the option is a folder or if we are at the root (needed for windows)
1204 | try:
1205 | if (path.isdir(self.cls.cwd) or self.cls.cwd == get_root_path()):
1206 | # List directories content
1207 | self.display_files(self.cls.cwd)
1208 | else:
1209 | multi = (
1210 | bool(sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_after_action", False)) and
1211 | "open" not in sublime.load_settings(FUZZY_SETTINGS).get("keep_panel_open_exceptions", [])
1212 | )
1213 |
1214 | # Open file
1215 | new_view = self.window.open_file(self.cls.cwd)
1216 | if new_view is not None:
1217 |
1218 | # Horrible ugly hack to ensure opened file gets focus
1219 | def fun(v, multi):
1220 | """Function to focus view."""
1221 |
1222 | v.window().focus_view(v)
1223 | if not multi:
1224 | v.window().show_quick_panel(["None"], None)
1225 | v.window().run_command("hide_overlay")
1226 |
1227 | sublime.set_timeout(lambda: fun(new_view, multi), 500)
1228 |
1229 | # If multi-file open is set, leave panel open after opening file
1230 | if multi:
1231 | self.cls.cwd = path.normpath(back_dir(self.cls.cwd))
1232 | self.display_files(self.cls.cwd, selection)
1233 | else:
1234 | self.cls.reset()
1235 | except Exception:
1236 | # Inaccessible folder try backing up
1237 | notify("{} is not accessible!".format(self.cls.cwd))
1238 | self.cls.cwd = back_dir(self.cls.cwd)
1239 | self.display_files(self.cls.cwd)
1240 | elif not self.cls.fuzzy_reload:
1241 | # Reset if not reloading
1242 | self.cls.reset()
1243 | else:
1244 | # Reset reload flag if reloading
1245 | self.cls.fuzzy_reload = False
1246 |
1247 |
1248 | def init_hidden():
1249 | """Initialize the "show hidden file" setting."""
1250 |
1251 | setting = sublime.load_settings(FUZZY_SETTINGS)
1252 | show_hidden = not bool(setting.get("show_system_hidden_files", False))
1253 | FuzzyFileNavCommand.set_hidden(show_hidden)
1254 | setting.clear_on_change('reload')
1255 | setting.add_on_change('reload', init_hidden)
1256 |
1257 |
1258 | def plugin_loaded():
1259 | """Setup plugin."""
1260 |
1261 | global PLATFORM
1262 | PLATFORM = sublime.platform()
1263 | init_hidden()
1264 |
--------------------------------------------------------------------------------