kopia lustrzana https://github.com/c9/core
663 wiersze
23 KiB
JavaScript
663 wiersze
23 KiB
JavaScript
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
|
|
});
|
|
}
|
|
});
|