Modules (180)

FindInFiles

Description

The core search functionality used by Find in Files and single-file Replace Batch.

Dependencies

Variables

MAX_DISPLAY_LENGTH Constant

Maximum length of text displayed in search results panel

    var MAX_DISPLAY_LENGTH = 200;
Public API

ZERO_FILES_TO_SEARCH Constant

Token used to indicate a specific reason for zero search results

Type
@type !Object
    var ZERO_FILES_TO_SEARCH = {};
Private Public API

_documentChangeHandler

Forward declarations

Private
    var _documentChangeHandler, _fileSystemChangeHandler, _fileNameChangeHandler, clearSearch;
Public API

searchModel

The search query and results model.

Type
SearchModel
    var searchModel = new SearchModel();

Functions

Private

_addListeners

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

_addSearchResultsForEntry

Add new search results for this entry and all of its children

entry (File,Directory)
Returns: 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();
            }
        });
    };
Private

_doSearch

queryInfo {query: string,caseSensitive: boolean,isRegexp: boolean}
Query info object
candidateFilesPromise non-nullable $.Promise
Promise from getCandidateFiles(), which was called earlier
filter nullable string
A "compiled" filter as returned by FileFilters.compile(), or null for no filter
Returns: ?$.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;
                            });
Private

_doSearchInOneFile

file non-nullable File
Returns: $.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

_getSearchMatches

contents string
queryExpr RegExp
Returns: !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

_inSearchScope

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.

file non-nullable File
Returns: 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

_initCache

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();
    };
Private

_isReadableText

Filters out files that are known binary types.

fullPath string
Returns: boolean
True if the file's contents can be read as text
    function _isReadableText(fullPath) {
        return !LanguageManager.getLanguageForPath(fullPath).isBinary();
    }
Private

_removeListeners

Remove the listeners that were tracking potential search result changes

    function _removeListeners() {
        DocumentModule.off("documentChange", _documentChangeHandler);
        FileSystem.off("change", _fileSystemChangeHandler);
        DocumentManager.off("fileNameChange", _fileNameChangeHandler);
    }
Private

_removeSearchResultsForEntry

Remove existing search results that match the given entry's path

entry (File,Directory)
        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;
                    }
                }
            });
        }
Private

_searchScopeChanged

    var _searchScopeChanged = function () {
        searchScopeChanged = true;
    };
Private

_searchcollapseResults

Notify node that the results should be collapsed

    function _searchcollapseResults() {
        if (FindUtils.isNodeSearchDisabled()) {
            return;
        }
        searchDomain.exec("collapseResults", FindUtils.isCollapsedResults());
    }
Private

_subtreeFilter

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.

file non-nullable File
scope nullable FileSystemEntry
Search scope, or null if whole project
Returns: 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;
    }
Private

_updateChangedDocs

    function _updateChangedDocs() {
        var key = null;
        for (key in changedFileList) {
            if (changedFileList.hasOwnProperty(key)) {
                _updateDocumentInNode(key);
            }
        }
    }
Private

_updateDocumentInNode

docPath string
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

_updateResults

doc Document
The Document that changed, should be the current one
changeList Array.<{from: {line:number,ch:number},to: {line:number,ch:number},text: !Array.<string>}>
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);
        }
    }
Public API

doReplace

Given a set of search results, replaces them with the given replaceText, either on disk or in memory.

results Object.<fullPath: string,{matches: Array.<{start: {line:number,ch:number},end: {line:number,ch:number},startOffset: number,endOffset: number,line: string}>,collapsed: boolean}>
The list of results to replace, as returned from _doSearch..
replaceText string
The text to replace each result with.
options nullable Object
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.
Returns: $.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;
        });
    }
Public API

doSearchInScope

Does a search in the given scope with the given filter. Used when you want to start a search programmatically.

queryInfo {query: string,caseSensitive: boolean,isRegexp: boolean}
Query info object
scope nullable Entry
Project file/subfolder to search within; else searches whole project.
filter nullable string
A "compiled" filter as returned by FileFilters.compile(), or null for no filter
replaceText nullable string
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.
candidateFilesPromise nullable $.Promise
If specified, a promise that should resolve with the same set of files that getCandidateFiles(scope) would return.
Returns: $.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);
    }

filesChanged

Inform node that the list of files has changed.

fileList array
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);
    }

filesRemoved

Inform node that the list of files have been removed.

fileList array
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);
    }
Public API

getCandidateFiles

Finds all candidate files to search in the given scope's subtree that are not binary content. Does NOT apply the current filter yet.

scope nullable FileSystemEntry
Search scope, or null if whole project
Returns: $.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);
        }
    }
Public API

getNextPageofSearchResults

Gets the next page of search recults to append to the result set.

Returns: 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;
});