├── .gitignore ├── .npmignore ├── vendor ├── universal-ctags-darwin ├── universal-ctags-linux └── universal-ctags-win32.exe ├── keymaps └── yang-plugin-structure-view.json ├── menus └── structure-view.cson ├── .eslintignore ├── templates └── structure-view.html ├── lib ├── tag-generators │ ├── javascript-sub.js │ ├── css.js │ ├── html.js │ ├── universal.js │ ├── javascript.js │ └── .ctags ├── tag-parser.js ├── util.js ├── main.js ├── tag-generator.js └── structure-view.js ├── LICENSE.md ├── .eslintrc ├── CHANGELOG.md ├── styles └── structure-view.less ├── package.json ├── TODO.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.un~ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintignore 2 | .eslintrc 3 | CHANGELOG.md 4 | TODO.md 5 | -------------------------------------------------------------------------------- /vendor/universal-ctags-darwin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/structure-view/HEAD/vendor/universal-ctags-darwin -------------------------------------------------------------------------------- /vendor/universal-ctags-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/structure-view/HEAD/vendor/universal-ctags-linux -------------------------------------------------------------------------------- /vendor/universal-ctags-win32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/structure-view/HEAD/vendor/universal-ctags-win32.exe -------------------------------------------------------------------------------- /keymaps/yang-plugin-structure-view.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-workspace": { 3 | "ctrl-o": "structure-view:toggle" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /menus/structure-view.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | 'atom-text-editor': [ 3 | { 4 | 'label': 'Toggle Structure View' 5 | 'command': 'structure-view:toggle' 6 | } 7 | ] 8 | 'menu': [ 9 | { 10 | 'label': "View" 11 | 'submenu': [ 12 | {'label': 'Toggle Structure View', 'command': 'structure-view:toggle'} 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore output folder 2 | build/ 3 | demo/ 4 | 5 | # Ignore mix js or css 6 | **/*.min.js 7 | **/*-min.js 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules 39 | jspm_packages 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # IntelliJ project files 48 | .idea 49 | *.iml 50 | out 51 | gen 52 | -------------------------------------------------------------------------------- /templates/structure-view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 |
8 |
{{noTagHint}}
9 |
10 |
    11 |
12 |
13 |
14 |
 
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /lib/tag-generators/javascript-sub.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | import jsctags from 'jsctags'; 3 | import path from 'path'; 4 | 5 | export default { 6 | 7 | async parseFile(ctx) { 8 | ctx.dir = path.dirname(ctx.file); 9 | const self = this, 10 | tags = await new Promise(resolve => { 11 | jsctags(ctx, (e, tags) => { 12 | if (e) console.log(e); 13 | resolve(self.parseTags(tags)); 14 | }); 15 | }); 16 | return { 17 | list: tags, 18 | tree: null 19 | }; 20 | }, 21 | 22 | parseTags(tags) { 23 | let res = {}; 24 | for (let i in tags) { 25 | // jsctags only provides two type of tag kind: "var", "func" 26 | if ('v' === tags[i].kind) tags[i].kind = 'var'; 27 | else if ('f' === tags[i].kind) tags[i].kind = 'function'; 28 | 29 | res[tags[i].id] = { 30 | name: tags[i].name, 31 | type: tags[i].kind, 32 | lineno: tags[i].lineno, 33 | // namespace: tags[i].namespace, 34 | parent: tags[i].namespace ? tags[i].parent : null, 35 | id: tags[i].id 36 | }; 37 | } 38 | return res; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alibaba Group Holding Limited and other contributors. 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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | # Install related eslint pkgs as dev dependency first 3 | "parser": "babel-eslint", 4 | "extends": [ 5 | "eslint:recommended", 6 | # "plugin:react/recommended" 7 | ], 8 | "globals": { 9 | # For unit test 10 | "it": true, 11 | "describe": true, 12 | 13 | # Fro Structure-View 14 | "atom": true 15 | }, 16 | "env": { 17 | "browser": true, 18 | "node": true, 19 | "es6": true 20 | }, 21 | 22 | # Error level: 23 | # 0: no error 24 | # 1: warning 25 | # 2: error 26 | "rules": { 27 | "strict": "off", 28 | 29 | "no-empty": [0], 30 | "no-console": [0], 31 | "no-case-declarations": [1], 32 | "no-unused-vars": [1, { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }], 33 | "no-redeclare": [2, {"builtinGlobals": true}], 34 | 35 | # "react/no-find-dom-node": [0], 36 | # "react/display-name": [0], 37 | # "react/prop-types": [0], 38 | # "react/require-default-props": [0], 39 | # "react/forbid-prop-types": [0], 40 | # "react/no-array-index-key": [0], 41 | # "react/jsx-indent": [1], 42 | # "react/jsx-indent-props": [1], 43 | # "react/jsx-tag-spacing": [1], 44 | # "react/jsx-first-prop-new-line": [1], 45 | # "react/jsx-max-props-per-line": [1], 46 | # "react/jsx-closing-bracket-location": [1], 47 | # "react/sort-comp": [1], 48 | # "react/jsx-wrap-multilines": [1], 49 | # "react/self-closing-comp": [1], 50 | # "react/jsx-curly-spacing": [1], 51 | # "react/no-did-mount-set-state": [1] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/tag-generators/css.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | import css from 'css'; 3 | 4 | export default { 5 | parseFile(ctx) { 6 | const ast = css.parse(ctx.content).stylesheet; 7 | if (!ast || ast.parsingErrors.length > 0) { 8 | atom.notifications.addError(`Parsing CSS source code failed!`, { 9 | detail: ast.parsingErrors.join('\n'), 10 | dismissable: true 11 | }); 12 | return { 13 | list: {}, 14 | tree: null 15 | }; 16 | } 17 | 18 | const tags = {}; 19 | this.parseAst(tags, ast.rules); 20 | // Parent of first level node is stylesheet 21 | for (let i in tags) tags[i].parent = null; 22 | 23 | return { 24 | list: {}, 25 | tree: tags 26 | }; 27 | }, 28 | 29 | parseAst(tags, ast) { 30 | for (let key in ast) { 31 | const i = ast[key], 32 | line = i.position.start.line; 33 | if ('rule' === i.type) { 34 | const name = i.selectors.join(',\n'), 35 | id = `${line}-${name}`; 36 | tags[id] = { 37 | name: name, 38 | type: 'sel', 39 | lineno: line, 40 | parent: i.parent, 41 | id: id 42 | }; 43 | if (i.declarations.length > 0) { 44 | tags[id].child = {}; 45 | this.parseAst(tags[id].child, i.declarations); 46 | } 47 | } else if ('declaration' === i.type) { 48 | const name = i.property, 49 | // value = i.value, 50 | id = `${line}-${name}`; 51 | tags[id] = { 52 | name: name, 53 | type: 'prop', 54 | lineno: line, 55 | parent: i.parent, 56 | id: id 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.2.1 (2017.12.11) 2 | 3 | - Avoid parsing error when using export of format `export { A, B }`. 4 | 5 | ## v0.2.0 (2017.11.23) 6 | 7 | - New toolbox section is provided with four shortcut buttons. 8 | - Fix [#11](https://github.com/alibaba/structure-view/issues/11). Support export class/function expression. 9 | - Fix tag name of 'export default' pattern from 'exports' to 'export default'. 10 | - Change trigger event for folding tree view to double click by default (See [#13](https://github.com/alibaba/structure-view/issues/13)), and make this behavior configurable. 11 | 12 | ### v0.1.8 (2017.11.20) 13 | 14 | - Add settings to hide variables and properties for cleaner tree and easier navigation. 15 | - Fix structure view auto toggle problem: SV should not show up when it's hidden before and active pane is switched from non-editor item (such as `Settings`) to an editor. 16 | 17 | ### v0.1.7 (2017.11.05) 18 | 19 | - Fix parsing error for script tag with no child. 20 | 21 | ### v0.1.6 (2017.11.05) 22 | 23 | - See [#2](https://github.com/alibaba/structure-view/issues/2). Support inline Javascript parsing in HTML. 24 | - Fix line jump error when target row is folded. 25 | 26 | ### v0.1.5 (2017.10.24) 27 | 28 | - See [#9](https://github.com/alibaba/structure-view/pull/9). Fix line jump error when `Soft Wrap` in `Settings` is enabled. 29 | - Add "function" type support for XYplorer script. 30 | 31 | ### v0.1.4 (2017.09.07) 32 | 33 | - Fix wrong type in `.ctags` config file. 34 | - Display icon of tags generated by `ctags`. 35 | 36 | ### v0.1.3 (2017.09.06) 37 | 38 | - Use the better API `onDidChangeActiveTextEditor` for Atom of which version is newer than 1.17. 39 | 40 | ## v0.1.2 (2017.09.05) 41 | 42 | - Public release. 43 | - Based on `ctags`, provided specific parsers for Javascript, HTML and CSS. 44 | -------------------------------------------------------------------------------- /lib/tag-parser.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | import { Point } from 'atom'; 3 | import _forEach from 'lodash/forEach'; 4 | 5 | export default class TagParser { 6 | constructor(tags, lang) { 7 | this.tags = tags; 8 | this.lang = lang; 9 | } 10 | 11 | parser() { 12 | if (this.tags.tree) { 13 | this.tags.list = {}; 14 | this.treeToList(this.tags.list, this.tags.tree); 15 | return this.tags.tree; 16 | } else if (this.tags.list) { 17 | let res = {}, 18 | data = this.tags.list; 19 | if (Object.keys(data).length === 0) return res; 20 | 21 | // Let items without parent as root node 22 | let childs = [], 23 | tagSet = {}; 24 | _forEach(data, item => { 25 | item.position = new Point(item.lineno - 1); 26 | if (!item.parent) res[item.id] = item; 27 | else childs.push(item); 28 | tagSet[item.id] = item; 29 | }); 30 | 31 | let missed = []; 32 | _forEach(childs, item => { 33 | // Save missed child if cannot find its parent in all tags 34 | if (!tagSet[item.parent]) missed.push(item); 35 | else { 36 | if (!tagSet[item.parent].child) tagSet[item.parent].child = {}; 37 | tagSet[item.parent].child[item.id] = item; 38 | } 39 | }); 40 | 41 | if (missed) { 42 | _forEach(missed, item => { 43 | res[item.id] = item; 44 | }); 45 | } 46 | 47 | this.tags.tree = res; 48 | } 49 | } 50 | 51 | treeToList(list, tree) { 52 | const self = this; 53 | _forEach(tree, (item, index) => { 54 | if (item.child && Object.keys(item.child).length === 0) delete item.child; 55 | item.position = new Point(item.lineno - 1); 56 | list[index] = item; 57 | if (item.child) self.treeToList(list, item.child); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | import $ from 'jquery'; 3 | 4 | export default { 5 | getScrollDistance($child, $parent) { 6 | const viewTop = $parent.offset().top, 7 | viewBottom = viewTop + $parent.height(), 8 | scrollTop = $parent.scrollTop(), 9 | // scrollBottom = scrollTop + $parent.height(), 10 | elemTop = $child.offset().top, 11 | elemBottom = elemTop + $child.height(); 12 | 13 | const ret = { 14 | needScroll: true, 15 | distance: 0 16 | }; 17 | // Element is upon or under the view 18 | if ((elemTop < viewTop) || (elemBottom > viewBottom)) ret.distance = scrollTop + elemTop - viewTop; 19 | else ret.needScroll = false; 20 | 21 | return ret; 22 | }, 23 | 24 | selectTreeNode($target, vm, opts) { 25 | if ($target.is('span')) $target = $target.parent(); 26 | if ($target.is('div')) $target = $target.parent(); 27 | if ($target.is('li')) { 28 | // ".toggle" would be TRUE if it's double click 29 | if (opts && opts.toggle) { 30 | $target.hasClass('list-nested-item') && $target[$target.hasClass('collapsed') ? 'removeClass' : 'addClass']('collapsed'); 31 | } 32 | let oldVal = vm.treeNodeId, 33 | val = $target.attr('node-id'); 34 | 35 | // Same node 36 | if (oldVal === val) return; 37 | 38 | oldVal && $('div.structure-view>div.tree-panel>ol').find('li.selected').removeClass('selected'); 39 | $target.addClass('selected'); 40 | vm.treeNodeId = val; 41 | } 42 | }, 43 | 44 | notify(title, msg) { 45 | atom.notifications.addInfo(title, { detail: msg, dismissable: true }); 46 | }, 47 | 48 | alert(title, msg) { 49 | atom.confirm({ 50 | message: title, 51 | detailedMessage: msg, 52 | buttons: { 53 | Close: function() { 54 | return; 55 | } 56 | } 57 | }); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /lib/tag-generators/html.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import HtmlParser from 'htmlparser2'; 4 | 5 | export default { 6 | 7 | parseFile(ctx) { 8 | const self = this; 9 | this.scriptNode = []; 10 | 11 | return new Promise(resolve => { 12 | const handler = new HtmlParser.DomHandler((err, dom) => { 13 | if (err) console.log(err); 14 | else { 15 | let tags = self.parseTags(dom); 16 | resolve(tags); 17 | } 18 | }); 19 | const parser = new HtmlParser.Parser(handler); 20 | parser.write(ctx.content); 21 | parser.end(); 22 | }); 23 | }, 24 | 25 | parseTags(tags) { 26 | const lineCountStart = 1; // First line is no.1 27 | const res = {}; 28 | this.setTagsLineno(res, tags, lineCountStart); 29 | return { 30 | tree: res, 31 | list: {}, 32 | scriptNode: this.scriptNode 33 | }; 34 | }, 35 | 36 | setTagsLineno(res, tags, line) { 37 | tags.forEach((i, index) => { 38 | const childs = i.children, 39 | data = i.data; 40 | if (childs) { 41 | const id = `${line}-${i.name}-${index}`; 42 | 43 | // Make symbol as element with its class, like div.a-class 44 | let name = i.name, 45 | attr = i.attribs; 46 | if (attr && attr.class) { 47 | name += '.' + attr.class.replace(' ', '.'); 48 | } 49 | 50 | res[id] = { 51 | name: name, 52 | type: 'elem', 53 | lineno: line, 54 | parent: i.parent, 55 | id: id 56 | } 57 | if (childs.length > 0) { 58 | res[id].child = {}; 59 | line = this.setTagsLineno(res[id].child, childs, line); 60 | 61 | // Inline script in HTML 62 | if (i.name === 'script') { 63 | res[id].content = childs[0].data; 64 | this.scriptNode.push(res[id]); 65 | } 66 | } 67 | } 68 | else if (data && data.includes('\n')) line += data.split('\n').length - 1; 69 | }); 70 | 71 | return line; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /styles/structure-view.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | .structure-view { 4 | width: 100%; 5 | height: 100%; 6 | padding: 4px; 7 | overflow-y: auto; 8 | overflow-x: hidden; 9 | 10 | .mask { 11 | display: none; 12 | text-align: center; 13 | position: absolute; 14 | z-index: 1; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | background-color: hsla(0,0%,100%,.4); 20 | 21 | > div { 22 | height: 40%; 23 | } 24 | } 25 | 26 | .list-tree.has-collapsable-children .list-nested-item > .list-item::before { 27 | margin-right: 2px; 28 | } 29 | 30 | .full-menu { 31 | user-select: none; 32 | } 33 | 34 | div.symbol-mixed-block { 35 | display: flex; 36 | align-items: center; 37 | justify-content: flex-start; 38 | font-size: 14px; 39 | } 40 | 41 | div.list-item { 42 | margin-left: 2.5px; 43 | } 44 | 45 | div.icon-circle { 46 | min-width: 18px; 47 | height: 18px; 48 | border-radius: 50%; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | margin-right: 7px; 53 | } 54 | 55 | div.icon-S { 56 | background-color: #6cc644; 57 | } 58 | 59 | div.icon-P { 60 | background-color: #6e5494; 61 | } 62 | 63 | div.icon-C { 64 | background-color: #40b4e5; 65 | } 66 | 67 | div.icon-D { 68 | background-color: #f45ba3; 69 | } 70 | 71 | div.icon-I, 72 | div.icon-V { 73 | background-color: #fc6d26; 74 | } 75 | 76 | div.icon-F, 77 | div.icon-M { 78 | background-color: #ea4335; 79 | } 80 | 81 | div.icon-U { 82 | background-color: #6a737b; 83 | } 84 | 85 | div.icon-circle > span { 86 | font-size: 11px; 87 | color: white; 88 | font-weight: 600; 89 | } 90 | 91 | div.sv-toolbox { 92 | border-style: solid; 93 | border-top: 0; 94 | border-left: 0; 95 | border-right: 0; 96 | border-bottom-width: 1px; 97 | border-bottom-color: @background-color-selected; 98 | 99 | button { 100 | margin-bottom: 0.5em; 101 | } 102 | .btn.icon:before { 103 | bottom: 2px; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structure-view", 3 | "main": "./lib/main", 4 | "version": "0.2.1", 5 | "description": "Structure View for ATOM, just like Outline view in Eclipse or Structure tool window in IDEA / WebStorm, provides quick navigation for symbols of source code with a tree view.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/alibaba/structure-view" 9 | }, 10 | "license": "MIT", 11 | "engines": { 12 | "atom": ">=1.0.0 <2.0.0" 13 | }, 14 | "dependencies": { 15 | "css": "^2.2.1", 16 | "esprima": "^4.0.0", 17 | "htmlparser2": "^3.9.2", 18 | "jquery": "^3.2.1", 19 | "jsctags": "latest", 20 | "lodash": "^4.17.10", 21 | "vue": "1.0.27-csp" 22 | }, 23 | "activationCommands": { 24 | "atom-workspace": [ 25 | "structure-view:toggle", 26 | "structure-view:show", 27 | "structure-view:hide" 28 | ] 29 | }, 30 | "configSchema": { 31 | "ShowVariables": { 32 | "title": "Show Variables", 33 | "description": "If you don't need variables in the structure of file, just uncheck this config.", 34 | "type": "boolean", 35 | "default": true 36 | }, 37 | "ShowProperties": { 38 | "title": "Show Properties", 39 | "description": "If you don't need properties in the structure of file (such as CSS), just uncheck this config.", 40 | "type": "boolean", 41 | "default": true 42 | }, 43 | "DoubleClickToFoldTreeView": { 44 | "title": "Double Click To Fold Tree View", 45 | "description": "If this value is false, then select tag and toggle the tree view would all by single click.", 46 | "type": "boolean", 47 | "default": true 48 | }, 49 | "AutoscrollFromSource": { 50 | "title": "Autoscroll from Source (Beta)", 51 | "description": "Enable this feature to have Atom automatically move the focus in the Structure View to the node that corresponds to the code where the cursor is currently positioned in the editor.", 52 | "type": "boolean", 53 | "default": false 54 | } 55 | }, 56 | "keywords": [ 57 | "taglist", 58 | "tagbar", 59 | "symbols", 60 | "structure", 61 | "outline", 62 | "navigation", 63 | "ctags" 64 | ], 65 | "devDependencies": { 66 | "babel-eslint": "^10.0.1", 67 | "eslint": "^5.6.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/tag-generators/universal.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | import path from 'path'; 3 | import { BufferedProcess } from 'atom'; 4 | 5 | export default { 6 | parseFile(ctx) { 7 | const command = path.resolve(__dirname, '..', '..', 'vendor', `universal-ctags-${process.platform}`), 8 | defaultCtagsFile = require.resolve('./.ctags'), 9 | self = this; 10 | let tags = [], 11 | args = [`--options=${defaultCtagsFile}`, '--fields=KsS']; 12 | 13 | // Not used 14 | if (atom.config.get('structure-view.useEditorGrammarAsCtagsLanguage') && ctx.lang) { 15 | args.push(`--language-force=${ctx.lang}`); 16 | } 17 | args.push('-nf', '-', ctx.file); 18 | 19 | return new Promise(resolve => { 20 | return new BufferedProcess({ 21 | command: command, 22 | args: args, 23 | stdout(lines) { 24 | return (() => { 25 | let result = []; 26 | for (let line of Array.from(lines.split('\n'))) { 27 | let tag = self.parseTagLine(line.trim(), ctx.lang); 28 | if (tag) result.push(tags.push(tag)); 29 | else result.push(undefined); 30 | } 31 | return result; 32 | })(); 33 | }, 34 | // Ctags config file may has something wrong that lead to error info 35 | // TODO: error notification for better UX 36 | stderr(e) { 37 | console.error(e); 38 | }, 39 | exit() { 40 | // Tag properties: name, kind, type, lineno, parent, id 41 | let count = 0; 42 | for (let i in tags) { 43 | tags[i].id = count; 44 | count++; 45 | } 46 | resolve({ 47 | list: tags, 48 | tree: null 49 | }); 50 | } 51 | }); 52 | }); 53 | }, 54 | parseTagLine(line, lang) { 55 | let sections = line.split('\t'); 56 | if (sections.length > 3) { 57 | let tag = { 58 | name: sections[0], 59 | kind: sections[3], 60 | type: sections[3], 61 | lineno: parseInt(sections[2]), 62 | parent: null 63 | }; 64 | // Not work for HTML at least 65 | if ((lang === 'Python') && (tag.type === 'member')) { 66 | tag.type = 'function'; 67 | } 68 | return tag; 69 | } else { 70 | return null; 71 | } 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | import { CompositeDisposable } from 'atom'; 3 | import $ from 'jquery'; 4 | import StructureView from './structure-view'; 5 | import Util from './util'; 6 | 7 | export default { 8 | structureView: null, 9 | 10 | activate() { 11 | this.subscriptions = new CompositeDisposable(); 12 | this.subscriptions.add(atom.commands.add('atom-workspace', { 13 | 'structure-view:toggle': () => this.switch(), 14 | 'structure-view:show': () => this.switch('on'), 15 | 'structure-view:hide': () => this.switch('off'), 16 | })); 17 | }, 18 | 19 | deactivate() { 20 | this.subscriptions.dispose(); 21 | this.structureView.destroy(); 22 | }, 23 | 24 | serialize() {}, 25 | 26 | switch (stat) { 27 | let editors = atom.workspace.getTextEditors(); 28 | if (editors.length < 1 || 29 | (editors.length === 1 && !editors[0].getPath())) return Util.alert('WARN', 'No file is opened!'); 30 | 31 | if (!this.structureView) this.structureView = new StructureView(); 32 | 33 | const rightDock = atom.workspace.getRightDock(); 34 | try { 35 | // Whatever do these first for performance 36 | rightDock.getPanes()[0].addItem(this.structureView); 37 | rightDock.getPanes()[0].activateItem(this.structureView); 38 | } catch (e) { 39 | if (e.message.includes('can only contain one instance of item')) { 40 | this.handleOneInstanceError(); 41 | } 42 | } 43 | 44 | // Sometimes dock title is hidden for somehow, 45 | // so force recalculate here to redraw 46 | $('ul.list-inline.tab-bar.inset-panel').height(); 47 | 48 | if (!stat) { 49 | rightDock.toggle(); 50 | this.structureView.vm.viewShow = !this.structureView.vm.viewShow; 51 | } else if ('on' === stat) { 52 | rightDock.show(); 53 | this.structureView.vm.viewShow = true; 54 | } else if ('off' === stat) { 55 | rightDock.hide(); 56 | this.structureView.vm.viewShow = false; 57 | } 58 | if (rightDock.isVisible()) this.structureView.initialize(); 59 | }, 60 | 61 | handleOneInstanceError() { 62 | let activePane = null; 63 | const rightDock = atom.workspace.getRightDock(); 64 | atom.workspace.getPanes().forEach(pane => { 65 | pane.getItems().forEach(item => { 66 | if (item === this.structureView) activePane = pane; 67 | }); 68 | }); 69 | if (activePane) { 70 | activePane.destroyItem(this.structureView, true); 71 | this.structureView.destroy(); 72 | } 73 | 74 | rightDock.getPanes()[0].addItem(this.structureView); 75 | rightDock.getPanes()[0].activateItem(this.structureView); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO List 2 | 3 | ## Feature Baseline 4 | ------ 5 | 6 | ### Done 7 | 8 | | Item | Priority | 9 | | ---------------------------------------- | -------- | 10 | | Show basic variable types, class, function, reference in tree view | High | 11 | | Node format: [symbol name] : [symbol type/value/class] | High | 12 | | Auto scroll to source | High | 13 | | Auto refresh tree view | High | 14 | | Universal parser for popular language | High | 15 | | Professional suppor for HTML | High | 16 | | Professional suppor for CSS | High | 17 | | Professional suppor for Javascript | High | 18 | | Warning when no symbol or no editor opened | High | 19 | | New icon for tree node | High | 20 | | Auto detect and toggle view when pane changed | High | 21 | | Auto scroll from source | Medium | 22 | | Support for inline JS | High | 23 | | Expand all | Medium | 24 | | Collapse all | Medium | 25 | 26 | ### Planned 27 | 28 | | Item | Priority | 29 | | ---------------------------------------- | -------- | 30 | | Support for inline CSS | High | 31 | | Professional support for Python | High | 32 | | Professional support for C/C++ | High | 33 | | Professional support for TypeScript | High | 34 | | Filter for symbol quick search | Medium | 35 | | Professional suppor for Less / Sass / Stylus | Medium | 36 | | Professional suppor for JSX / Jade | Medium | 37 | | Sorting tree node by a to z | Low | 38 | | Sorting tree node by symbol type | Low | 39 | 40 | 41 | 42 | ## Settings 43 | 44 | ------ 45 | 46 | | Item | Priority | Status | 47 | | ----------------------- | -------- | ------- | 48 | | Auto scroll from source | Low | Done | 49 | | Auto scroll to source | Low | Planned | 50 | | View width | Low | Planned | 51 | | Auto toggle | Low | Planned | 52 | | Auto hide | Low | Planned | 53 | | Filter to hide symbol | Low | Planned | 54 | 55 | 56 | 57 | ## MISC & Details 58 | 59 | ------ 60 | 61 | - Unit test and coverage: 62 | - Test/Spec script using [Mocha](https://mochajs.org/) , refer to [symbols-view-spec](https://github.com/atom/symbols-view/blob/master/spec/symbols-view-spec.js) . 63 | - Coverage using [Istanbul](https://github.com/gotwarlost/istanbul) . 64 | - Auto expand all nodes of a path when `AutoscrollFromSource` is enabled. 65 | - Technical doc about how to contribute, including introduce architecture of this plugin, API, and other rules like coding style, main focus... 66 | -------------------------------------------------------------------------------- /lib/tag-generator.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | export default class TagGenerator { 7 | constructor(path1, scopeName) { 8 | this.path = path1; 9 | this.scopeName = scopeName; 10 | } 11 | 12 | getLanguage() { 13 | let needle; 14 | if (typeof this.path === 'string' && (needle = path.extname(this.path), ['.cson', '.gyp'].includes(needle))) { 15 | return 'Cson'; 16 | } 17 | 18 | return { 19 | 'source.c': 'c', 20 | 'source.cpp': 'cpp', 21 | 'source.clojure': 'lisp', 22 | 'source.coffee': 'coffeescript', 23 | 'source.css': 'css', 24 | 'source.css.less': 'less', 25 | 'source.css.scss': 'scss', 26 | 'source.gfm': 'markdown', 27 | 'source.go': 'go', 28 | 'source.java': 'java', 29 | 'source.js': 'javascript', 30 | 'source.es6': 'javascript', 31 | 'source.js.jsx': 'javascript', 32 | 'source.jsx': 'javascript', 33 | 'source.json': 'json', 34 | 'source.makefile': 'make', 35 | 'source.objc': 'c', 36 | 'source.objcpp': 'cpp', 37 | 'source.python': 'python', 38 | 'source.ruby': 'ruby', 39 | 'source.sass': 'sass', 40 | 'source.yaml': 'yaml', 41 | 'text.html': 'html', 42 | 'text.html.basic': 'html', 43 | 'text.html.php': 'php', 44 | 'source.livecodescript': 'liveCode', 45 | 'source.scilab': 'scilab', // Scilab 46 | 'source.matlab': 'scilab', // Matlab 47 | 'source.octave': 'scilab', // GNU Octave 48 | 49 | // For backward-compatibility with Atom versions < 0.166 50 | 'source.c++': 'cpp', 51 | 'source.objc++': 'cpp' 52 | }[this.scopeName]; 53 | } 54 | 55 | async generate() { 56 | if (!this.lang) this.lang = this.getLanguage(); 57 | if (!fs.statSync(this.path).isFile()) return {}; 58 | 59 | let Gen; 60 | try { 61 | Gen = require(`./tag-generators/${this.lang}`); 62 | } catch (e) { 63 | Gen = require('./tag-generators/universal'); 64 | } 65 | 66 | const ctx = { 67 | file: this.path, 68 | content: fs.readFileSync(this.path, 'utf8'), 69 | lang: this.lang 70 | }; 71 | let tags = await Gen.parseFile(ctx); // tags contains list and tree data structure 72 | 73 | // For inline script in HTML 74 | if (tags.scriptNode && tags.scriptNode.length) await this.inlineScriptHandler(tags.scriptNode, ctx); 75 | return tags; 76 | } 77 | 78 | async inlineScriptHandler(nodes, ctx) { 79 | let parser = require('./tag-generators/javascript'); 80 | for (let i in nodes) { 81 | let parent = nodes[i]; 82 | let tags = await parser.parseFile({ 83 | content: parent.content, 84 | file: ctx.file 85 | }); 86 | if (tags.tree && Object.keys(tags.tree).length) { 87 | parent.child = tags.tree; 88 | this.fixLineno(parent); 89 | } 90 | // If JS error exists, jsctags would work 91 | else if (tags.list) { 92 | if (!parent.child) parent.child = {}; 93 | for (let j in tags.list) { 94 | let item = tags.list[j]; 95 | parent.child[item.id] = item; 96 | } 97 | this.fixLineno(parent); 98 | } 99 | } 100 | } 101 | 102 | fixLineno(parent, baseLineno) { 103 | // Line number of root node is the base number 104 | if (!baseLineno) baseLineno = parent.lineno; 105 | for (let i in parent.child) { 106 | let child = parent.child[i]; 107 | child.lineno += baseLineno - 1; 108 | if (child.child) this.fixLineno(child, baseLineno); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Structure-View 2 | Structure View for [ATOM](https://atom.io/) editor, just like Outline view in Eclipse or Structure tool window in IDEA / WebStorm, provides quick navigation for symbols of source code with a tree view. 3 | 4 | ![demo](https://user-images.githubusercontent.com/8896124/30044182-61ee94c6-922e-11e7-8181-10122681a1d9.gif) 5 | 6 | *Pull requests are welcomed! Raise an issue [here](https://github.com/alibaba/structure-view/issues) if you have any question.* 7 | 8 | ## Table of Contents 9 | 10 | - [Installation](#installation) 11 | - [Language Support](#language-support) 12 | - [Usage](#usage) 13 | - [Settings](#settings) 14 | - [Contribution](#contribution) 15 | - [License](#license) 16 | - [TODO](#todo) 17 | 18 | 19 | 20 | 21 | ## Installation 22 | 23 | Two ways to install: 24 | 25 | - From command line: 26 | 27 | ```bash 28 | apm install structure-view 29 | ``` 30 | 31 | - From Atom editor: 32 | 33 | Settings/Preferences ➔ Packages ➔ Search for `structure-view` 34 | 35 | 36 | 37 | 38 | ## Language Support 39 | 40 | | Lanuage | File Extensions | AST Parser | 41 | | ---------- | ---------------------------------------- | ---------------------------------------- | 42 | | HTML | `.html` , `.njk` , `.xtpl` , ... | [htmlparser2](https://github.com/fb55/htmlparser2) | 43 | | CSS | `.css` | [css](https://github.com/reworkcss/css) | 44 | | Javascript | `.js` | [esprima](http://esprima.org/) / [jsctags](https://github.com/ramitos/jsctags) | 45 | | Others | `.coffe` , `.less` , `.scss` , `.sass` , `.yaml` , `.yml` , `.md` , `.markdown` , `.mdown` , `.mkd` , `.mkdown` , `.ron` , `.json` , `.cson` , `.gyp` , `.c` , `.cpp` , `.mm` , `.py`, `.rb` , `.php` , `.module` , `.go` , `.pl` , `.pod` , `.es6` , `.jsx` , `.es` , `.hx` , `.nim` , `.rs` , `.lc` , `.livecodescript` , `.irev` , `.sql` , `.bdy` , `.spc` , `.pls` , `plb` , `.ddl` , `.pks` , `.pkb` , `.sce` , `.sci` , `.m` , `.kla` , `.ini` | [ctags](http://ctags.sourceforge.net/) | 46 | 47 | 48 | 49 | ## Usage 50 | 51 | #### Commands 52 | 53 | You can find all these commands by [`Command Palette`](http://flight-manual.atom.io/getting-started/sections/atom-basics/). 54 | 55 | - `Structure View: Hide` 56 | - `Structure View: Show` 57 | - `Structure View: Toggle` 58 | 59 | #### Shortcut 60 | 61 | - `Ctrl-o` : `Structure View: Toggle` 62 | 63 | #### Operations 64 | 65 | - Single click: navigation of tag 66 | - Double click: collapse/expand the tree of selected tag 67 | 68 | 69 | 70 | 71 | ## Settings 72 | 73 | | Feature | Description | Default | 74 | | ------------------------------ | ---------------------------------------- | ------- | 75 | | Show Variables | If you don't need variables in the structure of file, just uncheck this config. | true | 76 | | Show Properties | If you don't need properties in the structure of file (such as CSS), just uncheck this config. | true | 77 | | Double Click To Fold Tree View | If this value is false, then select tag and toggle the tree view would all by single click. | true | 78 | | Autoscroll from Source (Beta) | Enable this feature to have Atom automatically move the focus in the Structure View to the node that corresponds to the code where the cursor is currently positioned in the editor. | false | 79 | 80 | 81 | 82 | ## Icon alphabet meaning 83 | 84 | ##### HTML 85 | 86 | - `<>` : Element 87 | 88 | ##### CSS 89 | 90 | - `S` : Selector 91 | - `P` : Property 92 | 93 | ##### Javascript 94 | 95 | - `C` : Class 96 | - `I` : Import 97 | - `F` : Function 98 | - `M` : Method 99 | - `V` : Variable 100 | 101 | #### Others 102 | 103 | - `U` : Unknown 104 | 105 | 106 | 107 | ## TODO 108 | 109 | See [`TODO.md`](./TODO.md). 110 | 111 | 112 | 113 | ## Contributing 114 | 115 | - Universal tag generator comes from [symbols-tree-view](https://github.com/xndcn/symbols-tree-view) 116 | 117 | 118 | 119 | 120 | ## License 121 | 122 | MIT 123 | -------------------------------------------------------------------------------- /lib/tag-generators/javascript.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | // More types support please refer to esprima source code 4 | const SINGLE_TAG_TYPE = [ 5 | 'ClassDeclaration', 6 | 'ExpressionStatement', 7 | 'ExportDefaultDeclaration', 8 | 'ExportNamedDeclaration', 9 | 'FunctionDeclaration', 10 | 'MethodDefinition' 11 | ]; 12 | const MULTI_TAGS_TYPE = [ 13 | 'ImportDeclaration', 14 | 'VariableDeclaration' 15 | ]; 16 | 17 | export default { 18 | 19 | init() { 20 | this.esprima = require('esprima'); 21 | }, 22 | 23 | async parseFile(ctx) { 24 | if (!this.esprima) this.init(); 25 | 26 | const esprima = this.esprima, 27 | tags = {}; 28 | 29 | let ast; 30 | try { 31 | ast = esprima.parseScript(ctx.content, { 32 | loc: true, 33 | tolerant: true 34 | }); 35 | } catch (e) { 36 | console.error(`${e}\n\nTry to use other parsing solution...`); 37 | // return { 38 | // err: `Error!!!\nLine number: ${e.lineNumber}\nDescription: ${e.description}` 39 | // }; 40 | const jsctags = require('./javascript-sub'); 41 | return (await jsctags.parseFile(ctx)); 42 | } 43 | 44 | this.parseDeclar(tags, ast.body); 45 | // Parent of first level node is script 46 | for (let i in tags) tags[i].parent = null; 47 | 48 | return { 49 | list: {}, 50 | tree: tags 51 | }; 52 | }, 53 | 54 | parseDeclar(tags, ast) { 55 | const self = this; 56 | ast.forEach(i => { 57 | let type = i.type, 58 | child = null, 59 | name, id; 60 | 61 | if (SINGLE_TAG_TYPE.includes(type)) { 62 | const line = i.loc.start.line; 63 | 64 | if ('ClassDeclaration' === type) { 65 | name = i.id.name; 66 | id = `${line}-${name}`; 67 | type = 'class'; 68 | 69 | if (i.body.body.length > 0) { 70 | child = {}; 71 | self.parseDeclar(child, i.body.body); 72 | } 73 | } 74 | 75 | // Only for `module.exports` now 76 | else if ('ExpressionStatement' === type) { 77 | let left = i.expression.left, 78 | right = i.expression.right; 79 | if (!left || !left.object || !left.property || !right) return; 80 | 81 | if ('module' !== left.object.name || 'exports' !== left.property.name) return; 82 | if ('ClassExpression' !== right.type && 'ObjectExpression' !== right.type) return; 83 | 84 | name = 'exports'; 85 | id = `${line}-${name}`; 86 | type = 'class'; 87 | child = {}; 88 | 89 | if ('ClassExpression' === right.type) self.parseDeclar(child, right.body.body); 90 | else if ('ObjectExpression' === right.type) self.parseExpr(child, right.properties); 91 | } 92 | 93 | /* 94 | * Pattern: export default expression; 95 | */ 96 | else if ('ExportDefaultDeclaration' === type) { 97 | name = 'export default'; 98 | id = `${line}-${name}`; 99 | type = 'class'; 100 | 101 | let dec = i.declaration; 102 | 103 | // Ignore 'export default XXX;', XXX should have been parsed before 104 | if ('ObjectExpression' === dec.type && 105 | dec.properties.length > 0) 106 | { 107 | child = {}; 108 | self.parseExpr(child, dec.properties); 109 | } 110 | else if ('ClassDeclaration' === dec.type && 111 | dec.body.body.length > 0) 112 | { 113 | child = {}; 114 | self.parseExpr(child, dec.body.body); 115 | } 116 | else if ('FunctionDeclaration' === dec.type && 117 | dec.body.body.length > 0) 118 | { 119 | type = 'function'; 120 | child = {}; 121 | self.parseDeclar(child, dec.body.body); 122 | } 123 | } 124 | 125 | /* 126 | * Pattern: export declaration. Declarations could be: 127 | * - class Foo {} 128 | * - function Foo {} 129 | */ 130 | else if ('ExportNamedDeclaration' === type) { 131 | let dec = i.declaration; 132 | 133 | if (!dec) { 134 | // TODO: for the case 'export { A, B }' 135 | if (dec.specifiers.length) {} 136 | 137 | return; 138 | } 139 | 140 | name = dec.id.name; 141 | id = `${line}-${name}`; 142 | type = 'class'; 143 | 144 | // Do not support variables now 145 | if (!name) return; 146 | 147 | if ('ClassDeclaration' === dec.type && 148 | dec.body.body.length > 0) 149 | { 150 | child = {}; 151 | self.parseExpr(child, dec.body.body); 152 | } 153 | else if ('FunctionDeclaration' === dec.type && 154 | dec.body.body.length > 0) 155 | { 156 | type = 'function'; 157 | child = {}; 158 | self.parseDeclar(child, dec.body.body); 159 | } 160 | 161 | } 162 | 163 | else if ('FunctionDeclaration' === type) { 164 | let params = []; 165 | i.params.forEach(p => { 166 | params.push(p.name); 167 | }); 168 | name = `${i.id.name}(${params.join(', ')})`; 169 | id = `${line}-${i.id.name}()`; 170 | type = 'function'; 171 | 172 | if (i.body.body.length > 0) { 173 | child = {}; 174 | self.parseDeclar(child, i.body.body); 175 | } 176 | } 177 | 178 | else if ('MethodDefinition' === type) { 179 | let params = []; 180 | i.value.params.forEach(p => { 181 | params.push(p.name); 182 | }); 183 | name = `${i.key.name}(${params.join(', ')})`; 184 | id = `${line}-${i.key.name}()`; 185 | type = 'method'; 186 | 187 | if (i.value.body.body.length > 0) { 188 | child = {}; 189 | self.parseDeclar(child, i.value.body.body); 190 | } 191 | } 192 | 193 | tags[id] = { 194 | name: name, 195 | type: type, 196 | lineno: line, 197 | parent: ast, 198 | child: child, 199 | id: id 200 | }; 201 | 202 | } else if (MULTI_TAGS_TYPE.includes(type)) { 203 | 204 | if ('ImportDeclaration' === type) { 205 | i.specifiers.forEach(sp => { 206 | let line = sp.loc.start.line; 207 | name = sp.local.name; 208 | id = `${line}-${name}`; 209 | type = 'import'; 210 | 211 | tags[id] = { 212 | name: name, 213 | type: type, 214 | lineno: line, 215 | parent: ast, 216 | child: child, 217 | id: id 218 | } 219 | }); 220 | } 221 | 222 | else if ('VariableDeclaration' === type) { 223 | i.declarations.forEach(v => { 224 | let line = v.loc.start.line; 225 | name = v.id.name; 226 | id = `${line}-${name}`; 227 | type = 'var'; 228 | 229 | if (v.init && 'CallExpression' === v.init.type) { 230 | let method = v.init.callee.property; 231 | if (method && method.name === 'extend') { 232 | child = {}; 233 | v.init.arguments.forEach(i => { 234 | if (i.properties) self.parseExpr(child, i.properties); 235 | }); 236 | } 237 | } else if (v.init && 'ObjectExpression' === v.init.type) { 238 | if (v.init.properties.length > 0) { 239 | child = {}; 240 | self.parseExpr(child, v.init.properties); 241 | } 242 | } 243 | 244 | tags[id] = { 245 | name: name, 246 | type: type, 247 | lineno: line, 248 | parent: ast, 249 | child: child, 250 | id: id 251 | } 252 | }); 253 | } 254 | } 255 | }); 256 | }, 257 | 258 | parseExpr(tags, ast) { 259 | const self = this; 260 | ast.forEach(i => { 261 | let type = i.value.type, 262 | line = i.loc.start.line, 263 | child = null, 264 | name, id; 265 | 266 | if ('FunctionExpression' === type) { 267 | let params = []; 268 | i.value.params.forEach(p => { 269 | params.push(p.name); 270 | }); 271 | 272 | name = `${i.key.name}(${params.join(', ')})`; 273 | id = `${line}-${i.key.name}()`; 274 | type = 'function'; 275 | 276 | if (i.value.body.body.length > 0) { 277 | child = {}; 278 | self.parseDeclar(child, i.value.body.body); 279 | } 280 | 281 | tags[id] = { 282 | name: name, 283 | type: type, 284 | lineno: line, 285 | parent: ast, 286 | child: child, 287 | id: id 288 | }; 289 | } else { 290 | type = 'prop'; 291 | name = i.key.value; 292 | if (i.key.value) name = i.key.value; 293 | else name = i.key.name; 294 | id = `${line}-${name}`; 295 | 296 | if (i.value.properties && i.value.properties.length > 0) { 297 | child = {}; 298 | self.parseExpr(child, i.value.properties); 299 | } 300 | 301 | tags[id] = { 302 | name: name, 303 | type: type, 304 | lineno: line, 305 | parent: ast, 306 | child: child, 307 | id: id 308 | }; 309 | } 310 | }); 311 | } 312 | }; 313 | -------------------------------------------------------------------------------- /lib/tag-generators/.ctags: -------------------------------------------------------------------------------- 1 | --langdef=CoffeeScript 2 | --langmap=CoffeeScript:.coffee 3 | --regex-CoffeeScript=/^[ \t]*(@?[a-zA-Z$_\.0-9]+)[ \t]*=[ \t]*.*/\1/v,variable/ 4 | --regex-CoffeeScript=/(^|=[ \t])*class ([A-Za-z_][A-Za-z0-9_]+\.)*([A-Za-z_][A-Za-z0-9_]+)( extends ([A-Za-z][A-Za-z0-9_.]*)+)?$/\3/c,class/ 5 | --regex-CoffeeScript=/^[ \t]*(module\.)?(exports\.)?@?(([A-Za-z][A-Za-z0-9_.]*)+):.*[-=]>.*$/\3/m,method/ 6 | --regex-CoffeeScript=/^[ \t]*(module\.)?(exports\.)?(([A-Za-z][A-Za-z0-9_.]*)+)[ \t]*=.*[-=]>.*$/\3/f,function/ 7 | --regex-CoffeeScript=/^[ \t]*@(([A-Za-z][A-Za-z0-9_.]*)+)[ \t]*=[^->\n]*$/\1/f,field/ 8 | --regex-CoffeeScript=/^[ \t]*@(([A-Za-z][A-Za-z0-9_.]*)+):[^->\n]*$/\1/f,static field/ 9 | --regex-CoffeeScript=/^[ \t]*(([A-Za-z][A-Za-z0-9_.]*)+):[^->\n]*$/\1/f,field/ 10 | 11 | --langdef=Css 12 | --langmap=Css:.css 13 | --langmap=Css:+.less 14 | --langmap=Css:+.scss 15 | --regex-Css=/^[ \t]*(.+)[ \t]*\{/\1/f,selector/ 16 | --regex-Css=/^[ \t]*(.+)[ \t]*,[ \t]*$/\1/f,selector/ 17 | 18 | --langdef=Sass 19 | --langmap=Sass:.sass 20 | --regex-Sass=/^[ \t]*([#.]*[a-zA-Z_0-9]+)[ \t]*$/\1/f,selector/ 21 | 22 | --langdef=Yaml 23 | --langmap=Yaml:.yaml 24 | --langmap=Yaml:+.yml 25 | --regex-Yaml=/^[ \t]*([a-zA-Z_0-9 ]+)[ \t]*\:[ \t]*/\1/f,function/ 26 | 27 | --regex-Html=/^[ \t]*<([a-zA-Z]+)[ \t]*.*id="([^"]+)".*>/\1#\2/m,member\tclass:id/ 28 | --regex-Html=/^[ \t]*<([a-zA-Z]+)[ \t]*.*class="([^"]+)".*>/\1.\2/m,member\tclass:class/ 29 | --regex-Html=/^[ \t]*<([a-zA-Z]+)[ \t]*>/\1/m,member\tclass:no-attr/ 30 | 31 | --langdef=Markdown 32 | --langmap=Markdown:.md 33 | --langmap=Markdown:+.markdown 34 | --langmap=Markdown:+.mdown 35 | --langmap=Markdown:+.mkd 36 | --langmap=Markdown:+.mkdown 37 | --langmap=Markdown:+.ron 38 | --regex-Markdown=/^#+[ \t]*([^#]+)/\1/f,member/ 39 | 40 | --langdef=Json 41 | --langmap=Json:.json 42 | --regex-Json=/^[ \t]*"([^"]+)"[ \t]*\:/\1/f,member/ 43 | 44 | --langdef=Cson 45 | --langmap=Cson:.cson 46 | --langmap=Cson:+.gyp 47 | --regex-Cson=/^[ \t]*'([^']+)'[ \t]*\:/\1/f,member/ 48 | --regex-Cson=/^[ \t]*"([^"]+)"[ \t]*\:/\1/f,member/ 49 | --regex-Cson=/^[ \t]*([^'"]+)[ \t]*\:/\1/f,member/ 50 | 51 | --langmap=C++:+.mm 52 | 53 | --langmap=Ruby:+(Rakefile) 54 | --regex-Ruby=/^[ \t]*describe[ \t]*['":]?([^'"]*)['"]?[ \t]*do/\1/d,describe/ 55 | --regex-Ruby=/^[ \t]*context[ \t]*['":]?([^'"]*)['"]?[ \t]*do/\1/c,context/ 56 | 57 | --langmap=Php:+.module 58 | 59 | --langdef=Go 60 | --langmap=Go:.go 61 | --regex-Go=/func([ \t]+\([^)]+\))?[ \t]+([a-zA-Z0-9_]+)/\2/f,func/ 62 | --regex-Go=/var[ \t]+([a-zA-Z_][a-zA-Z0-9_]*)/\1/v,var/ 63 | --regex-Go=/type[ \t]+([a-zA-Z_][a-zA-Z0-9_]*)/\1/t,type/ 64 | 65 | --langmap=perl:+.pod 66 | --regex-perl=/with[ \t]+([^;]+)[ \t]*?;/\1/w,role,roles/ 67 | --regex-perl=/extends[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/ 68 | --regex-perl=/use[ \t]+base[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/ 69 | --regex-perl=/use[ \t]+parent[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/ 70 | --regex-perl=/Mojo::Base[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/ 71 | --regex-perl=/^[ \t]*?use[ \t]+([^;]+)[ \t]*?;/\1/u,use,uses/ 72 | --regex-perl=/^[ \t]*?require[ \t]+((\w|\:)+)/\1/r,require,requires/ 73 | --regex-perl=/^[ \t]*?has[ \t]+['"]?(\w+)['"]?/\1/a,attribute,attributes/ 74 | --regex-perl=/^[ \t]*?\*(\w+)[ \t]*?=/\1/a,alias,aliases/ 75 | --regex-perl=/->helper\([ \t]?['"]?(\w+)['"]?/\1/h,helper,helpers/ 76 | --regex-perl=/^[ \t]*?our[ \t]*?[\$@%](\w+)/\1/o,our,ours/ 77 | --regex-perl=/^\=head1[ \t]+(.+)/\1/p,pod,Plain Old Documentation/ 78 | --regex-perl=/^\=head2[ \t]+(.+)/-- \1/p,pod,Plain Old Documentation/ 79 | --regex-perl=/^\=head[3-5][ \t]+(.+)/---- \1/p,pod,Plain Old Documentation/ 80 | 81 | --langdef=Javascript 82 | --langmap=javascript:.js.es6.es.jsx 83 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*\{/\5/,object/ 84 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*(\[|(new[ \t]+)?Array\()/\5/,array/ 85 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*[^"]'[^']*/\5/,string/ 86 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*(true|false)/\5/,boolean/ 87 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*[0-9]+/\5/,number/ 88 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*null/\5/,null/ 89 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|const|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*undefined/\5/,undefined/ 90 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*[^tfn0-9"'{\[]+([,;=]|$)/\5/,unknown/ 91 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*=[ \t]*.+([,;=]|$)/\5/,variable/ 92 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|([A-Za-z_$][A-Za-z0-9_$.]*\.)+))[ \t]*([A-Za-z_$][A-Za-z0-9_$.]*)[ \t]*[ \t]*([,;]|$)/\5/,undefined/ 93 | --regex-JavaScript=/(,|(;|^)[ \t]*)const[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*=[ \t]*.+([,;=]|$)/\3/,constant/ 94 | --regex-JavaScript=/(,|(;|^)[ \t]*(var|let|([A-Za-z_$][A-Za-z0-9_$.]*\.)*))[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*=[ \t]*function[ \t]*\(/\5/,function-expression/ 95 | --regex-JavaScript=/(,|(;|^))[ \t]*(([A-Za-z_$][A-Za-z0-9_$.]*\.)+prototype\.)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*=[ \t]*function[ \t]*\(/\4\5/,prototype-method/ 96 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*function[ \t]*\(/\2/,object-method/ 97 | --regex-JavaScript=/function[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*\([^)]*\)/\1/,function-declaration/ 98 | --regex-JavaScript=/(,|^|\*\/)[ \t]*(while|if|for|switch|function|([A-Za-z_$][A-Za-z0-9_$]*))[ \t]*\([^)]*\)[ \t]*\{/\3/,function/ 99 | --regex-JavaScript=/(,|^|\*\/|\{)[ \t]*get[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*\([ \t]*\)[ \t]*\{/get \2/,getter/ 100 | --regex-JavaScript=/(,|^|\*\/|\{)[ \t]*set[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*\([ \t]*([A-Za-z_$][A-Za-z0-9_$]*)?[ \t]*\)[ \t]*\{/set \2/,setter/ 101 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*\{/\2/,object/ 102 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*\[/\2/,array/ 103 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*[^"]'[^']*/\2/,string/ 104 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*(true|false)/\2/,boolean/ 105 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*[0-9]+/\2/,number/ 106 | --regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)[ \t]*:[ \t]*[^=]+([,;]|$)/\2/,variable/ 107 | --regex-javascript=/^[ \t]*(var|let|const)[ \t]+([A-Z][A-Za-z0-9_$]+)[ \t]*=[ \t]*function/\2/C,class/ 108 | --regex-javascript=/^[ \t]*class[ \t]+([A-Za-z0-9_$]+)/\1/C,class/ 109 | 110 | 111 | --langdef=haxe 112 | --langmap=haxe:.hx 113 | --regex-haxe=/^package[ \t]+([A-Za-z0-9_.]+)/\1/p,package/ 114 | --regex-haxe=/^[ \t]*[(@:macro|private|public|static|override|inline|dynamic)( \t)]*function[ \t]+([A-Za-z0-9_]+)/\1/f,function/ 115 | --regex-haxe=/^[ \t]*([private|public|static|protected|inline][ \t]*)+var[ \t]+([A-Za-z0-9_]+)/\2/v,variable/ 116 | --regex-haxe=/^[ \t]*package[ \t]*([A-Za-z0-9_]+)/\1/p,package/ 117 | --regex-haxe=/^[ \t]*(extern[ \t]*|@:native\([^)]*\)[ \t]*)*class[ \t]+([A-Za-z0-9_]+)[ \t]*[^\{]*/\2/c,class/ 118 | --regex-haxe=/^[ \t]*(extern[ \t]+)?interface[ \t]+([A-Za-z0-9_]+)/\2/i,interface/ 119 | --regex-haxe=/^[ \t]*typedef[ \t]+([A-Za-z0-9_]+)/\1/t,typedef/ 120 | --regex-haxe=/^[ \t]*enum[ \t]+([A-Za-z0-9_]+)/\1/t,typedef/ 121 | --regex-haxe=/^[ \t]*+([A-Za-z0-9_]+)(;|\([^)]*:[^)]*\))/\1/t,enum_field/ 122 | 123 | --langdef=Nim 124 | --langmap=Nim:.nim 125 | --regex-Nim=/^[\t\s]*proc\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/f,function/ 126 | --regex-Nim=/^[\t\s]*iterator\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/i,iterator/ 127 | --regex-Nim=/^[\t\s]*macro\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/m,macro/ 128 | --regex-Nim=/^[\t\s]*method\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/h,method/ 129 | --regex-Nim=/^[\t\s]*template\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/t,generics/ 130 | --regex-Nim=/^[\t\s]*converter\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/c,converter/ 131 | 132 | --langdef=Rust 133 | --langmap=Rust:.rs 134 | --regex-Rust=/^[ \t]*(#\[[^\]]\][ \t]*)*(pub[ \t]+)?(extern[ \t]+)?("[^"]+"[ \t]+)?(unsafe[ \t]+)?fn[ \t]+([a-zA-Z0-9_]+)/\6/f,function/ 135 | --regex-Rust=/^[ \t]*(pub[ \t]+)?type[ \t]+([a-zA-Z0-9_]+)/\2/T,typedef/ 136 | --regex-Rust=/^[ \t]*(pub[ \t]+)?enum[ \t]+([a-zA-Z0-9_]+)/\2/g,enum/ 137 | --regex-Rust=/^[ \t]*(pub[ \t]+)?struct[ \t]+([a-zA-Z0-9_]+)/\2/s,struct/ 138 | --regex-Rust=/^[ \t]*(pub[ \t]+)?mod[ \t]+([a-zA-Z0-9_]+)/\2/m,namespace/ 139 | --regex-Rust=/^[ \t]*(pub[ \t]+)?static[ \t]+([a-zA-Z0-9_]+)/\2/c,constant/ 140 | --regex-Rust=/^[ \t]*(pub[ \t]+)?trait[ \t]+([a-zA-Z0-9_]+)/\2/t,method/ 141 | --regex-Rust=/^[ \t]*(pub[ \t]+)?impl([ \t\n]*<[^>]*>)?[ \t]+(([a-zA-Z0-9_:]+)[ \t]*(<[^>]*>)?[ \t]+(for)[ \t]+)?([a-zA-Z0-9_]+)/\4 \6 \7/i,generic/ 142 | --regex-Rust=/^[ \t]*macro_rules![ \t]+([a-zA-Z0-9_]+)/\1/d,macro/ 143 | 144 | --langdef=LiveCode 145 | --langmap=LiveCode:.livecodescript 146 | --langmap=LiveCode:+.lc 147 | --langmap=LiveCode:+.irev 148 | --regex-LiveCode=/^[ \t]*(private[ \t]+)*(on|command)[ \t]*([A-Za-z0-9_]+)/\3/h,method/ 149 | --regex-LiveCode=/^[ \t]*(private[ \t]+)*function[ \t]+([A-Za-z0-9_]+)/\2/f,function/ 150 | --regex-LiveCode=/^[ \t]*constant[ \t]+([A-Za-z0-9_]+)/\1/c,constant/ 151 | --regex-LiveCode=/^[ \t]*(global|local)[ \t]+([A-Za-z0-9_]+)/\2/v,variable/ 152 | 153 | --langdef=sql 154 | --langmap=sql:.sql 155 | --langmap=sql:+.bdy 156 | --langmap=sql:+.spc 157 | --langmap=sql:+.pls 158 | --langmap=sql:+.plb 159 | --langmap=sql:+.ddl 160 | --langmap=sql:+.pks 161 | --langmap=sql:+.pkb 162 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(table)[\t]+([^.]+\.)?([a-zA-Z0-9_@.]+)/\4/t,table/i 163 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(procedure|package)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/p,procedure/i 164 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(function)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/f,function/i 165 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(trigger)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/r,trigger/i 166 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(event)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/e,event/i 167 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(index)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/i,index/i 168 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9\t]*)?(publication|subscription to|synchronization user)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/m,mobilink/i 169 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9 \t]*)?(variable)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/v,variable/i 170 | --regex-sql=/^[ \t]*create[ \t]+([a-zA-Z0-9\t]*)?(rule|schema|server|datatype|database|message)[\t]+([^.]+\.)?"?([a-zA-Z0-9_@.]+)/\4/o,other/I 171 | 172 | --langdef=Scilab 173 | --langmap=Scilab:.sce 174 | --langmap=Scilab:+.sci 175 | --langmap=Scilab:+.m 176 | --langmap=Scilab:+.kla 177 | --regex-Scilab=#(^///[ \t]*\$begin[ \t])+ScriptHeader#Script-Header#s,package#i 178 | --regex-Scilab=#^[ \t]*function.*[ \t]]*([a-zA-Z_][a-zA-Z0-9_]*)\(#\1#q,function# 179 | 180 | --langdef=ini 181 | --langmap=ini:.ini 182 | --regex-ini=/^[ \t]*(\[.+\])[ \t]*$/\1/t,generics/ 183 | 184 | --langdef=XYplorer 185 | --langmap=XYplorer:.xys 186 | --langmap=XYplorer:+.xyi 187 | --regex-XYplorer=/^function[ \t]+([a-zA-Z0-9_]*)[ \t]*\([^)]*\)[ \t]*\{/\1/,function/ 188 | -------------------------------------------------------------------------------- /lib/structure-view.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Vue from 'vue'; 4 | import $ from 'jquery'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import _find from 'lodash/find'; 8 | import _forEach from 'lodash/forEach'; 9 | import TagGenerator from './tag-generator'; 10 | import TagParser from './tag-parser'; 11 | import Util from './util'; 12 | 13 | export default class StructureView { 14 | 15 | constructor() { 16 | const htmlString = fs.readFileSync(path.join(__dirname, '..', 'templates', 'structure-view.html'), { 17 | encoding: 'utf-8' 18 | }); 19 | this.element = $(htmlString).get(0); 20 | this.viewType = 'structureView'; 21 | this.vm = new Vue({ 22 | el: this.element, 23 | data: { 24 | treeNodeId: null, 25 | nodeSet: {}, 26 | cursorListener: null, 27 | textEditorListener: null, 28 | editorSaveListener: {}, 29 | viewLoading: true, 30 | noTagHint: null, 31 | lastFile: null, 32 | viewShow: false, 33 | CONFIG_DBLCLICK_TO_FOLD_TREE: atom.config.get('structure-view.DoubleClickToFoldTreeView'), 34 | CONFIG_SHOW_VARIABLES: atom.config.get('structure-view.ShowVariables'), 35 | CONFIG_SHOW_PROPERTIES: atom.config.get('structure-view.ShowProperties') 36 | }, 37 | methods: { 38 | onToggleTreeNode(evt) { 39 | if (this.CONFIG_DBLCLICK_TO_FOLD_TREE) { 40 | Util.selectTreeNode($(evt.target), this, { 41 | toggle: true 42 | }); 43 | } 44 | }, 45 | onSelectTreeNode(evt) { 46 | // If double click is not enable, tree should be toggled by single click 47 | if (this.CONFIG_DBLCLICK_TO_FOLD_TREE) { 48 | Util.selectTreeNode($(evt.target), this, { 49 | toggle: false 50 | }); 51 | } else { 52 | Util.selectTreeNode($(evt.target), this, { 53 | toggle: true 54 | }); 55 | } 56 | }, 57 | onToggleWholeTree(evt) { 58 | let val = evt.target.value; 59 | if (val === 'expand') { 60 | $('div.structure-view>div.tree-panel>ol>li').removeClass('collapsed'); 61 | } else { 62 | $('div.structure-view>div.tree-panel>ol>li').addClass('collapsed'); 63 | } 64 | }, 65 | onClickGuide() { 66 | atom.workspace.open('atom://config/packages/structure-view').then(() => { 67 | document.getElementById('usage').scrollIntoView(); 68 | }); 69 | }, 70 | onOpenSettingsTab() { 71 | atom.workspace.open('atom://config/packages/structure-view').then(() => { 72 | document.getElementsByClassName('section-heading icon-gear')[0].scrollIntoView() 73 | }); 74 | } 75 | }, 76 | created() { 77 | atom.config.onDidChange('structure-view.DoubleClickToFoldTreeView', ret => { 78 | this.CONFIG_DBLCLICK_TO_FOLD_TREE = ret.newValue; 79 | }); 80 | atom.config.onDidChange('structure-view.ShowVariables', ret => { 81 | this.CONFIG_SHOW_VARIABLES = ret.newValue; 82 | }); 83 | atom.config.onDidChange('structure-view.ShowProperties', ret => { 84 | this.CONFIG_SHOW_PROPERTIES = ret.newValue; 85 | }); 86 | }, 87 | watch: { 88 | treeNodeId(val) { 89 | if (!this.lastFile) return; 90 | let position = this.nodeSet[val].position, 91 | // getActiveTextEditor can not get editor after click left tree when on windows before v1.8.0 92 | editor = atom.workspace.getTextEditors().find(i => { 93 | return i.getPath() === this.lastFile; 94 | }); 95 | if (editor) { 96 | let row = position.row; 97 | // Blocks of code could be folded 98 | if (editor.isFoldedAtBufferRow(row)) editor.unfoldBufferRow(row); 99 | // Lines can be soft-wrapped 100 | if (editor.isSoftWrapped()) { 101 | editor.setCursorBufferPosition(position); 102 | } else { 103 | editor.setCursorScreenPosition(position); 104 | } 105 | editor.scrollToCursorPosition(); 106 | } 107 | }, 108 | viewLoading(val) { 109 | $(this.$el).find('.mask')[val ? 'show' : 'hide'](); 110 | } 111 | } 112 | }); 113 | } 114 | 115 | initialize() { 116 | this.vm.viewLoading = true; 117 | this.render(); 118 | if (atom.config.get('structure-view.SelectTagWhenCursorChanged')) { 119 | this.listenOnCursorPositionChange(); 120 | } 121 | this.listenOnTextEditorChange(); 122 | this.listenOnTextEditorSave(atom.workspace.getActiveTextEditor()); 123 | } 124 | 125 | async render(filePath) { 126 | let editor = atom.workspace.getActiveTextEditor(); 127 | if (!filePath && editor) { 128 | filePath = editor.getPath(); 129 | } 130 | if (filePath) { 131 | let scopeName = editor.getGrammar().scopeName; 132 | let tags = await new TagGenerator(filePath, scopeName).generate(); 133 | if (tags.err) { 134 | this.vm.noTagHint = tags.err; 135 | } else { 136 | (new TagParser(tags, 'javascript')).parser(); 137 | 138 | if (tags.list && Object.keys(tags.list).length > 0) { 139 | this.renderTree(tags.tree); 140 | this.vm.nodeSet = tags.list; 141 | this.vm.noTagHint = null; 142 | } else { 143 | this.vm.noTagHint = 'No tag in the file.'; 144 | } 145 | } 146 | this.vm.lastFile = filePath; 147 | } 148 | else { 149 | this.vm.noTagHint = 'No file is opened.'; 150 | } 151 | this.vm.viewLoading = false; 152 | } 153 | 154 | renderTree(nodes) { 155 | let html = this.treeGenerator(nodes); 156 | $('div.structure-view>div>ol').html(html); 157 | } 158 | 159 | listenOnCursorPositionChange() { 160 | const self = this, 161 | activeEditor = atom.workspace.getActiveTextEditor(); 162 | if (activeEditor) { 163 | this.vm.cursorListener = activeEditor.onDidChangeCursorPosition(e => { 164 | let nRow = e.newScreenPosition.row; 165 | if (nRow !== e.oldScreenPosition.row) { 166 | let tag = _find(self.vm.nodeSet, item => { 167 | return item.position.row === nRow; 168 | }); 169 | // Same node would not change view 170 | if (tag && tag.id !== self.treeNodeId) { 171 | let $tag = $(this.element).find(`li[node-id="${tag.id}"]`); 172 | if ($tag.length > 0) { 173 | // {top: 0, left: 0} means node is hidden 174 | // TODO: expand parent tree node 175 | if ($tag.offset().top === 0 && $tag.offset().left === 0) return; 176 | 177 | Util.selectTreeNode($tag, this); 178 | let ret = Util.getScrollDistance($tag, $(this.element)); 179 | if (ret.needScroll) $(this.element).scrollTop(ret.distance); 180 | } 181 | } 182 | } 183 | }); 184 | } 185 | } 186 | 187 | listenOnTextEditorChange() { 188 | if (this.vm.textEditorListener) return; 189 | 190 | const self = this; 191 | // ::onDidChangeActiveTextEditor API is only supported after 1.18.0 192 | const rightDock = atom.workspace.getRightDock(); 193 | if (atom.appVersion >= '1.18') { 194 | this.vm.textEditorListener = atom.workspace.onDidChangeActiveTextEditor(editor => { 195 | if ( 196 | self.vm.viewShow && 197 | editor && 198 | editor.element && 199 | 'ATOM-TEXT-EDITOR' === editor.element.nodeName 200 | ) { 201 | // Do not show view when view is hidden by user 202 | if (!rightDock.isVisible() && !self.vm.lastFile) rightDock.show(); 203 | 204 | // For changed file 205 | self.render(editor.getPath()); 206 | 207 | // Add save event listener 208 | self.listenOnTextEditorSave(editor); 209 | } else { 210 | rightDock.hide(); 211 | self.vm.lastFile = ''; 212 | } 213 | }); 214 | } else { 215 | this.vm.textEditorListener = atom.workspace.onDidChangeActivePaneItem(editor => { 216 | // Ensure pane item is an edior 217 | if ( 218 | self.vm.viewShow && 219 | editor && 220 | editor.element && 221 | 'ATOM-TEXT-EDITOR' === editor.element.nodeName 222 | ) { 223 | if (!rightDock.isVisible() && !self.vm.lastFile) rightDock.show(); 224 | 225 | // Skip render if file is not changed and view has content 226 | if (self.vm.lastFile === editor.getPath() && !self.vm.noTagHint) return; 227 | 228 | self.render(editor.getPath()); 229 | // Add save event listener 230 | self.listenOnTextEditorSave(editor); 231 | } 232 | // Do nothing if click SV itself 233 | else if (editor && 'structureView' === editor.viewType); 234 | // Do not close right dock if other item exists 235 | else if (rightDock.getPaneItems().length > 1) { 236 | self.render(); 237 | } else { 238 | rightDock.hide(); 239 | self.vm.lastFile = ''; 240 | } 241 | }); 242 | } 243 | } 244 | 245 | listenOnTextEditorSave(editor) { 246 | if (editor) { 247 | const listener = this.vm.editorSaveListener, 248 | self = this; 249 | if (!listener[editor.id]) listener[editor.id] = editor.onDidSave(i => { 250 | self.render(i.path); 251 | }); 252 | } 253 | } 254 | 255 | treeGenerator(data) { 256 | const self = this; 257 | let array = [], 258 | letter; 259 | 260 | _forEach(data, item => { 261 | switch (item.type) { 262 | 263 | case 'sel': // CSS 264 | case 'selector': // LESS, SASS 265 | letter = 'S'; 266 | break; 267 | case 'prop': // CSS 268 | if (self.vm.CONFIG_SHOW_PROPERTIES) { 269 | letter = 'P'; 270 | } else { 271 | return; 272 | } 273 | break; 274 | case 'elem': // HTML 275 | letter = ''; 276 | break; 277 | 278 | case 'class': // JS 279 | case 'context': // Ruby 280 | letter = 'C'; 281 | break; 282 | case 'describe': // Ruby 283 | letter = 'D'; 284 | break; 285 | case 'import': // JS 286 | letter = 'I'; 287 | break; 288 | case 'function': // JS, C 289 | letter = 'F'; 290 | break; 291 | case 'method': // JS 292 | case 'member': // JSON, CSON, MARKDOWN 293 | letter = 'M'; 294 | break; 295 | case 'var': // JS 296 | case 'variable': // C 297 | case 'macro': 298 | if (self.vm.CONFIG_SHOW_VARIABLES) { 299 | letter = 'V'; 300 | } else { 301 | return; 302 | } 303 | break; 304 | default: 305 | letter = 'U'; 306 | break; 307 | } 308 | let iconTpl; 309 | if (item.type === 'elem') { 310 | iconTpl = ``; 311 | } else { 312 | iconTpl = `
${letter}
`; 313 | } 314 | 315 | let entry = `
  • 316 |
    317 | ${iconTpl} 318 | ${item.name} 319 |
    320 |
  • `; 321 | 322 | if (item.child) { 323 | let childContent = self.treeGenerator(item.child); 324 | 325 | if (childContent.length != 0) { 326 | entry = `
  • 327 |
    328 | ${iconTpl} 329 | ${item.name} 330 |
    331 |
      ${childContent}
    332 |
  • `; 333 | } 334 | } 335 | 336 | array.push(entry); 337 | 338 | }); 339 | 340 | return array.join(''); 341 | } 342 | 343 | serialize() {} 344 | 345 | destroy() { 346 | this.element.remove(); 347 | if (this.vm.cursorListener) { 348 | this.vm.cursorListener.dispose(); 349 | this.vm.cursorListener = null; 350 | } 351 | if (this.vm.textEditorListener) { 352 | this.vm.textEditorListener.dispose(); 353 | this.vm.textEditorListener = null; 354 | } 355 | _forEach(this.vm.editorSaveListener, item => { 356 | item.dispose() 357 | }); 358 | this.vm.editorSaveListener = {}; 359 | // this.vm.$destroy(); 360 | } 361 | 362 | getElement() { 363 | return this.element; 364 | } 365 | 366 | getTitle() { 367 | return 'Structure View'; 368 | } 369 | } 370 | --------------------------------------------------------------------------------