├── .python-version ├── CTags.sublime-settings ├── Context.sublime-menu ├── Default.sublime-commands ├── Default.sublime-keymap ├── Default.sublime-mousemap ├── LICENSE ├── Main.sublime-menu ├── README.md ├── Side Bar.sublime-menu ├── messages.json ├── messages ├── 0.3.0.md ├── 0.3.1.md ├── 0.3.2.md ├── 0.3.3.md ├── 0.3.4.md ├── 0.3.5.md ├── 0.3.6.md ├── 0.3.7.md ├── 0.3.8.md ├── 0.3.9.md └── 0.4.0.md ├── plugin.py └── plugins ├── __init__.py ├── activity_indicator.py ├── cmds.py ├── ctags.py ├── edit.py ├── ranking ├── __init__.py ├── parse.py └── rank.py └── utils.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /CTags.sublime-settings: -------------------------------------------------------------------------------- 1 | // Place your settings in the file "User/CTags.sublime-settings", which 2 | // overrides the settings in here. 3 | { 4 | // Enable debugging. 5 | // 6 | // When enabled, this will result in debug output being printed to the 7 | // console. This can be useful for debugging issues. 8 | "debug": false, 9 | 10 | // Enable auto-complete. 11 | // 12 | // When enabled, this turns on a basic "auto-complete" feature, similar to 13 | // a very rudimentary "Intellisense(TM)". This is useful for providing 14 | // better suggestions than stock Sublime Text could provide. 15 | "autocomplete": false, 16 | 17 | // Path to ctags executable. 18 | // 19 | // Alter this value if your ctags command is not in the PATH, or if using 20 | // a different version of ctags to that in the path (i.e. for OSX). 21 | // 22 | // NOTE: You *should not* place entire commands here. These commands are 23 | // built automatically using the values below. For example, this is OK: 24 | // 25 | // "command": "/usr/bin/ctags" 26 | // 27 | // This, on the other hand, won't work! 28 | // 29 | // "command": "ctags -R -f .tags --exclude=some/path" 30 | // 31 | "command": "", 32 | 33 | // Enable recursive searching of directories when building tag files. 34 | // 35 | // When enabled, this is equivalent to `-R` parameter. Set to true to 36 | // enable recursive search of directories when generating tag files. 37 | "recursive" : true, 38 | 39 | // Default read/write location of the tags file. 40 | // 41 | // This is equivalent to the `-f [FILENAME]` parameter. There is likely no 42 | // reason to change this unless you have a large number of existing tags 43 | // files you'd like to use that already have a different name. In this 44 | // case perhaps consider using the 'extra_tag_files' setting instead. 45 | "tag_file" : ".tags", 46 | 47 | // Additional tag files names to search. 48 | // 49 | // These are searched in addition to the file name given in 'tag_file' 50 | "extra_tag_files": [".gemtags", "tags"], 51 | 52 | // Additional options to pass to ctags. 53 | // 54 | // Any addition options you may wish to pass to the ctags executable. For 55 | // example: 56 | // 57 | // ["--exclude=some/path", "--exclude=some/other/path", ...] 58 | "opts" : [], 59 | 60 | // Tag "kind"s to ignore. 61 | // 62 | // A ctags tagfile describes a number of different "kind"s, described in 63 | // tag FORMAT file found here: 64 | // 65 | // http://ctags.sourceforge.net/FORMAT 66 | // 67 | // These can be filtered (i.e. ignored). For example - 'import' statements 68 | // should be ignored in Python. These are of kind "i", e.g. 69 | // 70 | // "type":"^i$" 71 | // 72 | "filters": { 73 | "source.python": {"type":"^i$"} 74 | }, 75 | 76 | // Definition "kind"s to ignore. 77 | // 78 | // This is very similar to the 'filters' option. However, this only 79 | // applies to the process that is used to find a definition. All filters 80 | // placed here will be used when the plugin is searching for a definition 81 | // in the file. 82 | "definition_filters": { 83 | "source.php": {"type":"^v$"} 84 | }, 85 | 86 | // Enable the ctags menu in the context menus. 87 | "show_context_menus": true, 88 | 89 | // Paths to additional tag files to include in tag search. 90 | // 91 | // This is a list of items in the following format: 92 | // 93 | // [["language", "platform"], "path"] 94 | // 95 | "extra_tag_paths": [ 96 | [["source.python", "windows"], "C:\\Python27\\Lib\\tags"] 97 | ], 98 | 99 | // Enable highlighting of selected symbol. 100 | // 101 | // When enabled, searched symbols will be highlighted when found. This 102 | // can be irritating in some instances, e.g. when in Vintage mode. In 103 | // these cases, setting this to false will disable this highlighting. 104 | "select_searched_symbol": true, 105 | 106 | // Set to false to not open an error dialog while tags are building 107 | "display_rebuilding_message": true, 108 | 109 | // Rank Manager language syntax regex and character sets 110 | // 111 | // Ex: Python 'and' ignore exp --> '\sand\s' - it must have whitespace 112 | // around it so it is not part of real name: gates.Nand.evaluate() 113 | "language_syntax": { 114 | "splitters" : [".", "::", "->"], 115 | "source.js": { 116 | "member_exp": { 117 | "chars": "[A-Za-z0-9_$]", 118 | "splitters": ["\\."], 119 | "open": ["\\{", "\\[", "\\("], 120 | "close": ["\\}", "\\]" , "\\)"], //close[i] must match open[i] 121 | "ignore": ["&", "\\|", "\\?", ":", "\\!", "'", "=", "\""], 122 | "stop": ["\\s", ","], 123 | "this": ["this", "me", "self", "that"] 124 | }, 125 | "reference_types": { 126 | "__symbol__(\\.call|\\.apply){0,1}\\s*?\\(": ["f", "fa"], 127 | "\\.fire\\s*?\\(\\s*?\\[\\'\"]__symbol__\\[\\'\"\\]": [ 128 | "eventHandler" 129 | ] 130 | } 131 | }, 132 | "source.python": { 133 | //python settings inherit JavaScript, with some overrides 134 | "inherit": "source.js", 135 | "member_exp": { 136 | "ignore": ["\\sand\\s", "\\sor\\s", "\\snot\\s", ":", "\\!", 137 | "'", "=", "\""], 138 | "this" : ["self"] 139 | } 140 | }, 141 | "source.java": { 142 | "inherit": "source.js", 143 | "member_exp": { 144 | "this" : ["this"] 145 | } 146 | }, 147 | "source.cs": { 148 | "inherit": "source.js", 149 | "member_exp": { 150 | "this" : ["this"] 151 | } 152 | } 153 | }, 154 | 155 | // Scope Filters 156 | // 157 | // Tags file may optionally contain tagfield for the scope of the tag. For 158 | // example: 159 | // 160 | // item .\fileHelper.js 420;" vp lineno:420 scope:420:19-422:9 161 | // 162 | // The re is used to extract 'scope:/beginLine:beginCol-endLine:endCol/' 163 | // 164 | // Different tags generators may generate this non-standard field in 165 | // different formats 166 | "scope_re": "(\\d.*?):(\\d.*?)-(\\d.*?):(\\d.*?)" 167 | } 168 | -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "-" 4 | }, 5 | { 6 | "caption": "Navigate to Definition", 7 | "command": "navigate_to_definition", 8 | "args": {} 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "CTags: Rebuild Tags", 4 | "command": "rebuild_tags" 5 | }, 6 | { 7 | "caption": "CTags: Show Symbols (file)", 8 | "command": "show_symbols", 9 | }, 10 | { 11 | "caption": "CTags: Show Symbols (current language)", 12 | "command": "show_symbols", 13 | "args": {"type": "lang"} 14 | }, 15 | { 16 | "caption": "CTags: Show Symbols (all)", 17 | "command": "show_symbols", 18 | "args": {"type": "multi"} 19 | }, 20 | { 21 | "caption": "Preferences: CTags Settings", 22 | "command": "edit_settings", "args": 23 | { 24 | "base_file": "${packages}/CTags/CTags.sublime-settings", 25 | "default": "// CTags Preferences – User\n// ================================================================\n{\n\t$0\n}\n" 26 | } 27 | }, 28 | { 29 | "caption": "Preferences: CTags Key Bindings", 30 | "command": "edit_settings", "args": 31 | { 32 | "base_file": "${packages}/CTags/Default.sublime-keymap", 33 | "user_file": "${packages}/CTags/Default ($platform).sublime-keymap", 34 | "default": "[\n\t$0\n]\n" 35 | } 36 | }, 37 | { 38 | "caption": "Preferences: CTags Mouse Bindings", 39 | "command": "edit_settings", "args": 40 | { 41 | "base_file": "${packages}/CTags/Default.sublime-mousemap", 42 | "user_file": "${packages}/CTags/Default ($platform).sublime-mousemap", 43 | "default": "[\n\t$0\n]\n" 44 | } 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "navigate_to_definition", 4 | "keys": ["ctrl+t", "ctrl+t"] 5 | }, 6 | { 7 | "command": "navigate_to_definition", 8 | "keys": ["ctrl+shift+period"] 9 | }, 10 | { 11 | "command": "search_for_definition", 12 | "keys": ["ctrl+t", "ctrl+y"] 13 | }, 14 | { 15 | "command": "jump_back", 16 | "keys": ["ctrl+t", "ctrl+b"] 17 | }, 18 | { 19 | "command": "jump_back", 20 | "keys": ["ctrl+shift+comma"] 21 | }, 22 | { 23 | "command": "rebuild_tags", 24 | "keys": ["ctrl+t", "ctrl+r"] 25 | }, 26 | { 27 | "command": "show_symbols", 28 | "context": [ 29 | { 30 | "key": "selector", 31 | "match_all": true, 32 | "operand": "source -source.css", 33 | "operator": "equal" 34 | } 35 | ], 36 | "keys": ["alt+s"] 37 | }, 38 | { 39 | "command": "show_symbols", 40 | "args": {"type": "multi"}, 41 | "context": [ 42 | { 43 | "key": "selector", 44 | "match_all": true, 45 | "operand": "source -source.css", 46 | "operator": "equal" 47 | } 48 | ], 49 | "keys": ["alt+shift+s"] 50 | }, 51 | { 52 | "command": "show_symbols", 53 | "args": {"type": "lang"}, 54 | "context": [ 55 | { 56 | "key": "selector", 57 | "match_all": true, 58 | "operand": "source -source.css", 59 | "operator": "equal" 60 | } 61 | ], 62 | "keys": ["ctrl+alt+shift+s"] 63 | }, 64 | { // override current default 65 | "command": "transpose", 66 | "context": [ 67 | { "key": "num_selections", "operator": "not_equal", "operand": 1 } 68 | ], 69 | "keys": ["ctrl+t"] 70 | } 71 | ] 72 | -------------------------------------------------------------------------------- /Default.sublime-mousemap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "button": "button1", 4 | "count": 1, 5 | "press_command": "drag_select", 6 | "modifiers": ["ctrl", "shift"], 7 | "command": "navigate_to_definition" 8 | }, 9 | // { 10 | // "button": "button2", 11 | // "count": 1, 12 | // "modifiers": ["ctrl", "shift"], 13 | // "command": "jump_back" 14 | // } 15 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 SublimeText 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "find", 4 | "children": [ 5 | { 6 | "caption": "-", 7 | "id": "find_tools_ctags" 8 | }, 9 | { 10 | "caption": "CTags", 11 | "id": "ctags", 12 | "children": [ 13 | { 14 | "command": "navigate_to_definition" 15 | }, 16 | { 17 | "command": "jump_prev" 18 | }, 19 | { 20 | "command": "rebuild_tags" 21 | }, 22 | { 23 | "caption": "Show Symbols (file)", 24 | "command": "show_symbols", 25 | }, 26 | { 27 | "caption": "Show Symbols (all)", 28 | "command": "show_symbols", 29 | //"arg_comment": "TODO", 30 | "args": { 31 | "type": "multi" 32 | }, 33 | } 34 | ] 35 | } 36 | ] 37 | }, 38 | { 39 | "id": "preferences", 40 | "children": [ 41 | { 42 | "caption": "Package Settings", 43 | "mnemonic": "P", 44 | "id": "package-settings", 45 | "children": [ 46 | { 47 | "caption": "CTags", 48 | "children": [ 49 | { 50 | "caption": "CTags Settings", 51 | "command": "edit_settings", 52 | "args": { 53 | "base_file": "${packages}/CTags/CTags.sublime-settings", 54 | "default": "// CTags Preferences – User\n// ================================================================\n{\n\t$0\n}\n" 55 | } 56 | }, 57 | { 58 | "caption": "Key Bindings", 59 | "command": "edit_settings", 60 | "args": { 61 | "base_file": "${packages}/CTags/Default.sublime-keymap", 62 | "user_file": "${packages}/CTags/Default ($platform).sublime-keymap", 63 | "default": "[\n\t$0\n]\n" 64 | } 65 | }, 66 | { 67 | "caption": "Mouse Bindings", 68 | "command": "edit_settings", 69 | "args": { 70 | "base_file": "${packages}/CTags/Default.sublime-mousemap", 71 | "user_file": "${packages}/CTags/Default ($platform).sublime-mousemap", 72 | "default": "[\n\t$0\n]\n" 73 | } 74 | } 75 | ] 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTags 2 | 3 | ![CI](https://github.com/SublimeText/CTags/actions/workflows/ci.yaml/badge.svg) 4 | 5 | This [Sublime Text][] package provides support for working with tags generated 6 | by [Exuberant CTags][] or [Universal CTags][]. 7 | 8 | `ctags` command is searched for on the system PATH. It works by doing a binary 9 | search of a memory-mapped tags file, so it will work efficiently with very large 10 | (50MB+) tags files if needed. 11 | 12 | 13 | ## Installation 14 | 15 | ### Package Control 16 | 17 | The easiest way to install is using [Package Control][]. It's listed as `CTags`. 18 | 19 | 1. Open `Command Palette` using menu item `Tools → Command Palette...` 20 | 2. Choose `Package Control: Install Package` 21 | 3. Find `CTags` and hit `Enter` 22 | 23 | ### Manual Download 24 | 25 | 1. [Download the `.zip`][release] 26 | 2. Unzip and rename folder to `CTags` 27 | 3. Copy folder into `Packages` directory, 28 | which can be found using the menu item `Preferences → Browse Packages...` 29 | 30 | ### Using Git 31 | 32 | Go to your Sublime Text Packages directory and clone the repository 33 | using the command below:: 34 | 35 | ```sh 36 | git clone https://github.com/SublimeText/CTags 37 | ``` 38 | 39 | 40 | ## Additional Setup Steps 41 | 42 | ### Linux 43 | 44 | To install ctags use your package manager. 45 | 46 | * For Debian-based systems (Ubuntu, Mint, etc.):: 47 | 48 | ```sh 49 | sudo apt-get install exuberant-ctags 50 | ``` 51 | 52 | or 53 | 54 | ```sh 55 | sudo apt-get install universal-ctags 56 | ``` 57 | 58 | * For Red Hat-based systems (Red Hat, Fedora, CentOS):: 59 | 60 | ```sh 61 | sudo yum install ctags 62 | ``` 63 | 64 | ### MacOS 65 | 66 | The default `ctags` executable in OSX does not support recursive directory 67 | search (i.e. `ctags -R`). To get a proper copy of ctags, use one of the 68 | following options: 69 | 70 | * Using [Homebrew][] 71 | 72 | ```sh 73 | brew install ctags 74 | ``` 75 | 76 | * Using [MacPorts][] 77 | 78 | ```sh 79 | port install ctags 80 | ``` 81 | 82 | Ensure that the `PATH` is updated so the correct version is run: 83 | 84 | * If `which ctags` doesn't point at ctags in `/usr/local/bin`, make sure 85 | you add `/usr/local/bin` to your `PATH` ahead of the folder 86 | `which ctags` reported. 87 | * Alternatively, add the path to the new `ctags` executable to the settings, 88 | under `command`. If you have Xcode / Apple Developer Tools installed this 89 | path will likely be `/usr/local/bin/ctags`. 90 | 91 | ### Windows 92 | 93 | * Download [Exuberant CTags binary][] or [Universal CTags binary][] 94 | 95 | * Extract `ctags.exe` from the downloaded zip to 96 | `C:\Program Files\Sublime Text` or any folder within your PATH so that 97 | Sublime Text can run it. 98 | 99 | * Alternatively, extract to any folder and add the path to this folder to 100 | the `command` setting. 101 | 102 | 103 | ## Usage 104 | 105 | This uses tag files created by the `ctags -R -f .tags` command by default 106 | (although this can be overridden in settings). 107 | 108 | The plugin will try to find a `.tags` file in the same directory as the 109 | current view, walking up directories until it finds one. If it can't find one 110 | it will offer to build one (in the directory of the current view) 111 | 112 | If a symbol can't be found in a tags file, it will search in additional 113 | locations that are specified in the `CTags.sublime-settings` file (see 114 | below). 115 | 116 | If you are a Rubyist, you can build a Ruby Gem's tags with the following 117 | script: 118 | 119 | ```ruby 120 | require 'bundler' 121 | paths = Bundler.load.specs.map(&:full_gem_path) 122 | system("ctags -R -f .gemtags #{paths.join(' ')}") 123 | ``` 124 | 125 | 126 | ## Settings 127 | 128 | To open CTags.sublime-settings 129 | 130 | 1. Open `Command Palette` using menu item `Tools → Command Palette...` 131 | 2. Choose `Preferences: CTags Settings` and hit `Enter` 132 | 133 | --- 134 | 135 | * `filters` will allow you to set scope specific filters against a field of 136 | the tag. In the excerpt above, imports tags like `from a import b` are 137 | filtered: 138 | 139 | ``` 140 | '(?P[^\t]+)\t' 141 | '(?P[^\t]+)\t' 142 | '(?P.*?);"\t' 143 | '(?P[^\t\r\n]+)' 144 | '(?:\t(?P.*))?' 145 | ``` 146 | 147 | * `extra_tag_paths` is a list of extra places to look for keyed by 148 | * `(selector, platform)`. Note the `platform` is tested against 149 | `sublime.platform()` so any values that function returns are valid. 150 | * `extra_tag_files` is a list of extra files relative to the original file 151 | * `command` is the path to the version of ctags to use, for example:: 152 | 153 | ```json 154 | "command" : "/usr/local/bin/ctags" 155 | ``` 156 | 157 | or: 158 | 159 | ```json 160 | "command" : "C:\\Users\\\\Downloads\\CTags\\ctag.exe" 161 | ``` 162 | 163 | The rest of the options are fairly self explanatory. 164 | 165 | ### Hide .tags files from side bar 166 | 167 | By default, Sublime will include ctags files in your project, which causes 168 | them to show up in the file tree and search results. To disable this behaviour 169 | you should add a `file_exclude_patterns` entry to your 170 | `Preferences.sublime-settings` or your project file. For example: 171 | 172 | ```json 173 | "file_exclude_patterns": [".tags", ".tags_sorted_by_file", ".gemtags"] 174 | ``` 175 | 176 | 177 | ## Support 178 | 179 | If there are any problems or you have a suggestion, [open an issue][issues], and 180 | we will receive a notification. 181 | 182 | 183 | ## Commands Listing 184 | 185 | | Command | Key Binding | Alt Binding | Mouse Binding 186 | |--- |--- |--- |--- 187 | | rebuild_ctags | ctrl+t, ctrl+r | | 188 | | navigate_to_definition | ctrl+t, ctrl+t | ctrl+> | ctrl+shift+left_click 189 | | jump_back | ctrl+t, ctrl+b | ctrl+< | ctrl+shift+right_click 190 | | show_symbols | alt+s | | 191 | | show_symbols (all files) | alt+shift+s | | 192 | | show_symbols (suffix) | ctrl+alt+shift+s | | 193 | 194 | 195 | [issues]: https://github.com/SublimeText/CTags/issues 196 | [release]: https://github.com/SublimeText/CTags/releases/latest 197 | 198 | [Sublime Text]: http://sublimetext.com/ 199 | [Package Control]: http://packagecontrol.io/ 200 | 201 | [Exuberant CTags]: http://ctags.sourceforge.net/ 202 | [Exuberant CTags binary]: http://prdownloads.sourceforge.net/ctags/ctags58.zip 203 | 204 | [Universal CTags]: https://github.com/universal-ctags/ctags 205 | [Universal CTags binary]: https://github.com/universal-ctags/ctags-win32/releases/latest 206 | 207 | [Homebrew]: https://brew.sh/ 208 | [MacPorts]: https://www.macports.org/ 209 | -------------------------------------------------------------------------------- /Side Bar.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "CTags: Rebuild Tags", 4 | "command": "rebuild_tags", 5 | "args": { 6 | "dirs": [], 7 | "files": [] 8 | } 9 | }, 10 | { 11 | "caption": "-", 12 | "id": "ctags_commands" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "README.rst", 3 | "0.3.0": "messages/0.3.0.md", 4 | "0.3.1": "messages/0.3.1.md", 5 | "0.3.2": "messages/0.3.2.md", 6 | "0.3.3": "messages/0.3.3.md", 7 | "0.3.4": "messages/0.3.4.md", 8 | "0.3.5": "messages/0.3.5.md", 9 | "0.3.6": "messages/0.3.6.md", 10 | "0.3.7": "messages/0.3.7.md", 11 | "0.3.8": "messages/0.3.8.md", 12 | "0.3.9": "messages/0.3.9.md", 13 | "0.4.0": "messages/0.4.0.md" 14 | } 15 | -------------------------------------------------------------------------------- /messages/0.3.0.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.0 2 | ================ 3 | 4 | - Added support for Sublime Text 3 5 | * Rename ``jump_back`` commmand to ``jump_prev`` to prevent conflict in ST3 6 | * Remove ``jump_back_to_last_modification`` command 7 | * Numerous fixes to ensure Python compatibility 8 | * Add abstraction library for ``sublime.view`` edit functions (``begin_edit`` 9 | and ``end_edit``) for which the API differs between ST2 and ST3 10 | * Add calls to ``sublime.error_message`` when errors occur 11 | * Apply general changes needed by Python3, i.e. print as a function 12 | - Major refactoring of code base 13 | * Logically reorder number of functions 14 | * Add unit tests for number of functions 15 | * Update and move existing unit tests to separate files 16 | * Add documentation to majority of functions 17 | * Add documentation to settings files 18 | * Remove unused functions (dead code) 19 | * Remove large swathes of text and place in separate files 20 | * Move small, one-time functions into inner functions or merge into other 21 | other functions 22 | * General formatting to abide to PEP-8 coding standards 23 | - Updated README and other documentation 24 | * Port to reStructuredText 25 | * Add additional sections for different package managers 26 | * Minor rewrites and spelling corrections 27 | * General formatting 28 | - Additional Changes 29 | * Remove ``ctags`` executable files 30 | 31 | Fixes 32 | ===== 33 | 34 | * Broken "Jump Back To Last Modification" command?, #159 35 | * To support customizable filename of tags file, #157 36 | * Corrected wrong documentation, added correct instructions for OS X 10.8, #151 37 | * Error in readme.md, #150 38 | * CTags can't jump_back in ST3, #148 39 | * Can't build CTags in OSX Mountain Lion, #146 40 | * Why does Navigate to Definition (and Jump Back) select text?, #128 41 | * Jump Back should go back to the line you were on, #127 42 | * Silently fails if ctags isn't installed, #93 43 | 44 | And the big one: 45 | 46 | * Incompatibility with ST3 beta, #115 47 | 48 | Resolves 49 | ======== 50 | 51 | N/A 52 | 53 | ******************************************************************************* 54 | 55 | For more detailed information about these changes, run ``git v0.2.0..v0.3.0`` 56 | on the Git repository found [here](https://github.com/SublimeText/CTags). 57 | -------------------------------------------------------------------------------- /messages/0.3.1.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.1 2 | ================ 3 | 4 | - Add option to target specific folders when building CTags 5 | * Add quick panel menu to allow building of tags for open file (or parent 6 | folder of open file if recursive flag set), all open folders, or one of 7 | the open folders 8 | 9 | Fixes 10 | ===== 11 | 12 | * Fills up entire hard drive (in Linux), #94 13 | 14 | Resolves 15 | ======== 16 | 17 | * Add method to select where tag file should be built, #164 18 | 19 | ******************************************************************************* 20 | 21 | For more detailed information about these changes, run ``git v0.3.0..v0.3.1`` 22 | on the Git repository found [here](https://github.com/SublimeText/CTags). 23 | -------------------------------------------------------------------------------- /messages/0.3.2.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.2 2 | ================ 3 | 4 | - Resolve issues raised by previous release. 5 | 6 | Fixes 7 | ===== 8 | 9 | * Ctags 0.3.0, SublimeText 3 build 3056, Error message - ctags: No files specified. Try "ctags --help"., #167 10 | * Error when rebuilding tags, #168 11 | * multiple errors wheh building or showing tags, #169 12 | * Cursor Bug in Jump Back, #172 13 | * error: ctags: No files specified. Try "ctags --help"., #173 14 | 15 | Resolves 16 | ======== 17 | 18 | * Fix unreference 'err_str' and the subprocess run of ctags, #171 19 | 20 | ******************************************************************************* 21 | 22 | For more detailed information about these changes, run ``git v0.3.1..v0.3.2`` 23 | on the Git repository found [here](https://github.com/SublimeText/CTags). 24 | -------------------------------------------------------------------------------- /messages/0.3.3.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.3 2 | ================ 3 | 4 | - Bug Fixes 5 | 6 | Fixes 7 | ===== 8 | 9 | * Fix navigation (especially for forward declarations), #176 10 | * Pass missing parameter to self.run for SublimeText 2 11 | * ST3 jumps to correct linebut scrolls off the scree, #160 12 | * Fix error awk in window in ctagsplugin.py, #158 13 | * when press ctrl+t,ctrl+t, the cursor jump to the declaration instead of its definition, #122 14 | 15 | Resolves 16 | ======== 17 | 18 | N/A 19 | 20 | ******************************************************************************* 21 | 22 | For more detailed information about these changes, run ``git v0.3.2..v0.3.3`` 23 | on the Git repository found [here](https://github.com/SublimeText/CTags). 24 | -------------------------------------------------------------------------------- /messages/0.3.4.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.4 2 | ================ 3 | 4 | - Bug Fixes 5 | 6 | Fixes 7 | ===== 8 | 9 | * README: Fix package name for Debian et al., #186 10 | * Fix a bug when utilizing additional search paths for tags files., #191 11 | * Fix a bug "WinError 6", #195 12 | * Changes to be committed:, #196 13 | 14 | Resolves 15 | ======== 16 | 17 | N/A 18 | 19 | ******************************************************************************* 20 | 21 | For more detailed information about these changes, run ``git v0.3.3..v0.3.4`` 22 | on the Git repository found [here](https://github.com/SublimeText/CTags). 23 | -------------------------------------------------------------------------------- /messages/0.3.5.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.5 2 | ================ 3 | 4 | - Improve documentation of settings to file 5 | - Bug Fixes 6 | 7 | Fixes 8 | ===== 9 | 10 | * overriding of "command" option fails on Windows 7, #207 11 | * ctags error 2(don't know the reason.), #204 12 | * Error when trying to rebuild ctags (5 args instead of 6), #203 13 | * Additional Opts Error, #2030 14 | * Error building '.tags_sorted_by_file' (UTF-8 issue), #201 15 | * UnicodeDecodeError: 'utf8' codec can't decode byte 0xf4 in position 13: invalid continuation byte, #194 16 | 17 | Resolves 18 | ======== 19 | 20 | * Fixed an inconsistent option name 'additional_options' to 'opts', #207 21 | * ignore UnicodeDecodeError for codecs.open() when resorting tags file., #200 22 | * Improve navigate_to_definition, #197 23 | 24 | ******************************************************************************* 25 | 26 | For more detailed information about these changes, run ``git v0.3.4..v0.3.5`` 27 | on the Git repository found [here](https://github.com/SublimeText/CTags). 28 | -------------------------------------------------------------------------------- /messages/0.3.6.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.6 2 | ================ 3 | 4 | - Update CTags regex to support tabs in ctags 5 | - Bug Fixes 6 | 7 | Fixes 8 | ===== 9 | 10 | * ValueError: dictionary update sequence element #0 has length 1; 2 is required, #209 11 | * overriding of "command" option fails on Windows 7, #205 12 | * is_enabled error when trying to navigate to definition, #183 13 | 14 | Resolves 15 | ======== 16 | 17 | N/A 18 | 19 | ******************************************************************************* 20 | 21 | For more detailed information about these changes, run ``git v0.3.5..v0.3.6`` 22 | on the Git repository found [here](https://github.com/SublimeText/CTags). 23 | -------------------------------------------------------------------------------- /messages/0.3.7.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.7 2 | ================ 3 | 4 | - Resolve regressions caused by multiple previous releases 5 | - General improvements in error handling and other corner cases 6 | - Bug Fixes 7 | 8 | Fixes 9 | ===== 10 | 11 | * Ruby: Exception and ? ignored., #177 12 | * Can't Jump to the definition which defined by #define, #213 13 | 14 | Resolves 15 | ======== 16 | 17 | * Travis-ci Integration, #218 18 | * Tests aren't cross platform, #219 19 | * Better formatted build warnings #220 20 | 21 | ******************************************************************************* 22 | 23 | For more detailed information about these changes, run ``git v0.3.6..v0.3.7`` 24 | on the Git repository found [here](https://github.com/SublimeText/CTags). 25 | -------------------------------------------------------------------------------- /messages/0.3.8.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.8 2 | ================ 3 | 4 | - Add build ctags options to sidebar 5 | - Bug Fixes 6 | 7 | Fixes 8 | ===== 9 | 10 | * Need to keep the tag path order , #227 11 | 12 | Resolves 13 | ======== 14 | 15 | * keep the search order, #239 16 | * Correctly decode error messages on *nix systems., #232 17 | * fix cursor after open_file, #231 18 | 19 | ******************************************************************************* 20 | 21 | For more detailed information about these changes, run ``git v0.3.7..v0.3.8`` 22 | on the Git repository found [here](https://github.com/SublimeText/CTags). 23 | -------------------------------------------------------------------------------- /messages/0.3.9.md: -------------------------------------------------------------------------------- 1 | Changes in 0.3.9 2 | ================ 3 | 4 | - Add build ctags options to sidebar 5 | - Bug Fixes 6 | 7 | Fixes 8 | ===== 9 | 10 | * Add missing 'icon' parameter to 'add_regions', #261 11 | * Fix accidentally using a symbol as a format string, #264 12 | * Fix issues with non-UTF8 charset, #267 13 | 14 | Resolves 15 | ======== 16 | 17 | N/A 18 | 19 | ******************************************************************************* 20 | 21 | For more detailed information about these changes, run ``git v0.3.8..v0.3.9`` 22 | on the Git repository found [here](https://github.com/SublimeText/CTags). 23 | -------------------------------------------------------------------------------- /messages/0.4.0.md: -------------------------------------------------------------------------------- 1 | Changes in 0.4.0 2 | ================ 3 | 4 | - Add ranking manager 5 | - Documentation improvements in settings 6 | - Bug Fixes 7 | 8 | Fixes 9 | ===== 10 | 11 | N/A 12 | 13 | Resolves 14 | ======== 15 | 16 | N/A 17 | 18 | ******************************************************************************* 19 | 20 | For more detailed information about these changes, run ``git v0.3.9..v0.4.0`` 21 | on the Git repository found [here](https://github.com/SublimeText/CTags). 22 | -------------------------------------------------------------------------------- /plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | A ctags plugin for Sublime Text. 3 | """ 4 | import sublime 5 | 6 | if int(sublime.version()) < 3143: 7 | print("CTags requires Sublime Text 3143+") 8 | 9 | else: 10 | import sys 11 | 12 | # Clear module cache to force reloading all modules of this package. 13 | prefix = __package__ + "." # don't clear the base package 14 | for module_name in [ 15 | module_name 16 | for module_name in sys.modules 17 | if module_name.startswith(prefix) and module_name != __name__ 18 | ]: 19 | del sys.modules[module_name] 20 | del prefix 21 | del sys 22 | 23 | # Publish Commands and EventListeners 24 | from .plugins.cmds import ( 25 | CTagsAutoComplete, 26 | NavigateToDefinition, 27 | RebuildTags, 28 | SearchForDefinition, 29 | ShowSymbols, 30 | TestCtags, 31 | ) 32 | 33 | from .plugins.edit import apply_edit 34 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SublimeText/CTags/45e7b8283b28475cf3f7d668b8bd6f8060813a76/plugins/__init__.py -------------------------------------------------------------------------------- /plugins/activity_indicator.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from threading import RLock 3 | 4 | 5 | class ActivityIndicator: 6 | """ 7 | An animated text-based indicator to show that some activity is in progress. 8 | 9 | The `target` argument should be a :class:`sublime.View` or :class:`sublime.Window`. 10 | The indicator will be shown in the status bar of that view or window. 11 | If `label` is provided, then it will be shown next to the animation. 12 | 13 | :class:`ActivityIndicator` can be used as a context manager. 14 | """ 15 | 16 | def __init__(self, label=None): 17 | self.label = label 18 | self.interval = 120 19 | self._lock = RLock() 20 | self._running = False 21 | self._ticks = 0 22 | self._view = None 23 | 24 | def __enter__(self): 25 | self.start() 26 | return self 27 | 28 | def __exit__(self, exc_type, exc_value, traceback): 29 | self.stop() 30 | 31 | def clear(self): 32 | if self._view: 33 | self._view.erase_status("_ctags_activity") 34 | self._view = None 35 | 36 | def start(self): 37 | """ 38 | Start displaying the indicator and animate it. 39 | 40 | :raise RuntimeError: if the indicator is already running. 41 | """ 42 | 43 | with self._lock: 44 | if self._running: 45 | raise RuntimeError("Timer is already running") 46 | self._running = True 47 | self._ticks = 0 48 | self.update(self.render_indicator_text()) 49 | sublime.set_timeout(self.tick, self.interval) 50 | 51 | def stop(self): 52 | """ 53 | Stop displaying the indicator. 54 | 55 | If the indicator is not running, do nothing. 56 | """ 57 | 58 | with self._lock: 59 | if self._running: 60 | self._running = False 61 | self.clear() 62 | 63 | def finish(self, message): 64 | """ 65 | Stop the indicator and display a final status message 66 | 67 | :param message: 68 | The final status message to display 69 | """ 70 | 71 | def clear(): 72 | with self._lock: 73 | self.clear() 74 | 75 | if self._running: 76 | with self._lock: 77 | self._running = False 78 | self.update(message) 79 | 80 | sublime.set_timeout(clear, 4000) 81 | 82 | def tick(self): 83 | """ 84 | Invoke status bar update with specified interval. 85 | """ 86 | 87 | if self._running: 88 | self._ticks += 1 89 | self.update(self.render_indicator_text()) 90 | sublime.set_timeout(self.tick, self.interval) 91 | 92 | def update(self, text): 93 | """ 94 | Update activity indicator and label in status bar. 95 | 96 | :param text: 97 | The text to display in the status bar 98 | """ 99 | 100 | view = sublime.active_window().active_view() 101 | if view and view != self._view: 102 | if self._view: 103 | self._view.erase_status("_ctags_activity") 104 | self._view = view 105 | if self._view: 106 | self._view.set_status("_ctags_activity", text) 107 | 108 | def render_indicator_text(self): 109 | """ 110 | Render activity indicator and label. 111 | 112 | :returns: 113 | The activity indicator string to display in the status bar 114 | """ 115 | 116 | text = "⣷⣯⣟⡿⢿⣻⣽⣾"[self._ticks % 8] 117 | if self.label: 118 | text += " " + self.label 119 | return text 120 | 121 | def set_label(self, label): 122 | with self._lock: 123 | self.label = label 124 | -------------------------------------------------------------------------------- /plugins/cmds.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import locale 3 | import os 4 | import pprint 5 | import re 6 | import string 7 | import subprocess 8 | import threading 9 | 10 | from collections import defaultdict 11 | from itertools import chain 12 | from operator import itemgetter as iget 13 | 14 | import sublime 15 | import sublime_plugin 16 | from sublime import status_message, error_message 17 | 18 | from .activity_indicator import ActivityIndicator 19 | 20 | from .ctags import ( 21 | FILENAME, 22 | PATH_ORDER, 23 | SYMBOL, 24 | build_ctags, 25 | parse_tag_lines, 26 | TagElements, 27 | TagFile, 28 | ) 29 | 30 | from .edit import Edit 31 | from .ranking.parse import Parser 32 | from .ranking.rank import RankMgr 33 | from .utils import * 34 | 35 | # 36 | # Contants 37 | # 38 | 39 | OBJECT_PUNCTUATORS = { 40 | "class": ".", 41 | "struct": "::", 42 | "function": "/", 43 | } 44 | 45 | ENTITY_SCOPE = "entity.name.function, entity.name.type, meta.toc-list" 46 | 47 | RUBY_SPECIAL_ENDINGS = r"\?|!" 48 | 49 | ON_LOAD = sublime_plugin.all_callbacks["on_load"] 50 | 51 | 52 | # 53 | # Functions 54 | # 55 | 56 | 57 | def select(view, region): 58 | sel_set = view.sel() 59 | sel_set.clear() 60 | sel_set.add(region) 61 | sublime.set_timeout(functools.partial(view.show_at_center, region), 1) 62 | 63 | 64 | def in_main(f): 65 | @functools.wraps(f) 66 | def done_in_main(*args, **kw): 67 | sublime.set_timeout(functools.partial(f, *args, **kw), 0) 68 | 69 | return done_in_main 70 | 71 | 72 | # TODO: allow thread per tag file. That makes more sense. 73 | 74 | 75 | def threaded(finish=None, msg="Thread already running"): 76 | def decorator(func): 77 | func.running = 0 78 | 79 | @functools.wraps(func) 80 | def threaded(*args, **kwargs): 81 | def run(): 82 | try: 83 | result = func(*args, **kwargs) 84 | if result is None: 85 | result = () 86 | 87 | elif not isinstance(result, tuple): 88 | result = (result,) 89 | 90 | if finish: 91 | sublime.set_timeout( 92 | functools.partial(finish, args[0], *result), 0 93 | ) 94 | finally: 95 | func.running = 0 96 | 97 | if not func.running: 98 | func.running = 1 99 | t = threading.Thread(target=run) 100 | t.setDaemon(True) 101 | t.start() 102 | else: 103 | status_message(msg) 104 | 105 | threaded.func = func 106 | 107 | return threaded 108 | 109 | return decorator 110 | 111 | 112 | def on_load(path=None, window=None, encoded_row_col=True, begin_edit=False): 113 | """ 114 | Decorator to open or switch to a file. 115 | 116 | Opens and calls the "decorated function" for the file specified by path, 117 | or the current file if no path is specified. In the case of the former, if 118 | the file is open in another tab that tab will gain focus, otherwise the 119 | file will be opened in a new tab with a requisite delay to allow the file 120 | to open. In the latter case, the "decorated function" will be called on 121 | the currently open file. 122 | 123 | :param path: path to a file 124 | :param window: the window to open the file in 125 | :param encoded_row_col: the ``sublime.ENCODED_POSITION`` flag for 126 | ``sublime.Window.open_file`` 127 | :param begin_edit: if editing the file being opened 128 | 129 | :returns: None 130 | """ 131 | window = window or sublime.active_window() 132 | 133 | def wrapper(f): 134 | # if no path, tag is in current open file, return that 135 | if not path: 136 | return f(window.active_view()) 137 | # else, open the relevant file 138 | view = window.open_file(os.path.normpath(path), encoded_row_col) 139 | 140 | def wrapped(): 141 | # if editing the open file 142 | if begin_edit: 143 | with Edit(view): 144 | f(view) 145 | else: 146 | f(view) 147 | 148 | # if buffer is still loading, wait for it to complete then proceed 149 | if view.is_loading(): 150 | 151 | class set_on_load: 152 | callbacks = ON_LOAD 153 | 154 | def __init__(self): 155 | # append self to callbacks 156 | self.callbacks.append(self) 157 | 158 | def remove(self): 159 | # remove self from callbacks, hence disconnecting it 160 | self.callbacks.remove(self) 161 | 162 | def on_load(self, view): 163 | # on file loading 164 | try: 165 | wrapped() 166 | finally: 167 | # disconnect callback 168 | self.remove() 169 | 170 | set_on_load() 171 | # else just proceed (file was likely open already in another tab) 172 | else: 173 | wrapped() 174 | 175 | return wrapper 176 | 177 | 178 | def find_tags_relative_to(path, tag_file): 179 | """ 180 | Find the tagfile relative to a file path. 181 | 182 | :param path: path to a file 183 | :param tag_file: name of tag file 184 | 185 | :returns: path of deepest tag file with name of ``tag_file`` 186 | """ 187 | if not path: 188 | return None 189 | 190 | dirs = os.path.dirname(os.path.normpath(path)).split(os.path.sep) 191 | 192 | while dirs: 193 | joined = os.path.sep.join(dirs + [tag_file]) 194 | 195 | if os.path.exists(joined) and not os.path.isdir(joined): 196 | return joined 197 | else: 198 | dirs.pop() 199 | 200 | return None 201 | 202 | 203 | def read_opts(view): 204 | # the first one is useful to change opts only on a specific project 205 | # (by adding ctags.opts to a project settings file) 206 | if not view: 207 | return setting("opts") 208 | return view.settings().get("ctags.opts") or setting("opts") 209 | 210 | 211 | def get_alternate_tags_paths(view, tags_file): 212 | """ 213 | Search for additional tag files. 214 | 215 | Search for additional tag files to use, including those define by a 216 | ``search_paths`` file, the ``extra_tag_path`` setting and the 217 | ``extra_tag_files`` setting. This is mostly used for including library tag 218 | files. 219 | 220 | :param view: sublime text view 221 | :param tags_file: path to a tag file 222 | 223 | :returns: list of valid, existing paths to additional tag files to search 224 | """ 225 | tags_paths = "%s_search_paths" % tags_file 226 | search_paths = [tags_file] 227 | 228 | # read and add additional tag file paths from file 229 | if os.path.exists(tags_paths): 230 | search_paths.extend(open(tags_paths, encoding="utf-8").read().split("\n")) 231 | 232 | # read and add additional tag file paths from 'extra_tag_paths' setting 233 | try: 234 | for (selector, platform), path in setting("extra_tag_paths"): 235 | if view.match_selector(view.sel()[0].begin(), selector): 236 | if sublime.platform() == platform: 237 | search_paths.append(os.path.join(path, setting("tag_file"))) 238 | except Exception as e: 239 | print(e) 240 | 241 | if os.path.exists(tags_paths): 242 | for extrafile in setting("extra_tag_files"): 243 | search_paths.append( 244 | os.path.normpath(os.path.join(os.path.dirname(tags_file), extrafile)) 245 | ) 246 | 247 | # ok, didn't find the tags file under the viewed file. 248 | # let's look in the currently opened folder 249 | for folder in view.window().folders(): 250 | search_paths.append(os.path.normpath(os.path.join(folder, setting("tag_file")))) 251 | for extrafile in setting("extra_tag_files"): 252 | search_paths.append(os.path.normpath(os.path.join(folder, extrafile))) 253 | 254 | # use list instead of set for keep order 255 | ret = [] 256 | for path in search_paths: 257 | if path and (path not in ret) and os.path.exists(path): 258 | ret.append(path) 259 | return ret 260 | 261 | 262 | def get_common_ancestor_folder(path, folders): 263 | """ 264 | Get common ancestor for a file and a list of folders. 265 | 266 | :param path: path to file 267 | :param folders: list of folder paths 268 | 269 | :returns: path to common ancestor for files and folders file 270 | """ 271 | old_path = "" # must initialise to nothing due to lack of do...while 272 | path = os.path.dirname(path) 273 | 274 | while path != old_path: # prevent continuing past root directory 275 | matches = [path for x in folders if x.startswith(path)] 276 | 277 | if matches: 278 | return max(matches) # in case of multiple matches, return closest 279 | 280 | old_path = path 281 | path = os.path.dirname(path) # go up one level 282 | 283 | return path # return the root directory 284 | 285 | 286 | # Scrolling functions 287 | 288 | 289 | def find_with_scope(view, pattern, scope, start_pos=0, cond=True, flags=0): 290 | max_pos = view.size() 291 | while start_pos < max_pos: 292 | estrs = pattern.split(r"\ufffd") 293 | if len(estrs) > 1: 294 | pattern = estrs[0] 295 | f = view.find(pattern, start_pos, flags) 296 | 297 | if not f or view.match_selector(f.begin(), scope) is cond: 298 | return f 299 | else: 300 | start_pos = f.end() 301 | 302 | return None 303 | 304 | 305 | def find_source(view, pattern, start_at, flags=sublime.LITERAL): 306 | return find_with_scope(view, pattern, "string", start_at, False, flags) 307 | 308 | 309 | def follow_tag_path(view, tag_path, pattern): 310 | regions = [sublime.Region(0, 0)] 311 | 312 | for p in list(tag_path)[1:-1]: 313 | while True: # .end() is BUG! 314 | regions.append(find_source(view, p, regions[-1].begin())) 315 | 316 | if regions[-1] in (None, regions[-2]) or view.match_selector( 317 | regions[-1].begin(), ENTITY_SCOPE 318 | ): 319 | regions = [r for r in regions if r is not None] 320 | break 321 | 322 | start_at = max(regions, key=lambda r: r.begin()).begin() - 1 323 | 324 | # find the ex_command pattern 325 | pattern_region = find_source(view, r"^" + escape_regex(pattern), start_at, flags=0) 326 | 327 | if setting("debug"): # leave a visual trail for easy debugging 328 | regions = regions + ([pattern_region] if pattern_region else []) 329 | view.erase_regions("tag_path") 330 | view.add_regions("tag_path", regions, "comment", "", 1) 331 | 332 | return pattern_region.begin() - 1 if pattern_region else None 333 | 334 | 335 | def scroll_to_tag(view, tag, hook=None): 336 | @on_load(os.path.join(tag.root_dir, tag.filename)) 337 | def and_then(view): 338 | do_find = True 339 | 340 | if tag.ex_command.isdigit(): 341 | look_from = view.text_point(int(tag.ex_command) - 1, 0) 342 | else: 343 | look_from = follow_tag_path(view, tag.tag_path, tag.ex_command) 344 | if not look_from: 345 | do_find = False 346 | 347 | if do_find: 348 | search_symbol = tag.get("def_symbol", tag.symbol) 349 | symbol_region = view.find( 350 | escape_regex(search_symbol) + r"(?:[^_]|$)", look_from, 0 351 | ) 352 | else: 353 | symbol_region = None 354 | 355 | if do_find and symbol_region: 356 | # Using reversed symbol_region so cursor stays in front of the 357 | # symbol. - 1 to discard the additional regex part. 358 | select_region = sublime.Region( 359 | symbol_region.end() - 1, symbol_region.begin() 360 | ) 361 | select(view, select_region) 362 | if not setting("select_searched_symbol"): 363 | view.run_command("exit_visual_mode") 364 | else: 365 | status_message('Can\'t find "%s"' % tag.symbol) 366 | 367 | if hook: 368 | hook(view) 369 | 370 | 371 | # Formatting helper functions 372 | 373 | 374 | def format_tag_for_quickopen(tag, show_path=True): 375 | """ 376 | Format a tag for use in quickopen panel. 377 | 378 | :param tag: tag to display in quickopen 379 | :param show_path: show path to file containing tag in quickopen 380 | 381 | :returns: formatted tag 382 | """ 383 | format_ = [] 384 | tag = TagElements(tag) 385 | f = "" 386 | 387 | for field in getattr(tag, "field_keys", []): 388 | if field in PATH_ORDER: 389 | punct = OBJECT_PUNCTUATORS.get(field, " -> ") 390 | f += string.Template(" %($field)s$punct%(symbol)s").substitute(locals()) 391 | 392 | format_ = [f % tag if f else tag.symbol, tag.ex_command] 393 | format_[1] = format_[1].strip() 394 | 395 | if show_path: 396 | format_.insert(1, tag.filename) 397 | 398 | return format_ 399 | 400 | 401 | def prepare_for_quickpanel(formatter=format_tag_for_quickopen): 402 | """ 403 | Prepare list of matching ctags for the quickpanel. 404 | 405 | :param formatter: formatter function to apply to tag 406 | 407 | :returns: tuple containing tag and formatted string representation of tag 408 | """ 409 | 410 | def compile_lists(sorter): 411 | args, display = [], [] 412 | 413 | for t in sorter(): 414 | display.append(formatter(t)) 415 | args.append(t) 416 | 417 | return args, display 418 | 419 | return compile_lists 420 | 421 | 422 | # File collection helper functions 423 | 424 | 425 | def get_rel_path_to_source(path, tag_file): 426 | """ 427 | Get relative path from tag_file to source file. 428 | 429 | :param path: path to a source file 430 | :param tag_file: path to a tag file 431 | :param multiple: if multiple tag files open 432 | 433 | :returns: list containing relative path from tag_file to source file 434 | """ 435 | tag_dir = os.path.dirname(tag_file) # get tag directory 436 | common_prefix = os.path.commonprefix((tag_dir, path)) 437 | relative_path = os.path.relpath(path, common_prefix) 438 | 439 | return relative_path 440 | 441 | 442 | def get_current_file_suffix(path): 443 | """ 444 | Get file extension 445 | 446 | :param path: path to a source file 447 | 448 | :returns: file extension for file 449 | """ 450 | _, file_suffix = os.path.splitext(path) 451 | 452 | return file_suffix 453 | 454 | 455 | # CTags commands 456 | 457 | 458 | def show_tag_panel(view, result, jump_directly): 459 | """ 460 | Handle tag navigation command. 461 | 462 | Jump directly to a tag entry, or show a quick panel with a list of 463 | matching tags 464 | """ 465 | if result not in (True, False, None): 466 | args, display = result 467 | if not args: 468 | return 469 | 470 | def on_select(i): 471 | if i != -1: 472 | # Work around bug in ST3 where the quick panel keeps focus after 473 | # selecting an entry. 474 | # See https://github.com/SublimeText/Issues/issues/39 475 | view.window().run_command("hide_overlay") 476 | scroll_to_tag(view, args[i]) 477 | 478 | if jump_directly and len(args) == 1: 479 | on_select(0) 480 | else: 481 | view.window().show_quick_panel(display, on_select) 482 | 483 | 484 | def ctags_goto_command(jump_directly=False): 485 | """ 486 | Decorator to goto a ctag entry. 487 | 488 | Allow jump to a ctags entry, directly or otherwise 489 | """ 490 | 491 | def wrapper(func): 492 | def command(self, edit, **args): 493 | view = self.view 494 | tags_file = find_tags_relative_to(view.file_name(), setting("tag_file")) 495 | 496 | if not tags_file: 497 | status_message("Can't find any relevant tags file") 498 | return 499 | 500 | result = func(self, self.view, args, tags_file) 501 | show_tag_panel(self.view, result, jump_directly) 502 | 503 | return command 504 | 505 | return wrapper 506 | 507 | 508 | def check_if_building(self, **args): 509 | """ 510 | Check if ctags are currently being built. 511 | """ 512 | if RebuildTags.build_ctags.func.running: 513 | status_message("Tags not available until built") 514 | return False 515 | return True 516 | 517 | 518 | # Goto definition under cursor commands 519 | 520 | 521 | class JumpToDefinition: 522 | """ 523 | Provider for NavigateToDefinition and SearchForDefinition commands. 524 | """ 525 | 526 | @staticmethod 527 | def run(symbol, region, sym_line, mbrParts, view, tags_file): 528 | # print('JumpToDefinition') 529 | 530 | tags = {} 531 | for tags_file in get_alternate_tags_paths(view, tags_file): 532 | with TagFile(tags_file, SYMBOL) as tagfile: 533 | tags = tagfile.get_tags_dict(symbol, filters=compile_filters(view)) 534 | if tags: 535 | break 536 | 537 | if not tags: 538 | # append to allow jump back to work 539 | view.window().run_command("goto_definition") 540 | return status_message('Can\'t find "%s"' % symbol) 541 | 542 | rankmgr = RankMgr(region, mbrParts, view, symbol, sym_line) 543 | 544 | @prepare_for_quickpanel() 545 | def sorted_tags(): 546 | taglist = tags.get(symbol, []) 547 | p_tags = rankmgr.sort_tags(taglist) 548 | if not p_tags: 549 | status_message('Can\'t find "%s"' % symbol) 550 | return p_tags 551 | 552 | return sorted_tags 553 | 554 | 555 | class NavigateToDefinition(sublime_plugin.TextCommand): 556 | """ 557 | Provider for the ``navigate_to_definition`` command. 558 | 559 | Command navigates to the definition for a symbol in the open file(s) or 560 | folder(s). 561 | """ 562 | 563 | is_enabled = check_if_building 564 | 565 | def __init__(self, args): 566 | sublime_plugin.TextCommand.__init__(self, args) 567 | self.endings = re.compile(RUBY_SPECIAL_ENDINGS) 568 | 569 | def is_visible(self): 570 | return setting("show_context_menus") 571 | 572 | @ctags_goto_command(jump_directly=True) 573 | def run(self, view, args, tags_file): 574 | region = view.sel()[0] 575 | if region.begin() == region.end(): # point 576 | region = view.word(region) 577 | 578 | # handle special line endings for Ruby 579 | language = view.settings().get("syntax") 580 | endings = view.substr(sublime.Region(region.end(), region.end() + 1)) 581 | 582 | if "Ruby" in language and self.endings.match(endings): 583 | region = sublime.Region(region.begin(), region.end() + 1) 584 | symbol = view.substr(region) 585 | 586 | sym_line = view.substr(view.line(region)) 587 | (row, col) = view.rowcol(region.begin()) 588 | line_to_symbol = sym_line[:col] 589 | # print ("line_to_symbol %s" % line_to_symbol) 590 | source = get_source(view) 591 | arrMbrParts = Parser.extract_member_exp(line_to_symbol, source) 592 | return JumpToDefinition.run( 593 | symbol, region, sym_line, arrMbrParts, view, tags_file 594 | ) 595 | 596 | 597 | class SearchForDefinition(sublime_plugin.WindowCommand): 598 | """ 599 | Provider for the ``search_for_definition`` command. 600 | 601 | Command searches for definition for a symbol in the open file(s) or 602 | folder(s). 603 | """ 604 | 605 | is_enabled = check_if_building 606 | 607 | def is_visible(self): 608 | return setting("show_context_menus") 609 | 610 | def run(self): 611 | self.window.show_input_panel( 612 | "", "", self.on_done, self.on_change, self.on_cancel 613 | ) 614 | 615 | def on_done(self, symbol): 616 | view = self.window.active_view() 617 | tags_file = find_tags_relative_to(view.file_name(), setting("tag_file")) 618 | 619 | if not tags_file: 620 | status_message("Can't find any relevant tags file") 621 | return 622 | 623 | result = JumpToDefinition.run(symbol, None, "", [], view, tags_file) 624 | show_tag_panel(view, result, True) 625 | 626 | def on_change(self, text): 627 | pass 628 | 629 | def on_cancel(self): 630 | pass 631 | 632 | 633 | # Show Symbol commands 634 | 635 | tags_cache = defaultdict(dict) 636 | 637 | 638 | class ShowSymbols(sublime_plugin.TextCommand): 639 | """ 640 | Provider for the ``show_symbols`` command. 641 | 642 | Command shows all symbols for the open file(s) or folder(s). 643 | """ 644 | 645 | is_enabled = check_if_building 646 | 647 | def is_visible(self): 648 | return setting("show_context_menus") 649 | 650 | @ctags_goto_command() 651 | def run(self, view, args, tags_file): 652 | if not tags_file: 653 | return 654 | 655 | symbol_type = args.get("type") 656 | multi = symbol_type == "multi" 657 | lang = symbol_type == "lang" 658 | 659 | if lang: 660 | # filter and cache by file suffix 661 | suffix = get_current_file_suffix(view.file_name()) 662 | key = suffix 663 | files = [] 664 | elif multi: 665 | # request all symbols of given tags file 666 | key = "__all__" 667 | files = [] 668 | else: 669 | # request symbols of current view's file 670 | key = view.file_name() 671 | if not key: 672 | return 673 | key = get_rel_path_to_source(key, tags_file) 674 | key = key.replace("\\", "/") 675 | files = [key] 676 | 677 | tags_file = tags_file + "_sorted_by_file" 678 | base_path = get_common_ancestor_folder( 679 | view.file_name(), view.window().folders() 680 | ) 681 | 682 | def get_tags(): 683 | with TagFile(tags_file, FILENAME) as tagfile: 684 | if lang: 685 | return tagfile.get_tags_dict_by_suffix( 686 | suffix, filters=compile_filters(view) 687 | ) 688 | elif multi: 689 | return tagfile.get_tags_dict(filters=compile_filters(view)) 690 | else: 691 | return tagfile.get_tags_dict(*files, filters=compile_filters(view)) 692 | 693 | if key in tags_cache[base_path]: 694 | print("loading symbols from cache") 695 | tags = tags_cache[base_path][key] 696 | else: 697 | print("loading symbols from file") 698 | tags = get_tags() 699 | tags_cache[base_path][key] = tags 700 | 701 | print(("loaded [%d] symbols" % len(tags))) 702 | 703 | if not tags: 704 | if multi: 705 | sublime.status_message( 706 | "No symbols found **FOR CURRENT FOLDERS**; Try Rebuild?" 707 | ) 708 | else: 709 | sublime.status_message( 710 | "No symbols found **FOR CURRENT FILE**; Try Rebuild?" 711 | ) 712 | 713 | path_cols = (0,) if len(files) > 1 or multi else () 714 | formatting = functools.partial( 715 | format_tag_for_quickopen, show_path=bool(path_cols) 716 | ) 717 | 718 | @prepare_for_quickpanel(formatting) 719 | def sorted_tags(): 720 | return sorted(chain(*(tags[k] for k in tags)), key=iget("tag_path")) 721 | 722 | return sorted_tags 723 | 724 | 725 | # Rebuild CTags commands 726 | 727 | 728 | class RebuildTags(sublime_plugin.WindowCommand): 729 | """ 730 | Provider for the ``rebuild_tags`` command. 731 | 732 | Command (re)builds tag files for the open file(s) or folder(s), reading 733 | relevant settings from the settings file. 734 | """ 735 | 736 | def run(self, dirs=None, files=None): 737 | """Handler for ``rebuild_tags`` command""" 738 | view = self.window.active_view() 739 | 740 | paths = [] 741 | if dirs: 742 | paths += dirs 743 | if files: 744 | paths += files 745 | 746 | if paths: 747 | self.build_ctags( 748 | paths, 749 | command=setting("command"), 750 | tag_file=setting("tag_file"), 751 | recursive=setting("recursive"), 752 | opts=read_opts(view), 753 | ) 754 | 755 | elif ( 756 | view is None or view.file_name() is None and len(self.window.folders()) <= 0 757 | ): 758 | status_message("Cannot build CTags: No file or folder open.") 759 | 760 | else: 761 | self.show_build_panel(view) 762 | 763 | def show_build_panel(self, view): 764 | """ 765 | Handle build ctags command. 766 | 767 | Allows user to select whether tags should be built for the current file, 768 | a given directory or all open directories. 769 | """ 770 | display = [] 771 | 772 | if view.file_name() is not None: 773 | if not setting("recursive"): 774 | display.append(["Open File", view.file_name()]) 775 | else: 776 | display.append( 777 | ["Open File's Directory", os.path.dirname(view.file_name())] 778 | ) 779 | 780 | if len(view.window().folders()) > 0: 781 | # append option to build for all open folders 782 | display.append( 783 | [ 784 | "All Open Folders", 785 | "; ".join( 786 | [ 787 | "'{0}'".format(os.path.split(x)[1]) 788 | for x in view.window().folders() 789 | ] 790 | ), 791 | ] 792 | ) 793 | # Append options to build for each open folder 794 | display.extend([[os.path.split(x)[1], x] for x in view.window().folders()]) 795 | 796 | def on_select(i): 797 | if i != -1: 798 | if display[i][0] == "All Open Folders": 799 | paths = view.window().folders() 800 | else: 801 | paths = display[i][1:] 802 | 803 | command = setting("command") 804 | recursive = setting("recursive") 805 | tag_file = setting("tag_file") 806 | opts = read_opts(view) 807 | 808 | self.build_ctags(paths, command, tag_file, recursive, opts) 809 | 810 | view.window().show_quick_panel(display, on_select) 811 | 812 | @threaded(msg="Already running CTags!") 813 | def build_ctags(self, paths, command, tag_file, recursive, opts): 814 | """ 815 | Build tags for the open file or folder(s). 816 | 817 | :param paths: paths to build ctags for 818 | :param command: ctags command 819 | :param tag_file: filename to use for the tag file. Defaults to ``tags`` 820 | :param recursive: specify if search should be recursive in directory 821 | given by path. This overrides filename specified by ``path`` 822 | :param opts: list of additional parameters to pass to the ``ctags`` 823 | executable 824 | 825 | :returns: None 826 | """ 827 | with ActivityIndicator("CTags: Rebuilding tags...") as progress: 828 | for i, path in enumerate(paths, start=1): 829 | if len(paths) > 1: 830 | progress.update( 831 | "CTags: Rebuilding tags [%d/%d]..." % (i, len(paths)) 832 | ) 833 | 834 | try: 835 | result = build_ctags( 836 | path=path, 837 | tag_file=tag_file, 838 | recursive=recursive, 839 | opts=opts, 840 | cmd=command, 841 | ) 842 | except IOError as e: 843 | error_message(e.strerror) 844 | return 845 | except subprocess.CalledProcessError as e: 846 | if sublime.platform() == "windows": 847 | str_err = " ".join(e.output.decode("windows-1252").splitlines()) 848 | else: 849 | str_err = e.output.decode( 850 | locale.getpreferredencoding() 851 | ).rstrip() 852 | 853 | error_message(str_err) 854 | return 855 | except Exception as e: 856 | error_message( 857 | "An unknown error occured.\nCheck the console for info." 858 | ) 859 | raise e 860 | 861 | in_main(lambda: tags_cache[os.path.dirname(result)].clear())() 862 | 863 | progress.finish("Finished building tags!") 864 | 865 | if tag_file in ctags_completions: 866 | del ctags_completions[tag_file] # clear the cached ctags list 867 | 868 | 869 | # Autocomplete commands 870 | 871 | 872 | ctags_completions = {} 873 | 874 | 875 | class CTagsAutoComplete(sublime_plugin.EventListener): 876 | def on_query_completions(self, view, prefix, locations): 877 | if not setting("autocomplete"): 878 | return None 879 | 880 | prefix = prefix.lower() 881 | 882 | tags_path = find_tags_relative_to(view.file_name(), setting("tag_file")) 883 | 884 | if not tags_path: 885 | return None 886 | 887 | if not os.path.exists(tags_path): 888 | return None 889 | 890 | if os.path.getsize(tags_path) > 100 * 1024 * 1024: 891 | return None 892 | 893 | if tags_path not in ctags_completions: 894 | tags = set() 895 | 896 | with open(tags_path, "r", encoding="utf-8") as fobj: 897 | for line in fobj: 898 | line = line.strip() 899 | if not line or line.startswith("!_TAG"): 900 | continue 901 | cols = line.split("\t", 1) 902 | tags.add(cols[0]) 903 | 904 | ctags_completions[tags_path] = tags 905 | 906 | return [ 907 | tag 908 | for tag in ctags_completions[tags_path] 909 | if tag.lower().startswith(prefix) 910 | ] 911 | 912 | 913 | # Test CTags commands 914 | 915 | 916 | class TestCtags(sublime_plugin.TextCommand): 917 | routine = None 918 | 919 | def run(self, edit, **args): 920 | if self.routine is None: 921 | self.routine = self.co_routine(self.view) 922 | next(self.routine) 923 | 924 | def __next__(self): 925 | try: 926 | next(self.routine) 927 | except Exception as e: 928 | print(e) 929 | self.routine = None 930 | 931 | def co_routine(self, view): 932 | tag_file = find_tags_relative_to(view.file_name(), setting("tag_file")) 933 | 934 | with open(tag_file, encoding="utf-8") as tf: 935 | tags = parse_tag_lines(tf, tag_class=TagElements) 936 | 937 | print("Starting Test") 938 | 939 | ex_failures = [] 940 | line_failures = [] 941 | 942 | for symbol, tag_list in list(tags.items()): 943 | for tag in tag_list: 944 | tag.root_dir = os.path.dirname(tag_file) 945 | 946 | def hook(av): 947 | test_context = av.sel()[0] 948 | 949 | if tag.ex_command.isdigit(): 950 | test_string = tag.symbol 951 | else: 952 | test_string = tag.ex_command 953 | test_context = av.line(test_context) 954 | 955 | if not av.substr(test_context).startswith(test_string): 956 | failure = "FAILURE %s" % pprint.pformat(tag) 957 | failure += av.file_name() 958 | 959 | if setting("debug"): 960 | if not sublime.ok_cancel_dialog("%s\n\n\n" % failure): 961 | self.routine = None 962 | 963 | return sublime.set_clipboard(failure) 964 | ex_failures.append(tag) 965 | sublime.set_timeout(self.__next__, 5) 966 | 967 | scroll_to_tag(view, tag, hook) 968 | yield 969 | 970 | failures = line_failures + ex_failures 971 | tags_tested = sum(len(v) for v in list(tags.values())) - len(failures) 972 | 973 | view = sublime.active_window().new_file() 974 | 975 | with Edit(view) as edit: 976 | edit.insert(view.size(), "%s Tags Tested OK\n" % tags_tested) 977 | edit.insert(view.size(), "%s Tags Failed" % len(failures)) 978 | 979 | view.set_scratch(True) 980 | view.set_name("CTags Test Results") 981 | 982 | if failures: 983 | sublime.set_clipboard(pprint.pformat(failures)) 984 | -------------------------------------------------------------------------------- /plugins/ctags.py: -------------------------------------------------------------------------------- 1 | """ 2 | A ctags wrapper, parser and sorter. 3 | """ 4 | 5 | import bisect 6 | import mmap 7 | import os 8 | import re 9 | import subprocess 10 | 11 | from subprocess import check_output 12 | 13 | # 14 | # Contants 15 | # 16 | 17 | TAGS_RE = re.compile( 18 | r"(?P[^\t]+)\t" 19 | r"(?P[^\t]+)\t" 20 | r'(?P(/.+/|\?.+\?|\d+));"\t' 21 | r"(?P[^\t\r\n]+)" 22 | r"(?:\t(?P.*))?" 23 | ) 24 | 25 | # column indexes 26 | SYMBOL = 0 27 | FILENAME = 1 28 | 29 | MATCHES_STARTWITH = "starts_with" 30 | 31 | PATH_ORDER = [ 32 | "function", 33 | "class", 34 | "struct", 35 | ] 36 | 37 | PATH_IGNORE_FIELDS = ("file", "access", "signature", "language", "line", "inherits") 38 | 39 | TAG_PATH_SPLITTERS = ("/", ".", "::", ":") 40 | 41 | # 42 | # Functions 43 | # 44 | 45 | # Helper functions 46 | 47 | 48 | def splits(string, *splitters): 49 | """ 50 | Split a string on a number of splitters. 51 | 52 | :param string: string to split 53 | :param splitters: characters to split string on 54 | 55 | :returns: ``string`` split on characters in ``string``""" 56 | if splitters: 57 | split = string.split(splitters[0]) 58 | for val in split: 59 | for char in splits(val, *splitters[1:]): 60 | yield char 61 | else: 62 | if string: 63 | yield string 64 | 65 | 66 | # Tag processing functions 67 | 68 | 69 | def parse_tag_lines(lines, order_by="symbol", tag_class=None, filters=None): 70 | """ 71 | Parse and sort a list of tags. 72 | 73 | Parse and sort a list of tags one by using a combination of regexen and 74 | Python functions. The end result is a dictionary containing all 'tags' or 75 | entries found in the list of tags, sorted and filtered in a manner 76 | specified by the user. 77 | 78 | :param lines: list of tag lines from a tagfile 79 | :param order_by: element by which the result should be sorted 80 | :param tag_class: a Class to wrap around the resulting dictionary 81 | :param filters: filters to apply to resulting dictionary 82 | 83 | :returns: tag object or dictionary containing a sorted, filtered version 84 | of the original input tag lines 85 | """ 86 | tags_lookup = {} 87 | 88 | for line in lines: 89 | skip = False 90 | 91 | if isinstance(line, Tag): # handle both text and tag objects 92 | line = line.line 93 | 94 | line = line.rstrip("\r\n") 95 | 96 | search_obj = TAGS_RE.search(line) 97 | 98 | if not search_obj: 99 | continue 100 | 101 | tag = search_obj.groupdict() # convert regex search result to dict 102 | 103 | tag = post_process_tag(tag) 104 | 105 | if tag_class is not None: # if 'casting' to a class 106 | tag = tag_class(tag) 107 | 108 | if filters: 109 | # apply filters, filtering out any matching entries 110 | for filt in filters: 111 | for key, val in list(filt.items()): 112 | if re.match(val, tag[key]): 113 | skip = True 114 | 115 | if skip: # if a filter was matched, ignore line (filter out) 116 | continue 117 | 118 | tags_lookup.setdefault(tag[order_by], []).append(tag) 119 | 120 | return tags_lookup 121 | 122 | 123 | def post_process_tag(tag): 124 | """ 125 | Process 'EX Command'-related elements of a tag. 126 | 127 | Process all 'EX Command'-related elements. The 'Ex Command' element has 128 | previously been split into the 'fields', 'type' and 'ex_command' elements. 129 | Break these down further as seen below:: 130 | 131 | =========== = ============= ========================================= 132 | original > new meaning/example 133 | =========== = ============= ========================================= 134 | symbol > symbol symbol name (i.e. class, variable) 135 | filename > filename file containing symbol 136 | . > tag_path tuple of (filename, [class], symbol) 137 | ex_command > ex_command line number or regex used to find symbol 138 | type > type type of symbol (i.e. class, method) 139 | fields > fields string of fields 140 | . > [field_keys] list of parsed field keys 141 | . > [field_one] parsed field element one 142 | . > [...] additional parsed field element 143 | =========== = ============= ========================================= 144 | 145 | Example:: 146 | 147 | =========== = ============= ========================================= 148 | original > new example 149 | =========== = ============= ========================================= 150 | symbol > symbol 'getSum' 151 | filename > filename 'DemoClass.java' 152 | . > tag_path ('DemoClass.java', 'DemoClass', 'getSum') 153 | ex_command > ex_command '\tprivate int getSum(int a, int b) {' 154 | type > type 'm' 155 | fields > fields 'class:DemoClass\tfile:' 156 | . > field_keys ['class', 'file'] 157 | . > class 'DemoClass' 158 | . > file '' 159 | =========== = ============= ========================================= 160 | 161 | :param tag: dict containing the unprocessed tag 162 | 163 | :returns: dict containing the processed tag 164 | """ 165 | tag.update(process_fields(tag)) 166 | 167 | tag["ex_command"] = process_ex_cmd(tag) 168 | 169 | tag.update(create_tag_path(tag)) 170 | 171 | return tag 172 | 173 | 174 | def process_ex_cmd(tag): 175 | """ 176 | Process the 'ex_command' element of a tag dictionary. 177 | 178 | Process the ex_command string - a line number or regex used to find symbol 179 | declaration - by unescaping the regex where used. 180 | 181 | :param tag: dict containing a tag 182 | 183 | :returns: updated 'ex_command' dictionary entry 184 | """ 185 | ex_cmd = tag.get("ex_command") 186 | 187 | if ex_cmd.isdigit(): # if a line number, do nothing 188 | return ex_cmd 189 | else: # else a regex, so unescape 190 | return re.sub(r"\\(\$|/|\^|\\)", r"\1", ex_cmd[2:-2]) # unescape regex 191 | 192 | 193 | def process_fields(tag): 194 | """ 195 | Process the 'field' element of a tag dictionary. 196 | 197 | Process the fields string - a comma-separated string of "key-value" pairs 198 | - by generating key-value pairs and appending them to the tag dictionary. 199 | Also append a list of keys for said pairs. 200 | 201 | :param tag: dict containing a tag 202 | 203 | :returns: dict containing the key-value pairs from the field element, plus 204 | a list of keys for said pairs 205 | """ 206 | fields = tag.get("fields") 207 | 208 | if not fields: # do nothing 209 | return {} 210 | 211 | # split the fields string into a dictionary of key-value pairs 212 | result = dict(f.split(":", 1) for f in fields.split("\t")) 213 | 214 | # append all keys to the dictionary 215 | result["field_keys"] = sorted(result.keys()) 216 | 217 | return result 218 | 219 | 220 | def create_tag_path(tag): 221 | """ 222 | Create a tag path entry for a tag dictionary. 223 | 224 | Creates a tag path entry for a tag dictionary from the field key-value 225 | pairs. Uses format:: 226 | 227 | [function] [class] [struct] [additional entries] symbol 228 | 229 | Where ``additional entries`` is any field key-value pair not found in 230 | ``PATH_IGNORE_FIELDS`` 231 | 232 | :param tag: dict containing a tag 233 | 234 | :returns: dict containing the 'tag_path' entry 235 | """ 236 | field_keys = tag.get("field_keys", [])[:] 237 | fields = [] 238 | tag_path = "" 239 | 240 | # sort field arguments related to path order in correct order 241 | for field in PATH_ORDER: 242 | if field in field_keys: 243 | fields.append(field) 244 | field_keys.pop(field_keys.index(field)) 245 | 246 | # append all remaining field arguments 247 | fields.extend(field_keys) 248 | 249 | # convert list of fields to dot-joined string, dropping any "ignore" fields 250 | for field in fields: 251 | if field not in PATH_IGNORE_FIELDS: 252 | tag_path += tag.get(field) + "." 253 | 254 | # append symbol as last item in string 255 | tag_path += tag.get("symbol") 256 | 257 | # split string on seperators and append tag filename to resulting list 258 | splitup = [tag.get("filename")] + list(splits(tag_path, *TAG_PATH_SPLITTERS)) 259 | 260 | # convert list to tuple 261 | result = {"tag_path": tuple(splitup)} 262 | 263 | return result 264 | 265 | 266 | # Tag building/sorting functions 267 | 268 | 269 | def build_ctags(path, cmd=None, tag_file=None, recursive=False, opts=None): 270 | """ 271 | Execute the ``ctags`` command using ``Popen``. 272 | 273 | :param path: path to file or directory (with all files) to generate 274 | ctags for. 275 | :param recursive: specify if search should be recursive in directory 276 | given by path. This overrides filename specified by ``path`` 277 | :param tag_file: filename to use for the tag file. Defaults to ``tags`` 278 | :param opts: list of additional options to pass to the ctags executable 279 | 280 | :returns: original ``tag_file`` filename 281 | """ 282 | # build the CTags command 283 | if cmd: 284 | cmd = [cmd] 285 | else: 286 | cmd = ["ctags"] 287 | 288 | if not os.path.exists(path): 289 | raise IOError( 290 | "'path' is not at valid directory or file path, or " "is not accessible" 291 | ) 292 | 293 | if os.path.isfile(path): 294 | cwd = os.path.dirname(path) 295 | else: 296 | cwd = path 297 | 298 | if tag_file: 299 | cmd.append("-f {0}".format(tag_file)) 300 | 301 | if opts: 302 | if type(opts) == list: 303 | cmd.extend(opts) 304 | else: # *should* be a list, but better safe than sorry 305 | cmd.append(opts) 306 | 307 | if recursive: # ignore any file specified in path if recursive set 308 | cmd.append("-R") 309 | elif os.path.isfile(path): 310 | filename = os.path.basename(path) 311 | cmd.append(filename) 312 | else: # search all files in current directory 313 | cmd.append(os.path.join(path, "*")) 314 | 315 | # workaround for the issue described here: 316 | # http://bugs.python.org/issue6689 317 | if os.name == "posix": 318 | cmd = " ".join(cmd) 319 | 320 | # execute the command 321 | check_output( 322 | cmd, cwd=cwd, shell=True, stdin=subprocess.PIPE, stderr=subprocess.STDOUT 323 | ) 324 | 325 | if not tag_file: # Exuberant ctags defaults to ``tags`` filename. 326 | tag_file = os.path.join(cwd, "tags") 327 | else: 328 | if os.path.dirname(tag_file) != cwd: 329 | tag_file = os.path.join(cwd, tag_file) 330 | 331 | # re-sort ctag file in filename order to improve search performance 332 | resort_ctags(tag_file) 333 | 334 | return tag_file 335 | 336 | 337 | def resort_ctags(tag_file): 338 | """ 339 | Rearrange ctags file for speed. 340 | 341 | Resorts (re-sort) a CTag file in order of file. This improves searching 342 | performance when searching tags by file as a binary search can be used. 343 | 344 | The algorithm works as so: 345 | 346 | For each line in the tag file 347 | Read the file name (``file_name``) the tag belongs to 348 | If not exists, create an empty array and store in the 349 | dictionary with the file name as key 350 | Save the line to this list 351 | Create a new ``tagfile`` file 352 | For each key in the sorted dictionary 353 | For each line in the list indicated by the key 354 | Split the line on tab character 355 | Remove the prepending ``.`` from the ``file_name`` part of 356 | the tag 357 | Join the line again and write the ``sorted_by_file`` file 358 | 359 | :param tag_file: The location of the tagfile to be sorted 360 | 361 | :returns: None 362 | """ 363 | groups = {} 364 | 365 | with open(tag_file, encoding="utf-8", errors="replace") as file_: 366 | for line in file_: 367 | # meta data not needed in sorted files 368 | if line.startswith("!_TAG"): 369 | continue 370 | 371 | # read all valid symbol tags, which contain at least 372 | # symbol name and containing file and build a list of tuples 373 | split = line.split("\t", FILENAME + 1) 374 | if len(split) > FILENAME: 375 | groups.setdefault(split[FILENAME], []).append(line) 376 | 377 | with open( 378 | tag_file + "_sorted_by_file", "w", encoding="utf-8", errors="replace" 379 | ) as file_: 380 | for group in sorted(groups): 381 | file_.writelines(groups[group]) 382 | 383 | 384 | # 385 | # Models 386 | # 387 | 388 | 389 | class TagElements(dict): 390 | """ 391 | Model the entries of a tag file. 392 | """ 393 | 394 | def __init__(self, *args, **kw): 395 | """Initialise Tag object""" 396 | dict.__init__(self, *args, **kw) 397 | self.__dict__ = self 398 | 399 | 400 | class Tag(object): 401 | """ 402 | Model a tag. 403 | 404 | This exists mainly to enable different types of sorting. 405 | """ 406 | 407 | def __init__(self, line, column=0): 408 | if isinstance(line, bytes): # python 3 compatibility 409 | line = line.decode("utf-8", "replace") 410 | self.line = line 411 | self.column = column 412 | 413 | def __lt__(self, other): 414 | try: 415 | return self.key < other 416 | except IndexError: 417 | return False 418 | 419 | def __gt__(self, other): 420 | try: 421 | return self.key > other 422 | except IndexError: 423 | return False 424 | 425 | def __getitem__(self, index): 426 | return self.line.split("\t", self.column + 1)[index] 427 | 428 | def __len__(self): 429 | return self.line.count("\t") + 1 430 | 431 | @property 432 | def key(self): 433 | return self[self.column] 434 | 435 | 436 | class TagFile(object): 437 | """ 438 | Model a tag file. 439 | 440 | This doesn't actually hold a entire tag file, due in part to the sheer 441 | size of some tag files (> 100 MB files are possible). Instead, it acts 442 | as a 'wrapper' of sorts around a file, providing functionality like 443 | searching for a retrieving tags, finding tags based on given criteria 444 | (prefix, suffix, exact), getting the directory of a tag and so forth. 445 | """ 446 | 447 | def __init__(self, path, column): 448 | """ 449 | Initialise object. 450 | 451 | The file indicated by ``path`` must be sorted by values in the column 452 | indicated by ``column``. 453 | 454 | :param path: path to a tag file 455 | :param column: column to search on 456 | 457 | :returns: None 458 | """ 459 | self.path = path 460 | self.column = column 461 | self.file = None 462 | self.mmap = None 463 | 464 | def __getitem__(self, index): 465 | """ 466 | Provide sequence-type interface to tag file. 467 | """ 468 | if not self.mmap: 469 | raise RuntimeError("No tag file open.") 470 | 471 | self.mmap.seek(index) 472 | result = self.mmap.readline() 473 | 474 | if index != 0: # handle first line 475 | result = self.mmap.readline() # get a complete line 476 | 477 | result = result.strip() 478 | if not result: 479 | raise IndexError("Invalid tag at index %d." % index) 480 | 481 | return Tag(result, self.column) 482 | 483 | def __len__(self): 484 | """ 485 | Get size of tag file in bytes. 486 | """ 487 | if not self.mmap: 488 | raise RuntimeError("No tag file open.") 489 | 490 | return len(self.mmap) 491 | 492 | def __enter__(self): 493 | """ 494 | Open file on enter when using ``with`` keyword. 495 | """ 496 | self.open() 497 | return self 498 | 499 | def __exit__(self, type_, value, traceback): 500 | """ 501 | Close file on exit when using ``with`` keyword. 502 | """ 503 | self.close() 504 | 505 | @property 506 | def dir(self): 507 | """ 508 | Get directory of tag file. 509 | """ 510 | return os.path.dirname(self.path) 511 | 512 | def open(self): 513 | """ 514 | Open file. 515 | """ 516 | self.file = open(self.path, "r", encoding="utf-8") 517 | self.mmap = mmap.mmap(self.file.fileno(), 0, access=mmap.ACCESS_READ) 518 | 519 | def close(self): 520 | """ 521 | Close file. 522 | """ 523 | if not self.mmap or not self.file: 524 | raise RuntimeError("No tag file open.") 525 | 526 | self.mmap.close() 527 | self.mmap = None 528 | self.file.close() 529 | self.file = None 530 | 531 | def search(self, exact_match=True, *tags): 532 | """ 533 | Search for one or more tags in the tag file. 534 | 535 | Search a tag file for given tags using a binary search. 536 | 537 | :param exact_match: if search should be an exact or partial match 538 | 539 | :returns: matching tags 540 | """ 541 | if not self.mmap: 542 | raise RuntimeError("No tag file open.") 543 | 544 | if not tags: 545 | while self.mmap.tell() < self.mmap.size(): 546 | result = Tag(self.mmap.readline().strip(), self.column) 547 | if result.line: 548 | yield result 549 | return 550 | 551 | for key in tags: 552 | left_index = bisect.bisect_left(self, key) 553 | if exact_match: 554 | result = self[left_index] 555 | while result.line and result[result.column] == key: 556 | yield result 557 | result = Tag(self.mmap.readline().strip(), self.column) 558 | else: 559 | result = self[left_index] 560 | while result.line and result[result.column].startswith(key): 561 | yield result 562 | result = Tag(self.mmap.readline().strip(), self.column) 563 | 564 | def search_by_suffix(self, suffix): 565 | """ 566 | Search for one or more tags with the given suffix in the tag file. 567 | 568 | Search a tag file for given tags with the given suffix, using a linear 569 | search. Note that this linear search requires the entire file be 570 | searched making it slow. Hence, it should be avoided if possible. 571 | 572 | :param suffix: suffix to search for 573 | 574 | :returns: matching tags 575 | """ 576 | if not self.file: 577 | raise RuntimeError("No tag file open.") 578 | 579 | for line in self.file: 580 | tag = Tag(line, self.column) 581 | if tag.key.endswith(suffix): 582 | yield tag 583 | 584 | def tag_class(self): 585 | """ 586 | Default class to wrap tag in. 587 | 588 | Allows wrapping of a parsed tag dict in a class, so elements can be 589 | accessed as class variables (i.e. ``class.variable``, rather than 590 | ``dict['variable']) 591 | """ 592 | return type("TagElements", (TagElements,), dict(root_dir=self.dir)) 593 | 594 | def get_tags_dict(self, *tags, **kw): 595 | """ 596 | Return the tags from a tag file as a dict. 597 | """ 598 | filters = kw.get("filters", []) 599 | return parse_tag_lines( 600 | self.search(True, *tags), tag_class=self.tag_class(), filters=filters 601 | ) 602 | 603 | def get_tags_dict_by_suffix(self, suffix, **kw): 604 | """ 605 | Return the tags with the given suffix of a tag file as a dict. 606 | """ 607 | filters = kw.get("filters", []) 608 | return parse_tag_lines( 609 | self.search_by_suffix(suffix), tag_class=self.tag_class(), filters=filters 610 | ) 611 | -------------------------------------------------------------------------------- /plugins/edit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Buffer editing for both ST2 and ST3 that 'just works'. 3 | 4 | Copyright, SublimeXiki project 5 | """ 6 | 7 | import inspect 8 | import sublime 9 | import sublime_plugin 10 | 11 | try: 12 | sublime.edit_storage 13 | except AttributeError: 14 | sublime.edit_storage = {} 15 | 16 | 17 | def run_callback(func, *args, **kwargs): 18 | spec = inspect.getfullargspec(func) 19 | if spec.args or spec.varargs: 20 | func(*args, **kwargs) 21 | else: 22 | func() 23 | 24 | 25 | class EditFuture: 26 | def __init__(self, func): 27 | self.func = func 28 | 29 | def resolve(self, view, edit): 30 | return self.func(view, edit) 31 | 32 | 33 | class EditStep: 34 | def __init__(self, cmd, *args): 35 | self.cmd = cmd 36 | self.args = args 37 | 38 | def run(self, view, edit): 39 | if self.cmd == "callback": 40 | return run_callback(self.args[0], view, edit) 41 | 42 | funcs = { 43 | "insert": view.insert, 44 | "erase": view.erase, 45 | "replace": view.replace, 46 | } 47 | func = funcs.get(self.cmd) 48 | if func: 49 | args = self.resolve_args(view, edit) 50 | func(edit, *args) 51 | 52 | def resolve_args(self, view, edit): 53 | args = [] 54 | for arg in self.args: 55 | if isinstance(arg, EditFuture): 56 | arg = arg.resolve(view, edit) 57 | args.append(arg) 58 | return args 59 | 60 | 61 | class Edit: 62 | def __init__(self, view): 63 | self.view = view 64 | self.steps = [] 65 | 66 | def __nonzero__(self): 67 | return bool(self.steps) 68 | 69 | @classmethod 70 | def future(cls, func): 71 | return EditFuture(func) 72 | 73 | def step(self, cmd, *args): 74 | step = EditStep(cmd, *args) 75 | self.steps.append(step) 76 | 77 | def insert(self, point, string): 78 | self.step("insert", point, string) 79 | 80 | def erase(self, region): 81 | self.step("erase", region) 82 | 83 | def replace(self, region, string): 84 | self.step("replace", region, string) 85 | 86 | def sel(self, start, end=None): 87 | if end is None: 88 | end = start 89 | self.step("sel", start, end) 90 | 91 | def callback(self, func): 92 | self.step("callback", func) 93 | 94 | def run(self, view, edit): 95 | for step in self.steps: 96 | step.run(view, edit) 97 | 98 | def __enter__(self): 99 | return self 100 | 101 | def __exit__(self, type_, value, traceback): 102 | view = self.view 103 | if sublime.version().startswith("2"): 104 | edit = view.begin_edit() 105 | self.run(view, edit) 106 | view.end_edit(edit) 107 | else: 108 | key = str(hash(tuple(self.steps))) 109 | sublime.edit_storage[key] = self.run 110 | view.run_command("apply_edit", {"key": key}) 111 | 112 | 113 | class apply_edit(sublime_plugin.TextCommand): 114 | def run(self, edit, key): 115 | sublime.edit_storage.pop(key)(self.view, edit) 116 | -------------------------------------------------------------------------------- /plugins/ranking/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SublimeText/CTags/45e7b8283b28475cf3f7d668b8bd6f8060813a76/plugins/ranking/__init__.py -------------------------------------------------------------------------------- /plugins/ranking/parse.py: -------------------------------------------------------------------------------- 1 | import re 2 | from ..utils import * 3 | 4 | # import spdb 5 | # spdb.start() 6 | 7 | 8 | class Parser: 9 | """ 10 | Parses tag references and tag definitions. Used for ranking 11 | """ 12 | 13 | @staticmethod 14 | def extract_member_exp(line_to_symbol, source): 15 | """ 16 | Extract receiver object e.g. receiver.mtd() 17 | Strip away brackets and operators. 18 | TODO:HIGH: Add base lang defs + Python/Ruby/C++/Java/C#/PHP overrides (should be very similar) 19 | TODO: comment and string support (eat as may contain brackets. add them to context - js['prop1']['prop-of-prop1']) 20 | """ 21 | lang = get_lang_setting(source) 22 | if not lang: 23 | return [line_to_symbol] 24 | 25 | # Get per-language syntax regex of brackets, splitters etc. 26 | mbr_exp = lang.get("member_exp") 27 | if mbr_exp is None: 28 | return [line_to_symbol] 29 | 30 | lstStop = mbr_exp.get("stop", []) 31 | if not lstStop: 32 | print( 33 | 'warning!: language has member_exp setting but it is ineffective: Must have "stop" key with array of regex to stop search backward from identifier' 34 | ) 35 | return [line_to_symbol] 36 | 37 | lstClose = mbr_exp.get("close", []) 38 | reClose = concat_re(lstClose) 39 | lstOpen = mbr_exp.get("open", []) 40 | reOpen = concat_re(lstOpen) 41 | lstIgnore = mbr_exp.get("ignore", []) 42 | reIgnore = concat_re(lstIgnore) 43 | if len(lstOpen) != len(lstClose): 44 | print("warning!: extract_member_exp: settings lstOpen must match lstClose") 45 | matchOpenClose = dict(zip(lstOpen, lstClose)) 46 | # Construct | regex from all open and close strings with capture (..) 47 | splex = concat_re(lstOpen + lstClose + lstIgnore + lstStop) 48 | 49 | reStop = concat_re(lstStop) 50 | splex = "({0}|{1})".format(splex, reIgnore) 51 | splat = re.split(splex, line_to_symbol) 52 | # print('splat=%s' % splat) 53 | # Stack iter reverse(splat) for detecting unbalanced e.g 'func(obj.yyy' 54 | # while skipping balanced brackets in getSlow(a && b).mtd() 55 | stack = [] 56 | lstMbr = [] 57 | insideExp = False 58 | for cur in reversed(splat): 59 | # Scan backwards from the symbol: If alpha-numeric - keep it. If 60 | # Closing bracket e.g ] or ) or } --> push into stack 61 | if re.match(reClose, cur): 62 | stack.append(cur) 63 | insideExp = True 64 | # If opening bracket --> match it from top-of-stack: If stack empty 65 | # - stop else If match pop-and-continue else stop scanning + 66 | # warning 67 | elif re.match(reOpen, cur): 68 | # '(' with no matching ')' --> func(obj.yyy case --> return obj.yyy 69 | if len(stack) == 0: 70 | break 71 | tokClose = stack.pop() 72 | tokCloseCur = matchOpenClose.get(cur) 73 | if tokClose != tokCloseCur: 74 | print( 75 | "non-matching brackets at the same nesting level: %s %s" 76 | % (tokCloseCur, tokClose) 77 | ) 78 | break 79 | insideExp = False 80 | # If white space --> stop. Do not stop for whitespace inside 81 | # open-close brackets nested expression 82 | elif re.match(reStop, cur): 83 | if not insideExp: 84 | break 85 | elif re.match(reIgnore, cur): 86 | pass 87 | else: 88 | lstMbr[0:0] = cur 89 | 90 | strMbrExp = "".join(lstMbr) 91 | 92 | lstSplit = mbr_exp.get("splitters", []) 93 | reSplit = concat_re(lstSplit) 94 | # Split member deref per-lang (-> and :: in PHP and C++) - use base if 95 | # not found 96 | arrMbrParts = list(filter(None, re.split(reSplit, strMbrExp))) 97 | # print('arrMbrParts=%s' % arrMbrParts) 98 | 99 | return arrMbrParts 100 | -------------------------------------------------------------------------------- /plugins/ranking/rank.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rank and Filter support for ctags plugin for Sublime Text 2/3. 3 | """ 4 | 5 | import os 6 | import re 7 | import string 8 | import sys 9 | 10 | from functools import reduce 11 | 12 | from ..utils import * 13 | 14 | 15 | def compile_definition_filters(view): 16 | filters = [] 17 | for selector, regexes in list(get_setting("definition_filters", {}).items()): 18 | if view.match_selector(view.sel() and view.sel()[0].begin() or 0, selector): 19 | filters.append(regexes) 20 | return filters 21 | 22 | 23 | def get_grams(str): 24 | """ 25 | Return a set of tri-grams (each tri-gram is a tuple) given a string: 26 | Ex: 'Dekel' --> {('d', 'e', 'k'), ('k', 'e', 'l'), ('e', 'k', 'e')} 27 | """ 28 | lstr = str.lower() 29 | return set(zip(lstr, lstr[1:], lstr[2:])) 30 | 31 | 32 | class RankMgr: 33 | """ 34 | For each matched Tag, calculates the rank score or filter it out. The remaining matches are sorted by decending score. 35 | """ 36 | 37 | def __init__(self, region, mbrParts, view, symbol, sym_line): 38 | self.region = region 39 | self.mbrParts = mbrParts 40 | self.view = view 41 | # Used by Rank by Definition Types 42 | self.symbol = symbol 43 | self.sym_line = sym_line 44 | 45 | self.lang = get_lang_setting(get_source(view)) 46 | self.mbr_exp = self.lang.get("member_exp", {}) 47 | 48 | self.def_filters = compile_definition_filters(view) 49 | 50 | self.fname_abs = ( 51 | view.file_name().lower() if not (view.file_name() is None) else None 52 | ) 53 | 54 | mbrGrams = [get_grams(part) for part in mbrParts] 55 | self.setMbrGrams = ( 56 | reduce(lambda s, t: s.union(t), mbrGrams) if mbrGrams else set() 57 | ) 58 | 59 | def pass_def_filter(self, o): 60 | for f in self.def_filters: 61 | for k, v in list(f.items()): 62 | if k in o: 63 | if re.match(v, o[k]): 64 | return False 65 | return True 66 | 67 | def eq_filename(self, rel_path): 68 | if self.fname_abs is None or rel_path is None: 69 | return False 70 | return self.fname_abs.endswith(rel_path.lstrip(".").lower()) 71 | 72 | def scope_filter(self, taglist): 73 | """ 74 | Given optional scope extended field tag.scope = 'startline:startcol-endline:endcol' - def-scope. 75 | Return: Tuple of 2 Lists: 76 | in_scope: Tags with matching scope: current cursor / caret position is contained in their start-end scope range. 77 | no_scope: Tags without scope or with global scope 78 | Usage: locals, local parameters Tags have scope (ex: in estr.js tag generator for JavaScript) 79 | """ 80 | in_scope = [] 81 | no_scope = [] 82 | for tag in taglist: 83 | if ( 84 | self.region is None 85 | or tag.get("scope") is None 86 | or tag.scope is None 87 | or tag.scope == "global" 88 | ): 89 | no_scope.append(tag) 90 | continue 91 | 92 | if not self.eq_filename(tag.filename): 93 | continue 94 | 95 | mch = re.search(get_setting("scope_re"), tag.scope) 96 | 97 | if mch: 98 | # .tags file is 1 based and region.begin() is 0 based 99 | beginLine = int(mch.group(1)) - 1 100 | beginCol = int(mch.group(2)) - 1 101 | endLine = int(mch.group(3)) - 1 102 | endCol = int(mch.group(4)) - 1 103 | beginPoint = self.view.text_point(beginLine, beginCol) 104 | endPoint = self.view.text_point(endLine, endCol) 105 | if self.region.begin() >= beginPoint and self.region.end() <= endPoint: 106 | in_scope.append(tag) 107 | 108 | return (in_scope, no_scope) 109 | 110 | RANK_MATCH_TYPE = 30 111 | tag_types = None 112 | 113 | def get_type_rank(self, tag): 114 | """ 115 | Rank by Definition Types: Rank Higher matching definitions with types matching to the GotoDef 116 | Use regex to identify the type 117 | """ 118 | # First time - compare current symbol line to the per-language list of regex: Each regex is mapped to 1 or more tag types 119 | # Try all regex to build a list of preferred / higher rank tag types 120 | if self.tag_types is None: 121 | self.tag_types = set() 122 | reference_types = self.lang.get("reference_types", {}) 123 | for re_ref, lstTypes in reference_types.items(): 124 | # replace special keyword __symbol__ with our reference symbol 125 | cur_re = re_ref.replace("__symbol__", self.symbol) 126 | if re.search(cur_re, self.sym_line): 127 | self.tag_types = self.tag_types.union(lstTypes) 128 | 129 | return self.RANK_MATCH_TYPE if tag.type in self.tag_types else 0 130 | 131 | RANK_EQ_FILENAME_RANK = 10 132 | reThis = None 133 | 134 | def get_samefile_rank(self, rel_path, mbrParts): 135 | """ 136 | If both reference and definition (tag) are in the same file --> Rank this tag higher. 137 | Tag from same file as reference --> Boost rank 138 | Tag from same file as reference and this|self.method() --> Double boost rank 139 | Note: Inheritence model (base class in different file) is not yet supported. 140 | """ 141 | if self.reThis is None: 142 | lstThis = self.mbr_exp.get("this") 143 | if lstThis: 144 | self.reThis = re.compile(concat_re(lstThis), re.IGNORECASE) 145 | elif self.mbr_exp: 146 | print( 147 | "Warning! Language that has syntax settings is expected to define this|self expression syntax" 148 | ) 149 | 150 | rank = 0 151 | if self.eq_filename(rel_path): 152 | rank += self.RANK_EQ_FILENAME_RANK 153 | if len(mbrParts) == 1 and self.reThis and self.reThis.match(mbrParts[-1]): 154 | # this.mtd() - rank candidate from current file very high. 155 | rank += self.RANK_EQ_FILENAME_RANK 156 | return rank 157 | 158 | RANK_EXACT_MATCH_RIGHTMOST_MBR_PART_TO_FILENAME = 20 159 | WEIGHT_RIGHTMOST_MBR_PART = 2 160 | MAX_WEIGHT_GRAM = 3 161 | WEIGHT_DECAY = 1.5 162 | 163 | def get_mbr_exp_match_tagfile_rank(self, rel_path, mbrParts): 164 | """ 165 | Object Member Expression File Ranking: Rank higher candiates tags path names that fuzzy match the .method() 166 | Rules: 167 | 1) youtube.fetch() --> mbrPaths = ['youtube'] --> get_rank of tag 'fetch' with rel_path a/b/Youtube.js ---> RANK_EXACT_MATCH_RIGHTMOST_MBR_PART_TO_FILENAME 168 | 2) vidtube.fetch() --> tag 'fetch' with rel_path google/video/youtube.js ---> fuzzy match of tri-grams of vidtube (vid,idt,dtu,tub,ube) with tri-grams from the path 169 | """ 170 | rank = 0 171 | if len(mbrParts) == 0: 172 | return rank 173 | 174 | rel_path_no_ext = rel_path.lstrip("." + os.sep) 175 | rel_path_no_ext = os.path.splitext(rel_path_no_ext)[0] 176 | pathParts = rel_path_no_ext.split(os.sep) 177 | if ( 178 | len(pathParts) >= 1 179 | and len(mbrParts) >= 1 180 | and pathParts[-1].lower() == mbrParts[-1].lower() 181 | ): 182 | rank += self.RANK_EXACT_MATCH_RIGHTMOST_MBR_PART_TO_FILENAME 183 | 184 | # Prepare dict of , where weight decays are we move 185 | # further away from the method call (to the left) 186 | pathGrams = [get_grams(part) for part in pathParts] 187 | wt = self.MAX_WEIGHT_GRAM 188 | dctPathGram = {} 189 | for setPathGram in reversed(pathGrams): 190 | dctPathPart = dict.fromkeys(setPathGram, wt) 191 | dctPathGram = merge_two_dicts_shallow(dctPathPart, dctPathGram) 192 | wt /= self.WEIGHT_DECAY 193 | 194 | for mbrGrm in self.setMbrGrams: 195 | rank += dctPathGram.get(mbrGrm, 0) 196 | 197 | return rank 198 | 199 | def get_combined_rank(self, tag, mbrParts): 200 | """ 201 | Calculate rank score per tag, combining several heuristics 202 | """ 203 | rank = 0 204 | 205 | # Type definition Rank 206 | rank += self.get_type_rank(tag) 207 | 208 | rel_path = tag.tag_path[0] 209 | # Same file and this.method() ranking 210 | rank += self.get_samefile_rank(rel_path, mbrParts) 211 | 212 | # Object Member Expression File Ranking 213 | rank += self.get_mbr_exp_match_tagfile_rank(rel_path, mbrParts) 214 | 215 | # print('rank = %d' % rank); 216 | return rank 217 | 218 | def sort_tags(self, taglist): 219 | # Scope Filter: If symbol matches at least 1 local scope tag - assume they hides non-scope and global scope tags. 220 | # If no local-scope (in_scope) matches --> keep the global / no scope matches (see in sorted_tags) and discard 221 | # the local-scope - because they are not locals of the current position 222 | # If object-receiver (someobj.symbol) --> refer to as global tag --> 223 | # filter out local-scope tags 224 | (in_scope, no_scope) = self.scope_filter(taglist) 225 | if ( 226 | len(self.setMbrGrams) == 0 and len(in_scope) > 0 227 | ): # TODO:Config: @symbol - in Ruby instance var (therefore never local var) 228 | p_tags = in_scope 229 | else: 230 | p_tags = no_scope 231 | 232 | p_tags = list(filter(lambda tag: self.pass_def_filter(tag), p_tags)) 233 | p_tags = sorted( 234 | p_tags, 235 | key=lambda tag: self.get_combined_rank(tag, self.mbrParts), 236 | reverse=True, 237 | ) 238 | return p_tags 239 | -------------------------------------------------------------------------------- /plugins/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | common utilities used by all ctags modules 3 | """ 4 | import re 5 | import sublime 6 | 7 | 8 | def get_settings(): 9 | """ 10 | Load settings. 11 | 12 | :returns: dictionary containing settings 13 | """ 14 | return sublime.load_settings("CTags.sublime-settings") 15 | 16 | 17 | def get_setting(key, default=None): 18 | """ 19 | Load individual setting. 20 | 21 | :param key: setting key to get value for 22 | :param default: default value to return if no value found 23 | 24 | :returns: value for ``key`` if ``key`` exists, else ``default`` 25 | """ 26 | return get_settings().get(key, default) 27 | 28 | 29 | setting = get_setting 30 | 31 | 32 | def concat_re(reList, escape=False, wrapCapture=False): 33 | """ 34 | concat list of regex into a single regex, used by re.split 35 | wrapCapture - if true --> adds () around the result regex --> split will keep the splitters in its output array. 36 | """ 37 | ret = "|".join((re.escape(spl) if escape else spl) for spl in reList) 38 | if wrapCapture: 39 | ret = "(" + ret + ")" 40 | return ret 41 | 42 | 43 | def dict_extend(dct, base): 44 | if not dct: 45 | dct = {} 46 | if base: 47 | deriv = base 48 | deriv = merge_two_dicts_deep(deriv, dct) 49 | else: 50 | deriv = dct 51 | return deriv 52 | 53 | 54 | def merge_two_dicts_shallow(x, y): 55 | """ 56 | Given two dicts, merge them into a new dict as a shallow copy. 57 | y members overwrite x members with the same keys. 58 | """ 59 | z = x.copy() 60 | z.update(y) 61 | return z 62 | 63 | 64 | def merge_two_dicts_deep(a, b, path=None): 65 | "Merges b into a including sub-dictionaries - recursive" 66 | if path is None: 67 | path = [] 68 | for key in b: 69 | if key in a: 70 | if isinstance(a[key], dict) and isinstance(b[key], dict): 71 | merge_two_dicts_deep(a[key], b[key], path + [str(key)]) 72 | elif a[key] == b[key]: 73 | pass # same leaf value 74 | else: 75 | a[key] = b[key] 76 | else: 77 | a[key] = b[key] 78 | return a 79 | 80 | 81 | RE_SPECIAL_CHARS = re.compile( 82 | "(\\\\|\\*|\\+|\\?|\\||\\{|\\}|\\[|\\]|\\(|\\)|\\^|\\$|\\.|\\#|\\ )" 83 | ) 84 | 85 | 86 | def escape_regex(s): 87 | return RE_SPECIAL_CHARS.sub(lambda m: "\\%s" % m.group(1), s) 88 | 89 | 90 | def get_source(view): 91 | """ 92 | return the language used in current caret or selection location 93 | """ 94 | scope_name = view.scope_name( 95 | view.sel()[0].begin() 96 | ) # ex: 'source.python meta.function-call.python ' 97 | source = re.split(" ", scope_name)[0] # ex: 'source.python' 98 | return source 99 | 100 | 101 | def get_lang_setting(source): 102 | """ 103 | given source (ex: 'source.python') --> return its language_syntax settings. 104 | A language can inherit its settings from another language, overidding as needed. 105 | """ 106 | lang = setting("language_syntax").get(source) 107 | if lang is not None: 108 | base = setting("language_syntax").get(lang.get("inherit")) 109 | lang = dict_extend(lang, base) 110 | else: 111 | lang = {} 112 | return lang 113 | 114 | 115 | def compile_filters(view): 116 | pt = view.sel() and view.sel()[0].begin() or 0 117 | return [ 118 | regexes 119 | for selector, regexes in setting("filters", {}).items() 120 | if view.match_selector(pt, selector) 121 | ] 122 | --------------------------------------------------------------------------------