├── .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 | --------------------------------------------------------------------------------