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

Commit

Permalink
Merge pull request #4686 from zaggino/replace_all
Browse files Browse the repository at this point in the history
Added 'All' button to FindReplace functionality.
  • Loading branch information
TomMalbran committed Aug 17, 2013
2 parents c4601cc + 3509df9 commit a9d2a17
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 6 deletions.
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);
});
});
});
});

});

0 comments on commit a9d2a17

Please sign in to comment.