c9-core/plugins/c9.ide.language.core/outline.js

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
});
}
});