├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── example.css ├── messages.json ├── messages ├── 1.1.4.txt ├── 1.1.5.txt ├── 1.1.6.txt ├── 1.2.0.txt └── install.txt ├── tableofcomments.py ├── tableofcomments.sublime-commands ├── tableofcomments.sublime-settings └── tests ├── README.txt ├── __init__.py ├── test_comment_syntax.py ├── test_get_comment_titles.py ├── test_get_sections.py ├── test_large_file.py ├── test_level_chars.py ├── test_level_depth.py ├── test_toc_output.py └── testcase.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Keiran O'Leary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table of comments 2 | 3 | A [Sublime Text 2 & 3](http://www.sublimetext.com) plugin that lets you organise and 4 | quick-jump between headings in your comments (like "jump to symbol") as well as 5 | optionally output a live table of contents within your document. 6 | 7 | ![demo](http://imgur.com/uIhsQ8A.gif) 8 | 9 | Organise your code with headings within your comments then run the plugin 10 | to open the quick-jump panel and easily navigate between sections of your document. 11 | Not only does it help you navigate your code quickly with custom headings, 12 | it keeps you organised and mindful of the overall structure of your document. 13 | 14 | 15 | ## How to install 16 | 17 | ### Via Package Control 18 | Open your command palette -> Package Control: Install Package -> Table of comments 19 | 20 | ### Manual 21 | 22 | Go to your packages folder(Preferences -> Browse Packages) 23 | ```bash 24 | # clone this repo 25 | git clone https://github.com/kizza/Table-of-comments 'Table of comments' 26 | ``` 27 | Or download the leatest [release](https://github.com/kizza/Table-of-comments/releases) 28 | and unzip it in a folder named `Table of comments` 29 | 30 | ## How to use it 31 | 32 | Simply start using headings within comments to organise your code using the format below. 33 | By default titles are represented by ">" but each title prefix can be customised via the settings. 34 | 35 | ``` 36 | /* 37 | * > Heading 1 38 | */ 39 | ... 40 | /* 41 | * >> Heading 2 42 | */ 43 | ... 44 | /* 45 | * >>> Heading 3 46 | */ 47 | ``` 48 | 49 | ### Jumping between headings 50 | 51 | The easiest way to run the plugin is via a keybinding so that you can open 52 | the quick-jump menu quickly whilst typing. 53 | 54 | 1. Add a keystroke binding within your preferences (recommended) 55 | Open "Preferenes -> Key Bindings - User" from the main menu then paste 56 | 57 | ``` 58 | { "keys": ["f1"], "command": "table_of_comments" } 59 | ``` 60 | (This example runs the plugin by pressing F1) 61 | 62 | 63 | 2. You can also run the plugin via the command palette (Crtl+ Shift + P). 64 | Simply find and execute "Table of Comments: Show" 65 | 66 | Then just like how you are able to quick-jump between functions and selectors 67 | you can now jump between the documentation and comment headings within your document. 68 | 69 | ### Moving up and down through comments 70 | 71 | You can also move to the next/prev comment from your local position. 72 | Here are some examples of the keybindings you can set in 73 | "Preferenes -> Key Bindings - User" from the main menu. 74 | 75 | ``` 76 | { "keys": ["alt+up"], "command": "table_of_comments", "args":{ "move":"up" } } 77 | { "keys": ["alt+down"], "command": "table_of_comments", "args":{ "move":"down" } } 78 | ``` 79 | Feel free to set any keyboard shortcuts you like. 80 | (This behaviour inspired by by [Sublime Move By Symbols](https://packagecontrol.io/packages/Move%20By%20Symbols) plugin) 81 | 82 | ### Folding and unfolding sections 83 | 84 | Current comment section (or all sections) can be folded and unfolded using commands or keyboard shortcuts. 85 | Here are some examples of the keybindings you can set in 86 | "Preferenes -> Key Bindings - User" from the main menu. 87 | 88 | Fold and unfold current section 89 | ``` 90 | { "keys": ["alt+["], "command": "table_of_comments", "args":{ "fold":true } } 91 | { "keys": ["alt+]"], "command": "table_of_comments", "args":{ "unfold":"true" } } 92 | ``` 93 | Fold and unfold all sections 94 | ``` 95 | { "keys": ["alt+shift+["], "command": "table_of_comments", "args":{ "fold":"all" } } 96 | { "keys": ["alt+shift+]"], "command": "table_of_comments", "args":{ "unfold":""all"" } } 97 | ``` 98 | 99 | ### Outputting a table of contents (optional) 100 | 101 | You can optionally output a maintained list of the headings within your document 102 | by placing "TOC" inside a separate comment anywhere within the document. 103 | 104 | For example placing... 105 | 106 | ```/* TOC */``` 107 | 108 | ...anywhere in your document will automatically update it to reflect the headings 109 | within your comments each time you run the plugin. 110 | 111 | ``` 112 | /* 113 | * TOC 114 | * 115 | * Heading 1 116 | * - Heading 2 117 | * -- Heading 3 118 | * --- Heading 4 119 | */ 120 | ``` 121 | 122 | You can change the title for your table of contents within the plugin settings. 123 | For example, you can change "TOC" to be "Within this document", then simply place 124 | that text within a comment in your document to have the table of content maintained 125 | with that heading. 126 | 127 | ### Customising the plugin 128 | 129 | You can tweak the plugin settings for parsing headings (ie. which characters 130 | designate each level of headings) as well as for formatting the table of contents output. 131 | 132 | To view the existing plugin settings run the command 133 | "Table of comments: Settings - Default" from the command palette (Ctrl + Shift + P). 134 | Then run "Table of comments: Settings - User" and paste in any of the settings 135 | you wish to change. 136 | 137 | Ultimately the above creates a "tableofcomments.sublime-settings" file in 138 | your "Packages/User" directory. 139 | 140 | #### Changing the heading characters 141 | 142 | For example you can use colons to designate level headings... 143 | 144 | ``` 145 | /* 146 | * : Heading 1 147 | * 148 | * :: Heading 2 149 | * 150 | * ::: Heading 3 151 | */ 152 | ``` 153 | 154 | By using the setting... 155 | 156 | ``` 157 | "level_char": ":", 158 | ``` 159 | 160 | #### Tweaking the table of comments title 161 | 162 | Rather than "TOC" designating the comment to be updated with your table of contents 163 | you could enter the setting... 164 | 165 | ``` 166 | "toc_title":"Within this document" 167 | ``` 168 | 169 | Which means you can use the comment below to manage your table of contents 170 | 171 | ``` 172 | /* Within this document */ 173 | ``` 174 | 175 | #### Only showing first level headings within the table of comments 176 | 177 | If you've dilegently organised lots of first, second and third headings within your 178 | document for the purposes of quick-jumping around, it may be too much to display 179 | them all within the table of comments. 180 | 181 | You could of course not include the /* TOC */ comment in your document, or use the 182 | setting below to only show first level headings. 183 | 184 | ``` 185 | "toc_level":"1" 186 | ``` 187 | 188 | #### Generate TOC on save 189 | 190 | To generate the TOC you need to enable the following configuration: 191 | 192 | ``` 193 | "toc_generate_on_save": true 194 | ``` 195 | 196 | 197 | -------------------------------------------------------------------------------- /example.css: -------------------------------------------------------------------------------- 1 | /* 2 | * TOC 3 | */ 4 | 5 | /* 6 | * > Heading 1 7 | */ 8 | header { background:#000; padding:0; } 9 | 10 | /* 11 | * >> Heading 2 12 | */ 13 | .logo { float:left; width:200px; } 14 | 15 | /* >>> Heading 3 */ 16 | h3 { 17 | color: inherit; 18 | font-family: inherit; 19 | font-weight: 500; 20 | line-height: 1.1; 21 | } 22 | 23 | /* >>>> Heading 4 */ 24 | div { 25 | float: right; 26 | margin: 0 auto; 27 | width: 50%; 28 | } 29 | 30 | /* >>>>> Heading 5 */ 31 | nav { float:right; } 32 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.1.4": "messages/1.1.4.txt", 4 | "1.1.5": "messages/1.1.5.txt", 5 | "1.1.6": "messages/1.1.6.txt", 6 | "1.2.0": "messages/1.2.0.txt" 7 | } 8 | -------------------------------------------------------------------------------- /messages/1.1.4.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ __ 2 | |_ _| | | | | / _| 3 | | | __ _| |__ | | ___ ___ | |_ 4 | | |/ _` | '_ \| |/ _ \ / _ \| _| 5 | | | (_| | |_) | | __/ | (_) | | 6 | \_/\__,_|_.__/|_|\___| \___/|_| 7 | 8 | _____ _ 9 | / __ \ | | 10 | | / \/ ___ _ __ ___ _ __ ___ ___ _ __ | |_ ___ 11 | | | / _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ __| 12 | | \__/\ (_) | | | | | | | | | | | __/ | | | |_\__ \ 13 | \____/\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__|___/ 14 | 15 | 16 | ------------------------------- 17 | 18 | Updates in 1.1.4: 19 | 20 | - Fixed several comment title parsing bugs 21 | - Added feature to highlight titles as they are scrolled through via the quick-jump menu 22 | 23 | 24 | Thank you! 25 | ------------------------------- 26 | You can learn more or get in contact at https://github.com/kizza/Table-of-comments 27 | -------------------------------------------------------------------------------- /messages/1.1.5.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ __ 2 | |_ _| | | | | / _| 3 | | | __ _| |__ | | ___ ___ | |_ 4 | | |/ _` | '_ \| |/ _ \ / _ \| _| 5 | | | (_| | |_) | | __/ | (_) | | 6 | \_/\__,_|_.__/|_|\___| \___/|_| 7 | 8 | _____ _ 9 | / __ \ | | 10 | | / \/ ___ _ __ ___ _ __ ___ ___ _ __ | |_ ___ 11 | | | / _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ __| 12 | | \__/\ (_) | | | | | | | | | | | __/ | | | |_\__ \ 13 | \____/\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__|___/ 14 | 15 | 16 | ------------------------------- 17 | 18 | Updates in 1.1.5: 19 | 20 | - Quick jump menu now opens at current location (available for ST3 only) 21 | - Added section code folding. Now current comment section (or all sections) can be 22 | folded and unfolded using commands or keyboard shortcuts 23 | - Several bug fixes 24 | 25 | 26 | Thank you! 27 | ------------------------------- 28 | You can learn more or get in contact at https://github.com/kizza/Table-of-comments 29 | -------------------------------------------------------------------------------- /messages/1.1.6.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ __ 2 | |_ _| | | | | / _| 3 | | | __ _| |__ | | ___ ___ | |_ 4 | | |/ _` | '_ \| |/ _ \ / _ \| _| 5 | | | (_| | |_) | | __/ | (_) | | 6 | \_/\__,_|_.__/|_|\___| \___/|_| 7 | 8 | _____ _ 9 | / __ \ | | 10 | | / \/ ___ _ __ ___ _ __ ___ ___ _ __ | |_ ___ 11 | | | / _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ __| 12 | | \__/\ (_) | | | | | | | | | | | __/ | | | |_\__ \ 13 | \____/\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__|___/ 14 | 15 | 16 | ------------------------------- 17 | 18 | Updates in 1.1.6: 19 | - Fixes #42 Current heading selection when showing the dialog is wrong sometimes 20 | - Fixes #43: Headings with comment_chars in main text are discarded 21 | 22 | 23 | 24 | Thank you! 25 | ------------------------------- 26 | You can learn more or get in contact at https://github.com/kizza/Table-of-comments 27 | -------------------------------------------------------------------------------- /messages/1.2.0.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ __ 2 | |_ _| | | | | / _| 3 | | | __ _| |__ | | ___ ___ | |_ 4 | | |/ _` | '_ \| |/ _ \ / _ \| _| 5 | | | (_| | |_) | | __/ | (_) | | 6 | \_/\__,_|_.__/|_|\___| \___/|_| 7 | 8 | _____ _ 9 | / __ \ | | 10 | | / \/ ___ _ __ ___ _ __ ___ ___ _ __ | |_ ___ 11 | | | / _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ __| 12 | | \__/\ (_) | | | | | | | | | | | __/ | | | |_\__ \ 13 | \____/\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__|___/ 14 | 15 | 16 | ------------------------------- 17 | 18 | Updates in 1.2.0: 19 | - Fixes #52 Adds the option to generate the TOC on save 20 | 21 | 22 | 23 | Thank you! 24 | ------------------------------- 25 | You can learn more or get in contact at https://github.com/kizza/Table-of-comments 26 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ __ 2 | |_ _| | | | | / _| 3 | | | __ _| |__ | | ___ ___ | |_ 4 | | |/ _` | '_ \| |/ _ \ / _ \| _| 5 | | | (_| | |_) | | __/ | (_) | | 6 | \_/\__,_|_.__/|_|\___| \___/|_| 7 | 8 | 9 | _____ _ 10 | / __ \ | | 11 | | / \/ ___ _ __ ___ _ __ ___ ___ _ __ | |_ ___ 12 | | | / _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ __| 13 | | \__/\ (_) | | | | | | | | | | | __/ | | | |_\__ \ 14 | \____/\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__|___/ 15 | 16 | 17 | 18 | This plugin lets you organise and quick-jump between headings in your comments as 19 | well as optionally output a live table of contents. 20 | 21 | How to use it 22 | =============================== 23 | Simply start using headings within comments to organise your code using the format below. 24 | By default titles are represented by ">" but each title prefix can be customised via the settings. 25 | 26 | ``` 27 | /* 28 | * > Heading 1 29 | */ 30 | ... 31 | /* 32 | * >> Heading 2 33 | */ 34 | ... 35 | /* 36 | * >>> Heading 3 37 | */ 38 | ``` 39 | 40 | Now open the command palette (Ctrl+Shift+P) and search for the `Table of Comments: Show` 41 | command and press enter. 42 | 43 | 44 | Available commands: 45 | 46 | - Table of Comments: Show -- Will show/generate the table of comments. 47 | - Table of Comments: Prev Title -- Move the cursor to the previous comment. 48 | - Table of Comments: Next Title -- Move the cursor to the next comment. 49 | 50 | - Table of Comments: Fold Current -- Folds the current section 51 | - Table of Comments: Unfold Current -- Unfolds the current section 52 | - Table of Comments: Fold All -- Folds all comment title sections 53 | - Table of Comments: Unfold All -- Unolds all comment title sections 54 | 55 | - Table of Comments: Settings - Default -- Open the default settings file. 56 | - Table of Comments: Settings - User -- Move the user settings file. 57 | 58 | 59 | Setting keyboard shortcuts: 60 | 61 | To run the show command when you press f1 you will have to open the 62 | Preferences -> Key Bindings - User and insert a new key binding: 63 | 64 | { "keys": ["f1"], "command": "table_of_comments" } 65 | 66 | 67 | Thank you! 68 | ------------------------------- 69 | I hope you enjoy using this plugin - and that it saves you some time as well. 70 | You can learn more or get in contact at https://github.com/kizza/Table-of-comments 71 | 72 | -------------------------------------------------------------------------------- /tableofcomments.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plugin to create a quick panel lookup that lets you jump between comment 3 | titles 4 | """ 5 | 6 | import os 7 | import imp 8 | import time 9 | import sys 10 | import sublime 11 | import sublime_plugin 12 | import re 13 | 14 | 15 | # 16 | # > Plugin command 17 | # 18 | class table_of_comments_command(sublime_plugin.TextCommand): 19 | 20 | def run(self, edit, move=None, fold=None, unfold=None, generate=None): 21 | toc = TableOfComments(self.view, edit) 22 | if move is not None: 23 | self.traverse_comments(toc, move) 24 | elif fold is not None or unfold is not None: 25 | self.fold_comments(toc, fold, unfold) 26 | elif generate is not None: 27 | toc.create_toc() 28 | else: 29 | self.show_quick_panel(toc) 30 | 31 | # >> Quick panel 32 | def show_quick_panel(self, toc): 33 | view = self.view 34 | toc._debug_start('Show quick panel') 35 | toc.create_toc() 36 | # Get current section from cursor 37 | show_index = 0 38 | current_section = toc.get_section_from_cursor() 39 | if current_section: 40 | show_index = current_section['index'] 41 | 42 | # Store positions for returning to 43 | return_to = [] 44 | for each in view.sel(): 45 | return_to.append(each) 46 | toc.return_to = return_to 47 | 48 | # Pop up the panel 49 | titles = toc.get_comment_titles('string') 50 | self.window = sublime.active_window() 51 | if sys.version_info < (3, 0): 52 | self.window.show_quick_panel(titles, toc.on_list_selected_done) 53 | else: 54 | self.window.show_quick_panel( # Pass on_highlighted callback 55 | titles, toc.on_list_selected_done, False, show_index, 56 | toc.on_list_selected_done) 57 | toc._debug_stop('Show quick panel') 58 | 59 | # >> Up down 60 | # Allows moving up and down through comments 61 | def traverse_comments(self, toc, move): 62 | view = self.view 63 | titles = toc.get_comment_titles() 64 | sel = view.sel() 65 | if len(sel) == 1: 66 | current_line_no, col_no = view.rowcol(sel[0].b) 67 | for x in range(len(titles)): 68 | item = titles[x] 69 | if move == 'up': # moving up 70 | if item['line'] < current_line_no: 71 | if x+1 < len(titles): 72 | if titles[x+1]['line'] >= current_line_no: 73 | return toc.on_list_selected_done(x) 74 | else: 75 | return toc.on_list_selected_done(x) 76 | else: # moving down 77 | if item['line'] > current_line_no: 78 | return toc.on_list_selected_done(x) 79 | 80 | # >> Fold comments 81 | def fold_comments(self, toc, fold, unfold): 82 | comments = self.view.find_by_selector('comment') 83 | is_all = fold == 'all' or unfold == 'all' 84 | 85 | # Get the content regions to fold 86 | fold_regions = [] 87 | 88 | if is_all: 89 | sections = toc.get_sections() 90 | for s in sections: 91 | content_region = s['content_region'] 92 | fold_regions.append(content_region) 93 | else: 94 | section = toc.get_section_from_cursor() 95 | fold_regions.append(section['content_region']) 96 | 97 | # Fold, unfold or toggle 98 | if fold is not None: 99 | self.view.fold(fold_regions) 100 | elif unfold is not None: 101 | self.view.unfold(fold_regions) 102 | elif self.view.fold(fold_regions) is False: 103 | self.view.unfold(fold_regions) 104 | 105 | 106 | # 107 | # > Plugin class 108 | # 109 | class TableOfComments: 110 | 111 | def __init__(self, view, edit): 112 | self.view = view 113 | self.edit = edit 114 | 115 | # 116 | # Debug timing functions 117 | # 118 | # 119 | timers = {} 120 | 121 | def _debug_start(self, ref): 122 | self.timers[ref] = time.time() 123 | 124 | def _debug_stop(self, ref): 125 | start_time = self.timers[ref] 126 | duration = time.time() - start_time 127 | self.timers[ref] = duration 128 | 129 | # 130 | # Table TOC tag 131 | # 132 | 133 | def get_toc_region(self, view): 134 | title = get_setting('toc_title', str) 135 | pattern = r'\/\*(\s|\*)*'+title+r'[^\/]*\/' 136 | matches = view.find_all(pattern) 137 | for region in (matches): 138 | if self.is_scope_or_comment(view, region): 139 | return region 140 | return None 141 | 142 | def is_in_toc_region(self, view, region): 143 | toc_region = self.get_toc_region(view) 144 | if toc_region: 145 | if region.a > toc_region.a and region.a < toc_region.b: 146 | return True 147 | return False 148 | 149 | def create_toc(self): 150 | view = self.view 151 | edit = self.edit 152 | region = self.get_toc_region(view) 153 | if region: 154 | toc = self.compile_toc(view) 155 | existing = view.substr(region) 156 | if existing != toc: 157 | view.replace(edit, region, toc) 158 | 159 | def compile_toc(self, view): 160 | self._debug_start('compile-toc') 161 | titles = self.get_comment_titles('string') 162 | title = get_setting('toc_title', str) 163 | start = get_setting('toc_start', str) 164 | line = get_setting('toc_line', str) 165 | end = get_setting('toc_end', str) 166 | front = "\n" + line 167 | output = start + front + title + front.rstrip() 168 | for title in titles: 169 | comment_level = title.count('-') + 1 170 | try: 171 | level = int(get_setting('toc_level', int)) 172 | if level >= comment_level: 173 | output += front + title 174 | except TypeError: 175 | output += front + title 176 | output += "\n"+end 177 | self._debug_stop('compile-toc') 178 | return output 179 | 180 | # 181 | # >> Quick panel 182 | # 183 | 184 | # Jump list quick menu selected 185 | def on_list_selected_done(self, picked): 186 | if picked == -1: 187 | self.view.sel().clear() 188 | for each in self.return_to: 189 | self.view.sel().add(each) 190 | self.view.show(self.view.sel()) 191 | else: 192 | titles = self.get_comment_titles() 193 | title = titles[picked] 194 | row = title['line'] 195 | point = self.view.text_point(row, 0) 196 | line_region = self.view.line(point) 197 | # Reference the 'text' within the line only 198 | text = title['text'] 199 | text = re.escape(text) 200 | text = text.replace('\>', '>') # ">" does not work when escaped 201 | text_region = self.view.find(text, line_region.a) 202 | 203 | # view.rowcol() returns a zero based line number 204 | line = int(title['line'])+1 205 | # Use goto_line to move the document then highlight 206 | if sublime.active_window().active_view(): 207 | sublime.active_window().active_view().run_command( 208 | "goto_line", {"line": line} 209 | ) 210 | self.view.sel().clear() 211 | self.view.sel().add(text_region) 212 | 213 | # 214 | # >> Parse 215 | # 216 | 217 | # Core parse function (returned as dict or list) 218 | def get_comment_titles(self, format='dict', test=None): 219 | self._debug_start('get-comment-titles') 220 | view = self.view 221 | level_char = get_setting('level_char', str) 222 | comment_chars = get_setting('comment_chars', str) 223 | comment = list(comment_chars) 224 | comment = 'DIV'.join(comment_chars) 225 | start = r'\s|'+re.escape(comment).replace('DIV', '|') 226 | # build the pattern to match the comment 227 | pattern = r'^('+start+')*?('+format_pattern(level_char)+'+)\s*' + \ 228 | r'(.+)('+start+')*?$' 229 | 230 | matches = view.find_all(pattern) 231 | results = [] 232 | toc_title = get_setting('toc_title', str) 233 | 234 | for match in matches: 235 | bits = view.lines(match) # go through each line 236 | for region in bits: 237 | # Ensure it's comment or source 238 | if not self.is_scope_or_comment(view, region): 239 | continue 240 | # Ensure not in toc region already 241 | if self.is_in_toc_region(view, region): 242 | continue 243 | 244 | line = view.substr(region) 245 | line_match = re.match(pattern, line) 246 | 247 | if not line_match: 248 | continue 249 | 250 | if level_char in line: 251 | # Add the level chars 252 | label = line_match.group(2) 253 | 254 | # Replace level char with toc char 255 | label = self.replace_level_chars(label) 256 | level = len(label) 257 | if label != '': 258 | label += ' ' 259 | 260 | # append the heading text, remove trailing comment chars 261 | text = line_match.group(3).strip(comment_chars+' ') 262 | label += text 263 | 264 | # Get the position 265 | if line != '' and line != toc_title: 266 | line_no, col_no = view.rowcol(region.b) 267 | if format == 'dict': 268 | results.append( 269 | {'label': label, 270 | 'text': text, 271 | 'level': level, 272 | 'region': region, 273 | 'line': line_no}) 274 | else: 275 | results.append(label) 276 | self._debug_stop('get-comment-titles') 277 | return results 278 | 279 | # 280 | # >> Plugin sections (regions) 281 | # 282 | 283 | # Returns list of sections dicts with all related values 284 | def get_sections(self): 285 | comments = self.view.find_by_selector('comment') 286 | titles = self.get_comment_titles() 287 | 288 | # Only get comment blocks with titles within them 289 | sections = [] 290 | for i in range(len(comments)): 291 | # we need to get the whole lines in order to match 292 | # indented title regions correctly 293 | comment = self.view.line(comments[i]) 294 | # If multiple lines returned check for valid lines 295 | comment_lines = self.view.split_by_newlines(comment) 296 | if len(comment_lines) > 0: 297 | fixed_comment_lines = [] 298 | for x in range(len(comment_lines)): 299 | if self.is_scope_or_comment(self.view, comment_lines[x]): 300 | fixed_comment_lines.append(comment_lines[x]) 301 | if len(fixed_comment_lines) > 0: 302 | comment = sublime.Region( 303 | fixed_comment_lines[0].a, 304 | fixed_comment_lines[len(fixed_comment_lines)-1].b 305 | ) 306 | # Append to sections 307 | for title in titles: 308 | if comment.contains(title['region']): 309 | title['title_region'] = comment 310 | sections.append(title) 311 | break 312 | 313 | # Get the fold regions (content blocks) 314 | s_no = len(sections) 315 | view_size = self.view.size() 316 | for i in range(s_no): 317 | section = sections[i] 318 | section['index'] = i 319 | region = section['title_region'] 320 | 321 | # content_region = the area that will be hidden when folded 322 | fold_start = region.b + 1 323 | fold_end = view_size 324 | 325 | # get the next section of equal or lower level 326 | for j in range(i+1, s_no): 327 | if sections[j]['level'] <= section['level']: 328 | fold_end = sections[j]['title_region'].a - 1 329 | break 330 | 331 | content_region = sublime.Region(fold_start, fold_end) 332 | section['content_region'] = content_region 333 | 334 | return sections 335 | 336 | # Returns the title and content region from cursor 337 | def get_section_from_cursor(self): 338 | # Current selection 339 | sel = self.view.sel()[0] 340 | line_no, col_no = self.view.rowcol(sel.b) 341 | 342 | # Find within sections 343 | sections = self.get_sections() 344 | 345 | for section in reversed(sections): 346 | if section['line'] <= line_no: 347 | return section 348 | return False 349 | 350 | # Only find titles within genuine comments 351 | # This will no doubt need to be improved over time for various syntaxes 352 | # ('string.quoted' makes python """ comments """ not trigger) 353 | def is_scope_or_comment(self, view, region): 354 | line = view.substr(region) 355 | # Trim to scope 356 | # If line starts with whitespace, the syntax returned is "source" not 357 | # "comment" for the initial char 358 | trimmed = line.lstrip() 359 | diff = len(line) - len(trimmed) 360 | scope = view.scope_name(region.a + diff) 361 | # Check out scope 362 | comments_scope = ['comment'] 363 | disallow = ['string.quoted'] 364 | for each in comments_scope: 365 | if scope.find(each) < 0: 366 | return False 367 | for each in disallow: 368 | if scope.find(each) > 0: 369 | return False 370 | return True 371 | 372 | def replace_level_chars(self, line): 373 | level_char = get_setting('level_char', str) 374 | toc_char = get_setting('toc_char', str) 375 | # remove the last char so level one has no indent 376 | line = line[:-1].replace(level_char, toc_char) 377 | return line 378 | 379 | 380 | # 381 | # Helpers 382 | # 383 | 384 | def format_pattern(pattern): 385 | pattern = re.escape(pattern) 386 | pattern = pattern.replace('\>', '>') 387 | return pattern 388 | 389 | 390 | def get_setting(name, typeof=str): 391 | settings = sublime.load_settings('tableofcomments.sublime-settings') 392 | setting = settings.get(name) 393 | if setting: 394 | if typeof == str: 395 | return setting 396 | if typeof == bool: 397 | return setting is True 398 | elif typeof == int: 399 | return int(settings.get(name, 500)) 400 | else: 401 | if typeof == str: 402 | return '' 403 | else: 404 | return None 405 | 406 | 407 | # 408 | # Testing infrastructure 409 | # 410 | 411 | if sys.version_info < (3, 0): 412 | import tests 413 | else: 414 | from . import tests 415 | 416 | 417 | class table_of_comments_run_tests_command(sublime_plugin.TextCommand): 418 | def run(self, edit): 419 | reload_test_bootstrap() 420 | tests.run(self.view, edit) 421 | 422 | 423 | # For developing, reload tests.* which in turn reloads it's sub packages 424 | basedir = os.getcwd() 425 | 426 | 427 | def reload_test_bootstrap(): 428 | os.chdir(basedir) 429 | path = 'tests' 430 | if sys.version_info < (3, 0): 431 | __import__(path) 432 | sys.modules[path] = reload(sys.modules[path]) 433 | else: 434 | imp.reload(eval(path)) 435 | 436 | 437 | class table_of_comments_auto_runner(sublime_plugin.EventListener): 438 | def on_pre_save(self, view): 439 | if get_setting('toc_generate_on_save', bool): 440 | view.run_command('table_of_comments', { 'generate': True }) 441 | 442 | -------------------------------------------------------------------------------- /tableofcomments.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Table of Comments: Show", 4 | "command": "table_of_comments" 5 | }, 6 | { 7 | "caption": "Table of Comments: Next Title", 8 | "command": "table_of_comments", 9 | "args": {"move": "up"} 10 | }, 11 | { 12 | "caption": "Table of Comments: Prev Title", 13 | "command": "table_of_comments", 14 | "args": {"move": "down"} 15 | }, 16 | { 17 | "caption": "Table of Comments: Fold Current", 18 | "command": "table_of_comments", 19 | "args": {"fold": true} 20 | }, 21 | { 22 | "caption": "Table of Comments: Unfold Current", 23 | "command": "table_of_comments", 24 | "args": {"unfold": true} 25 | }, 26 | { 27 | "caption": "Table of Comments: Fold All", 28 | "command": "table_of_comments", 29 | "args": {"fold": "all"} 30 | }, 31 | { 32 | "caption": "Table of Comments: Unfold All", 33 | "command": "table_of_comments", 34 | "args": {"unfold": "all"} 35 | }, 36 | { 37 | "caption": "Table of Comments: Run Tests", 38 | "command": "table_of_comments_run_tests" 39 | }, 40 | { 41 | "caption": "Table of Comments: Settings - Default", 42 | "command": "open_file", 43 | "args": {"file": "${packages}/Table of comments/tableofcomments.sublime-settings"} 44 | }, 45 | { 46 | "caption": "Table of Comments: Settings - User", 47 | "command": "open_file", 48 | "args": {"file": "${packages}/User/tableofcomments.sublime-settings"} 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /tableofcomments.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | * Settings for parsing comment titles 4 | */ 5 | 6 | /* 7 | * The character you would like to be shown in the TOC for the 8 | * depth of the comment 9 | */ 10 | "toc_char" : "-", 11 | 12 | /* 13 | * The character you use to mark a comment for the TOC. 14 | * // > Means level 1 15 | * // >> Means level 2 16 | * // etc. 17 | */ 18 | "level_char" : ">", 19 | 20 | /* 21 | * Table of contents options 22 | */ 23 | "toc_start": "/*", // Start table of contents with 24 | "toc_title": "TOC", // Title to display (and look for) for table of contents creation 25 | "toc_line": "* ", // Prefix of each line within table of contents 26 | "toc_end": "*/", // End table of contents with 27 | 28 | /* 29 | * Enter the number of depth the comments should be visible in the TOC. 30 | * E.g. "toc_level": 1, to only show the top titles. 31 | * Use "toc_level": 0, to show all titles. 32 | */ 33 | "toc_level": 0, // default 0 34 | 35 | /* 36 | * These characters are allowed to appear before the parsed title within the comment 37 | * (if unsure leave as is) 38 | */ 39 | "comment_chars": "/*#|", 40 | 41 | /* 42 | * Generate the TOC table on the save action. 43 | */ 44 | "toc_generate_on_save": false 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/README.txt: -------------------------------------------------------------------------------- 1 | 2 | About 3 | ---------------- 4 | 5 | This is a simple test suite for Table of comments. 6 | It emulates the standard unit test functionality of 7 | test class, methods and assertions. 8 | 9 | 10 | Setup 11 | ---------------- 12 | 13 | The test suite is configured within __init__.py regarding 14 | which test classes to run. 15 | 16 | Each new test class needs to be imported into __init__, 17 | listed within the reload_modules() function and listed within 18 | the run() function as a test. 19 | 20 | Within each test class, every function starting with "test_" 21 | is run as a test 22 | 23 | To run this test suite open the command palette (Ctrl + Shift + P) 24 | to execute the command "Table of comments: Run Tests" 25 | 26 | 27 | Results 28 | ---------------- 29 | Each class and method is listed with a "." for the number of passed 30 | test assertions made and a "F" for failed assertions. 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | """ Unit Testing framework for sublime plugins """ 3 | 4 | import sys 5 | import sublime 6 | import os 7 | import imp 8 | from os import walk 9 | 10 | # 11 | # Add additional classes as required 12 | # 13 | if sys.version_info < (3, 0): 14 | from tests.test_get_comment_titles import TestGetCommentTitles 15 | from tests.test_level_depth import TestLevelDepth 16 | from tests.test_level_chars import TestLevelChars 17 | from tests.test_comment_syntax import TestCommentSyntax 18 | from tests.test_toc_output import TestTocOutput 19 | from .test_large_file import TestLargeFile 20 | from .test_get_sections import TestGetSections 21 | else: 22 | from .test_get_comment_titles import TestGetCommentTitles 23 | from .test_level_depth import TestLevelDepth 24 | from .test_level_chars import TestLevelChars 25 | from .test_comment_syntax import TestCommentSyntax 26 | from .test_toc_output import TestTocOutput 27 | from .test_large_file import TestLargeFile 28 | from .test_get_sections import TestGetSections 29 | 30 | 31 | # Returns list of properties for each test class names matching "test_*.py" 32 | def auto_get_test_modules(): 33 | f = [] 34 | tests_path = os.path.join( 35 | sublime.packages_path(), "Table of comments", "tests") 36 | for (dirpath, dirnames, filenames) in walk(tests_path): 37 | for filename in filenames: 38 | if filename.startswith('test_') and filename.endswith('.py'): 39 | path = filename.replace('.py', '') 40 | classname = '' 41 | bits = path.split('_') 42 | for bit in bits: 43 | classname += bit.title() 44 | f.append({ 45 | 'filename': filename, 46 | 'classname': classname, 47 | 'path': path 48 | }) 49 | return f 50 | 51 | 52 | # 53 | # Reload module functionality (borrowed from sublimelint for ease when 54 | # developing/debugging) 55 | # 56 | # Sublime Text auto loads the primary plugin module when the file is changed 57 | # but doesn't do this for sub modules 58 | # 59 | def reload_test_modules(): 60 | modules = auto_get_test_modules() 61 | load_module('testcase') 62 | for module in modules: 63 | load_module(module['path']) 64 | 65 | 66 | def load_module(path): 67 | basedir = os.getcwd() 68 | os.chdir(basedir) 69 | if sys.version_info < (3, 0): 70 | path = 'tests.'+path 71 | __import__(path) 72 | sys.modules[path] = reload(sys.modules[path]) 73 | else: 74 | imp.reload(eval(path)) 75 | 76 | 77 | # 78 | # Run the tests 79 | # 80 | def run(view, edit): 81 | reload_test_modules() 82 | reload_test_modules() 83 | reload_test_modules() 84 | view = create_new_view(view) 85 | view.set_name('Unit Tests') 86 | output = "Table of Comments Unit Tests\n" + "=" * 50 + "\n" 87 | 88 | # Load and create all the test classes from file system 89 | tests = [] 90 | modules = auto_get_test_modules() 91 | for module in modules: 92 | test_class = eval(module['classname'] + '(view, edit)') 93 | tests.append(test_class) 94 | 95 | # Customise test run (for specific items or order while developing) 96 | # tests = [ 97 | # TestGetSections(view, edit), 98 | # ] 99 | 100 | # Run tests for results 101 | for test in tests: 102 | output += get_test_output(test) 103 | 104 | # Append errors 105 | output += get_test_errors(tests) 106 | 107 | # Output test results 108 | output = "/*\n\n" + output + "\n\n*/" 109 | view.set_syntax_file('Packages/JavaScript/JavaScript.tmLanguage') 110 | view.replace(edit, sublime.Region(0, view.size()), '') 111 | view.insert(edit, 0, output) 112 | highlight(view) 113 | view.set_scratch(True) # Allow results to close without saivng 114 | 115 | 116 | # Highlight the test result regions 117 | def highlight(view): 118 | # regions = view.find_all('\.') # OK regions 119 | regions = view.find_all('F') # Error regions 120 | add = [] 121 | for region in regions: 122 | line = view.line(region) 123 | if view.substr(line).find('(') > 0 and view.rowcol(line.a)[0] > 0: 124 | add.append(region) 125 | add_region(view, 'table-of-comments-error', 'error', add) 126 | 127 | 128 | def add_region(view, key, style, add): 129 | if sys.version_info < (3, 0): 130 | view.add_regions(key, add, 'storage', sublime.DRAW_EMPTY) 131 | else: 132 | view.add_regions(key, add, 'storage', '', sublime.DRAW_NO_OUTLINE) 133 | 134 | 135 | def unhighlight(view): 136 | view.erase_regions('table-of-comments-ok') 137 | view.erase_regions('table-of-comments-error') 138 | 139 | 140 | def create_new_view(view): 141 | view = sublime.active_window().new_file() 142 | return view 143 | 144 | 145 | def get_test_output(test): 146 | output = "\nRunning " + test.title + "\n" + "-" * 50 + "\n" 147 | test.setup() 148 | output += test.run() 149 | test.teardown() 150 | return output 151 | 152 | 153 | def get_test_errors(tests): 154 | errors = [] 155 | for test in tests: 156 | if len(test.errors) > 0: 157 | errors += test.errors 158 | if len(errors) > 0: 159 | return "\n\n"+str(len(errors))+" errors were found\n" + \ 160 | ('=' * 50) + "\n\n - " + "\n - ".join(errors) 161 | 162 | return "\n" + ('=' * 50) + "\n" + "Yes! All tests from " + \ 163 | str(len(tests)) + " classes passed." 164 | -------------------------------------------------------------------------------- /tests/test_comment_syntax.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 0): 3 | import testcase 4 | from tableofcomments import TableOfComments 5 | else: 6 | from . import testcase 7 | from ..tableofcomments import TableOfComments 8 | 9 | 10 | # 11 | # Tests getting titles within a variety of comment syntaxes 12 | # 13 | class TestCommentSyntax(testcase.TestCase): 14 | 15 | title = "Test Comment Syntax" 16 | 17 | # Using find() to check if result is correct 18 | def test_javascript_syntax(self): 19 | # Setup 20 | self.set_syntax('javascript') 21 | self.set_settings({'level_char': '>'}) 22 | self.set_text(self.text_javascript()) 23 | # Test 24 | toc = TableOfComments(self.view, self.edit) 25 | titles = toc.get_comment_titles('string') 26 | self.check_titles_match(titles) 27 | 28 | def test_css_syntax(self): 29 | # Setup 30 | self.view.set_syntax_file('Packages/CSS/CSS.tmLanguage') 31 | self.set_syntax('css') 32 | self.set_settings({'level_char': '#'}) 33 | self.set_text(self.text_css()) 34 | # Test 35 | toc = TableOfComments(self.view, self.edit) 36 | titles = toc.get_comment_titles('string', 'test') 37 | self.check_titles_match(titles) 38 | 39 | def test_python_syntax(self): 40 | # Setup 41 | self.set_syntax('python') 42 | self.set_settings({'level_char': '>'}) 43 | self.set_text(self.text_python()) 44 | # Test 45 | toc = TableOfComments(self.view, self.edit) 46 | titles = toc.get_comment_titles('string') 47 | self.check_titles_match(titles) 48 | 49 | def check_titles_match(self, titles): 50 | for title in ['Heading 1', '- Heading 2', '-- Heading 3']: 51 | if title in titles: 52 | self.ok() 53 | else: 54 | self.error('Missing title ' + title) 55 | 56 | def text_javascript(self): 57 | return """ 58 | /* 59 | * > Heading 1 60 | */ 61 | var func = function(x){ 62 | return x > 3; 63 | } 64 | /* >> Heading 2 */ 65 | 66 | // >>> Heading 3 67 | 68 | """ 69 | 70 | def text_css(self): 71 | return """ 72 | /* 73 | * # Heading 1 74 | */ 75 | #id { display:none } /* Should not show */ 76 | 77 | /* ## Heading 2 */ 78 | 79 | /* ### Heading 3 */ 80 | 81 | """ 82 | 83 | def text_python(self): 84 | return """ 85 | # > Heading 1 86 | def function(x): 87 | return x > 3 88 | 89 | # >> Heading 2 90 | 91 | # >>> Heading 3 92 | 93 | """ 94 | -------------------------------------------------------------------------------- /tests/test_get_comment_titles.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 0): 3 | import testcase 4 | else: 5 | from . import testcase 6 | 7 | 8 | # 9 | # This is an example text function 10 | # 11 | # - Every function starting with "test_" is run within this testclass 12 | # - You set the input set_text() as well as any particular settings with 13 | # set_settings() 14 | # - Finally you can run find() or text_equals() to check results which appear 15 | # in the test output summary 16 | # 17 | class TestGetCommentTitles(testcase.TestCase): 18 | 19 | title = "Get Comment Titles (Core)" 20 | 21 | # Using find() to check if result is correct 22 | def test_get_basic(self): 23 | 24 | # Setup environment 25 | self.set_text(self.text_javascript()) 26 | self.set_settings({'level_char': '>'}) 27 | 28 | # Run the basic parse function and check them 29 | toc = self.get_plugin() 30 | titles = toc.get_comment_titles('string') 31 | for title in ['Heading 1', '- Heading 2', '-- Heading 3']: 32 | if title in titles: 33 | self.ok() 34 | else: 35 | self.error('Missing title '+title) 36 | 37 | # Text used to perform tests on 38 | def text_javascript(self): 39 | return """ 40 | /* 41 | * > Heading 1 42 | */ 43 | 44 | /* >> Heading 2 */ 45 | 46 | // >>> Heading 3 47 | 48 | """ 49 | 50 | # Fix for #26 51 | def test_comment_char_within_comment(self): 52 | self.set_syntax('javascript') 53 | toc = self.get_plugin() 54 | 55 | # Check with "-" char 56 | self.set_settings({'level_char': '-'}) 57 | self.set_text('/* - Foo - Bar */') 58 | titles = toc.get_comment_titles('string') 59 | self.assert_true('Foo - Bar' in titles) 60 | 61 | # Check with ">" char 62 | self.set_settings({'level_char': '>'}) 63 | self.set_text('/* > Modules => My Module (Test 4) */') 64 | titles = toc.get_comment_titles('string') 65 | self.assert_true('Modules => My Module (Test 4)' in titles) 66 | -------------------------------------------------------------------------------- /tests/test_get_sections.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sublime 3 | if sys.version_info < (3, 0): 4 | import testcase 5 | else: 6 | from . import testcase 7 | 8 | 9 | # 10 | # This is an example text function 11 | # 12 | # - Every function starting with "test_" is run within this testclass 13 | # - You set the input set_text() as well as any particular settings with 14 | # set_settings() 15 | # - Finally you can run find() or text_equals() to check results which appear 16 | # in the test output summary 17 | # 18 | class TestGetSections(testcase.TestCase): 19 | 20 | title = "Test Sections" 21 | 22 | # Using find() to check if result is correct 23 | def test_get_sections(self): 24 | self.set_text(self.text_javascript()) 25 | self.set_settings({'level_char': '>'}) 26 | toc = self.get_plugin() 27 | sections = toc.get_sections() 28 | 29 | # Should be 3 sections 30 | self.assert_true(len(sections) == 3) 31 | 32 | # Match up values 33 | test_regions = [ 34 | {'title': [1, 20], 'content': [21, 30], 'text': 'Heading 1'}, 35 | {'title': [31, 49], 'content': [50, 58], 'text': 'Heading 2'}, 36 | {'title': [59, 76], 'content': [77, 83], 'text': 'Heading 3'} 37 | ] 38 | 39 | for i in range(len(sections)): 40 | # Test title regions match 41 | self.assert_true( 42 | sections[i]['title_region'] == sublime.Region( 43 | test_regions[i]['title'][0], 44 | test_regions[i]['title'][1] 45 | ), 46 | 'Title region mismatch for item '+str(i) 47 | ) 48 | # Test content regions match 49 | self.assert_true( 50 | sections[i]['content_region'] == sublime.Region( 51 | test_regions[i]['content'][0], 52 | test_regions[i]['content'][1] 53 | ), 54 | 'Content region mismatch for item '+str(i) 55 | ) 56 | # Test section text matches 57 | self.assert_true( 58 | sections[i]['text'] == test_regions[i]['text'], 59 | 'Section "text" mismatch' 60 | ) 61 | 62 | # Text used to perform tests on 63 | def text_javascript(self): 64 | return """ 65 | /* 66 | * > Heading 1 67 | */ 68 | body { 69 | 70 | } 71 | /* >> Heading 2 */ 72 | div { 73 | 74 | } 75 | // >>> Heading 3 76 | a { 77 | 78 | } 79 | """ 80 | -------------------------------------------------------------------------------- /tests/test_large_file.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 0): 3 | import testcase 4 | else: 5 | from . import testcase 6 | 7 | 8 | # 9 | # This test function tests the duration for a large file 10 | # 11 | class TestLargeFile(testcase.TestCase): 12 | 13 | title = "Large amount of content" 14 | 15 | # Create a large text file, runs the plugin with the output time 16 | def test_large_file(self): 17 | text = '' 18 | depth = 1 19 | for i in range(100): 20 | text += self._test_text_title(depth) + '\n' 21 | text += self._test_text_body() + '\n' 22 | depth = 1 if depth == 5 else depth + 1 23 | self.set_settings({'level_char': '>'}) 24 | self.set_syntax('javascript') 25 | self.set_text(text) 26 | toc = self.get_plugin() 27 | toc.get_comment_titles() 28 | timers = toc.timers 29 | for key in timers: 30 | duration = "{0:.6f}".format(timers[key]) 31 | self.assert_true( 32 | timers[key] < 0.9, 33 | '"'+key+'" takes too long ('+duration+' seconds)' 34 | ) 35 | 36 | 37 | # 38 | # Initial text used in above test 39 | # 40 | 41 | def _test_text_title(self, depth): 42 | return '/* ' + (depth * '>') + ' Lorem ipsum dolor sit amet */' 43 | 44 | def _test_text_body(self): 45 | return """ 46 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 47 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 48 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 49 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 50 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 51 | in culpa qui officia deserunt mollit anim id est laborum. 52 | """ 53 | -------------------------------------------------------------------------------- /tests/test_level_chars.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 0): 3 | import testcase 4 | else: 5 | from . import testcase 6 | 7 | 8 | # 9 | # This test function tests elements of the heading levels 10 | # 11 | class TestLevelChars(testcase.TestCase): 12 | 13 | title = "Heading Level Characters" 14 | 15 | def test_level_chars(self): 16 | # Check for "#" as level char 17 | self.set_text(self._test_text1()) 18 | self.set_settings({'level_char': '#'}) 19 | self.run_plugin() 20 | self.find('* Heading 1') 21 | 22 | # Check for "-" as level char 23 | self.set_text(self._test_text2()) 24 | self.set_settings({'level_char': '-'}) 25 | self.run_plugin() 26 | self.find('* Heading 1') 27 | 28 | # Check for ":" as level char 29 | self.set_text(self._test_text3()) 30 | self.set_settings({'level_char': ':'}) 31 | self.run_plugin() 32 | self.find('* Heading 1') 33 | 34 | 35 | # 36 | # Initial text used in above tests 37 | # 38 | 39 | def _test_text1(self): 40 | return """ 41 | /* 42 | * TOC 43 | */ 44 | 45 | // # Heading 1 46 | 47 | // ## Heading 2 48 | 49 | """ 50 | 51 | def _test_text2(self): 52 | return """ 53 | /* 54 | * TOC 55 | */ 56 | 57 | // - Heading 1 58 | 59 | // -- Heading 2 60 | 61 | """ 62 | 63 | def _test_text3(self): 64 | return """ 65 | /* 66 | * TOC 67 | */ 68 | 69 | // : Heading 1 70 | 71 | // :: Heading 2 72 | 73 | """ 74 | -------------------------------------------------------------------------------- /tests/test_level_depth.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 0): 3 | import testcase 4 | else: 5 | from . import testcase 6 | 7 | 8 | # 9 | # Tests elements of the heading levels 10 | # 11 | class TestLevelDepth(testcase.TestCase): 12 | 13 | title = "Test Level Depth" 14 | 15 | # Test that headings can go at least to 16 | def test_level_depth_unlimited(self): 17 | self.set_settings({'toc_level': 0}) 18 | self.set_text(self.text()) 19 | self.run_plugin() 20 | self.find('* Heading 1') 21 | self.find('----- Heading 6') 22 | 23 | # Test that the the "toc_level" setting 24 | def test_level_depth_limited(self): 25 | self.set_settings({'toc_level': 2}) 26 | self.set_text(self.text()) 27 | self.run_plugin() 28 | if self.get_text().find('----- Heading 6') == -1: 29 | self.ok() 30 | else: 31 | self.error('Should not find heading level 6') 32 | 33 | def text(self): 34 | return """ 35 | /* 36 | * TOC 37 | * 38 | * Heading 1 39 | * +++++ Heading 6 40 | */ 41 | 42 | // > Heading 1 43 | 44 | // >>>>>> Heading 6 45 | 46 | """ 47 | -------------------------------------------------------------------------------- /tests/test_toc_output.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info < (3, 0): 3 | import testcase 4 | else: 5 | from . import testcase 6 | 7 | 8 | # 9 | # Tests that the TOC output list is correct 10 | # 11 | class TestTocOutput(testcase.TestCase): 12 | 13 | title = "TOC Output" 14 | 15 | # Using find() to check if result is correct 16 | def test_toc_basic(self): 17 | # Setup environment 18 | self.set_text(self.text()) 19 | self.set_settings({'level_char': '>', 'toc_char': '-'}) 20 | self.run_plugin() 21 | 22 | # This checks that the headings appear 23 | self.find('* Heading 1') 24 | self.find('* - Heading 2') 25 | self.find('* -- Heading 3') 26 | 27 | # This is a full text output test (which ) 28 | self.text_equals(self.result()) 29 | 30 | def test_toc_different_toc_char(self): 31 | # Setup environment 32 | self.set_text(self.text()) 33 | self.set_settings({'level_char': '>'}) 34 | self.set_settings({'toc_char': '+'}) 35 | self.run_plugin() 36 | self.find('* + Heading 2') 37 | 38 | # Text used to perform tests on 39 | def text(self): 40 | return """ 41 | /* 42 | * TOC 43 | */ 44 | 45 | // > Heading 1 46 | 47 | // >> Heading 2 48 | 49 | // >>> Heading 3 50 | """ 51 | 52 | def result(self): 53 | return """ 54 | /* 55 | * TOC 56 | * 57 | * Heading 1 58 | * - Heading 2 59 | * -- Heading 3 60 | */ 61 | 62 | // > Heading 1 63 | 64 | // >> Heading 2 65 | 66 | // >>> Heading 3 67 | """ 68 | -------------------------------------------------------------------------------- /tests/testcase.py: -------------------------------------------------------------------------------- 1 | """ Default class inherited by all test classes that provides basic behaviour 2 | to write a test class easily """ 3 | 4 | import sublime 5 | import inspect 6 | import re 7 | import sys 8 | 9 | if sys.version_info < (3, 0): 10 | from tableofcomments import TableOfComments 11 | else: 12 | from ..tableofcomments import TableOfComments 13 | 14 | 15 | class TestCase(): 16 | 17 | output = '' 18 | errors = [] 19 | 20 | def __init__(self, view, edit): 21 | self.output = '' 22 | self.errors = [] 23 | self.view = view 24 | self.edit = edit 25 | 26 | # Called before class is run 27 | def setup(self): 28 | self.view.set_syntax_file('Packages/JavaScript/JavaScript.tmLanguage') 29 | 30 | # Runs all test methods 31 | def run(self): 32 | self.backup_plugin_settings() 33 | self.set_settings({ 34 | 'level_char': '>', 35 | 'toc_char': '-' 36 | }) 37 | methods = self.get_test_methods() 38 | for testname in methods: 39 | self.output += testname + '() ' 40 | eval('self.'+testname+'()') 41 | self.output += '\n' 42 | self.restore_plugin_settings() 43 | return self.output 44 | 45 | # Called after class is run 46 | def teardown(self): 47 | pass 48 | 49 | # 50 | # Helper functions 51 | # 52 | 53 | def get_test_methods(self): 54 | test_methods = [] 55 | members = inspect.getmembers(self, predicate=inspect.ismethod) 56 | for name, func in members: 57 | if name.find("test_") == 0: 58 | test_methods.append(name) 59 | return test_methods 60 | 61 | def set_text(self, text): 62 | self.view.replace(self.edit, sublime.Region(0, self.view.size()), '') 63 | self.view.insert(self.edit, 0, text) 64 | 65 | def get_text(self): 66 | return self.view.substr(sublime.Region(0, self.view.size())) 67 | 68 | def get_plugin(self): 69 | # return '' 70 | return TableOfComments(self.view, self.edit) 71 | 72 | def run_plugin(self): 73 | self.view.run_command('table_of_comments') 74 | 75 | def set_syntax(self, syntax): 76 | shortcuts = { 77 | 'javascript': 'Packages/JavaScript/JavaScript.tmLanguage', 78 | 'python': 'Packages/Python/Python.tmLanguage', 79 | 'css': 'Packages/CSS/CSS.tmLanguage' 80 | } 81 | if syntax in shortcuts.keys(): 82 | syntax = shortcuts[syntax] 83 | self.view.set_syntax_file(syntax) 84 | 85 | # 86 | # Settings functions 87 | # (allows us to have differnt settings for different tests - and restore to 88 | # normal afterwards) 89 | # 90 | 91 | def backup_plugin_settings(self): 92 | self.settings = sublime.load_settings( 93 | 'tableofcomments.sublime-settings') 94 | self._original_settings = {} 95 | for name in ['toc_char', 'level_char', 'toc_level']: 96 | if self.settings.has(name): 97 | self._original_settings[name] = self.settings.get(name) 98 | 99 | def restore_plugin_settings(self): 100 | if self._original_settings: 101 | values = self._original_settings 102 | for name in values: 103 | self.settings.set(name, values[name]) 104 | sublime.save_settings('tableofcomments.sublime-settings') 105 | 106 | def set_settings(self, settings): 107 | for name in settings: 108 | self.settings.set(name, settings[name]) 109 | sublime.save_settings('tableofcomments.sublime-settings') 110 | 111 | # 112 | # Result functions 113 | # 114 | 115 | def error(self, text): 116 | self.errors.append(text) 117 | self.output += 'F' 118 | 119 | def ok(self): 120 | self.output += '.' 121 | 122 | # 123 | # Assert functions for unit tests 124 | # 125 | 126 | def assert_true(self, value, msg='Was not true'): 127 | if value is True: 128 | self.ok() 129 | return True 130 | else: 131 | self.error(msg) 132 | return False 133 | 134 | def assert_false(self, value, msg='Was not false'): 135 | if value is False: 136 | self.ok() 137 | return True 138 | else: 139 | self.error(msg) 140 | return False 141 | 142 | # Assert function to see if entire result text equals the sent text 143 | def text_equals(self, sent): 144 | text = self.get_text() 145 | if text.strip() == sent.strip(): 146 | self.ok() 147 | else: 148 | self.error( 149 | "Text not equal betwen \n# : From..." + "\n---" + text + 150 | "\n---\nto...\n---"+sent+"\n---" 151 | ) 152 | 153 | # Assert function to check for sent text witin result text 154 | def find(self, text): 155 | result = self.get_text() 156 | match = result.find(text) 157 | if match >= 0: 158 | self.ok() 159 | return True 160 | else: 161 | self.error("Couldn't find \"" + text + "\"") 162 | return False 163 | --------------------------------------------------------------------------------