├── messages.json ├── Default (OSX).sublime-mousemap ├── Default (Windows).sublime-mousemap ├── Default (Linux).sublime-mousemap ├── messages └── 1.1.0.txt ├── Default.sublime-commands ├── packages.json ├── todo_results.hidden-tmLanguage ├── Default (OSX).sublime-keymap ├── Default (Linux).sublime-keymap ├── Default (Windows).sublime-keymap ├── README.markdown └── todo.py /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.1.0": "messages/1.1.0.txt" 3 | } 4 | -------------------------------------------------------------------------------- /Default (OSX).sublime-mousemap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "button": "button1", "count": 2, "modifiers": ["alt"], 4 | "command": "mouse_goto_comment" 5 | } 6 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-mousemap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "button": "button1", "count": 2, "modifiers": ["alt"], 4 | "command": "mouse_goto_comment" 5 | } 6 | ] -------------------------------------------------------------------------------- /Default (Linux).sublime-mousemap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "button": "button1", "count": 2, "modifiers": ["shift"], 4 | "command": "mouse_goto_comment" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /messages/1.1.0.txt: -------------------------------------------------------------------------------- 1 | SublimeTODO 1.1.0 Changelog: 2 | 3 | - Repo reorg to support explicit versioning and Package Control upgrade messages. 4 | 5 | - [LINUX] alt + dblclick is replaced by shift + dblclick to prevent clash 6 | with common alt+click+drag method of moving windows. 7 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Show TODOs: Project and open files", 4 | "command": "todo" 5 | }, 6 | { 7 | "caption": "Show TODOs: Open files only", 8 | "command": "todo", "args": {"open_files_only": true} 9 | } 10 | ] -------------------------------------------------------------------------------- /packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.2", 3 | "packages": [ 4 | { 5 | "name": "SublimeTODO", 6 | "description": "Extract TODO-type comments from open files and project folders", 7 | "author": "Rob Cowie", 8 | "homepage": "https://github.com/robcowie/SublimeTODO", 9 | "last_modified": "2012-11-21 09:53:00", 10 | "platforms": { 11 | "*": [ 12 | { 13 | "version": "1.1.3", 14 | "url": "https://nodeload.github.com/robcowie/SublimeTODO/zipball/1.1.3" 15 | } 16 | ] 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /todo_results.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | TODO Results 7 | 8 | patterns 9 | 10 | 11 | match 12 | 13 | ^[\s]{0,2}([\d]+\.) ([^:]+)(:)([\d]+) (.*)$ 14 | captures 15 | 16 | 1 17 | 18 | name 19 | constant.numeric.item-number.todo-list 20 | 21 | 2 22 | 23 | name 24 | entity.name.filename.todo-list 25 | 26 | 3 27 | 28 | name 29 | punctuation.definition.delimiter 30 | 31 | 4 32 | 33 | name 34 | constant.numeric.line-number.todo-list 35 | 36 | 5 37 | 38 | name 39 | entity.name.match.todo-list 40 | 41 | 42 | 43 | 44 | scopeName 45 | text.todo-list 46 | uuid 47 | 2ce8c28e-9c29-4f7f-bc65-7c5311732d29 48 | 49 | 50 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | // { 4 | // "keys": ["n"], "command": "goto_next_result", 5 | // "context": [ 6 | // { "key": "setting.todo_results", "operator": "equal", "operand": true } 7 | // ] 8 | // } 9 | 10 | { 11 | "keys": ["n"], "command": "navigate_results", 12 | "context": [ 13 | {"key": "setting.command_mode", "operand": true} 14 | // {"key": "setting.todo_results"} 15 | ], 16 | "args": {"direction": "forward"} 17 | }, 18 | 19 | { 20 | "keys": ["down"], "command": "navigate_results", 21 | "context": [ 22 | {"key": "setting.command_mode", "operand": true} 23 | // {"key": "setting.todo_results"} 24 | ], 25 | "args": {"direction": "forward"} 26 | }, 27 | 28 | { 29 | "keys": ["j"], "command": "navigate_results", 30 | "context": [ 31 | {"key": "setting.command_mode", "operand": true} 32 | // {"key": "setting.todo_results"} 33 | ], 34 | "args": {"direction": "forward"} 35 | }, 36 | 37 | { 38 | "keys": ["p"], "command": "navigate_results", 39 | "context": [ 40 | {"key": "setting.command_mode", "operand": true} 41 | // {"key": "setting.todo_results"} 42 | ], 43 | "args": {"direction": "backward"} 44 | }, 45 | 46 | { 47 | "keys": ["up"], "command": "navigate_results", 48 | "context": [ 49 | {"key": "setting.command_mode", "operand": true} 50 | //{"key": "setting.todo_results"} 51 | ], 52 | "args": {"direction": "backward"} 53 | }, 54 | 55 | { 56 | "keys": ["k"], "command": "navigate_results", 57 | "context": [ 58 | {"key": "setting.command_mode", "operand": true} 59 | //{"key": "setting.todo_results"} 60 | ], 61 | "args": {"direction": "backward"} 62 | }, 63 | 64 | { 65 | "keys": ["c"], "command": "clear_selection", 66 | "context": [ 67 | {"key": "setting.command_mode", "operand": true} 68 | // {"key": "setting.todo_results"} 69 | ] 70 | }, 71 | 72 | { 73 | "keys": ["enter"], "command": "goto_comment", 74 | "context": [ 75 | {"key": "setting.command_mode", "operand": true} 76 | // {"key": "setting.todo_results"} 77 | ] 78 | } 79 | 80 | ] 81 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | // { 4 | // "keys": ["n"], "command": "goto_next_result", 5 | // "context": [ 6 | // { "key": "setting.todo_results", "operator": "equal", "operand": true } 7 | // ] 8 | // } 9 | 10 | { 11 | "keys": ["n"], "command": "navigate_results", 12 | "context": [ 13 | {"key": "setting.command_mode", "operand": true} 14 | // {"key": "setting.todo_results"} 15 | ], 16 | "args": {"direction": "forward"} 17 | }, 18 | 19 | { 20 | "keys": ["down"], "command": "navigate_results", 21 | "context": [ 22 | {"key": "setting.command_mode", "operand": true} 23 | // {"key": "setting.todo_results"} 24 | ], 25 | "args": {"direction": "forward"} 26 | }, 27 | 28 | { 29 | "keys": ["j"], "command": "navigate_results", 30 | "context": [ 31 | {"key": "setting.command_mode", "operand": true} 32 | // {"key": "setting.todo_results"} 33 | ], 34 | "args": {"direction": "forward"} 35 | }, 36 | 37 | { 38 | "keys": ["p"], "command": "navigate_results", 39 | "context": [ 40 | {"key": "setting.command_mode", "operand": true} 41 | // {"key": "setting.todo_results"} 42 | ], 43 | "args": {"direction": "backward"} 44 | }, 45 | 46 | { 47 | "keys": ["up"], "command": "navigate_results", 48 | "context": [ 49 | {"key": "setting.command_mode", "operand": true} 50 | //{"key": "setting.todo_results"} 51 | ], 52 | "args": {"direction": "backward"} 53 | }, 54 | 55 | { 56 | "keys": ["k"], "command": "navigate_results", 57 | "context": [ 58 | {"key": "setting.command_mode", "operand": true} 59 | //{"key": "setting.todo_results"} 60 | ], 61 | "args": {"direction": "backward"} 62 | }, 63 | 64 | { 65 | "keys": ["c"], "command": "clear_selection", 66 | "context": [ 67 | {"key": "setting.command_mode", "operand": true} 68 | // {"key": "setting.todo_results"} 69 | ] 70 | }, 71 | 72 | { 73 | "keys": ["enter"], "command": "goto_comment", 74 | "context": [ 75 | {"key": "setting.command_mode", "operand": true} 76 | // {"key": "setting.todo_results"} 77 | ] 78 | } 79 | 80 | ] 81 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | // { 4 | // "keys": ["n"], "command": "goto_next_result", 5 | // "context": [ 6 | // { "key": "setting.todo_results", "operator": "equal", "operand": true } 7 | // ] 8 | // } 9 | 10 | { 11 | "keys": ["n"], "command": "navigate_results", 12 | "context": [ 13 | {"key": "setting.command_mode", "operand": true} 14 | // {"key": "setting.todo_results"} 15 | ], 16 | "args": {"direction": "forward"} 17 | }, 18 | 19 | { 20 | "keys": ["down"], "command": "navigate_results", 21 | "context": [ 22 | {"key": "setting.command_mode", "operand": true} 23 | // {"key": "setting.todo_results"} 24 | ], 25 | "args": {"direction": "forward"} 26 | }, 27 | 28 | { 29 | "keys": ["j"], "command": "navigate_results", 30 | "context": [ 31 | {"key": "setting.command_mode", "operand": true} 32 | // {"key": "setting.todo_results"} 33 | ], 34 | "args": {"direction": "forward"} 35 | }, 36 | 37 | { 38 | "keys": ["p"], "command": "navigate_results", 39 | "context": [ 40 | {"key": "setting.command_mode", "operand": true} 41 | // {"key": "setting.todo_results"} 42 | ], 43 | "args": {"direction": "backward"} 44 | }, 45 | 46 | { 47 | "keys": ["up"], "command": "navigate_results", 48 | "context": [ 49 | {"key": "setting.command_mode", "operand": true} 50 | //{"key": "setting.todo_results"} 51 | ], 52 | "args": {"direction": "backward"} 53 | }, 54 | 55 | { 56 | "keys": ["k"], "command": "navigate_results", 57 | "context": [ 58 | {"key": "setting.command_mode", "operand": true} 59 | //{"key": "setting.todo_results"} 60 | ], 61 | "args": {"direction": "backward"} 62 | }, 63 | 64 | { 65 | "keys": ["c"], "command": "clear_selection", 66 | "context": [ 67 | {"key": "setting.command_mode", "operand": true} 68 | // {"key": "setting.todo_results"} 69 | ] 70 | }, 71 | 72 | { 73 | "keys": ["enter"], "command": "goto_comment", 74 | "context": [ 75 | {"key": "setting.command_mode", "operand": true} 76 | // {"key": "setting.todo_results"} 77 | ] 78 | } 79 | 80 | ] 81 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Sublime TODOs 2 | 3 | A Sublime Text 2 plugin to extract and list TODO comments from open files and 4 | project folders. 5 | 6 | Take a look at [this screencast](http://webdesign.tutsplus.com/tutorials/applications/quick-tip-streamline-your-todo-lists-in-sublime-text-2/) (courtesy of Shannon Huffman) for an overview. 7 | 8 | 9 | # Install 10 | 11 | The preferred method is to use the [Sublime Package Manager](http://wbond.net/sublime_packages/package_control). Alternatively, checkout from github: 12 | 13 | ```sh 14 | $ cd Sublime Text 2/Packages 15 | $ git clone https://robcowie@github.com/robcowie/SublimeTODO.git 16 | ``` 17 | 18 | # Config 19 | 20 | All plugin configuration must be placed in user or project-specific settings inside a `todo` object, for example; 21 | 22 | ```javascript 23 | { 24 | // other user config ... 25 | "todo": { 26 | "patterns": {} 27 | } 28 | } 29 | ``` 30 | 31 | See an example user settings file [here](https://gist.github.com/2049887). 32 | 33 | 34 | ## Adding comment patterns 35 | 36 | Extraction uses regular expressions that return one match group 37 | representing the message. Default patterns are provided for `TODO`, `NOTE`, `FIXME` 38 | and `CHANGED` comments. 39 | To override or provide more patterns, add `patterns` to user settings, e.g. 40 | 41 | ```javascript 42 | "patterns": { 43 | "TODO": "TODO[\\s]*?:+(?P.*)$", 44 | "NOTE": "NOTE[\\s]*?:+(?P.*)$", 45 | "FIXME": "FIX ?ME[\\s]*?:+(?P\\S.*)$", 46 | "CHANGED": "CHANGED[\\s]*?:+(?P\\S.*)$" 47 | } 48 | ``` 49 | 50 | Note that the pattern _must_ provide at least one named group which will be used to group the comments in results. 51 | 52 | By default, searching is not case sensitive. You can change this behaviour by adding 53 | 54 | "case_sensitive": true 55 | 56 | to the todo settings object. 57 | 58 | 59 | ## Excluding files and folders 60 | 61 | Global settings `folder_exclude_patterns`, `file_exclude_patterns` and `binary_file_patterns` are excluded from search results. 62 | 63 | To exclude further directories, add directory names (not glob pattern or regexp) to `folder_exclude_patterns` in todo settings: 64 | 65 | ```javascript 66 | "todo": { 67 | "folder_exclude_patterns": [ 68 | "vendor", 69 | "tmp" 70 | ] 71 | } 72 | ``` 73 | 74 | To add file excludes, add glob patterns to `file_exclude_patterns`: 75 | 76 | ```javascript 77 | "file_exclude_patterns": [ 78 | "*.css" 79 | ] 80 | ``` 81 | 82 | 83 | ## Results title 84 | 85 | Override the results view title by setting `result_title` 86 | 87 | ```javascript 88 | "result_title": "TODO Results" 89 | ``` 90 | 91 | # Usage 92 | 93 | `Show TODOs: Project and open files` scans all files in your project 94 | `Show TODOs: Open files only` scans only open, saved files 95 | Both are triggered from the command palette. No default key bindings are provided. 96 | 97 | ## Navigating results 98 | 99 | Results can be navigated by keyboard and mouse: 100 | 101 | * `n`ext, `p`revious, `c`lear, `enter` 102 | * `alt-double click` (`shift-double click` in Linux) 103 | 104 | Note that due to the lack of support for context in mousemaps right now, 105 | alt-double click will trigger in _any_ document, though it should be a no-op. 106 | 107 | # License 108 | 109 | All of SublimeTODO is licensed under the MIT license. 110 | 111 | Copyright (c) 2012 Rob Cowie 112 | 113 | 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: 114 | 115 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 116 | 117 | 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. 118 | -------------------------------------------------------------------------------- /todo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ## TODO: Implement TODO_IGNORE setting (http://mdeering.com/posts/004-get-your-textmate-todos-and-fixmes-under-control) 4 | ## TODO: Make the output clickable (å la find results) 5 | ## TODO: Occasional NoneType bug 6 | ## todo: Make the sections foldable (define them as regions?) 7 | 8 | """""" 9 | 10 | from collections import namedtuple 11 | from datetime import datetime 12 | import functools 13 | import fnmatch 14 | from itertools import groupby 15 | import logging 16 | from os import path, walk 17 | import re 18 | import threading 19 | 20 | import sublime 21 | import sublime_plugin 22 | 23 | 24 | DEBUG = True 25 | 26 | DEFAULT_SETTINGS = { 27 | 'result_title': 'TODO Results', 28 | 29 | 'core_patterns': { 30 | 'TODO': r'TODO[\s]*?:+(?P.*)$', 31 | 'NOTE': r'NOTE[\s]*?:+(?P.*)$', 32 | 'FIXME': r'FIX ?ME[\s]*?:+(?P.*)$', 33 | 'CHANGED': r'CHANGED[\s]*?:+(?P.*)$' 34 | }, 35 | 36 | 'patterns': {} 37 | } 38 | 39 | Message = namedtuple('Message', 'type, msg') 40 | 41 | ## LOGGING SETUP 42 | try: 43 | from logging import NullHandler 44 | except ImportError: 45 | class NullHandler(logging.Handler): 46 | def handle(self, record): 47 | pass 48 | 49 | def emit(self, record): 50 | pass 51 | 52 | def createLock(self): 53 | self.lock = None 54 | 55 | log = logging.getLogger('SublimeTODO') 56 | log.handlers = [] ## hack to prevent extraneous handlers on ST2 auto-reload 57 | log.addHandler(NullHandler()) 58 | log.setLevel(logging.INFO) 59 | if DEBUG: 60 | log.addHandler(logging.StreamHandler()) 61 | log.setLevel(logging.DEBUG) 62 | 63 | 64 | def do_when(conditional, callback, *args, **kwargs): 65 | if conditional(): 66 | return callback(*args, **kwargs) 67 | sublime.set_timeout(functools.partial(do_when, conditional, callback, *args, **kwargs), 50) 68 | 69 | 70 | class Settings(dict): 71 | """Combine default and user settings""" 72 | def __init__(self, user_settings): 73 | settings = DEFAULT_SETTINGS.copy() 74 | settings.update(user_settings) 75 | ## Combine core_patterns and patterns 76 | settings['core_patterns'].update(settings['patterns']) 77 | settings['patterns'] = settings.pop('core_patterns') 78 | super(Settings, self).__init__(settings) 79 | 80 | 81 | class ThreadProgress(object): 82 | def __init__(self, thread, message, success_message, file_counter): 83 | self.thread = thread 84 | self.message = message 85 | self.success_message = success_message 86 | self.file_counter = file_counter 87 | self.addend = 1 88 | self.size = 8 89 | sublime.set_timeout(lambda: self.run(0), 100) 90 | 91 | def run(self, i): 92 | if not self.thread.is_alive(): 93 | if hasattr(self.thread, 'result') and not self.thread.result: 94 | sublime.status_message('') 95 | return 96 | sublime.status_message(self.success_message) 97 | return 98 | 99 | before = i % self.size 100 | after = (self.size - 1) - before 101 | sublime.status_message('%s [%s=%s] (%s files scanned)' % \ 102 | (self.message, ' ' * before, ' ' * after, self.file_counter)) 103 | if not after: 104 | self.addend = -1 105 | if not before: 106 | self.addend = 1 107 | i += self.addend 108 | sublime.set_timeout(lambda: self.run(i), 100) 109 | 110 | 111 | 112 | class TodoExtractor(object): 113 | def __init__(self, settings, filepaths, dirpaths, ignored_dirs, ignored_file_patterns, 114 | file_counter): 115 | self.filepaths = filepaths 116 | self.dirpaths = dirpaths 117 | self.patterns = settings['patterns'] 118 | self.settings = settings 119 | self.file_counter = file_counter 120 | self.ignored_dirs = ignored_dirs 121 | self.ignored_files = ignored_file_patterns 122 | self.log = logging.getLogger('SublimeTODO.extractor') 123 | 124 | def iter_files(self): 125 | """""" 126 | seen_paths_ = [] 127 | files = self.filepaths 128 | dirs = self.dirpaths 129 | exclude_dirs = self.ignored_dirs 130 | 131 | for filepath in files: 132 | pth = path.realpath(path.abspath(filepath)) 133 | if pth not in seen_paths_: 134 | seen_paths_.append(pth) 135 | yield pth 136 | 137 | for dirpath in dirs: 138 | dirpath = path.abspath(dirpath) 139 | for dirpath, dirnames, filenames in walk(dirpath): 140 | ## remove excluded dirs 141 | ## TODO: These are not patterns. Consider making them glob patterns 142 | for dir in exclude_dirs: 143 | if dir in dirnames: 144 | self.log.debug(u'Ignoring dir: {0}'.format(dir)) 145 | dirnames.remove(dir) 146 | 147 | for filepath in filenames: 148 | pth = path.join(dirpath, filepath) 149 | pth = path.realpath(path.abspath(pth)) 150 | if pth not in seen_paths_: 151 | seen_paths_.append(pth) 152 | yield pth 153 | 154 | def filter_files(self, files): 155 | """""" 156 | exclude_patterns = [re.compile(patt) for patt in self.ignored_files] 157 | for filepath in files: 158 | if any(patt.match(filepath) for patt in exclude_patterns): 159 | continue 160 | yield filepath 161 | 162 | def search_targets(self): 163 | """Yield filtered filepaths for message extraction""" 164 | return self.filter_files(self.iter_files()) 165 | 166 | def extract(self): 167 | """""" 168 | message_patterns = '|'.join(self.patterns.values()) 169 | case_sensitivity = 0 if self.settings.get('case_sensitive', False) else re.IGNORECASE 170 | patt = re.compile(message_patterns, case_sensitivity) 171 | for filepath in self.search_targets(): 172 | try: 173 | f = open(filepath) 174 | self.log.debug(u'Scanning {0}'.format(filepath)) 175 | for linenum, line in enumerate(f): 176 | for mo in patt.finditer(line): 177 | ## Remove the non-matched groups 178 | matches = [Message(msg_type, msg) for msg_type, msg in mo.groupdict().iteritems() if msg] 179 | for match in matches: 180 | yield {'filepath': filepath, 'linenum': linenum + 1, 'match': match} 181 | except IOError: 182 | ## Probably a broken symlink 183 | f = None 184 | finally: 185 | self.file_counter.increment() 186 | if f is not None: 187 | f.close() 188 | 189 | 190 | class TodoRenderer(object): 191 | def __init__(self, settings, window, file_counter): 192 | self.window = window 193 | self.settings = settings 194 | self.file_counter = file_counter 195 | 196 | @property 197 | def view_name(self): 198 | """The name of the new results view. Defined in settings.""" 199 | return self.settings['result_title'] 200 | 201 | @property 202 | def header(self): 203 | hr = u'+ {0} +'.format('-' * 76) 204 | return u'{hr}\n| TODOS @ {0:<68} |\n| {1:<76} |\n{hr}\n'.format( 205 | datetime.now().strftime('%A %d %B %Y %H:%M').decode("utf-8"), 206 | u'{0} files scanned'.format(self.file_counter), 207 | hr=hr) 208 | 209 | @property 210 | def view(self): 211 | existing_results = [v for v in self.window.views() 212 | if v.name() == self.view_name and v.is_scratch()] 213 | if existing_results: 214 | v = existing_results[0] 215 | else: 216 | v = self.window.new_file() 217 | v.set_name(self.view_name) 218 | v.set_scratch(True) 219 | v.settings().set('todo_results', True) 220 | return v 221 | 222 | def format(self, messages): 223 | """Yield lines for rendering into results view. Includes headers and 224 | blank lines. 225 | Lines are returned in the form (type, content, [data]) where type is either 226 | 'header', 'whitespace' or 'result' 227 | """ 228 | key_func = lambda m: m['match'].type 229 | messages = sorted(messages, key=key_func) 230 | 231 | for message_type, matches in groupby(messages, key=key_func): 232 | matches = list(matches) 233 | if matches: 234 | yield ('header', u'\n## {0} ({1})'.format(message_type.upper().decode('utf8', 'ignore'), len(matches)), {}) 235 | for idx, m in enumerate(matches, 1): 236 | msg = m['match'].msg.decode('utf8', 'ignore') ## Don't know the file encoding 237 | filepath = path.basename(m['filepath']) 238 | line = u"{idx}. {filepath}:{linenum} {msg}".format( 239 | idx=idx, filepath=filepath, linenum=m['linenum'], msg=msg) 240 | yield ('result', line, m) 241 | 242 | def render_to_view(self, formatted_results): 243 | """This blocks the main thread, so make it quick""" 244 | ## Header 245 | result_view = self.view 246 | edit = result_view.begin_edit() 247 | result_view.erase(edit, sublime.Region(0, result_view.size())) 248 | result_view.insert(edit, result_view.size(), self.header) 249 | result_view.end_edit(edit) 250 | 251 | ## Region : match_dicts 252 | regions = {} 253 | 254 | ## Result sections 255 | for linetype, line, data in formatted_results: 256 | edit = result_view.begin_edit() 257 | insert_point = result_view.size() 258 | result_view.insert(edit, insert_point, line) 259 | if linetype == 'result': 260 | rgn = sublime.Region(insert_point, result_view.size()) 261 | regions[rgn] = data 262 | result_view.insert(edit, result_view.size(), u'\n') 263 | result_view.end_edit(edit) 264 | 265 | result_view.add_regions('results', regions.keys(), '') 266 | 267 | ## Store {Region : data} map in settings 268 | ## TODO: Abstract this out to a storage class Storage.get(region) ==> data dict 269 | ## Region() cannot be stored in settings, so convert to a primitive type 270 | # d_ = regions 271 | d_ = dict(('{0},{1}'.format(k.a, k.b), v) for k, v in regions.iteritems()) 272 | result_view.settings().set('result_regions', d_) 273 | 274 | ## Set syntax and settings 275 | result_view.set_syntax_file('Packages/SublimeTODO/todo_results.hidden-tmLanguage') 276 | result_view.settings().set('line_padding_bottom', 2) 277 | result_view.settings().set('line_padding_top', 2) 278 | result_view.settings().set('word_wrap', False) 279 | result_view.settings().set('command_mode', True) 280 | self.window.focus_view(result_view) 281 | 282 | 283 | class WorkerThread(threading.Thread): 284 | def __init__(self, extractor, renderer): 285 | self.extractor = extractor 286 | self.renderer = renderer 287 | threading.Thread.__init__(self) 288 | 289 | def run(self): 290 | ## Extract in this thread 291 | todos = self.extractor.extract() 292 | rendered = list(self.renderer.format(todos)) 293 | 294 | ## Render into new window in main thread 295 | def render(): 296 | self.renderer.render_to_view(rendered) 297 | sublime.set_timeout(render, 10) 298 | 299 | 300 | class FileScanCounter(object): 301 | """Thread-safe counter used to update the status bar""" 302 | def __init__(self): 303 | self.ct = 0 304 | self.lock = threading.RLock() 305 | self.log = logging.getLogger('SublimeTODO') 306 | 307 | def __call__(self, filepath): 308 | self.log.debug(u'Scanning %s' % filepath) 309 | self.increment() 310 | 311 | def __str__(self): 312 | with self.lock: 313 | return '%d' % self.ct 314 | 315 | def increment(self): 316 | with self.lock: 317 | self.ct += 1 318 | 319 | def reset(self): 320 | with self.lock: 321 | self.ct = 0 322 | 323 | 324 | class TodoCommand(sublime_plugin.TextCommand): 325 | 326 | def search_paths(self, window, open_files_only=False): 327 | """Return (filepaths, dirpaths)""" 328 | return ( 329 | [view.file_name() for view in window.views() if view.file_name()], 330 | window.folders() if not open_files_only else [] 331 | ) 332 | 333 | def run(self, edit, open_files_only=False): 334 | window = self.view.window() 335 | settings = Settings(self.view.settings().get('todo', {})) 336 | 337 | 338 | ## TODO: Cleanup this init code. Maybe move it to the settings object 339 | filepaths, dirpaths = self.search_paths(window, open_files_only=open_files_only) 340 | 341 | ignored_dirs = settings.get('folder_exclude_patterns', []) 342 | ## Get exclude patterns from global settings 343 | ## Is there really no better way to access global settings? 344 | global_settings = sublime.load_settings('Global.sublime-settings') 345 | ignored_dirs.extend(global_settings.get('folder_exclude_patterns', [])) 346 | 347 | exclude_file_patterns = settings.get('file_exclude_patterns', []) 348 | exclude_file_patterns.extend(global_settings.get('file_exclude_patterns', [])) 349 | exclude_file_patterns.extend(global_settings.get('binary_file_patterns', [])) 350 | exclude_file_patterns = [fnmatch.translate(patt) for patt in exclude_file_patterns] 351 | 352 | file_counter = FileScanCounter() 353 | extractor = TodoExtractor(settings, filepaths, dirpaths, ignored_dirs, 354 | exclude_file_patterns, file_counter) 355 | renderer = TodoRenderer(settings, window, file_counter) 356 | 357 | worker_thread = WorkerThread(extractor, renderer) 358 | worker_thread.start() 359 | ThreadProgress(worker_thread, 'Finding TODOs', '', file_counter) 360 | 361 | 362 | class NavigateResults(sublime_plugin.TextCommand): 363 | DIRECTION = {'forward': 1, 'backward': -1} 364 | STARTING_POINT = {'forward': -1, 'backward': 0} 365 | 366 | def __init__(self, view): 367 | super(NavigateResults, self).__init__(view) 368 | 369 | def run(self, edit, direction): 370 | view = self.view 371 | settings = view.settings() 372 | results = self.view.get_regions('results') 373 | if not results: 374 | sublime.status_message('No results to navigate') 375 | return 376 | 377 | ##NOTE: numbers stored in settings are coerced to floats or longs 378 | selection = int(settings.get('selected_result', self.STARTING_POINT[direction])) 379 | selection = selection + self.DIRECTION[direction] 380 | try: 381 | target = results[selection] 382 | except IndexError: 383 | target = results[0] 384 | selection = 0 385 | 386 | settings.set('selected_result', selection) 387 | ## Create a new region for highlighting 388 | target = target.cover(target) 389 | view.add_regions('selection', [target], 'selected', 'dot') 390 | view.show(target) 391 | 392 | 393 | class ClearSelection(sublime_plugin.TextCommand): 394 | def run(self, edit): 395 | self.view.erase_regions('selection') 396 | self.view.settings().erase('selected_result') 397 | 398 | 399 | class GotoComment(sublime_plugin.TextCommand): 400 | def __init__(self, *args): 401 | self.log = logging.getLogger('SublimeTODO.nav') 402 | super(GotoComment, self).__init__(*args) 403 | 404 | def run(self, edit): 405 | ## Get the idx of selected result region 406 | selection = int(self.view.settings().get('selected_result', -1)) 407 | ## Get the region 408 | selected_region = self.view.get_regions('results')[selection] 409 | ## Convert region to key used in result_regions (this is tedious, but 410 | ## there is no other way to store regions with associated data) 411 | data = self.view.settings().get('result_regions')['{0},{1}'.format(selected_region.a, selected_region.b)] 412 | self.log.debug(u'Goto comment at {filepath}:{linenum}'.format(**data)) 413 | new_view = self.view.window().open_file(data['filepath']) 414 | do_when(lambda: not new_view.is_loading(), lambda: new_view.run_command("goto_line", {"line": data['linenum']})) 415 | 416 | 417 | class MouseGotoComment(sublime_plugin.TextCommand): 418 | def __init__(self, *args): 419 | self.log = logging.getLogger('SublimeTODO.nav') 420 | super(MouseGotoComment, self).__init__(*args) 421 | 422 | def highlight(self, region): 423 | target = region.cover(region) 424 | self.view.add_regions('selection', [target], 'selected', 'dot') 425 | self.view.show(target) 426 | 427 | def get_result_region(self, pos): 428 | line = self.view.line(pos) 429 | return line 430 | 431 | def run(self, edit): 432 | if not self.view.settings().get('result_regions'): 433 | return 434 | ## get selected line 435 | pos = self.view.sel()[0].end() 436 | result = self.get_result_region(pos) 437 | self.highlight(result) 438 | data = self.view.settings().get('result_regions')['{0},{1}'.format(result.a, result.b)] 439 | self.log.debug(u'Goto comment at {filepath}:{linenum}'.format(**data)) 440 | new_view = self.view.window().open_file(data['filepath']) 441 | do_when(lambda: not new_view.is_loading(), lambda: new_view.run_command("goto_line", {"line": data['linenum']})) 442 | --------------------------------------------------------------------------------