kopia lustrzana https://github.com/c9/core
1026 wiersze
38 KiB
JavaScript
1026 wiersze
38 KiB
JavaScript
define(function(require, exports, module) {
|
|
"use strict";
|
|
|
|
main.consumes = [
|
|
"Plugin", "settings", "ui", "layout",
|
|
"menus", "tabManager", "commands", "tooltip", "apf"
|
|
];
|
|
main.provides = ["findreplace"];
|
|
return main;
|
|
|
|
function main(options, imports, register) {
|
|
var Plugin = imports.Plugin;
|
|
var settings = imports.settings;
|
|
var ui = imports.ui;
|
|
var menus = imports.menus;
|
|
var layout = imports.layout;
|
|
var commands = imports.commands;
|
|
var tooltip = imports.tooltip;
|
|
var tabs = imports.tabManager;
|
|
var apf = imports.apf;
|
|
|
|
var css = require("text!./findreplace.css");
|
|
var skin = require("text!./skin.xml");
|
|
var markup = require("text!./findreplace.xml");
|
|
|
|
var lib = require("plugins/c9.ide.find.replace/libsearch");
|
|
|
|
var asyncSearch = require("./async_search");
|
|
var Range = require("ace/range").Range;
|
|
|
|
/***** Initialization *****/
|
|
|
|
var plugin = new Plugin("Ajax.org", main.consumes);
|
|
var emit = plugin.getEmitter();
|
|
|
|
var libsearch = lib(settings, execFind, toggleDialog, restore, toggleOption);
|
|
|
|
var searchRow, txtFind, winSearchReplace, txtReplace;
|
|
var tooltipSearchReplace, divSearchCount;
|
|
var btnPrev, btnNext, btnReplace, btnReplaceAll, hbox, btnCollapse;
|
|
var chk = {};
|
|
|
|
var currentRange, lastSearchOptions;
|
|
var timer, startPos = {};
|
|
|
|
function toggleOption() {
|
|
var ch;
|
|
switch (this.name) {
|
|
case "regex": ch = chk.regEx; break;
|
|
case "wholeWords": ch = chk.wholeWords; break;
|
|
case "matchCase": ch = chk.matchCase; break;
|
|
}
|
|
ch.change(!ch.checked, true);
|
|
|
|
execFind();
|
|
}
|
|
|
|
var loaded = false;
|
|
function load() {
|
|
if (loaded) return false;
|
|
loaded = true;
|
|
|
|
function isSupported(editor) {
|
|
if (apf.activeElement === txtFind || apf.activeElement === txtReplace)
|
|
return true;
|
|
return editor && editor.ace;
|
|
}
|
|
|
|
function isSupportedRW(editor) {
|
|
var ace = isSupported(editor);
|
|
return ace === true || !ace ? ace : !ace.getOption("readOnly");
|
|
}
|
|
|
|
commands.addCommands({
|
|
replace: {
|
|
bindKey: { mac: "Option-Command-F", win: "Alt-Shift-F|Ctrl-H" },
|
|
hint: "search for a string inside the active document and replace it",
|
|
isAvailable: isSupportedRW,
|
|
exec: function(env, args, request) {
|
|
toggleDialog(1, true);
|
|
}
|
|
},
|
|
replaceall: {
|
|
bindKey: { mac: "", win: "" },
|
|
hint: "search for a string inside the active document and replace all",
|
|
isAvailable: isSupportedRW,
|
|
exec: function(env, args, request) {
|
|
replaceAll();
|
|
}
|
|
},
|
|
replacenext: {
|
|
isAvailable: isSupportedRW,
|
|
exec: function(env, args, request) {
|
|
replace();
|
|
}
|
|
},
|
|
replaceprevious: {
|
|
isAvailable: isSupportedRW,
|
|
exec: function(env, args, request) {
|
|
replace(true);
|
|
}
|
|
},
|
|
findAll: {
|
|
isAvailable: isSupported,
|
|
bindKey: { mac: "Ctrl-Alt-G", win: "Ctrl-Alt-K" },
|
|
exec: function(editor) {
|
|
findAgain(editor.ace, 0);
|
|
},
|
|
},
|
|
findnext: {
|
|
isAvailable: isSupported,
|
|
bindKey: { mac: "Command-G", win: "Ctrl-K" },
|
|
exec: function(editor) {
|
|
findAgain(editor.ace, 1);
|
|
},
|
|
},
|
|
findprevious: {
|
|
isAvailable: isSupported,
|
|
bindKey: { mac: "Command-Shift-G", win: "Ctrl-Shift-K" },
|
|
exec: function(editor) {
|
|
findAgain(editor.ace, -1);
|
|
},
|
|
},
|
|
find: {
|
|
hint: "open the quicksearch dialog to quickly search for a phrase",
|
|
bindKey: { mac: "Command-F", win: "Ctrl-F" },
|
|
isAvailable: isSupported,
|
|
exec: function(env, args, request) {
|
|
toggleDialog(1, false);
|
|
}
|
|
},
|
|
hidesearchreplace: {
|
|
bindKey: { mac: "ESC", win: "ESC" },
|
|
isAvailable: function(editor) {
|
|
return winSearchReplace && winSearchReplace.visible;
|
|
},
|
|
exec: function(env, args, request) {
|
|
toggleDialog(-1);
|
|
}
|
|
}
|
|
}, plugin);
|
|
|
|
menus.addItemByPath("Find/Find...", new ui.item({
|
|
command: "find"
|
|
}), 100, plugin);
|
|
menus.addItemByPath("Find/Find Next", new ui.item({
|
|
command: "findnext"
|
|
}), 200, plugin);
|
|
menus.addItemByPath("Find/Find Previous", new ui.item({
|
|
command: "findprevious"
|
|
}), 300, plugin);
|
|
menus.addItemByPath("Find/~", new ui.divider(), 400, plugin);
|
|
menus.addItemByPath("Find/Replace...", new ui.item({
|
|
command: "replace"
|
|
}), 500, plugin);
|
|
menus.addItemByPath("Find/Replace Next", new ui.item({
|
|
command: "replacenext",
|
|
}), 600, plugin);
|
|
menus.addItemByPath("Find/Replace Previous", new ui.item({
|
|
command: "replaceprevious",
|
|
}), 700, plugin);
|
|
menus.addItemByPath("Find/Replace All", new ui.item({
|
|
command: "replaceall"
|
|
}), 800, plugin);
|
|
|
|
tabs.on("focus", function(e) {
|
|
if (winSearchReplace && winSearchReplace.visible) {
|
|
if (e.tab && e.tab.editor.ace) {
|
|
winSearchReplace.enable();
|
|
execFind(false, "highlight");
|
|
}
|
|
else {
|
|
winSearchReplace.disable();
|
|
btnCollapse.enable();
|
|
updateCounter();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var drawn = false;
|
|
function draw() {
|
|
if (drawn) return;
|
|
drawn = true;
|
|
|
|
// Import CSS
|
|
ui.insertCss(css, null, plugin);
|
|
|
|
// Import Skin
|
|
ui.insertSkin({
|
|
name: "searchreplace",
|
|
data: skin,
|
|
}, plugin);
|
|
|
|
// Create UI elements
|
|
searchRow = layout.findParent(plugin);
|
|
ui.insertMarkup(null, markup, plugin);
|
|
|
|
txtFind = plugin.getElement("txtFind");
|
|
winSearchReplace = plugin.getElement("winSearchReplace");
|
|
txtReplace = plugin.getElement("txtReplace");
|
|
tooltipSearchReplace = plugin.getElement("tooltipSearchReplace");
|
|
chk.searchSelection = plugin.getElement("chkSearchSelection");
|
|
divSearchCount = plugin.getElement("divSearchCount");
|
|
hbox = plugin.getElement("hbox");
|
|
chk.regEx = plugin.getElement("chkRegEx");
|
|
chk.wrapAround = plugin.getElement("chkWrapAround");
|
|
chk.matchCase = plugin.getElement("chkMatchCase");
|
|
chk.wholeWords = plugin.getElement("chkWholeWords");
|
|
chk.preserveCase = plugin.getElement("chkPreserveCase");
|
|
btnPrev = plugin.getElement("btnPrev");
|
|
btnNext = plugin.getElement("btnNext");
|
|
btnReplace = plugin.getElement("btnReplace");
|
|
btnReplaceAll = plugin.getElement("btnReplaceAll");
|
|
btnCollapse = plugin.getElement("btnCollapse");
|
|
|
|
btnNext.on("click", function() { findNext(false); });
|
|
btnPrev.on("click", function() { findNext(true); });
|
|
btnReplace.on("click", function() { replace(); });
|
|
btnReplaceAll.on("click", function() { replaceAll(); });
|
|
btnCollapse.on("click", function() { toggleDialog(-1); });
|
|
|
|
txtFind.$ext.appendChild(divSearchCount.$ext);
|
|
txtFind.$ext.appendChild(btnPrev.$ext);
|
|
txtFind.$ext.appendChild(btnNext.$ext);
|
|
|
|
var first = 0;
|
|
function resize() {
|
|
if (first++ < 2) { return; } // Skip first 2 calls
|
|
|
|
var h = winSearchReplace.$ext.scrollHeight;
|
|
if (Math.abs(winSearchReplace.height - h) < 1) { return; }
|
|
winSearchReplace.setHeight(h);
|
|
winSearchReplace.$ext.style.height = "";
|
|
ui.layout.forceResize(null, true);
|
|
}
|
|
|
|
txtFind.ace.renderer.on("autosize", resize);
|
|
txtReplace.ace.renderer.on("autosize", resize);
|
|
|
|
var timer, control;
|
|
txtReplace.on("focus", function() {
|
|
if (control) control.stop();
|
|
control = {};
|
|
|
|
// I'd rather use css anims, but they didn't seem to work
|
|
apf.tween.single(txtReplace.$ext.parentNode, {
|
|
type: "boxFlex",
|
|
from: txtReplace.$ext.parentNode.style[apf.CSS_FLEX_PROP] || 1,
|
|
to: 3,
|
|
anim: apf.tween.easeOutCubic,
|
|
control: control,
|
|
steps: 15,
|
|
interval: 1,
|
|
onfinish: function() {
|
|
ui.layout.forceResize(null, true);
|
|
}
|
|
});
|
|
});
|
|
txtReplace.on("blur", function() {
|
|
if (txtReplace.getValue())
|
|
return;
|
|
|
|
if (control) control.stop();
|
|
control = {};
|
|
|
|
// I'd rather use css anims, but they didn't seem to work
|
|
apf.tween.single(txtReplace.$ext.parentNode, {
|
|
type: "boxFlex",
|
|
from: txtReplace.$ext.parentNode.style[apf.CSSPREFIX + "BoxFlex"] || 3,
|
|
to: 1,
|
|
anim: apf.tween.easeOutCubic,
|
|
control: control,
|
|
steps: 15,
|
|
interval: 1,
|
|
onfinish: function() {
|
|
ui.layout.forceResize(null, true);
|
|
}
|
|
});
|
|
});
|
|
|
|
settings.on("read", function(e) {
|
|
settings.setDefaults("state/ace/search", [
|
|
["regex", "false"],
|
|
["matchcase", "false"],
|
|
["wholeword", "false"],
|
|
["backwards", "false"],
|
|
["wraparound", "true"],
|
|
["highlightmatches", "true"],
|
|
["preservecase", "false"]
|
|
]);
|
|
}, plugin);
|
|
|
|
var kb = libsearch.addSearchKeyboardHandler(txtReplace, "replace");
|
|
kb.bindKeys({
|
|
"Return": function(codebox) { replace(); },
|
|
"Shift-Return": function(codebox) { replace(true); }
|
|
});
|
|
|
|
document.body.appendChild(tooltipSearchReplace.$ext);
|
|
|
|
chk.regEx.on("prop.value", function(e) {
|
|
libsearch.setRegexpMode(txtFind, ui.isTrue(e.value));
|
|
});
|
|
libsearch.setRegexpMode(txtFind, chk.regEx.checked);
|
|
|
|
libsearch.setReplaceFieldMode(txtReplace, "extended");
|
|
|
|
decorateCheckboxes(hbox);
|
|
|
|
[txtReplace].forEach(function(node) {
|
|
tooltip.add(node.$ext, {
|
|
message: node.label,
|
|
width: "auto",
|
|
timeout: 0,
|
|
tooltip: tooltipSearchReplace.$ext,
|
|
animate: false,
|
|
getPosition: function() {
|
|
var pos = ui.getAbsolutePosition(winSearchReplace.$ext);
|
|
var pos2 = ui.getAbsolutePosition(node.$ext, winSearchReplace.$ext);
|
|
var left = pos[0] + pos2[0];
|
|
var top = pos[1];
|
|
return [left, top - 16];
|
|
}
|
|
}, plugin);
|
|
});
|
|
|
|
libsearch.addSearchKeyboardHandler(txtFind, "search");
|
|
txtFind.ace.session.on("change", function(e) {
|
|
clearTimeout(timer);
|
|
var find = !libsearch.keyStroke;
|
|
timer = setTimeout(function() {
|
|
execFind(false, find ? false : "highlight");
|
|
}, 20);
|
|
});
|
|
|
|
txtFind.ace.commands.on("exec", function(e) {
|
|
if (/centerselection|fold|comment/i.test(e.command.name)) {
|
|
getAce().execCommand(e.command.name);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
|
|
initFindInRange();
|
|
|
|
emit("draw");
|
|
}
|
|
|
|
/***** Methods *****/
|
|
|
|
function decorateCheckboxes(parent) {
|
|
var cbs = parent.selectNodes("//a:checkbox");
|
|
|
|
cbs.forEach(function(cb) {
|
|
cb.on("click", function(e) {
|
|
if (this.name == "chkSearchSelection") {
|
|
if (chk.searchSelection.checked && !txtReplace.ace.isFocused())
|
|
txtFind.focus();
|
|
updateFindInRangeMarker(e);
|
|
}
|
|
execFind(undefined, "highlight");
|
|
});
|
|
|
|
tooltip.add(cb.$ext, {
|
|
message: cb.label,
|
|
width: "auto",
|
|
timeout: 0,
|
|
tooltip: tooltipSearchReplace.$ext,
|
|
animate: false,
|
|
getPosition: function() {
|
|
var pos = ui.getAbsolutePosition(winSearchReplace.$ext);
|
|
var left = cb.$ext.getBoundingClientRect().left;
|
|
var top = pos[1];
|
|
return [left, top - 16];
|
|
}
|
|
}, plugin);
|
|
});
|
|
}
|
|
|
|
function updateCounter(total, current, msg, wrapped) {
|
|
var oIter = divSearchCount.$ext;
|
|
if (!oIter) return;
|
|
|
|
msg = msg || "";
|
|
|
|
var color = wrapped ? "blue" : "";
|
|
|
|
if (typeof total == "number" && typeof current == "number") {
|
|
if (!total) {
|
|
current = 0;
|
|
color = "red";
|
|
} else {
|
|
current = getOptions().backwards ? total - current : current + 1;
|
|
}
|
|
msg = current + " of " + total + msg;
|
|
}
|
|
oIter.style.color = color;
|
|
oIter.textContent = msg;
|
|
}
|
|
|
|
function setStartPos(ace, force) {
|
|
if (!startPos.range || force) {
|
|
startPos.range = ace.getSelectionRange();
|
|
}
|
|
if (chk.searchSelection.checked) {
|
|
var range = ace.getSelectionRange();
|
|
var isValid = ace.session.getTextRange(range).length > 100;
|
|
if (!isValid || currentRange && range.isEqual(currentRange))
|
|
range = null;
|
|
|
|
if (marker || !range && startPos.id == getSessionId(ace.session))
|
|
range = startPos.searchRange;
|
|
|
|
if (range && !range.isEmpty())
|
|
startPos.searchRange = range;
|
|
else
|
|
startPos.searchRange = null;
|
|
}
|
|
startPos.scrollTop = ace.session.getScrollTop();
|
|
startPos.scrollLeft = ace.session.getScrollLeft();
|
|
startPos.id = getSessionId(ace.session);
|
|
}
|
|
|
|
function initFromEditor(ace) {
|
|
if (!ace.selection.isEmpty() && !ace.selection.isMultiLine())
|
|
txtFind.setValue(ace.getCopyText());
|
|
}
|
|
|
|
function toggleDialog(force, isReplace, noselect, callback) {
|
|
var tab = tabs.focussedTab;
|
|
var editor = tab && tab.editor;
|
|
|
|
draw();
|
|
|
|
tooltipSearchReplace.$ext.style.display = "none";
|
|
|
|
if (!force && !winSearchReplace.visible || force > 0) {
|
|
if (!editor || !editor.ace)
|
|
return;
|
|
|
|
winSearchReplace.enable();
|
|
|
|
var ace = getAce();
|
|
var fromEditor = ace && editor && editor.ace == ace;
|
|
if (fromEditor) {
|
|
if (!isReplace)
|
|
initFromEditor(ace);
|
|
|
|
setStartPos(ace, ace.selection.isEmpty());
|
|
}
|
|
|
|
if (!winSearchReplace.visible)
|
|
showUi(callback);
|
|
|
|
// chk.searchSelection.uncheck();
|
|
var input = isReplace ? txtReplace : txtFind;
|
|
input.focus();
|
|
input.select();
|
|
}
|
|
else if (winSearchReplace.visible) {
|
|
txtFind.ace.saveHistory();
|
|
if (!noselect)
|
|
tabs.focusTab(tab);
|
|
hideUi(null, callback);
|
|
}
|
|
else if (callback)
|
|
callback();
|
|
|
|
return false;
|
|
}
|
|
|
|
function showUi(callback) {
|
|
btnReplaceAll.setCaption(searchRow.getWidth() < 800 ? "All" : "Replace All");
|
|
|
|
layout.setFindArea(winSearchReplace, {}, callback);
|
|
|
|
btnCollapse.setValue(1);
|
|
}
|
|
|
|
function hideUi(animate, callback) {
|
|
layout.setFindArea(null, {}, callback);
|
|
btnCollapse.setValue(0);
|
|
}
|
|
|
|
function restore() {
|
|
if (!startPos)
|
|
return false;
|
|
|
|
var editor = getAce();
|
|
editor.selection.setSelectionRange(startPos.range || startPos.searchRange);
|
|
editor.session.setScrollTop(startPos.scrollTop);
|
|
editor.session.setScrollLeft(startPos.scrollLeft);
|
|
}
|
|
|
|
function getOptions() {
|
|
var options = {
|
|
backwards: false,
|
|
wrap: chk.wrapAround.checked,
|
|
caseSensitive: chk.matchCase.checked,
|
|
wholeWord: chk.wholeWords.checked,
|
|
regExp: chk.regEx.checked
|
|
};
|
|
var ace = getAce();
|
|
if (chk.searchSelection.checked) {
|
|
options.range = startPos.searchRange;
|
|
}
|
|
else {
|
|
options.range = null;
|
|
}
|
|
var newLineMode = ace.session.getNewLineMode();
|
|
txtFind.ace.session.setNewLineMode(newLineMode);
|
|
txtReplace.ace.session.setNewLineMode(newLineMode);
|
|
|
|
return options;
|
|
}
|
|
|
|
function findNext(backwards) {
|
|
execFind(backwards, true);
|
|
}
|
|
|
|
/*
|
|
* type can be highlight-> only update highlighting,
|
|
* next|true -> skip current selection
|
|
* falsy -> do not skip current
|
|
*/
|
|
function execFind(reverseBackwards, type, options, callback) {
|
|
var ace = getAce();
|
|
if (!ace || !txtFind)
|
|
return;
|
|
|
|
if (timer)
|
|
timer = clearTimeout(timer);
|
|
|
|
var searchTxt = txtFind.getValue();
|
|
|
|
if (!options)
|
|
options = getOptions();
|
|
|
|
if (reverseBackwards)
|
|
options.backwards = !options.backwards;
|
|
|
|
if (options.regExp) {
|
|
libsearch.checkRegExp(txtFind,
|
|
tooltipSearchReplace, winSearchReplace);
|
|
}
|
|
|
|
var range = ace.selection.getRange();
|
|
|
|
if (type === true)
|
|
type = "next";
|
|
|
|
if (type == "next")
|
|
txtFind.ace.saveHistory();
|
|
|
|
if (type == "next" || !currentRange)
|
|
currentRange = range;
|
|
|
|
options.skipCurrent = type == "next";
|
|
options.start = currentRange;
|
|
|
|
options.needle = searchTxt;
|
|
|
|
if (options.range && type != "highlight")
|
|
addFindInRangeMarker(options.range, ace.session);
|
|
else if (!options.range)
|
|
removeFindInRangeMarker();
|
|
|
|
var re = ace.$search.$assembleRegExp(options, true);
|
|
if (!re) {
|
|
updateCounter();
|
|
if (type != "highlight") {
|
|
var pos = options.start[options.backwards ? "end" : "start"];
|
|
var newRange = options.range || Range.fromPoints(pos, pos);
|
|
ace.revealRange(newRange);
|
|
}
|
|
return callback && callback();
|
|
}
|
|
|
|
if (type != "highlight") {
|
|
lastSearchOptions = options;
|
|
}
|
|
|
|
options.re = re;
|
|
options.source = re.source;
|
|
options.flags = re.ignoreCase ? "igm" : "gm";
|
|
asyncSearch.execFind(ace.session, options, function(result) {
|
|
if (result == "waiting")
|
|
return updateCounter("...");
|
|
|
|
result = result || { total: 0, current: 0 };
|
|
updateCounter(result.total, result.current, null, result.wrapped);
|
|
|
|
if (!result.start || !result.end) {
|
|
result.start =
|
|
result.end = range[!options.backwards ? "start" : "end"];
|
|
}
|
|
var newRange = Range.fromPoints(result.start, result.end);
|
|
|
|
if (options.range && newRange.isEmpty())
|
|
newRange = options.range;
|
|
|
|
if (type == "next")
|
|
currentRange = newRange;
|
|
|
|
if (type != "highlight")
|
|
ace.revealRange(newRange);
|
|
|
|
if (options.findAll) {
|
|
selectAll(result);
|
|
} else {
|
|
// highlight
|
|
ace.session.highlight(re);
|
|
ace.session._signal("changeBackMarker");
|
|
}
|
|
|
|
callback && callback(result);
|
|
});
|
|
|
|
function selectAll(result) {
|
|
var indexArray = result.matches;
|
|
var value = result.value;
|
|
var startIndex = result.offset;
|
|
var re = options.re;
|
|
if (!indexArray.length)
|
|
return;
|
|
|
|
var doc = ace.session.doc;
|
|
var ranges = [];
|
|
var startPos = { row: 0, column: 0 };
|
|
var endPos = { row: 0, column: 0 };
|
|
var start = 0, end = 0, offset = 0;
|
|
for (var i = 0; i < indexArray.length; i++) {
|
|
var index = indexArray[i] + startIndex;
|
|
re.lastIndex = index;
|
|
var match = re.exec(value);
|
|
var txt = match[0];
|
|
var len = txt.length;
|
|
startPos = doc.indexToPosition(index + offset - start + startPos.column, startPos.row);
|
|
start = index + offset;
|
|
end = index + len + offset;
|
|
endPos = doc.indexToPosition(end - start + startPos.column, startPos.row);
|
|
ranges.push(Range.fromPoints(startPos, endPos));
|
|
}
|
|
ace.selection.fromJSON(ranges);
|
|
}
|
|
}
|
|
|
|
function findAgain(ace, direction) {
|
|
if (!ace.selection.isEmpty() && lastSearchOptions) {
|
|
var text = ace.session.getTextRange();
|
|
var match = lastSearchOptions.re && lastSearchOptions.re.exec(text);
|
|
if (!match || match[0] != text)
|
|
lastSearchOptions = null;
|
|
}
|
|
|
|
if (lastSearchOptions) {
|
|
if (chk.searchSelection.checked) {
|
|
chk.searchSelection.uncheck();
|
|
removeFindInRangeMarker(true);
|
|
delete lastSearchOptions.range;
|
|
delete lastSearchOptions.indexRange;
|
|
marker = null;
|
|
}
|
|
|
|
lastSearchOptions.backwards = direction == -1;
|
|
lastSearchOptions.findAll = direction == 0;
|
|
execFind(null, true, lastSearchOptions);
|
|
} else if (direction == -1) {
|
|
ace.findPrevious();
|
|
} else if (direction == 1) {
|
|
ace.findNext();
|
|
} else if (direction == 0) {
|
|
ace.findAll();
|
|
}
|
|
}
|
|
|
|
function replace(backwards) {
|
|
var ace = getAce();
|
|
if (!ace)
|
|
return;
|
|
|
|
var options = getOptions();
|
|
options.needle = txtFind.getValue();
|
|
var re = ace.$search.$assembleRegExp(options, true);
|
|
var replaceFn = getReplaceFunction(options);
|
|
var range = ace.selection.getRange();
|
|
execFind(backwards, false, options, function(result) {
|
|
if (!ace.selection.getRange().isEqual(range))
|
|
return; // found new one
|
|
if (result && result.total) {
|
|
re.lastIndex = result.startIndex;
|
|
var match = re.exec(result.value);
|
|
var replacement = match && replaceFn(match);
|
|
if (match[0] != replacement) {
|
|
range.end = ace.session.replace(range, replacement);
|
|
}
|
|
if (options.backwards) {
|
|
range.end = range.start;
|
|
} else {
|
|
range.start = range.end;
|
|
}
|
|
ace.selection.setRange(range);
|
|
}
|
|
findNext(backwards);
|
|
});
|
|
txtReplace.ace.saveHistory();
|
|
}
|
|
|
|
function replaceAll(cb) {
|
|
var ace = getAce();
|
|
if (!ace)
|
|
return;
|
|
|
|
var options = getOptions();
|
|
options.needle = txtFind.getValue();
|
|
var re = ace.$search.$assembleRegExp(options, true);
|
|
if (!re) {
|
|
return updateCounter();
|
|
}
|
|
options.re = re;
|
|
options.source = re.source;
|
|
options.flags = re.ignoreCase ? "igm" : "gm";
|
|
options.findAll = true;
|
|
|
|
var replaceFn = getReplaceFunction(options);
|
|
ace.$search.set({ preserveCase: chk.preserveCase.checked });
|
|
|
|
asyncSearch.execFind(ace.session, options, function(result) {
|
|
var replaced = 0;
|
|
var indexArray = result.matches;
|
|
var value = result.value;
|
|
var startIndex = result.offset;
|
|
var re = options.re;
|
|
if (!indexArray.length)
|
|
return replaced;
|
|
|
|
var doc = ace.session.doc;
|
|
|
|
var startPos = { row: 0, column: 0 };
|
|
var endPos = { row: 0, column: 0 };
|
|
var start = 0, end = 0, offset = 0;
|
|
var range = new Range();
|
|
for (var i = 0; i < indexArray.length; i++) {
|
|
var index = indexArray[i] + startIndex;
|
|
re.lastIndex = index;
|
|
var match = re.exec(value);
|
|
var txt = match[0];
|
|
var len = txt.length;
|
|
startPos = doc.indexToPosition(index + offset - start + startPos.column, startPos.row);
|
|
start = index + offset;
|
|
end = index + len + offset;
|
|
endPos = doc.indexToPosition(end - start + startPos.column, startPos.row);
|
|
range.start = startPos;
|
|
range.end = endPos;
|
|
var replacement = replaceFn(match);
|
|
if (txt != replacement) {
|
|
doc.replace(range, replacement);
|
|
offset += replacement.length - txt.length;
|
|
}
|
|
}
|
|
|
|
updateCounter();
|
|
cb && cb();
|
|
});
|
|
|
|
txtReplace.ace.saveHistory();
|
|
}
|
|
|
|
function getReplaceFunction(options) {
|
|
var val = txtReplace.getValue();
|
|
options.preserveCase = chk.preserveCase.checked;
|
|
|
|
if (options.replaceMode == "literal")
|
|
return function() { return val; };
|
|
|
|
var fmtParts = [];
|
|
function add(p) {
|
|
var last = fmtParts.length - 1;
|
|
if (p && typeof p == "string" && typeof fmtParts[last] == "string")
|
|
fmtParts[last] += p;
|
|
else if (typeof p == "number" || p)
|
|
fmtParts.push(p);
|
|
}
|
|
var lut = { n: "\n", t: "\t", r: "\r", "&": 0, U: -1, L: -2, E: -3, u: -4, l: -5 };
|
|
var re = /\$([\$&\d])|\\([\\ULulEntr\d])/g;
|
|
var index = 0, m;
|
|
while ((m = re.exec(val))) {
|
|
add(val.substring(index, m.index));
|
|
index = re.lastIndex;
|
|
var part = m[1] || m[2];
|
|
if (/\d/.test(part))
|
|
part = options.regExp ? parseInt(part, 10) : part;
|
|
else if (part in lut)
|
|
part = lut[part];
|
|
add(part);
|
|
}
|
|
add(val.substr(index));
|
|
|
|
if (fmtParts.length == 1 && typeof fmtParts[0] == "string" && !options.preserveCase)
|
|
return function() { return fmtParts[0]; };
|
|
|
|
return function(match) {
|
|
var gChangeCase = 0;
|
|
var changeCase = 0;
|
|
var result = "";
|
|
for (var i = 0; i < fmtParts.length; i++) {
|
|
var ch = fmtParts[i];
|
|
if (typeof ch === "number") {
|
|
if (ch < 0) {
|
|
switch (ch) {
|
|
case -1: gChangeCase = 1; break;
|
|
case -2: gChangeCase = 2; break;
|
|
case -3: gChangeCase = 0; break;
|
|
case -4: changeCase = 1; break;
|
|
case -5: changeCase = 2; break;
|
|
}
|
|
continue;
|
|
}
|
|
ch = match[ch] || "";
|
|
}
|
|
if (gChangeCase)
|
|
ch = gChangeCase === 1 ? ch.toUpperCase() : ch.toLowerCase();
|
|
if (changeCase && ch) {
|
|
result += changeCase === 1 ? ch[0].toUpperCase() : ch[0].toLowerCase();
|
|
ch = ch.substr(1);
|
|
changeCase = 0;
|
|
}
|
|
|
|
result += ch;
|
|
}
|
|
|
|
if (options.preserveCase) {
|
|
var input = match[0];
|
|
var replacement = result.split("");
|
|
for (var i = Math.min(input.length, replacement.length); i--;) {
|
|
var ch = input[i];
|
|
if (ch && ch.toLowerCase() != ch)
|
|
replacement[i] = replacement[i].toUpperCase();
|
|
else
|
|
replacement[i] = replacement[i].toLowerCase();
|
|
}
|
|
result = replacement.join("");
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
function getAce() {
|
|
var tab = tabs.focussedTab;
|
|
var editor = tab && tab.editor;
|
|
return editor && editor.ace;
|
|
}
|
|
|
|
function getSessionId(session) {
|
|
return (session.c9doc || session.c9session).name;
|
|
}
|
|
|
|
var marker;
|
|
function addFindInRangeMarker(range, session) {
|
|
removeFindInRangeMarker();
|
|
|
|
if (!range || !session || range.isEmpty())
|
|
return;
|
|
|
|
var start = new Range(0, 0, range.start.row, range.start.column);
|
|
var end = new Range(range.end.row, range.end.column, Number.MAX_VALUE, Number.MAX_VALUE);
|
|
start.id = session.addMarker(start, "findInRangeMarker", true, "line", true);
|
|
end.id = session.addMarker(end, "findInRangeMarker", true, "line", true);
|
|
|
|
range.start = start.end = session.doc.createAnchor(start.end.row, start.end.column);
|
|
range.end = end.start = session.doc.createAnchor(end.start.row, end.start.column);
|
|
|
|
return marker = { start: start, end: end, session: session };
|
|
}
|
|
|
|
function removeFindInRangeMarker(reset) {
|
|
if (!marker) return;
|
|
|
|
var session = marker.session;
|
|
session.removeMarker(marker.start.id);
|
|
session.removeMarker(marker.end.id);
|
|
|
|
marker.start.end.detach();
|
|
marker.end.start.detach();
|
|
marker = null;
|
|
}
|
|
|
|
function updateFindInRangeMarker(e) {
|
|
var changeFocusInside = false;
|
|
var isBlur = e.name == "blur";
|
|
var target = isBlur ? e.toElement : e.fromElement;
|
|
if (target && target.$ext) {
|
|
changeFocusInside = winSearchReplace.$ext.contains(target.$ext);
|
|
}
|
|
|
|
if (changeFocusInside)
|
|
return;
|
|
|
|
var ace = getAce();
|
|
if (isBlur || !ace)
|
|
return removeFindInRangeMarker();
|
|
|
|
if (e.fromElement && e.fromElement.editor) {
|
|
if (e.fromElement.editor.ace == ace)
|
|
setStartPos(ace);
|
|
|
|
execFind(false, "highlight");
|
|
}
|
|
|
|
if (!startPos.searchRange || startPos.id !== getSessionId(ace.session) || e.name == "click")
|
|
setStartPos(ace);
|
|
|
|
if (chk.searchSelection.checked)
|
|
addFindInRangeMarker(getOptions().range, ace.session);
|
|
else
|
|
removeFindInRangeMarker();
|
|
if (e.name)
|
|
currentRange = null;
|
|
}
|
|
|
|
function initFindInRange() {
|
|
winSearchReplace.addEventListener("focus", updateFindInRangeMarker);
|
|
winSearchReplace.addEventListener("blur", updateFindInRangeMarker);
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function() {
|
|
load();
|
|
});
|
|
plugin.on("enable", function() {
|
|
|
|
});
|
|
plugin.on("disable", function() {
|
|
|
|
});
|
|
plugin.on("unload", function() {
|
|
loaded = false;
|
|
asyncSearch.terminateWorker();
|
|
searchRow = txtFind = winSearchReplace = txtReplace = null;
|
|
tooltipSearchReplace = divSearchCount = null;
|
|
btnPrev = btnNext = btnReplace = btnReplaceAll = hbox = btnCollapse = null;
|
|
currentRange = lastSearchOptions = timer = null;
|
|
startPos = {};
|
|
});
|
|
|
|
/***** Register and define API *****/
|
|
|
|
/**
|
|
* Implements the search and replace UI for Cloud9.
|
|
* @singleton
|
|
*/
|
|
/**
|
|
* Fetches a ui element. You can use this method both sync and async.
|
|
*
|
|
* The search in files plugin has the following elements:
|
|
*
|
|
* * txtFind - `{ui.textbox}`
|
|
* * winSearchReplace - `{ui.window}`
|
|
* * txtReplace - `{ui.textbox}`
|
|
* * tooltipSearchReplace - `{ui.label}`
|
|
* * chkSearchSelection - `{ui.checkbox}`
|
|
* * chkRegEx - `{ui.checkbox}`
|
|
* * chkWrapAround - `{ui.checkbox}`
|
|
* * chkMatchCase - `{ui.checkbox}`
|
|
* * chkWholeWords - `{ui.checkbox}`
|
|
* * chkPreserveCase - `{ui.checkbox}`
|
|
* * btnPrev - `{ui.button}`
|
|
* * btnNext - `{ui.button}`
|
|
* * btnReplace - `{ui.button}`
|
|
* * btnReplaceAll - `{ui.button}`
|
|
* * btnCollapse - `{ui.button}`
|
|
*
|
|
* @method getElement
|
|
* @param {String} name the id of the element to fetch.
|
|
* @param {Function} [callback] the function to call when the
|
|
* element is available (could be immediately)
|
|
*/
|
|
plugin.freezePublicAPI({
|
|
/**
|
|
*
|
|
*/
|
|
get aml() { return winSearchReplace; },
|
|
|
|
/**
|
|
* Toggles the visibility of the search and replace panel.
|
|
* @param {Number} force Set to -1 to force hide the panel,
|
|
* or set to 1 to force show the panel.
|
|
*/
|
|
toggle: toggleDialog,
|
|
|
|
/**
|
|
* Return the cursor and selection to where it was, prior to
|
|
* starting searching.
|
|
*/
|
|
restore: restore,
|
|
|
|
/**
|
|
* Find the next occurance of the search query. If wrap around is
|
|
* turned on, the search will continue from the beginning when it
|
|
* reaches the end of the file.
|
|
* @param {Boolean} backwards When set to true the search direction is reversed.
|
|
*/
|
|
findNext: findNext,
|
|
|
|
/**
|
|
* Replace the next occurance of the query with whatever the user
|
|
* entered in the replace textbox.
|
|
* @param {Boolean} backwards When set to true the search direction is reversed.
|
|
*/
|
|
replace: replace,
|
|
|
|
/**
|
|
* Replace all occurences of the query with whatever the user
|
|
* entered in the replace textbox.
|
|
*/
|
|
replaceAll: replaceAll
|
|
});
|
|
|
|
register(null, {
|
|
findreplace: plugin
|
|
});
|
|
}
|
|
}); |