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

Added 'All' button to FindReplace functionality. #4686

Merged
merged 1 commit into from
Aug 17, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/htmlContent/search-replace-panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div id="replace-all-results" class="bottom-panel vert-resizable top-resizer no-focus">
<div class="toolbar simple-toolbar-layout">
<input type="checkbox" class="check-all" />
<div class="title"></div>
<button class="replace-checked">{{BUTTON_REPLACE}}</button>
<a href="#" class="close">&times;</a>
</div>
<div class="table-container resizable-content"></div>
</div>
11 changes: 11 additions & 0 deletions src/htmlContent/search-replace-results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<table class="table table-striped table-condensed row-highlight">
<tbody>
{{#searchResults}}
<tr class="replace-row" data-match="{{index}}">
<td class="checkbox-column"><input type="checkbox" class="check-one" checked="true" /></td>
<td class="line-column">{{line}}</td>
<td>{{pre}}<span class="highlight">{{highlight}}</span>{{post}}</td>
</tr>
{{/searchResults}}
</tbody>
</table>
6 changes: 5 additions & 1 deletion src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ define({
"WITH" : "With",
"BUTTON_YES" : "Yes",
"BUTTON_NO" : "No",
"BUTTON_ALL" : "All",
"BUTTON_STOP" : "Stop",
"BUTTON_REPLACE" : "Replace",

"OPEN_FILE" : "Open File",
"SAVE_FILE_AS" : "Save File",
Expand All @@ -124,7 +126,9 @@ define({
"RELEASE_NOTES" : "Release Notes",
"NO_UPDATE_TITLE" : "You're up to date!",
"NO_UPDATE_MESSAGE" : "You are running the latest version of {APP_NAME}.",


"FIND_REPLACE_TITLE" : "Replace \"{0}\" with \"{1}\" &mdash; {3} {2} matches",

"FIND_IN_FILES_TITLE" : "\"{4}\" found {5} &mdash; {0} {1} in {2} {3}",
"FIND_IN_FILES_SCOPED" : "in <span class='dialog-filename'>{0}</span>",
"FIND_IN_FILES_NO_SCOPE" : "in project",
Expand Down
158 changes: 153 additions & 5 deletions src/search/FindReplace.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, doReplace */
/*global define, $, doReplace, Mustache */
/*unittests: FindReplace*/


Expand All @@ -35,17 +35,38 @@ define(function (require, exports, module) {
"use strict";

var CommandManager = require("command/CommandManager"),
AppInit = require("utils/AppInit"),
Commands = require("command/Commands"),
DocumentManager = require("document/DocumentManager"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
Editor = require("editor/Editor"),
EditorManager = require("editor/EditorManager"),
ModalBar = require("widgets/ModalBar").ModalBar,
PanelManager = require("view/PanelManager"),
Resizer = require("utils/Resizer"),
StatusBar = require("widgets/StatusBar"),
ViewUtils = require("utils/ViewUtils");

var searchReplacePanelTemplate = require("text!htmlContent/search-replace-panel.html"),
searchReplaceResultsTemplate = require("text!htmlContent/search-replace-results.html");

/** @cost Constant used to define the maximum results to show */
var FIND_REPLACE_MAX = 300;

/** @type {!Panel} Panel that shows results of replaceAll action */
var replaceAllPanel = null;

/** @type {?Document} Instance of the currently opened document when replaceAllPanel is visible */
var currentDocument = null;

/** @type {$.Element} jQuery elements used in the replaceAll panel */
var $replaceAllContainer,
$replaceAllTable;

var modalBar,
isFindFirst = false;

function SearchState() {
this.posFrom = this.posTo = this.query = null;
this.marked = [];
Expand Down Expand Up @@ -281,6 +302,110 @@ define(function (require, exports, module) {
}
}

/**
* @private
* Closes a panel with search-replace results.
* Main purpose is to make sure that events are correctly detached from current document.
*/
function _closeReplaceAllPanel() {
if (replaceAllPanel !== null && replaceAllPanel.isVisible()) {
replaceAllPanel.hide();
}
$(currentDocument).off("change.replaceAll");
}

/**
* @private
* Shows a panel with search results and offers to replace them,
* user can use checkboxes to select which results he wishes to replace.
* @param {Editor} editor - Currently active editor that was used to invoke this action.
* @param {string|RegExp} replaceWhat - Query that will be passed into CodeMirror Cursor to search for results.
* @param {string} replaceWith - String that should be used to replace chosen results.
* @param {?function} replaceFunction - If replaceWhat is a RegExp, than this function is used to replace matches in that RegExp.
*/
function _showReplaceAllPanel(editor, replaceWhat, replaceWith, replaceFunction) {
var results = [],
cm = editor._codeMirror,
cursor = getSearchCursor(cm, replaceWhat),
from,
to,
line,
multiLine;

// Collect all results from document
while (cursor.findNext()) {
from = cursor.from();
to = cursor.to();
line = editor.document.getLine(from.line);
multiLine = from.line !== to.line;

results.push({
index: results.length, // add indexes to array
from: from,
to: to,
line: StringUtils.format(Strings.FIND_IN_FILES_LINE, from.line + 1),
pre: line.slice(0, from.ch),
highlight: line.slice(from.ch, multiLine ? undefined : to.ch),
post: multiLine ? "\u2026" : line.slice(to.ch)
});

if (results.length >= FIND_REPLACE_MAX) {
break;
}
}

// This text contains some formatting, so all the strings are assumed to be already escaped
var summary = StringUtils.format(
Strings.FIND_REPLACE_TITLE,
replaceWhat.toString(),
replaceWith.toString(),
results.length,
results.length >= FIND_REPLACE_MAX ? Strings.FIND_IN_FILES_MORE_THAN : ""
);

// Insert the search summary
$replaceAllContainer.find(".title").html(summary);

// All checkboxes are checked by default
$replaceAllContainer.find(".check-all").prop("checked", true);

// Attach event to replace button
$replaceAllContainer.find("button.replace-checked").off().on("click", function (e) {
$replaceAllTable.find(".check-one:checked")
.closest(".replace-row")
.toArray()
.reverse()
.forEach(function (checkedRow) {
var match = results[$(checkedRow).data("match")],
rw = typeof replaceWhat === "string" ? replaceWith : replaceWith.replace(/\$(\d)/, replaceFunction);
editor.document.replaceRange(rw, match.from, match.to, "+replaceAll");
});
_closeReplaceAllPanel();
});

// Insert the search results
$replaceAllTable
.empty()
.append(Mustache.render(searchReplaceResultsTemplate, {searchResults: results}))
.off()
.on("click", ".check-one", function (e) {
e.stopPropagation();
})
.on("click", ".replace-row", function (e) {
var match = results[$(e.currentTarget).data("match")];
editor.setSelection(match.from, match.to, true);
});

// we can't safely replace after document has been modified
// this handler is only attached, when replaceAllPanel is visible
currentDocument = DocumentManager.getCurrentDocument();
$(currentDocument).on("change.replaceAll", function () {
_closeReplaceAllPanel();
});

replaceAllPanel.show();
}

var replaceQueryDialog = Strings.CMD_REPLACE +
': <input type="text" style="width: 10em"/> <div class="message"><span style="color: #888">(' +
Strings.SEARCH_REGEXP_INFO + ')</span></div><div class="error"></div>';
Expand All @@ -289,6 +414,7 @@ define(function (require, exports, module) {
var doReplaceConfirm = Strings.CMD_REPLACE +
'? <button id="replace-yes" class="btn">' + Strings.BUTTON_YES +
'</button> <button id="replace-no" class="btn">' + Strings.BUTTON_NO +
'</button> <button id="replace-all" class="btn">' + Strings.BUTTON_ALL +
'</button> <button id="replace-stop" class="btn">' + Strings.BUTTON_STOP + '</button>';

function replace(editor, all) {
Expand All @@ -298,7 +424,7 @@ define(function (require, exports, module) {
if (!query) {
return;
}

query = parseQuery(query);
createModalBar(replacementQueryDialog, true);
$(modalBar).on("closeOk", function (e, text) {
Expand Down Expand Up @@ -343,6 +469,8 @@ define(function (require, exports, module) {
doReplace(match);
} else if (e.target.id === "replace-no") {
advance();
} else if (e.target.id === "replace-all") {
_showReplaceAllPanel(editor, query, text, fnMatch);
} else if (e.target.id === "replace-stop") {
// Destroy modalBar on stop
modalBar = null;
Expand All @@ -358,13 +486,13 @@ define(function (require, exports, module) {
}
});
});

// Prepopulate the replace field with the current selection, if any
getDialogTextField()
.val(cm.getSelection())
.get(0).select();
}

function _launchFind() {
var editor = EditorManager.getActiveEditor();
if (editor) {
Expand Down Expand Up @@ -397,6 +525,26 @@ define(function (require, exports, module) {
}
}

// Initialize items dependent on HTML DOM
AppInit.htmlReady(function () {
var panelHtml = Mustache.render(searchReplacePanelTemplate, Strings);
replaceAllPanel = PanelManager.createBottomPanel("findReplace-all.panel", $(panelHtml), 100);
$replaceAllContainer = replaceAllPanel.$panel;
$replaceAllTable = $replaceAllContainer.children(".table-container");

// Attach events to the panel
replaceAllPanel.$panel
.on("click", ".close", function () {
_closeReplaceAllPanel();
})
.on("click", ".check-all", function (e) {
var isChecked = $(this).is(":checked");
replaceAllPanel.$panel.find(".check-one").prop("checked", isChecked);
});
});

$(DocumentManager).on("currentDocumentChange", _closeReplaceAllPanel);

CommandManager.register(Strings.CMD_FIND, Commands.EDIT_FIND, _launchFind);
CommandManager.register(Strings.CMD_FIND_NEXT, Commands.EDIT_FIND_NEXT, _findNext);
CommandManager.register(Strings.CMD_REPLACE, Commands.EDIT_REPLACE, _replace);
Expand Down
20 changes: 20 additions & 0 deletions src/styles/brackets.less
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,26 @@ a, img {
vertical-align: baseline;
}

/* Find-Replace All UI */

#replace-all-results {
.check-all {
margin: 0px 22px 0px 6px;
}
.title {
margin-right: 25px;
}
table {
td.checkbox-column {
width: 20px;
text-align: center;
}
td.line-column {
width: 100px;
}
}
}

/* Modal bar for Find/Quick Open */

.modal-bar.modal-bar-hide {
Expand Down
33 changes: 33 additions & 0 deletions test/spec/FindReplace-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,39 @@ define(function (require, exports, module) {
expectSelection({start: {line: 0, ch: 0}, end: {line: 0, ch: 18}});
});
});

describe("Search -> Replace", function () {
beforeEach(setupFullEditor);

it("should find and replace one string", function () {
runs(function () {
CommandManager.execute(Commands.EDIT_REPLACE);
enterSearchText("foo");
pressEnter();
});

waitsForSearchBarReopen();

runs(function () {
enterSearchText("bar");
pressEnter();
});

waitsForSearchBarReopen();

runs(function () {
expectSelection(fooExpectedMatches[0]);
expect(/foo/i.test(myEditor.getSelectedText())).toBe(true);

expect($("#replace-yes").is(":visible")).toBe(true);
$("#replace-yes").click();
$("#replace-stop").click();

myEditor.setSelection(fooExpectedMatches[0].start, fooExpectedMatches[0].end);
expect(/bar/i.test(myEditor.getSelectedText())).toBe(true);
});
});
});
});

});