define(function(require, exports, module) { main.consumes = [ "Panel", "c9", "settings", "ui", "menus", "panels", "tabManager", "language", "util", "language.jumptodef", "navigate", "layout", "commands", "jsonalyzer" ]; main.provides = ["outline"]; return main; function main(options, imports, register) { var Panel = imports.Panel; var settings = imports.settings; var ui = imports.ui; var util = imports.util; var menus = imports.menus; var panels = imports.panels; var layout = imports.layout; var navigate = imports.navigate; var tabs = imports.tabManager; var language = imports.language; var commands = imports.commands; var jsonalyzer = imports.jsonalyzer; var jumptodef = imports["language.jumptodef"]; var escapeHTML = require("ace/lib/lang").escapeHTML; var Range = require("ace/range").Range; var search = require("../c9.ide.navigate/search"); var markup = require("text!./outline.xml"); var Tree = require("ace_tree/tree"); var TreeData = require("./outlinedp"); /***** Initialization *****/ var plugin = new Panel("Ajax.org", main.consumes, { index: options.index || 50, width: 250, caption: "Outline", buttonCSSClass: "outline", minWidth: 130, where: options.where || "right", autohide: true }); var fullOutline = []; var ignoreFocusOnce = false; var tree, tdOutline, winOutline, textbox, treeParent; // UI Elements var originalLine, originalColumn, originalTab; var focussed, isActive, outline, timer, dirty, scheduled, scheduleWatcher; var isUnordered, lastFilter, hasNavigateOutline; var worker; var COLLAPSE_AREA = 14; var loaded = false; function load() { if (loaded) return false; loaded = true; plugin.setCommand({ name: "outline", hint: "search for a definition and jump to it", bindKey: { mac: "Command-Shift-E", win: "Ctrl-Shift-E" }, exec: function() { if (isActive) { if (focussed) { panels.deactivate("outline"); tabs.focussedTab && tabs.focussedTab.aml.focus(); } else { textbox.focus(); } } else { panels.activate("outline"); } } }); isActive = panels.isActive("outline"); // Menus menus.addItemByPath("Goto/Goto Symbol...", new apf.item({ command: "outline" }), 110, plugin); language.getWorker(function(err, worker) { worker.on("outline", onOutlineData); }); // Hook events to get the focussed tab tabs.on("open", function(e) { var tab = e.tab; if (!isActive || !tab.path && !tab.document.meta.newfile || !tab.editor.ace || tab != tabs.focussedTab) return; if (!originalTab) originalTab = e.tab; updateOutline(true); }); tabs.on("focusSync", onTabFocus); tabs.on("tabDestroy", function(e) { if (isActive && e.last) clear(); }); var cursorTimeout; language.on("cursormove", function() { if (cursorTimeout || !isActive) return; cursorTimeout = setTimeout(function moveSelection() { if ((dirty || scheduled) && isActive) return setTimeout(moveSelection, 50); try { handleCursor(); } finally { cursorTimeout = null; } }, 100); }); // TODO also in scm.commit - move to panel? panels.on("showPanelOutline", function(e) { plugin.autohide = !e.button; }, plugin); panels.on("hidePanelOutline", function(e) { plugin.autohide = true; }, plugin); if (isActive && tabs.focussedTab) { plugin.autohide = false; updateOutline(); onTabFocus({ tab: tabs.focussedTab }); } updateInitialOutline(); // Extends navigate with outline support var wasActive, onsel = function() { if (!navigate.tree.isFocused()) return; var node = navigate.tree.selection.getCursor(); if (node) onSelect(node); }; navigate.on("outline", function(e) { var value = e.value; if (!tdOutline) createProvider(); tabs.focusTab(e.tab, true); hasNavigateOutline = true; wasActive = isActive; isActive = true; onTabFocus(e, true); navigate.tree.off("changeSelection", onsel); navigate.tree.setDataProvider(tdOutline); navigate.tree.on("changeSelection", onsel); renderOutline(true, value); ui.setStyleClass(navigate.tree.container, "outline"); }); navigate.on("outline.stop", function(e) { hasNavigateOutline = false; renderOutline(true); navigate.tree.off("changeSelection", onsel); ui.setStyleClass(navigate.tree.container, "", ["outline"]); isActive = wasActive; }); } /** * Get an initial outline, taking into account that there may * be some time required before all (unknown number of) outliner * language plugins are loaded. */ function updateInitialOutline() { setTimeout(updateOutline.bind(null, true), 4000); setTimeout(updateOutline.bind(null, true), 6000); setTimeout(updateOutline.bind(null, true), 8000); setTimeout(updateOutline.bind(null, true), 10000); setTimeout(updateOutline.bind(null, true), 20000); } function onTabFocus(event, force) { var tab = event.tab; var session; if (originalTab == tab && force !== true) return; // Remove change listener if (originalTab) { session = originalTab.document.getSession().session; session && session.off("changeMode", onChange); session && session.off("change", onChange); } if ((!tab.path && !tab.document.meta.newfile) || tab.editorType !== "ace") { originalTab = null; return clear(tab.editorType); } if (!tab.editor) return tab.document.once("setEditor", onTabFocus.bind(null, event)); // Add change listener session = tab.document.getSession().session; session && session.on("changeMode", onChange); session && session.on("change", onChange); originalTab = tab; if (isActive) updateOutline(true); } function onChange() { if (isActive && originalTab == tabs.focussedTab) updateOutline(); } function handleCursor(ignoreFocus) { if (isActive && originalTab && originalTab == tabs.focussedTab) { var ace = originalTab.editor.ace; if (!outline || !ace.selection.isEmpty() || (!tree || tree.isFocused() && !ignoreFocus)) return; var selected = findCursorInOutline(outline, ace.getCursorPosition()); if (tdOutline.$selectedNode == selected) return; if (selected) tree.selection.selectNode(selected); else tree.selection.selectNode(0); tree.renderer.scrollCaretIntoView(null, 0.5); } } function createProvider() { // Import CSS ui.insertCss(require("text!./outline.css"), null, plugin); // Define data provider tdOutline = new TreeData(); } var drawn = false; function draw(options) { if (drawn) return; drawn = true; // Create UI elements ui.insertMarkup(options.aml, markup, plugin); treeParent = plugin.getElement("outlineTree"); textbox = plugin.getElement("textbox"); winOutline = options.aml; var key = commands.getPrettyHotkey("outline"); textbox.setAttribute("initial-message", "Filter (" + key + ")"); // Create the Ace Tree tree = new Tree(treeParent.$int); tree.renderer.setScrollMargin(0, 10); tree.renderer.scrollBarV.$minWidth = 10; if (!tdOutline) createProvider(); // Assign the dataprovider tree.setDataProvider(tdOutline); // @TODO this is probably not sufficient layout.on("resize", function() { tree.resize(); }, plugin); tree.textInput = textbox.ace.textInput; textbox.ace.commands.addCommands([ { bindKey: "ESC", exec: function() { if (!originalTab || !originalTab.loaded) return clear(); if (originalLine) { var ace = originalTab && originalTab.editor.ace; ace.gotoLine(originalLine, originalColumn, settings.getBool("editors/code/@animatedscroll")); originalLine = originalColumn = null; } textbox.setValue(""); tabs.focusTab(originalTab); } }, { bindKey: "Up", exec: function() { tree.execCommand("goUp"); } }, { bindKey: "Down", exec: function() { tree.execCommand("goDown"); } }, { bindKey: "Enter", exec: function() { onSelect(); textbox.setValue(""); originalTab.loaded && tabs.focusTab(originalTab); } } ]); textbox.ace.on("input", function(e) { renderOutline(); onSelect(); }); tree.on("userSelect", function() { if (tree.isFocused()) onSelect(); }); function onAllBlur(e) { if (!winOutline.visible || !plugin.autohide) return; var to = e.toElement; if (!to || apf.isChildOf(winOutline, to, true)) { return; } // TODO add better support for overlay panels setTimeout(function() { plugin.hide(); }, 10); } apf.addEventListener("movefocus", onAllBlur); function onFocus() { focussed = true; ui.setStyleClass(treeParent.$int, "focus"); var tab = tabs.focussedTab; var ace = tab && tab.editor.ace; if (!ace) return; var cursor = ace.getCursorPosition(); originalLine = cursor.row + 1; originalColumn = cursor.column; } function onBlur() { focussed = false; ui.setStyleClass(treeParent.$int, "", ["focus"]); } textbox.ace.on("blur", onBlur); textbox.ace.on("focus", onFocus); language.getWorker(function(err, _worker) { worker = _worker; timer = setInterval(function() { if (dirty) updateOutline(true); }, 1000); }); } /***** Methods *****/ function updateOutline(now) { if (!isActive) return; dirty = true; if (now && tabs.focussedTab && !scheduled) { if (!worker) { return language.getWorker(function(err, _worker) { worker = _worker; updateOutline(true); }); } // have to use timeout since worker uses timeout as well setTimeout(function () { dirty = false; worker.emit("outline", { data: { ignoreFilter: false, path: tabs.focussedTab && tabs.focussedTab.path }}); }); // Don't schedule new job until data received or timeout scheduled = true; clearTimeout(scheduleWatcher); scheduleWatcher = setTimeout(function() { scheduled = false; }, 10000); } } function findCursorInOutline(json, cursor) { return isUnordered && findPrecise(json, cursor) || findApprox(json, cursor); function findPrecise(json, cursor) { for (var i = 0; i < json.length; i++) { var elem = json[i]; if (cursor.row < elem.pos.sl || cursor.row > (elem.pos.el || elem.pos.sl)) continue; var childResult = elem.items && findPrecise(elem.items, cursor); return childResult || elem; } return null; } function findApprox(json, cursor) { var result; for (var i = 0; i < json.length; i++) { var elem = json[i]; var childResult = elem.items && findApprox(elem.items, cursor); if (childResult) result = childResult; else if (elem.pos.sl <= cursor.row) result = elem; } return result; } } function onOutlineData(event) { scheduled = false; if (hasNavigateOutline) return; var data = event.data; if (data.error) { // TODO: show error in outline? console.error("Oh noes! " + data.error); return; } var tab = tabs.focussedTab; var editor = tab && tab.editor; if (!tab || (!tab.path && !tab.document.meta.newfile) || !editor.ace) return; if (dirty || tab.path !== data.path) updateOutline(true); else clearTimeout(scheduleWatcher); fullOutline = event.data.body; isUnordered = event.data.isUnordered; renderOutline(event.data.showNow); } function renderOutline(ignoreFilter, filter) { var tab = tabs.focussedTab; var editor = tab && tab.editor; if (!tab || !tab.path && !tab.document.meta.newfile || !editor.ace) return; originalTab = tab; if (!filter) filter = ignoreFilter ? "" : (textbox ? textbox.getValue() : lastFilter); outline = search.treeSearch(fullOutline, filter, true); lastFilter = filter; var ace = editor.ace; var selected = findCursorInOutline(outline, ace.getCursorPosition()); tdOutline.setRoot(outline); tdOutline.selected = selected; tdOutline.filter = filter; tdOutline.reFilter = escapeHTML(util.escapeRegExp(filter)); if (filter) { tree && tree.select(tree.provider.getNodeAtIndex(0)); if (hasNavigateOutline) navigate.tree.select(navigate.tree.provider.getNodeAtIndex(0)); } else if (selected) { tdOutline.selection.selectNode(selected); } if (drawn) { tree.resize(); handleCursor(ignoreFocusOnce); ignoreFocusOnce = false; } return selected; } function onSelect(node) { if (!node) node = tree.selection.getCursor(); if (!node) return; // ok, there really is no node if (!originalTab.loaded) return clear(); var ace = originalTab.editor.ace; var pos = jumptodef.addUnknownColumn(ace, node.pos, node.name); var displayPos = node.displayPos ? jumptodef.addUnknownColumn(ace, node.displayPos, node.name) : undefined; scrollToDefinition(ace, pos.sl, pos.el); var range = displayPos ? new Range( displayPos.sl, displayPos.sc, displayPos.el || displayPos.sl, displayPos.el > displayPos.sl ? displayPos.ec || 0 : displayPos.ec || displayPos.sc ) : new Range(pos.sl, pos.sc, pos.sl, pos.sc); // todo fold back ace.session.unfold(range); ace.selection.setSelectionRange(range); } function clear(type) { if (textbox) { textbox.setValue(""); if (type === "terminal") tdOutline.setRoot([{ icon: "property", name: "Terminal" }]); else tdOutline.setRoot({}); } } function scrollToDefinition(ace, line, lineEnd) { var lineHeight = ace.renderer.$cursorLayer.config.lineHeight; var lineVisibleStart = ace.renderer.scrollTop / lineHeight; var linesVisible = ace.renderer.$size.height / lineHeight; lineEnd = Math.min(lineEnd, line + linesVisible); if (lineVisibleStart <= line && lineEnd <= lineVisibleStart + linesVisible) return; var SAFETY = 1.5; ace.scrollToLine(Math.round((line + lineEnd) / 2 - SAFETY), true); } function addOutlinePlugin(path, contents, plugin) { var template = require("text!./outline_template.js"); template = template.replace("{{CONFIG}}", function() { return contents; }); jsonalyzer.registerWorkerHandler(path, template); plugin.addOther(function() { if (jsonalyzer.unregisterWorkerHandler) jsonalyzer.unregisterWorkerHandler(path); }); } /***** Lifecycle *****/ plugin.on("load", function() { load(); }); plugin.on("draw", function(e) { load(); draw(e); }); plugin.on("show", function(e) { isActive = true; tree.resize(); plugin.autohide = !e.button; if (e.button === "restoreSettings") return; textbox.focus(); textbox.select(); updateOutline(true); handleCursor(true); ignoreFocusOnce = true; }); plugin.on("hide", function(e) { // tree.clearSelection(); isActive = false; }); plugin.on("unload", function() { loaded = false; drawn = false; fullOutline = []; ignoreFocusOnce = false; tree = null; tdOutline = null; winOutline = null; textbox = null; treeParent = null; originalLine = null; originalColumn = null; originalTab = null; focussed = null; isActive = null; outline = null; timer = null; dirty = null; scheduled = null; scheduleWatcher = null; isUnordered = null; lastFilter = null; hasNavigateOutline = null; worker = null; clearInterval(timer); }); /***** Register and define API *****/ /** * Outline panel. Allows a user to navigate to a file from a structured * listing of all it's members and events. * @singleton * @extends Panel **/ /** * @command outline */ /** * Fires when the outline panel shows * @event showPanelOutline * @member panels */ /** * Fires when the outline panel hides * @event hidePanelOutline * @member panels */ plugin.freezePublicAPI({ /** * */ addOutlinePlugin: addOutlinePlugin, /** * @ignore */ get tree() { return tree }, }); register(null, { outline: plugin }); } });