diff --git a/native/v8_extensions/tags.h b/native/v8_extensions/tags.h index 27c8d977b..48cbcd34e 100644 --- a/native/v8_extensions/tags.h +++ b/native/v8_extensions/tags.h @@ -1,5 +1,6 @@ #include "include/cef_base.h" #include "include/cef_v8.h" +#include "readtags.h" namespace v8_extensions { @@ -15,6 +16,9 @@ public: // Provide the reference counting implementation for this class. IMPLEMENT_REFCOUNTING(Tags); + +private: + CefRefPtr ParseEntry(tagEntry entry); }; } diff --git a/native/v8_extensions/tags.js b/native/v8_extensions/tags.js index 46ff72c52..cdef853d0 100644 --- a/native/v8_extensions/tags.js +++ b/native/v8_extensions/tags.js @@ -4,4 +4,7 @@ var $tags = {}; native function find(path, tag); $tags.find = find; + native function getAllTagsAsync(path, callback); + $tags.getAllTagsAsync = getAllTagsAsync; + })(); diff --git a/native/v8_extensions/tags.mm b/native/v8_extensions/tags.mm index 44564f4e2..000bbdc12 100644 --- a/native/v8_extensions/tags.mm +++ b/native/v8_extensions/tags.mm @@ -1,5 +1,4 @@ #import "tags.h" -#import "readtags.h" #import namespace v8_extensions { @@ -10,6 +9,16 @@ Tags::Tags() : CefV8Handler() { CefRegisterExtension("v8/tags", [extensionCode UTF8String], this); } +CefRefPtr Tags::ParseEntry(tagEntry entry) { + CefRefPtr tagEntry = CefV8Value::CreateObject(NULL); + tagEntry->SetValue("name", CefV8Value::CreateString(entry.name), V8_PROPERTY_ATTRIBUTE_NONE); + tagEntry->SetValue("file", CefV8Value::CreateString(entry.file), V8_PROPERTY_ATTRIBUTE_NONE); + if (entry.address.pattern) { + tagEntry->SetValue("pattern", CefV8Value::CreateString(entry.address.pattern), V8_PROPERTY_ATTRIBUTE_NONE); + } + return tagEntry; +} + bool Tags::Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, @@ -26,28 +35,9 @@ bool Tags::Execute(const CefString& name, tagEntry entry; std::vector> entries; if (tagsFind(tagFile, &entry, tag.c_str(), TAG_FULLMATCH | TAG_OBSERVECASE) == TagSuccess) { - CefRefPtr tagEntry = CefV8Value::CreateObject(NULL); - tagEntry->SetValue("name", CefV8Value::CreateString(entry.name), V8_PROPERTY_ATTRIBUTE_NONE); - tagEntry->SetValue("file", CefV8Value::CreateString(entry.file), V8_PROPERTY_ATTRIBUTE_NONE); - if (entry.kind) { - tagEntry->SetValue("kind", CefV8Value::CreateString(entry.kind), V8_PROPERTY_ATTRIBUTE_NONE); - } - if (entry.address.pattern) { - tagEntry->SetValue("pattern", CefV8Value::CreateString(entry.address.pattern), V8_PROPERTY_ATTRIBUTE_NONE); - } - entries.push_back(tagEntry); - + entries.push_back(ParseEntry(entry)); while (tagsFindNext(tagFile, &entry) == TagSuccess) { - tagEntry = CefV8Value::CreateObject(NULL); - tagEntry->SetValue("name", CefV8Value::CreateString(entry.name), V8_PROPERTY_ATTRIBUTE_NONE); - tagEntry->SetValue("file", CefV8Value::CreateString(entry.file), V8_PROPERTY_ATTRIBUTE_NONE); - if (entry.kind) { - tagEntry->SetValue("kind", CefV8Value::CreateString(entry.kind), V8_PROPERTY_ATTRIBUTE_NONE); - } - if (entry.address.pattern) { - tagEntry->SetValue("pattern", CefV8Value::CreateString(entry.address.pattern), V8_PROPERTY_ATTRIBUTE_NONE); - } - entries.push_back(tagEntry); + entries.push_back(ParseEntry(entry)); } } @@ -60,6 +50,52 @@ bool Tags::Execute(const CefString& name, return true; } + if (name == "getAllTagsAsync") { + std::string path = arguments[0]->GetStringValue().ToString(); + CefRefPtr callback = arguments[1]; + CefRefPtr context = CefV8Context::GetCurrentContext(); + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(queue, ^{ + tagFileInfo info; + tagFile* tagFile; + tagFile = tagsOpen(path.c_str(), &info); + std::vector entries; + + if (info.status.opened) { + tagEntry entry; + while (tagsNext(tagFile, &entry) == TagSuccess) { + entry.name = strdup(entry.name); + entry.file = strdup(entry.file); + if (entry.address.pattern) { + entry.address.pattern = strdup(entry.address.pattern); + } + entries.push_back(entry); + } + tagsClose(tagFile); + } + + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + dispatch_async(mainQueue, ^{ + context->Enter(); + CefRefPtr v8Tags = CefV8Value::CreateArray(entries.size()); + for (int i = 0; i < entries.size(); i++) { + v8Tags->SetValue(i, ParseEntry(entries[i])); + free((void*)entries[i].name); + free((void*)entries[i].file); + if (entries[i].address.pattern) { + free((void*)entries[i].address.pattern); + } + } + CefV8ValueList callbackArgs; + callbackArgs.push_back(v8Tags); + callback->ExecuteFunction(callback, callbackArgs); + context->Exit(); + }); + }); + return true; + } + return false; } diff --git a/src/extensions/outline-view/spec/outline-view-spec.coffee b/src/extensions/outline-view/spec/outline-view-spec.coffee index 81574e5fd..e133e86c5 100644 --- a/src/extensions/outline-view/spec/outline-view-spec.coffee +++ b/src/extensions/outline-view/spec/outline-view-spec.coffee @@ -20,7 +20,7 @@ describe "OutlineView", -> it "initially displays all JavaScript functions with line numbers", -> rootView.open('sample.js') expect(rootView.find('.outline-view')).not.toExist() - rootView.getActiveEditor().trigger "outline-view:toggle" + rootView.getActiveEditor().trigger "outline-view:toggle-file-outline" expect(outlineView.find('.loading')).toHaveText 'Generating symbols...' waitsFor -> @@ -31,16 +31,16 @@ describe "OutlineView", -> 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:first').find('.function-details')).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' + expect(outlineView.list.children('li:last').find('.function-details')).toHaveText 'Line 2' expect(outlineView).not.toHaveClass "error" expect(outlineView.error).not.toBeVisible() it "displays error when no tags match text in mini-editor", -> rootView.open('sample.js') expect(rootView.find('.outline-view')).not.toExist() - rootView.getActiveEditor().trigger "outline-view:toggle" + rootView.getActiveEditor().trigger "outline-view:toggle-file-outline" waitsFor -> setArraySpy.callCount > 0 @@ -67,7 +67,7 @@ describe "OutlineView", -> it "shows an error message when no matching tags are found", -> rootView.open('sample.txt') expect(rootView.find('.outline-view')).not.toExist() - rootView.getActiveEditor().trigger "outline-view:toggle" + rootView.getActiveEditor().trigger "outline-view:toggle-file-outline" setErrorSpy = spyOn(outlineView, "setError").andCallThrough() waitsFor -> @@ -151,3 +151,24 @@ describe "OutlineView", -> outlineView.confirmed(outlineView.array[0]) expect(rootView.getActiveEditor().getPath()).toBe rootView.project.resolve("tagged-duplicate.js") expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [0,4] + + describe "project outline", -> + it "displays all tags", -> + rootView.open("tagged.js") + expect(rootView.find('.outline-view')).not.toExist() + rootView.trigger "outline-view:toggle-project-outline" + expect(outlineView.find('.loading')).toHaveText 'Loading symbols...' + + waitsFor -> + setArraySpy.callCount > 0 + + runs -> + expect(outlineView.find('.loading')).toBeEmpty() + expect(rootView.find('.outline-view')).toExist() + expect(outlineView.list.children('li').length).toBe 4 + expect(outlineView.list.children('li:first').find('.function-name')).toHaveText 'callMeMaybe' + expect(outlineView.list.children('li:first').find('.function-details')).toHaveText 'tagged.js' + expect(outlineView.list.children('li:last').find('.function-name')).toHaveText 'thisIsCrazy' + expect(outlineView.list.children('li:last').find('.function-details')).toHaveText 'tagged.js' + expect(outlineView).not.toHaveClass "error" + expect(outlineView.error).not.toBeVisible() diff --git a/src/extensions/outline-view/src/keymap.coffee b/src/extensions/outline-view/src/keymap.coffee index 7dbf07894..05e4eae7f 100644 --- a/src/extensions/outline-view/src/keymap.coffee +++ b/src/extensions/outline-view/src/keymap.coffee @@ -1,3 +1,4 @@ window.keymap.bindKeys '.editor' - 'meta-j': 'outline-view:toggle' + 'meta-j': 'outline-view:toggle-file' + 'meta-J': 'outline-view:toggle-project' 'meta-.': 'outline-view:jump-to-declaration' diff --git a/src/extensions/outline-view/src/outline-view.coffee b/src/extensions/outline-view/src/outline-view.coffee index 0e068cacc..7785a9266 100644 --- a/src/extensions/outline-view/src/outline-view.coffee +++ b/src/extensions/outline-view/src/outline-view.coffee @@ -14,7 +14,8 @@ class OutlineView extends SelectList requireStylesheet 'select-list.css' requireStylesheet 'outline-view/src/outline-view.css' @instance = new OutlineView(rootView) - rootView.command 'outline-view:toggle', => @instance.toggle() + rootView.command 'outline-view:toggle-file-outline', => @instance.toggleFileOutline() + rootView.command 'outline-view:toggle-project-outline', => @instance.toggleProjectOutline() rootView.command 'outline-view:jump-to-declaration', => @instance.jumpToDeclaration() @viewClass: -> "#{super} outline-view" @@ -24,22 +25,26 @@ class OutlineView extends SelectList initialize: (@rootView) -> super - itemForElement: ({position, name}) -> + itemForElement: ({position, name, file}) -> $$ -> @li => @div name, class: 'function-name' @div class: 'right', => - @div "Line #{position.row + 1}", class: 'function-line' + if position + text = "Line #{position.row + 1}" + else + text = fs.base(file) + @div text, class: 'function-details' @div class: 'clear-float' - toggle: -> + toggleFileOutline: -> if @hasParent() @cancel() else - @populate() + @populateFileOutline() @attach() - populate: -> + populateFileOutline: -> tags = [] callback = (tag) -> tags.push tag path = @rootView.getActiveEditor().getPath() @@ -47,6 +52,26 @@ class OutlineView extends SelectList new TagGenerator(path, callback).generate().done => if tags.length > 0 @miniEditor.show() + @maxItem = Infinity + @setArray(tags) + else + @miniEditor.hide() + @setError("No symbols found") + setTimeout (=> @detach()), 2000 + + toggleProjectOutline: -> + if @hasParent() + @cancel() + else + @populateProjectOutline() + @attach() + + populateProjectOutline: -> + @setLoading("Loading symbols...") + TagReader.getAllTags(@rootView.getActiveEditor()).done (tags) => + if tags.length > 0 + @miniEditor.show() + @maxItems = 10 @setArray(tags) else @miniEditor.hide() @@ -57,9 +82,11 @@ class OutlineView extends SelectList @cancel() @openTag(tag) - openTag: ({position, file}) -> - @rootView.openInExistingEditor(file, true, true) if file - @moveToPosition(position) + openTag: (tag) -> + position = tag.position + position = @getTagLine(tag) unless position + @rootView.openInExistingEditor(tag.file, true, true) if tag.file + @moveToPosition(position) if position moveToPosition: (position) -> editor = @rootView.getActiveEditor() diff --git a/src/extensions/outline-view/src/outline-view.css b/src/extensions/outline-view/src/outline-view.css index 7f3fda574..a5146bc38 100644 --- a/src/extensions/outline-view/src/outline-view.css +++ b/src/extensions/outline-view/src/outline-view.css @@ -23,7 +23,7 @@ float: right; } -.outline-view ol .function-line { +.outline-view ol .function-details { display: inline-block; margin: 4px 0; margin-right: .5em; diff --git a/src/extensions/outline-view/src/tag-reader.coffee b/src/extensions/outline-view/src/tag-reader.coffee index a0e309b32..49e9aa042 100644 --- a/src/extensions/outline-view/src/tag-reader.coffee +++ b/src/extensions/outline-view/src/tag-reader.coffee @@ -1,13 +1,28 @@ fs = require 'fs' +$ = require 'jquery' module.exports = +getTagsFile: (editor) -> + project = editor.rootView().project + tagsFile = project.resolve("tags") or project.resolve("TAGS") + return tagsFile if fs.isFile(tagsFile) + find: (editor) -> word = editor.getTextInRange(editor.getCursor().getCurrentWordBufferRange()) return [] unless word.length > 0 - project = editor.rootView().project - tagsFile = project.resolve("tags") or project.resolve("TAGS") - return [] unless fs.isFile(tagsFile) + tagsFile = @getTagsFile(editor) + return [] unless tagsFile $tags.find(tagsFile, word) or [] + +getAllTags: (editor, callback) -> + deferred = $.Deferred() + tagsFile = @getTagsFile(editor) + if tagsFile + $tags.getAllTagsAsync tagsFile, (tags) => + deferred.resolve(tags) + else + deferred.resolve([]) + deferred.promise()