Adding a search bar and project-wide search

This commit is contained in:
Brian Smith 2015-10-22 20:47:23 -06:00
parent 855b79decc
commit 520f8c0740
11 changed files with 322 additions and 4 deletions

View file

@ -44,6 +44,8 @@
{ "label": "Toggle Macro Recording", "command": "ace:togglemacro" },
{ "label": "Replay Macro", "command": "ace:command", "argument": "replaymacro" },
{ "label": "Search All Files", "command": "searchbar:show-project-search" },
{ "label": "Add Directory", "command": "project:add-dir" },
{ "label": "Remove All Directories", "command": "project:remove-all" },
{ "label": "Refresh Directories", "command": "project:refresh-dir" },

View file

@ -35,6 +35,7 @@
"Ctrl-Shift-P": { "command": "palette:open", "argument": "command" },
"Ctrl-R": { "command": "palette:open", "argument": "reference" },
"Ctrl-G": { "command": "palette:open", "argument": "line" },
"Ctrl-Shift-F": { "command": "searchbar:show-project-search" },
"Ctrl-M": { "ace": "jumptomatching" },
"Ctrl-Shift-M": { "command": "sublime:expand-to-matching" },
"Ctrl-Q": { "command": "ace:togglemacro" },

View file

@ -70,6 +70,9 @@
//By default, the palette searches the current file's text only unless you widen the scope.
//If you'd like it to search all open files by default, set this option to true.
"searchAllFiles": false,
//set the max search results that project wide search will return.
"maxSearchMatches": 50,
//set a regex to ignore in project view/search
"ignoreFiles": "node_modules",
//set this to enable autosave every X minutes

69
css/searchbar.less Normal file
View file

@ -0,0 +1,69 @@
.searchbar {
display: none;
color: darken(@foreground, 30%);
background-color: darken(@background, 10%);
z-index: 999;
padding: 8px;
box-sizing: content-box;
&.active {
display: block;
}
input[type=checkbox] {
display:none;
}
input[type=checkbox] + label {
display: inline-block;
color: darken(@foreground, 30%);
font-size: 16px;
width: 22px;
margin: 0 4px;
cursor: pointer;
}
input[type=checkbox]:checked + label {
font-weight: bold;
color: @accent;
}
.search-box {
width: ~"calc(100% - 120px)";
display: inline-block;
border: none;
padding: 4px;
background: transparent;
background-color: @background;
color: @foreground;
font-size: 12px;
&:focus {
outline: none;
}
}
button.find-all {
display: inline-block;
border: none;
outline: none;
background: transparent;
color: darken(@foreground, 30%);
cursor: pointer;
&:hover {
color: @accent;
}
}
a.close {
color: darken(@foreground, 30%);
display: inline-block;
font-size: 16px;
cursor: pointer;
font-family: Consolas,Monaco,monospace;
margin-left: 8px;
&:hover {
color: @accent;
}
}
}

View file

@ -9,4 +9,5 @@
@import "menus";
@import "dialog";
@import "palette";
@import "project";
@import "project";
@import "searchbar";

View file

@ -10,6 +10,7 @@
@import "dialog";
@import "palette";
@import "project";
@import "searchbar";
.central {
border-width: 0px 1px 0px 0px;

View file

@ -13,6 +13,7 @@ require([
"fileManager",
"ui/menus",
"ui/palette",
"ui/searchbar",
"ui/cli",
"ui/theme",
"api",
@ -72,7 +73,7 @@ require([
iconUrl: "icon-128.png",
title: i18n.get("notificationUpdateAvailable"),
message: i18n.get("notificationUpdateDetail", details.version),
buttons: [
buttons: [
{ title: i18n.get("notificationUpdateOK") },
{ title: i18n.get("notificationUpdateWait") }
]

View file

@ -52,6 +52,12 @@ Tracks any current project settings, files, and navigable directories.
Exposes the manager object to dependent modules, primarily so that the
palette can get the list of files for Go To File.
searchbar.js
------------
Allows the user to search all files in the project. Exposes only the
``searchbar:show-project-search`` command.
statusbar.js
------------

226
js/ui/searchbar.js Normal file
View file

@ -0,0 +1,226 @@
define([
"sessions",
"command",
"editor",
"storage/file",
"settings!user",
"ui/statusbar",
"ui/projectManager",
], function(sessions, command, editor, File, Settings, status, project) {
var Searchbar = function() {
var self = this;
this.element = document.find(".searchbar");
this.input = this.element.find(".search-box");
this.maxMatches = Settings.get("user").maxSearchMatches || 50;
command.on("init:restart", function() {
self.maxMatches = Settings.get("user").maxSearchMatches || 50;
});
this.currentSearch = {
matches: 0,
running: false
};
this.bindInput();
this.bindButtons();
};
Searchbar.prototype = {
bindInput: function() {
var input = this.input;
var self = this;
input.on("keydown", function(e) {
//escape
if (e.keyCode == 27) {
self.deactivate();
return;
}
//enter
if (e.keyCode == 13) {
e.stopImmediatePropagation();
e.preventDefault();
self.search();
return;
}
});
},
bindButtons: function() {
var self = this;
var findAll = this.element.find("button.find-all");
var close = this.element.find("a.close");
findAll.on("click", function() {
self.search();
});
close.on("click", function() {
self.deactivate();
});
},
// todo add regex support
// todo add search history
// we don't have to worry about the files blacklist because they are already removed from the project structure
search: function() {
if (this.currentSearch.running) {
return false;
}
var self = this;
this.currentSearch = {
matches: 0,
running: true
};
var isCaseSensitive = this.currentSearch.isCaseSensitive = this.element.find("#search-case-check").checked;
var displayQuery = this.input.value;
this.currentSearch.searchQuery = isCaseSensitive ? displayQuery : displayQuery.toUpperCase();
var resultsTab = this.currentSearch.resultsTab = sessions.addFile("Searching for:\n" + displayQuery + "\n");
resultsTab.fileName = "Results: " + displayQuery;
resultsTab.addEventListener('close', function() {
self.currentSearch.running = false;
})
var fileEntryList = this.getFlatFileEntryList();
var filesScanned = 0;
var consecutiveIOs = 0;
function searchMoreOrExit() {
var searchedEverything = fileEntryList.length === 0;
if (!searchedEverything && self.currentSearch.running) {
var file = fileEntryList.pop();
filesScanned++;
self.searchFile(file, searchMoreOrExit);
} else if (--consecutiveIOs === 0) { // check if the file that just finished is the last one
self.printSearchSummary(searchedEverything, filesScanned);
}
}
// we queue multiple files to be read at once so the cpu doesn't wait each time
for (var i = 0; i < 10; i++) {
consecutiveIOs++;
searchMoreOrExit();
};
},
// the array is returned in reverse order so we can use .pop() later
getFlatFileEntryList: function() {
var fileList = [];
function searchDirectory(node) {
for (var i = node.children.length - 1; i >= 0; i--) {
var childNode = node.children[i];
if (childNode.isDirectory) {
searchDirectory(childNode);
} else if (childNode.entry.isFile) {
fileList.push(childNode.entry);
}
}
}
for (var i = project.directories.length - 1; i >= 0; i--) {
searchDirectory(project.directories[i]);
};
return fileList;
},
searchFile: function(nodeEntry, cb) {
var self = this;
var prevLine = "";
var options = this.currentSearch;
chrome.fileSystem.getDisplayPath(nodeEntry, function(path) {
var file = new File(nodeEntry);
if (!options.running) {
return cb();
}
file.read(function(err, data) {
var lines = data.split("\n");
var line, msg;
var firstFind = true;
var printedLines = {}; // only print each line once per file per search
for (var i = 0; i < lines.length && options.running; i++) {
compareLine = options.isCaseSensitive ? lines[i] : lines[i].toUpperCase();
if (compareLine.indexOf(options.searchQuery) > -1) {
if (++options.matches >= self.maxMatches) {
options.running = false;
}
msg = "";
if (!printedLines[i] && !printedLines[i-1]) { // only add break if it and the line before it have not been printed
if (firstFind) {
msg += "\n" + nodeEntry.fullPath + "\n";
firstFind = false;
} else {
msg += "...\n";
}
msg += self.formatResultCode(i-1, prevLine);
msg += self.formatResultCode(i, lines[i]);
printedLines[i] = true;
}
if (i < lines.length - 1) {
msg += self.formatResultCode(i+1, lines[i+1]);
printedLines[i+1] = true;
}
self.appendToResults(msg);
}
prevLine = lines[i];
}
cb();
});
});
},
printSearchSummary: function(searchedEverything, filesScanned) {
this.appendToResults("\n\n" + this.currentSearch.matches + " matches found. " + filesScanned + " files scanned.");
if (!searchedEverything) {
this.appendToResults("\nSearch was cancelled. You can change the maximum number of search results allowed in User Preferences.")
}
this.currentSearch.running = false;
},
formatResultCode: function(lineNumber, code) {
return " " + (lineNumber+1) + ": " + code + "\n";
},
appendToResults: function(text) {
if (text === "") {
return;
}
var resultsTab = this.currentSearch.resultsTab;
var insertRow = resultsTab.doc.getLength();
resultsTab.doc.insert({row: insertRow, column: 0}, text);
},
activate: function(mode) {
var highlighted = editor.getSelectedText();
if (highlighted) {
this.input.value = highlighted;
}
this.element.addClass("active");
this.input.focus();
},
deactivate: function() {
this.currentSearch.running = false; // cancel search
this.element.removeClass("active");
}
};
var searchbar = new Searchbar();
command.on("searchbar:show-project-search", function() {
searchbar.activate();
});
return searchbar;
});

View file

@ -35,6 +35,13 @@
</div>
<div class="searchbar">
<input type='checkbox' id="search-case-check"/><label for="search-case-check" title="Match Case">Aa</label>
<input class="search-box" type="text">
<button class="find-all">Find All</button>
<a class="close">x</a>
</div>
<div class="bottom-bar">
<div class="status-text"></div>
<div class="mode-theme">
@ -42,7 +49,7 @@
<select class="theme" command="editor:theme" tabindex=-1 title="Select editor theme"></select>
</div>
</div>
<div class="command-line">
<input type="text" placeholder="caret:command arguments">
<a command="app:hide-prompt" class="close" title="Hide prompt">&times;</a>
@ -51,7 +58,7 @@
<div class="palette enter">
<div class="main">
<h1 class="mode"></h1>
<input class="request"></input>
<input class="request">
</div>
<ul class="results"></ul>
</div>

View file

@ -11,6 +11,7 @@ component, it offers powerful features like:
- command palette/smart go to
- hackable, synchronized configuration files
- project files and folder view
- fast project-wide string search
More information, links to Caret in the Chrome Web Store, and an
external package file are available at http://thomaswilburn.net/caret.