├── .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 |
9 |
10 |
12 |
13 |
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 | 
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 |
--------------------------------------------------------------------------------