├── .gitignore ├── Default.sublime-keymap ├── LICENSE ├── Main.sublime-menu ├── README.md ├── VulData.sublime-settings ├── VulHint.py └── plugin.sublime-settings /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | .DS_Store 91 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+space"], "command": "goto_next" } 3 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 5alt 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 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id" : "VulHint", 4 | "caption": "VulHint", 5 | "children": 6 | [ 7 | { 8 | "caption" : "enable", 9 | "command" : "enable" 10 | }, 11 | { 12 | "caption" : "disable", 13 | "command" : "disable" 14 | }, 15 | { 16 | "caption" : "clear", 17 | "command" : "clear" 18 | } 19 | ] 20 | } 21 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VulHint 2 | 3 | VulHint 是一款 sublime text 3 的代码审计插件。 4 | 5 | 插件包含一些预置的正则规则和提示的内容,在当前文本中匹配到代码的时候,会加框显示,鼠标移动到危险函数的时候,会展示提示信息。 6 | 7 | 在代码审计的时候,本插件可以针对相关知识点做出提醒。 8 | 9 | ## 漏洞提示 10 | 11 | 在编辑器行号前有标记符号,对规则匹配到的部分用方框标出。鼠标移动到该行会有相应提示。如果一行中有多个标记,则所有的提示信息都会出现,鼠标移动到某一个上则只会出现该规则的提示。 12 | 13 | `alt+space` 可以切换到下一个提示点。 14 | 15 | ## 数据扩展 16 | 17 | `VulData.sublime-settings` 中存放了相关的规则数据,可以方便的自行添加。 18 | 19 | 语言类型按照文件后缀名判断。每种语言类型下有不同的规则。 20 | 21 | 规则的格式为。 22 | ``` 23 | "rule_name": {"keyword":[], "discription": "test", "pattern": "regexp", bbais, ebias} 24 | ``` 25 | 26 | 其中 27 | * keyword 鼠标悬浮位置 28 | * discription 悬浮显示内容,html显示 29 | * pattern 正则表达式 30 | * abais 开始处偏移 31 | * bbais 结尾处偏移 32 | * enable 是否启用该条规则 33 | 34 | 本插件已经内置了php的一些规则,欢迎大家提交新的规则以及对现有规则进行优化。 35 | 36 | ## License 37 | 38 | MIT 39 | 40 | ## 联系 41 | 42 | http://5alt.me 43 | 44 | md5_salt [AT] qq.com 45 | 46 | ## 参考资料 47 | 48 | Seay代码审计系统 49 | 50 | https://www.sublimetext.com/docs/3/api_reference.html 51 | 52 | https://cnpagency.com/blog/creating-sublime-text-3-plugins-part-1/ 53 | 54 | https://github.com/bradrobertson/sublime-packages 55 | 56 | http://zxhfighter.github.io/blog/javascript/2013/07/30/sublime-plugin.html 57 | 58 | -------------------------------------------------------------------------------- /VulData.sublime-settings: -------------------------------------------------------------------------------- 1 | // author: md5_salt 2 | /* 3 | "rule_name": {"keyword":[], "discription": "test", "pattern": "regexp", bbais, ebias} 4 | keyword 鼠标悬浮位置 5 | discription 悬浮显示内容 6 | pattern 正则表达式 7 | abais 开始处偏移 8 | bbais 结尾处偏移 9 | enable 是否启用该条规则 10 | */ 11 | 12 | { 13 | "php":{ 14 | "extract": { 15 | "keyword": ["extract"], 16 | "discription": "extract默认覆盖已有变量", 17 | "pattern": "\\b(extract)\\s{0,5}\\(.{0,30}\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}\\s{0,5},{0,1}\\s{0,5}(EXTR_OVERWRITE){0,1}\\s{0,5}\\)", 18 | "abais": 0, 19 | "bbais": 0, 20 | "enable": 1 21 | }, 22 | "parse_str": { 23 | "keyword": ["parse_str"], 24 | "discription": "parse_str对字符串进行一次url解码,同名变量覆盖前面的", 25 | "pattern": "\\b(mb_){0,1}parse_str\\s{0,10}\\(.{0,40}\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}", 26 | "abais": 0, 27 | "bbais": -1, 28 | "enable": 1 29 | }, 30 | "parse_url": { 31 | "keyword": ["parse_url"], 32 | "discription": "", 33 | "pattern": "\\b(parse_url)\\(", 34 | "abais": 0, 35 | "bbais": -1, 36 | "enable": 1 37 | }, 38 | "weak_equal": { 39 | "keyword": ["=="], 40 | "discription": "弱类型比较", 41 | "pattern": "[^=!]==[^=]", 42 | "abais": 1, 43 | "bbais": -1, 44 | "enable": 1 45 | }, 46 | "class_dynamic_call": { 47 | "keyword": ["->$"], 48 | "discription": "动态调用", 49 | "pattern": "->\\$", 50 | "abais": 0, 51 | "bbais": 0, 52 | "enable": 1 53 | }, 54 | "preg_match": { 55 | "keyword": ["preg_match", "preg_match_all"], 56 | "discription": "是否有起始和结束标记,在m的情况下.不匹配换行%0a绕过,$匹配行尾或者换行符", 57 | "pattern": "\\b(preg_match|preg_match_all)\\(", 58 | "abais": 0, 59 | "bbais": -1, 60 | "enable": 1 61 | }, 62 | "preg_replace": { 63 | "keyword": ["preg_replace"], 64 | "discription": "preg_replace中的replacement部分可以有特殊含义的标记 \\0或者$0", 65 | "pattern": "\\b(preg_replace)\\(", 66 | "abais": 0, 67 | "bbais": -1, 68 | "enable": 1 69 | }, 70 | "in_array": { 71 | "keyword": ["in_array"], 72 | "discription": "是否非严格模式比较", 73 | "pattern": "\\b(in_array)\\(", 74 | "abais": 0, 75 | "bbais": -1, 76 | "enable": 1 77 | }, 78 | "array_search": { 79 | "keyword": ["array_search"], 80 | "discription": "是否非严格模式比较", 81 | "pattern": "\\b(array_search)\\(", 82 | "abais": 0, 83 | "bbais": -1, 84 | "enable": 1 85 | }, 86 | "unserialize": { 87 | "keyword": ["unserialize"], 88 | "discription": "是否完全可控,用UAF打", 89 | "pattern": "\\b(unserialize)\\(", 90 | "abais": 0, 91 | "bbais": -1, 92 | "enable": 1 93 | }, 94 | "file_operation":{ 95 | "keyword": ["unlink", "copy", "fwrite", "file_put_contents", "bzopen"], 96 | "discription": "跨目录,是否存在条件竞争,windows下通配符、NTFS流", 97 | "pattern": "\\b(unlink|copy|fwrite|file_put_contents|bzopen)\\s{0,10}\\(.{0,40}\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}", 98 | "abais": 0, 99 | "bbais": 0, 100 | "enable": 1 101 | }, 102 | "str_replace": { 103 | "keyword": ["str_replace"], 104 | "discription": "多次出现绕过", 105 | "pattern": "\\b(str_replace)\\(", 106 | "abais": 0, 107 | "bbais": -1, 108 | "enable": 1 109 | }, 110 | "urldecode": { 111 | "keyword": ["urldecode"], 112 | "discription": "二次url编码", 113 | "pattern": "\\b(urldecode)\\(", 114 | "abais": 0, 115 | "bbais": -1, 116 | "enable": 1 117 | }, 118 | "callback": { 119 | "keyword": ["uasort", "uksort", "usort", "array_diff_uassoc", "array_diff_ukey", "array_filter", "array_intersect_uassoc", "array_intersect_ukey", "array_map", "array_reduce", "array_udiff_assoc", "array_udiff_uassoc", "array_udiff_uassoc", "array_udiff", "array_uintersect_assoc", "array_uintersect_uassoc", "array_uintersect_uassoc", "array_uintersect", "array_walk_recursive", "array_walk", "call_user_func_array", "call_user_func", "preg_replace_callback"], 120 | "discription": "回调函数", 121 | "pattern": "\\b(uasort|uksort|usort|array_diff_uassoc|array_diff_ukey|array_filter|array_intersect_uassoc|array_intersect_ukey|array_map|array_reduce|array_udiff_assoc|array_udiff_uassoc|array_udiff_uassoc|array_udiff|array_uintersect_assoc|array_uintersect_uassoc|array_uintersect_uassoc|array_uintersect|array_walk_recursive|array_walk|call_user_func_array|call_user_func|preg_replace_callback)\\(", 122 | "abais": 0, 123 | "bbais": -1, 124 | "enable": 1 125 | }, 126 | "function_dynamic_call": { 127 | "keyword": [], 128 | "discription": "$a()动态函数调用", 129 | "pattern": "\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}\\s{0,5}\\(", 130 | "abais": 0, 131 | "bbais": -1, 132 | "enable": 1 133 | }, 134 | "double$": { 135 | "keyword": [], 136 | "discription": "$$可能变量覆盖", 137 | "pattern": "\\${{0,1}\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}\\s{0,4}=\\s{0,4}.{0,20}\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}", 138 | "abais": 0, 139 | "bbais": 0, 140 | "enable": 1 141 | }, 142 | "system":{ 143 | "keyword": ["system", "passthru", "pcntl_exec", "shell_exec", "escapeshellcmd", "exec"], 144 | "discription": "命令执行", 145 | "pattern": "\\b(system|passthru|pcntl_exec|shell_exec|escapeshellcmd|exec)\\s{0,10}\\(.{0,40}\\$\\w{1,20}((\\[[\"']|\\[)\\${0,1}[\\w\\[\\]\"']{0,30}){0,1}", 146 | "abais": 0, 147 | "bbais": 0, 148 | "enable": 1 149 | }, 150 | "rand":{ 151 | "keyword": ["rand", "mt_rand"], 152 | "discription": "随机数预测", 153 | "pattern": "\\b(rand|mt_rand|srand|mt_srand)\\(", 154 | "abais": 0, 155 | "bbais": 0, 156 | "enable": 1 157 | }, 158 | "iconv":{ 159 | "keyword": ["iconv"], 160 | "discription": "字符串编码转换,字符串截断问题,宽字节问题,Unicode替换问题", 161 | "pattern": "\\b(iconv)\\(", 162 | "abais": 0, 163 | "bbais": -1, 164 | "enable": 1 165 | }, 166 | "json_decode":{ 167 | "keyword": ["json_decode"], 168 | "discription": "json解码,可能存在弱类型比较问题", 169 | "pattern": "\\b(json_decode)\\(", 170 | "abais": 0, 171 | "bbais": 0, 172 | "enable": 1 173 | }, 174 | "sprintf":{ 175 | "keyword": ["sprintf", "vsprintf"], 176 | "discription": "php的格式化字符串问题,格式化字符串是否可控", 177 | "pattern": "\\b(sprintf|vsprintf)\\s{0,10}\\(", 178 | "abais": 0, 179 | "bbais": -1, 180 | "enable": 1 181 | }, 182 | "env_xss":{ 183 | "keyword": ["PATH_INFO", "PHP_SELF"], 184 | "discription": "这两个环境变量没有经过url编码,可能引发xss", 185 | "pattern": "\\b(PATH_INFO|PHP_SELF)\\(", 186 | "abais": 0, 187 | "bbais": 0, 188 | "enable": 1 189 | }, 190 | "escapeshellarg":{ 191 | "keyword": ["escapeshellarg"], 192 | "discription": "将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,不能用escapeshellcmd再次处理", 193 | "pattern": "\\b(escapeshellarg)\\(", 194 | "abais": 0, 195 | "bbais": 0, 196 | "enable": 1 197 | }, 198 | "escapeshellcmd":{ 199 | "keyword": ["escapeshellcmd"], 200 | "discription": "会对&#;|*?~<>^()[]{}$\\, \\x0A 和 \\xFF进行转义,'和 \"仅在不配对儿的时候被转义,不能用escapeshellarg再次处理", 201 | "pattern": "\\b(escapeshellcmd)\\(", 202 | "abais": 0, 203 | "bbais": 0, 204 | "enable": 1 205 | }, 206 | "basename":{ 207 | "keyword": ["basename"], 208 | "discription": "在win平台下,`\\/`都可以作为basename的分隔符,但是在Linux平台下只有`/`可以作为分隔。windows下之前如果用了addslashes会导致文件名中单引号逃逸。", 209 | "pattern": "\\b(basename)\\(", 210 | "abais": 0, 211 | "bbais": 0, 212 | "enable": 1 213 | }, 214 | "getimagesize":{ 215 | "keyword": ["getimagesize"], 216 | "discription": "windows平台如果传入路径可控,可用于猜测文件夹路径(dedecms找后台)", 217 | "pattern": "\\b(getimagesize)\\(", 218 | "abais": 0, 219 | "bbais": 0, 220 | "enable": 1 221 | }, 222 | "$_SERVER":{ 223 | "keyword": ["$_SERVER"], 224 | "discription": "$_SERVER 大多没过滤,伪造host等,自行解析REQUEST_URI和$_GET结果不一致,PHP_SELF部分可控", 225 | "pattern": "\\b(\\$_SERVER\\[)\\(", 226 | "abais": 0, 227 | "bbais": 0, 228 | "enable": 1 229 | } 230 | }, 231 | "js":{ 232 | "window.addEventListener":{ 233 | "keyword": ["addEventListener"], 234 | "discription": "监听message事件", 235 | "pattern": "\\b(addEventListener)\\s{0,10}\\(['\"]\\b(message)['\"]", 236 | "abais": 0, 237 | "bbais": 0, 238 | "enable": 1 239 | }, 240 | "chrome.runtime.onMessage":{ 241 | "keyword": ["onMessage"], 242 | "discription": "插件onMessage", 243 | "pattern": "\\b(chrome\\.runtime\\.onMessage)", 244 | "abais": 0, 245 | "bbais": 0, 246 | "enable": 1 247 | }, 248 | "chrome.runtime.onMessageExternal":{ 249 | "keyword": ["onMessageExternal"], 250 | "discription": "插件接收外部消息onMessageExternal", 251 | "pattern": "\\b(chrome\\.runtime\\.onMessageExternal)", 252 | "abais": 0, 253 | "bbais": 0, 254 | "enable": 1 255 | }, 256 | "chrome.runtime.onConnectExternal":{ 257 | "keyword": ["onConnectExternal"], 258 | "discription": "插件接收外部连接onConnectExternal", 259 | "pattern": "\\b(chrome\\.runtime\\.onConnectExternal)", 260 | "abais": 0, 261 | "bbais": 0, 262 | "enable": 1 263 | }, 264 | "location":{ 265 | "keyword": ["location"], 266 | "discription": "location操作", 267 | "pattern": "\\b(location)[\\.\\s=]", 268 | "abais": 0, 269 | "bbais": -1, 270 | "enable": 1 271 | }, 272 | "innerHTML":{ 273 | "keyword": ["innerHTML"], 274 | "discription": "innerHTML操作", 275 | "pattern": "\\.innerHTML", 276 | "abais": 0, 277 | "bbais": 0, 278 | "enable": 1 279 | }, 280 | "html":{ 281 | "keyword": ["html"], 282 | "discription": "jQuery .html() 操作", 283 | "pattern": "\\.html\\s{0,10}\\(\\s{0,10}[^)]", 284 | "abais": 1, 285 | "bbais": -2, 286 | "enable": 1 287 | }, 288 | "document.write":{ 289 | "keyword": ["write"], 290 | "discription": "document.write", 291 | "pattern": "\\b(document\\.write)\\s{0,10}\\(", 292 | "abais": 0, 293 | "bbais": -1, 294 | "enable": 1 295 | }, 296 | "eval":{ 297 | "keyword": ["eval"], 298 | "discription": "eval", 299 | "pattern": "\\b(eval)\\s{0,10}\\(", 300 | "abais": 0, 301 | "bbais": -1, 302 | "enable": 1 303 | }, 304 | "setTimeout":{ 305 | "keyword": ["setTimeout"], 306 | "discription": "setTimeout第一个参数是字符串", 307 | "pattern": "\\b(setTimeout)\\s{0,10}\\(\\s{0,10}['\"]", 308 | "abais": 0, 309 | "bbais": -1, 310 | "enable": 1 311 | }, 312 | "setInterval":{ 313 | "keyword": ["setInterval"], 314 | "discription": "setInterval第一个参数是字符串", 315 | "pattern": "\\b(setInterval)\\s{0,10}\\(\\s{0,10}['\"]", 316 | "abais": 0, 317 | "bbais": -1, 318 | "enable": 1 319 | }, 320 | } 321 | 322 | } -------------------------------------------------------------------------------- /VulHint.py: -------------------------------------------------------------------------------- 1 | # author: md5_salt 2 | import sublime 3 | import sublime_plugin 4 | 5 | import re 6 | 7 | g_regions = [] 8 | g_region_lines = [] 9 | g_jump_index = 0 10 | 11 | g_line_regions = {} 12 | 13 | class VulHint(sublime_plugin.EventListener): 14 | lang = None 15 | data = {} 16 | 17 | def on_load_async(self, view): 18 | if not sublime.load_settings("plugin.sublime-settings").get("enable", 1): 19 | return 20 | self.init(view) 21 | self.mark_vul(view) 22 | get_lines(view) 23 | 24 | def on_post_save_async(self, view): 25 | if not sublime.load_settings("plugin.sublime-settings").get("enable", 1): 26 | return 27 | global g_regions 28 | self.init(view) 29 | self.mark_vul(view) 30 | get_lines(view) 31 | 32 | def on_hover(self, view, point, hover_zone): 33 | if not sublime.load_settings("plugin.sublime-settings").get("enable", 1): 34 | return 35 | global g_regions 36 | global g_region_lines 37 | global g_jump_index 38 | 39 | global g_line_regions 40 | 41 | if not self.lang or not self.data: 42 | return 43 | #self.init(view) 44 | # locate smiles in the string. smiles string should be at the beginning and followed by tab (cxsmiles) 45 | # hovered_line_text = view.substr(view.word(point)).strip() 46 | #hovered_line_text = view.substr(view.line(point)).strip() 47 | if (hover_zone == sublime.HOVER_TEXT): 48 | word = view.substr(view.word(point)).strip() 49 | for key in g_regions: 50 | val = self.data[key] 51 | if word in val["keyword"]: 52 | hovered_text = '

%s

'%(val["discription"]) 53 | view.show_popup(hovered_text, 54 | flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, 55 | location=point) 56 | g_jump_index = g_region_lines.index(view.rowcol(point)[0]) 57 | return 58 | line = view.rowcol(point)[0] 59 | if g_line_regions.get(line): 60 | hovered_text = '' 61 | for key in g_line_regions.get(line): 62 | val = self.data[key] 63 | hovered_text += '

%s


'%(val["discription"]) 64 | view.show_popup(hovered_text, flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, location=point) 65 | g_jump_index = g_region_lines.index(view.rowcol(point)[0]) 66 | 67 | return 68 | 69 | def init(self, view): 70 | global g_regions 71 | clear_mark(view) 72 | g_regions = [] 73 | self.lang = self.guess_lang(view) 74 | if self.lang in ['html', 'htm']: 75 | self.lang = 'js' 76 | self.data = sublime.load_settings("VulData.sublime-settings").get(self.lang, {}) 77 | 78 | def mark_vul(self, view): 79 | global g_regions 80 | #print([self.data[i]["discription"] for i in self.data]) 81 | if not self.lang or not self.data: 82 | return 83 | for key,val in self.data.items(): 84 | if not val['enable']: continue 85 | vul = view.find_all(val['pattern']) 86 | if not vul: continue 87 | for i in vul: 88 | i.a += val["abais"] 89 | i.b += val["bbais"] 90 | view.add_regions(key, vul, "string", "cross", sublime.DRAW_OUTLINED|sublime.DRAW_STIPPLED_UNDERLINE) 91 | g_regions.append(key) 92 | 93 | 94 | def guess_lang(self, view=None, path=None, sublime_scope=None): 95 | if not view: 96 | return None 97 | filename = view.file_name() 98 | return filename.split('.')[-1].lower() 99 | 100 | 101 | def clear_mark(view): 102 | global g_regions 103 | if not g_regions: return 104 | for i in g_regions: 105 | view.erase_regions(i) 106 | 107 | def get_lines(view): 108 | global g_regions 109 | global g_region_lines 110 | global g_line_regions 111 | 112 | g_line_regions = {} 113 | g_region_lines = set() 114 | for region in g_regions: 115 | for i in view.get_regions(region): 116 | line = view.rowcol(i.a)[0] 117 | g_region_lines.add(line) 118 | if g_line_regions.get(line, None): 119 | g_line_regions[view.rowcol(i.a)[0]].add(region) 120 | else: 121 | g_line_regions[view.rowcol(i.a)[0]] = set([region]) 122 | g_region_lines = sorted(g_region_lines) 123 | 124 | 125 | 126 | class GotoNextCommand(sublime_plugin.TextCommand): 127 | 128 | def run(self, edit): 129 | global g_jump_index, g_region_lines 130 | # Convert from 1 based to a 0 based line number 131 | line = g_region_lines[g_jump_index] 132 | g_jump_index = (g_jump_index + 1)%len(g_region_lines) 133 | 134 | # Negative line numbers count from the end of the buffer 135 | if line < 0: 136 | lines, _ = self.view.rowcol(self.view.size()) 137 | line = lines + line + 1 138 | 139 | pt = self.view.text_point(line, 0) 140 | 141 | self.view.sel().clear() 142 | self.view.sel().add(sublime.Region(pt)) 143 | 144 | self.view.show(pt) 145 | 146 | class EnableCommand(sublime_plugin.TextCommand): 147 | def run(self, edit): 148 | sublime.load_settings("plugin.sublime-settings").set("enable", 1) 149 | sublime.save_settings("plugin.sublime-settings") 150 | 151 | class DisableCommand(sublime_plugin.TextCommand): 152 | def run(self, edit): 153 | sublime.load_settings("plugin.sublime-settings").set("enable", 0) 154 | sublime.save_settings("plugin.sublime-settings") 155 | 156 | class ClearCommand(sublime_plugin.TextCommand): 157 | def run(self, edit): 158 | clear_mark(self.view) -------------------------------------------------------------------------------- /plugin.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "enable": 1 3 | } --------------------------------------------------------------------------------