436 lines
No EOL
13 KiB
JavaScript
436 lines
No EOL
13 KiB
JavaScript
define([
|
|
"sessions",
|
|
"command",
|
|
"editor",
|
|
"settings!menus,user",
|
|
"ui/statusbar",
|
|
"ui/projectManager",
|
|
"util/template!templates/paletteItem.html",
|
|
"util/dom2"
|
|
], function(sessions, command, editor, Settings, status, project, inflate) {
|
|
|
|
var TokenIterator = ace.require("ace/token_iterator").TokenIterator;
|
|
var refTest = /identifier|variable|function/;
|
|
var jsRefTest = /entity\.name\.function/;
|
|
|
|
//build a regex that finds special regex characters for sanitization
|
|
var antiregex = new RegExp("(\\\\|\\" + "?.*+[](){}|^$".split("").join("|\\") + ")", "g");
|
|
var sanitize = function(text) {
|
|
//turn HTML into escaped text for presentation
|
|
return text.replace(/\</g, "<").replace(/\>/g, ">").trim();
|
|
};
|
|
|
|
var re = {
|
|
file: /^([^:#@]*)/,
|
|
line: /:(\d*)/,
|
|
reference: /@([^:#]*)/,
|
|
search: /#([^:@]*)/
|
|
};
|
|
|
|
var prefixes = {
|
|
":": "line",
|
|
"@": "reference",
|
|
"#": "search"
|
|
};
|
|
|
|
var modes = {
|
|
"line": ":",
|
|
"search": "#",
|
|
"reference": "@"
|
|
};
|
|
|
|
var DEBOUNCE = 50;
|
|
|
|
var Palette = function() {
|
|
this.homeTab = null;
|
|
this.results = [];
|
|
this.cache = {};
|
|
this.allFiles = [];
|
|
this.files = [];
|
|
this.pending = null;
|
|
this.selected = 0;
|
|
this.element = document.find(".palette");
|
|
this.input = this.element.find("input");
|
|
this.resultList = this.element.find(".results");
|
|
this.commandMode = false;
|
|
this.searchAll = false;
|
|
this.bindInput();
|
|
};
|
|
Palette.prototype = {
|
|
bindInput: function() {
|
|
var input = this.input;
|
|
var self = this;
|
|
|
|
input.on("blur", function() {
|
|
self.deactivate();
|
|
});
|
|
|
|
input.on("keydown", function(e) {
|
|
//escape
|
|
if (e.keyCode == 27) {
|
|
sessions.restoreLocation();
|
|
editor.clearSelection();
|
|
return input.blur();
|
|
}
|
|
//enter
|
|
if (e.keyCode == 13) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
self.executeCurrent();
|
|
return;
|
|
}
|
|
//up/down
|
|
if (e.keyCode == 38 || e.keyCode == 40) {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
self.navigateList(e.keyCode == 38 ? -1 : 1);
|
|
self.render();
|
|
return;
|
|
}
|
|
//backspace -- falls through
|
|
if (e.keyCode == 8) {
|
|
//clear search progress cache
|
|
self.files = self.allFiles;
|
|
}
|
|
self.selected = 0;
|
|
});
|
|
|
|
input.on("keyup", function(e) {
|
|
if (self.pending) {
|
|
return;
|
|
}
|
|
self.pending = setTimeout(function() {
|
|
self.pending = null;
|
|
self.parse(input.value);
|
|
}, DEBOUNCE);
|
|
});
|
|
},
|
|
|
|
parse: function(query) {
|
|
var startsWith = query[0];
|
|
if (startsWith in prefixes) {
|
|
this.commandMode = false;
|
|
}
|
|
if (this.commandMode) {
|
|
this.findCommands(query);
|
|
} else {
|
|
this.findLocations(query);
|
|
}
|
|
this.render();
|
|
},
|
|
|
|
findCommands: function(query) {
|
|
if (query.length == 0) return this.results = [];
|
|
var fuzzyCommand = new RegExp(query
|
|
.split("")
|
|
.map(function(char) { return char.replace(antiregex, "\\$1")})
|
|
.join(".*"),
|
|
"i");
|
|
var results = [];
|
|
var menus = Settings.get("menus");
|
|
var menuWalker = function(menu) {
|
|
for (var i = 0; i < menu.length; i++) {
|
|
var item = menu[i];
|
|
//skip dividers and other special cases
|
|
if (typeof item == "string") continue;
|
|
if (item.minVersion && item.minVersion > window.navigator.version) continue;
|
|
if (item.command && fuzzyCommand.test(item.palette || item.label)) {
|
|
results.push(item);
|
|
}
|
|
if (item.sub) {
|
|
menuWalker(item.sub);
|
|
}
|
|
}
|
|
};
|
|
menuWalker(menus);
|
|
this.results = results.slice(0, 10);
|
|
if (this.results.length < 10) {
|
|
var commandMap = {};
|
|
//do not duplicate results from menu
|
|
this.results.forEach(function(r) { commandMap[r.command] = true; });
|
|
for (var i = 0; i < command.list.length; i++) {
|
|
var c = command.list[i];
|
|
if (c.command in commandMap) continue;
|
|
if (fuzzyCommand.test(c.label)) {
|
|
this.results.push(c);
|
|
//once we have 10, quit
|
|
if (this.results.length >= 10) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
getTabValues: function(tab) {
|
|
var name = tab.fileName;
|
|
if (!this.cache[name]) {
|
|
return this.cacheTab(tab);
|
|
}
|
|
var cache = this.cache[name];
|
|
for (var i = 0; i < cache.length; i++) {
|
|
if (cache[i].tab == tab) {
|
|
return cache[i];
|
|
}
|
|
}
|
|
return this.cacheTab(tab);
|
|
},
|
|
|
|
cacheTab: function(tab) {
|
|
//create cache entry
|
|
var entry = {
|
|
tab: tab,
|
|
refs: [],
|
|
text: tab.getValue()
|
|
};
|
|
//create token iterator, search for all references
|
|
var ti = new TokenIterator(tab, 0);
|
|
var token;
|
|
while (token = ti.stepForward()) {
|
|
if (tab.syntaxMode == "javascript" ? jsRefTest.test(token.type) : refTest.test(token.type)) {
|
|
//this is a match, let's store it as a valid result object
|
|
var row = ti.getCurrentTokenRow();
|
|
var col = ti.getCurrentTokenColumn();
|
|
var line = sanitize(tab.getLine(row));
|
|
entry.refs.push({
|
|
tab: tab,
|
|
line: row,
|
|
value: token.value,
|
|
label: tab.fileName + ":" + row,
|
|
sublabel: line,
|
|
column: col
|
|
});
|
|
}
|
|
}
|
|
var name = tab.fileName;
|
|
if (!this.cache[name]) {
|
|
this.cache[name] = [ entry ];
|
|
} else {
|
|
this.cache[name].push(entry);
|
|
}
|
|
return entry;
|
|
},
|
|
|
|
//note: this function is WAY TOO LONG
|
|
findLocations: function(query) {
|
|
var file = re.file.test(query) && re.file.exec(query)[1];
|
|
var line = re.line.test(query) && Number(re.line.exec(query)[1]) - 1;
|
|
var search = re.search.test(query) && re.search.exec(query)[1];
|
|
var reference = re.reference.test(query) && re.reference.exec(query)[1];
|
|
var results = [];
|
|
var self = this;
|
|
|
|
var tabs, projectFiles = [];
|
|
|
|
if (file) {
|
|
//search through open files by name
|
|
var fuzzyFile = new RegExp(file
|
|
.replace(/ /g, "")
|
|
.split("")
|
|
.map(function(char) { return char.replace(antiregex, "\\$1") })
|
|
.join(".*?"),
|
|
"i");
|
|
tabs = sessions.getAllTabs().filter(function(tab) {
|
|
return fuzzyFile.test(tab.fileName);
|
|
});
|
|
//check the project for matches as well
|
|
this.files = this.files.filter(function(path) { return fuzzyFile.test(path) });
|
|
|
|
//sort files by relevance
|
|
this.files.sort(function(a, b) {
|
|
//first check the filename for each
|
|
var aFile = a.split(/[\/\\]/).pop();
|
|
var bFile = b.split(/[\/\\]/).pop();
|
|
var aMatch = fuzzyFile.exec(aFile);
|
|
var bMatch = fuzzyFile.exec(bFile);
|
|
//if either file matches...
|
|
if (aMatch || bMatch) {
|
|
//and if one doesn't, the match wins
|
|
if (!aMatch) {
|
|
return 1;
|
|
} else if (!bMatch) {
|
|
return -1
|
|
} else {
|
|
var aScore = aMatch.pop().length + aMatch.index;
|
|
var bScore = bMatch.pop().length + bMatch.index;
|
|
var comparison = aScore - bScore;
|
|
//identical scores? sort on path
|
|
if (comparison == 0) {
|
|
return a < b ? -1 : 1;
|
|
}
|
|
return comparison;
|
|
}
|
|
}
|
|
//otherwise, sort shorter full-path match sequences higher
|
|
var aResult = fuzzyFile.exec(a).pop();
|
|
var bResult = fuzzyFile.exec(b).pop();
|
|
var len = aResult.length - bResult.length;
|
|
//identical lengths, sort on path
|
|
if (len == 0) {
|
|
return a < b ? -1 : 1;
|
|
}
|
|
return len;
|
|
});
|
|
|
|
//transform into result objects
|
|
projectFiles = this.files.map(function(path) {
|
|
return {
|
|
label: path.substr(path.search(/[^\/\\]+$/)),
|
|
sublabel: path,
|
|
command: "project:open-file",
|
|
argument: path
|
|
};
|
|
});
|
|
} else {
|
|
//the search domain is the current tab
|
|
var current = this.homeTab;
|
|
tabs = [ current ];
|
|
if (this.searchAll) {
|
|
tabs.push.apply(tabs, sessions.getAllTabs().filter(function(t) { return t !== current }));
|
|
}
|
|
}
|
|
|
|
tabs = tabs.map(function(t) {
|
|
return {
|
|
tab: t,
|
|
line: line
|
|
};
|
|
});
|
|
|
|
if (search) {
|
|
//find text in open tab(s)
|
|
try {
|
|
var crawl = new RegExp(search.replace(antiregex, "\\$1"), "gi");
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
var results = [];
|
|
tabs.forEach(function(t) {
|
|
if (results.length >= 10) return;
|
|
var found;
|
|
var lines = [];
|
|
var text = self.getTabValues(t.tab).text;
|
|
while (found = crawl.exec(text)) {
|
|
var position = t.tab.doc.indexToPosition(found.index);
|
|
if (lines.indexOf(position.row) > -1) {
|
|
continue;
|
|
}
|
|
lines.push(position.row);
|
|
var result = {
|
|
tab: t.tab
|
|
};
|
|
result.label = result.tab.fileName;
|
|
result.sublabel = sanitize(result.tab.getLine(position.row));
|
|
result.line = position.row;
|
|
results.push(result);
|
|
if (results.length >= 10) return;
|
|
}
|
|
});
|
|
tabs = results;
|
|
} else if (reference !== false) {
|
|
//search by symbol reference
|
|
try {
|
|
var crawl = new RegExp(reference.replace(antiregex, "\\$1"), "i");
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
var results = [];
|
|
tabs.forEach(function(t) {
|
|
if (results.length >= 10) return;
|
|
var refs = self.getTabValues(t.tab).refs;
|
|
for (var i = 0; i < refs.length; i++) {
|
|
if (crawl.test(refs[i].value)) {
|
|
var len = results.push(refs[i]);
|
|
if (len >= 10) return;
|
|
}
|
|
}
|
|
});
|
|
tabs = results;
|
|
}
|
|
|
|
this.results = tabs.concat(projectFiles).slice(0, 10);
|
|
|
|
if (this.results.length) {
|
|
var current = this.results[this.selected];
|
|
if (!current.tab) return;
|
|
sessions.raiseBlurred(current.tab);
|
|
if (current.line) {
|
|
editor.clearSelection();
|
|
editor.moveCursorTo(current.line, current.column || 0);
|
|
if (current.column) {
|
|
editor.execCommand("selectwordright");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
executeCurrent: function() {
|
|
var current = this.results[this.selected];
|
|
if (!current) return;
|
|
if (current.command) {
|
|
status.toast("Executing: " + current.label + "...");
|
|
command.fire(current.command, current.argument);
|
|
} else {
|
|
//must be the file search
|
|
command.fire("session:check-file");
|
|
}
|
|
this.deactivate();
|
|
if (!current.retainFocus) editor.focus();
|
|
},
|
|
|
|
activate: function(mode) {
|
|
this.homeTab = sessions.getCurrent();
|
|
this.results = [];
|
|
this.cache = {};
|
|
this.allFiles = this.files = project.getPaths();
|
|
this.selected = 0;
|
|
this.searchAll = Settings.get("user").searchAllFiles;
|
|
this.commandMode = mode == "command";
|
|
this.input.value = modes[mode] || "";
|
|
this.render();
|
|
this.element.addClass("active");
|
|
this.input.focus();
|
|
},
|
|
|
|
deactivate: function() {
|
|
this.element.removeClass("active");
|
|
if (this.pending) clearTimeout(this.pending);
|
|
},
|
|
|
|
navigateList: function(interval) {
|
|
this.selected = (this.selected + interval) % this.results.length;
|
|
if (this.selected < 0) {
|
|
this.selected = this.results.length + this.selected;
|
|
}
|
|
var current = this.results[this.selected];
|
|
if (current && current.tab) {
|
|
sessions.raiseBlurred(current.tab);
|
|
}
|
|
this.render();
|
|
},
|
|
|
|
render: function() {
|
|
var self = this;
|
|
this.element.find(".mode").innerHTML = this.commandMode ? "Command:" : "Go To:";
|
|
this.resultList.innerHTML = "";
|
|
this.results.slice(0, 10).forEach(function(r, i) {
|
|
var element = inflate.get("templates/paletteItem.html", {
|
|
label: r.palette || r.label || (r.tab ? r.tab.fileName : ""),
|
|
sublabel: r.sublabel,
|
|
isCurrent: i == self.selected
|
|
});
|
|
self.resultList.append(element);
|
|
});
|
|
}
|
|
};
|
|
|
|
var palette = new Palette();
|
|
|
|
command.on("palette:open", function(mode) {
|
|
sessions.saveLocation();
|
|
palette.activate(mode);
|
|
});
|
|
|
|
return palette;
|
|
|
|
}); |