Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit 0c6e475

Browse files
committed
Merge pull request #4729 from TomMalbran/tom/find-in-files-p3
Find in Files Improvements (Part 3: Auto-update)
2 parents 25087f5 + 8a9bf5e commit 0c6e475

File tree

2 files changed

+172
-37
lines changed

2 files changed

+172
-37
lines changed

src/document/Document.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ define(function (require, exports, module) {
388388
// future, we should fix things so that we either don't need mock documents or that this
389389
// is factored so it will just run in both.
390390
$(this).triggerHandler("change", [this, changeList]);
391+
$(exports).triggerHandler("documentChange", [this, changeList]);
391392
};
392393

393394
/**

src/search/FindInFiles.js

Lines changed: 171 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ define(function (require, exports, module) {
4949
Strings = require("strings"),
5050
StringUtils = require("utils/StringUtils"),
5151
ProjectManager = require("project/ProjectManager"),
52+
DocumentModule = require("document/Document"),
5253
DocumentManager = require("document/DocumentManager"),
5354
EditorManager = require("editor/EditorManager"),
5455
PanelManager = require("view/PanelManager"),
@@ -69,7 +70,8 @@ define(function (require, exports, module) {
6970

7071
/** @cost Constants used to define the maximum results show per page and found in a single file */
7172
var RESULTS_PER_PAGE = 100,
72-
FIND_IN_FILE_MAX = 300;
73+
FIND_IN_FILE_MAX = 300,
74+
UPDATE_TIMEOUT = 400;
7375

7476
/**
7577
* Map of all the last search results
@@ -86,12 +88,18 @@ define(function (require, exports, module) {
8688
/** @type {string} The current search query */
8789
var currentQuery = "";
8890

91+
/** @type {RegExp} The current search query regular expression */
92+
var currentQueryExpr = null;
93+
8994
/** @type {Array.<FileEntry>} An array of the files where it should look or null/empty to search the entire project */
9095
var currentScope = null;
9196

9297
/** @type {boolean} True if the matches in a file reached FIND_IN_FILE_MAX */
9398
var maxHitsFoundInFile = false;
9499

100+
/** @type {string} The setTimeout id, used to clear it if required */
101+
var timeoutID = null;
102+
95103
/** @type {$.Element} jQuery elements used in the search results */
96104
var $searchResults,
97105
$searchSummary,
@@ -102,7 +110,7 @@ define(function (require, exports, module) {
102110
/**
103111
* @private
104112
* Returns a regular expression from the given query and shows an error in the modal-bar if it was invalid
105-
* @param {!string} query - The query from the modal-bar input
113+
* @param {string} query The query from the modal-bar input
106114
* @return {RegExp}
107115
*/
108116
function _getQueryRegExp(query) {
@@ -184,8 +192,8 @@ define(function (require, exports, module) {
184192

185193
/**
186194
* Shows the search dialog
187-
* @param {?string} initialString Default text to prepopulate the search field with
188-
* @param {?Entry} scope Search scope, or null to search whole project
195+
* @param {string=} initialString Default text to prepopulate the search field with
196+
* @param {Entry=} scope Search scope, or null to search whole project
189197
* @returns {$.Promise} that is resolved with the string to search for
190198
*/
191199
FindInFilesDialog.prototype.showDialog = function (initialString, scope) {
@@ -238,6 +246,7 @@ define(function (require, exports, module) {
238246
function _hideSearchResults() {
239247
if (searchResultsPanel.isVisible()) {
240248
searchResultsPanel.hide();
249+
$(DocumentModule).off(".findInFiles");
241250
}
242251
}
243252

@@ -537,11 +546,126 @@ define(function (require, exports, module) {
537546
_hideSearchResults();
538547
}
539548
}
540-
549+
550+
551+
552+
/**
553+
* @private
554+
* Shows the search results and tries to restore the previous scroll and selection
555+
*/
556+
function _restoreSearchResults() {
557+
if (searchResultsPanel.isVisible()) {
558+
var scrollTop = $searchContent.scrollTop(),
559+
index = $selectedRow ? $selectedRow.index() : null,
560+
numMatches = _countFilesMatches().matches;
561+
562+
if (currentStart > numMatches) {
563+
currentStart = _getLastCurrentStart(numMatches);
564+
}
565+
_showSearchResults();
566+
567+
$searchContent.scrollTop(scrollTop);
568+
if (index) {
569+
$selectedRow = $searchContent.find("tr:eq(" + index + ")");
570+
$selectedRow.addClass("selected");
571+
}
572+
}
573+
}
574+
575+
/**
576+
* @private
577+
* Update the search results using the given list of changes fr the given document
578+
* @param {Document} doc The Document that changed, should be the current one
579+
* @param {{from: {line:number,ch:number}, to: {line:number,ch:number}, text: string, next: change}} change
580+
* A linked list as described in the Document constructor
581+
* @param {boolean} resultsChanged True when the search results changed from a file change
582+
*/
583+
function _updateSearchResults(doc, change, resultsChanged) {
584+
var i, diff, matches,
585+
fullPath = doc.file.fullPath,
586+
lines = [],
587+
start = 0,
588+
howMany = 0;
589+
590+
// There is no from or to positions, so the entire file changed, we must search all over again
591+
if (!change.from || !change.to) {
592+
_addSearchMatches(fullPath, doc.getText(), currentQueryExpr);
593+
resultsChanged = true;
594+
595+
} else {
596+
// Get only the lines that changed
597+
for (i = 0; i < change.text.length; i++) {
598+
lines.push(doc.getLine(change.from.line + i));
599+
}
600+
601+
// We need to know how many lines changed to update the rest of the lines
602+
if (change.from.line !== change.to.line) {
603+
diff = change.from.line - change.to.line;
604+
} else {
605+
diff = lines.length - 1;
606+
}
607+
608+
if (searchResults[fullPath]) {
609+
// Search the last match before a replacement, the amount of matches deleted and update
610+
// the lines values for all the matches after the change
611+
searchResults[fullPath].matches.forEach(function (item) {
612+
if (item.end.line < change.from.line) {
613+
start++;
614+
} else if (item.end.line <= change.to.line) {
615+
howMany++;
616+
} else {
617+
item.start.line += diff;
618+
item.end.line += diff;
619+
}
620+
});
621+
622+
// Delete the lines that where deleted or replaced
623+
if (howMany > 0) {
624+
searchResults[fullPath].matches.splice(start, howMany);
625+
}
626+
resultsChanged = true;
627+
}
628+
629+
// Searches only over the lines that changed
630+
matches = _getSearchMatches(lines.join("\r\n"), currentQueryExpr);
631+
if (matches && matches.length) {
632+
// Updates the line numbers, since we only searched part of the file
633+
matches.forEach(function (value, key) {
634+
matches[key].start.line += change.from.line;
635+
matches[key].end.line += change.from.line;
636+
});
637+
638+
// If the file index exists, add the new matches to the file at the start index found before
639+
if (searchResults[fullPath]) {
640+
Array.prototype.splice.apply(searchResults[fullPath].matches, [start, 0].concat(matches));
641+
// If not, add the matches to a new file index
642+
} else {
643+
searchResults[fullPath] = {
644+
matches: matches,
645+
collapsed: false
646+
};
647+
}
648+
resultsChanged = true;
649+
}
650+
651+
// All the matches where deleted, remove the file from the results
652+
if (searchResults[fullPath] && !searchResults[fullPath].matches.length) {
653+
delete searchResults[fullPath];
654+
resultsChanged = true;
655+
}
656+
657+
// This is link to the next change object, so we need to keep searching
658+
if (change.next) {
659+
return _updateSearchResults(doc, change.next, resultsChanged);
660+
}
661+
}
662+
return resultsChanged;
663+
}
664+
541665
/**
542666
* @private
543-
* @param {!FileInfo} fileInfo File in question
544-
* @param {?Entry} scope Search scope, or null if whole project
667+
* @param {!FileInfo} fileInfo File in question
668+
* @param {?Entry} scope Search scope, or null if whole project
545669
* @return {boolean}
546670
*/
547671
function _inScope(fileInfo, scope) {
@@ -556,12 +680,39 @@ define(function (require, exports, module) {
556680
}
557681
return true;
558682
}
559-
683+
684+
/**
685+
* @private
686+
* Tries to update the search result on document changes
687+
* @param {$.Event} event
688+
* @param {Document} document
689+
* @param {{from: {line:number,ch:number}, to: {line:number,ch:number}, text: string, next: change}} change
690+
* A linked list as described in the Document constructor
691+
*/
692+
function _documentChangeHandler(event, document, change) {
693+
if (searchResultsPanel.isVisible() && _inScope(document.file, currentScope)) {
694+
var updateResults = _updateSearchResults(document, change, false);
695+
696+
if (timeoutID) {
697+
window.clearTimeout(timeoutID);
698+
updateResults = true;
699+
}
700+
if (updateResults) {
701+
timeoutID = window.setTimeout(function () {
702+
_restoreSearchResults();
703+
timeoutID = null;
704+
}, UPDATE_TIMEOUT);
705+
}
706+
}
707+
}
708+
709+
710+
560711
/**
561712
* @private
562713
* Displays a non-modal embedded dialog above the code mirror editor that allows the user to do
563714
* a find operation across all files in the project.
564-
* @param {?Entry} scope Project file/subfolder to search within; else searches whole project.
715+
* @param {?Entry} scope Project file/subfolder to search within; else searches whole project.
565716
*/
566717
function _doFindInFiles(scope) {
567718
if (scope instanceof NativeFileSystem.InaccessibleFileEntry) {
@@ -579,15 +730,17 @@ define(function (require, exports, module) {
579730
searchResults = {};
580731
currentStart = 0;
581732
currentQuery = "";
733+
currentQueryExpr = null;
582734
currentScope = scope;
583735
maxHitsFoundInFile = false;
584736

585737
dialog.showDialog(initialString, scope)
586738
.done(function (query) {
587739
if (query) {
588-
currentQuery = query;
589-
var queryExpr = _getQueryRegExp(query);
590-
if (!queryExpr) {
740+
currentQuery = query;
741+
currentQueryExpr = _getQueryRegExp(query);
742+
743+
if (!currentQueryExpr) {
591744
return;
592745
}
593746
StatusBar.showBusyIndicator(true);
@@ -602,7 +755,7 @@ define(function (require, exports, module) {
602755
// Search one file
603756
DocumentManager.getDocumentForPath(fileInfo.fullPath)
604757
.done(function (doc) {
605-
_addSearchMatches(fileInfo.fullPath, doc.getText(), queryExpr);
758+
_addSearchMatches(fileInfo.fullPath, doc.getText(), currentQueryExpr);
606759
result.resolve();
607760
})
608761
.fail(function (error) {
@@ -617,6 +770,7 @@ define(function (require, exports, module) {
617770
// Done searching all files: show results
618771
_showSearchResults();
619772
StatusBar.hideBusyIndicator();
773+
$(DocumentModule).on("documentChange.findInFiles", _documentChangeHandler);
620774
})
621775
.fail(function () {
622776
console.log("find in files failed.");
@@ -637,23 +791,6 @@ define(function (require, exports, module) {
637791
}
638792

639793

640-
/**
641-
* @private
642-
* Shows the search results and tries to restore the previous scroll and selection
643-
*/
644-
function _restoreSearchResults() {
645-
var scrollTop = $searchContent.scrollTop(),
646-
index = $selectedRow ? $selectedRow.index() : null;
647-
648-
_showSearchResults();
649-
650-
$searchContent.scrollTop(scrollTop);
651-
if ($selectedRow) {
652-
$selectedRow = $searchContent.find("tr:eq(" + index + ")");
653-
$selectedRow.addClass("selected");
654-
}
655-
}
656-
657794
/**
658795
* @private
659796
* Moves the search results from the previous path to the new one and updates the results list, if required
@@ -688,7 +825,7 @@ define(function (require, exports, module) {
688825
* @param {string} path
689826
*/
690827
function _pathDeletedHandler(event, path) {
691-
var resultsChanged = false, numMatches;
828+
var resultsChanged = false;
692829

693830
if (searchResultsPanel.isVisible()) {
694831
// Update the search results
@@ -701,16 +838,13 @@ define(function (require, exports, module) {
701838

702839
// Restore the results if needed
703840
if (resultsChanged) {
704-
numMatches = _countFilesMatches().matches;
705-
if (currentStart > numMatches) {
706-
currentStart = _getLastCurrentStart(numMatches);
707-
}
708841
_restoreSearchResults();
709842
}
710843
}
711844
}
712845

713846

847+
714848
// Initialize items dependent on HTML DOM
715849
AppInit.htmlReady(function () {
716850
var panelHtml = Mustache.render(searchPanelTemplate, Strings);
@@ -722,8 +856,8 @@ define(function (require, exports, module) {
722856
});
723857

724858
// Initialize: register listeners
725-
$(DocumentManager).on("fileNameChange", _fileNameChangeHandler);
726-
$(DocumentManager).on("pathDeleted", _pathDeletedHandler);
859+
$(DocumentManager).on("fileNameChange", _fileNameChangeHandler);
860+
$(DocumentManager).on("pathDeleted", _pathDeletedHandler);
727861
$(ProjectManager).on("beforeProjectClose", _hideSearchResults);
728862

729863
// Initialize: command handlers

0 commit comments

Comments
 (0)