From 520f8c0740c6da92c322a3f83473e485f1efd66f Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 22 Oct 2015 20:47:23 -0600 Subject: [PATCH] Adding a search bar and project-wide search --- config/commands.json | 2 + config/keys.json | 1 + config/user.json | 3 + css/searchbar.less | 69 +++++++++++++ css/seed-dark.less | 3 +- css/seed.less | 1 + js/main.js | 3 +- js/ui/readme.rst | 6 ++ js/ui/searchbar.js | 226 +++++++++++++++++++++++++++++++++++++++++++ main.html | 11 ++- readme.rst | 1 + 11 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 css/searchbar.less create mode 100644 js/ui/searchbar.js diff --git a/config/commands.json b/config/commands.json index a498f20..4c9d4f8 100644 --- a/config/commands.json +++ b/config/commands.json @@ -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" }, diff --git a/config/keys.json b/config/keys.json index 6c34be7..1186d59 100644 --- a/config/keys.json +++ b/config/keys.json @@ -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" }, diff --git a/config/user.json b/config/user.json index f76f600..09d6103 100644 --- a/config/user.json +++ b/config/user.json @@ -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 diff --git a/css/searchbar.less b/css/searchbar.less new file mode 100644 index 0000000..fb09917 --- /dev/null +++ b/css/searchbar.less @@ -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; + } + } +} \ No newline at end of file diff --git a/css/seed-dark.less b/css/seed-dark.less index 7395a2a..fb18086 100644 --- a/css/seed-dark.less +++ b/css/seed-dark.less @@ -9,4 +9,5 @@ @import "menus"; @import "dialog"; @import "palette"; -@import "project"; \ No newline at end of file +@import "project"; +@import "searchbar"; \ No newline at end of file diff --git a/css/seed.less b/css/seed.less index 1a17c34..79dc51f 100644 --- a/css/seed.less +++ b/css/seed.less @@ -10,6 +10,7 @@ @import "dialog"; @import "palette"; @import "project"; +@import "searchbar"; .central { border-width: 0px 1px 0px 0px; diff --git a/js/main.js b/js/main.js index 2d0fd0d..dc43bf9 100644 --- a/js/main.js +++ b/js/main.js @@ -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") } ] diff --git a/js/ui/readme.rst b/js/ui/readme.rst index 936f252..08240a9 100644 --- a/js/ui/readme.rst +++ b/js/ui/readme.rst @@ -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 ------------ diff --git a/js/ui/searchbar.js b/js/ui/searchbar.js new file mode 100644 index 0000000..9a1acb2 --- /dev/null +++ b/js/ui/searchbar.js @@ -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; +}); diff --git a/main.html b/main.html index 313d8fb..11603a5 100644 --- a/main.html +++ b/main.html @@ -35,6 +35,13 @@ + +
@@ -42,7 +49,7 @@
- +
× @@ -51,7 +58,7 @@

- +
    diff --git a/readme.rst b/readme.rst index b317033..6a90255 100644 --- a/readme.rst +++ b/readme.rst @@ -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.