UI and controller logic for find/replace across multiple files within the project.
FUTURE:
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();
}
}
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
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;
});
Once the indexing has finished, clear the indexing spinner
function _searchIndexingFinished() {
if (_findBar) {
_findBar.hideIndexingSpinner();
}
}
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();
}
}
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());
}
}
function _showFindBarForSubtree() {
FindUtils.notifySearchScopeChanged();
var selectedEntry = ProjectManager.getSelectedItem();
_showFindBar(selectedEntry);
}
function _showReplaceBar() {
FindUtils.notifySearchScopeChanged();
_showFindBar(null, true);
}
function _showReplaceBarForSubtree() {
FindUtils.notifySearchScopeChanged();
var selectedEntry = ProjectManager.getSelectedItem();
_showFindBar(selectedEntry, true);
}
Does a search in the given scope with the given filter. Replace the result list once the search is complete.
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();
});
}
Does a search in the given scope with the given filter. Shows the result list once the search is complete.
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();
});
}