diff --git a/.atom/default-config.coffee b/.atom/default-config.coffee index fdc7da926..ec91fae41 100644 --- a/.atom/default-config.coffee +++ b/.atom/default-config.coffee @@ -8,3 +8,4 @@ requireExtension 'snippets' requireExtension 'status-bar' requireExtension 'wrap-guide' requireExtension 'markdown-preview' +requireExtension 'outline-view' diff --git a/spec/extensions/outline-view-spec.coffee b/spec/extensions/outline-view-spec.coffee new file mode 100644 index 000000000..85f477ad0 --- /dev/null +++ b/spec/extensions/outline-view-spec.coffee @@ -0,0 +1,83 @@ +RootView = require 'root-view' +OutlineView = require 'outline-view' +TagGenerator = require 'outline-view/tag-generator' + +describe "OutlineView", -> + [rootView, outlineView] = [] + + beforeEach -> + rootView = new RootView(require.resolve('fixtures')) + rootView.activateExtension(OutlineView) + outlineView = OutlineView.instance + rootView.attachToDom() + + afterEach -> + rootView.deactivate() + + it "displays all JavaScript functions with line numbers", -> + tags = [] + waitsForPromise -> + tags = [] + path = require.resolve('fixtures/sample.js') + callback = (tag) -> + tags.push tag + generator = new TagGenerator(path, callback) + generator.generate() + + runs -> + rootView.open('sample.js') + expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [0,0] + expect(rootView.find('.outline-view')).not.toExist() + outlineView.setArray(tags) + outlineView.attach() + expect(rootView.find('.outline-view')).toExist() + expect(outlineView.list.children('li').length).toBe 2 + expect(outlineView.list.children('li:first').find('.function-name')).toHaveText 'quicksort' + expect(outlineView.list.children('li:first').find('.function-line')).toHaveText 'Line 1' + expect(outlineView.list.children('li:last').find('.function-name')).toHaveText 'quicksort.sort' + expect(outlineView.list.children('li:last').find('.function-line')).toHaveText 'Line 2' + + it "moves the cursor to the selected function", -> + tags = [] + waitsForPromise -> + tags = [] + path = require.resolve('fixtures/sample.js') + callback = (tag) -> + tags.push tag + generator = new TagGenerator(path, callback) + generator.generate() + + runs -> + rootView.open('sample.js') + expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [0,0] + expect(rootView.find('.outline-view')).not.toExist() + outlineView.setArray(tags) + outlineView.attach() + expect(rootView.find('.outline-view')).toExist() + outlineView.confirmed(tags[1]) + expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [1,0] + + describe "TagGenerator", -> + it "generates tags for all JavaScript functions", -> + waitsForPromise -> + tags = [] + path = require.resolve('fixtures/sample.js') + callback = (tag) -> + tags.push tag + generator = new TagGenerator(path, callback) + generator.generate().done -> + expect(tags.length).toBe 2 + expect(tags[0].name).toBe "quicksort" + expect(tags[0].position.row).toBe 0 + expect(tags[1].name).toBe "quicksort.sort" + expect(tags[1].position.row).toBe 1 + + it "generates no tags for text file", -> + waitsForPromise -> + tags = [] + path = require.resolve('fixtures/sample.txt') + callback = (tag) -> + tags.push tag + generator = new TagGenerator(path, callback) + generator.generate().done -> + expect(tags.length).toBe 0 diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 4bec7c356..067dd93e5 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -854,7 +854,7 @@ class Editor extends View break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) popScope() - # push on top of common prefix until scopStacks == desiredScopes + # push on top of common prefix until scopeStack == desiredScopes for j in [i...desiredScopes.length] pushScope(desiredScopes[j]) diff --git a/src/extensions/outline-view/index.coffee b/src/extensions/outline-view/index.coffee new file mode 100644 index 000000000..1a19f8519 --- /dev/null +++ b/src/extensions/outline-view/index.coffee @@ -0,0 +1 @@ +module.exports = require 'outline-view/outline-view' diff --git a/src/extensions/outline-view/keymap.coffee b/src/extensions/outline-view/keymap.coffee new file mode 100644 index 000000000..46344df44 --- /dev/null +++ b/src/extensions/outline-view/keymap.coffee @@ -0,0 +1,2 @@ +window.keymap.bindKeys '.editor' + 'ctrl-o': 'outline-view:toggle' diff --git a/src/extensions/outline-view/outline-view.coffee b/src/extensions/outline-view/outline-view.coffee new file mode 100644 index 000000000..57a94e6b4 --- /dev/null +++ b/src/extensions/outline-view/outline-view.coffee @@ -0,0 +1,56 @@ +{View, $$} = require 'space-pen' +SelectList = require 'select-list' +Editor = require 'editor' +TagGenerator = require 'outline-view/tag-generator' + +module.exports = +class OutlineView extends SelectList + + @activate: (rootView) -> + requireStylesheet 'select-list.css' + requireStylesheet 'outline-view/outline-view.css' + @instance = new OutlineView(rootView) + rootView.command 'outline-view:toggle', => @instance.toggle() + + @viewClass: -> "#{super} outline-view" + + filterKey: 'name' + + initialize: (@rootView) -> + super + + itemForElement: ({position, name}) -> + $$ -> + @li => + @div name, class: 'function-name' + @div class: 'right', => + @div "Line #{position.row + 1}", class: 'function-line' + @div class: 'clear-float' + + toggle: -> + if @hasParent() + @cancel() + else + @populate() + + populate: -> + tags = [] + callback = (tag) -> + tags.push tag + path = @rootView.getActiveEditor().getPath() + new TagGenerator(path, callback).generate().done => + if tags.length > 0 + @setArray(tags) + @attach() + + confirmed : ({position, name}) -> + @cancel() + @rootView.getActiveEditor().setCursorBufferPosition(position) + + cancelled: -> + @miniEditor.setText('') + @rootView.focus() if @miniEditor.isFocused + + attach: -> + @rootView.append(this) + @miniEditor.focus() diff --git a/src/extensions/outline-view/outline-view.css b/src/extensions/outline-view/outline-view.css new file mode 100644 index 000000000..1b67d9139 --- /dev/null +++ b/src/extensions/outline-view/outline-view.css @@ -0,0 +1,38 @@ +.outline-view { + width: 50%; + margin-left: -25%; +} + +.outline-view ol { + max-height: 300px; +} + +.outline-view ol li { + padding: 2px; + border-bottom: 1px solid rgba(255, 255, 255, .05); +} + +.outline-view ol .function-name { + float: left; + display: inline-block; + margin-right: .5em; + margin: 4px 0; +} + +.outline-view li .right { + float: right; +} + +.outline-view ol .function-line { + display: inline-block; + margin: 4px 0; + margin-right: .5em; + font-size: 90%; + color: #ddd; + -webkit-border-radius: 3px; + padding: 0 4px; +} + +.outline-view ol .function-line { + background: rgba(0, 0, 0, .2); +} diff --git a/src/extensions/outline-view/tag-generator.coffee b/src/extensions/outline-view/tag-generator.coffee new file mode 100644 index 000000000..c0079fbde --- /dev/null +++ b/src/extensions/outline-view/tag-generator.coffee @@ -0,0 +1,47 @@ +Point = require 'point' +ChildProcess = require 'child-process' + +module.exports = +class TagGenerator + + constructor: (@path, @callback) -> + + parsePrefix: (section = "") -> + if section.indexOf('class:') is 0 + section.substring(6) + else if section.indexOf('namespace:') is 0 + section.substring(10) + else if section.indexOf('file:') is 0 + section.substring(5) + else if section.indexOf('signature:') is 0 + section.substring(10) + else + section + + parseTagLine: (line) -> + sections = line.split('\t') + return null if sections.length < 4 + + label = sections[0] + line = parseInt(sections[2]) - 1 + if prefix = @parsePrefix(sections[4]) + label = "#{prefix}::#{label}" + if signature = @parsePrefix(sections[5]) + label = "#{label}#{signature}" + + tag = + position: new Point(line, 0) + name: label + + return tag + + generate: -> + options = + bufferLines: true + stdout: (data) => + lines = data.split('\n') + for line in lines + tag = @parseTagLine(line) + @callback(tag) if tag + command = "ctags --fields=+KS -nf - #{@path}" + ChildProcess.exec(command, options)