Adding a search bar and project-wide search
This commit is contained in:
parent
855b79decc
commit
520f8c0740
11 changed files with 322 additions and 4 deletions
|
@ -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" },
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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
69
css/searchbar.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,4 +9,5 @@
|
|||
@import "menus";
|
||||
@import "dialog";
|
||||
@import "palette";
|
||||
@import "project";
|
||||
@import "project";
|
||||
@import "searchbar";
|
|
@ -10,6 +10,7 @@
|
|||
@import "dialog";
|
||||
@import "palette";
|
||||
@import "project";
|
||||
@import "searchbar";
|
||||
|
||||
.central {
|
||||
border-width: 0px 1px 0px 0px;
|
||||
|
|
|
@ -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") }
|
||||
]
|
||||
|
|
|
@ -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
226
js/ui/searchbar.js
Normal 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;
|
||||
});
|
11
main.html
11
main.html
|
@ -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">×</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>
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue