├── LICENSE ├── Main.sublime-menu ├── README.md ├── common.py ├── messages.json ├── messages ├── 0.1.1.txt ├── 0.1.2.txt ├── 0.1.3.txt ├── 0.1.4.txt ├── 0.1.5.txt ├── 0.1.6.txt └── install.txt ├── outline-Dark.hidden-tmTheme ├── outline.hidden-tmLanguage ├── outline.hidden-tmTheme ├── outline.py ├── outline.sublime-commands ├── outline.sublime-settings ├── screenshot.png └── show.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Michael Kleehammer and Allen Bargi 4 | Copyright (c) 2016 Xiongbing Jin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "Outline", 16 | "children": 17 | [ 18 | { 19 | "command": "edit_settings", 20 | "args": { 21 | "base_file": "${packages}/Outline/outline.sublime-settings", 22 | "default": "{\n\t$0\n}\n" 23 | }, 24 | "caption": "Settings" 25 | }, 26 | { "caption": "-" }, 27 | { 28 | "command": "open_file", 29 | "args": {"file": "${packages}/Outline/outline.sublime-settings"}, 30 | "caption": "Settings – Default" 31 | }, 32 | { 33 | "command": "open_file", 34 | "args": {"file": "${packages}/User/outline.sublime-settings"}, 35 | "caption": "Settings – User" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Outline for Sublime Text 3 2 | 3 | ### Overview 4 | 5 | Inspired by [FileBrowser](https://github.com/aziz/SublimeFileBrowser), this package shows the outline of your file, class/function name list of your code, or table of content/toc of your markdown/LaTeX document in a sidebar-style tab. 6 | 7 | ![Screenshot](screenshot.png?raw=true "Screenshot") 8 | 9 | ### Installation 10 | 11 | #### Install via Package Control 12 | 13 | This package is available on [Package Control](https://packagecontrol.io/). Search for `Outline`. 14 | 15 | #### Manual installation 16 | 17 | 1. Clone or download this repository to your hard drive using the green `Clone or download` button 18 | 2. Rename the cloned or extracted folder to `Outline`. Make sure `outline.py` is at the root of the `Outline` folder. 19 | 3. Move the `Outline` folder to your Sublime Text's `Packages` folder. To find the `Packages` folder, click menu `Preferences` > `Browse Packages`. 20 | 4. Restart Sublime Text, and press `Ctrl + Shift + P` to select your preferred layout (`Browse Mode`) 21 | 22 | ### Default layout 23 | 24 | The outline tab can be set as a sidebar on the left or right. Press `Ctrl + Shift + P` and select either `Browse Mode: Outline (Left)` or `Browse Mode: Outline (Right)` to set your preferred layout. 25 | 26 | If you also use [FileBrowser](https://github.com/aziz/SublimeFileBrowser), you can use both in three different layouts: 27 | 28 | * `FileBrowser` left, `Outline` right 29 | * `FileBrowser` top left, `Outline` bottom left 30 | * `FileBrowser` top right, `Outline` bottom right 31 | 32 | To use `FileBrowser` and `Outline` together, please close the `FileBrowser` sidebar first and then use the correponding `Browse Mode` command to set the layout, otherwise the `Outline` view may not work as intended. 33 | 34 | ### Color theme 35 | 36 | `Outline` can be configured to use the current editor color scheme. To do so, change the setting below: 37 | 38 | ```json 39 | "outline_inherit_color_scheme": true 40 | ``` 41 | 42 | `Outline` has two built-in color themes: Bright (default theme) and Dark. To switch to the Dark theme, add the following to your user settings file. Open the user settings file by "Preferences > Package Settings > Outline > Settings" (Sublime Text version 3124 or later), or "Settings - User": 43 | 44 | ```json 45 | "outline_inherit_color_scheme": false 46 | "color_scheme": "Packages/Outline/outline-Dark.hidden-tmTheme" 47 | ``` 48 | 49 | Remove `-Dark` or remove the entire line to return to the bright theme. To customize your own color theme, see [this issue](https://github.com/warmdev/SublimeOutline/issues/1). 50 | 51 | ### Outline content and indentation 52 | 53 | Outline is updated when you save a file or switch between files. 54 | 55 | Content and indentation in the `Outline` tab is controlled by the `Symbol List.tmPreferences` file (file name may differ) corresponding to the syntax of your file. 56 | 57 | ### Known issue 58 | 59 | * This package may not work if you use a multi-column/row layout. 60 | 61 | ### License 62 | 63 | This plugin is licensed under the MIT license. 64 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | '''Common stuff, used in other modules''' 4 | 5 | from __future__ import print_function 6 | import re, os, fnmatch, sys, itertools 7 | import sublime 8 | from sublime import Region 9 | from os.path import isdir, join, basename 10 | 11 | if sublime.platform() == 'windows': 12 | import ctypes 13 | 14 | ST3 = int(sublime.version()) >= 3000 15 | 16 | if ST3: 17 | MARK_OPTIONS = sublime.DRAW_NO_OUTLINE 18 | else: 19 | MARK_OPTIONS = 0 20 | 21 | OS = sublime.platform() 22 | NT = OS == 'windows' 23 | LIN = OS == 'linux' 24 | OSX = OS == 'osx' 25 | RE_FILE = re.compile(r'^(\s*)([^\\//].*)$') 26 | PARENT_SYM = u"⠤" 27 | 28 | 29 | def first(seq, pred): 30 | '''similar to built-in any() but return the object instead of boolean''' 31 | return next((item for item in seq if pred(item)), None) 32 | 33 | 34 | def sort_nicely(names): 35 | """ Sort the given list in the way that humans expect. 36 | Source: http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html 37 | """ 38 | convert = lambda text: int(text) if text.isdigit() else text.lower() 39 | alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] 40 | names.sort(key=alphanum_key) 41 | 42 | 43 | def print(*args, **kwargs): 44 | """ Redefine print() function; the reason is the inconsistent treatment of 45 | unicode literals among Python versions used in ST2. 46 | Redefining/tweaking built-in things is relatively safe; of course, when 47 | ST2 will become irrelevant, this def might be removed undoubtedly. 48 | """ 49 | if not (ST3 or NT): 50 | args = (s.encode('utf-8') if isinstance(s, unicode) else str(s) for s in args) 51 | else: 52 | args = (s if isinstance(s, str if ST3 else unicode) else str(s) for s in args) 53 | sep, end = kwargs.get('sep', ' '), kwargs.get('end', '\n') 54 | sys.stdout.write(sep.join(s for s in args) + end) 55 | 56 | 57 | def set_proper_scheme(view): 58 | ''' 59 | this is callback, it is not meant to be called directly 60 | view.settings().add_on_change('color_scheme', lambda: set_proper_scheme(view)) 61 | set once, right after view is created 62 | _note_, color_scheme must not be set directly, but in a setting file 63 | ''' 64 | # Since we cannot create file with syntax, there is moment when view has no settings, 65 | # but it is activated, so some plugins (e.g. Color Highlighter) set wrong color scheme 66 | if view.settings().get('outline_rename_mode', False): 67 | outline_settings = sublime.load_settings('outline-rename-mode.sublime-settings') 68 | else: 69 | outline_settings = sublime.load_settings('outline.sublime-settings') 70 | 71 | if view.settings().get('color_scheme') == outline_settings.get('color_scheme'): 72 | return 73 | 74 | view.settings().set('color_scheme', outline_settings.get('color_scheme')) 75 | 76 | 77 | def calc_width(view): 78 | ''' 79 | return float width, which must be 80 | 0.0 < width < 1.0 (other values acceptable, but cause unfriendly layout) 81 | used in show.show() and "outline_select" command with other_group=True 82 | ''' 83 | width = view.settings().get('outline_width', 0.3) 84 | if isinstance(width, float): 85 | width -= width//1 # must be less than 1 86 | elif isinstance(width, int if ST3 else long): # assume it is pixels 87 | wport = view.viewport_extent()[0] 88 | width = 1 - round((wport - width) / wport, 2) 89 | if width >= 1: 90 | width = 0.9 91 | else: 92 | sublime.error_message(u'FileBrowser:\n\noutline_width set to ' 93 | u'unacceptable type "%s", please change it.\n\n' 94 | u'Fallback to default 0.3 for now.' % type(width)) 95 | width = 0.3 96 | return width or 0.1 # avoid 0.0 97 | 98 | 99 | def get_group(groups, nag): 100 | ''' 101 | groups amount of groups in window 102 | nag number of active group 103 | return number of neighbour group 104 | ''' 105 | if groups <= 4 and nag < 2: 106 | group = 1 if nag == 0 else 0 107 | elif groups == 4 and nag >= 2: 108 | group = 3 if nag == 2 else 2 109 | else: 110 | group = nag - 1 111 | return group 112 | 113 | 114 | def relative_path(rpath): 115 | u'''rpath is either list or empty string (if list, we need only first item); 116 | return either empty string or rpath[0] (or its parent), e.g. 117 | foo/bar/ → foo/bar/ 118 | foo/bar → foo/ 119 | ''' 120 | if rpath: 121 | rpath = rpath[0] 122 | if rpath[~0] != os.sep: 123 | rpath = os.path.split(rpath)[0] + os.sep 124 | if rpath == os.sep: 125 | rpath = '' 126 | return rpath 127 | 128 | 129 | def hijack_window(): 130 | '''Execute on loading plugin or on new window open; 131 | allow to open FB automatically in ST3 132 | ''' 133 | settings = sublime.load_settings('outline.sublime-settings') 134 | command = settings.get("outline_hijack_new_window") 135 | if command: 136 | if command == "jump_list": 137 | sublime.set_timeout(lambda: sublime.windows()[-1].run_command("outline_jump_list"), 1) 138 | else: 139 | sublime.set_timeout(lambda: sublime.windows()[-1].run_command("outline", {"immediate": True}), 1) 140 | 141 | 142 | class outlineBaseCommand: 143 | """ 144 | Convenience functions for outline TextCommands 145 | """ 146 | @property 147 | def path(self): 148 | return self.view.settings().get('outline_path') 149 | 150 | def get_path(self): 151 | path = self.path 152 | if path == 'ThisPC\\': 153 | path = '' 154 | return path 155 | 156 | def filecount(self): 157 | """ 158 | Returns the number of files and directories in the view. 159 | """ 160 | return self.view.settings().get('outline_count', 0) 161 | 162 | def move_to_extreme(self, extreme="bof"): 163 | """ 164 | Moves the cursor to the beginning or end of file list. Clears all sections. 165 | """ 166 | files = self.fileregion(with_parent_link=True) 167 | self.view.sel().clear() 168 | if extreme == "bof": 169 | ext_region = Region(files.a, files.a) 170 | else: 171 | name_point = self.view.extract_scope(self.view.line(files.b).a + 2).a 172 | ext_region = Region(name_point, name_point) 173 | self.view.sel().add(ext_region) 174 | self.view.show_at_center(ext_region) 175 | 176 | def move(self, forward=None): 177 | """ 178 | Moves the cursor one line forward or backwards. Clears all sections. 179 | """ 180 | assert forward in (True, False), 'forward must be set to True or False' 181 | 182 | files = self.fileregion(with_parent_link=True) 183 | if not files: 184 | return 185 | 186 | new_sels = [] 187 | for s in list(self.view.sel()): 188 | new_sels.append(self._get_name_point(self.next_line(forward, s.a, files))) 189 | 190 | self.view.sel().clear() 191 | for n in new_sels: 192 | self.view.sel().add(Region(n, n)) 193 | name_point = new_sels[~0] if forward else new_sels[0] 194 | surroundings = True if self.view.rowcol(name_point)[0] < 3 else False 195 | self.view.show(name_point, surroundings) 196 | 197 | def next_line(self, forward, pt, filergn): 198 | '''Return Region of line for pt within filergn''' 199 | if filergn.contains(pt): 200 | # Try moving by one line. 201 | line = self.view.line(pt) 202 | pt = forward and (line.b + 1) or (line.a - 1) 203 | if not filergn.contains(pt): 204 | # Not (or no longer) in the list of files, so move to the closest edge. 205 | pt = (pt > filergn.b) and filergn.b or filergn.a 206 | return self.view.line(pt) 207 | 208 | def _get_name_point(self, line): 209 | '''Return point at which filename starts (i.e. after icon & whitspace)''' 210 | scope = self.view.scope_name(line.a) 211 | if 'indent' in scope: 212 | name_point = self.view.extract_scope(line.a).b 213 | else: 214 | name_point = line.a + (2 if not 'parent_dir' in scope else 0) 215 | return name_point 216 | 217 | def show_parent(self): 218 | return self.view.settings().get('outline_show_parent', False) 219 | 220 | def fileregion(self, with_parent_link=False): 221 | """ 222 | Returns a region containing the lines containing filenames. 223 | If there are no filenames None is returned. 224 | """ 225 | if with_parent_link: 226 | all_items = sorted(self.view.find_by_selector('outline.item')) 227 | else: 228 | all_items = sorted(self.view.find_by_selector('outline.item.directory') + 229 | self.view.find_by_selector('outline.item.file')) 230 | if not all_items: 231 | return None 232 | return Region(all_items[0].a, all_items[~0].b) 233 | 234 | def get_parent(self, line, path): 235 | u''' 236 | Returns relative path for line 237 | • line is a region 238 | • path is self.path 239 | • self.index is list stored in view settings as 'outline_index' 240 | ''' 241 | return self.get_fullpath_for(line).replace(path, '', 1) 242 | 243 | def get_fullpath_for(self, line): 244 | return self.index[self.view.rowcol(line.a)[0]] 245 | 246 | def get_all(self): 247 | """ 248 | Returns a list of all filenames in the view. 249 | outline_index is always supposed to represent current state of view, 250 | each item matches corresponding line, thus list will never be empty unless sth went wrong; 251 | if header is enabled then first two elements are empty strings 252 | """ 253 | index = self.view.settings().get('outline_index', []) 254 | if not index: 255 | return sublime.error_message(u'FileBrowser:\n\n"outline_index" is empty,\n' 256 | u'that shouldn’t happen ever, there is some bug.') 257 | return index 258 | 259 | def get_all_relative(self, path): 260 | return [f.replace(path, '', 1) for f in self.get_all()] 261 | 262 | def get_selected(self, parent=True, full=False): 263 | """ 264 | parent 265 | if False, returned list does not contain PARENT_SYM even if it is in view 266 | full 267 | if True, items in returned list are full paths, else relative 268 | 269 | Returns a list of selected filenames. 270 | self.index should be assigned before call it 271 | """ 272 | fileregion = self.fileregion(with_parent_link=parent) 273 | if not fileregion: 274 | return None 275 | path = self.get_path() 276 | names = [] 277 | for line in self._get_lines([s for s in self.view.sel()], fileregion): 278 | text = self.get_fullpath_for(line) if full else self.get_parent(line, path) 279 | if text and text not in names: 280 | names.append(text) 281 | return names 282 | 283 | def get_marked(self, full=False): 284 | '''self.index should be assigned before call it''' 285 | if not self.filecount(): 286 | return [] 287 | path = self.get_path() 288 | names = [] 289 | for line in self.view.get_regions('marked'): 290 | text = self.get_fullpath_for(line) if full else self.get_parent(line, path) 291 | if text and text not in names: 292 | names.append(text) 293 | return names 294 | 295 | def _mark(self, mark, regions): 296 | """ 297 | Marks the requested files. 298 | 299 | mark 300 | True, False, or a function with signature `func(oldmark, filename)`. 301 | The function should return True or False. 302 | 303 | regions 304 | List of region(s). Only files within the region will be modified. 305 | """ 306 | filergn = self.fileregion() 307 | if not filergn: 308 | return 309 | 310 | self.index = self.get_all() 311 | # We can't update regions for a key, only replace, so we need to record the existing marks. 312 | marked = dict((self.get_fullpath_for(r), r) for r in self.view.get_regions('marked') if not r.empty()) 313 | 314 | for line in self._get_lines(regions, filergn): 315 | filename = self.get_fullpath_for(line) 316 | 317 | if mark not in (True, False): 318 | newmark = mark(filename in marked, filename) 319 | assert newmark in (True, False), u'Invalid mark: {0}'.format(newmark) 320 | else: 321 | newmark = mark 322 | 323 | if newmark: 324 | name_point = self._get_name_point(line) 325 | marked[filename] = Region(name_point, line.b) 326 | else: 327 | marked.pop(filename, None) 328 | 329 | if marked: 330 | r = sorted(list(marked.values()), key=lambda region: region.a) 331 | self.view.add_regions('marked', r, 'outline.marked', '', MARK_OPTIONS) 332 | else: 333 | self.view.erase_regions('marked') 334 | 335 | def _get_lines(self, regions, within): 336 | ''' 337 | regions is a list of non-overlapping region(s), each may have many lines 338 | within is a region which is supposed to contain each line 339 | ''' 340 | return (line for line in itertools.chain(*(self.view.lines(r) for r in regions)) if within.contains(line)) 341 | 342 | def set_ui_in_rename_mode(self, edit): 343 | header = self.view.settings().get('outline_header', False) 344 | if header: 345 | regions = self.view.find_by_selector('text.outline header.outline punctuation.definition.separator.outline') 346 | else: 347 | regions = self.view.find_by_selector('text.outline outline.item.parent_dir') 348 | if not regions: 349 | return 350 | region = regions[0] 351 | start = region.begin() 352 | self.view.erase(edit, region) 353 | if header: 354 | new_text = u"——[RENAME MODE]——" + u"—"*(region.size()-17) 355 | else: 356 | new_text = u"⠤ [RENAME MODE]" 357 | self.view.insert(edit, start, new_text) 358 | 359 | def set_status(self): 360 | '''Update status-bar; 361 | self.show_hidden must be assigned before call it''' 362 | # if view isnot focused, view.window() may be None 363 | window = self.view.window() or sublime.active_window() 364 | path_in_project = any(folder == self.path[:-1] for folder in window.folders()) 365 | settings = self.view.settings() 366 | copied_items = settings.get('outline_to_copy', []) 367 | cut_items = settings.get('outline_to_move', []) 368 | status = u" 𝌆 [?: Help] {0}Hidden: {1}{2}{3}".format( 369 | 'Project root, ' if path_in_project else '', 370 | 'On' if self.show_hidden else 'Off', 371 | ', copied(%d)' % len(copied_items) if copied_items else '', 372 | ', cut(%d)' % len(cut_items) if cut_items else '' 373 | ) 374 | self.view.set_status("__FileBrowser__", status) 375 | 376 | def prepare_filelist(self, names, path, goto, indent): 377 | '''About self.index see outlineRefreshCommand 378 | could be called from outlineExpand.expand_single_folder 379 | or from outlineRefresh.continue_refresh 380 | ''' 381 | items = [] 382 | tab = self.view.settings().get('tab_size') 383 | line = self.view.line(self.sel.a if self.sel is not None else self.view.sel()[0].a) 384 | content = self.view.substr(line).replace('\t', ' '*tab) 385 | ind = re.compile('^(\s*)').match(content).group(1) 386 | level = indent * int((len(ind) / tab) + 1) if ind else indent 387 | files = [] 388 | index_dirs = [] 389 | index_files = [] 390 | for name in names: 391 | full_name = join(path, goto, name) 392 | if isdir(full_name): 393 | index_dirs.append(u'%s%s' % (full_name, os.sep)) 394 | items.append(''.join([level, u"▸ ", name, os.sep])) 395 | else: 396 | index_files.append(full_name) 397 | files.append(''.join([level, u"≡ ", name])) 398 | index = index_dirs + index_files 399 | self.index = self.index[:self.number_line] + index + self.index[self.number_line:] 400 | items += files 401 | return items 402 | 403 | def is_hidden(self, filename, path, goto=''): 404 | if not (path or goto): # special case for ThisPC 405 | return False 406 | tests = self.view.settings().get('outline_hidden_files_patterns', ['.*']) 407 | if isinstance(tests, str): 408 | tests = [tests] 409 | if any(fnmatch.fnmatch(filename, pattern) for pattern in tests): 410 | return True 411 | if sublime.platform() != 'windows': 412 | return False 413 | # check for attribute on windows: 414 | try: 415 | attrs = ctypes.windll.kernel32.GetFileAttributesW(join(path, goto, filename)) 416 | assert attrs != -1 417 | result = bool(attrs & 2) 418 | except (AttributeError, AssertionError): 419 | result = False 420 | return result 421 | 422 | def try_listing_directory(self, path): 423 | '''Return tuple of two element 424 | items sorted list of filenames in path, or empty list 425 | error exception message, or empty string 426 | ''' 427 | items, error = [], '' 428 | try: 429 | if not self.show_hidden: 430 | items = [name for name in os.listdir(path) if not self.is_hidden(name, path)] 431 | else: 432 | items = os.listdir(path) 433 | except OSError as e: 434 | error = str(e) 435 | if NT: 436 | error = error.split(':')[0].replace('[Error 5] ', 'Access denied').replace('[Error 3] ', 'Not exists, press r to refresh') 437 | if not ST3 and LIN: 438 | error = error.decode('utf8') 439 | else: 440 | sort_nicely(items) 441 | finally: 442 | return items, error 443 | 444 | def try_listing_only_dirs(self, path): 445 | '''Same as self.try_listing_directory, but items contains only directories. 446 | Used for prompt completion''' 447 | items, error = self.try_listing_directory(path) 448 | if items: 449 | items = [n for n in items if isdir(join(path, n))] 450 | return (items, error) 451 | 452 | def restore_marks(self, marked=None): 453 | if marked: 454 | # Even if we have the same filenames, they may have moved so we have to manually 455 | # find them again. 456 | path = self.get_path() 457 | regions = [] 458 | for mark in marked: 459 | matches = self._find_in_view(mark) 460 | for region in matches: 461 | filename = self.get_parent(region, path) 462 | if filename == mark: 463 | regions.append(region) 464 | # if it is found, no need to check other mathes, so break 465 | break 466 | self._mark(mark=True, regions=regions) 467 | else: 468 | self.view.erase_regions('marked') 469 | 470 | def restore_sels(self, sels=None): 471 | ''' 472 | sels is tuple of two elements: 473 | 0 list of filenames 474 | relative paths to search in the view 475 | 1 list of Regions 476 | copy of view.sel(), used for fallback if filenames are not found 477 | in view (e.g. user deleted selected file) 478 | ''' 479 | if sels: 480 | seled_fnames, seled_regions = sels 481 | path = self.get_path() 482 | regions = [] 483 | for selection in seled_fnames: 484 | matches = self._find_in_view(selection) 485 | for region in matches: 486 | filename = self.get_parent(region, path) 487 | if filename == selection: 488 | name_point = self._get_name_point(region) 489 | regions.append(Region(name_point, name_point)) 490 | break 491 | if regions: 492 | return self._add_sels(regions) 493 | else: 494 | # e.g. when user remove file(s), we just restore sel RegionSet 495 | # despite positions may be wrong sometimes 496 | return self._add_sels(seled_regions) 497 | # fallback: 498 | return self._add_sels() 499 | 500 | def _find_in_view(self, item): 501 | '''item is Unicode''' 502 | fname = re.escape(basename(os.path.abspath(item)) or item.rstrip(os.sep)) 503 | if item[~0] == os.sep: 504 | pattern = u'^\s*[▸▾] ' 505 | sep = re.escape(os.sep) 506 | else: 507 | pattern = u'^\s*≡ ' 508 | sep = '' 509 | return self.view.find_all(u'%s%s%s' % (pattern, fname, sep)) 510 | 511 | def _add_sels(self, sels=None): 512 | self.view.sel().clear() 513 | 514 | if sels: 515 | eof = self.view.size() 516 | for s in sels: 517 | if s.begin() <= eof: 518 | self.view.sel().add(s) 519 | 520 | if not sels or not list(self.view.sel()): # all sels more than eof 521 | item = (self.view.find_by_selector('text.outline outline.item.parent_dir ') or 522 | self.view.find_by_selector('text.outline outline.item.directory string.name.directory.outline ') or 523 | self.view.find_by_selector('text.outline outline.item.file string.name.file.outline ')) 524 | s = Region(item[0].a, item[0].a) if item else Region(0, 0) 525 | self.view.sel().add(s) 526 | 527 | self.view.show_at_center(s) 528 | 529 | def display_path(self, folder): 530 | display = folder 531 | home = os.path.expanduser("~") 532 | if folder.startswith(home): 533 | display = folder.replace(home, "~", 1) 534 | return display 535 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "0.1.1": "messages/0.1.1.txt", 4 | "0.1.2": "messages/0.1.2.txt", 5 | "0.1.3": "messages/0.1.3.txt", 6 | "0.1.4": "messages/0.1.4.txt", 7 | "0.1.5": "messages/0.1.5.txt", 8 | "0.1.6": "messages/0.1.6.txt" 9 | } 10 | -------------------------------------------------------------------------------- /messages/0.1.1.txt: -------------------------------------------------------------------------------- 1 | 0.1.1 2 | 3 | * Added package settings menu items (Preferences > Package Settings > Outline) 4 | * Added dark theme. To enable the dark theme, add the following line to your package user settings file. Open the user settings file by "Preferences > Package Settings > Outline > Settings" (Sublime Text version 3124 or later), or "Settings - User" 5 | 6 | ```json 7 | "color_scheme": "Packages/Outline/outline-Dark.hidden-tmTheme" 8 | ``` -------------------------------------------------------------------------------- /messages/0.1.2.txt: -------------------------------------------------------------------------------- 1 | 0.1.2 2 | 3 | * Added ability to sort outline alphabetically by setting or by command 4 | - By setting: add the following line in settings and change the value to `true`: 5 | 6 | ```json 7 | "outline_alphabetical": true, 8 | ``` 9 | 10 | - By command: press `Ctrl + Shift + P` and select `Outline: Toggle Sort` 11 | -------------------------------------------------------------------------------- /messages/0.1.3.txt: -------------------------------------------------------------------------------- 1 | 0.1.3 2 | 3 | * Added ability to keep outline in sync with current file position (outline view will scroll to current symbol and have it highlighted). Note this does not work with alphabetical sort. 4 | 5 | * Added setting to turn on/off the syncing feature (default is on) 6 | 7 | ```json 8 | "outline_sync": true, 9 | ``` 10 | -------------------------------------------------------------------------------- /messages/0.1.4.txt: -------------------------------------------------------------------------------- 1 | 0.1.4 2 | 3 | * Fixed issue where after closing outline sidebar, focus is on an empty tab 4 | 5 | * When clicking on a symbol item in the outline pane, highlight in the main view depends on the setting `outline_main_view_highlight_mode`: 6 | 7 | * `cursor`: no highlight, put cursor at the end of the symbol line 8 | * `symbol`: highlight the current symbol (same as previous implementation) 9 | * `block`: highlight the entire symbol block (i.e. between current and next symbol) 10 | -------------------------------------------------------------------------------- /messages/0.1.5.txt: -------------------------------------------------------------------------------- 1 | 0.1.5 2 | 3 | * Added option to use editor color scheme for outline. Change setting `outline_inherit_color_scheme` to `true`. Close and reopen outline tab for the change to take effect. If you change the editor color scheme, close and reopen outline tab for the new color scheme to take effect. 4 | -------------------------------------------------------------------------------- /messages/0.1.6.txt: -------------------------------------------------------------------------------- 1 | 0.1.6 2 | 3 | * Made the width of the outline sidebar user customizable. Use `outline_width` setting to change the width (default 0.2, i.e. 20% of the entire window width) 4 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | Thank you for installing the Outline package. 2 | 3 | To start, bring up the Command Palette with `Ctrl + Shift + P`, and select `Browse Mode: Outline (Left)` or another preferred layout. 4 | 5 | Please visit https://github.com/warmdev/SublimeOutline for more details. -------------------------------------------------------------------------------- /outline-Dark.hidden-tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Browse 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #272822 14 | foreground 15 | #222222 16 | caret 17 | #367cc055 18 | lineHighlight 19 | #367cc055 20 | gutter 21 | #272822 22 | invisibles 23 | #88222233 24 | selection 25 | #22222233 26 | selectionBorder 27 | #22222233 28 | inactiveSelection 29 | #22222219 30 | shadow 31 | #272822 32 | shadowWidth 33 | 16 34 | 35 | 36 | 37 | 38 | 39 | name 40 | Project Symbols > Star 41 | scope 42 | punctuation.definition.projects.star.symbol.outline 43 | settings 44 | 45 | foreground 46 | #7c8692 47 | 48 | 49 | 50 | name 51 | Project Symbols > Chevron 52 | scope 53 | punctuation.definition.projects.chevron.symbol.outline 54 | settings 55 | 56 | foreground 57 | #7c869266 58 | 59 | 60 | 61 | 62 | name 63 | Project Path 64 | scope 65 | string.name.project_path.outline 66 | settings 67 | 68 | foreground 69 | #707e8b 70 | 71 | 72 | 73 | 74 | name 75 | File/Dir Symbols 76 | scope 77 | punctuation.definition.directory.symbol.outline, punctuation.definition.file.symbol.outline, outline.item.parent_dir 78 | settings 79 | 80 | foreground 81 | #7c8692 82 | 83 | 84 | 85 | 86 | name 87 | Dir Slash 88 | scope 89 | punctuation.definition.directory.slash.outline 90 | settings 91 | 92 | foreground 93 | #919aa388 94 | 95 | 96 | 97 | 98 | name 99 | File Extension 100 | scope 101 | string.name.file.extension.outline 102 | settings 103 | 104 | foreground 105 | #919aa3 106 | 107 | 108 | 109 | 110 | name 111 | Dir Name 112 | scope 113 | string.name.directory.outline, string.name.project.outline 114 | settings 115 | 116 | fontStyle 117 | bold 118 | 119 | 120 | 121 | 122 | name 123 | Header 124 | scope 125 | header.outline 126 | settings 127 | 128 | foreground 129 | #f8f8f2 130 | 131 | 132 | 133 | 134 | name 135 | Header punctuations 136 | scope 137 | header.outline punctuation.definition.separator.outline 138 | settings 139 | 140 | foreground 141 | #6f798733 142 | 143 | 144 | 145 | 146 | name 147 | Header Rename Mode Title 148 | scope 149 | punctuation.definition.rename_mode 150 | settings 151 | 152 | foreground 153 | #E99023 154 | 155 | 156 | 157 | 158 | name 159 | Marked 160 | scope 161 | outline.marked 162 | settings 163 | 164 | foreground 165 | #FFFFFF 166 | background 167 | #4b96d6 168 | 169 | 170 | 171 | 172 | 173 | name 174 | Help: Table Name 175 | scope 176 | outline.help.header 177 | settings 178 | 179 | foreground 180 | #707E8B 181 | fontStyle 182 | bold 183 | 184 | 185 | 186 | 187 | name 188 | Help: Table Header 189 | scope 190 | outline.table.th 191 | settings 192 | 193 | background 194 | #4B96D6 195 | 196 | 197 | 198 | 199 | name 200 | Help: Table Header > Title 201 | scope 202 | outline.table.th title 203 | settings 204 | 205 | foreground 206 | #FFFFFF 207 | 208 | 209 | 210 | 211 | name 212 | Help: Table Header 213 | scope 214 | outline.table.tr 215 | settings 216 | 217 | background 218 | #FFFFFF 219 | 220 | 221 | 222 | 223 | name 224 | Help: Table punctuations 225 | scope 226 | outline.table.punctuations 227 | settings 228 | 229 | foreground 230 | #d9dee5 231 | background 232 | #d9dee5 233 | 234 | 235 | 236 | 237 | name 238 | Help: Shortcut 239 | scope 240 | outline.table.tr command_shortcut 241 | settings 242 | 243 | foreground 244 | #D6774B 245 | fontStyle 246 | bold 247 | 248 | 249 | 250 | 251 | name 252 | Help: Command Description 253 | scope 254 | outline.table.tr command_desc 255 | settings 256 | 257 | foreground 258 | #000000 259 | 260 | 261 | 262 | 263 | 264 | name 265 | Error 266 | scope 267 | string.error.outline 268 | settings 269 | 270 | foreground 271 | #aa0000 272 | 273 | 274 | 275 | 276 | name 277 | VCS Modified 278 | scope 279 | item.modified.outline 280 | settings 281 | 282 | background 283 | #d9dee0 284 | foreground 285 | #F09609 286 | 287 | 288 | 289 | 290 | name 291 | VCS Untracked 292 | scope 293 | item.untracked.outline 294 | settings 295 | 296 | background 297 | #d9dee0 298 | foreground 299 | #00c406 300 | 301 | 302 | 303 | 304 | name 305 | VCS Colorblind 306 | scope 307 | item.colorblind.outline 308 | settings 309 | 310 | background 311 | #000000 312 | 313 | 314 | 315 | 316 | uuid 317 | 07379361-ce49-4e6c-a03f-26378a7a2131 318 | 319 | 320 | -------------------------------------------------------------------------------- /outline.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | outline 7 | scopeName 8 | text.outline 9 | uuid 10 | ed69e186-9ee2-11e5-8994-feff819cdc9f 11 | fileTypes 12 | 13 | outline 14 | 15 | patterns 16 | 17 | 18 | 19 | match 20 | ^(\s*)([▸▾] )([^\\/]*)(\\|/)?(.*)?$ 21 | name 22 | outline.item.directory 23 | captures 24 | 25 | 1 26 | 27 | name 28 | indent 29 | 30 | 2 31 | 32 | name 33 | punctuation.definition.directory.symbol.outline 34 | 35 | 3 36 | 37 | name 38 | string.name.directory.outline 39 | 40 | 4 41 | 42 | name 43 | punctuation.definition.directory.slash.outline 44 | 45 | 5 46 | 47 | name 48 | string.error.outline 49 | 50 | 51 | 52 | 53 | 54 | match 55 | ^(\s*)(≡ )(\S.*?(\.[^\.\n]+)?)$ 56 | name 57 | outline.item.file 58 | captures 59 | 60 | 1 61 | 62 | name 63 | indent 64 | 65 | 2 66 | 67 | name 68 | punctuation.definition.file.symbol.outline 69 | 70 | 3 71 | 72 | name 73 | string.name.file.outline 74 | 75 | 4 76 | 77 | name 78 | string.name.file.extension.outline 79 | 80 | 81 | 82 | 83 | 84 | match 85 | ^⠤(\s*\[.+\]){0,1}$ 86 | name 87 | outline.item.parent_dir 88 | captures 89 | 90 | 1 91 | 92 | name 93 | punctuation.definition.rename_mode.outline 94 | 95 | 96 | 97 | 98 | 99 | begin 100 | (\S(.+)?$) 101 | end 102 | ^(—+)(\[RENAME MODE\]){0,1}(—*)\n 103 | name 104 | header.outline 105 | endCaptures 106 | 107 | 1 108 | 109 | name 110 | punctuation.definition.separator.outline 111 | 112 | 2 113 | 114 | name 115 | punctuation.definition.rename_mode.outline 116 | 117 | 3 118 | 119 | name 120 | punctuation.definition.separator.outline 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /outline.hidden-tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Browse 7 | settings 8 | 9 | 10 | settings 11 | 12 | background 13 | #d9dee5 14 | foreground 15 | #222222 16 | caret 17 | #367cc055 18 | lineHighlight 19 | #367cc055 20 | gutter 21 | #d9dee5 22 | invisibles 23 | #88222233 24 | selection 25 | #22222233 26 | selectionBorder 27 | #22222233 28 | inactiveSelection 29 | #22222219 30 | shadow 31 | #d9dee5 32 | shadowWidth 33 | 16 34 | 35 | 36 | 37 | 38 | 39 | name 40 | Project Symbols > Star 41 | scope 42 | punctuation.definition.projects.star.symbol.outline 43 | settings 44 | 45 | foreground 46 | #7c8692 47 | 48 | 49 | 50 | name 51 | Project Symbols > Chevron 52 | scope 53 | punctuation.definition.projects.chevron.symbol.outline 54 | settings 55 | 56 | foreground 57 | #7c869266 58 | 59 | 60 | 61 | 62 | name 63 | Project Path 64 | scope 65 | string.name.project_path.outline 66 | settings 67 | 68 | foreground 69 | #707e8b 70 | 71 | 72 | 73 | 74 | name 75 | File/Dir Symbols 76 | scope 77 | punctuation.definition.directory.symbol.outline, punctuation.definition.file.symbol.outline, outline.item.parent_dir 78 | settings 79 | 80 | foreground 81 | #7c8692 82 | 83 | 84 | 85 | 86 | name 87 | Dir Slash 88 | scope 89 | punctuation.definition.directory.slash.outline 90 | settings 91 | 92 | foreground 93 | #919aa388 94 | 95 | 96 | 97 | 98 | name 99 | File Extension 100 | scope 101 | string.name.file.extension.outline 102 | settings 103 | 104 | foreground 105 | #919aa3 106 | 107 | 108 | 109 | 110 | name 111 | Dir Name 112 | scope 113 | string.name.directory.outline, string.name.project.outline 114 | settings 115 | 116 | fontStyle 117 | bold 118 | 119 | 120 | 121 | 122 | name 123 | Header 124 | scope 125 | header.outline 126 | settings 127 | 128 | foreground 129 | #707e8b 130 | 131 | 132 | 133 | 134 | name 135 | Header punctuations 136 | scope 137 | header.outline punctuation.definition.separator.outline 138 | settings 139 | 140 | foreground 141 | #6f798733 142 | 143 | 144 | 145 | 146 | name 147 | Header Rename Mode Title 148 | scope 149 | punctuation.definition.rename_mode 150 | settings 151 | 152 | foreground 153 | #E99023 154 | 155 | 156 | 157 | 158 | name 159 | Marked 160 | scope 161 | outline.marked 162 | settings 163 | 164 | foreground 165 | #FFFFFF 166 | background 167 | #4b96d6 168 | 169 | 170 | 171 | 172 | 173 | name 174 | Help: Table Name 175 | scope 176 | outline.help.header 177 | settings 178 | 179 | foreground 180 | #707E8B 181 | fontStyle 182 | bold 183 | 184 | 185 | 186 | 187 | name 188 | Help: Table Header 189 | scope 190 | outline.table.th 191 | settings 192 | 193 | background 194 | #4B96D6 195 | 196 | 197 | 198 | 199 | name 200 | Help: Table Header > Title 201 | scope 202 | outline.table.th title 203 | settings 204 | 205 | foreground 206 | #FFFFFF 207 | 208 | 209 | 210 | 211 | name 212 | Help: Table Header 213 | scope 214 | outline.table.tr 215 | settings 216 | 217 | background 218 | #FFFFFF 219 | 220 | 221 | 222 | 223 | name 224 | Help: Table punctuations 225 | scope 226 | outline.table.punctuations 227 | settings 228 | 229 | foreground 230 | #d9dee5 231 | background 232 | #d9dee5 233 | 234 | 235 | 236 | 237 | name 238 | Help: Shortcut 239 | scope 240 | outline.table.tr command_shortcut 241 | settings 242 | 243 | foreground 244 | #D6774B 245 | fontStyle 246 | bold 247 | 248 | 249 | 250 | 251 | name 252 | Help: Command Description 253 | scope 254 | outline.table.tr command_desc 255 | settings 256 | 257 | foreground 258 | #000000 259 | 260 | 261 | 262 | 263 | 264 | name 265 | Error 266 | scope 267 | string.error.outline 268 | settings 269 | 270 | foreground 271 | #aa0000 272 | 273 | 274 | 275 | 276 | name 277 | VCS Modified 278 | scope 279 | item.modified.outline 280 | settings 281 | 282 | background 283 | #d9dee0 284 | foreground 285 | #F09609 286 | 287 | 288 | 289 | 290 | name 291 | VCS Untracked 292 | scope 293 | item.untracked.outline 294 | settings 295 | 296 | background 297 | #d9dee0 298 | foreground 299 | #00c406 300 | 301 | 302 | 303 | 304 | name 305 | VCS Colorblind 306 | scope 307 | item.colorblind.outline 308 | settings 309 | 310 | background 311 | #000000 312 | 313 | 314 | 315 | 316 | uuid 317 | 07379361-ce49-4e6c-a03f-26378a7a2131 318 | 319 | 320 | -------------------------------------------------------------------------------- /outline.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime import Region 3 | from sublime_plugin import WindowCommand, TextCommand, EventListener 4 | from .show import show, refresh_sym_view, get_sidebar_views_groups, get_sidebar_status, binary_search 5 | 6 | class OutlineCommand(WindowCommand): 7 | def run(self, immediate=False, single_pane=False, project=False, other_group=False, layout=0): 8 | show(self.window, single_pane=single_pane, other_group=other_group, layout=layout) 9 | 10 | class OutlineCloseSidebarCommand(WindowCommand): 11 | def run(self): 12 | active_view = self.window.active_view() 13 | 14 | for v in self.window.views(): 15 | if u'𝌆' in v.name(): 16 | self.window.focus_view(v) 17 | self.window.run_command('close_file') 18 | 19 | self.window.set_layout({"cols": [0.0, 1.0], "rows": [0.0, 1.0], "cells": [[0, 0, 1, 1]]}) 20 | self.window.focus_view(active_view) 21 | 22 | class OutlineRefreshCommand(TextCommand): 23 | def run(self, edit, symlist=None, symkeys=None, path=None, to_expand=None, toggle=None): 24 | self.view.erase(edit, Region(0, self.view.size())) 25 | if symlist and self.view.settings().get('outline_alphabetical'): 26 | symlist, symkeys = (list(t) for t in zip(*sorted(zip(symlist, symkeys)))) 27 | self.view.insert(edit, 0, "\n".join(symlist)) 28 | self.view.settings().set('symlist', symlist) 29 | self.view.settings().set('symkeys', symkeys) 30 | self.view.settings().set('current_file', path) 31 | self.view.sel().clear() 32 | 33 | class OutlineToggleSortCommand(TextCommand): 34 | def run(self, edit): 35 | sym_view = None 36 | for v in self.view.window().views(): 37 | if u'𝌆' in v.name(): 38 | v.settings().set('outline_alphabetical', not v.settings().get('outline_alphabetical')) 39 | sym_view = v 40 | 41 | symlist = self.view.get_symbols() 42 | refresh_sym_view(sym_view, symlist, self.view.file_name()) 43 | 44 | class OutlineEventHandler(EventListener): 45 | def on_selection_modified(self, view): 46 | if 'outline.hidden-tmLanguage' not in view.settings().get('syntax'): 47 | return 48 | 49 | sym_view, sym_group, fb_view, fb_group = get_sidebar_views_groups(view) 50 | 51 | sym_view = view 52 | window = view.window() 53 | sym_group, i = window.get_view_index(sym_view) 54 | 55 | if len(sym_view.sel()) == 0: 56 | return 57 | 58 | (row, col) = sym_view.rowcol(sym_view.sel()[0].begin()) 59 | 60 | active_view = None 61 | for group in range(window.num_groups()): 62 | if group != sym_group and group != fb_group: 63 | active_view = window.active_view_in_group(group) 64 | if active_view != None: 65 | symkeys = None 66 | # get the symbol list 67 | symlist = active_view.get_symbols() 68 | # depending on setting, set different regions 69 | if sym_view.settings().get('outline_main_view_highlight_mode') == 'cursor': 70 | symbol_line_ends = [active_view.line(range.a).end() for range, symbol in symlist] 71 | symkeys = list(zip(symbol_line_ends, symbol_line_ends)) 72 | if sym_view.settings().get('outline_main_view_highlight_mode') == 'symbol': 73 | symkeys = sym_view.settings().get('symkeys') 74 | if sym_view.settings().get('outline_main_view_highlight_mode') == 'block': 75 | symbol_block_begins = [active_view.line(range.a).begin() for range, symbol in symlist] 76 | symbol_blocks_ends = [x - 1 for x in symbol_block_begins[1:len(symbol_block_begins)]] + [active_view.size()] 77 | symkeys = list(zip(symbol_block_begins, symbol_blocks_ends)) 78 | if not symkeys: 79 | return 80 | region_position = symkeys[row] 81 | r = Region(region_position[0], region_position[1]) 82 | active_view.show_at_center(r) 83 | active_view.sel().clear() 84 | active_view.sel().add(r) 85 | window.focus_view(active_view) 86 | 87 | def on_activated(self, view): 88 | if u'𝌆' in view.name(): 89 | return 90 | # Avoid error message when console opens, as console is also a view, albeit with a group index of -1 91 | if view.window().get_view_index(view)[0] == -1: 92 | return 93 | 94 | if not get_sidebar_status(view): 95 | return 96 | 97 | sym_view, sym_group, fb_view, fb_group = get_sidebar_views_groups(view) 98 | 99 | if sym_view != None: 100 | if sym_view.settings().get('current_file') == view.file_name() and view.file_name() != None: 101 | return 102 | else: 103 | sym_view.settings().set('current_file', view.file_name()) 104 | 105 | symlist = view.get_symbols() 106 | 107 | refresh_sym_view(sym_view, symlist, view.file_name()) 108 | 109 | def on_pre_save(self, view): 110 | if u'𝌆' in view.name(): 111 | return 112 | # this is not absolutely necessary, and prevents views that do not have a file reference from displaying 113 | # the symbol list 114 | # but it solves a console error if the console is activiated, as console is also a view.... 115 | if view.file_name() == None: 116 | return 117 | 118 | if not get_sidebar_status(view): 119 | return 120 | 121 | sym_view, sym_group, fb_view, fb_group = get_sidebar_views_groups(view) 122 | 123 | if sym_view != None: 124 | # Note here is the only place that differs from on_activate_view 125 | if sym_view.settings().get('current_file') != view.file_name(): 126 | sym_view.settings().set('current_file', view.file_name()) 127 | 128 | symlist = view.get_symbols() 129 | refresh_sym_view(sym_view, symlist, view.file_name()) 130 | 131 | # sync the outline view with current file location 132 | if view.window() is None or not sym_view.settings().get('outline_sync'): 133 | return 134 | # get the current cursor location 135 | point = view.sel()[0].begin() 136 | # get the current symbol and its line in outline 137 | range_lows = [view.line(range.a).begin() for range, symbol in symlist] 138 | range_sorted = [0] + range_lows[1:len(range_lows)] + [view.size()] 139 | sym_line = binary_search(range_sorted, point) - 1 140 | 141 | if (sym_view is not None): 142 | sym_point_start = sym_view.text_point(sym_line, 0) 143 | # center symview at the point 144 | sym_view.show_at_center(sym_point_start) 145 | sym_view.sel().clear() 146 | sym_view.sel().add(sym_view.line(sym_point_start)) 147 | view.window().focus_view(view) 148 | -------------------------------------------------------------------------------- /outline.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Browse Mode: Outline (Right)", 4 | "command": "outline", 5 | "args": { 6 | "immediate": true, 7 | "other_group": "right", 8 | "single_pane": true, 9 | "project": true, 10 | "layout": 0 11 | } 12 | }, 13 | { 14 | "caption": "Browse Mode: Outline (Left)", 15 | "command": "outline", 16 | "args": { 17 | "immediate": true, 18 | "other_group": "right", 19 | "single_pane": true, 20 | "project": true, 21 | "layout": 1 22 | } 23 | }, 24 | { 25 | "caption": "Browse Mode: FileBrowser Left, Outline Right", 26 | "command": "outline", 27 | "args": { 28 | "immediate": true, 29 | "other_group": "right", 30 | "single_pane": true, 31 | "project": true, 32 | "layout": 2 33 | } 34 | }, 35 | { 36 | "caption": "Browse Mode: FileBrowser Top Left, Outline Bottom Left", 37 | "command": "outline", 38 | "args": { 39 | "immediate": true, 40 | "other_group": "right", 41 | "single_pane": true, 42 | "project": true, 43 | "layout": 3 44 | } 45 | }, 46 | { 47 | "caption": "Browse Mode: FileBrowser Top Right, Outline Bottom Right", 48 | "command": "outline", 49 | "args": { 50 | "immediate": true, 51 | "other_group": "right", 52 | "single_pane": true, 53 | "project": true, 54 | "layout": 4 55 | } 56 | }, 57 | { 58 | "caption": "Browse Mode: Close sidebar(s)", 59 | "command": "outline_close_sidebar", 60 | "args": { 61 | } 62 | }, 63 | { 64 | "caption": "Outline: Toggle Sort", 65 | "command": "outline_toggle_sort", 66 | "args": { 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /outline.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // If you don't like `..` symbol on the top of each view that moves you to 3 | // the parent directory and want to hide it you can set this to false 4 | "outline_show_parent": true, 5 | 6 | // If you want to see header (underlined full path) on top of file list 7 | "outline_header": false, 8 | 9 | // If you want to see full path in tab title and thus in window title 10 | // if tab is focused 11 | "outline_show_full_path": false, 12 | 13 | // If true 'Send to Trash' command will confirm the action with a dialog box 14 | "outline_confirm_send2trash": true, 15 | 16 | // Showing hidden files by default 17 | "outline_show_hidden_files": true, 18 | 19 | // Customize the patterns used to determine if a file should be hidden 20 | // e.g. [".*", "__pycache__", "*.pyc"] 21 | "outline_hidden_files_patterns": [".*"], 22 | 23 | // (ST3 Only) shows a FileBrowser or a jump list view in new window by default 24 | // false: hijacking is disabled 25 | // "jump_list": shows the project jump list 26 | // "outline" : shows a normal FileBrower view defaults to user directory 27 | "outline_hijack_new_window": false, 28 | 29 | // Path to git executable. 30 | // Wildcards are supported, e.g. if you use GitHub for Windows it would be: 31 | // "git_path": "~/Appdata/Local/GitHub/PortableGit_*/cmd/git.exe" 32 | // To disable git integration set to empty string: 33 | // "git_path": "" 34 | "git_path": "git", 35 | 36 | // Path to hg executable. 37 | // Wildcards are supported, e.g. if you use TortoiseHg it would be: 38 | // "hg_path": "%ProgramFiles%/TortoiseHg/hg.exe" 39 | // To disable hg integration set to empty string: 40 | // "hg_path": "" 41 | "hg_path": "hg", 42 | 43 | // true: the default, pressing Enter on a directory uses the current view. 44 | // false: a new view is created. 45 | "outline_reuse_view": true, 46 | 47 | // true: Enable alphabetical sort in output. 48 | // false: default because of losing parent dependences. 49 | "outline_alphabetical": false, 50 | 51 | // true: default, outline is in sync with current file position 52 | // false: no syncing, may be useful if use alphabetical sort 53 | "outline_sync": true, 54 | 55 | // when clicking on a symbol in the outline pane, how to highlight in the main view 56 | // "cursor": put the cursor at the end of the symbol line 57 | // "symbol": highlight the symbol in the main view 58 | // "block": highlight the entire block between the current symbol and next symbol 59 | "outline_main_view_highlight_mode": "symbol", 60 | 61 | // true: outline view will use the same color scheme as current file 62 | // false: outline will use its own color schemes (default or dark) 63 | "outline_inherit_color_scheme": true, 64 | 65 | // How should be positioned the Browse Mode view opened when jumping 66 | // "left": the default, open the view on the left 67 | // "right": open the view on the right 68 | // true: take all space available 69 | // false: don’t open any view 70 | "outline_open_on_jump": "left", 71 | 72 | // true: keep the active window if empty when jumping 73 | // false: the default, always open a new window when jumping 74 | "outline_smart_jump": false, 75 | 76 | // Width for FileBrowser column (as sidebar) accepts float or int: 77 | // float 0.1-0.9 put 1 is a window width, so e.g. 0.5 is half of window 78 | // int e.g. 99 pixels, if value will be more than current window width 79 | // then value will be treated as 0.9 80 | "outline_width": 0.2, 81 | 82 | // Automatically disable Vintageous to avoid keybindings incompatibilities; 83 | // note it is Vintageous setting, if it does not work you should report into 84 | // appropriate repository https://github.com/guillermooo/Vintageous/issues 85 | "__vi_external_disable": true, 86 | 87 | // The rest of these settings are normal sublime settings to 88 | // make FileBrowser views look more like a file browser than an editor 89 | // This plugin does not change font and font size, but it's 90 | // highly recommended to customize them in your user files. 91 | "color_scheme": "Packages/Outline/outline.hidden-tmTheme", 92 | "line_numbers": false, 93 | "word_wrap": false, 94 | "spell_check": false, 95 | "scroll_past_end": false, 96 | "draw_indent_guides": false, 97 | "fold_buttons": false, 98 | "drag_text": false, 99 | "highlight_line": true, 100 | "margin": 0, 101 | "tab_size": 3, 102 | "rulers": [], 103 | "caret_extra_width": 0 // extra caret width causes partially hidden chars 104 | // "font_face": "comic sans", 105 | // "font_size": 13, 106 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warmdev/SublimeOutline/220e3b0e9f21aed3ed68517aab2dc2cf82c5066d/screenshot.png -------------------------------------------------------------------------------- /show.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sublime 6 | from os.path import basename 7 | 8 | ST3 = int(sublime.version()) >= 3000 9 | 10 | if ST3: 11 | from .common import first, set_proper_scheme, calc_width, get_group 12 | else: 13 | from common import first, set_proper_scheme, calc_width, get_group 14 | 15 | 16 | def set_active_group(window, view, other_group): 17 | nag = window.active_group() 18 | if other_group: 19 | group = 0 if other_group == 'left' else 1 20 | groups = window.num_groups() 21 | if groups == 1: 22 | width = calc_width(view) 23 | cols = [0.0, width, 1.0] if other_group == 'left' else [0.0, 1-width, 1.0] 24 | window.set_layout({"cols": cols, "rows": [0.0, 1.0], "cells": [[0, 0, 1, 1], [1, 0, 2, 1]]}) 25 | elif view: 26 | group = get_group(groups, nag) 27 | window.set_view_index(view, group, 0) 28 | else: 29 | group = nag 30 | 31 | # when other_group is left, we need move all views to right except FB view 32 | if nag == 0 and other_group == 'left' and group == 0: 33 | for v in reversed(window.views_in_group(nag)[1:]): 34 | window.set_view_index(v, 1, 0) 35 | 36 | return (nag, group) 37 | 38 | 39 | def set_view(view_id, window, ignore_existing, single_pane): 40 | view = None 41 | active_view = None 42 | if view_id: 43 | # The Goto command was used so the view is already known and its contents should be 44 | # replaced with the new path. 45 | view = first(window.views(), lambda v: v.id() == view_id) 46 | 47 | if not view and not ignore_existing: 48 | # See if any reusable view exists in case of single_pane argument 49 | any_path = lambda v: v.score_selector(0, "text.outline") > 0 50 | view = first(window.views(), any_path if single_pane else same_path) 51 | 52 | if not view: 53 | active_view = window.active_view() 54 | view = window.new_file() 55 | view.set_syntax_file('Packages/Outline/outline.hidden-tmLanguage') 56 | view.set_scratch(True) 57 | if view.settings().get('outline_inherit_color_scheme'): 58 | view.settings().set('color_scheme', active_view.settings().get('color_scheme')) 59 | else: 60 | view.settings().add_on_change('color_scheme', lambda: set_proper_scheme(view)) 61 | 62 | reset_sels = True 63 | else: 64 | reset_sels = path != view.settings().get('outline_path', '') 65 | 66 | return (view, reset_sels) 67 | 68 | 69 | def show(window, view_id=None, ignore_existing=False, single_pane=False, other_group='', layout=1): 70 | """ 71 | Determines the correct view to use, creating one if necessary, and prepares it. 72 | """ 73 | symlist = [] 74 | file_path = None 75 | prev_focus = None 76 | if other_group: 77 | prev_focus = window.active_view() 78 | symlist = prev_focus.get_symbols() 79 | file_path = prev_focus.file_name() 80 | # simulate 'toggle sidebar': 81 | if prev_focus and 'outline' in prev_focus.scope_name(0): 82 | window.run_command('close_file') 83 | return 84 | 85 | view, reset_sels = set_view(view_id, window, ignore_existing, single_pane) 86 | 87 | nag, group = set_active_group(window, view, other_group) 88 | 89 | if other_group and prev_focus: 90 | window.focus_view(prev_focus) 91 | 92 | view_name = "Outline" 93 | 94 | if ST3: 95 | name = u"𝌆 {0}".format(view_name) 96 | else: 97 | name = u"■ {0}".format(view_name) 98 | 99 | view.set_name(name) 100 | view.settings().set('outline_rename_mode', False) 101 | window.focus_view(view) 102 | 103 | if layout >= 2: 104 | window.run_command('dired', {'immediate': True, 'other_group': 'right', 'single_pane': True, 'project': True}) 105 | 106 | width = calc_width(view) 107 | if layout == 0: 108 | window.set_layout({"cols": [0.0, width, 1-width, 1.0], "rows": [0.0, 0.5, 1.0], "cells": [[2, 0, 3, 2], [0, 0, 2, 2]]}) 109 | elif layout == 1: 110 | window.set_layout({"cols": [0.0, width, 1-width, 1.0], "rows": [0.0, 0.5, 1.0], "cells": [[0, 0, 1, 2], [1, 0, 3, 2]]}) 111 | elif layout == 2: 112 | window.set_layout({"cols": [0.0, width, 1-width, 1.0], "rows": [0.0, 0.5, 1.0], "cells": [[2, 0, 3, 2], [1, 0, 2, 2], [0, 0, 1, 2]]}) 113 | elif layout == 3: 114 | window.set_layout({"cols": [0.0, width, 1-width, 1.0], "rows": [0.0, 0.5, 1.0], "cells": [[0, 1, 1, 2], [1, 0, 3, 2], [0, 0, 1, 1]]}) 115 | elif layout == 4: 116 | window.set_layout({"cols": [0.0, width, 1-width, 1.0], "rows": [0.0, 0.5, 1.0], "cells": [[2, 1, 3, 2], [0, 0, 2, 2], [2, 0, 3, 1]]}) 117 | 118 | window.set_view_index(view, 0, 0) 119 | 120 | for v in reversed(window.views_in_group(0)[1:]): 121 | if layout >= 2 and u"𝌆" in v.name(): 122 | window.set_view_index(v, 2, 0) 123 | else: 124 | window.set_view_index(v, 1, 0) 125 | 126 | window.focus_view(prev_focus) 127 | 128 | refresh_sym_view(view, symlist, file_path) 129 | 130 | def refresh_sym_view(sym_view, symlist, path): 131 | l = [symbol for range, symbol in symlist] 132 | k = [(range.a, range.b) for range, symbol in symlist] 133 | if sym_view != None: 134 | sym_view.settings().erase('symlist') 135 | sym_view.settings().erase('symkeys') 136 | sym_view.run_command('outline_refresh', {'symlist': l, 'symkeys': k, 'path': path}) 137 | 138 | def get_sidebar_views_groups(view): 139 | window = view.window() 140 | views = window.views() 141 | sym_view = None 142 | sym_group = None 143 | fb_view = None 144 | fb_group = None 145 | for v in views: 146 | if 'outline.hidden-tmLanguage' in v.settings().get('syntax'): 147 | sym_view = v 148 | sym_group, i = window.get_view_index(sym_view) 149 | if u'𝌆' in v.name() and v.id() != sym_view.id(): 150 | fb_view = v 151 | if fb_view != None: 152 | fb_group, j = window.get_view_index(fb_view) 153 | 154 | return (sym_view, sym_group, fb_view, fb_group) 155 | 156 | def get_sidebar_status(view): 157 | sidebar_on = False 158 | for v in view.window().views(): 159 | if u'𝌆' in v.name(): 160 | sidebar_on = True 161 | 162 | return sidebar_on 163 | 164 | # given a sorted array, returns the location of x if inserted into the array 165 | def binary_search(array, x): 166 | low = 0 167 | high = len(array) - 1 168 | mid = 0 169 | 170 | while low < high: 171 | # middle location 172 | mid = (high + low) // 2 173 | if array[mid] <= x: 174 | low = mid + 1 175 | else: 176 | high = mid 177 | 178 | return low 179 | --------------------------------------------------------------------------------