c9-core/plugins/c9.ide.navigate/navigate.js

689 wiersze
23 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"Panel", "settings", "ui", "watcher", "menus", "tabManager", "find",
"fs", "panels", "fs.cache", "preferences", "c9", "tree", "commands",
"layout", "util", "c9.analytics"
];
main.provides = ["navigate"];
return main;
function main(options, imports, register) {
var Panel = imports.Panel;
var settings = imports.settings;
var ui = imports.ui;
var c9 = imports.c9;
var fs = imports.fs;
var fsCache = imports["fs.cache"];
var tabs = imports.tabManager;
var menus = imports.menus;
var layout = imports.layout;
var watcher = imports.watcher;
var panels = imports.panels;
var util = imports.util;
var find = imports.find;
var filetree = imports.tree;
var prefs = imports.preferences;
var commands = imports.commands;
var analytics = imports["c9.analytics"];
var markup = require("text!./navigate.xml");
var search = require('./search');
var Tree = require("ace_tree/tree");
var ListData = require("./dataprovider");
var basename = require("path").basename;
/***** Initialization *****/
var plugin = new Panel("Ajax.org", main.consumes, {
index: options.index || 200,
caption: "Navigate",
buttonCSSClass: "navigate",
minWidth: 130,
autohide: true,
where: options.where || "left"
});
var emit = plugin.getEmitter();
var winGoToFile, txtGoToFile, tree, ldSearch;
var lastSearch, lastPreviewed, cleaning, intoOutline;
var isReloadScheduled;
var dirty = true;
var arrayCache = [];
var loadListAtInit = options.loadListAtInit;
var timer;
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
var command = plugin.setCommand({
name: "navigate",
hint: "search for a filename, line or symbol and jump to it",
bindKey: { mac: "Command-E|Command-P", win: "Ctrl-E" },
extra: function(editor, args, e) {
if (args && args.keyword) {
txtGoToFile.setValue(args.keyword);
filter(args.keyword);
}
if (args && args.source !== "click") {
analytics.log("Opened Navigate using shortcut");
}
}
});
commands.addCommand({
name: "navigate_altkey",
hint: "search for a filename, line or symbol and jump to it",
bindKey: { mac: "Command-O", win: "Ctrl-O" },
group: "Panels",
exec: command.exec
}, plugin);
panels.on("afterAnimate", function() {
if (panels.isActive("navigate"))
tree && tree.resize();
});
// Menus
menus.addItemByPath("Goto/Goto Anything...", new ui.item({
command: "navigate"
}), 100, plugin);
// Settings
settings.on("read", function() {
settings.setDefaults("user/general", [["preview-navigate", "false"]]);
}, plugin);
// Prefs
prefs.add({
"General": {
"Tree & Navigate": {
"Enable Preview on Navigation": {
type: "checkbox",
position: 2000,
path: "user/general/@preview-navigate"
}
}
}
}, plugin);
// Update when the fs changes
var quickUpdate = markDirty.bind(null, null, 2000);
var newfile = function(e) {
// Only mark dirty if file didn't exist yet
if (arrayCache.indexOf(e.path) == -1
&& !e.path.match(/(?:^|\/)\./)
&& !e.path.match(/\/(?:state|user|project)\.settings$/))
arrayCache.push(e.path);
};
fs.on("afterWriteFile", newfile);
fs.on("afterSymlink", newfile);
var rmfile = function(e) {
var idx = arrayCache.indexOf(e.path);
if (~idx) arrayCache.splice(idx, 1);
};
fs.on("afterUnlink", rmfile);
fs.on("afterRmfile", rmfile);
var rmdir = function(e) {
var path = e.path;
var len = path.length;
for (var i = arrayCache.length - 1; i >= 0; i--) {
if (arrayCache[i].substr(0, len) == path)
arrayCache.splice(i, 1);
}
};
fs.on("afterRmdir", rmdir);
fs.on("afterCopy", quickUpdate);
fs.on("afterMkdir", quickUpdate);
fs.on("afterMkdirP", quickUpdate);
fs.on("afterRename", function(e) {
rmfile(e);
newfile({ path: e.args[1] });
});
// Or when a watcher fires
watcher.on("delete", quickUpdate);
watcher.on("directory", quickUpdate);
// Or when the user refreshes the tree
filetree.on("refresh", markDirty);
// Or when we change the visibility of hidden files
fsCache.on("setShowHidden", quickUpdate);
// Pre-load file list
if (loadListAtInit)
updateFileCache();
}
function offlineHandler(e) {
// Online
if (e.state & c9.STORAGE) {
txtGoToFile.enable();
//@Harutyun This doesn't work
// tree.enable();
}
// Offline
else {
// do not close panel while typing
if (!txtGoToFile.ace.isFocused())
txtGoToFile.disable();
//@Harutyun This doesn't work
// tree.disable();
}
}
var drawn = false;
function draw(options) {
if (drawn) return;
drawn = true;
// Create UI elements
ui.insertMarkup(options.aml, markup, plugin);
// Import CSS
ui.insertCss(require("text!./style.css"), plugin);
var treeParent = plugin.getElement("navigateList");
txtGoToFile = plugin.getElement("txtGoToFile");
winGoToFile = options.aml;
// Create the Ace Tree
tree = new Tree(treeParent.$int);
ldSearch = new ListData(arrayCache);
ldSearch.search = search;
ldSearch.isLoading = function() { return updating; };
// Assign the dataprovider
tree.setDataProvider(ldSearch);
tree.renderer.setScrollMargin(0, 10);
// @TODO this is probably not sufficient
layout.on("resize", function() { tree.resize(); }, plugin);
tree.textInput = txtGoToFile.ace.textInput;
var key = commands.getPrettyHotkey("navigate");
txtGoToFile.setAttribute("initial-message", key);
txtGoToFile.ace.commands.addCommands([
{
bindKey: "ESC",
exec: function() { plugin.hide(); }
}, {
bindKey: "Enter",
exec: function() { openFile(true); }
}, {
bindKey: "Shift-Enter",
exec: function() { openFile(false, true); }
}, {
bindKey: "Shift-Space",
exec: function() { previewFile(true); }
},
]);
function forwardToTree() {
cleanInput();
tree.execCommand(this.name);
}
txtGoToFile.ace.commands.addCommands([
"centerselection",
"goToStart",
"goToEnd",
"pageup",
"gotopageup",
"pagedown",
"gotopageDown",
"scrollup",
"scrolldown",
"goUp",
"goDown",
"selectUp",
"selectDown",
"selectMoreUp",
"selectMoreDown"
].map(function(name) {
var command = tree.commands.byName[name];
return {
name: command.name,
bindKey: command.editorKey || command.bindKey,
exec: forwardToTree
};
}));
tree.on("click", function(ev) {
var e = ev.domEvent;
if (!e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey)
if (tree.selection.getSelectedNodes().length === 1)
openFile(true);
});
tree.selection.$wrapAround = true;
txtGoToFile.ace.on("input", onInput);
tree.selection.on("change", function() {
previewFile();
});
function onblur(e) {
if (!winGoToFile || !winGoToFile.visible)
return;
var to = e.toElement;
if (!to || apf.isChildOf(winGoToFile, to, true)
|| (lastPreviewed && tabs.previewTab
&& tabs.previewTab === lastPreviewed
&& (apf.isChildOf(lastPreviewed.aml.relPage, to, true)
|| lastPreviewed.aml == to))) {
return;
}
if (to.localName == "menu")
return;
// TODO add better support for overlay panels
setTimeout(function() { plugin.hide(); }, 10);
}
apf.addEventListener("movefocus", onblur);
// Focus the input field
setTimeout(function() {
txtGoToFile.focus();
}, 10);
// Offline
c9.on("stateChange", offlineHandler, plugin);
offlineHandler({ state: c9.status });
}
/***** Methods *****/
function onInput(updatePreview) {
var val = txtGoToFile.getValue();
var parts, tab;
if (cleaning) {
cleaning = false;
return;
}
if (~val.indexOf("@")) {
parts = val.split("@");
if (parts[0]) {
if (lastSearch != parts[0])
filter(parts[0]);
if (updatePreview !== false) previewFile(true);
tab = lastPreviewed || tabs.focussedTab;
if (tab) {
emit("outline", { value: parts[1], tab: tab });
intoOutline = true;
}
}
else {
tab = tabs.focussedTab;
if (tab) {
emit("outline", { value: parts[1], tab: tab });
intoOutline = true;
}
}
}
else {
if (intoOutline)
stopOutline();
if (~val.indexOf(":"))
parts = /^(.*?):(\d*)(?::(\d+))?$/g.exec(val);
if (parts) {
if (parts[1]) {
if (lastSearch != parts[1])
filter(parts[1]);
if (updatePreview !== false) previewFile(true);
tab = lastPreviewed || tabs.focussedTab;
if (tab && parts[2])
tab.editor.ace.gotoLine(parts[2], parts[3]);
}
else {
tab = tabs.focussedTab;
if (tab && parts[2])
tab.editor.ace.gotoLine(parts[2], parts[3]);
}
}
else if (updatePreview !== false) {
filter(val);
}
}
if (dirty && val.length > 0 && ldSearch.loaded) {
dirty = false;
updateFileCache(true);
}
}
function reloadResults() {
if (!winGoToFile) {
if (isReloadScheduled)
return;
isReloadScheduled = true;
plugin.once("draw", function() {
isReloadScheduled = false;
reloadResults();
});
return;
}
// Wait until window is visible
if (!winGoToFile.visible) {
winGoToFile.on("prop.visible", function visible(e) {
if (e.value) {
reloadResults();
winGoToFile.off("prop.visible", visible);
}
});
return;
}
var sel = tree.selection.getSelectedNodes();
if (lastSearch) {
filter(lastSearch, sel.length, true);
} else {
ldSearch.updateData(arrayCache);
}
}
function markDirty(options, timeout) {
// Ignore hidden files
var path = options && options.path || "";
if (path && !fsCache.showHidden && path.charAt(0) == ".")
return;
if (timeout <= 0) {
clearTimeout(timer);
if (timeout < 0) {
timer = setTimeout(function() {
updateFileCache(true);
}, -1 * timeout);
}
else {
updateFileCache(true);
}
return;
}
dirty = true;
if (panels.isActive("navigate")) {
clearTimeout(timer);
timer = setTimeout(function() {
updateFileCache(true);
}, timeout || 60000);
}
}
var updating = false;
function updateFileCache(isDirty) {
clearTimeout(timer);
if (updating || c9.readOnly)
return;
updating = true;
find.getFileList({
path: "/",
nocache: isDirty,
hidden: fsCache.showHidden,
buffer: true
}, function(err, data) {
if (err)
console.error(err);
else
arrayCache = data.trim().split("\n");
updating = false;
reloadResults();
});
dirty = false;
}
function stopOutline() {
if (!intoOutline) return;
emit("outline.stop");
tree.setDataProvider(ldSearch);
intoOutline = false;
}
function cleanInput() {
var value = txtGoToFile.getValue();
if (value.match(/[:\@]/)) {
cleaning = true;
txtGoToFile.setValue(value.split(":")[0].split("@")[0]);
}
}
/**
* Searches through the dataset
*
*/
var lastResults;
function filter(keyword, nosel, clear) {
keyword = keyword.replace(/\*/g, "").replace(/^\.\//, "");
if (!arrayCache.length) {
lastSearch = keyword;
return;
}
// Needed for highlighting
ldSearch.keyword = keyword;
var searchResults;
if (!keyword) {
var result = arrayCache.slice();
// More prioritization for already open files
tabs.getTabs().forEach(function (tab) {
if (!tab.path
|| tab.document.meta.preview) return;
var idx = result.indexOf(tab.path);
if (idx > -1) {
result.splice(idx, 1);
result.unshift(tab.path);
}
});
searchResults = result;
}
else {
tree.provider.setScrollTop(0);
var base;
if (lastSearch && !clear)
base = keyword.substr(0, lastSearch.length) == lastSearch
? lastResults : arrayCache;
else
base = arrayCache;
searchResults = search.fileSearch(base, keyword);
}
lastSearch = keyword;
lastResults = searchResults.newlist;
if (searchResults)
ldSearch.updateData(searchResults);
if (nosel || !searchResults.length)
return;
var first = -1;
if (keyword) {
first = 0;
// See if there are open files that match the search results
// and the first if in the displayed results
var openTabs = tabs.getTabs(), hash = {};
for (var i = openTabs.length - 1; i >= 0; i--) {
var tab = openTabs[i];
if (!tab.document.meta.preview && tab.path) {
if (basename(tab.path).indexOf(keyword) === 0)
hash[tab.path] = true;
}
}
// loop over all visible items. If we find a visible item
// that is in the `hash`, select it and return.
var last = tree.renderer.$size.height / tree.provider.rowHeight;
for (var i = 0; i < last; i++) {
if (hash[ldSearch.visibleItems[i]]) {
first = i;
break;
}
}
}
// select the first item in the list
tree.select(tree.provider.getNodeAtIndex(first));
}
function openFile(noanim, nohide) {
if (!ldSearch.loaded)
return false;
var nodes = tree.selection.getSelectedNodes();
var cursor = tree.selection.getCursor();
// Cancel Preview and Keep the tab if there's only one
if (tabs.preview({ cancel: true, keep: nodes.length == 1 }) === true
|| intoOutline)
return nohide || plugin.hide();
nohide || plugin.hide();
var fn = function() {};
for (var i = 0, l = nodes.length; i < l; i++) {
var id = nodes[i].id;
if (!id) continue;
var path = id;
var focus = id === cursor.id;
tabs.open({
path: path,
noanim: l > 1,
focus: focus && (nohide ? "soft" : true)
}, fn);
}
lastPreviewed = null;
}
function previewFile(force) {
if ((!lastPreviewed || !lastPreviewed.loaded)
&& !settings.getBool("user/general/@preview-navigate") && !force)
return;
if (!ldSearch.loaded)
return false;
var node = tree.selection.getCursor();
var value = node && node.id;
if (!value)
return;
var path = util.normalizePath(value);
lastPreviewed = tabs.preview({ path: path }, function() {
onInput(false);
});
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("draw", function(e) {
draw(e);
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
});
plugin.on("show", function(e) {
analytics.log("Opened Navigate");
cleanInput();
txtGoToFile.focus();
txtGoToFile.select();
if (dirty)
updateFileCache(true);
});
plugin.on("hide", function(e) {
// Cancel Preview
tabs.preview({ cancel: true });
// Stop Outline if there
stopOutline();
// Prevent files from being refreshed
clearTimeout(timer);
txtGoToFile.blur();
});
plugin.on("unload", function() {
loaded = false;
drawn = false;
winGoToFile = null;
txtGoToFile = null;
tree = null;
ldSearch = null;
lastSearch = null;
lastPreviewed = null;
cleaning = null;
intoOutline = null;
isReloadScheduled = null;
dirty = true;
updating = false;
});
/***** Register and define API *****/
/**
* Navigation panel. Allows a user to navigate to files by searching
* for a fuzzy string that matches the path of the file.
* @singleton
* @extends Panel
**/
/**
* @command navigate
*/
/**
* Fires when the navigate panel shows
* @event showPanelNavigate
* @member panels
*/
/**
* Fires when the navigate panel hides
* @event hidePanelNavigate
* @member panels
*/
plugin.freezePublicAPI({
/**
* @property {Object} The tree implementation
* @private
*/
get tree() { return tree; },
/**
*
*/
markDirty: markDirty
});
register(null, {
navigate: plugin
});
}
});