Modules (181)

FindInFilesUI

Description

UI and controller logic for find/replace across multiple files within the project.

FUTURE:

  • Handle matches that span multiple lines

Dependencies

Variables

MAX_IN_MEMORY Constant

Type
Maximum number of files to do replacements in-memory instead of on disk.
    var MAX_IN_MEMORY = 20;
Private

_findBar

Type
FindBar
Private
    var _findBar = null;
Private

_finishReplaceBatch

Type
Function
Private
    var _finishReplaceBatch;
Private

_resultsView

Type
SearchResultsView
Private
    var _resultsView = null;

Functions

Private Public API

_closeFindBar

    function _closeFindBar() {
        if (_findBar) {
            _findBar.close();
        }
    }
Private

_defferedSearch

Issues a search if find bar is visible and is multi file search and not instant search

    function _defferedSearch() {
        if (_findBar && _findBar._options.multifile && !_findBar._options.replace) {
            _findBar.redoInstantSearch();
        }
    }
Private

_finishReplaceBatch

model SearchModel
The model for the search associated with ths replace.
    function _finishReplaceBatch(model) {
        var replaceText = model.replaceText;
        if (replaceText === null) {
            return;
        }

        // Clone the search results so that they don't get updated in the middle of the replacement.
        var resultsClone = _.cloneDeep(model.results),
            replacedFiles = Object.keys(resultsClone).filter(function (path) {
                return FindUtils.hasCheckedMatches(resultsClone[path]);
            }),
            isRegexp = model.queryInfo.isRegexp;

        function processReplace(forceFilesOpen) {
            StatusBar.showBusyIndicator(true);
            FindInFiles.doReplace(resultsClone, replaceText, { forceFilesOpen: forceFilesOpen, isRegexp: isRegexp })
                .fail(function (errors) {
                    var message = Strings.REPLACE_IN_FILES_ERRORS + FileUtils.makeDialogFileList(
                            errors.map(function (errorInfo) {
                                return ProjectManager.makeProjectRelativeIfPossible(errorInfo.item);
                            })
                        );

                    Dialogs.showModalDialog(
                        DefaultDialogs.DIALOG_ID_ERROR,
                        Strings.REPLACE_IN_FILES_ERRORS_TITLE,
                        message,
                        [
                            {
                                className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
                                id        : Dialogs.DIALOG_BTN_OK,
                                text      : Strings.BUTTON_REPLACE_WITHOUT_UNDO
                            }
                        ]
                    );
                })
                .always(function () {
                    StatusBar.hideBusyIndicator();
                });
        }

        if (replacedFiles.length <= MAX_IN_MEMORY) {
            // Just do the replacements in memory.
            _resultsView.close();
            processReplace(true);
        } else {
            Dialogs.showModalDialog(
                DefaultDialogs.DIALOG_ID_INFO,
                Strings.REPLACE_WITHOUT_UNDO_WARNING_TITLE,
                StringUtils.format(Strings.REPLACE_WITHOUT_UNDO_WARNING, MAX_IN_MEMORY),
                [
                    {
                        className : Dialogs.DIALOG_BTN_CLASS_NORMAL,
                        id        : Dialogs.DIALOG_BTN_CANCEL,
                        text      : Strings.CANCEL
                    },
                    {
                        className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
                        id        : Dialogs.DIALOG_BTN_OK,
                        text      : Strings.BUTTON_REPLACE_WITHOUT_UNDO
                    }
                ]
            )
                .done(function (id) {
                    if (id === Dialogs.DIALOG_BTN_OK) {
                        _resultsView.close();
                        processReplace(false);
                    }
                });
        }
    }

    // Command handlers
Private

_searchIfRequired

Schedules a search on search scope/filter changes. Have to schedule as when we listen to this event, the file filters might not have been updated yet.

    function _searchIfRequired() {
        if (!FindUtils.isInstantSearchDisabled() && _findBar && _findBar._options.multifile && !_findBar._options.replace) {
            setTimeout(_defferedSearch, 100);
        }
    }

    // Initialize items dependent on HTML DOM
    AppInit.htmlReady(function () {
        var model = FindInFiles.searchModel;
        _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results");
        _resultsView
            .on("replaceBatch", function () {
                _finishReplaceBatch(model);
            })
            .on("close", function () {
                FindInFiles.clearSearch();
            })
            .on("getNextPage", function () {
                FindInFiles.getNextPageofSearchResults().done(function () {
                    if (FindInFiles.searchModel.hasResults()) {
                        _resultsView.showNextPage();
                    }
                });
            })
            .on("getLastPage", function () {
                FindInFiles.getAllSearchResults().done(function () {
                    if (FindInFiles.searchModel.hasResults()) {
                        _resultsView.showLastPage();
                    }
                });
            });
    });

    // Initialize: register listeners
    ProjectManager.on("beforeProjectClose", function () { _resultsView.close(); });

    // Initialize: command handlers
    CommandManager.register(Strings.CMD_FIND_IN_FILES,       Commands.CMD_FIND_IN_FILES,       _showFindBar);
    CommandManager.register(Strings.CMD_FIND_IN_SUBTREE,     Commands.CMD_FIND_IN_SUBTREE,     _showFindBarForSubtree);

    CommandManager.register(Strings.CMD_REPLACE_IN_FILES,    Commands.CMD_REPLACE_IN_FILES,    _showReplaceBar);
    CommandManager.register(Strings.CMD_REPLACE_IN_SUBTREE,  Commands.CMD_REPLACE_IN_SUBTREE,  _showReplaceBarForSubtree);

    FindUtils.on(FindUtils.SEARCH_INDEXING_STARTED, _searchIndexingStarted);
    FindUtils.on(FindUtils.SEARCH_INDEXING_FINISHED, _searchIndexingFinished);
    FindUtils.on(FindUtils.SEARCH_FILE_FILTERS_CHANGED, _searchIfRequired);
    FindUtils.on(FindUtils.SEARCH_SCOPE_CHANGED, _searchIfRequired);

    // Public exports
    exports.searchAndShowResults = searchAndShowResults;
    exports.searchAndReplaceResults = searchAndReplaceResults;

    // For unit testing
    exports._showFindBar  = _showFindBar;
    exports._closeFindBar = _closeFindBar;
});
Private

_searchIndexingFinished

Once the indexing has finished, clear the indexing spinner

    function _searchIndexingFinished() {
        if (_findBar) {
            _findBar.hideIndexingSpinner();
        }
    }
Private

_searchIndexingStarted

When the search indexing is started, we need to show the indexing status on the find bar if present.

    function _searchIndexingStarted() {
        if (_findBar && _findBar._options.multifile && FindUtils.isIndexingInProgress()) {
            _findBar.showIndexingSpinner();
        }
    }
Private Public API

_showFindBar

scope nullable Entry
Project file/subfolder to search within; else searches whole project.
showReplace optional boolean
If true, show the Replace controls.
    function _showFindBar(scope, showReplace) {
        FindUtils.notifySearchScopeChanged();
        // If the scope is a file with a custom viewer, then we
        // don't show find in files dialog.
        if (scope && !EditorManager.canOpenPath(scope.fullPath)) {
            return;
        }

        if (scope instanceof InMemoryFile) {
            CommandManager.execute(Commands.FILE_OPEN, { fullPath: scope.fullPath }).done(function () {
                CommandManager.execute(Commands.CMD_FIND);
            });
            return;
        }

        // Get initial query/replace text
        var currentEditor = EditorManager.getActiveEditor(),
            initialQuery = FindBar.getInitialQuery(_findBar, currentEditor);

        // Close our previous find bar, if any. (The open() of the new _findBar will
        // take care of closing any other find bar instances.)
        if (_findBar) {
            _findBar.close();
        }

        _findBar = new FindBar({
            multifile: true,
            replace: showReplace,
            initialQuery: initialQuery.query,
            initialReplaceText: initialQuery.replaceText,
            queryPlaceholder: Strings.FIND_QUERY_PLACEHOLDER,
            scopeLabel: FindUtils.labelForScope(scope)
        });
        _findBar.open();

        // TODO Should push this state into ModalBar (via a FindBar API) instead of installing a callback like this.
        // Custom closing behavior: if in the middle of executing search, blur shouldn't close ModalBar yet. And
        // don't close bar when opening Edit Filter dialog either.
        _findBar._modalBar.isLockedOpen = function () {
            // TODO: should have state for whether the search is executing instead of looking at find bar state
            // TODO: should have API on filterPicker to figure out if dialog is open
            return !_findBar.isEnabled() || $(".modal.instance .exclusions-editor").length > 0;
        };

        var candidateFilesPromise = FindInFiles.getCandidateFiles(scope),  // used for eventual search, and in exclusions editor UI
            filterPicker;

        function handleQueryChange() {
            // Check the query expression on every input event. This way the user is alerted
            // to any RegEx syntax errors immediately.
            var queryInfo = _findBar.getQueryInfo(),
                queryResult = FindUtils.parseQueryInfo(queryInfo);

            // Enable the replace button appropriately.
            _findBar.enableReplace(queryResult.valid);

            if (queryResult.valid || queryResult.empty) {
                _findBar.showNoResults(false);
                _findBar.showError(null);
            } else {
                _findBar.showNoResults(true, false);
                _findBar.showError(queryResult.error);
            }
        }

        function startSearch(replaceText) {
            var queryInfo = _findBar.getQueryInfo(),
                disableFindBar = FindUtils.isNodeSearchDisabled() || (replaceText ? true : false);
            if (queryInfo && queryInfo.query) {
                _findBar.enable(!disableFindBar);
                StatusBar.showBusyIndicator(disableFindBar);
                if (queryInfo.isRegexp) {
                    HealthLogger.searchDone(HealthLogger.SEARCH_REGEXP);
                }
                if (queryInfo.isCaseSensitive) {
                    HealthLogger.searchDone(HealthLogger.SEARCH_CASE_SENSITIVE);
                }

                var filter;
                if (filterPicker) {
                    filter = FileFilters.commitPicker(filterPicker);
                } else {
                    // Single-file scope: don't use any file filters
                    filter = null;
                }
                searchAndShowResults(queryInfo, scope, filter, replaceText, candidateFilesPromise);
            }
            return null;
        }

        function startReplace() {
            startSearch(_findBar.getReplaceText());
        }

        _findBar
            .on("doFind.FindInFiles", function () {
                // Subtle issue: we can't just pass startSearch directly as the handler, because
                // we don't want it to get the event object as an argument.
                startSearch();
            })
            .on("queryChange.FindInFiles", handleQueryChange)
            .on("close.FindInFiles", function (e) {
                _findBar.off(".FindInFiles");
                _findBar = null;
            });

        if (showReplace) {
            // We shouldn't get a "doReplace" in this case, since the Replace button
            // is hidden when we set options.multifile.
            _findBar.on("doReplaceBatch.FindInFiles", startReplace);
        }

        var oldModalBarHeight = _findBar._modalBar.height();

        // Show file-exclusion UI *unless* search scope is just a single file
        if (!scope || scope.isDirectory) {
            var exclusionsContext = {
                label: FindUtils.labelForScope(scope),
                promise: candidateFilesPromise
            };

            filterPicker = FileFilters.createFilterPicker(exclusionsContext);
            // TODO: include in FindBar? (and disable it when FindBar is disabled)
            _findBar._modalBar.getRoot().find(".scope-group").append(filterPicker);
        }

        handleQueryChange();

        // Appending FilterPicker and query text can change height of modal bar, so resize editor.
        // Preserve scroll position of the current full editor across the editor refresh, adjusting
        // for the height of the modal bar so the code doesn't appear to shift if possible.
        var fullEditor = EditorManager.getCurrentFullEditor(),
            scrollPos;
        if (fullEditor) {
            scrollPos = fullEditor.getScrollPos();
            scrollPos.y -= oldModalBarHeight;   // modalbar already showing, adjust for old height
        }
        WorkspaceManager.recomputeLayout();
        if (fullEditor) {
            fullEditor._codeMirror.scrollTo(scrollPos.x, scrollPos.y + _findBar._modalBar.height());
        }
    }
Private

_showFindBarForSubtree

    function _showFindBarForSubtree() {
        FindUtils.notifySearchScopeChanged();
        var selectedEntry = ProjectManager.getSelectedItem();
        _showFindBar(selectedEntry);
    }
Private

_showReplaceBar

    function _showReplaceBar() {
        FindUtils.notifySearchScopeChanged();
        _showFindBar(null, true);
    }
Private

_showReplaceBarForSubtree

    function _showReplaceBarForSubtree() {
        FindUtils.notifySearchScopeChanged();
        var selectedEntry = ProjectManager.getSelectedItem();
        _showFindBar(selectedEntry, true);
    }
Public API

searchAndReplaceResults

Does a search in the given scope with the given filter. Replace the result list once the search is complete.

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.
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 searchAndReplaceResults(queryInfo, scope, filter, replaceText, candidateFilesPromise) {
        return FindInFiles.doSearchInScope(queryInfo, scope, filter, replaceText, candidateFilesPromise)
            .done(function (zeroFilesToken) {
                // Done searching all files: replace all
                if (FindInFiles.searchModel.hasResults()) {
                    _finishReplaceBatch(FindInFiles.searchModel);

                    if (_findBar) {
                        _findBar.enable(true);
                        _findBar.focus();
                    }

                }
                StatusBar.hideBusyIndicator();
            })
            .fail(function (err) {
                console.log("replace all failed: ", err);
                StatusBar.hideBusyIndicator();
            });
    }
Public API

searchAndShowResults

Does a search in the given scope with the given filter. Shows the result list once the search is complete.

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.
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 searchAndShowResults(queryInfo, scope, filter, replaceText, candidateFilesPromise) {
        return FindInFiles.doSearchInScope(queryInfo, scope, filter, replaceText, candidateFilesPromise)
            .done(function (zeroFilesToken) {
                // Done searching all files: show results
                if (FindInFiles.searchModel.hasResults()) {
                    _resultsView.open();

                    if (_findBar) {
                        _findBar.enable(true);
                        _findBar.focus();
                    }

                } else {
                    _resultsView.close();

                    if (_findBar) {
                        var showMessage = false;
                        _findBar.enable(true);
                        if (zeroFilesToken === FindInFiles.ZERO_FILES_TO_SEARCH) {
                            _findBar.showError(StringUtils.format(Strings.FIND_IN_FILES_ZERO_FILES, FindUtils.labelForScope(FindInFiles.searchModel.scope)), true);
                        } else {
                            showMessage = true;
                        }
                        _findBar.showNoResults(true, showMessage);
                    }
                }

                StatusBar.hideBusyIndicator();
            })
            .fail(function (err) {
                console.log("find in files failed: ", err);
                StatusBar.hideBusyIndicator();
            });
    }