/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.8
src/search/FindInFiles.js
951 строка
40 KB
Martin Zagora
Merge pull request #11602 from amrelnaggar/findinfiles-bugfix
27 авг 2016, 10:44
27 авг 2016, 10:44
57b5131
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /* * The core search functionality used by Find in Files and single-file Replace Batch. */ define(function (require, exports, module) { "use strict"; var _ = require("thirdparty/lodash"), FileFilters = require("search/FileFilters"), Async = require("utils/Async"), StringUtils = require("utils/StringUtils"), ProjectManager = require("project/ProjectManager"), PreferencesManager = require("preferences/PreferencesManager"), DocumentModule = require("document/Document"), DocumentManager = require("document/DocumentManager"), MainViewManager = require("view/MainViewManager"), FileSystem = require("filesystem/FileSystem"), LanguageManager = require("language/LanguageManager"), SearchModel = require("search/SearchModel").SearchModel, PerfUtils = require("utils/PerfUtils"), NodeDomain = require("utils/NodeDomain"), FileUtils = require("file/FileUtils"), FindUtils = require("search/FindUtils"), HealthLogger = require("utils/HealthLogger"); var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), _modulePath = FileUtils.getNativeModuleDirectoryPath(module), _nodePath = "node/FindInFilesDomain", _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), searchDomain = new NodeDomain("FindInFiles", _domainPath), searchScopeChanged = false, findOrReplaceInProgress = false, changedFileList = {}; /** * Token used to indicate a specific reason for zero search results * @const @type {!Object} */ var ZERO_FILES_TO_SEARCH = {}; /** * Maximum length of text displayed in search results panel * @const */ var MAX_DISPLAY_LENGTH = 200; /** * The search query and results model. * @type {SearchModel} */ var searchModel = new SearchModel(); /* Forward declarations */ var _documentChangeHandler, _fileSystemChangeHandler, _fileNameChangeHandler, clearSearch; /** Remove the listeners that were tracking potential search result changes */ function _removeListeners() { DocumentModule.off("documentChange", _documentChangeHandler); FileSystem.off("change", _fileSystemChangeHandler); DocumentManager.off("fileNameChange", _fileNameChangeHandler); } /** Add listeners to track events that might change the search result set */ function _addListeners() { // Avoid adding duplicate listeners - e.g. if a 2nd search is run without closing the old results panel first _removeListeners(); DocumentModule.on("documentChange", _documentChangeHandler); FileSystem.on("change", _fileSystemChangeHandler); DocumentManager.on("fileNameChange", _fileNameChangeHandler); } function nodeFileCacheComplete(event, numFiles, cacheSize) { if (/\/test\/SpecRunner\.html$/.test(window.location.pathname)) { // Ignore the event in the SpecRunner window return; } var projectRoot = ProjectManager.getProjectRoot(), projectName = projectRoot ? projectRoot.name : null; if (!projectName) { console.error("'File cache complete' event received, but no project root found"); projectName = "noName00"; } FindUtils.setInstantSearchDisabled(false); // Node search could be disabled if some error has happened in node. But upon // project change, if we get this message, then it means that node search is working, // we re-enable node search. If a search fails, node search will be switched off eventually. FindUtils.setNodeSearchDisabled(false); FindUtils.notifyIndexingFinished(); HealthLogger.setProjectDetail(projectName, numFiles, cacheSize); } /** * @private * Searches through the contents and returns an array of matches * @param {string} contents * @param {RegExp} queryExpr * @return {!Array.<{start: {line:number,ch:number}, end: {line:number,ch:number}, line: string}>} */ function _getSearchMatches(contents, queryExpr) { // Quick exit if not found or if we hit the limit if (searchModel.foundMaximum || contents.search(queryExpr) === -1) { return []; } var match, lineNum, line, ch, totalMatchLength, matchedLines, numMatchedLines, lastLineLength, endCh, padding, leftPadding, rightPadding, highlightOffset, highlightEndCh, lines = StringUtils.getLines(contents), matches = []; while ((match = queryExpr.exec(contents)) !== null) { lineNum = StringUtils.offsetToLineNum(lines, match.index); line = lines[lineNum]; ch = match.index - contents.lastIndexOf("\n", match.index) - 1; // 0-based index matchedLines = match[0].split("\n"); numMatchedLines = matchedLines.length; totalMatchLength = match[0].length; lastLineLength = matchedLines[matchedLines.length - 1].length; endCh = (numMatchedLines === 1 ? ch + totalMatchLength : lastLineLength); highlightEndCh = (numMatchedLines === 1 ? endCh : line.length); highlightOffset = 0; if (highlightEndCh <= MAX_DISPLAY_LENGTH) { // Don't store more than 200 chars per line line = line.substr(0, MAX_DISPLAY_LENGTH); } else if (totalMatchLength > MAX_DISPLAY_LENGTH) { // impossible to display the whole match line = line.substr(ch, ch + MAX_DISPLAY_LENGTH); highlightOffset = ch; } else { // Try to have both beginning and end of match displayed padding = MAX_DISPLAY_LENGTH - totalMatchLength; rightPadding = Math.floor(Math.min(padding / 2, line.length - highlightEndCh)); leftPadding = Math.ceil(padding - rightPadding); highlightOffset = ch - leftPadding; line = line.substring(highlightOffset, highlightEndCh + rightPadding); } matches.push({ start: {line: lineNum, ch: ch}, end: {line: lineNum + numMatchedLines - 1, ch: endCh}, highlightOffset: highlightOffset, // Note that the following offsets from the beginning of the file are *not* updated if the search // results change. These are currently only used for multi-file replacement, and we always // abort the replace (by shutting the results panel) if we detect any result changes, so we don't // need to keep them up to date. Eventually, we should either get rid of the need for these (by // doing everything in terms of line/ch offsets, though that will require re-splitting files when // doing a replace) or properly update them. startOffset: match.index, endOffset: match.index + totalMatchLength, line: line, result: match, isChecked: true }); // We have the max hits in just this 1 file. Stop searching this file. // This fixed issue #1829 where code hangs on too many hits. // Adds one over MAX_TOTAL_RESULTS in order to know if the search has exceeded // or is equal to MAX_TOTAL_RESULTS. Additional result removed in SearchModel if (matches.length > SearchModel.MAX_TOTAL_RESULTS) { queryExpr.lastIndex = 0; break; } // Pathological regexps like /^/ return 0-length matches. Ensure we make progress anyway if (totalMatchLength === 0) { queryExpr.lastIndex++; } } return matches; } /** * @private * Update the search results using the given list of changes for the given document * @param {Document} doc The Document that changed, should be the current one * @param {Array.<{from: {line:number,ch:number}, to: {line:number,ch:number}, text: !Array.<string>}>} changeList * An array of changes as described in the Document constructor */ function _updateResults(doc, changeList) { var i, diff, matches, lines, start, howMany, resultsChanged = false, fullPath = doc.file.fullPath, resultInfo = searchModel.results[fullPath]; // Remove the results before we make any changes, so the SearchModel can accurately update its count. searchModel.removeResults(fullPath); changeList.forEach(function (change) { lines = []; start = 0; howMany = 0; // There is no from or to positions, so the entire file changed, we must search all over again if (!change.from || !change.to) { // TODO: add unit test exercising timestamp logic in this case // We don't just call _updateSearchMatches() here because we want to continue iterating through changes in // the list and update at the end. resultInfo = {matches: _getSearchMatches(doc.getText(), searchModel.queryExpr), timestamp: doc.diskTimestamp}; resultsChanged = true; } else { // Get only the lines that changed for (i = 0; i < change.text.length; i++) { lines.push(doc.getLine(change.from.line + i)); } // We need to know how many newlines were inserted/deleted in order to update the rest of the line indices; // this is the total number of newlines inserted (which is the length of the lines array minus // 1, since the last line in the array is inserted without a newline after it) minus the // number of original newlines being removed. diff = lines.length - 1 - (change.to.line - change.from.line); if (resultInfo) { // Search the last match before a replacement, the amount of matches deleted and update // the lines values for all the matches after the change resultInfo.matches.forEach(function (item) { if (item.end.line < change.from.line) { start++; } else if (item.end.line <= change.to.line) { howMany++; } else { item.start.line += diff; item.end.line += diff; } }); // Delete the lines that where deleted or replaced if (howMany > 0) { resultInfo.matches.splice(start, howMany); } resultsChanged = true; } // Searches only over the lines that changed matches = _getSearchMatches(lines.join("\r\n"), searchModel.queryExpr); if (matches.length) { // Updates the line numbers, since we only searched part of the file matches.forEach(function (value, key) { matches[key].start.line += change.from.line; matches[key].end.line += change.from.line; }); // If the file index exists, add the new matches to the file at the start index found before if (resultInfo) { Array.prototype.splice.apply(resultInfo.matches, [start, 0].concat(matches)); // If not, add the matches to a new file index } else { // TODO: add unit test exercising timestamp logic in self case resultInfo = { matches: matches, collapsed: false, timestamp: doc.diskTimestamp }; } resultsChanged = true; } } }); // Always re-add the results, even if nothing changed. if (resultInfo && resultInfo.matches.length) { searchModel.setResults(fullPath, resultInfo); } if (resultsChanged) { // Pass `true` for quickChange here. This will make listeners debounce the change event, // avoiding lots of updates if the user types quickly. searchModel.fireChanged(true); } } /** * Checks that the file matches the given subtree scope. To fully check whether the file * should be in the search set, use _inSearchScope() instead - a supserset of this. * * @param {!File} file * @param {?FileSystemEntry} scope Search scope, or null if whole project * @return {boolean} */ function _subtreeFilter(file, scope) { if (scope) { if (scope.isDirectory) { // Dirs always have trailing slash, so we don't have to worry about being // a substring of another dir name return file.fullPath.indexOf(scope.fullPath) === 0; } else { return file.fullPath === scope.fullPath; } } return true; } /** * Filters out files that are known binary types. * @param {string} fullPath * @return {boolean} True if the file's contents can be read as text */ function _isReadableText(fullPath) { return !LanguageManager.getLanguageForPath(fullPath).isBinary(); } /** * Finds all candidate files to search in the given scope's subtree that are not binary content. Does NOT apply * the current filter yet. * @param {?FileSystemEntry} scope Search scope, or null if whole project * @return {$.Promise} A promise that will be resolved with the list of files in the scope. Never rejected. */ function getCandidateFiles(scope) { function filter(file) { return _subtreeFilter(file, scope) && _isReadableText(file.fullPath); } // If the scope is a single file, just check if the file passes the filter directly rather than // trying to use ProjectManager.getAllFiles(), both for performance and because an individual // in-memory file might be an untitled document that doesn't show up in getAllFiles(). if (scope && scope.isFile) { return new $.Deferred().resolve(filter(scope) ? [scope] : []).promise(); } else { return ProjectManager.getAllFiles(filter, true, true); } } /** * Checks that the file is eligible for inclusion in the search (matches the user's subtree scope and * file exclusion filters, and isn't binary). Used when updating results incrementally - during the * initial search, these checks are done in bulk via getCandidateFiles() and the filterFileList() call * after it. * @param {!File} file * @return {boolean} */ function _inSearchScope(file) { // Replicate the checks getCandidateFiles() does if (searchModel && searchModel.scope) { if (!_subtreeFilter(file, searchModel.scope)) { return false; } } else { // Still need to make sure it's within project or working set // In getCandidateFiles(), this is covered by the baseline getAllFiles() itself if (file.fullPath.indexOf(ProjectManager.getProjectRoot().fullPath) !== 0) { if (MainViewManager.findInWorkingSet(MainViewManager.ALL_PANES, file.fullPath) === -1) { return false; } } } if (!_isReadableText(file.fullPath)) { return false; } // Replicate the filtering filterFileList() does return FileFilters.filterPath(searchModel.filter, file.fullPath); } /** * @private * Tries to update the search result on document changes * @param {$.Event} event * @param {Document} document * @param {<{from: {line:number,ch:number}, to: {line:number,ch:number}, text: !Array.<string>}>} change * A change list as described in the Document constructor */ _documentChangeHandler = function (event, document, change) { if (!findOrReplaceInProgress) { changedFileList[document.file.fullPath] = true; } else { if (_inSearchScope(document.file)) { _updateResults(document, change); } } }; /** * @private * Finds search results in the given file and adds them to 'searchResults.' Resolves with * true if any matches found, false if none found. Errors reading the file are treated the * same as if no results found. * * Does not perform any filtering - assumes caller has already vetted this file as a search * candidate. * * @param {!File} file * @return {$.Promise} */ function _doSearchInOneFile(file) { var result = new $.Deferred(); DocumentManager.getDocumentText(file) .done(function (text, timestamp) { // Note that we don't fire a model change here, since this is always called by some outer batch // operation that will fire it once it's done. var matches = _getSearchMatches(text, searchModel.queryExpr); searchModel.setResults(file.fullPath, {matches: matches, timestamp: timestamp}); result.resolve(!!matches.length); }) .fail(function () { // Always resolve. If there is an error, this file // is skipped and we move on to the next file. result.resolve(false); }); return result.promise(); } /** * @private * Inform node that the document has changed [along with its contents] * @param {string} docPath the path of the changed document */ function _updateDocumentInNode(docPath) { DocumentManager.getDocumentForPath(docPath).done(function (doc) { var updateObject = { "filePath": docPath, "docContents": doc.getText() }; searchDomain.exec("documentChanged", updateObject); }); } /** * @private * sends all changed documents that we have tracked to node */ function _updateChangedDocs() { var key = null; for (key in changedFileList) { if (changedFileList.hasOwnProperty(key)) { _updateDocumentInNode(key); } } } /** * @private * Executes the Find in Files search inside the current scope. * @param {{query: string, caseSensitive: boolean, isRegexp: boolean}} queryInfo Query info object * @param {!$.Promise} candidateFilesPromise Promise from getCandidateFiles(), which was called earlier * @param {?string} filter A "compiled" filter as returned by FileFilters.compile(), or null for no filter * @return {?$.Promise} A promise that's resolved with the search results (or ZERO_FILES_TO_SEARCH) or rejected when the find competes. * Will be null if the query is invalid. */ function _doSearch(queryInfo, candidateFilesPromise, filter) { searchModel.filter = filter; var queryResult = searchModel.setQueryInfo(queryInfo); if (!queryResult) { return null; } var scopeName = searchModel.scope ? searchModel.scope.fullPath : ProjectManager.getProjectRoot().fullPath, perfTimer = PerfUtils.markStart("FindIn: " + scopeName + " - " + queryInfo.query); findOrReplaceInProgress = true; return candidateFilesPromise .then(function (fileListResult) { // Filter out files/folders that match user's current exclusion filter fileListResult = FileFilters.filterFileList(filter, fileListResult); if (searchModel.isReplace || FindUtils.isNodeSearchDisabled()) { if (fileListResult.length) { searchModel.allResultsAvailable = true; return Async.doInParallel(fileListResult, _doSearchInOneFile); } else { return ZERO_FILES_TO_SEARCH; } } var searchDeferred = new $.Deferred(); if (fileListResult.length) { var searchObject; if (searchScopeChanged) { var files = fileListResult .filter(function (entry) { return entry.isFile && _isReadableText(entry.fullPath); }) .map(function (entry) { return entry.fullPath; }); /* The following line prioritizes the open Document in editor and * pushes it to the top of the filelist. */ files = FindUtils.prioritizeOpenFile(files, FindUtils.getOpenFilePath()); searchObject = { "files": files, "queryInfo": queryInfo, "queryExpr": searchModel.queryExpr }; searchScopeChanged = false; } else { searchObject = { "queryInfo": queryInfo, "queryExpr": searchModel.queryExpr }; } if (searchModel.isReplace) { searchObject.getAllResults = true; } _updateChangedDocs(); FindUtils.notifyNodeSearchStarted(); searchDomain.exec("doSearch", searchObject) .done(function (rcvd_object) { FindUtils.notifyNodeSearchFinished(); if (!rcvd_object || !rcvd_object.results) { console.log('no node falling back to brackets search'); FindUtils.setNodeSearchDisabled(true); searchDeferred.fail(); clearSearch(); return; } searchModel.results = rcvd_object.results; searchModel.numMatches = rcvd_object.numMatches; searchModel.numFiles = rcvd_object.numFiles; searchModel.exceedsMaximum = rcvd_object.exceedsMaximum; searchModel.allResultsAvailable = rcvd_object.allResultsAvailable; searchDeferred.resolve(); }) .fail(function () { FindUtils.notifyNodeSearchFinished(); console.log('node fails'); FindUtils.setNodeSearchDisabled(true); clearSearch(); searchDeferred.reject(); }); return searchDeferred.promise(); } else { return ZERO_FILES_TO_SEARCH; } }) .then(function (zeroFilesToken) { exports._searchDone = true; // for unit tests PerfUtils.addMeasurement(perfTimer); if (zeroFilesToken === ZERO_FILES_TO_SEARCH) { return zeroFilesToken; } else { return searchModel.results; } }, function (err) { console.log("find in files failed: ", err); PerfUtils.finalizeMeasurement(perfTimer); // In jQuery promises, returning the error here propagates the rejection, // unlike in Promises/A, where we would need to re-throw it to do so. return err; }); } /** * @private * Clears any previous search information, removing update listeners and clearing the model. * @param {?Entry} scope Project file/subfolder to search within; else searches whole project. */ clearSearch = function () { findOrReplaceInProgress = false; searchModel.clear(); }; /** * Does a search in the given scope with the given filter. Used when you want to start a search * programmatically. * @param {{query: string, caseSensitive: boolean, isRegexp: boolean}} queryInfo Query info object * @param {?Entry} scope Project file/subfolder to search within; else searches whole project. * @param {?string} filter A "compiled" filter as returned by FileFilters.compile(), or null for no filter * @param {?string} replaceText If this is a replacement, the text to replace matches with. This is just * stored in the model for later use - the replacement is not actually performed right now. * @param {?$.Promise} candidateFilesPromise If specified, a promise that should resolve with the same set of files that * getCandidateFiles(scope) would return. * @return {$.Promise} A promise that's resolved with the search results or rejected when the find competes. */ function doSearchInScope(queryInfo, scope, filter, replaceText, candidateFilesPromise) { clearSearch(); searchModel.scope = scope; if (replaceText !== undefined) { searchModel.isReplace = true; searchModel.replaceText = replaceText; } candidateFilesPromise = candidateFilesPromise || getCandidateFiles(scope); return _doSearch(queryInfo, candidateFilesPromise, filter); } /** * Given a set of search results, replaces them with the given replaceText, either on disk or in memory. * @param {Object.<fullPath: string, {matches: Array.<{start: {line:number,ch:number}, end: {line:number,ch:number}, startOffset: number, endOffset: number, line: string}>, collapsed: boolean}>} results * The list of results to replace, as returned from _doSearch.. * @param {string} replaceText The text to replace each result with. * @param {?Object} options An options object: * forceFilesOpen: boolean - Whether to open all files in editors and do replacements there rather than doing the * replacements on disk. Note that even if this is false, files that are already open in editors will have replacements * done in memory. * isRegexp: boolean - Whether the original query was a regexp. If true, $-substitution is performed on the replaceText. * @return {$.Promise} A promise that's resolved when the replacement is finished or rejected with an array of errors * if there were one or more errors. Each individual item in the array will be a {item: string, error: string} object, * where item is the full path to the file that could not be updated, and error is either a FileSystem error or one * of the `FindInFiles.ERROR_*` constants. */ function doReplace(results, replaceText, options) { return FindUtils.performReplacements(results, replaceText, options).always(function () { // For UI integration testing only exports._replaceDone = true; }); } /** * @private * Flags that the search scope has changed, so that the file list for the following search is recomputed */ var _searchScopeChanged = function () { searchScopeChanged = true; }; /** * Notify node that the results should be collapsed */ function _searchcollapseResults() { if (FindUtils.isNodeSearchDisabled()) { return; } searchDomain.exec("collapseResults", FindUtils.isCollapsedResults()); } /** * Inform node that the list of files has changed. * @param {array} fileList The list of files that changed. */ function filesChanged(fileList) { if (FindUtils.isNodeSearchDisabled() || fileList.length === 0) { return; } var updateObject = { "fileList": fileList }; if (searchModel.filter) { updateObject.filesInSearchScope = FileFilters.getPathsMatchingFilter(searchModel.filter, fileList); _searchScopeChanged(); } searchDomain.exec("filesChanged", updateObject); } /** * Inform node that the list of files have been removed. * @param {array} fileList The list of files that was removed. */ function filesRemoved(fileList) { if (FindUtils.isNodeSearchDisabled()) { return; } var updateObject = { "fileList": fileList }; if (searchModel.filter) { updateObject.filesInSearchScope = FileFilters.getPathsMatchingFilter(searchModel.filter, fileList); _searchScopeChanged(); } searchDomain.exec("filesRemoved", updateObject); } /** * @private * Moves the search results from the previous path to the new one and updates the results list, if required * @param {$.Event} event * @param {string} oldName * @param {string} newName */ _fileNameChangeHandler = function (event, oldName, newName) { var resultsChanged = false; // Update the search results _.forEach(searchModel.results, function (item, fullPath) { if (fullPath.indexOf(oldName) === 0) { // node search : inform node about the rename filesRemoved([fullPath]); filesChanged([fullPath.replace(oldName, newName)]); if (findOrReplaceInProgress) { searchModel.removeResults(fullPath); searchModel.setResults(fullPath.replace(oldName, newName), item); resultsChanged = true; } } }); if (resultsChanged) { searchModel.fireChanged(); } }; /** * @private * Updates search results in response to FileSystem "change" event * @param {$.Event} event * @param {FileSystemEntry} entry * @param {Array.<FileSystemEntry>=} added Added children * @param {Array.<FileSystemEntry>=} removed Removed children */ _fileSystemChangeHandler = function (event, entry, added, removed) { var resultsChanged = false; /* * Remove existing search results that match the given entry's path * @param {(File|Directory)} entry */ function _removeSearchResultsForEntry(entry) { Object.keys(searchModel.results).forEach(function (fullPath) { if (fullPath === entry.fullPath || (entry.isDirectory && fullPath.indexOf(entry.fullPath) === 0)) { // node search : inform node that the file is removed filesRemoved([fullPath]); if (findOrReplaceInProgress) { searchModel.removeResults(fullPath); resultsChanged = true; } } }); } /* * Add new search results for this entry and all of its children * @param {(File|Directory)} entry * @return {jQuery.Promise} Resolves when the results have been added */ function _addSearchResultsForEntry(entry) { var addedFiles = [], addedFilePaths = [], deferred = new $.Deferred(); // gather up added files var visitor = function (child) { // Replicate filtering that getAllFiles() does if (ProjectManager.shouldShow(child)) { if (child.isFile && _isReadableText(child.name)) { // Re-check the filtering that the initial search applied if (_inSearchScope(child)) { addedFiles.push(child); addedFilePaths.push(child.fullPath); } } return true; } return false; }; entry.visit(visitor, function (err) { if (err) { deferred.reject(err); return; } //node Search : inform node about the file changes filesChanged(addedFilePaths); if (findOrReplaceInProgress) { // find additional matches in all added files Async.doInParallel(addedFiles, function (file) { return _doSearchInOneFile(file) .done(function (foundMatches) { resultsChanged = resultsChanged || foundMatches; }); }).always(deferred.resolve); } }); return deferred.promise(); } if (!entry) { // TODO: re-execute the search completely? return; } var addPromise; if (entry.isDirectory) { if (!added || !removed || (added.length === 0 && removed.length === 0)) { // If the added or removed sets are null, must redo the search for the entire subtree - we // don't know which child files/folders may have been added or removed. _removeSearchResultsForEntry(entry); var deferred = $.Deferred(); addPromise = deferred.promise(); entry.getContents(function (err, entries) { Async.doInParallel(entries, _addSearchResultsForEntry).always(deferred.resolve); }); } else { removed.forEach(_removeSearchResultsForEntry); addPromise = Async.doInParallel(added, _addSearchResultsForEntry); } } else { // entry.isFile _removeSearchResultsForEntry(entry); addPromise = _addSearchResultsForEntry(entry); } addPromise.always(function () { // Restore the results if needed if (resultsChanged) { searchModel.fireChanged(); } }); }; /** * On project change, inform node about the new list of files that needs to be crawled. * Instant search is also disabled for the time being till the crawl is complete in node. */ var _initCache = function () { function filter(file) { return _subtreeFilter(file, null) && _isReadableText(file.fullPath); } FindUtils.setInstantSearchDisabled(true); //we always listen for filesytem changes. _addListeners(); if (!PreferencesManager.get("findInFiles.nodeSearch")) { return; } ProjectManager.getAllFiles(filter, true, true) .done(function (fileListResult) { var files = fileListResult, filter = FileFilters.getActiveFilter(); if (filter && filter.patterns.length > 0) { files = FileFilters.filterFileList(FileFilters.compile(filter.patterns), files); } files = files.filter(function (entry) { return entry.isFile && _isReadableText(entry.fullPath); }).map(function (entry) { return entry.fullPath; }); FindUtils.notifyIndexingStarted(); searchDomain.exec("initCache", files); }); _searchScopeChanged(); }; /** * Gets the next page of search recults to append to the result set. * @return {object} A promise that's resolved with the search results or rejected when the find competes. */ function getNextPageofSearchResults() { var searchDeferred = $.Deferred(); if (searchModel.allResultsAvailable) { return searchDeferred.resolve().promise(); } _updateChangedDocs(); FindUtils.notifyNodeSearchStarted(); searchDomain.exec("nextPage") .done(function (rcvd_object) { FindUtils.notifyNodeSearchFinished(); if (searchModel.results) { var resultEntry; for (resultEntry in rcvd_object.results ) { if (rcvd_object.results.hasOwnProperty(resultEntry)) { searchModel.results[resultEntry.toString()] = rcvd_object.results[resultEntry]; } } } else { searchModel.results = rcvd_object.results; } searchModel.fireChanged(); searchDeferred.resolve(); }) .fail(function () { FindUtils.notifyNodeSearchFinished(); console.log('node fails'); FindUtils.setNodeSearchDisabled(true); searchDeferred.reject(); }); return searchDeferred.promise(); } function getAllSearchResults() { var searchDeferred = $.Deferred(); if (searchModel.allResultsAvailable) { return searchDeferred.resolve().promise(); } _updateChangedDocs(); FindUtils.notifyNodeSearchStarted(); searchDomain.exec("getAllResults") .done(function (rcvd_object) { FindUtils.notifyNodeSearchFinished(); searchModel.results = rcvd_object.results; searchModel.numMatches = rcvd_object.numMatches; searchModel.numFiles = rcvd_object.numFiles; searchModel.allResultsAvailable = true; searchModel.fireChanged(); searchDeferred.resolve(); }) .fail(function () { FindUtils.notifyNodeSearchFinished(); console.log('node fails'); FindUtils.setNodeSearchDisabled(true); searchDeferred.reject(); }); return searchDeferred.promise(); } ProjectManager.on("projectOpen", _initCache); FindUtils.on(FindUtils.SEARCH_FILE_FILTERS_CHANGED, _searchScopeChanged); FindUtils.on(FindUtils.SEARCH_SCOPE_CHANGED, _searchScopeChanged); FindUtils.on(FindUtils.SEARCH_COLLAPSE_RESULTS, _searchcollapseResults); searchDomain.on("crawlComplete", nodeFileCacheComplete); // Public exports exports.searchModel = searchModel; exports.doSearchInScope = doSearchInScope; exports.doReplace = doReplace; exports.getCandidateFiles = getCandidateFiles; exports.clearSearch = clearSearch; exports.ZERO_FILES_TO_SEARCH = ZERO_FILES_TO_SEARCH; exports.getNextPageofSearchResults = getNextPageofSearchResults; exports.getAllSearchResults = getAllSearchResults; // For unit tests only exports._documentChangeHandler = _documentChangeHandler; exports._fileNameChangeHandler = _fileNameChangeHandler; exports._fileSystemChangeHandler = _fileSystemChangeHandler; });