Merge pull request #517 from pulsar-edit/bundle-find-and-replace

Bundle find and replace
This commit is contained in:
Maurício Szabo 2023-05-09 11:46:57 -03:00 committed by GitHub
commit 7bc10280c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 11897 additions and 8 deletions

View File

@ -60,7 +60,7 @@
"etch": "0.14.1",
"event-kit": "^2.5.3",
"exception-reporting": "file:packages/exception-reporting",
"find-and-replace": "https://github.com/atom-community/find-and-replace/archive/refs/tags/v0.220.1.tar.gz",
"find-and-replace": "file:packages/find-and-replace",
"find-parent-dir": "^0.3.0",
"focus-trap": "6.3.0",
"fs-admin": "0.19.0",
@ -203,7 +203,7 @@
"dev-live-reload": "file:./packages/dev-live-reload",
"encoding-selector": "file:./packages/encoding-selector",
"exception-reporting": "file:./packages/exception-reporting",
"find-and-replace": "0.220.1",
"find-and-replace": "file:./packages/find-and-replace",
"fuzzy-finder": "1.14.3",
"github": "0.36.14",
"git-diff": "file:./packages/git-diff",

View File

@ -31,7 +31,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **dev-live-reload** | [`./dev-live-reload`](./dev-live-reload) | |
| **encoding-selector** | [`./encoding-selector`](./encoding-selector) | |
| **exception-reporting** | [`./exception-reporting`](./exception-reporting) | |
| **find-and-replace** | [`pulsar-edit/find-and-replace`][find-and-replace] | |
| **find-and-replace** | [`./find-and-replace`][find-and-replace] | |
| **fuzzy-finder** | [`pulsar-edit/fuzzy-finder`][fuzzy-finder] | |
| **github** | [`pulsar-edit/github`][github] | |
| **git-diff** | [`./git-diff`](./git-diff) | |
@ -100,8 +100,6 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **whitespace** | [`./whitespace`](./whitespace) | |
| **wrap-guide** | [`./wrap-guide`](./wrap-guide) | |
[find-and-replace]: https://github.com/pulsar-edit/find-and-replace
[fuzzy-finder]: https://github.com/pulsar-edit/fuzzy-finder
[github]: https://github.com/pulsar-edit/github
[notifications]: https://github.com/pulsar-edit/notifications
[snippets]: https://github.com/pulsar-edit/snippets

View File

@ -0,0 +1,20 @@
Copyright (c) 2014 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,17 @@
# Find and Replace package
Find and replace in the current buffer or across the entire project.
## Find in buffer
Using the shortcut <kbd>cmd-f</kbd> (Mac) or <kbd>ctrl-f</kbd> (Windows and Linux).
![screen shot 2013-11-26 at 12 25 22 pm](https://f.cloud.github.com/assets/69169/1625938/a859fa70-56d9-11e3-8b2a-ac37c5033159.png)
## Find in project
Using the shortcut <kbd>cmd-shift-f</kbd> (Mac) or <kbd>ctrl-shift-f</kbd> (Windows and Linux).
![screen shot 2013-11-26 at 12 26 02 pm](https://f.cloud.github.com/assets/69169/1625945/b216d7b8-56d9-11e3-8b14-6afc33467be9.png)
## Provided Service
If you need access the marker layer containing result markers for a given editor, use the `find-and-replace@0.0.1` service. The service exposes one method, `resultsMarkerLayerForTextEditor`, which takes a `TextEditor` and returns a `TextEditorMarkerLayer` that you can interact with. Keep in mind that any work you do in synchronous event handlers on this layer will impact the performance of find and replace.

View File

@ -0,0 +1,85 @@
'.platform-darwin':
'cmd-F': 'project-find:show'
'cmd-f': 'find-and-replace:show'
'cmd-alt-f': 'find-and-replace:show-replace'
'.platform-win32, .platform-linux':
'ctrl-F': 'project-find:show'
'ctrl-f': 'find-and-replace:show'
'.platform-darwin atom-text-editor':
'cmd-g': 'find-and-replace:find-next'
'cmd-G': 'find-and-replace:find-previous'
'cmd-f3': 'find-and-replace:find-next-selected'
'cmd-shift-f3': 'find-and-replace:find-previous-selected'
'cmd-ctrl-g': 'find-and-replace:select-all'
'cmd-d': 'find-and-replace:select-next'
'cmd-alt-e': 'find-and-replace:replace-next'
'cmd-e': 'find-and-replace:use-selection-as-find-pattern'
'cmd-shift-e': 'find-and-replace:use-selection-as-replace-pattern'
'cmd-u': 'find-and-replace:select-undo'
'cmd-k cmd-d': 'find-and-replace:select-skip'
'.platform-win32 atom-text-editor, .platform-linux atom-text-editor':
'f3': 'find-and-replace:find-next'
'shift-f3': 'find-and-replace:find-previous'
'ctrl-f3': 'find-and-replace:find-next-selected'
'ctrl-shift-f3': 'find-and-replace:find-previous-selected'
'alt-f3': 'find-and-replace:select-all'
'ctrl-d': 'find-and-replace:select-next'
'ctrl-e': 'find-and-replace:use-selection-as-find-pattern'
'ctrl-shift-e': 'find-and-replace:use-selection-as-replace-pattern'
'ctrl-u': 'find-and-replace:select-undo'
'ctrl-k ctrl-d': 'find-and-replace:select-skip'
'.platform-darwin .find-and-replace':
'shift-enter': 'find-and-replace:show-previous'
'cmd-enter': 'find-and-replace:confirm'
'alt-enter': 'find-and-replace:find-all'
'cmd-alt-/': 'find-and-replace:toggle-regex-option'
'cmd-alt-c': 'find-and-replace:toggle-case-option'
'cmd-alt-s': 'find-and-replace:toggle-selection-option'
'cmd-alt-w': 'find-and-replace:toggle-whole-word-option'
'.platform-win32 .find-and-replace, .platform-linux .find-and-replace':
'shift-enter': 'find-and-replace:show-previous'
'ctrl-enter': 'find-and-replace:confirm'
'alt-enter': 'find-and-replace:find-all'
'ctrl-alt-/': 'find-and-replace:toggle-regex-option'
'ctrl-shift-c': 'find-and-replace:toggle-case-option'
'.platform-darwin .project-find':
'cmd-enter': 'project-find:confirm'
'cmd-alt-/': 'project-find:toggle-regex-option'
'cmd-alt-c': 'project-find:toggle-case-option'
'cmd-alt-w': 'project-find:toggle-whole-word-option'
'.platform-win32 .project-find, .platform-linux .project-find':
'ctrl-enter': 'project-find:confirm'
'ctrl-alt-/': 'project-find:toggle-regex-option'
'ctrl-shift-c': 'project-find:toggle-case-option'
'.find-and-replace, .project-find, .project-find .results-view':
'tab': 'find-and-replace:focus-next'
'shift-tab': 'find-and-replace:focus-previous'
'.platform-darwin .find-and-replace .replace-container atom-text-editor':
'cmd-enter': 'find-and-replace:replace-all'
'.platform-darwin .project-find .replace-container atom-text-editor':
'cmd-enter': 'project-find:replace-all'
'.platform-win32 .find-and-replace .replace-container atom-text-editor':
'ctrl-enter': 'find-and-replace:replace-all'
'.platform-win32 .project-find .replace-container atom-text-editor':
'ctrl-enter': 'project-find:replace-all'
'.platform-linux .find-and-replace .replace-container atom-text-editor':
'ctrl-enter': 'find-and-replace:replace-all'
'.platform-linux .project-find .replace-container atom-text-editor':
'ctrl-enter': 'project-find:replace-all'
'.results-view':
'home': 'core:move-to-top'
'ctrl-home': 'core:move-to-top'
'end': 'core:move-to-bottom'
'ctrl-end': 'core:move-to-bottom'

View File

@ -0,0 +1,305 @@
const { Point, Range, Emitter, CompositeDisposable, TextBuffer } = require('atom');
const FindOptions = require('./find-options');
const escapeHelper = require('./escape-helper');
const Util = require('./project/util');
const ResultsMarkerLayersByEditor = new WeakMap;
module.exports =
class BufferSearch {
constructor(findOptions) {
this.findOptions = findOptions;
this.emitter = new Emitter;
this.subscriptions = null;
this.markers = [];
this.editor = null;
}
onDidUpdate(callback) {
return this.emitter.on('did-update', callback);
}
onDidError(callback) {
return this.emitter.on('did-error', callback);
}
onDidChangeCurrentResult(callback) {
return this.emitter.on('did-change-current-result', callback);
}
setEditor(editor) {
this.editor = editor;
if (this.subscriptions) this.subscriptions.dispose();
if (this.editor) {
this.subscriptions = new CompositeDisposable;
this.subscriptions.add(this.editor.onDidStopChanging(this.bufferStoppedChanging.bind(this)));
this.subscriptions.add(this.editor.onDidAddSelection(this.setCurrentMarkerFromSelection.bind(this)));
this.subscriptions.add(this.editor.onDidChangeSelectionRange(this.setCurrentMarkerFromSelection.bind(this)));
this.resultsMarkerLayer = this.resultsMarkerLayerForTextEditor(this.editor);
if (this.resultsLayerDecoration) this.resultsLayerDecoration.destroy();
this.resultsLayerDecoration = this.editor.decorateMarkerLayer(this.resultsMarkerLayer, {type: 'highlight', class: 'find-result'});
}
this.recreateMarkers();
}
getEditor() { return this.editor; }
setFindOptions(newParams) { return this.findOptions.set(newParams); }
getFindOptions() { return this.findOptions; }
resultsMarkerLayerForTextEditor(editor) {
let layer = ResultsMarkerLayersByEditor.get(editor)
if (!layer) {
layer = editor.addMarkerLayer({maintainHistory: false});
ResultsMarkerLayersByEditor.set(editor, layer);
}
return layer;
}
patternMatchesEmptyString(findPattern) {
const findOptions = new FindOptions(this.findOptions.serialize());
findOptions.set({findPattern});
try {
return findOptions.getFindPatternRegex().test('');
} catch (e) {
this.emitter.emit('did-error', e);
return false;
}
}
search(findPattern, otherOptions) {
let options = {findPattern};
Object.assign(options, otherOptions);
const changedParams = this.findOptions.set(options);
if (!this.editor ||
changedParams.findPattern != null ||
changedParams.useRegex != null ||
changedParams.wholeWord != null ||
changedParams.caseSensitive != null ||
changedParams.inCurrentSelection != null ||
(this.findOptions.inCurrentSelection === true
&& !selectionsEqual(this.editor.getSelectedBufferRanges(), this.selectedRanges))) {
this.recreateMarkers();
}
}
replace(markers, replacePattern) {
if (!markers || markers.length === 0) return;
this.findOptions.set({replacePattern});
const preserveCaseOnReplace = atom.config.get('find-and-replace.preserveCaseOnReplace')
this.editor.transact(() => {
let findRegex = null
if (this.findOptions.useRegex) {
findRegex = this.getFindPatternRegex();
replacePattern = escapeHelper.unescapeEscapeSequence(replacePattern);
}
for (let i = 0, n = markers.length; i < n; i++) {
const marker = markers[i]
const bufferRange = marker.getBufferRange();
const replacedText = this.editor.getTextInBufferRange(bufferRange)
let replacementText = findRegex ? replacedText.replace(findRegex, replacePattern) : replacePattern;
replacementText = preserveCaseOnReplace ? Util.preserveCase(replacementText, replacedText): replacementText
this.editor.setTextInBufferRange(bufferRange, replacementText);
marker.destroy();
this.markers.splice(this.markers.indexOf(marker), 1);
}
});
return this.emitter.emit('did-update', this.markers.slice());
}
destroy() {
if (this.subscriptions) this.subscriptions.dispose();
}
/*
Section: Private
*/
recreateMarkers() {
if (this.resultsMarkerLayer) {
this.resultsMarkerLayer.clear()
}
this.markers.length = 0;
const markers = this.createMarkers(Point.ZERO, Point.INFINITY);
if (markers) {
this.markers = markers;
return this.emitter.emit("did-update", this.markers.slice());
}
}
createMarkers(start, end) {
let newMarkers = [];
if (this.findOptions.findPattern && this.editor) {
this.selectedRanges = this.editor.getSelectedBufferRanges()
let searchRanges = []
if (this.findOptions.inCurrentSelection) {
searchRanges.push(...this.selectedRanges.filter(range => !range.isEmpty()))
}
if (searchRanges.length === 0) {
searchRanges.push(Range(start, end))
}
const buffer = this.editor.getBuffer()
const regex = this.getFindPatternRegex(buffer.hasAstral && buffer.hasAstral())
if (regex) {
try {
for (const range of searchRanges) {
const bufferMarkers = this.editor.getBuffer().findAndMarkAllInRangeSync(
this.resultsMarkerLayer.bufferMarkerLayer,
regex,
range,
{invalidate: 'inside'}
);
for (const bufferMarker of bufferMarkers) {
newMarkers.push(this.resultsMarkerLayer.getMarker(bufferMarker.id))
}
}
} catch (error) {
this.emitter.emit('did-error', error);
return false;
}
} else {
return false;
}
}
return newMarkers;
}
bufferStoppedChanging({changes}) {
let marker;
let scanEnd = Point.ZERO;
let markerIndex = 0;
for (let change of changes) {
const changeStart = change.start;
const changeEnd = change.start.traverse(change.newExtent);
if (changeEnd.isLessThan(scanEnd)) continue;
let precedingMarkerIndex = -1;
while (marker = this.markers[markerIndex]) {
if (marker.isValid()) {
if (marker.getBufferRange().end.isGreaterThan(changeStart)) { break; }
precedingMarkerIndex = markerIndex;
} else {
this.markers[markerIndex] = this.recreateMarker(marker);
}
markerIndex++;
}
let followingMarkerIndex = -1;
while (marker = this.markers[markerIndex]) {
if (marker.isValid()) {
followingMarkerIndex = markerIndex;
if (marker.getBufferRange().start.isGreaterThanOrEqual(changeEnd)) { break; }
} else {
this.markers[markerIndex] = this.recreateMarker(marker);
}
markerIndex++;
}
let spliceStart, scanStart
if (precedingMarkerIndex >= 0) {
spliceStart = precedingMarkerIndex;
scanStart = this.markers[precedingMarkerIndex].getBufferRange().start;
} else {
spliceStart = 0;
scanStart = Point.ZERO;
}
let spliceEnd
if (followingMarkerIndex >= 0) {
spliceEnd = followingMarkerIndex;
scanEnd = this.markers[followingMarkerIndex].getBufferRange().end;
} else {
spliceEnd = Infinity;
scanEnd = Point.INFINITY;
}
const newMarkers = this.createMarkers(scanStart, scanEnd) || [];
const oldMarkers = this.markers.splice(spliceStart, (spliceEnd - spliceStart) + 1, ...newMarkers);
for (let oldMarker of oldMarkers) {
oldMarker.destroy();
}
markerIndex += newMarkers.length - oldMarkers.length;
}
while (marker = this.markers[++markerIndex]) {
if (!marker.isValid()) {
this.markers[markerIndex] = this.recreateMarker(marker);
}
}
this.emitter.emit('did-update', this.markers.slice());
this.currentResultMarker = null;
this.setCurrentMarkerFromSelection();
}
setCurrentMarkerFromSelection() {
const marker = this.findMarker(this.editor.getSelectedBufferRange());
if (marker === this.currentResultMarker) return;
if (this.currentResultMarker) {
this.resultsLayerDecoration.setPropertiesForMarker(this.currentResultMarker, null);
this.currentResultMarker = null;
}
if (marker && !marker.isDestroyed()) {
this.resultsLayerDecoration.setPropertiesForMarker(marker, {type: 'highlight', class: 'current-result'});
this.currentResultMarker = marker;
}
this.emitter.emit('did-change-current-result', this.currentResultMarker);
}
findMarker(range) {
if (this.resultsMarkerLayer) {
return this.resultsMarkerLayer.findMarkers({
startBufferPosition: range.start,
endBufferPosition: range.end
})[0];
}
}
recreateMarker(marker) {
const range = marker.getBufferRange()
marker.destroy();
return this.createMarker(range);
}
createMarker(range) {
return this.resultsMarkerLayer.markBufferRange(range, {invalidate: 'inside'});
}
getFindPatternRegex(forceUnicode) {
try {
return this.findOptions.getFindPatternRegex(forceUnicode);
} catch (e) {
this.emitter.emit('did-error', e);
return null;
}
}
};
function selectionsEqual(selectionsA, selectionsB) {
if (selectionsA.length === selectionsB.length) {
for (let i = 0; i < selectionsA.length; i++) {
if (!selectionsA[i].isEqual(selectionsB[i])) {
return false
}
}
return true
} else {
return false
}
}

View File

@ -0,0 +1,26 @@
const fs = require('fs-plus')
const path = require('path')
class DefaultFileIcons {
iconClassForPath (filePath) {
const extension = path.extname(filePath)
if (fs.isSymbolicLinkSync(filePath)) {
return 'icon-file-symlink-file'
} else if (fs.isReadmePath(filePath)) {
return 'icon-book'
} else if (fs.isCompressedExtension(extension)) {
return 'icon-file-zip'
} else if (fs.isImageExtension(extension)) {
return 'icon-file-media'
} else if (fs.isPdfExtension(extension)) {
return 'icon-file-pdf'
} else if (fs.isBinaryExtension(extension)) {
return 'icon-file-binary'
} else {
return 'icon-file-text'
}
}
}
module.exports = new DefaultFileIcons()

View File

@ -0,0 +1,17 @@
module.exports = {
unescapeEscapeSequence(string) {
return string.replace(/\\(.)/gm, function(match, char) {
if (char === 't') {
return '\t';
} else if (char === 'n') {
return '\n';
} else if (char === 'r') {
return '\r';
} else if (char === '\\') {
return '\\';
} else {
return match;
}
});
}
};

View File

@ -0,0 +1,103 @@
const _ = require('underscore-plus');
const {Emitter} = require('atom');
const Params = [
'findPattern',
'replacePattern',
'pathsPattern',
'useRegex',
'wholeWord',
'caseSensitive',
'inCurrentSelection',
'leadingContextLineCount',
'trailingContextLineCount'
];
module.exports = class FindOptions {
constructor(state) {
let left, left1, left2, left3, left4, left5;
if (state == null) { state = {}; }
this.emitter = new Emitter;
this.findPattern = '';
this.replacePattern = state.replacePattern != null ? state.replacePattern : '';
this.pathsPattern = state.pathsPattern != null ? state.pathsPattern : '';
this.useRegex = (left = state.useRegex != null ? state.useRegex : atom.config.get('find-and-replace.useRegex')) != null ? left : false;
this.caseSensitive = (left1 = state.caseSensitive != null ? state.caseSensitive : atom.config.get('find-and-replace.caseSensitive')) != null ? left1 : false;
this.wholeWord = (left2 = state.wholeWord != null ? state.wholeWord : atom.config.get('find-and-replace.wholeWord')) != null ? left2 : false;
this.inCurrentSelection = (left3 = state.inCurrentSelection != null ? state.inCurrentSelection : atom.config.get('find-and-replace.inCurrentSelection')) != null ? left3 : false;
this.leadingContextLineCount = (left4 = state.leadingContextLineCount != null ? state.leadingContextLineCount : atom.config.get('find-and-replace.leadingContextLineCount')) != null ? left4 : 0;
this.trailingContextLineCount = (left5 = state.trailingContextLineCount != null ? state.trailingContextLineCount : atom.config.get('find-and-replace.trailingContextLineCount')) != null ? left5 : 0;
}
onDidChange(callback) {
return this.emitter.on('did-change', callback);
}
onDidChangeUseRegex(callback) {
return this.emitter.on('did-change-useRegex', callback);
}
onDidChangeReplacePattern(callback) {
return this.emitter.on('did-change-replacePattern', callback);
}
serialize() {
const result = {};
for (let param of Array.from(Params)) {
result[param] = this[param];
}
return result;
}
set(newParams) {
if (newParams == null) { newParams = {}; }
let changedParams = {};
for (let key of Array.from(Params)) {
if ((newParams[key] != null) && (newParams[key] !== this[key])) {
if (changedParams == null) { changedParams = {}; }
this[key] = (changedParams[key] = newParams[key]);
}
}
if (Object.keys(changedParams).length) {
for (let param in changedParams) {
const val = changedParams[param];
this.emitter.emit(`did-change-${param}`);
}
this.emitter.emit('did-change', changedParams);
}
return changedParams;
}
getFindPatternRegex(forceUnicode) {
let expression;
if (forceUnicode == null) { forceUnicode = false; }
for (let i = 0, end = this.findPattern.length, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
if (this.findPattern.charCodeAt(i) > 128) {
forceUnicode = true;
break;
}
}
let flags = 'gm';
if (!this.caseSensitive) { flags += 'i'; }
if (forceUnicode) { flags += 'u'; }
if (this.useRegex) {
expression = this.findPattern;
} else {
expression = escapeRegExp(this.findPattern);
}
if (this.wholeWord) { expression = `\\b${expression}\\b`; }
return new RegExp(expression, flags);
}
}
// This is different from _.escapeRegExp, which escapes dashes. Escaped dashes
// are not allowed outside of character classes in RegExps with the `u` flag.
//
// See atom/find-and-replace#1022
var escapeRegExp = string => string.replace(/[\/\\^$*+?.()|[\]{}]/g, '\\$&');

View File

@ -0,0 +1,805 @@
const _ = require('underscore-plus');
const {TextBuffer, TextEditor, CompositeDisposable} = require('atom');
const Util = require('./project/util');
const etch = require('etch');
const $ = etch.dom;
module.exports =
class FindView {
constructor(model, {findBuffer, replaceBuffer, findHistoryCycler, replaceHistoryCycler} = {}) {
this.model = model;
this.findBuffer = findBuffer;
this.replaceBuffer = replaceBuffer;
this.findHistoryCycler = findHistoryCycler;
this.replaceHistoryCycler = replaceHistoryCycler;
this.subscriptions = new CompositeDisposable();
etch.initialize(this)
this.findHistoryCycler.addEditorElement(this.findEditor.getElement());
this.replaceHistoryCycler.addEditorElement(this.replaceEditor.getElement());
this.handleEvents();
this.clearMessage();
this.updateOptionViews();
this.updateSyntaxHighlighting();
this.updateFindEnablement();
this.updateReplaceEnablement();
this.createWrapIcon();
}
update(props) {}
render() {
return (
$.div({tabIndex: -1, className: 'find-and-replace'},
$.header({className: 'header'},
$.span({ref: 'closeButton', className: 'header-item close-button pull-right'},
$.i({className: "icon icon-x clickable"})
),
$.span({ref: 'descriptionLabel', className: 'header-item description'},
'Find in Current Buffer'
),
$.span({className: 'header-item options-label pull-right'},
$.span({}, 'Finding with Options: '),
$.span({ref: 'optionsLabel', className: 'options'}),
$.span({className: 'btn-group btn-toggle btn-group-options'},
$.button({ref: 'regexOptionButton', className: 'btn'},
$.svg({className: "icon", innerHTML: '<use href="#find-and-replace-icon-regex" />'})
),
$.button({ref: 'caseOptionButton', className: 'btn'},
$.svg({className: "icon", innerHTML: '<use href="#find-and-replace-icon-case" />'})
),
$.button({ref: 'selectionOptionButton', className: 'btn option-selection'},
$.svg({className: "icon", innerHTML: '<use href="#find-and-replace-icon-selection" />'})
),
$.button({ref: 'wholeWordOptionButton', className: 'btn option-whole-word'},
$.svg({className: "icon", innerHTML: '<use href="#find-and-replace-icon-word" />'})
)
)
)
),
$.section({className: 'input-block find-container'},
$.div({className: 'input-block-item input-block-item--flex editor-container'},
$(TextEditor, {
ref: 'findEditor',
mini: true,
placeholderText: 'Find in current buffer',
buffer: this.findBuffer
}),
$.div({className: 'find-meta-container'},
$.span({ref: 'resultCounter', className: 'text-subtle result-counter'})
)
),
$.div({className: 'input-block-item'},
$.div({className: 'btn-group btn-group-find'},
$.button({ref: 'nextButton', className: 'btn btn-next'}, 'Find')
),
$.div({className: 'btn-group btn-group-find-all'},
$.button({ref: 'findAllButton', className: 'btn btn-all'}, 'Find All')
)
)
),
$.section({className: 'input-block replace-container'},
$.div({className: 'input-block-item input-block-item--flex editor-container'},
$(TextEditor, {
ref: 'replaceEditor',
mini: true,
placeholderText: 'Replace in current buffer',
buffer: this.replaceBuffer
})
),
$.div({className: 'input-block-item'},
$.div({className: 'btn-group btn-group-replace'},
$.button({ref: 'replaceNextButton', className: 'btn btn-next'}, 'Replace')
),
$.div({className: 'btn-group btn-group-replace-all'},
$.button({ref: 'replaceAllButton', className: 'btn btn-all'}, 'Replace All')
)
)
),
$.svg({style: {display: 'none'}, innerHTML: `
<symbol id="find-and-replace-icon-regex" viewBox="0 0 20 16" stroke="none" fill-rule="evenodd">
<rect x="3" y="10" width="3" height="3" rx="1"></rect>
<rect x="12" y="3" width="2" height="9" rx="1"></rect>
<rect transform="translate(13.000000, 7.500000) rotate(60.000000) translate(-13.000000, -7.500000) " x="12" y="3" width="2" height="9" rx="1"></rect>
<rect transform="translate(13.000000, 7.500000) rotate(-60.000000) translate(-13.000000, -7.500000) " x="12" y="3" width="2" height="9" rx="1"></rect>
</symbol>
<symbol id="find-and-replace-icon-case" viewBox="0 0 20 16" stroke="none" fill-rule="evenodd">
<path d="M10.919,13 L9.463,13 C9.29966585,13 9.16550052,12.9591671 9.0605,12.8775 C8.95549947,12.7958329 8.8796669,12.6943339 8.833,12.573 L8.077,10.508 L3.884,10.508 L3.128,12.573 C3.09066648,12.6803339 3.01716722,12.7783329 2.9075,12.867 C2.79783279,12.9556671 2.66366746,13 2.505,13 L1.042,13 L5.018,2.878 L6.943,2.878 L10.919,13 Z M4.367,9.178 L7.594,9.178 L6.362,5.811 C6.30599972,5.66166592 6.24416701,5.48550102 6.1765,5.2825 C6.108833,5.07949898 6.04233366,4.85900119 5.977,4.621 C5.91166634,4.85900119 5.84750032,5.08066564 5.7845,5.286 C5.72149969,5.49133436 5.65966697,5.67099923 5.599,5.825 L4.367,9.178 Z M18.892,13 L18.115,13 C17.9516658,13 17.8233338,12.9755002 17.73,12.9265 C17.6366662,12.8774998 17.5666669,12.7783341 17.52,12.629 L17.366,12.118 C17.1839991,12.2813341 17.0055009,12.4248327 16.8305,12.5485 C16.6554991,12.6721673 16.4746676,12.7759996 16.288,12.86 C16.1013324,12.9440004 15.903001,13.0069998 15.693,13.049 C15.4829989,13.0910002 15.2496679,13.112 14.993,13.112 C14.6896651,13.112 14.4096679,13.0711671 14.153,12.9895 C13.896332,12.9078329 13.6758342,12.7853342 13.4915,12.622 C13.3071657,12.4586658 13.1636672,12.2556679 13.061,12.013 C12.9583328,11.7703321 12.907,11.4880016 12.907,11.166 C12.907,10.895332 12.9781659,10.628168 13.1205,10.3645 C13.262834,10.100832 13.499665,9.8628344 13.831,9.6505 C14.162335,9.43816561 14.6033306,9.2620007 15.154,9.122 C15.7046694,8.9819993 16.3883292,8.90266676 17.205,8.884 L17.205,8.464 C17.205,7.98333093 17.103501,7.62750116 16.9005,7.3965 C16.697499,7.16549885 16.4023352,7.05 16.015,7.05 C15.7349986,7.05 15.5016676,7.08266634 15.315,7.148 C15.1283324,7.21333366 14.9661673,7.28683292 14.8285,7.3685 C14.6908326,7.45016707 14.5636672,7.52366634 14.447,7.589 C14.3303327,7.65433366 14.2020007,7.687 14.062,7.687 C13.9453327,7.687 13.8450004,7.65666697 13.761,7.596 C13.6769996,7.53533303 13.6093336,7.46066711 13.558,7.372 L13.243,6.819 C14.0690041,6.06299622 15.0653275,5.685 16.232,5.685 C16.6520021,5.685 17.0264983,5.75383264 17.3555,5.8915 C17.6845016,6.02916736 17.9633322,6.22049877 18.192,6.4655 C18.4206678,6.71050122 18.5944994,7.00333163 18.7135,7.344 C18.8325006,7.68466837 18.892,8.05799797 18.892,8.464 L18.892,13 Z M15.532,11.922 C15.7093342,11.922 15.8726659,11.9056668 16.022,11.873 C16.1713341,11.8403332 16.3124993,11.7913337 16.4455,11.726 C16.5785006,11.6606663 16.7068327,11.5801671 16.8305,11.4845 C16.9541673,11.3888329 17.0789993,11.2756673 17.205,11.145 L17.205,9.934 C16.7009975,9.95733345 16.279835,10.0004997 15.9415,10.0635 C15.603165,10.1265003 15.3313343,10.2069995 15.126,10.305 C14.9206656,10.4030005 14.7748337,10.5173327 14.6885,10.648 C14.6021662,10.7786673 14.559,10.9209992 14.559,11.075 C14.559,11.3783349 14.6488324,11.5953327 14.8285,11.726 C15.0081675,11.8566673 15.2426652,11.922 15.532,11.922 L15.532,11.922 Z"></path>
</symbol>
<symbol id="find-and-replace-icon-selection" viewBox="0 0 20 16" stroke="none" fill-rule="evenodd">
<rect opacity="0.6" x="17" y="9" width="2" height="4"></rect>
<rect opacity="0.6" x="14" y="9" width="2" height="4"></rect>
<rect opacity="0.6" x="1" y="3" width="2" height="4"></rect>
<rect x="1" y="9" width="11" height="4"></rect>
<rect x="5" y="3" width="14" height="4"></rect>
</symbol>
<symbol id="find-and-replace-icon-word" viewBox="0 0 20 16" stroke="none" fill-rule="evenodd">
<rect opacity="0.6" x="1" y="3" width="2" height="6"></rect>
<rect opacity="0.6" x="17" y="3" width="2" height="6"></rect>
<rect x="6" y="3" width="2" height="6"></rect>
<rect x="12" y="3" width="2" height="6"></rect>
<rect x="9" y="3" width="2" height="6"></rect>
<path d="M4.5,13 L15.5,13 L16,13 L16,12 L15.5,12 L4.5,12 L4,12 L4,13 L4.5,13 L4.5,13 Z"></path>
<path d="M4,10.5 L4,12.5 L4,13 L5,13 L5,12.5 L5,10.5 L5,10 L4,10 L4,10.5 L4,10.5 Z"></path>
<path d="M15,10.5 L15,12.5 L15,13 L16,13 L16,12.5 L16,10.5 L16,10 L15,10 L15,10.5 L15,10.5 Z"></path>
</symbol>
<symbol id="find-and-replace-context-lines-before" viewBox="0 0 20 16" stroke="none" fill-rule="evenodd">
<rect opacity="0.6" x="2" y="11" width="16" height="2"></rect>
<rect x="2" y="7" width="10" height="2"></rect>
<rect x="2" y="3" width="10" height="2"></rect>
</symbol>
<symbol id="find-and-replace-context-lines-after" viewBox="0 0 20 16" stroke="none" fill-rule="evenodd">
<rect x="2" y="11" width="10" height="2"></rect>
<rect x="2" y="7" width="10" height="2"></rect>
<rect opacity="0.6" x="2" y="3" width="16" height="2"></rect>
</symbol>
`})
)
);
}
get findEditor() { return this.refs.findEditor; }
get replaceEditor() { return this.refs.replaceEditor; }
destroy() {
if (this.subscriptions) this.subscriptions.dispose();
if (this.tooltipSubscriptions) this.tooltipSubscriptions.dispose();
}
setPanel(panel) {
this.panel = panel;
this.subscriptions.add(this.panel.onDidChangeVisible(visible => {
if (visible) {
this.didShow();
} else {
this.didHide();
}
}));
}
didShow() {
atom.views.getView(atom.workspace).classList.add('find-visible');
if (this.tooltipSubscriptions) return;
this.tooltipSubscriptions = new CompositeDisposable(
atom.tooltips.add(this.refs.closeButton, {
title: 'Close Panel <span class="keystroke">Esc</span>',
html: true
}),
atom.tooltips.add(this.refs.regexOptionButton, {
title: "Use Regex",
keyBindingCommand: 'find-and-replace:toggle-regex-option',
keyBindingTarget: this.findEditor.element
}),
atom.tooltips.add(this.refs.caseOptionButton, {
title: "Match Case",
keyBindingCommand: 'find-and-replace:toggle-case-option',
keyBindingTarget: this.findEditor.element
}),
atom.tooltips.add(this.refs.selectionOptionButton, {
title: "Only In Selection",
keyBindingCommand: 'find-and-replace:toggle-selection-option',
keyBindingTarget: this.findEditor.element
}),
atom.tooltips.add(this.refs.wholeWordOptionButton, {
title: "Whole Word",
keyBindingCommand: 'find-and-replace:toggle-whole-word-option',
keyBindingTarget: this.findEditor.element
})
);
}
didHide() {
this.hideAllTooltips();
let workspaceElement = atom.views.getView(atom.workspace);
workspaceElement.focus();
workspaceElement.classList.remove('find-visible');
}
hideAllTooltips() {
this.tooltipSubscriptions.dispose();
this.tooltipSubscriptions = null;
}
handleEvents() {
this.findEditor.onDidStopChanging(() => this.liveSearch());
this.refs.nextButton.addEventListener('click', e => e.shiftKey ? this.findPrevious({focusEditorAfter: true}) : this.findNext({focusEditorAfter: true}));
this.refs.findAllButton.addEventListener('click', this.findAll.bind(this));
this.subscriptions.add(atom.commands.add('atom-workspace', {
'find-and-replace:find-next': () => this.findNext({focusEditorAfter: true}),
'find-and-replace:find-previous': () => this.findPrevious({focusEditorAfter: true}),
'find-and-replace:find-all': () => this.findAll({focusEditorAfter: true}),
'find-and-replace:find-next-selected': this.findNextSelected.bind(this),
'find-and-replace:find-previous-selected': this.findPreviousSelected.bind(this),
'find-and-replace:use-selection-as-find-pattern': this.setSelectionAsFindPattern.bind(this),
'find-and-replace:use-selection-as-replace-pattern': this.setSelectionAsReplacePattern.bind(this)
}));
this.refs.replaceNextButton.addEventListener('click', e => e.shiftKey ? this.replacePrevious() : this.replaceNext());
this.refs.replaceAllButton.addEventListener('click', this.replaceAll.bind(this));
this.subscriptions.add(atom.commands.add('atom-workspace', {
'find-and-replace:replace-previous': this.replacePrevious.bind(this),
'find-and-replace:replace-next': this.replaceNext.bind(this),
'find-and-replace:replace-all': this.replaceAll.bind(this),
}));
this.subscriptions.add(atom.commands.add(this.findEditor.element, {
'core:confirm': () => this.confirm(),
'find-and-replace:confirm': () => this.confirm(),
'find-and-replace:show-previous': () => this.showPrevious()
}));
this.subscriptions.add(atom.commands.add(this.replaceEditor.element, {
'core:confirm': () => this.replaceNext()
}));
this.subscriptions.add(atom.commands.add(this.element, {
'core:close': () => this.panel && this.panel.hide(),
'core:cancel': () => this.panel && this.panel.hide(),
'find-and-replace:focus-next': this.toggleFocus.bind(this),
'find-and-replace:focus-previous': this.toggleFocus.bind(this),
'find-and-replace:toggle-regex-option': this.toggleRegexOption.bind(this),
'find-and-replace:toggle-case-option': this.toggleCaseOption.bind(this),
'find-and-replace:toggle-selection-option': this.toggleSelectionOption.bind(this),
'find-and-replace:toggle-whole-word-option': this.toggleWholeWordOption.bind(this)
}));
this.subscriptions.add(this.model.onDidUpdate(this.markersUpdated.bind(this)));
this.subscriptions.add(this.model.onDidError(this.findError.bind(this)));
this.subscriptions.add(this.model.onDidChangeCurrentResult(this.updateResultCounter.bind(this)));
this.subscriptions.add(this.model.getFindOptions().onDidChange(this.updateOptionViews.bind(this)));
this.subscriptions.add(this.model.getFindOptions().onDidChangeUseRegex(this.updateSyntaxHighlighting.bind(this)));
this.refs.closeButton.addEventListener('click', () => this.panel && this.panel.hide());
this.refs.regexOptionButton.addEventListener('click', this.toggleRegexOption.bind(this));
this.refs.caseOptionButton.addEventListener('click', this.toggleCaseOption.bind(this));
this.refs.selectionOptionButton.addEventListener('click', this.toggleSelectionOption.bind(this));
this.refs.wholeWordOptionButton.addEventListener('click', this.toggleWholeWordOption.bind(this));
this.element.addEventListener('focus', () => this.findEditor.element.focus());
this.element.addEventListener('click', (e) => {
if (e.target.tagName === 'button') {
let workspaceElement = atom.views.getView(atom.workspace);
workspaceElement.focus();
}
});
}
focusFindEditor() {
const activeEditor = atom.workspace.getCenter().getActiveTextEditor()
let selectedText = activeEditor && activeEditor.getSelectedText()
if (selectedText && selectedText.indexOf('\n') < 0) {
if (this.model.getFindOptions().useRegex) {
selectedText = Util.escapeRegex(selectedText);
}
this.findEditor.setText(selectedText);
}
this.findEditor.element.focus();
this.findEditor.selectAll();
}
focusReplaceEditor() {
const activeEditor = atom.workspace.getCenter().getActiveTextEditor()
const selectedText = activeEditor && activeEditor.getSelectedText()
if (selectedText && selectedText.indexOf('\n') < 0) {
this.replaceEditor.setText(selectedText);
}
this.replaceEditor.getElement().focus();
this.replaceEditor.selectAll();
}
toggleFocus() {
if (this.findEditor.element.hasFocus()) {
this.replaceEditor.element.focus();
} else {
this.findEditor.element.focus();
}
}
confirm() {
this.findNext({focusEditorAfter: atom.config.get('find-and-replace.focusEditorAfterSearch')});
}
showPrevious() {
this.findPrevious({focusEditorAfter: atom.config.get('find-and-replace.focusEditorAfterSearch')});
}
liveSearch() {
let findPattern = this.findEditor.getText();
if (findPattern.length === 0 || (findPattern.length >= atom.config.get('find-and-replace.liveSearchMinimumCharacters') && !this.model.patternMatchesEmptyString(findPattern))) {
return this.model.search(findPattern);
}
}
search(findPattern, options) {
if (arguments.length === 1 && typeof findPattern === 'object') {
options = findPattern;
findPattern = null;
}
if (findPattern == null) { findPattern = this.findEditor.getText(); }
this.model.search(findPattern, options);
}
findAll(options = {focusEditorAfter: true}) {
this.findAndSelectResult(this.selectAllMarkers, options);
}
findNext(options = {focusEditorAfter: false}) {
this.findAndSelectResult(this.selectFirstMarkerAfterCursor, options);
}
findPrevious(options = {focusEditorAfter: false}) {
this.findAndSelectResult(this.selectFirstMarkerBeforeCursor, options);
}
findAndSelectResult(selectFunction, {focusEditorAfter, fieldToFocus}) {
this.search();
this.findHistoryCycler.store();
if (this.markers && this.markers.length > 0) {
selectFunction.call(this);
if (fieldToFocus) {
fieldToFocus.getElement().focus();
} else if (focusEditorAfter) {
let workspaceElement = atom.views.getView(atom.workspace);
workspaceElement.focus();
} else {
this.findEditor.getElement().focus();
}
} else {
atom.beep();
}
}
replaceNext() {
this.replace('findNext', 'firstMarkerIndexStartingFromCursor');
}
replacePrevious() {
this.replace('findPrevious', 'firstMarkerIndexBeforeCursor');
}
replace(nextOrPreviousFn, nextIndexFn) {
this.search();
this.findHistoryCycler.store();
this.replaceHistoryCycler.store();
if (this.markers && this.markers.length > 0) {
let currentMarker = this.model.currentResultMarker;
if (!currentMarker) {
let position = this[nextIndexFn]();
if (position) {
currentMarker = this.markers[position.index];
}
}
this.model.replace([currentMarker], this.replaceEditor.getText());
this[nextOrPreviousFn]({fieldToFocus: this.replaceEditor});
} else {
atom.beep();
}
}
replaceAll() {
this.search();
if (this.markers && this.markers.length > 0) {
this.findHistoryCycler.store();
this.replaceHistoryCycler.store();
this.model.replace(this.markers, this.replaceEditor.getText());
} else {
atom.beep();
}
}
markersUpdated(markers) {
this.markers = markers;
this.findError = null;
this.updateResultCounter();
this.updateFindEnablement();
this.updateReplaceEnablement();
if (this.model.getFindOptions().findPattern) {
let results = this.markers.length;
let resultsStr = results ? _.pluralize(results, 'result') : 'No results';
this.element.classList.remove('has-results', 'has-no-results');
this.element.classList.add(results ? 'has-results' : 'has-no-results');
this.setInfoMessage(`${resultsStr} found for '${this.model.getFindOptions().findPattern}'`);
if (this.findEditor.getElement().hasFocus() && results > 0 && atom.config.get('find-and-replace.scrollToResultOnLiveSearch')) {
this.findAndSelectResult(this.selectFirstMarkerStartingFromCursor, {focusEditorAfter: false});
}
} else {
this.clearMessage();
}
}
findError(error) {
this.setErrorMessage(error.message);
}
updateResultCounter() {
let text;
const index = this.markers && this.markers.indexOf(this.model.currentResultMarker);
if (index != null && index > -1) {
text = `${ index + 1} of ${this.markers.length}`;
} else {
if (this.markers == null || this.markers.length === 0) {
text = "no results";
} else if (this.markers.length === 1) {
text = "1 found";
} else {
text = `${this.markers.length} found`;
}
}
this.refs.resultCounter.textContent = text;
}
setInfoMessage(infoMessage) {
this.refs.descriptionLabel.textContent = infoMessage;
this.refs.descriptionLabel.classList.remove('text-error');
}
setErrorMessage(errorMessage) {
this.refs.descriptionLabel.textContent = errorMessage;
this.refs.descriptionLabel.classList.add('text-error');
}
clearMessage() {
this.element.classList.remove('has-results', 'has-no-results');
this.refs.descriptionLabel.innerHTML = 'Find in Current Buffer'
this.refs.descriptionLabel.classList.remove('text-error');
}
selectFirstMarkerAfterCursor() {
let marker = this.firstMarkerIndexAfterCursor();
if (!marker) { return; }
let {index, wrapped} = marker;
this.selectMarkerAtIndex(index, wrapped);
}
selectFirstMarkerStartingFromCursor() {
let marker = this.firstMarkerIndexAfterCursor(true);
if (!marker) { return; }
let {index, wrapped} = marker;
this.selectMarkerAtIndex(index, wrapped);
}
selectFirstMarkerBeforeCursor() {
let marker = this.firstMarkerIndexBeforeCursor();
if (!marker) { return; }
let {index, wrapped} = marker;
this.selectMarkerAtIndex(index, wrapped);
}
firstMarkerIndexStartingFromCursor() {
return this.firstMarkerIndexAfterCursor(true);
}
firstMarkerIndexAfterCursor(indexIncluded = false) {
const editor = this.model.getEditor();
if (!editor || !this.markers) return null;
const selection = editor.getLastSelection();
let {start, end} = selection.getBufferRange();
if (selection.isReversed()) start = end;
for (let index = 0, length = this.markers.length; index < length; index++) {
const marker = this.markers[index];
const markerStartPosition = marker.bufferMarker.getStartPosition();
switch (markerStartPosition.compare(start)) {
case -1:
continue;
case 0:
if (!indexIncluded) continue;
break;
}
return {index, wrapped: null};
}
return {index: 0, wrapped: 'up'};
}
firstMarkerIndexBeforeCursor() {
const editor = this.model.getEditor();
if (!editor) return null;
const selection = this.model.getEditor().getLastSelection();
let {start, end} = selection.getBufferRange();
if (selection.isReversed()) start = end;
for (let index = this.markers.length - 1; index >= 0; index--) {
const marker = this.markers[index];
const markerEndPosition = marker.bufferMarker.getEndPosition();
if (markerEndPosition.isLessThan(start)) {
return {index, wrapped: null};
}
}
return {index: this.markers.length - 1, wrapped: 'down'};
}
selectAllMarkers() {
if (!this.markers || this.markers.length === 0) return;
let ranges = (Array.from(this.markers).map((marker) => marker.getBufferRange()));
let scrollMarker = this.markers[this.firstMarkerIndexAfterCursor().index];
let editor = this.model.getEditor();
for(let range of ranges) {
editor.unfoldBufferRow(range.start.row);
}
editor.setSelectedBufferRanges(ranges, {flash: true});
editor.scrollToBufferPosition(scrollMarker.getStartBufferPosition(), {center: true});
}
selectMarkerAtIndex(markerIndex, wrapped) {
let marker;
if (!this.markers || this.markers.length === 0) return;
if (marker = this.markers[markerIndex]) {
let editor = this.model.getEditor();
let bufferRange = marker.getBufferRange();
let screenRange = marker.getScreenRange();
if (
screenRange.start.row < editor.getFirstVisibleScreenRow() ||
screenRange.end.row > editor.getLastVisibleScreenRow()
) {
switch (wrapped) {
case 'up':
this.showWrapIcon('icon-move-up');
break;
case 'down':
this.showWrapIcon('icon-move-down');
break;
}
}
editor.unfoldBufferRow(bufferRange.start.row);
editor.setSelectedBufferRange(bufferRange, {flash: true});
editor.scrollToCursorPosition({center: true});
}
}
setSelectionAsFindPattern() {
let editor = this.model.getEditor();
if (editor && editor.getSelectedText) {
let findPattern = editor.getSelectedText() || editor.getWordUnderCursor();
if (this.model.getFindOptions().useRegex) {
findPattern = Util.escapeRegex(findPattern);
}
if (findPattern) {
this.findEditor.setText(findPattern);
this.findEditor.getElement().focus();
this.findEditor.selectAll();
this.search();
}
}
}
setSelectionAsReplacePattern() {
let editor = this.model.getEditor();
if (editor && editor.getSelectedText) {
let replacePattern = editor.getSelectedText() || editor.getWordUnderCursor();
if (this.model.getFindOptions().useRegex) {
replacePattern = Util.escapeRegex(replacePattern);
}
if (replacePattern) {
this.replaceEditor.setText(replacePattern);
this.replaceEditor.getElement().focus();
this.replaceEditor.selectAll();
}
}
}
findNextSelected() {
this.setSelectionAsFindPattern();
this.findNext({focusEditorAfter: true});
}
findPreviousSelected() {
this.setSelectionAsFindPattern();
this.findPrevious({focusEditorAfter: true});
}
updateOptionViews() {
this.updateOptionButtons();
this.updateOptionsLabel();
}
updateSyntaxHighlighting() {
if (this.model.getFindOptions().useRegex) {
this.findEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp'));
this.replaceEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp.replacement'));
} else {
this.findEditor.setGrammar(atom.grammars.nullGrammar);
this.replaceEditor.setGrammar(atom.grammars.nullGrammar);
}
}
updateOptionsLabel() {
const label = [];
if (this.model.getFindOptions().useRegex) {
label.push('Regex');
}
if (this.model.getFindOptions().caseSensitive) {
label.push('Case Sensitive');
} else {
label.push('Case Insensitive');
}
if (this.model.getFindOptions().inCurrentSelection) {
label.push('Within Current Selection');
}
if (this.model.getFindOptions().wholeWord) {
label.push('Whole Word');
}
this.refs.optionsLabel.textContent = label.join(', ');
}
updateOptionButtons() {
this.setOptionButtonState(this.refs.regexOptionButton, this.model.getFindOptions().useRegex);
this.setOptionButtonState(this.refs.caseOptionButton, this.model.getFindOptions().caseSensitive);
this.setOptionButtonState(this.refs.selectionOptionButton, this.model.getFindOptions().inCurrentSelection);
this.setOptionButtonState(this.refs.wholeWordOptionButton, this.model.getFindOptions().wholeWord);
}
setOptionButtonState(optionButton, selected) {
if (selected) {
optionButton.classList.add('selected');
} else {
optionButton.classList.remove('selected');
}
}
anyMarkersAreSelected() {
let editor = this.model.getEditor();
if (editor) {
return editor.getSelectedBufferRanges().some(selectedRange => {
return this.model.findMarker(selectedRange);
});
}
}
toggleRegexOption() {
this.search({useRegex: !this.model.getFindOptions().useRegex});
if (!this.anyMarkersAreSelected()) {
this.selectFirstMarkerAfterCursor();
}
}
toggleCaseOption() {
this.search({caseSensitive: !this.model.getFindOptions().caseSensitive});
if (!this.anyMarkersAreSelected()) {
this.selectFirstMarkerAfterCursor();
}
}
toggleSelectionOption() {
this.search({inCurrentSelection: !this.model.getFindOptions().inCurrentSelection});
let editor = this.model.getEditor();
if (editor && editor.getSelectedBufferRanges().every(range => range.isEmpty())) {
this.selectFirstMarkerAfterCursor();
}
}
toggleWholeWordOption() {
this.search(this.findEditor.getText(), {wholeWord: !this.model.getFindOptions().wholeWord});
if (!this.anyMarkersAreSelected()) {
this.selectFirstMarkerAfterCursor();
}
}
updateFindEnablement() {
let editor = this.model.getEditor();
let isDisabled = this.refs.findAllButton.classList.contains('disabled');
let hadFindTooltip = !!this.findTooltipSubscriptions;
if (hadFindTooltip) this.findTooltipSubscriptions.dispose();
this.findTooltipSubscriptions = new CompositeDisposable;
if (editor && (!hadFindTooltip || isDisabled)) {
this.refs.findAllButton.classList.remove('disabled');
this.refs.nextButton.classList.remove('disabled');
this.findTooltipSubscriptions.add(atom.tooltips.add(this.refs.nextButton, {
title: "Find Next",
keyBindingCommand: 'find-and-replace:find-next',
keyBindingTarget: this.findEditor.element
}));
this.findTooltipSubscriptions.add(atom.tooltips.add(this.refs.findAllButton, {
title: "Find All",
keyBindingCommand: 'find-and-replace:find-all',
keyBindingTarget: this.findEditor.element
}));
} else if (!editor && (!hadFindTooltip || !isDisabled)) {
this.refs.findAllButton.classList.add('disabled');
this.refs.nextButton.classList.add('disabled');
this.findTooltipSubscriptions.add(atom.tooltips.add(this.refs.nextButton, {
title: "Find Next [when in a text document]"
}));
this.findTooltipSubscriptions.add(atom.tooltips.add(this.refs.findAllButton, {
title: "Find All [when in a text document]"
}));
}
}
updateReplaceEnablement() {
let canReplace = this.markers && this.markers.length > 0;
if (canReplace && !this.refs.replaceAllButton.classList.contains('disabled')) return;
if (this.replaceTooltipSubscriptions) this.replaceTooltipSubscriptions.dispose();
this.replaceTooltipSubscriptions = new CompositeDisposable;
if (canReplace) {
this.refs.replaceAllButton.classList.remove('disabled');
this.refs.replaceNextButton.classList.remove('disabled');
this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceNextButton, {
title: "Replace Next",
keyBindingCommand: 'find-and-replace:replace-next',
keyBindingTarget: this.replaceEditor.element
}));
this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
title: "Replace All",
keyBindingCommand: 'find-and-replace:replace-all',
keyBindingTarget: this.replaceEditor.element
}));
} else {
this.refs.replaceAllButton.classList.add('disabled');
this.refs.replaceNextButton.classList.add('disabled');
this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceNextButton, {
title: "Replace Next [when there are results]"
}));
this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
title: "Replace All [when there are results]"
}));
}
}
createWrapIcon() {
this.wrapIcon = document.createElement('div');
}
showWrapIcon(icon) {
if (!atom.config.get('find-and-replace.showSearchWrapIcon')) return;
let editor = this.model.getEditor();
if (!editor) return;
let editorView = atom.views.getView(editor);
if (!editorView.parentNode) return;
editorView.parentNode.appendChild(this.wrapIcon);
this.wrapIcon.className = `find-wrap-icon ${icon} visible`;
clearTimeout(this.wrapTimeout);
this.wrapTimeout = setTimeout((() => this.wrapIcon.classList.remove('visible')), 500);
}
};

View File

@ -0,0 +1,292 @@
const {CompositeDisposable, Disposable, TextBuffer} = require('atom');
const SelectNext = require('./select-next');
const {History, HistoryCycler} = require('./history');
const FindOptions = require('./find-options');
const BufferSearch = require('./buffer-search');
const getIconServices = require('./get-icon-services');
const FindView = require('./find-view');
const ProjectFindView = require('./project-find-view');
const ResultsModel = require('./project/results-model');
const ResultsPaneView = require('./project/results-pane');
module.exports = {
activate(param) {
// Convert old config setting for backward compatibility.
if (param == null) { param = {}; }
const {findOptions, findHistory, replaceHistory, pathsHistory} = param;
if (atom.config.get('find-and-replace.openProjectFindResultsInRightPane')) {
atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'right');
}
atom.config.unset('find-and-replace.openProjectFindResultsInRightPane');
atom.workspace.addOpener(function(filePath) {
if (filePath.indexOf(ResultsPaneView.URI) !== -1) { return new ResultsPaneView(); }
});
this.subscriptions = new CompositeDisposable;
this.currentItemSub = new Disposable;
this.findHistory = new History(findHistory);
this.replaceHistory = new History(replaceHistory);
this.pathsHistory = new History(pathsHistory);
this.findOptions = new FindOptions(findOptions);
this.findModel = new BufferSearch(this.findOptions);
this.resultsModel = new ResultsModel(this.findOptions);
this.subscriptions.add(atom.workspace.getCenter().observeActivePaneItem(paneItem => {
this.subscriptions.delete(this.currentItemSub);
this.currentItemSub.dispose();
if (atom.workspace.isTextEditor(paneItem)) {
return this.findModel.setEditor(paneItem);
} else if (paneItem?.observeEmbeddedTextEditor != null) {
this.currentItemSub = paneItem.observeEmbeddedTextEditor(editor => {
if (atom.workspace.getCenter().getActivePaneItem() === paneItem) {
return this.findModel.setEditor(editor);
}
});
return this.subscriptions.add(this.currentItemSub);
} else if (paneItem?.getEmbeddedTextEditor != null) {
return this.findModel.setEditor(paneItem.getEmbeddedTextEditor());
} else {
return this.findModel.setEditor(null);
}
})
);
this.subscriptions.add(atom.commands.add('.find-and-replace, .project-find', 'window:focus-next-pane', () => atom.views.getView(atom.workspace).focus())
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'project-find:show', () => {
this.createViews();
return showPanel(this.projectFindPanel, this.findPanel, () => this.projectFindView.focusFindElement());
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'project-find:toggle', () => {
this.createViews();
return togglePanel(this.projectFindPanel, this.findPanel, () => this.projectFindView.focusFindElement());
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'project-find:show-in-current-directory', ({target}) => {
this.createViews();
this.findPanel.hide();
this.projectFindPanel.show();
this.projectFindView.focusFindElement();
return this.projectFindView.findInCurrentlySelectedDirectory(target);
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'find-and-replace:use-selection-as-find-pattern', () => {
if (this.projectFindPanel?.isVisible() || this.findPanel?.isVisible()) { return; }
return this.createViews();
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'find-and-replace:use-selection-as-replace-pattern', () => {
if (this.projectFindPanel?.isVisible() || this.findPanel?.isVisible()) { return; }
return this.createViews();
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'find-and-replace:toggle', () => {
this.createViews();
return togglePanel(this.findPanel, this.projectFindPanel, () => this.findView.focusFindEditor());
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'find-and-replace:show', () => {
this.createViews();
return showPanel(this.findPanel, this.projectFindPanel, () => this.findView.focusFindEditor());
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'find-and-replace:show-replace', () => {
this.createViews();
return showPanel(this.findPanel, this.projectFindPanel, () => this.findView.focusReplaceEditor());
})
);
this.subscriptions.add(atom.commands.add('atom-workspace', 'find-and-replace:clear-history', () => {
this.findHistory.clear();
return this.replaceHistory.clear();
})
);
// Handling cancel in the workspace + code editors
const handleEditorCancel = ({target}) => {
const isMiniEditor = (target.tagName === 'ATOM-TEXT-EDITOR') && target.hasAttribute('mini');
if (!isMiniEditor) {
this.findPanel?.hide();
return this.projectFindPanel?.hide();
}
};
this.subscriptions.add(atom.commands.add('atom-workspace', {
'core:cancel': handleEditorCancel,
'core:close': handleEditorCancel
}
)
);
const selectNextObjectForEditorElement = editorElement => {
if (this.selectNextObjects == null) { this.selectNextObjects = new WeakMap(); }
const editor = editorElement.getModel();
let selectNext = this.selectNextObjects.get(editor);
if (selectNext == null) {
selectNext = new SelectNext(editor);
this.selectNextObjects.set(editor, selectNext);
}
return selectNext;
};
var showPanel = function(panelToShow, panelToHide, postShowAction) {
panelToHide.hide();
panelToShow.show();
return postShowAction?.();
};
var togglePanel = function(panelToToggle, panelToHide, postToggleAction) {
panelToHide.hide();
if (panelToToggle.isVisible()) {
return panelToToggle.hide();
} else {
panelToToggle.show();
return postToggleAction?.();
}
};
return this.subscriptions.add(atom.commands.add('.editor:not(.mini)', {
'find-and-replace:select-next'(event) {
return selectNextObjectForEditorElement(this).findAndSelectNext();
},
'find-and-replace:select-all'(event) {
return selectNextObjectForEditorElement(this).findAndSelectAll();
},
'find-and-replace:select-undo'(event) {
return selectNextObjectForEditorElement(this).undoLastSelection();
},
'find-and-replace:select-skip'(event) {
return selectNextObjectForEditorElement(this).skipCurrentSelection();
}
}
)
);
},
consumeElementIcons(service) {
getIconServices().setElementIcons(service);
return new Disposable(() => getIconServices().resetElementIcons());
},
consumeFileIcons(service) {
getIconServices().setFileIcons(service);
return new Disposable(() => getIconServices().resetFileIcons());
},
toggleAutocompletions(value) {
if ((this.findView == null)) {
return;
}
if (value) {
this.autocompleteSubscriptions = new CompositeDisposable;
const disposable = this.autocompleteWatchEditor?.(this.findView.findEditor, ['default']);
if (disposable != null) {
return this.autocompleteSubscriptions.add(disposable);
}
} else {
return this.autocompleteSubscriptions?.dispose();
}
},
consumeAutocompleteWatchEditor(watchEditor) {
this.autocompleteWatchEditor = watchEditor;
atom.config.observe(
'find-and-replace.autocompleteSearches',
value => this.toggleAutocompletions(value));
return new Disposable(() => {
this.autocompleteSubscriptions?.dispose();
return this.autocompleteWatchEditor = null;
});
},
provideService() {
return {resultsMarkerLayerForTextEditor: this.findModel.resultsMarkerLayerForTextEditor.bind(this.findModel)};
},
createViews() {
if (this.findView != null) { return; }
const findBuffer = new TextBuffer;
const replaceBuffer = new TextBuffer;
const pathsBuffer = new TextBuffer;
const findHistoryCycler = new HistoryCycler(findBuffer, this.findHistory);
const replaceHistoryCycler = new HistoryCycler(replaceBuffer, this.replaceHistory);
const pathsHistoryCycler = new HistoryCycler(pathsBuffer, this.pathsHistory);
const options = {findBuffer, replaceBuffer, pathsBuffer, findHistoryCycler, replaceHistoryCycler, pathsHistoryCycler};
this.findView = new FindView(this.findModel, options);
this.projectFindView = new ProjectFindView(this.resultsModel, options);
this.findPanel = atom.workspace.addBottomPanel({item: this.findView, visible: false, className: 'tool-panel panel-bottom'});
this.projectFindPanel = atom.workspace.addBottomPanel({item: this.projectFindView, visible: false, className: 'tool-panel panel-bottom'});
this.findView.setPanel(this.findPanel);
this.projectFindView.setPanel(this.projectFindPanel);
// HACK: Soooo, we need to get the model to the pane view whenever it is
// created. Creation could come from the opener below, or, more problematic,
// from a deserialize call when splitting panes. For now, all pane views will
// use this same model. This needs to be improved! I dont know the best way
// to deal with this:
// 1. How should serialization work in the case of a shared model.
// 2. Or maybe we create the model each time a new pane is created? Then
// ProjectFindView needs to know about each model so it can invoke a search.
// And on each new model, it will run the search again.
//
// See https://github.com/atom/find-and-replace/issues/63
// This makes projectFindView accesible in ResultsPaneView so that resultsModel
// can be properly set for ResultsPaneView instances and ProjectFindView instance
// as different pane views don't necessarily use same models anymore
// but most recent pane view and projectFindView do
ResultsPaneView.projectFindView = this.projectFindView;
return this.toggleAutocompletions(atom.config.get('find-and-replace.autocompleteSearches'));
},
deactivate() {
this.findPanel?.destroy();
this.findPanel = null;
this.findView?.destroy();
this.findView = null;
this.findModel?.destroy();
this.findModel = null;
this.projectFindPanel?.destroy();
this.projectFindPanel = null;
this.projectFindView?.destroy();
this.projectFindView = null;
ResultsPaneView.model = null;
this.autocompleteSubscriptions?.dispose();
this.autocompleteManagerService = null;
this.subscriptions?.dispose();
return this.subscriptions = null;
},
serialize() {
return {
findOptions: this.findOptions.serialize(),
findHistory: this.findHistory.serialize(),
replaceHistory: this.replaceHistory.serialize(),
pathsHistory: this.pathsHistory.serialize()
};
}
};

View File

@ -0,0 +1,66 @@
const DefaultFileIcons = require('./default-file-icons')
const {Emitter, Disposable, CompositeDisposable} = require('atom')
let iconServices
module.exports = function () {
if (!iconServices) iconServices = new IconServices()
return iconServices
}
class IconServices {
constructor () {
this.emitter = new Emitter()
this.elementIcons = null
this.elementIconDisposables = new CompositeDisposable()
this.fileIcons = DefaultFileIcons
}
onDidChange (callback) {
return this.emitter.on('did-change', callback)
}
resetElementIcons () {
this.setElementIcons(null)
}
resetFileIcons () {
this.setFileIcons(DefaultFileIcons)
}
setElementIcons (service) {
if (service !== this.elementIcons) {
if (this.elementIconDisposables != null) {
this.elementIconDisposables.dispose()
}
if (service) { this.elementIconDisposables = new CompositeDisposable() }
this.elementIcons = service
return this.emitter.emit('did-change')
}
}
setFileIcons (service) {
if (service !== this.fileIcons) {
this.fileIcons = service
return this.emitter.emit('did-change')
}
}
updateIcon (view, filePath) {
if (this.elementIcons) {
if (view.refs && view.refs.icon instanceof Element) {
if (view.iconDisposable) {
view.iconDisposable.dispose()
this.elementIconDisposables.remove(view.iconDisposable)
}
view.iconDisposable = this.elementIcons(view.refs.icon, filePath)
this.elementIconDisposables.add(view.iconDisposable)
}
} else {
let iconClass = this.fileIcons.iconClassForPath(filePath, 'find-and-replace') || ''
if (Array.isArray(iconClass)) {
iconClass = iconClass.join(' ')
}
view.refs.icon.className = iconClass + ' icon'
}
}
}

View File

@ -0,0 +1,76 @@
_ = require 'underscore-plus'
{Emitter} = require 'atom'
HISTORY_MAX = 25
class History
constructor: (@items=[]) ->
@emitter = new Emitter
@length = @items.length
onDidAddItem: (callback) ->
@emitter.on 'did-add-item', callback
serialize: ->
@items[-HISTORY_MAX..]
getLast: ->
_.last(@items)
getAtIndex: (index) ->
@items[index]
add: (text) ->
@items.push(text)
@length = @items.length
@emitter.emit 'did-add-item', text
clear: ->
@items = []
@length = 0
# Adds the ability to cycle through history
class HistoryCycler
# * `buffer` an {Editor} instance to attach the cycler to
# * `history` a {History} object
constructor: (@buffer, @history) ->
@index = @history.length
@history.onDidAddItem (text) =>
@buffer.setText(text) if text isnt @buffer.getText()
addEditorElement: (editorElement) ->
atom.commands.add editorElement,
'core:move-up': => @previous()
'core:move-down': => @next()
previous: ->
if @history.length is 0 or (@atLastItem() and @buffer.getText() isnt @history.getLast())
@scratch = @buffer.getText()
else if @index > 0
@index--
@buffer.setText @history.getAtIndex(@index) ? ''
next: ->
if @index < @history.length - 1
@index++
item = @history.getAtIndex(@index)
else if @scratch
item = @scratch
else
item = ''
@buffer.setText item
atLastItem: ->
@index is @history.length - 1
store: ->
text = @buffer.getText()
return if not text or text is @history.getLast()
@scratch = null
@history.add(text)
@index = @history.length - 1
module.exports = {History, HistoryCycler}

View File

@ -0,0 +1,566 @@
const fs = require('fs-plus');
const path = require('path');
const _ = require('underscore-plus');
const { TextEditor, Disposable, CompositeDisposable } = require('atom');
const etch = require('etch');
const Util = require('./project/util');
const ResultsModel = require('./project/results-model');
const ResultsPaneView = require('./project/results-pane');
const $ = etch.dom;
module.exports =
class ProjectFindView {
constructor(model, {findBuffer, replaceBuffer, pathsBuffer, findHistoryCycler, replaceHistoryCycler, pathsHistoryCycler}) {
this.model = model
this.findBuffer = findBuffer
this.replaceBuffer = replaceBuffer
this.pathsBuffer = pathsBuffer
this.findHistoryCycler = findHistoryCycler;
this.replaceHistoryCycler = replaceHistoryCycler;
this.pathsHistoryCycler = pathsHistoryCycler;
this.subscriptions = new CompositeDisposable()
this.modelSupbscriptions = new CompositeDisposable()
etch.initialize(this)
this.handleEvents();
this.findHistoryCycler.addEditorElement(this.findEditor.element);
this.replaceHistoryCycler.addEditorElement(this.replaceEditor.element);
this.pathsHistoryCycler.addEditorElement(this.pathsEditor.element);
this.onlyRunIfChanged = true;
this.clearMessages();
this.updateOptionViews();
this.updateSyntaxHighlighting();
}
update() {}
render() {
return (
$.div({tabIndex: -1, className: 'project-find padded'},
$.header({className: 'header'},
$.span({ref: 'closeButton', className: 'header-item close-button pull-right'},
$.i({className: "icon icon-x clickable"})
),
$.span({ref: 'descriptionLabel', className: 'header-item description'}),
$.span({className: 'header-item options-label pull-right'},
$.span({}, 'Finding with Options: '),
$.span({ref: 'optionsLabel', className: 'options'}),
$.span({className: 'btn-group btn-toggle btn-group-options'},
$.button({ref: 'regexOptionButton', className: 'btn option-regex'},
$.svg({className: "icon", innerHTML: `<use xlink:href="#find-and-replace-icon-regex" />`})
),
$.button({ref: 'caseOptionButton', className: 'btn option-case-sensitive'},
$.svg({className: "icon", innerHTML: `<use xlink:href="#find-and-replace-icon-case" />`})
),
$.button({ref: 'wholeWordOptionButton', className: 'btn option-whole-word'},
$.svg({className: "icon", innerHTML:`<use xlink:href="#find-and-replace-icon-word" />`})
)
)
)
),
$.section({ref: 'replacmentInfoBlock', className: 'input-block'},
$.progress({ref: 'replacementProgress', className: 'inline-block'}),
$.span({ref: 'replacmentInfo', className: 'inline-block'}, 'Replaced 2 files of 10 files')
),
$.section({className: 'input-block find-container'},
$.div({className: 'input-block-item input-block-item--flex editor-container'},
etch.dom(TextEditor, {
ref: 'findEditor',
mini: true,
placeholderText: 'Find in project',
buffer: this.findBuffer
})
),
$.div({className: 'input-block-item'},
$.div({className: 'btn-group btn-group-find'},
$.button({ref: 'findAllButton', className: 'btn'}, 'Find All')
)
)
),
$.section({className: 'input-block replace-container'},
$.div({className: 'input-block-item input-block-item--flex editor-container'},
etch.dom(TextEditor, {
ref: 'replaceEditor',
mini: true,
placeholderText: 'Replace in project',
buffer: this.replaceBuffer
})
),
$.div({className: 'input-block-item'},
$.div({className: 'btn-group btn-group-replace-all'},
$.button({ref: 'replaceAllButton', className: 'btn disabled'}, 'Replace All')
)
)
),
$.section({className: 'input-block paths-container'},
$.div({className: 'input-block-item editor-container'},
etch.dom(TextEditor, {
ref: 'pathsEditor',
mini: true,
placeholderText: 'File/directory pattern: For example `src` to search in the "src" directory; `*.js` to search all JavaScript files; `!src` to exclude the "src" directory; `!*.json` to exclude all JSON files',
buffer: this.pathsBuffer
})
)
)
)
);
}
get findEditor() { return this.refs.findEditor }
get replaceEditor() { return this.refs.replaceEditor }
get pathsEditor() { return this.refs.pathsEditor }
destroy() {
if (this.subscriptions) this.subscriptions.dispose();
if (this.tooltipSubscriptions) this.tooltipSubscriptions.dispose();
if (this.modelSupbscriptions) this.modelSupbscriptions.dispose();
this.model = null;
}
setPanel(panel) {
this.panel = panel;
this.subscriptions.add(this.panel.onDidChangeVisible(visible => {
if (visible) {
this.didShow();
} else {
this.didHide();
}
}));
}
didShow() {
atom.views.getView(atom.workspace).classList.add('find-visible');
if (this.tooltipSubscriptions != null) { return; }
this.updateReplaceAllButtonEnablement();
this.tooltipSubscriptions = new CompositeDisposable(
atom.tooltips.add(this.refs.closeButton, {
title: 'Close Panel <span class="keystroke">Esc</span>',
html: true
}),
atom.tooltips.add(this.refs.regexOptionButton, {
title: "Use Regex",
keyBindingCommand: 'project-find:toggle-regex-option',
keyBindingTarget: this.findEditor.element
}),
atom.tooltips.add(this.refs.caseOptionButton, {
title: "Match Case",
keyBindingCommand: 'project-find:toggle-case-option',
keyBindingTarget: this.findEditor.element
}),
atom.tooltips.add(this.refs.wholeWordOptionButton, {
title: "Whole Word",
keyBindingCommand: 'project-find:toggle-whole-word-option',
keyBindingTarget: this.findEditor.element
}),
atom.tooltips.add(this.refs.findAllButton, {
title: "Find All",
keyBindingCommand: 'find-and-replace:search',
keyBindingTarget: this.findEditor.element
})
);
}
didHide() {
this.hideAllTooltips();
let workspaceElement = atom.views.getView(atom.workspace);
workspaceElement.focus();
workspaceElement.classList.remove('find-visible');
}
hideAllTooltips() {
this.tooltipSubscriptions.dispose();
this.tooltipSubscriptions = null;
}
handleEvents() {
this.subscriptions.add(atom.commands.add('atom-workspace', {
'find-and-replace:use-selection-as-find-pattern': () => this.setSelectionAsFindPattern(),
'find-and-replace:use-selection-as-replace-pattern': () => this.setSelectionAsReplacePattern()
}));
this.subscriptions.add(atom.commands.add(this.element, {
'find-and-replace:focus-next': () => this.focusNextElement(1),
'find-and-replace:focus-previous': () => this.focusNextElement(-1),
'core:confirm': () => this.confirm(),
'core:close': () => this.panel && this.panel.hide(),
'core:cancel': () => this.panel && this.panel.hide(),
'project-find:confirm': () => this.confirm(),
'project-find:toggle-regex-option': () => this.toggleRegexOption(),
'project-find:toggle-case-option': () => this.toggleCaseOption(),
'project-find:toggle-whole-word-option': () => this.toggleWholeWordOption(),
'project-find:replace-all': () => this.replaceAll()
}));
let updateInterfaceForSearching = () => {
this.setInfoMessage('Searching...');
};
let updateInterfaceForResults = results => {
if (results.matchCount === 0 && results.findPattern === '') {
this.clearMessages();
} else {
this.generateResultsMessage(results);
}
this.updateReplaceAllButtonEnablement(results);
};
const resetInterface = () => {
this.clearMessages();
this.updateReplaceAllButtonEnablement(null);
};
this.handleEvents.resetInterface = resetInterface;
let afterSearch = () => {
if (atom.config.get('find-and-replace.closeFindPanelAfterSearch')) {
this.panel && this.panel.hide();
}
}
let searchFinished = results => {
afterSearch();
updateInterfaceForResults(results);
};
const addModelHandlers = () => {
this.modelSupbscriptions.add(this.model.onDidClear(resetInterface));
this.modelSupbscriptions.add(this.model.onDidClearReplacementState(updateInterfaceForResults));
this.modelSupbscriptions.add(this.model.onDidStartSearching(updateInterfaceForSearching));
this.modelSupbscriptions.add(this.model.onDidNoopSearch(afterSearch));
this.modelSupbscriptions.add(this.model.onDidFinishSearching(searchFinished));
this.modelSupbscriptions.add(this.model.getFindOptions().onDidChange(this.updateOptionViews.bind(this)));
this.modelSupbscriptions.add(this.model.getFindOptions().onDidChangeUseRegex(this.updateSyntaxHighlighting.bind(this)));
}
this.handleEvents.addModelHandlers=addModelHandlers;
addModelHandlers();
this.element.addEventListener('focus', () => this.findEditor.element.focus());
this.refs.closeButton.addEventListener('click', () => this.panel && this.panel.hide());
this.refs.regexOptionButton.addEventListener('click', () => this.toggleRegexOption());
this.refs.caseOptionButton.addEventListener('click', () => this.toggleCaseOption());
this.refs.wholeWordOptionButton.addEventListener('click', () => this.toggleWholeWordOption());
this.refs.replaceAllButton.addEventListener('click', () => this.replaceAll());
this.refs.findAllButton.addEventListener('click', () => this.search());
const focusCallback = () => this.onlyRunIfChanged = false;
window.addEventListener('focus', focusCallback);
this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', focusCallback)))
this.findEditor.getBuffer().onDidChange(() => {
this.updateReplaceAllButtonEnablement(this.model.getResultsSummary());
});
this.handleEventsForReplace();
}
handleEventsForReplace() {
this.replaceEditor.getBuffer().onDidChange(() => this.model.clearReplacementState());
this.replaceEditor.onDidStopChanging(() => this.model.getFindOptions().set({replacePattern: this.replaceEditor.getText()}));
this.replacementsMade = 0;
const addReplaceModelHandlers = () => {
this.modelSupbscriptions.add(this.model.onDidStartReplacing(promise => {
this.replacementsMade = 0;
this.refs.replacmentInfoBlock.style.display = '';
this.refs.replacementProgress.removeAttribute('value');
}));
this.modelSupbscriptions.add(this.model.onDidReplacePath(result => {
this.replacementsMade++;
this.refs.replacementProgress.value = this.replacementsMade / this.model.getPathCount();
this.refs.replacmentInfo.textContent = `Replaced ${this.replacementsMade} of ${_.pluralize(this.model.getPathCount(), 'file')}`;
}));
this.modelSupbscriptions.add(this.model.onDidFinishReplacing(result => this.onFinishedReplacing(result)));
}
this.handleEventsForReplace.addReplaceModelHandlers=addReplaceModelHandlers;
addReplaceModelHandlers();
}
focusNextElement(direction) {
const elements = [
this.findEditor.element,
this.replaceEditor.element,
this.pathsEditor.element
];
let focusedIndex = elements.findIndex(el => el.hasFocus()) + direction;
if (focusedIndex >= elements.length) focusedIndex = 0;
if (focusedIndex < 0) focusedIndex = elements.length - 1;
elements[focusedIndex].focus();
elements[focusedIndex].getModel().selectAll();
}
focusFindElement() {
const activeEditor = atom.workspace.getCenter().getActiveTextEditor();
let selectedText = activeEditor && activeEditor.getSelectedText()
if (selectedText && selectedText.indexOf('\n') < 0) {
if (this.model.getFindOptions().useRegex) {
selectedText = Util.escapeRegex(selectedText);
}
this.findEditor.setText(selectedText);
}
this.findEditor.getElement().focus();
this.findEditor.selectAll();
}
confirm() {
if (this.findEditor.getText().length === 0) {
this.model.clear();
return;
}
this.findHistoryCycler.store();
this.replaceHistoryCycler.store();
this.pathsHistoryCycler.store();
let searchPromise = this.search({onlyRunIfChanged: this.onlyRunIfChanged});
this.onlyRunIfChanged = true;
return searchPromise;
}
async search(options) {
// We always want to set the options passed in, even if we dont end up doing the search
if (options == null) { options = {}; }
this.model.getFindOptions().set(options);
let findPattern = this.findEditor.getText();
let pathsPattern = this.pathsEditor.getText();
let replacePattern = this.replaceEditor.getText();
let {onlyRunIfActive, onlyRunIfChanged} = options;
if ((onlyRunIfActive && !this.model.active) || !findPattern) return Promise.resolve();
await this.showResultPane()
try {
return await this.model.search(findPattern, pathsPattern, replacePattern, options);
} catch (e) {
this.setErrorMessage(e.message);
}
}
replaceAll() {
if (!this.model.matchCount) {
atom.beep();
return;
}
const findPattern = this.model.getLastFindPattern();
const currentPattern = this.findEditor.getText();
if (findPattern && findPattern !== currentPattern) {
atom.confirm({
message: `The searched pattern '${findPattern}' was changed to '${currentPattern}'`,
detailedMessage: `Please run the search with the new pattern '${currentPattern}' before running a replace-all`,
buttons: ['OK']
});
return;
}
return this.showResultPane().then(() => {
const pathsPattern = this.pathsEditor.getText();
const replacePattern = this.replaceEditor.getText();
const message = `This will replace '${findPattern}' with '${replacePattern}' ${_.pluralize(this.model.matchCount, 'time')} in ${_.pluralize(this.model.pathCount, 'file')}`;
const buttonChosen = atom.confirm({
message: 'Are you sure you want to replace all?',
detailedMessage: message,
buttons: ['OK', 'Cancel']
});
if (buttonChosen === 0) {
this.clearMessages();
return this.model.replace(pathsPattern, replacePattern, this.model.getPaths());
}
});
}
directoryPathForElement(element) {
const directoryElement = element.closest('.directory');
if (directoryElement) {
const pathElement = directoryElement.querySelector('[data-path]')
return pathElement && pathElement.dataset.path;
} else {
const activeEditor = atom.workspace.getCenter().getActiveTextEditor();
if (activeEditor) {
const editorPath = activeEditor.getPath()
if (editorPath) {
return path.dirname(editorPath);
}
}
}
}
findInCurrentlySelectedDirectory(selectedElement) {
const absolutePath = this.directoryPathForElement(selectedElement);
if (absolutePath) {
let [rootPath, relativePath] = atom.project.relativizePath(absolutePath);
if (rootPath && atom.project.getDirectories().length > 1) {
relativePath = path.join(path.basename(rootPath), relativePath);
}
this.pathsEditor.setText(relativePath);
this.findEditor.getElement().focus();
this.findEditor.selectAll();
}
}
showResultPane() {
let options = {searchAllPanes: true};
let openDirection = atom.config.get('find-and-replace.projectSearchResultsPaneSplitDirection');
if (openDirection !== 'none') { options.split = openDirection; }
return atom.workspace.open(ResultsPaneView.URI, options);
}
onFinishedReplacing(results) {
if (!results.replacedPathCount) atom.beep();
this.refs.replacmentInfoBlock.style.display = 'none';
}
generateResultsMessage(results) {
let message = Util.getSearchResultsMessage(results);
if (results.replacedPathCount != null) { message = Util.getReplacementResultsMessage(results); }
this.setInfoMessage(message);
}
clearMessages() {
this.element.classList.remove('has-results', 'has-no-results');
this.setInfoMessage('Find in Project');
this.refs.replacmentInfoBlock.style.display = 'none';
}
setInfoMessage(infoMessage) {
this.refs.descriptionLabel.innerHTML = infoMessage;
this.refs.descriptionLabel.classList.remove('text-error');
}
setErrorMessage(errorMessage) {
this.refs.descriptionLabel.innerHTML = errorMessage;
this.refs.descriptionLabel.classList.add('text-error');
}
updateReplaceAllButtonEnablement(results) {
const canReplace = results &&
results.matchCount &&
results.findPattern == this.findEditor.getText();
if (canReplace && !this.refs.replaceAllButton.classList.contains('disabled')) return;
if (this.replaceTooltipSubscriptions) this.replaceTooltipSubscriptions.dispose();
this.replaceTooltipSubscriptions = new CompositeDisposable;
if (canReplace) {
this.refs.replaceAllButton.classList.remove('disabled');
this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
title: "Replace All",
keyBindingCommand: 'project-find:replace-all',
keyBindingTarget: this.replaceEditor.element
}));
} else {
this.refs.replaceAllButton.classList.add('disabled');
this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
title: "Replace All [run a search to enable]"}
));
}
}
setSelectionAsFindPattern() {
const editor = atom.workspace.getCenter().getActivePaneItem();
if (editor && editor.getSelectedText) {
let pattern = editor.getSelectedText() || editor.getWordUnderCursor();
if (this.model.getFindOptions().useRegex) {
pattern = Util.escapeRegex(pattern);
}
if (pattern) {
this.findEditor.setText(pattern);
this.findEditor.getElement().focus();
this.findEditor.selectAll();
}
}
}
setSelectionAsReplacePattern() {
const editor = atom.workspace.getCenter().getActivePaneItem();
if (editor && editor.getSelectedText) {
let pattern = editor.getSelectedText() || editor.getWordUnderCursor();
if (this.model.getFindOptions().useRegex) {
pattern = Util.escapeRegex(pattern);
}
if (pattern) {
this.replaceEditor.setText(pattern);
this.replaceEditor.getElement().focus();
this.replaceEditor.selectAll();
}
}
}
updateOptionViews() {
this.updateOptionButtons();
this.updateOptionsLabel();
}
updateSyntaxHighlighting() {
if (this.model.getFindOptions().useRegex) {
this.findEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp'));
return this.replaceEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp.replacement'));
} else {
this.findEditor.setGrammar(atom.grammars.nullGrammar);
return this.replaceEditor.setGrammar(atom.grammars.nullGrammar);
}
}
updateOptionsLabel() {
const label = [];
if (this.model.getFindOptions().useRegex) {
label.push('Regex');
}
if (this.model.getFindOptions().caseSensitive) {
label.push('Case Sensitive');
} else {
label.push('Case Insensitive');
}
if (this.model.getFindOptions().wholeWord) {
label.push('Whole Word');
}
this.refs.optionsLabel.textContent = label.join(', ');
}
updateOptionButtons() {
this.setOptionButtonState(this.refs.regexOptionButton, this.model.getFindOptions().useRegex);
this.setOptionButtonState(this.refs.caseOptionButton, this.model.getFindOptions().caseSensitive);
this.setOptionButtonState(this.refs.wholeWordOptionButton, this.model.getFindOptions().wholeWord);
}
setOptionButtonState(optionButton, selected) {
if (selected) {
optionButton.classList.add('selected');
} else {
optionButton.classList.remove('selected');
}
}
toggleRegexOption() {
this.search({onlyRunIfActive: true, useRegex: !this.model.getFindOptions().useRegex});
}
toggleCaseOption() {
this.search({onlyRunIfActive: true, caseSensitive: !this.model.getFindOptions().caseSensitive});
}
toggleWholeWordOption() {
this.search({onlyRunIfActive: true, wholeWord: !this.model.getFindOptions().wholeWord});
}
};

View File

@ -0,0 +1,103 @@
const etch = require('etch');
const $ = etch.dom;
module.exports = class ListView {
constructor({items, heightForItem, itemComponent, className}) {
this.items = items;
this.heightForItem = heightForItem;
this.itemComponent = itemComponent;
this.className = className;
this.previousScrollTop = 0
this.previousClientHeight = 0
etch.initialize(this);
const resizeObserver = new ResizeObserver(() => etch.update(this));
resizeObserver.observe(this.element);
this.element.addEventListener('scroll', () => etch.update(this));
}
update({items, heightForItem, itemComponent, className} = {}) {
if (items) this.items = items;
if (heightForItem) this.heightForItem = heightForItem;
if (itemComponent) this.itemComponent = itemComponent;
if (className) this.className = className;
return etch.update(this)
}
render() {
const children = [];
let itemTopPosition = 0;
if (this.element) {
let {scrollTop, clientHeight} = this.element;
if (clientHeight > 0) {
this.previousScrollTop = scrollTop
this.previousClientHeight = clientHeight
} else {
scrollTop = this.previousScrollTop
clientHeight = this.previousClientHeight
}
const scrollBottom = scrollTop + clientHeight;
let i = 0;
for (; i < this.items.length; i++) {
let itemBottomPosition = itemTopPosition + this.heightForItem(this.items[i], i);
if (itemBottomPosition > scrollTop) break;
itemTopPosition = itemBottomPosition;
}
for (; i < this.items.length; i++) {
const item = this.items[i];
const itemHeight = this.heightForItem(this.items[i], i);
children.push(
$.div(
{
style: {
position: 'absolute',
height: `${itemHeight}px`,
width: '100%',
top: `${itemTopPosition}px`
},
key: i
},
etch.dom(this.itemComponent, {
item: item,
top: Math.max(0, scrollTop - itemTopPosition),
bottom: Math.min(itemHeight, scrollBottom - itemTopPosition)
})
)
);
itemTopPosition += itemHeight;
if (itemTopPosition >= scrollBottom) {
i++
break;
}
}
for (; i < this.items.length; i++) {
itemTopPosition += this.heightForItem(this.items[i], i);
}
}
return $.div(
{
className: 'results-view-container',
style: {
position: 'relative',
height: '100%',
overflow: 'auto',
}
},
$.ol(
{
ref: 'list',
className: this.className,
style: {height: `${itemTopPosition}px`}
},
...children
)
);
}
};

View File

@ -0,0 +1,286 @@
const getIconServices = require('../get-icon-services');
const { Range } = require('atom');
const {
LeadingContextRow,
TrailingContextRow,
ResultPathRow,
MatchRow,
ResultRowGroup
} = require('./result-row');
const {showIf} = require('./util');
const _ = require('underscore-plus');
const path = require('path');
const assert = require('assert');
const etch = require('etch');
const $ = etch.dom;
class ResultPathRowView {
constructor({groupData, isSelected}) {
const props = {groupData, isSelected};
this.props = Object.assign({}, props);
etch.initialize(this);
getIconServices().updateIcon(this, groupData.filePath);
}
destroy() {
return etch.destroy(this)
}
update({groupData, isSelected}) {
const props = {groupData, isSelected};
if (!_.isEqual(props, this.props)) {
this.props = Object.assign({}, props);
etch.update(this);
}
}
writeAfterUpdate() {
getIconServices().updateIcon(this, this.props.groupData.filePath);
}
render() {
let relativePath = this.props.groupData.filePath;
if (atom.project) {
let rootPath;
[rootPath, relativePath] = atom.project.relativizePath(this.props.groupData.filePath);
if (rootPath && atom.project.getDirectories().length > 1) {
relativePath = path.join(path.basename(rootPath), relativePath);
}
}
const groupData = this.props.groupData;
return (
$.li(
{
className: [
// This triggers the CSS displaying the "expand / collapse" arrows
// See `styles/lists.less` in the atom-ui repository for details
'list-nested-item',
groupData.isCollapsed ? 'collapsed' : '',
this.props.isSelected ? 'selected' : ''
].join(' ').trim(),
key: groupData.filePath
},
$.div(
{
className: 'list-item path-row',
dataset: { filePath: groupData.filePath }
},
$.span({
dataset: {name: path.basename(groupData.filePath)},
ref: 'icon',
className: 'icon'
}),
$.span({className: 'path-name bright'}, relativePath),
$.span(
{ref: 'description', className: 'path-match-number'},
`(${groupData.matchCount} match${groupData.matchCount === 1 ? '' : 'es'})`
)
)
)
)
}
};
class MatchRowView {
constructor({rowData, groupData, isSelected, replacePattern, regex}) {
const props = {rowData, groupData, isSelected, replacePattern, regex};
const previewData = {matches: rowData.matches, replacePattern, regex};
this.props = Object.assign({}, props);
this.previewData = previewData;
this.previewNode = this.generatePreviewNode(previewData);
etch.initialize(this);
}
update({rowData, groupData, isSelected, replacePattern, regex}) {
const props = {rowData, groupData, isSelected, replacePattern, regex};
const previewData = {matches: rowData.matches, replacePattern, regex};
if (!_.isEqual(props, this.props)) {
if (!_.isEqual(previewData, this.previewData)) {
this.previewData = previewData;
this.previewNode = this.generatePreviewNode(previewData);
}
this.props = Object.assign({}, props);
etch.update(this);
}
}
generatePreviewNode({matches, replacePattern, regex}) {
const subnodes = [];
let prevMatchEnd = matches[0].lineTextOffset;
for (const match of matches) {
const range = Range.fromObject(match.range);
const prefixStart = Math.max(0, prevMatchEnd - match.lineTextOffset);
const matchStart = range.start.column - match.lineTextOffset;
// TODO - Handle case where (prevMatchEnd < match.lineTextOffset)
// The solution probably needs Workspace.scan to be reworked to account
// for multiple matches lines first
const prefix = match.lineText.slice(prefixStart, matchStart);
let replacementText = ''
if (replacePattern && regex) {
replacementText = match.matchText.replace(regex, replacePattern);
} else if (replacePattern) {
replacementText = replacePattern;
}
subnodes.push(
$.span({}, prefix),
$.span(
{
className:
`match ${replacementText ? 'highlight-error' : 'highlight-info'}`
},
match.matchText
),
$.span(
{
className: 'replacement highlight-success',
style: showIf(replacementText)
},
replacementText
)
);
prevMatchEnd = range.end.column;
}
const lastMatch = matches[matches.length - 1];
const suffix = lastMatch.lineText.slice(
prevMatchEnd - lastMatch.lineTextOffset
);
return $.span(
{className: 'preview'},
...subnodes,
$.span({}, suffix)
);
}
render() {
return (
$.li(
{
className: [
'list-item',
'match-row',
this.props.isSelected ? 'selected' : '',
this.props.rowData.separator ? 'separator' : ''
].join(' ').trim(),
dataset: {
filePath: this.props.groupData.filePath,
matchLineNumber: this.props.rowData.lineNumber,
}
},
$.span(
{className: 'line-number text-subtle'},
this.props.rowData.lineNumber + 1
),
this.previewNode
)
);
}
};
class ContextRowView {
constructor({rowData, groupData, isSelected}) {
const props = {rowData, groupData, isSelected};
this.props = Object.assign({}, props);
etch.initialize(this);
}
destroy() {
return etch.destroy(this)
}
update({rowData, groupData, isSelected}) {
const props = {rowData, groupData, isSelected};
if (!_.isEqual(props, this.props)) {
this.props = Object.assign({}, props);
etch.update(this);
}
}
render() {
return (
$.li(
{
className: [
'list-item',
'context-row',
this.props.rowData.separator ? 'separator' : ''
].join(' ').trim(),
dataset: {
filePath: this.props.groupData.filePath,
matchLineNumber: this.props.rowData.matchLineNumber
},
},
$.span({className: 'line-number text-subtle'}, this.props.rowData.lineNumber + 1),
$.span({className: 'preview'}, $.span({}, this.props.rowData.line))
)
)
}
}
function getRowViewType(row) {
if (row instanceof ResultPathRow) {
return ResultPathRowView;
}
if (row instanceof MatchRow) {
return MatchRowView;
}
if (row instanceof LeadingContextRow) {
return ContextRowView;
}
if (row instanceof TrailingContextRow) {
return ContextRowView;
}
assert(false);
}
module.exports =
class ResultRowView {
constructor({item}) {
const props = {
rowData: Object.assign({}, item.row.data),
groupData: Object.assign({}, item.row.group.data),
isSelected: item.isSelected,
replacePattern: item.replacePattern,
regex: item.regex
};
this.props = props;
this.rowViewType = getRowViewType(item.row);
etch.initialize(this);
}
destroy() {
return etch.destroy(this);
}
update({item}) {
const props = {
rowData: Object.assign({}, item.row.data),
groupData: Object.assign({}, item.row.group.data),
isSelected: item.isSelected,
replacePattern: item.replacePattern,
regex: item.regex
}
this.props = props;
this.rowViewType = getRowViewType(item.row);
etch.update(this);
}
render() {
return $(this.rowViewType, this.props);
}
};

View File

@ -0,0 +1,154 @@
const {Range} = require('atom');
class LeadingContextRow {
constructor(rowGroup, line, separator, matchLineNumber, rowOffset) {
this.group = rowGroup
this.rowOffset = rowOffset
// props
this.data = {
separator,
line,
lineNumber: matchLineNumber - rowOffset,
matchLineNumber,
}
}
}
class TrailingContextRow {
constructor(rowGroup, line, separator, matchLineNumber, rowOffset) {
this.group = rowGroup
this.rowOffset = rowOffset
// props
this.data = {
separator,
line,
lineNumber: matchLineNumber + rowOffset,
matchLineNumber,
}
}
}
class ResultPathRow {
constructor(rowGroup) {
this.group = rowGroup
// props
this.data = {
separator: false,
}
}
}
class MatchRow {
constructor(rowGroup, separator, lineNumber, matches) {
this.group = rowGroup
// props
this.data = {
separator,
lineNumber,
matchLineNumber: lineNumber,
matches,
}
}
}
class ResultRowGroup {
constructor(result, findOptions) {
this.data = { isCollapsed: false }
this.setResult(result)
this.rows = []
this.collapsedRows = []
this.generateRows(findOptions)
this.previousRowCount = this.rows.length
}
setResult(result) {
this.result = result
this.data = {
filePath: result.filePath,
matchCount: result.matches.length,
isCollapsed: this.data.isCollapsed,
}
}
generateRows(findOptions) {
const { leadingContextLineCount, trailingContextLineCount } = findOptions
this.leadingContextLineCount = leadingContextLineCount
this.trailingContextLineCount = trailingContextLineCount
let rowArrays = [ [new ResultPathRow(this)] ]
// This loop accumulates the match lines and the context lines of the
// result; the added complexity comes from the fact that there musn't be
// context lines between adjacent match lines
let prevMatch = null
let prevMatchRow = null
let prevLineNumber
for (const match of this.result.matches) {
const { leadingContextLines } = match
const lineNumber = Range.fromObject(match.range).start.row
let leadCount
if (prevMatch) {
const interval = Math.max(lineNumber - prevLineNumber - 1, 0)
const trailCount = Math.min(trailingContextLineCount, interval)
const { trailingContextLines } = prevMatch
rowArrays.push(
trailingContextLines.slice(0, trailCount).map((line, i) => (
new TrailingContextRow(this, line, false, prevLineNumber, i + 1)
))
)
leadCount = Math.min(leadingContextLineCount, interval - trailCount)
} else {
leadCount = Math.min(leadingContextLineCount, leadingContextLines.length)
}
rowArrays.push(
leadingContextLines.slice(leadingContextLines.length - leadCount).map((line, i) => (
new LeadingContextRow(this, line, false, lineNumber, leadCount - i)
))
)
if (prevMatchRow && lineNumber === prevLineNumber) {
prevMatchRow.data.matches.push(match)
} else {
prevMatchRow = new MatchRow(this, false, lineNumber, [match])
rowArrays.push([ prevMatchRow ])
}
prevMatch = match
prevLineNumber = lineNumber
}
const { trailingContextLines } = prevMatch
rowArrays.push(
trailingContextLines.slice(0, trailingContextLineCount).map((line, i) => (
new TrailingContextRow(this, line, false, prevLineNumber, i + 1)
))
)
this.rows = [].concat(...rowArrays)
this.collapsedRows = [ this.rows[0] ]
let prevRow = null
for (const row of this.rows) {
row.data.separator = (
prevRow &&
row.data.lineNumber != null && prevRow.data.lineNumber != null &&
row.data.lineNumber > prevRow.data.lineNumber + 1
) ? true : false
prevRow = row
}
}
displayedRows() {
return this.data.isCollapsed ? this.collapsedRows : this.rows
}
}
module.exports = {
LeadingContextRow,
TrailingContextRow,
ResultPathRow,
MatchRow,
ResultRowGroup
}

View File

@ -0,0 +1,388 @@
const _ = require('underscore-plus')
const {Emitter, TextEditor, Range} = require('atom')
const escapeHelper = require('../escape-helper')
class Result {
static create (result) {
if (result && result.matches && result.matches.length) {
const matches = []
for (const m of result.matches) {
const range = Range.fromObject(m.range)
const matchSplit = m.matchText.split('\n')
const linesSplit = m.lineText.split('\n')
// If the result spans across multiple lines, process each of
// them separately by creating separate `matches` objects for
// each line on the match.
for (let row = range.start.row; row <= range.end.row; row++) {
const lineText = linesSplit[row - range.start.row]
const matchText = matchSplit[row - range.start.row]
// When receiving multiline results from opened buffers, only
// the first result line is provided on the `lineText` property.
// This makes it impossible to properly render the part of the result
// that's part of other lines.
// In order to prevent an error we just need to ignore these parts.
if (lineText === undefined || matchText === undefined) {
continue
}
// Adapt the range column number based on which line we're at:
// - the first line of a multiline result will always start at the range start
// and will end at the end of the line.
// - middle lines will start at 0 and end at the end of the line
// - last line will start at 0 and end at the range end.
const startColumn = row === range.start.row ? range.start.column : 0
const endColumn = row === range.end.row ? range.end.column : lineText.length
matches.push({
matchText,
lineText,
lineTextOffset: m.lineTextOffset,
range: {
start: {
row,
column: startColumn
},
end: {
row,
column: endColumn
}
},
leadingContextLines: m.leadingContextLines,
trailingContextLines: m.trailingContextLines
})
}
}
return new Result({filePath: result.filePath, matches})
} else {
return null
}
}
constructor (result) {
_.extend(this, result)
}
}
module.exports = class ResultsModel {
constructor (findOptions) {
this.onContentsModified = this.onContentsModified.bind(this)
this.findOptions = findOptions
this.emitter = new Emitter()
atom.workspace.getCenter().observeActivePaneItem(item => {
if (item instanceof TextEditor && atom.project.contains(item.getPath())) {
item.onDidStopChanging(() => this.onContentsModified(item))
}
})
this.clear()
}
onDidClear (callback) {
return this.emitter.on('did-clear', callback)
}
onDidClearSearchState (callback) {
return this.emitter.on('did-clear-search-state', callback)
}
onDidClearReplacementState (callback) {
return this.emitter.on('did-clear-replacement-state', callback)
}
onDidSearchPaths (callback) {
return this.emitter.on('did-search-paths', callback)
}
onDidErrorForPath (callback) {
return this.emitter.on('did-error-for-path', callback)
}
onDidNoopSearch (callback) {
return this.emitter.on('did-noop-search', callback)
}
onDidStartSearching (callback) {
return this.emitter.on('did-start-searching', callback)
}
onDidCancelSearching (callback) {
return this.emitter.on('did-cancel-searching', callback)
}
onDidFinishSearching (callback) {
return this.emitter.on('did-finish-searching', callback)
}
onDidStartReplacing (callback) {
return this.emitter.on('did-start-replacing', callback)
}
onDidFinishReplacing (callback) {
return this.emitter.on('did-finish-replacing', callback)
}
onDidSearchPath (callback) {
return this.emitter.on('did-search-path', callback)
}
onDidReplacePath (callback) {
return this.emitter.on('did-replace-path', callback)
}
onDidAddResult (callback) {
return this.emitter.on('did-add-result', callback)
}
onDidSetResult (callback) {
return this.emitter.on('did-set-result', callback)
}
onDidRemoveResult (callback) {
return this.emitter.on('did-remove-result', callback)
}
clear () {
this.clearSearchState()
this.clearReplacementState()
this.emitter.emit('did-clear', this.getResultsSummary())
}
clearSearchState () {
this.pathCount = 0
this.matchCount = 0
this.regex = null
this.results = {}
this.active = false
this.searchErrors = null
if (this.inProgressSearchPromise != null) {
this.inProgressSearchPromise.cancel()
this.inProgressSearchPromise = null
}
this.emitter.emit('did-clear-search-state', this.getResultsSummary())
}
clearReplacementState () {
this.replacePattern = null
this.replacedPathCount = null
this.replacementCount = null
this.replacementErrors = null
this.emitter.emit('did-clear-replacement-state', this.getResultsSummary())
}
shouldRerunSearch (findPattern, pathsPattern, options = {}) {
return (
!options.onlyRunIfChanged ||
findPattern == null ||
findPattern !== this.lastFindPattern ||
pathsPattern == null ||
pathsPattern !== this.lastPathsPattern
)
}
async search (findPattern, pathsPattern, replacePattern, options = {}) {
if (!this.shouldRerunSearch(findPattern, pathsPattern, options)) {
this.emitter.emit('did-noop-search')
return Promise.resolve()
}
const {keepReplacementState} = options
if (keepReplacementState) {
this.clearSearchState()
} else {
this.clear()
}
this.lastFindPattern = findPattern
this.lastPathsPattern = pathsPattern
this.findOptions.set(_.extend({findPattern, replacePattern, pathsPattern}, options))
this.regex = this.findOptions.getFindPatternRegex()
this.active = true
const searchPaths = this.pathsArrayFromPathsPattern(pathsPattern)
const onPathsSearched = numberOfPathsSearched => {
this.emitter.emit('did-search-paths', numberOfPathsSearched)
}
const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore')
const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter')
const startTime = Date.now()
const useRipgrep = atom.config.get('find-and-replace.useRipgrep')
const enablePCRE2 = atom.config.get('find-and-replace.enablePCRE2')
this.inProgressSearchPromise = atom.workspace.scan(
this.regex,
{
paths: searchPaths,
onPathsSearched,
leadingContextLineCount,
ripgrep: useRipgrep,
PCRE2: enablePCRE2,
trailingContextLineCount
},
(result, error) => {
if (result) {
this.setResult(result.filePath, Result.create(result))
} else {
if (this.searchErrors == null) { this.searchErrors = [] }
this.searchErrors.push(error)
this.emitter.emit('did-error-for-path', error)
}
})
this.emitter.emit('did-start-searching', this.inProgressSearchPromise)
const message = await this.inProgressSearchPromise
if (message === 'cancelled') {
this.emitter.emit('did-cancel-searching')
} else {
const resultsSummary = this.getResultsSummary()
this.inProgressSearchPromise = null
this.emitter.emit('did-finish-searching', resultsSummary)
}
}
replace (pathsPattern, replacePattern, replacementPaths) {
if (!this.findOptions.findPattern || (this.regex == null)) { return }
this.findOptions.set({replacePattern, pathsPattern})
if (this.findOptions.useRegex) { replacePattern = escapeHelper.unescapeEscapeSequence(replacePattern) }
this.active = false // not active until the search is finished
this.replacedPathCount = 0
this.replacementCount = 0
const promise = atom.workspace.replace(this.regex, replacePattern, replacementPaths, (result, error) => {
if (result) {
if (result.replacements) {
this.replacedPathCount++
this.replacementCount += result.replacements
}
this.emitter.emit('did-replace-path', result)
} else {
if (this.replacementErrors == null) { this.replacementErrors = [] }
this.replacementErrors.push(error)
this.emitter.emit('did-error-for-path', error)
}
})
this.emitter.emit('did-start-replacing', promise)
return promise.then(() => {
this.emitter.emit('did-finish-replacing', this.getResultsSummary())
return this.search(this.findOptions.findPattern, this.findOptions.pathsPattern,
this.findOptions.replacePattern, {keepReplacementState: true})
}).catch(e => console.error(e.stack))
}
setActive (isActive) {
if ((isActive && this.findOptions.findPattern) || !isActive) {
this.active = isActive
}
}
getActive () { return this.active }
getFindOptions () { return this.findOptions }
getLastFindPattern () { return this.lastFindPattern }
getResultsSummary () {
const findPattern = this.lastFindPattern != null ? this.lastFindPattern : this.findOptions.findPattern
const { replacePattern } = this.findOptions
return {
findPattern,
replacePattern,
pathCount: this.pathCount,
matchCount: this.matchCount,
searchErrors: this.searchErrors,
replacedPathCount: this.replacedPathCount,
replacementCount: this.replacementCount,
replacementErrors: this.replacementErrors
}
}
getPathCount () {
return this.pathCount
}
getMatchCount () {
return this.matchCount
}
getPaths () {
return Object.keys(this.results)
}
getResult (filePath) {
return this.results[filePath]
}
setResult (filePath, result) {
if (result == null) {
return this.removeResult(filePath)
}
if (!this.results[filePath]) {
return this.addResult(filePath, result)
}
this.matchCount += result.matches.length - this.results[filePath].matches.length
this.results[filePath] = result
this.emitter.emit('did-set-result', {filePath, result})
}
addResult (filePath, result) {
this.pathCount++
this.matchCount += result.matches.length
this.results[filePath] = result
this.emitter.emit('did-add-result', {filePath, result})
}
removeResult (filePath) {
if (!this.results[filePath]) {
return
}
this.pathCount--
this.matchCount -= this.results[filePath].matches.length
const result = this.results[filePath]
delete this.results[filePath]
this.emitter.emit('did-remove-result', {filePath, result})
}
onContentsModified (editor) {
if (!this.active || !this.regex || !editor.getPath()) { return }
const matches = []
const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore')
const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter')
editor.scan(this.regex,
{leadingContextLineCount, trailingContextLineCount},
(match) => matches.push(match)
)
const result = Result.create({filePath: editor.getPath(), matches})
this.setResult(editor.getPath(), result)
this.emitter.emit('did-finish-searching', this.getResultsSummary())
}
pathsArrayFromPathsPattern (pathsPattern) {
return pathsPattern.trim().split(',').map((inputPath) => inputPath.trim())
}
}
// Exported for tests
module.exports.Result = Result

View File

@ -0,0 +1,362 @@
const _ = require('underscore-plus');
const {CompositeDisposable} = require('atom');
const ResultsView = require('./results-view');
const ResultsModel = require('./results-model');
const {showIf, getSearchResultsMessage, escapeHtml} = require('./util');
const etch = require('etch');
const $ = etch.dom;
module.exports =
class ResultsPaneView {
constructor() {
this.model = ResultsPaneView.projectFindView.model;
this.model.setActive(true);
this.isLoading = false;
this.searchErrors = [];
this.searchResults = null;
this.searchingIsSlow = false;
this.numberOfPathsSearched = 0;
this.searchContextLineCountBefore = 0;
this.searchContextLineCountAfter = 0;
this.uri = ResultsPaneView.URI;
etch.initialize(this);
this.onFinishedSearching(this.model.getResultsSummary());
this.element.addEventListener('focus', this.focused.bind(this));
this.element.addEventListener('click', event => {
switch (event.target) {
case this.refs.collapseAll:
this.collapseAllResults();
break;
case this.refs.expandAll:
this.expandAllResults();
break;
case this.refs.decrementLeadingContextLines:
this.decrementLeadingContextLines();
break;
case this.refs.toggleLeadingContextLines:
this.toggleLeadingContextLines();
break;
case this.refs.incrementLeadingContextLines:
this.incrementLeadingContextLines();
break;
case this.refs.decrementTrailingContextLines:
this.decrementTrailingContextLines();
break;
case this.refs.toggleTrailingContextLines:
this.toggleTrailingContextLines();
break;
case this.refs.incrementTrailingContextLines:
this.incrementTrailingContextLines();
case this.refs.dontOverrideTab:
this.dontOverrideTab();
break;
}
})
this.subscriptions = new CompositeDisposable(
this.model.onDidStartSearching(this.onSearch.bind(this)),
this.model.onDidFinishSearching(this.onFinishedSearching.bind(this)),
this.model.onDidClear(this.onCleared.bind(this)),
this.model.onDidClearReplacementState(this.onReplacementStateCleared.bind(this)),
this.model.onDidSearchPaths(this.onPathsSearched.bind(this)),
this.model.onDidErrorForPath(error => this.appendError(error.message)),
atom.config.observe('find-and-replace.searchContextLineCountBefore', this.searchContextLineCountChanged.bind(this)),
atom.config.observe('find-and-replace.searchContextLineCountAfter', this.searchContextLineCountChanged.bind(this))
);
}
update() {}
destroy() {
this.model.setActive(false);
this.subscriptions.dispose();
if(this.separatePane)
this.model = null;
}
render() {
const matchCount = this.searchResults && this.searchResults.matchCount;
return (
$.div(
{
tabIndex: -1,
className: `preview-pane pane-item ${matchCount === 0 ? 'no-results' : ''}`,
},
$.div({className: 'preview-header'},
$.span({
ref: 'previewCount',
className: 'preview-count inline-block',
innerHTML: this.isLoading
? 'Searching...'
: (getSearchResultsMessage(this.searchResults) || 'Project search results')
}),
$.button(
{
ref: 'dontOverrideTab',
style: {display: matchCount == 0 || this.isLoading ? 'none' : ''},
className: 'btn'
}, "Don't override this tab"),
$.div(
{
ref: 'previewControls',
className: 'preview-controls',
style: {display: matchCount > 0 ? '' : 'none'}
},
this.searchContextLineCountBefore > 0 ?
$.div({className: 'btn-group'},
$.button(
{
ref: 'decrementLeadingContextLines',
className: 'btn' + (this.model.getFindOptions().leadingContextLineCount === 0 ? ' disabled' : '')
}, '-'),
$.button(
{
ref: 'toggleLeadingContextLines',
className: 'btn'
},
$.svg(
{
className: 'icon',
innerHTML: '<use xlink:href="#find-and-replace-context-lines-before" />'
}
)
),
$.button(
{
ref: 'incrementLeadingContextLines',
className: 'btn' + (this.model.getFindOptions().leadingContextLineCount >= this.searchContextLineCountBefore ? ' disabled' : '')
}, '+')
) : null,
this.searchContextLineCountAfter > 0 ?
$.div({className: 'btn-group'},
$.button(
{
ref: 'decrementTrailingContextLines',
className: 'btn' + (this.model.getFindOptions().trailingContextLineCount === 0 ? ' disabled' : '')
}, '-'),
$.button(
{
ref: 'toggleTrailingContextLines',
className: 'btn'
},
$.svg(
{
className: 'icon',
innerHTML: '<use xlink:href="#find-and-replace-context-lines-after" />'
}
)
),
$.button(
{
ref: 'incrementTrailingContextLines',
className: 'btn' + (this.model.getFindOptions().trailingContextLineCount >= this.searchContextLineCountAfter ? ' disabled' : '')
}, '+')
) : null,
$.div({className: 'btn-group'},
$.button({ref: 'collapseAll', className: 'btn'}, 'Collapse All'),
$.button({ref: 'expandAll', className: 'btn'}, 'Expand All')
)
),
$.div({className: 'inline-block', style: showIf(this.isLoading)},
$.div({className: 'loading loading-spinner-tiny inline-block'}),
$.div(
{
className: 'inline-block',
style: showIf(this.isLoading && this.searchingIsSlow)
},
$.span({ref: 'searchedCount', className: 'searched-count'},
this.numberOfPathsSearched.toString()
),
$.span({}, ' paths searched')
)
)
),
$.ul(
{
ref: 'errorList',
className: 'error-list list-group padded',
style: showIf(this.searchErrors.length > 0)
},
...this.searchErrors.map(message =>
$.li({className: 'text-error'}, escapeHtml(message))
)
),
etch.dom(ResultsView, {ref: 'resultsView', model: this.model}),
$.ul(
{
className: 'centered background-message no-results-overlay',
style: showIf(matchCount === 0)
},
$.li({}, 'No Results')
)
)
);
}
copy() {
return new ResultsPaneView();
}
getTitle() {
return 'Project Find Results';
}
getIconName() {
return 'search';
}
getURI() {
return this.uri;
}
focused() {
this.refs.resultsView.element.focus();
}
appendError(message) {
this.searchErrors.push(message)
etch.update(this);
}
onSearch(searchPromise) {
this.isLoading = true;
this.searchingIsSlow = false;
this.numberOfPathsSearched = 0;
setTimeout(() => {
this.searchingIsSlow = true;
etch.update(this);
}, 500);
etch.update(this);
let stopLoading = () => {
this.isLoading = false;
etch.update(this);
};
return searchPromise.then(stopLoading, stopLoading);
}
onPathsSearched(numberOfPathsSearched) {
this.numberOfPathsSearched = numberOfPathsSearched;
etch.update(this);
}
onFinishedSearching(results) {
this.searchResults = results;
if (results.searchErrors || results.replacementErrors) {
this.searchErrors =
_.pluck(results.replacementErrors, 'message')
.concat(_.pluck(results.searchErrors, 'message'));
} else {
this.searchErrors = [];
}
etch.update(this);
}
onReplacementStateCleared(results) {
this.searchResults = results;
this.searchErrors = [];
etch.update(this);
}
onCleared() {
this.isLoading = false;
this.searchErrors = [];
this.searchResults = {};
this.searchingIsSlow = false;
this.numberOfPathsSearched = 0;
etch.update(this);
}
collapseAllResults() {
this.refs.resultsView.collapseAllResults();
this.refs.resultsView.element.focus();
}
expandAllResults() {
this.refs.resultsView.expandAllResults();
this.refs.resultsView.element.focus();
}
decrementLeadingContextLines() {
this.refs.resultsView.decrementLeadingContextLines();
etch.update(this);
}
toggleLeadingContextLines() {
this.refs.resultsView.toggleLeadingContextLines();
etch.update(this);
}
incrementLeadingContextLines() {
this.refs.resultsView.incrementLeadingContextLines();
etch.update(this);
}
decrementTrailingContextLines() {
this.refs.resultsView.decrementTrailingContextLines();
etch.update(this);
}
toggleTrailingContextLines() {
this.refs.resultsView.toggleTrailingContextLines();
etch.update(this);
}
incrementTrailingContextLines() {
this.refs.resultsView.incrementTrailingContextLines();
etch.update(this);
}
searchContextLineCountChanged() {
this.searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
this.searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
// update the visible line count in the find options to not exceed the maximum available lines
let findOptionsChanged = false;
if (this.searchContextLineCountBefore < this.model.getFindOptions().leadingContextLineCount) {
this.model.getFindOptions().leadingContextLineCount = this.searchContextLineCountBefore;
findOptionsChanged = true;
}
if (this.searchContextLineCountAfter < this.model.getFindOptions().trailingContextLineCount) {
this.model.getFindOptions().trailingContextLineCount = this.searchContextLineCountAfter;
findOptionsChanged = true;
}
etch.update(this);
if (findOptionsChanged) {
etch.update(this.refs.resultsView);
}
}
dontOverrideTab(){
let view = ResultsPaneView.projectFindView;
view.handleEvents.resetInterface();
view.model = new ResultsModel(view.model.findOptions);
this.uri = ResultsPaneView.URI + "/" + this.model.getLastFindPattern();
this.refs.dontOverrideTab.classList.add('disabled');
view.modelSupbscriptions.dispose();
view.handleEvents.addModelHandlers();
view.handleEventsForReplace.addReplaceModelHandlers();
this.separatePane=true;
}
}
module.exports.URI = "atom://find-and-replace/project-results";

View File

@ -0,0 +1,711 @@
const { Range, CompositeDisposable, Disposable } = require('atom');
const ResultRowView = require('./result-row-view');
const {
LeadingContextRow,
TrailingContextRow,
ResultPathRow,
MatchRow,
ResultRowGroup
} = require('./result-row');
const ListView = require('./list-view');
const etch = require('etch');
const binarySearch = require('binary-search')
const path = require('path');
const _ = require('underscore-plus');
const $ = etch.dom;
const reverseDirections = {
left: 'right',
right: 'left',
up: 'down',
down: 'up'
};
const filepathComp = (path1, path2) => path1.localeCompare(path2)
module.exports =
class ResultsView {
constructor({model}) {
this.model = model;
this.pixelOverdraw = 100;
this.resultRowGroups = Object.values(model.results).map(result =>
new ResultRowGroup(result, this.model.getFindOptions())
)
this.resultRowGroups.sort((group1, group2) => filepathComp(
group1.result.filePath, group2.result.filePath
))
this.rowGroupLengths = this.resultRowGroups.map(group => group.rows.length)
this.resultRows = [].concat(...this.resultRowGroups.map(group => group.rows))
this.selectedRowIndex = this.resultRows.length ? 0 : -1
this.fakeGroup = new ResultRowGroup({
filePath: 'fake-file-path',
matches: [{
range: [[0, 1], [0, 2]],
leadingContextLines: ['test-line-before'],
trailingContextLines: ['test-line-after'],
lineTextOffset: 1,
lineText: 'fake-line-text',
matchText: 'fake-match-text',
}],
},
{
leadingContextLineCount: 1,
trailingContextLineCount: 0
})
etch.initialize(this);
const resizeObserver = new ResizeObserver(this.invalidateItemHeights.bind(this));
resizeObserver.observe(this.element);
this.element.addEventListener('mousedown', this.handleClick.bind(this));
this.subscriptions = new CompositeDisposable(
atom.config.observe('editor.fontFamily', this.fontFamilyChanged.bind(this)),
this.model.onDidAddResult(this.didAddResult.bind(this)),
this.model.onDidSetResult(this.didSetResult.bind(this)),
this.model.onDidRemoveResult(this.didRemoveResult.bind(this)),
this.model.onDidClearSearchState(this.didClearSearchState.bind(this)),
this.model.getFindOptions().onDidChangeReplacePattern(() => etch.update(this)),
atom.commands.add(this.element, {
'core:move-up': this.moveUp.bind(this),
'core:move-down': this.moveDown.bind(this),
'core:move-left': this.collapseResult.bind(this),
'core:move-right': this.expandResult.bind(this),
'core:page-up': this.pageUp.bind(this),
'core:page-down': this.pageDown.bind(this),
'core:move-to-top': this.moveToTop.bind(this),
'core:move-to-bottom': this.moveToBottom.bind(this),
'core:confirm': this.confirmResult.bind(this),
'core:copy': this.copyResult.bind(this),
'find-and-replace:copy-path': this.copyPath.bind(this),
'find-and-replace:open-in-new-tab': this.openInNewTab.bind(this),
})
);
}
update() {}
destroy() {
this.subscriptions.dispose();
}
getRowHeight(resultRow) {
if (resultRow instanceof LeadingContextRow) {
return this.contextRowHeight
} else if (resultRow instanceof TrailingContextRow) {
return this.contextRowHeight
} else if (resultRow instanceof ResultPathRow) {
return this.pathRowHeight
} else if (resultRow instanceof MatchRow) {
return this.matchRowHeight
}
}
render () {
this.maintainPreviousScrollPosition();
let regex = null, replacePattern = null;
if (this.model.replacedPathCount == null) {
regex = this.model.regex;
replacePattern = this.model.getFindOptions().replacePattern;
}
return $.div(
{
className: 'results-view focusable-panel',
tabIndex: '-1',
style: this.previewStyle
},
$.ol(
{
className: 'list-tree has-collapsable-children',
style: {visibility: 'hidden', position: 'absolute', overflow: 'hidden', left: 0, top: 0, right: 0}
},
$(ResultRowView, {
ref: 'dummyResultPathRowView',
item: {
row: this.fakeGroup.rows[0],
regex, replacePattern
}
}),
$(ResultRowView, {
ref: 'dummyContextRowView',
item: {
row: this.fakeGroup.rows[1],
regex, replacePattern
}
}),
$(ResultRowView, {
ref: 'dummyMatchRowView',
item: {
row: this.fakeGroup.rows[2],
regex, replacePattern
}
})
),
$(ListView, {
ref: 'listView',
className: 'list-tree has-collapsable-children',
itemComponent: ResultRowView,
heightForItem: item => this.getRowHeight(item.row),
items: this.resultRows.map((row, i) => ({
row,
isSelected: i === this.selectedRowIndex,
regex,
replacePattern
}))
})
);
}
async invalidateItemHeights() {
const {
dummyResultPathRowView,
dummyMatchRowView,
dummyContextRowView,
} = this.refs;
const pathRowHeight = dummyResultPathRowView.element.offsetHeight
const matchRowHeight = dummyMatchRowView.element.offsetHeight
const contextRowHeight = dummyContextRowView.element.offsetHeight
const clientHeight = this.refs.listView && this.refs.listView.element.clientHeight;
if (matchRowHeight !== this.matchRowHeight ||
pathRowHeight !== this.pathRowHeight ||
contextRowHeight !== this.contextRowHeight ||
clientHeight !== this.clientHeight) {
this.matchRowHeight = matchRowHeight;
this.pathRowHeight = pathRowHeight;
this.contextRowHeight = contextRowHeight;
this.clientHeight = clientHeight;
await etch.update(this);
}
etch.update(this);
}
// This method should be the only one allowed to modify this.resultRows
spliceRows(start, deleteCount, rows) {
this.resultRows.splice(start, deleteCount, ...rows)
if (this.selectedRowIndex >= start + deleteCount) {
this.selectedRowIndex += rows.length - deleteCount
this.scrollToSelectedMatch()
} else if (this.selectedRowIndex >= start + rows.length) {
this.selectRow(start + rows.length - 1)
}
}
invalidateRowGroup(firstRowIndex, groupIndex) {
const { leadingContextLineCount, trailingContextLineCount } = this.model.getFindOptions()
const rowGroup = this.resultRowGroups[groupIndex]
if (!rowGroup.data.isCollapsed) {
rowGroup.generateRows(this.model.getFindOptions())
}
this.spliceRows(
firstRowIndex, this.rowGroupLengths[groupIndex],
rowGroup.displayedRows()
)
this.rowGroupLengths[groupIndex] = rowGroup.displayedRows().length
}
getGroupCountBefore(filePath) {
const res = binarySearch(
this.resultRowGroups, filePath,
(rowGroup, needle) => filepathComp(rowGroup.result.filePath, needle)
)
return res < 0 ? -res - 1 : res
}
getRowCountBefore(groupIndex) {
let rowCount = 0
for (let i = 0; i < groupIndex; ++i) {
rowCount += this.resultRowGroups[i].displayedRows().length
}
return rowCount
}
// These four methods are the only ones allowed to modify this.resultRowGroups
didAddResult({result, filePath}) {
const groupIndex = this.getGroupCountBefore(filePath)
const rowGroup = new ResultRowGroup(result, this.model.getFindOptions())
this.resultRowGroups.splice(groupIndex, 0, rowGroup)
this.rowGroupLengths.splice(groupIndex, 0, rowGroup.rows.length)
const rowIndex = this.getRowCountBefore(groupIndex)
this.spliceRows(rowIndex, 0, rowGroup.displayedRows())
if (this.selectedRowIndex === -1) {
this.selectRow(0)
}
etch.update(this);
}
didSetResult({result, filePath}) {
const groupIndex = this.getGroupCountBefore(filePath)
const rowGroup = this.resultRowGroups[groupIndex]
const rowIndex = this.getRowCountBefore(groupIndex)
rowGroup.result = result
this.invalidateRowGroup(rowIndex, groupIndex)
etch.update(this);
}
didRemoveResult({filePath}) {
const groupIndex = this.getGroupCountBefore(filePath)
const rowGroup = this.resultRowGroups[groupIndex]
const rowIndex = this.getRowCountBefore(groupIndex)
this.spliceRows(rowIndex, rowGroup.displayedRows().length, [])
this.resultRowGroups.splice(groupIndex, 1)
this.rowGroupLengths.splice(groupIndex, 1)
etch.update(this);
}
didClearSearchState() {
this.selectedRowIndex = -1
this.resultRowGroups = []
this.resultRows = []
etch.update(this);
}
handleClick(event) {
const clickedItem = event.target.closest('.list-item');
if (!clickedItem) return;
const groupIndex = this.getGroupCountBefore(clickedItem.dataset.filePath)
const group = this.resultRowGroups[groupIndex]
if (clickedItem.matches('.context-row, .match-row')) {
// The third argument restricts the range to omit the path row
const rowIndex = binarySearch(
group.rows, clickedItem.dataset.matchLineNumber,
((row, lineNb) => row.data.lineNumber - lineNb),
1
)
this.selectRow(this.getRowCountBefore(groupIndex) + rowIndex)
} else {
// If the user clicks on the left of a match, the match group is collapsed
this.selectRow(this.getRowCountBefore(groupIndex))
}
// Only apply confirmResult (open editor, collapse group) on left click
if (!event.ctrlKey && event.button === 0 && event.which !== 3) {
this.confirmResult({pending: event.detail === 1});
event.preventDefault();
}
etch.update(this);
}
// This method should be the only one allowed to modify this.selectedRowIndex
selectRow(i) {
if (this.resultRows.length === 0) {
this.selectedRowIndex = -1
return etch.update(this)
}
if (i < 0) {
this.selectedRowIndex = 0
} else if (i >= this.resultRows.length) {
this.selectedRowIndex = this.resultRows.length - 1
} else {
this.selectedRowIndex = i
}
const resultRow = this.resultRows[this.selectedRowIndex]
if (resultRow instanceof LeadingContextRow) {
this.selectedRowIndex += resultRow.rowOffset
} else if (resultRow instanceof TrailingContextRow) {
this.selectedRowIndex -= resultRow.rowOffset
}
if (i >= this.resultRows.length) {
this.scrollToBottom()
} else {
this.scrollToSelectedMatch()
}
return etch.update(this)
}
selectFirstResult() {
return this.selectRow(0)
}
moveToTop() {
return this.selectRow(0)
}
moveToBottom() {
return this.selectRow(this.resultRows.length)
}
pageUp() {
if (this.refs.listView) {
const {clientHeight} = this.refs.listView.element
const position = this.positionOfSelectedResult()
return this.selectResultAtPosition(position - clientHeight)
}
}
pageDown() {
if (this.refs.listView) {
const {clientHeight} = this.refs.listView.element
const position = this.positionOfSelectedResult()
return this.selectResultAtPosition(position + clientHeight)
}
}
positionOfSelectedResult() {
let y = 0;
for (let i = 0; i < this.selectedRowIndex; i++) {
y += this.getRowHeight(this.resultRows[i])
}
return y
}
selectResultAtPosition(position) {
if (this.refs.listView && this.model.getPathCount() > 0) {
const {clientHeight} = this.refs.listView.element
let top = 0
for (let i = 0; i < this.resultRows.length; i++) {
const bottom = top + this.getRowHeight(this.resultRows[i])
if (bottom > position) {
return this.selectRow(i)
}
top = bottom
}
}
return this.selectRow(this.resultRows.length)
}
moveDown() {
if (this.selectedRowIndex === -1) {
return this.selectRow(0)
}
for (let i = this.selectedRowIndex + 1; i < this.resultRows.length; i++) {
const row = this.resultRows[i]
if (row instanceof ResultPathRow || row instanceof MatchRow) {
return this.selectRow(i)
}
}
return this.selectRow(this.resultRows.length)
}
moveUp() {
if (this.selectedRowIndex === -1) {
return this.selectRow(0)
}
for (let i = this.selectedRowIndex - 1; i >= 0; i--) {
const row = this.resultRows[i]
if (row instanceof ResultPathRow || row instanceof MatchRow) {
return this.selectRow(i)
}
}
return this.selectRow(0)
}
selectedRow() {
return this.resultRows[this.selectedRowIndex]
}
expandResult() {
if (this.selectedRowIndex === -1) {
return
}
const rowGroup = this.selectedRow().group
const groupIndex = this.resultRowGroups.indexOf(rowGroup)
const rowIndex = this.getRowCountBefore(groupIndex)
if (!rowGroup.data.isCollapsed) {
if (this.selectedRowIndex === rowIndex) {
this.selectRow(rowIndex + 1)
}
return
}
rowGroup.data.isCollapsed = false
this.invalidateRowGroup(rowIndex, groupIndex)
this.selectRow(rowIndex + 1)
return etch.update(this);
}
collapseResult() {
if (this.selectedRowIndex === -1) {
return
}
const rowGroup = this.selectedRow().group
if (rowGroup.data.isCollapsed) {
return
}
const groupIndex = this.resultRowGroups.indexOf(rowGroup)
const rowIndex = this.getRowCountBefore(groupIndex)
rowGroup.data.isCollapsed = true
this.invalidateRowGroup(rowIndex, groupIndex)
return etch.update(this);
}
// This is the method called when clicking a result or pressing enter
async confirmResult({pending} = {}) {
if (this.selectedRowIndex === -1) {
return
}
const selectedRow = this.selectedRow()
if (selectedRow instanceof MatchRow) {
this.currentScrollTop = this.getScrollTop();
const match = selectedRow.data.matches[0]
const editor = await atom.workspace.open(selectedRow.group.result.filePath, {
pending,
searchAllPanes: true,
split: reverseDirections[atom.config.get('find-and-replace.projectSearchResultsPaneSplitDirection')]
})
editor.unfoldBufferRow(match.range.start.selectedRow)
editor.setSelectedBufferRange(match.range, {flash: true})
editor.scrollToCursorPosition()
} else if (selectedRow.group.data.isCollapsed) {
this.expandResult()
} else {
this.collapseResult()
}
}
copyResult() {
if (this.selectedRowIndex === -1) {
return
}
const selectedRow = this.selectedRow()
if (selectedRow.data.matches) {
// TODO - If row has multiple matches, copy them all, using the same
// algorithm as `Selection.copy`; ideally, that algorithm should be
// isolated for D.R.Y. purposes
atom.clipboard.write(selectedRow.data.matches[0].lineText);
}
}
copyPath() {
if (this.selectedRowIndex === -1) {
return
}
const {filePath} = this.selectedRow().group.result
let [projectPath, relativePath] = atom.project.relativizePath(filePath);
if (projectPath && atom.project.getDirectories().length > 1) {
relativePath = path.join(path.basename(projectPath), relativePath);
}
atom.clipboard.write(relativePath);
}
async openInNewTab() {
if (this.selectedRowIndex !== -1) {
const result = this.selectedRow();
if (result) {
let editor;
const editors = atom.workspace.getTextEditors();
const filepath = result.group.result.filePath;
const exists = _.some(editors, (editor) => editor.getPath() == filepath);
if (!exists) {
editor = await atom.workspace.open(filepath, {
activatePane: false,
activateItem: false
});
}
else {
editor = await atom.workspace.open(filepath);
}
if (result.data.matches) {
const match = result.data.matches[0];
if (match && editor) {
editor.unfoldBufferRow(match.range.start.selectedRow);
editor.setSelectedBufferRange(match.range, {flash: true});
editor.scrollToCursorPosition();
}
}
}
}
}
expandAllResults() {
let rowIndex = 0
// Since the whole array is re-generated, this makes splices cheaper
this.resultRows = []
for (let i = 0; i < this.resultRowGroups.length; i++) {
const group = this.resultRowGroups[i]
group.data.isCollapsed = false
this.invalidateRowGroup(rowIndex, i)
rowIndex += group.displayedRows().length
}
this.scrollToSelectedMatch();
return etch.update(this);
}
collapseAllResults() {
let rowIndex = 0
// Since the whole array is re-generated, this makes splices cheaper
this.resultRows = []
for (let i = 0; i < this.resultRowGroups.length; i++) {
const group = this.resultRowGroups[i]
group.data.isCollapsed = true
this.invalidateRowGroup(rowIndex, i)
rowIndex += group.displayedRows().length
}
this.scrollToSelectedMatch();
return etch.update(this);
}
decrementLeadingContextLines() {
if (this.model.getFindOptions().leadingContextLineCount > 0) {
this.model.getFindOptions().leadingContextLineCount--;
return this.contextLinesChanged();
}
}
toggleLeadingContextLines() {
if (this.model.getFindOptions().leadingContextLineCount > 0) {
this.model.getFindOptions().leadingContextLineCount = 0;
return this.contextLinesChanged();
} else {
const searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
if (this.model.getFindOptions().leadingContextLineCount < searchContextLineCountBefore) {
this.model.getFindOptions().leadingContextLineCount = searchContextLineCountBefore;
return this.contextLinesChanged();
}
}
}
incrementLeadingContextLines() {
const searchContextLineCountBefore = atom.config.get('find-and-replace.searchContextLineCountBefore');
if (this.model.getFindOptions().leadingContextLineCount < searchContextLineCountBefore) {
this.model.getFindOptions().leadingContextLineCount++;
return this.contextLinesChanged();
}
}
decrementTrailingContextLines() {
if (this.model.getFindOptions().trailingContextLineCount > 0) {
this.model.getFindOptions().trailingContextLineCount--;
return this.contextLinesChanged();
}
}
toggleTrailingContextLines() {
if (this.model.getFindOptions().trailingContextLineCount > 0) {
this.model.getFindOptions().trailingContextLineCount = 0;
return this.contextLinesChanged();
} else {
const searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
if (this.model.getFindOptions().trailingContextLineCount < searchContextLineCountAfter) {
this.model.getFindOptions().trailingContextLineCount = searchContextLineCountAfter;
return this.contextLinesChanged();
}
}
}
incrementTrailingContextLines() {
const searchContextLineCountAfter = atom.config.get('find-and-replace.searchContextLineCountAfter');
if (this.model.getFindOptions().trailingContextLineCount < searchContextLineCountAfter) {
this.model.getFindOptions().trailingContextLineCount++;
return this.contextLinesChanged();
}
}
async contextLinesChanged() {
let rowIndex = 0
// Since the whole array is re-generated, this makes splices cheaper
this.resultRows = []
for (let i = 0; i < this.resultRowGroups.length; i++) {
const group = this.resultRowGroups[i]
this.invalidateRowGroup(rowIndex, i)
rowIndex += group.displayedRows().length
}
await etch.update(this);
this.scrollToSelectedMatch();
}
scrollToSelectedMatch() {
if (this.selectedRowIndex === -1) {
return
}
if (this.refs.listView) {
const top = this.positionOfSelectedResult();
const bottom = top + this.getRowHeight(this.selectedRow());
if (bottom > this.getScrollTop() + this.refs.listView.element.clientHeight) {
this.setScrollTop(bottom - this.refs.listView.element.clientHeight);
} else if (top < this.getScrollTop()) {
this.setScrollTop(top);
}
}
}
scrollToBottom() {
this.setScrollTop(this.getScrollHeight());
}
scrollToTop() {
this.setScrollTop(0);
}
setScrollTop (scrollTop) {
if (this.refs.listView) {
this.refs.listView.element.scrollTop = scrollTop;
this.refs.listView.element.dispatchEvent(new UIEvent('scroll'))
}
}
getScrollTop () {
return this.refs.listView ? this.refs.listView.element.scrollTop : 0;
}
getScrollHeight () {
return this.refs.listView ? this.refs.listView.element.scrollHeight : 0;
}
maintainPreviousScrollPosition() {
if(this.selectedRowIndex === -1 || !this.currentScrollTop) {
return;
}
this.setScrollTop(this.currentScrollTop);
}
fontFamilyChanged(fontFamily) {
this.previewStyle = {fontFamily};
etch.update(this);
}
};

View File

@ -0,0 +1,65 @@
_ = require 'underscore-plus'
escapeNode = null
escapeHtml = (str) ->
escapeNode ?= document.createElement('div')
escapeNode.innerText = str
escapeNode.innerHTML
escapeRegex = (str) ->
str.replace /[.?*+^$[\]\\(){}|-]/g, (match) -> "\\" + match
sanitizePattern = (pattern) ->
pattern = escapeHtml(pattern)
pattern.replace(/\n/g, '\\n').replace(/\t/g, '\\t')
getReplacementResultsMessage = ({findPattern, replacePattern, replacedPathCount, replacementCount}) ->
if replacedPathCount
"<span class=\"text-highlight\">Replaced <span class=\"highlight-error\">#{sanitizePattern(findPattern)}</span> with <span class=\"highlight-success\">#{sanitizePattern(replacePattern)}</span> #{_.pluralize(replacementCount, 'time')} in #{_.pluralize(replacedPathCount, 'file')}</span>"
else
"<span class=\"text-highlight\">Nothing replaced</span>"
getSearchResultsMessage = (results) ->
if results?.findPattern?
{findPattern, matchCount, pathCount, replacedPathCount} = results
if matchCount
"#{_.pluralize(matchCount, 'result')} found in #{_.pluralize(pathCount, 'file')} for <span class=\"highlight-info\">#{sanitizePattern(findPattern)}</span>"
else
"No #{if replacedPathCount? then 'more' else ''} results found for '#{sanitizePattern(findPattern)}'"
else
''
showIf = (condition) ->
if condition
null
else
{display: 'none'}
capitalize = (str) -> str[0].toUpperCase() + str.toLowerCase().slice(1)
titleize = (str) -> str.toLowerCase().replace(/(?:^|\s)\S/g, (capital) -> capital.toUpperCase())
preserveCase = (text, reference) ->
# If replaced text is capitalized (strict) like a sentence, capitalize replacement
if reference is capitalize(reference.toLowerCase())
capitalize(text)
# If replaced text is titleized (i.e., each word start with an uppercase), titleize replacement
else if reference is titleize(reference.toLowerCase())
titleize(text)
# If replaced text is uppercase, uppercase replacement
else if reference is reference.toUpperCase()
text.toUpperCase()
# If replaced text is lowercase, lowercase replacement
else if reference is reference.toLowerCase()
text.toLowerCase()
else
text
module.exports = {
escapeHtml, escapeRegex, sanitizePattern, getReplacementResultsMessage,
getSearchResultsMessage, showIf, preserveCase
}

View File

@ -0,0 +1,164 @@
const _ = require('underscore-plus');
const {CompositeDisposable, Range} = require('atom');
// Find and select the next occurrence of the currently selected text.
//
// The word under the cursor will be selected if the selection is empty.
module.exports = class SelectNext {
constructor(editor) {
this.editor = editor;
this.selectionRanges = [];
}
findAndSelectNext() {
if (this.editor.getLastSelection().isEmpty()) {
return this.selectWord();
} else {
return this.selectNextOccurrence();
}
}
findAndSelectAll() {
if (this.editor.getLastSelection().isEmpty()) { this.selectWord(); }
return this.selectAllOccurrences();
}
undoLastSelection() {
this.updateSavedSelections();
if (this.selectionRanges.length < 1) { return; }
if (this.selectionRanges.length > 1) {
this.selectionRanges.pop();
this.editor.setSelectedBufferRanges(this.selectionRanges);
} else {
this.editor.clearSelections();
}
return this.editor.scrollToCursorPosition();
}
skipCurrentSelection() {
this.updateSavedSelections();
if (this.selectionRanges.length < 1) { return; }
if (this.selectionRanges.length > 1) {
const lastSelection = this.selectionRanges.pop();
this.editor.setSelectedBufferRanges(this.selectionRanges);
return this.selectNextOccurrence({start: lastSelection.end});
} else {
this.selectNextOccurrence();
this.selectionRanges.shift();
if (this.selectionRanges.length < 1) { return; }
return this.editor.setSelectedBufferRanges(this.selectionRanges);
}
}
selectWord() {
this.editor.selectWordsContainingCursors();
const lastSelection = this.editor.getLastSelection();
if (this.wordSelected = this.isWordSelected(lastSelection)) {
const disposables = new CompositeDisposable;
const clearWordSelected = () => {
this.wordSelected = null;
return disposables.dispose();
};
disposables.add(lastSelection.onDidChangeRange(clearWordSelected));
return disposables.add(lastSelection.onDidDestroy(clearWordSelected));
}
}
selectAllOccurrences() {
const range = [[0, 0], this.editor.getEofBufferPosition()];
return this.scanForNextOccurrence(range, ({range, stop}) => {
return this.addSelection(range);
});
}
selectNextOccurrence(options) {
if (options == null) { options = {}; }
const startingRange = options.start != null ? options.start : this.editor.getSelectedBufferRange().end;
let range = this.findNextOccurrence([startingRange, this.editor.getEofBufferPosition()]);
if (range == null) { range = this.findNextOccurrence([[0, 0], this.editor.getSelections()[0].getBufferRange().start]); }
if (range != null) { return this.addSelection(range); }
}
findNextOccurrence(scanRange) {
let foundRange = null;
this.scanForNextOccurrence(scanRange, function({range, stop}) {
foundRange = range;
return stop();
});
return foundRange;
}
addSelection(range) {
const reversed = this.editor.getLastSelection().isReversed();
const selection = this.editor.addSelectionForBufferRange(range, {reversed});
return this.updateSavedSelections(selection);
}
scanForNextOccurrence(range, callback) {
const selection = this.editor.getLastSelection();
let text = _.escapeRegExp(selection.getText());
if (this.wordSelected) {
const nonWordCharacters = atom.config.get('editor.nonWordCharacters');
text = `(^|[ \t${_.escapeRegExp(nonWordCharacters)}]+)${text}(?=$|[\\s${_.escapeRegExp(nonWordCharacters)}]+)`;
}
return this.editor.scanInBufferRange(new RegExp(text, 'g'), range, function(result) {
let prefix;
if (prefix = result.match[1]) {
result.range = result.range.translate([0, prefix.length], [0, 0]);
}
return callback(result);
});
}
updateSavedSelections(selection=null) {
const selections = this.editor.getSelections();
if (selections.length < 3) { this.selectionRanges = []; }
if (this.selectionRanges.length === 0) {
return Array.from(selections).map((s) => this.selectionRanges.push(s.getBufferRange()));
} else if (selection) {
const selectionRange = selection.getBufferRange();
if (this.selectionRanges.some(existingRange => existingRange.isEqual(selectionRange))) { return; }
return this.selectionRanges.push(selectionRange);
}
}
isNonWordCharacter(character) {
const nonWordCharacters = atom.config.get('editor.nonWordCharacters');
return new RegExp(`[ \t${_.escapeRegExp(nonWordCharacters)}]`).test(character);
}
isNonWordCharacterToTheLeft(selection) {
const selectionStart = selection.getBufferRange().start;
const range = Range.fromPointWithDelta(selectionStart, 0, -1);
return this.isNonWordCharacter(this.editor.getTextInBufferRange(range));
}
isNonWordCharacterToTheRight(selection) {
const selectionEnd = selection.getBufferRange().end;
const range = Range.fromPointWithDelta(selectionEnd, 0, 1);
return this.isNonWordCharacter(this.editor.getTextInBufferRange(range));
}
isWordSelected(selection) {
if (selection.getBufferRange().isSingleLine()) {
const selectionRange = selection.getBufferRange();
const lineRange = this.editor.bufferRangeForBufferRow(selectionRange.start.row);
const nonWordCharacterToTheLeft = _.isEqual(selectionRange.start, lineRange.start) ||
this.isNonWordCharacterToTheLeft(selection);
const nonWordCharacterToTheRight = _.isEqual(selectionRange.end, lineRange.end) ||
this.isNonWordCharacterToTheRight(selection);
const containsOnlyWordCharacters = !this.isNonWordCharacter(selection.getText());
return nonWordCharacterToTheLeft && nonWordCharacterToTheRight && containsOnlyWordCharacters;
} else {
return false;
}
}
}

View File

@ -0,0 +1,36 @@
'menu': [
'label': 'Find'
'submenu': [
{ 'label': 'Find in Buffer', 'command': 'find-and-replace:show'}
{ 'label': 'Replace in Buffer', 'command': 'find-and-replace:show-replace'}
{ 'label': 'Select Next', 'command': 'find-and-replace:select-next'}
{ 'label': 'Select All', 'command': 'find-and-replace:select-all'}
{ 'label': 'Toggle Find in Buffer', 'command': 'find-and-replace:toggle'}
{ 'type': 'separator' }
{ 'label': 'Find in Project', 'command': 'project-find:show'}
{ 'label': 'Toggle Find in Project', 'command': 'project-find:toggle'}
{ 'type': 'separator' }
{ 'label': 'Find All', 'command': 'find-and-replace:find-all'}
{ 'label': 'Find Next', 'command': 'find-and-replace:find-next'}
{ 'label': 'Find Previous', 'command': 'find-and-replace:find-previous'}
{ 'label': 'Replace Next', 'command': 'find-and-replace:replace-next'}
{ 'label': 'Replace All', 'command': 'find-and-replace:replace-all'}
{ 'type': 'separator' }
{ 'label': 'Clear History', 'command': 'find-and-replace:clear-history'}
{ 'type': 'separator' }
]
]
'context-menu':
'.tree-view li.directory': [
{ 'label': 'Search in Folder', 'command': 'project-find:show-in-current-directory' }
]
'.list-item.match-row': [
{ 'label': 'Open in New Tab', 'command': 'find-and-replace:open-in-new-tab' }
{ 'label': 'Copy', 'command': 'core:copy' }
{ 'label': 'Copy Path', 'command': 'find-and-replace:copy-path' }
]
'.list-item.path-row': [
{ 'label': 'Open in New Tab', 'command': 'find-and-replace:open-in-new-tab' }
{ 'label': 'Copy Path', 'command': 'find-and-replace:copy-path' }
]

View File

@ -0,0 +1,146 @@
{
"name": "find-and-replace",
"main": "./lib/find",
"description": "Find and replace within buffers and across the project.",
"version": "0.219.8",
"license": "MIT",
"activationCommands": {
"atom-workspace": [
"project-find:show",
"project-find:toggle",
"project-find:show-in-current-directory",
"find-and-replace:show",
"find-and-replace:toggle",
"find-and-replace:find-all",
"find-and-replace:find-next",
"find-and-replace:find-previous",
"find-and-replace:find-next-selected",
"find-and-replace:find-previous-selected",
"find-and-replace:use-selection-as-find-pattern",
"find-and-replace:use-selection-as-replace-pattern",
"find-and-replace:show-replace",
"find-and-replace:replace-next",
"find-and-replace:replace-all",
"find-and-replace:select-next",
"find-and-replace:select-all",
"find-and-replace:clear-history"
]
},
"repository": "https://github.com/pulsar-edit/find-and-replace",
"engines": {
"atom": "*"
},
"dependencies": {
"binary-search": "^1.3.3",
"etch": "0.9.3",
"fs-plus": "^3.0.0",
"temp": "^0.8.3",
"underscore-plus": "1.x"
},
"devDependencies": {
"dedent": "^0.6.0"
},
"consumedServices": {
"atom.file-icons": {
"versions": {
"1.0.0": "consumeFileIcons"
}
},
"autocomplete.watchEditor": {
"versions": {
"1.0.0": "consumeAutocompleteWatchEditor"
}
},
"file-icons.element-icons": {
"versions": {
"1.0.0": "consumeElementIcons"
}
}
},
"providedServices": {
"find-and-replace": {
"description": "Atom's bundled find-and-replace package",
"versions": {
"0.0.1": "provideService"
}
}
},
"configSchema": {
"focusEditorAfterSearch": {
"type": "boolean",
"default": false,
"description": "Focus the editor and select the next match when a file search is executed. If no matches are found, the editor will not be focused."
},
"projectSearchResultsPaneSplitDirection": {
"type": "string",
"default": "none",
"enum": [
"none",
"right",
"down"
],
"title": "Direction to open results pane",
"description": "Direction to split the active pane when showing project search results. If 'none', the results will be shown in the active pane."
},
"closeFindPanelAfterSearch": {
"type": "boolean",
"default": false,
"title": "Close Project Find Panel After Search",
"description": "Close the find panel after executing a project-wide search."
},
"scrollToResultOnLiveSearch": {
"type": "boolean",
"default": false,
"title": "Scroll To Result On Live-Search (incremental find in buffer)",
"description": "Scroll to and select the closest match while typing in the buffer find box."
},
"liveSearchMinimumCharacters": {
"type": "integer",
"default": 3,
"minimum": 0,
"description": "The minimum number of characters which need to be typed into the buffer find box before search starts matching and highlighting matches as you type."
},
"searchContextLineCountBefore": {
"type": "integer",
"default": 3,
"minimum": 0,
"description": "The number of extra lines of context to query before the match for project results"
},
"searchContextLineCountAfter": {
"type": "integer",
"default": 3,
"minimum": 0,
"description": "The number of extra lines of context to query after the match for project results"
},
"showSearchWrapIcon": {
"type": "boolean",
"default": true,
"title": "Show Search Wrap Icon",
"description": "Display a visual cue over the editor when looping through search results."
},
"useRipgrep": {
"type": "boolean",
"default": false,
"title": "Use ripgrep",
"description": "Use the experimental `ripgrep` search crawler. This will make searches substantially faster on large projects."
},
"enablePCRE2": {
"type": "boolean",
"default": false,
"title": "Enable PCRE2 regex engine",
"description": "Enable PCRE2 regex engine (applies only to `ripgrep` search). This will enable additional regex features such as lookbehind, but may make searches slower."
},
"autocompleteSearches": {
"type": "boolean",
"default": false,
"title": "Autocomplete Search",
"description": "Autocompletes entries in the find search field."
},
"preserveCaseOnReplace": {
"type": "boolean",
"default": false,
"title": "Preserve case during replace.",
"description": "Keep the replaced text case during replace: replacing 'user' with 'person' will replace 'User' with 'Person' and 'USER' with 'PERSON'."
}
}
}

View File

@ -0,0 +1,23 @@
/** @babel */
export async function conditionPromise (condition) {
const startTime = Date.now()
while (true) {
await timeoutPromise(100)
if (await condition()) {
return
}
if (Date.now() - startTime > 5000) {
throw new Error("Timed out waiting on condition")
}
}
}
function timeoutPromise (timeout) {
return new Promise(function (resolve) {
global.setTimeout(resolve, timeout)
})
}

View File

@ -0,0 +1,856 @@
const dedent = require('dedent')
const {TextEditor, Range} = require('atom');
const FindOptions = require('../lib/find-options');
const BufferSearch = require('../lib/buffer-search');
describe('BufferSearch', () => {
let model, editor, buffer, markersListener, currentResultListener, searchSpy;
beforeEach(() => {
editor = new TextEditor();
buffer = editor.getBuffer();
// TODO - remove this conditional after Atom 1.25 ships
if (buffer.findAndMarkAllInRangeSync) {
searchSpy = spyOn(buffer, 'findAndMarkAllInRangeSync').andCallThrough();
} else {
searchSpy = spyOn(buffer, 'scanInRange').andCallThrough();
}
editor.setText(dedent`
-----------
aaa bbb ccc
ddd aaa bbb
ccc ddd aaa
-----------
aaa bbb ccc
ddd aaa bbb
ccc ddd aaa
-----------
`);
advanceClock(buffer.stoppedChangingDelay);
const findOptions = new FindOptions({findPattern: "a+"});
model = new BufferSearch(findOptions);
markersListener = jasmine.createSpy('markersListener');
model.onDidUpdate(markersListener);
currentResultListener = jasmine.createSpy('currentResultListener');
model.onDidChangeCurrentResult(currentResultListener);
model.setEditor(editor);
markersListener.reset();
model.search("a+", {
caseSensitive: false,
useRegex: true,
wholeWord: false
});
});
afterEach(() => {
model.destroy();
editor.destroy();
});
function getHighlightedRanges() {
const ranges = [];
const decorations = editor.decorationsStateForScreenRowRange(0, editor.getLineCount())
for (const id in decorations) {
const decoration = decorations[id];
if (['find-result', 'current-result'].includes(decoration.properties.class)) {
ranges.push(decoration.screenRange);
}
}
return ranges
.sort((a, b) => a.compare(b))
.map(range => range.serialize());
};
function expectUpdateEvent() {
expect(markersListener.callCount).toBe(1);
const emittedMarkerRanges = markersListener
.mostRecentCall.args[0]
.map(marker => marker.getBufferRange().serialize());
expect(emittedMarkerRanges).toEqual(getHighlightedRanges());
markersListener.reset();
};
function expectNoUpdateEvent() {
expect(markersListener).not.toHaveBeenCalled();
}
function scannedRanges() {
return searchSpy.argsForCall.map(args => args.find(arg => arg instanceof Range))
}
it("highlights all the occurrences of the search regexp", () => {
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[0, 0], [Infinity, Infinity]]
]);
});
describe("when the buffer changes", () => {
beforeEach(() => {
markersListener.reset();
searchSpy.reset();
});
describe("when changes occur in the middle of the buffer", () => {
it("removes any invalidated search results and recreates markers in the changed regions", () => {
editor.setCursorBufferPosition([2, 5]);
editor.addCursorAtBufferPosition([6, 5]);
editor.insertText(".");
editor.insertText(".");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 5]],
[[2, 7], [2, 9]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 5]],
[[6, 7], [6, 9]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[1, 0], [3, 11]],
[[5, 0], [7, 11]]
]);
})
});
describe("when changes occur within the first search result", () => {
it("rescans the buffer from the beginning to the first valid marker", () => {
editor.setCursorBufferPosition([1, 2]);
editor.insertText(".");
editor.insertText(".");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 2]],
[[1, 4], [1, 5]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[0, 0], [2, 7]]
]);
})
});
describe("when changes occur within the last search result", () => {
it("rescans the buffer from the last valid marker to the end", () => {
editor.setCursorBufferPosition([7, 9]);
editor.insertText(".");
editor.insertText(".");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 9]],
[[7, 11], [7, 13]]
]);
expect(scannedRanges()).toEqual([
[[6, 4], [Infinity, Infinity]]
]);
})
});
describe("when changes occur within two adjacent markers", () => {
it("rescans the changed region in a single scan", () => {
editor.setCursorBufferPosition([2, 5]);
editor.addCursorAtBufferPosition([3, 9]);
editor.insertText(".");
editor.insertText(".");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 5]],
[[2, 7], [2, 9]],
[[3, 8], [3, 9]],
[[3, 11], [3, 13]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[1, 0], [5, 3]]
]);
})
});
describe("when changes extend an existing search result", () => {
it("updates the results with the new extended ranges", () => {
editor.setCursorBufferPosition([2, 4]);
editor.addCursorAtBufferPosition([6, 7]);
editor.insertText("a");
editor.insertText("a");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 6], [2, 9]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 9]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 9]],
[[7, 8], [7, 11]]
]);
})
});
describe("when the changes are before any marker", () => {
it("doesn't change the markers", () => {
editor.setCursorBufferPosition([0, 3]);
editor.insertText("..");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[0, 0], [1, 3]]
]);
})
});
describe("when the changes are between markers", () => {
it("doesn't change the markers", () => {
editor.setCursorBufferPosition([3, 1]);
editor.insertText("..");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 10], [3, 13]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 10], [3, 13]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[2, 4], [3, 13]]
]);
})
});
describe("when the changes are after all the markers", () => {
it("doesn't change the markers", () => {
editor.setCursorBufferPosition([8, 3]);
editor.insertText("..");
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[7, 8], [Infinity, Infinity]]
]);
})
});
describe("when the changes are undone", () => {
it("recreates any temporarily-invalidated markers", () => {
editor.setCursorBufferPosition([2, 5]);
editor.insertText(".");
editor.insertText(".");
editor.backspace();
editor.backspace();
expectNoUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
advanceClock(buffer.stoppedChangingDelay);
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([]);
})
});
});
describe("when the 'in current selection' option is set to true", () => {
beforeEach(() => {
model.setFindOptions({inCurrentSelection: true})
})
describe("if the current selection is non-empty", () => {
beforeEach(() => {
editor.setSelectedBufferRange([[1, 3], [4, 5]]);
})
it("only searches in the given selection", () => {
model.search("a+");
expect(scannedRanges().pop()).toEqual([[1, 3], [4, 5]]);
expect(getHighlightedRanges()).toEqual([
[[2, 4], [2, 7]],
[[3, 8], [3, 11]]
]);
})
it("executes another search if the current selection is different from the last search's selection", () => {
model.search("a+");
editor.setSelectedBufferRange([[5, 0], [5, 2]]);
model.search("a+");
expect(scannedRanges().pop()).toEqual([[5, 0], [5, 2]]);
expect(getHighlightedRanges()).toEqual([
[[5, 0], [5, 2]]
]);
})
it("does not execute another search if the current selection is idential to the last search's selection", () => {
spyOn(model, 'recreateMarkers').andCallThrough()
model.search("a+");
model.search("a+");
expect(model.recreateMarkers.callCount).toBe(1)
})
})
describe("if there are multiple non-empty selections", () => {
beforeEach(() => {
editor.setSelectedBufferRanges([
[[1, 3], [2, 11]],
[[5, 2], [8, 0]],
]);
})
it("searches in all the selections", () => {
model.search("a+");
expect(getHighlightedRanges()).toEqual([
[[2, 4], [2, 7]],
[[5, 2], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
})
it("executes another search if the current selection is different from the last search's selection", () => {
spyOn(model, 'recreateMarkers').andCallThrough()
model.search("a+");
editor.setSelectedBufferRanges([
[[1, 3], [2, 11]],
[[5, 1], [8, 0]],
]);
model.search("a+");
expect(model.recreateMarkers.callCount).toBe(2)
expect(getHighlightedRanges()).toEqual([
[[2, 4], [2, 7]],
[[5, 1], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
})
it("does not execute another search if the current selection is idential to the last search's selection", () => {
spyOn(model, 'recreateMarkers').andCallThrough()
editor.setSelectedBufferRanges([
[[1, 3], [2, 11]],
[[5, 1], [8, 0]],
]);
model.search("a+");
model.search("a+");
expect(model.recreateMarkers.callCount).toBe(1)
})
})
describe("if the current selection is empty", () => {
beforeEach(() => {
editor.setSelectedBufferRange([[0, 0], [0, 0]]);
})
it("ignores the option and searches the entire buffer", () => {
model.search("a+");
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[2, 4], [2, 7]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges().pop()).toEqual([[0, 0], [Infinity, Infinity]]);
})
})
})
describe("replacing a search result", () => {
beforeEach(() => {
searchSpy.reset()
});
it("replaces the marked text with the given string", () => {
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
editor.setSelectedBufferRange(markers[1].getBufferRange());
expect(currentResultListener).toHaveBeenCalledWith(markers[1]);
currentResultListener.reset();
model.replace([markers[1]], "new-text");
expect(editor.getText()).toBe(dedent`
-----------
aaa bbb ccc
ddd new-text bbb
ccc ddd aaa
-----------
aaa bbb ccc
ddd aaa bbb
ccc ddd aaa
-----------
`);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
const markerToSelect = markers[2];
const rangeToSelect = markerToSelect.getBufferRange();
editor.setSelectedBufferRange(rangeToSelect);
expect(currentResultListener).toHaveBeenCalledWith(markerToSelect);
currentResultListener.reset();
advanceClock(buffer.stoppedChangingDelay);
expectUpdateEvent();
expect(getHighlightedRanges()).toEqual([
[[1, 0], [1, 3]],
[[3, 8], [3, 11]],
[[5, 0], [5, 3]],
[[6, 4], [6, 7]],
[[7, 8], [7, 11]]
]);
expect(scannedRanges()).toEqual([
[[1, 0], [3, 11]]
]);
expect(currentResultListener).toHaveBeenCalled();
expect(currentResultListener.mostRecentCall.args[0].getBufferRange()).toEqual(rangeToSelect);
expect(currentResultListener.mostRecentCall.args[0].isDestroyed()).toBe(false);
});
it("replaces the marked text with the given string that contains escaped escape sequence", () => {
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "new-text\\\\n");
expect(editor.getText()).toBe([
'-----------',
'new-text\\n bbb ccc',
'ddd new-text\\n bbb',
'ccc ddd new-text\\n',
'-----------',
'new-text\\n bbb ccc',
'ddd new-text\\n bbb',
'ccc ddd new-text\\n',
'-----------',
].join('\n'));
});
});
describe(".prototype.resultsMarkerLayerForTextEditor(editor)", () =>
it("creates or retrieves the results marker layer for the given editor", () => {
const layer1 = model.resultsMarkerLayerForTextEditor(editor);
// basic check that this is the expected results layer
expect(layer1.findMarkers().length).toBeGreaterThan(0);
for (const marker of layer1.findMarkers()) {
expect(editor.getTextInBufferRange(marker.getBufferRange())).toMatch(/a+/);
}
const editor2 = new TextEditor();
model.setEditor(editor2);
const layer2 = model.resultsMarkerLayerForTextEditor(editor2);
model.setEditor(editor);
expect(model.resultsMarkerLayerForTextEditor(editor)).toBe(layer1);
expect(model.resultsMarkerLayerForTextEditor(editor2)).toBe(layer2);
model.search("c+", {
caseSensitive: false,
useRegex: true,
wholeWord: false
});
expect(layer1.findMarkers().length).toBeGreaterThan(0);
for (const marker of layer1.findMarkers()) {
expect(editor.getTextInBufferRange(marker.getBufferRange())).toMatch(/c+/);
}
})
);
});
describe("BufferSearch", () => {
let model, editor, markersListener, currentResultListener;
beforeEach(() => {
editor = new TextEditor();
spyOn(editor, 'scanInBufferRange').andCallThrough();
editor.setText(dedent`
-----------
aaa bbb ccc
ddd Aaa bbb
CCC DDD aaa
-----------
AAA Bbb cCc
Ddd Aaa Bbb
ccc DDD Aaa
-----------
`);
advanceClock(editor.buffer.stoppedChangingDelay);
const findOptions = new FindOptions({findPattern: "aaa"});
model = new BufferSearch(findOptions);
markersListener = jasmine.createSpy('markersListener');
model.onDidUpdate(markersListener);
currentResultListener = jasmine.createSpy('currentResultListener');
model.onDidChangeCurrentResult(currentResultListener);
model.setEditor(editor);
markersListener.reset();
});
afterEach(() => {
model.destroy();
editor.destroy();
});
describe("when replacing text with preserve case on", () => {
beforeEach(() => {
atom.config.set('find-and-replace.preserveCaseOnReplace', true)
});
it("preserves case.", () => {
model.search("aaa", {
caseSensitive: false,
useRegex: false,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "foo");
expect(editor.getText()).toBe(dedent`
-----------
foo bbb ccc
ddd Foo bbb
CCC DDD foo
-----------
FOO Bbb cCc
Ddd Foo Bbb
ccc DDD Foo
-----------
`);
});
it("preserves case using regex search.", () => {
model.search("a+", {
caseSensitive: false,
useRegex: true,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "foo");
expect(editor.getText()).toBe(dedent`
-----------
foo bbb ccc
ddd Foo bbb
CCC DDD foo
-----------
FOO Bbb cCc
Ddd Foo Bbb
ccc DDD Foo
-----------
`);
});
it("preserves case across words.", () => {
model.search("aaa", {
caseSensitive: false,
useRegex: false,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "foo bar");
expect(editor.getText()).toBe(dedent`
-----------
foo bar bbb ccc
ddd Foo bar bbb
CCC DDD foo bar
-----------
FOO BAR Bbb cCc
Ddd Foo bar Bbb
ccc DDD Foo bar
-----------
`);
});
it("preserves case only when it's consistent between searched words.", () => {
model.search("aaa bbb", {
caseSensitive: false,
useRegex: false,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "foo");
expect(editor.getText()).toBe(dedent`
-----------
foo ccc
ddd Foo
CCC DDD aaa
-----------
foo cCc
Ddd Foo
ccc DDD Aaa
-----------
`);
});
it("preserves case and honors caseSensitive option.", () => {
model.search("Aaa", {
caseSensitive: true,
useRegex: false,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "foo");
expect(editor.getText()).toBe(dedent`
-----------
aaa bbb ccc
ddd Foo bbb
CCC DDD aaa
-----------
AAA Bbb cCc
Ddd Foo Bbb
ccc DDD Foo
-----------
`);
});
it("preserves case of original replacement when capitalized.", () => {
model.search("aaa", {
caseSensitive: false,
useRegex: false,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "FoO");
expect(editor.getText()).toBe(dedent`
-----------
foo bbb ccc
ddd Foo bbb
CCC DDD foo
-----------
FOO Bbb cCc
Ddd Foo Bbb
ccc DDD Foo
-----------
`);
});
it("preserves case of sentence, title, upper and lower case.", () => {
editor.setText(dedent`
x aaa bbb x
x Aaa bbb x
x aaa Bbb x
x Aaa Bbb x
x AAA BBB x
x aaA bbb x
x aaa bbB x
x aaA bbB x
`);
advanceClock(editor.buffer.stoppedChangingDelay);
model.search("aAa bBb", {
caseSensitive: false,
useRegex: false,
wholeWord: false
});
const markers = markersListener.mostRecentCall.args[0];
markersListener.reset();
model.replace(markers, "xxX yYy");
expect(editor.getText()).toBe(dedent`
x xxx yyy x
x Xxx yyy x
x xxX yYy x
x Xxx Yyy x
x XXX YYY x
x xxX yYy x
x xxX yYy x
x xxX yYy x
`);
});
});
});

View File

@ -0,0 +1,49 @@
const BufferSearch = require('../lib/buffer-search')
const EmbeddedEditorItem = require('./item/embedded-editor-item')
const DeferredEditorItem = require('./item/deferred-editor-item');
const UnrecognizedItem = require('./item/unrecognized-item');
describe('Find', () => {
describe('updating the find model', () => {
beforeEach(async () => {
atom.workspace.addOpener(EmbeddedEditorItem.opener)
atom.workspace.addOpener(UnrecognizedItem.opener)
atom.workspace.addOpener(DeferredEditorItem.opener)
const activationPromise = atom.packages.activatePackage('find-and-replace')
atom.commands.dispatch(atom.views.getView(atom.workspace), 'find-and-replace:show')
await activationPromise
spyOn(BufferSearch.prototype, 'setEditor')
})
it("sets the find model's editor whenever an editor is focused", async () => {
let editor = await atom.workspace.open()
expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(editor)
editor = await atom.workspace.open('sample.js')
expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(editor)
})
it("sets the find model's editor to an embedded text editor", async () => {
const embedded = await atom.workspace.open(EmbeddedEditorItem.uri)
expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(embedded.refs.theEditor)
})
it("sets the find model's editor to an embedded text editor after activation", async () => {
const deferred = await atom.workspace.open(DeferredEditorItem.uri)
expect(BufferSearch.prototype.setEditor).not.toHaveBeenCalled()
await deferred.showEditor()
expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(deferred.refs.theEditor)
await deferred.hideEditor()
expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(null)
})
it("sets the find model's editor to null if a non-editor is focused", async () => {
await atom.workspace.open(UnrecognizedItem.uri)
expect(BufferSearch.prototype.setEditor).toHaveBeenCalledWith(null)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
test test test test test test test test test test test test test test test test test test test test test test a b c d e f g h i j k l abcdefghijklmnopqrstuvwxyz

View File

@ -0,0 +1,90 @@
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
11
11
1
11
1
1
1
1
1

View File

@ -0,0 +1 @@
test test test test test test test test test test test test test test test test test test test test test test a b c d e f g h i j k l abcdefghijklmnopqrstuvwxyz

View File

@ -0,0 +1,23 @@
class quicksort
sort: (items) ->
return items if items.length <= 1
pivot = items.shift()
left = []
right = []
# Comment in the middle (and add the word 'items' again)
while items.length > 0
current = items.shift()
if current < pivot
left.push(current)
else
right.push(current);
sort(left).concat(pivot).concat(sort(right))
noop: ->
# just a noop
exports.modules = quicksort

View File

@ -0,0 +1,13 @@
var quicksort = function () {
var sort = function(items) {
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
return sort(left).concat(pivot).concat(sort(right));
};
return sort(Array.apply(this, arguments));
};

View File

@ -0,0 +1,23 @@
class quicksort
sort: (items) ->
return items if items.length <= 1
pivot = items.shift()
left = []
right = []
# Comment in the middle (and add the word 'items' again)
while items.length > 0
current = items.shift()
if current < pivot
left.push(current)
else
right.push(current);
sort(left).concat(pivot).concat(sort(right))
noop: ->
# just a noop
exports.modules = quicksort

View File

@ -0,0 +1,13 @@
var quicksort = function () {
var sort = function(items) {
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
return sort(left).concat(pivot).concat(sort(right));
};
return sort(Array.apply(this, arguments));
};

View File

@ -0,0 +1,9 @@
const genPromiseToCheck = fn => new Promise(resolve => {
const interval = setInterval(() => { if(fn()) resolve() }, 100)
setTimeout(() => {
resolve()
clearInterval(interval)
}, 4000)
})
module.exports = { genPromiseToCheck }

View File

@ -0,0 +1,71 @@
// An active workspace item that embeds an AtomTextEditor we wish to expose to Find and Replace that does not become
// available until some time after the item is activated.
const etch = require('etch');
const $ = etch.dom;
const { TextEditor, Emitter } = require('atom');
class DeferredEditorItem {
static opener(u) {
if (u === DeferredEditorItem.uri) {
return new DeferredEditorItem();
} else {
return undefined;
}
}
constructor() {
this.editorShown = false;
this.emitter = new Emitter();
etch.initialize(this);
}
render() {
if (this.editorShown) {
return (
$.div({className: 'wrapper'},
etch.dom(TextEditor, {ref: 'theEditor'})
)
)
} else {
return (
$.div({className: 'wrapper'}, 'Empty')
)
}
}
update() {
return etch.update(this)
}
observeEmbeddedTextEditor(cb) {
if (this.editorShown) {
cb(this.refs.theEditor)
}
return this.emitter.on('did-change-embedded-text-editor', cb)
}
async showEditor() {
const wasShown = this.editorShown
this.editorShown = true
await this.update()
if (!wasShown) {
this.emitter.emit('did-change-embedded-text-editor', this.refs.theEditor)
}
}
async hideEditor() {
const wasShown = this.editorShown
this.editorShown = false
await this.update()
if (wasShown) {
this.emitter.emit('did-change-embedded-text-editor', null)
}
}
}
DeferredEditorItem.uri = 'atom://find-and-replace/spec/deferred-editor'
module.exports = DeferredEditorItem

View File

@ -0,0 +1,38 @@
// An active workspace item that embeds an AtomTextEditor we wish to expose to Find and Replace.
const etch = require('etch');
const $ = etch.dom;
const { TextEditor } = require('atom');
class EmbeddedEditorItem {
static opener(u) {
if (u === EmbeddedEditorItem.uri) {
return new EmbeddedEditorItem();
} else {
return undefined;
}
}
constructor() {
etch.initialize(this);
}
render() {
return (
$.div({className: 'wrapper'},
etch.dom(TextEditor, {ref: 'theEditor'})
)
)
}
update() {}
getEmbeddedTextEditor() {
return this.refs.theEditor
}
}
EmbeddedEditorItem.uri = 'atom://find-and-replace/spec/embedded-editor'
module.exports = EmbeddedEditorItem

View File

@ -0,0 +1,30 @@
// An active workspace item that doesn't contain a TextEditor.
const etch = require('etch');
const $ = etch.dom;
class UnrecognizedItem {
static opener(u) {
if (u === UnrecognizedItem.uri) {
return new UnrecognizedItem();
} else {
return undefined;
}
}
constructor() {
etch.initialize(this);
}
render() {
return (
$.div({className: 'wrapper'}, 'Some text')
)
}
update() {}
}
UnrecognizedItem.uri = 'atom://find-and-replace/spec/unrecognized'
module.exports = UnrecognizedItem

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
/** @babel */
const {
LeadingContextRow,
TrailingContextRow,
ResultPathRow,
MatchRow,
ResultRowGroup
} = require("../lib/project/result-row");
describe("ResultRowGroup", () => {
const lines = (new Array(18)).fill().map((x, i) => `line-${i}`)
const rg = (i) => [[i, 0], [i, lines[i].length]]
const testedRowIndices = [0, 7, 13, 16, 17]
const result = {
filePath: 'fake-file-path',
matches: testedRowIndices.map(lineNb => ({
range: rg(lineNb),
leadingContextLines: lines.slice(Math.max(lineNb - 3, 0), lineNb),
trailingContextLines: lines.slice(lineNb + 1, lineNb + 4),
lineTextOffset: 0,
lineText: lines[lineNb],
matchText: 'fake-match-text'
}))
}
describe("generateRows", () => {
it("generates a path row and several match rows", () => {
const rowGroup = new ResultRowGroup(
result,
{ leadingContextLineCount: 0, trailingContextLineCount: 0 }
)
const expectedRows = [
new ResultPathRow(rowGroup),
new MatchRow(rowGroup, false, 0, [ result.matches[0] ]),
new MatchRow(rowGroup, true, 7, [ result.matches[1] ]),
new MatchRow(rowGroup, true, 13, [ result.matches[2] ]),
new MatchRow(rowGroup, true, 16, [ result.matches[3] ]),
new MatchRow(rowGroup, false, 17, [ result.matches[4] ])
]
for (let i = 0; i < rowGroup.rows.length; ++i) {
expect(rowGroup.rows[i].data).toEqual(expectedRows[i].data)
}
})
it("generates context rows between matches", () => {
const rowGroup = new ResultRowGroup(
result,
{ leadingContextLineCount: 3, trailingContextLineCount: 2 }
)
const expectedRows = [
new ResultPathRow(rowGroup),
new MatchRow(rowGroup, false, 0, [ result.matches[0] ]),
new TrailingContextRow(rowGroup, lines[1], false, 0, 1),
new TrailingContextRow(rowGroup, lines[2], false, 0, 2),
new LeadingContextRow(rowGroup, lines[4], true, 7, 3),
new LeadingContextRow(rowGroup, lines[5], false, 7, 2),
new LeadingContextRow(rowGroup, lines[6], false, 7, 1),
new MatchRow(rowGroup, false, 7, [ result.matches[1] ]),
new TrailingContextRow(rowGroup, lines[8], false, 7, 1),
new TrailingContextRow(rowGroup, lines[9], false, 7, 2),
new LeadingContextRow(rowGroup, lines[10], false, 13, 3),
new LeadingContextRow(rowGroup, lines[11], false, 13, 2),
new LeadingContextRow(rowGroup, lines[12], false, 13, 1),
new MatchRow(rowGroup, false, 13, [ result.matches[2] ]),
new TrailingContextRow(rowGroup, lines[14], false, 13, 1),
new TrailingContextRow(rowGroup, lines[15], false, 13, 2),
new MatchRow(rowGroup, false, 16, [ result.matches[3] ]),
new MatchRow(rowGroup, false, 17, [ result.matches[4] ])
]
for (let i = 0; i < rowGroup.rows.length; ++i) {
expect(rowGroup.rows[i].data).toEqual(expectedRows[i].data)
}
})
})
describe("getLineNumber", () => {
it("generates correct line numbers", () => {
const rowGroup = new ResultRowGroup(
result,
{ leadingContextLineCount: 1, trailingContextLineCount: 1 }
)
expect(rowGroup.rows.slice(1).map(row => row.data.lineNumber)).toEqual(
[0, 1, 6, 7, 8, 12, 13, 14, 15, 16, 17]
)
})
})
})

View File

@ -0,0 +1,130 @@
/** @babel */
const path = require("path");
const ResultsModel = require("../lib/project/results-model");
const FindOptions = require("../lib/find-options");
describe("ResultsModel", () => {
let editor, resultsModel;
beforeEach(async () => {
atom.config.set("core.excludeVcsIgnoredPaths", false);
atom.config.set("find-and-replace.searchContextLineCountBefore", 2);
atom.config.set("find-and-replace.searchContextLineCountAfter", 3);
atom.project.setPaths([path.join(__dirname, "fixtures/project")]);
editor = await atom.workspace.open("sample.js");
resultsModel = new ResultsModel(new FindOptions());
});
describe("searching for a pattern", () => {
it("populates the model with all the results, and updates in response to changes in the buffer", async () => {
const resultAddedSpy = jasmine.createSpy();
const resultSetSpy = jasmine.createSpy();
const resultRemovedSpy = jasmine.createSpy();
resultsModel.onDidAddResult(resultAddedSpy);
resultsModel.onDidSetResult(resultSetSpy);
resultsModel.onDidRemoveResult(resultRemovedSpy);
await resultsModel.search("items", "*.js", "");
expect(resultAddedSpy).toHaveBeenCalled();
expect(resultAddedSpy.callCount).toBe(1);
let result = resultsModel.getResult(editor.getPath());
expect(result.matches.length).toBe(6);
expect(resultsModel.getPathCount()).toBe(1);
expect(resultsModel.getMatchCount()).toBe(6);
expect(resultsModel.getPaths()).toEqual([editor.getPath()]);
expect(result.matches[0].leadingContextLines.length).toBe(1);
expect(result.matches[0].leadingContextLines[0]).toBe("var quicksort = function () {");
expect(result.matches[0].trailingContextLines.length).toBe(3);
expect(result.matches[0].trailingContextLines[0]).toBe(" if (items.length <= 1) return items;");
expect(result.matches[0].trailingContextLines[1]).toBe(" var pivot = items.shift(), current, left = [], right = [];");
expect(result.matches[0].trailingContextLines[2]).toBe(" while(items.length > 0) {");
expect(result.matches[5].leadingContextLines.length).toBe(2);
expect(result.matches[5].trailingContextLines.length).toBe(3);
editor.setText("there are some items in here");
advanceClock(editor.buffer.stoppedChangingDelay);
expect(resultAddedSpy.callCount).toBe(1);
expect(resultSetSpy.callCount).toBe(1);
result = resultsModel.getResult(editor.getPath());
expect(result.matches.length).toBe(1);
expect(resultsModel.getPathCount()).toBe(1);
expect(resultsModel.getMatchCount()).toBe(1);
expect(resultsModel.getPaths()).toEqual([editor.getPath()]);
expect(result.matches[0].lineText).toBe("there are some items in here");
expect(result.matches[0].leadingContextLines.length).toBe(0);
expect(result.matches[0].trailingContextLines.length).toBe(0);
editor.setText("no matches in here");
advanceClock(editor.buffer.stoppedChangingDelay);
expect(resultAddedSpy.callCount).toBe(1);
expect(resultSetSpy.callCount).toBe(1);
expect(resultRemovedSpy.callCount).toBe(1);
result = resultsModel.getResult(editor.getPath());
expect(result).not.toBeDefined();
expect(resultsModel.getPathCount()).toBe(0);
expect(resultsModel.getMatchCount()).toBe(0);
resultsModel.clear();
spyOn(editor, "scan").andCallThrough();
editor.setText("no matches in here");
advanceClock(editor.buffer.stoppedChangingDelay);
expect(editor.scan).not.toHaveBeenCalled();
expect(resultsModel.getPathCount()).toBe(0);
expect(resultsModel.getMatchCount()).toBe(0);
});
it("ignores changes in untitled buffers", async () => {
await atom.workspace.open();
await resultsModel.search("items", "*.js", "");
editor = atom.workspace.getCenter().getActiveTextEditor();
editor.setText("items\nitems");
spyOn(editor, "scan").andCallThrough();
advanceClock(editor.buffer.stoppedChangingDelay);
expect(editor.scan).not.toHaveBeenCalled();
});
it("contains valid match objects after destroying a buffer (regression)", async () => {
await resultsModel.search('items', '*.js', '');
advanceClock(editor.buffer.stoppedChangingDelay)
editor.getBuffer().destroy()
const result = resultsModel.getResult(editor.getPath())
expect(result.matches[0].lineText).toBe(" var sort = function(items) {")
});
});
describe("cancelling a search", () => {
let cancelledSpy;
beforeEach(() => {
cancelledSpy = jasmine.createSpy();
resultsModel.onDidCancelSearching(cancelledSpy);
});
it("populates the model with all the results, and updates in response to changes in the buffer", async () => {
const searchPromise = resultsModel.search("items", "*.js", "");
expect(resultsModel.inProgressSearchPromise).toBeTruthy();
resultsModel.clear();
expect(resultsModel.inProgressSearchPromise).toBeFalsy();
await searchPromise;
expect(cancelledSpy).toHaveBeenCalled();
});
it("populates the model with all the results, and updates in response to changes in the buffer", async () => {
resultsModel.search("items", "*.js", "");
await resultsModel.search("sort", "*.js", "");
expect(cancelledSpy).toHaveBeenCalled();
expect(resultsModel.getPathCount()).toBe(1);
expect(resultsModel.getMatchCount()).toBe(5);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,636 @@
/** @babel */
const path = require('path');
const SelectNext = require('../lib/select-next');
const dedent = require('dedent');
describe("SelectNext", () => {
let workspaceElement, editorElement, editor;
beforeEach(async () => {
workspaceElement = atom.views.getView(atom.workspace);
atom.project.setPaths([path.join(__dirname, 'fixtures')]);
editor = await atom.workspace.open('sample.js');
editorElement = atom.views.getView(editor);
jasmine.attachToDOM(workspaceElement);
const activationPromise = atom.packages.activatePackage("find-and-replace");
atom.commands.dispatch(editorElement, 'find-and-replace:show');
await activationPromise;
});
describe("find-and-replace:select-next", () => {
describe("when nothing is selected", () => {
it("selects the word under the cursor", () => {
editor.setCursorBufferPosition([1, 3]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([[[1, 2], [1, 5]]]);
});
});
describe("when a word is selected", () => {
describe("when the selection was created using select-next", () => {
beforeEach(() => {});
it("selects the next occurrence of the selected word skipping any non-word matches", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([0, 0]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]],
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]],
[[5, 6], [5, 9]]
]);
editor.setText("Testing reallyTesting");
editor.setCursorBufferPosition([0, 0]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 7]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 7]]
]);});});
describe("when the selection was not created using select-next", () => {
it("selects the next occurrence of the selected characters including non-word matches", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setSelectedBufferRange([[0, 0], [0, 3]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[1, 2], [1, 5]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[1, 2], [1, 5]],
[[2, 0], [2, 3]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[1, 2], [1, 5]],
[[2, 0], [2, 3]],
[[3, 8], [3, 11]]
]);
editor.setText("Testing reallyTesting");
editor.setSelectedBufferRange([[0, 0], [0, 7]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 7]],
[[0, 14], [0, 21]]
]);
});
});
});
describe("when part of a word is selected", () => {
it("selects the next occurrence of the selected text", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setSelectedBufferRange([[1, 2], [1, 5]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[1, 2], [1, 5]],
[[2, 0], [2, 3]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[1, 2], [1, 5]],
[[2, 0], [2, 3]],
[[3, 8], [3, 11]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[1, 2], [1, 5]],
[[2, 0], [2, 3]],
[[3, 8], [3, 11]],
[[4, 0], [4, 3]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[1, 2], [1, 5]],
[[2, 0], [2, 3]],
[[3, 8], [3, 11]],
[[4, 0], [4, 3]],
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[1, 2], [1, 5]],
[[2, 0], [2, 3]],
[[3, 8], [3, 11]],
[[4, 0], [4, 3]],
[[5, 6], [5, 9]],
[[0, 0], [0, 3]]
]);
});
});
describe("when a non-word is selected", () => {
it("selects the next occurrence of the selected text", () => {
editor.setText(dedent`
<!
<a
`);
editor.setSelectedBufferRange([[0, 0], [0, 1]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 1]],
[[1, 0], [1, 1]]
]);
})
});
describe("when the word is at a line boundary", () => {
it("does not select the newlines", () => {
editor.setText(dedent`
a
a
`);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 1]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 1]],
[[2, 0], [2, 1]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 1]],
[[2, 0], [2, 1]]
]);
});
});
it('honors the reversed orientation of previous selections', () => {
editor.setText('ab ab ab ab')
editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: true})
atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
expect(editor.getSelections().length).toBe(2)
expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
expect(editor.getSelections().length).toBe(3)
expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: false})
atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
expect(editor.getSelections().length).toBe(2)
expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
expect(editor.getSelections().length).toBe(3)
expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
})
});
describe("find-and-replace:select-all", () => {
describe("when there is no selection", () => {
it("find and selects all occurrences of the word under the cursor", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]],
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]],
[[5, 6], [5, 9]]
]);
})
});
describe("when a word is selected", () => {
describe("when the word was selected using select-next", () => {
it("find and selects all occurrences of the word", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]],
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]],
[[5, 6], [5, 9]]
]);
});
});
describe("when the word was not selected using select-next", () => {
it("find and selects all occurrences including non-words", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setSelectedBufferRange([[3, 8], [3, 11]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
expect(editor.getSelectedBufferRanges()).toEqual([
[[3, 8], [3, 11]],
[[0, 0], [0, 3]],
[[1, 2], [1, 5]],
[[2, 0], [2, 3]],
[[4, 0], [4, 3]],
[[5, 6], [5, 9]]
]);
});
});
});
describe("when a non-word is selected", () => {
it("selects the next occurrence of the selected text", () => {
editor.setText(dedent`
<!
<a\
`);
editor.setSelectedBufferRange([[0, 0], [0, 1]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-all');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 1]],
[[1, 0], [1, 1]]
]);
});
});
it('honors the reversed orientation of previous selections', () => {
editor.setText('ab ab ab ab')
editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: true})
atom.commands.dispatch(editorElement, 'find-and-replace:select-all')
expect(editor.getSelections().length).toBe(4)
expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: false})
atom.commands.dispatch(editorElement, 'find-and-replace:select-all')
expect(editor.getSelections().length).toBe(4)
expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
})
});
describe("find-and-replace:select-undo", () => {
describe("when there is no selection", () => {
it("does nothing", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 0]]
]);
})
});
describe("when a word is selected", () => {
it("unselects current word", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setSelectedBufferRange([[3, 8], [3, 11]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[3, 11], [3, 11]]
]);
})
});
describe("when two words are selected", () => {
it("unselects words in order", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setSelectedBufferRange([[3, 8], [3, 11]]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[3, 8], [3, 11]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[3, 11], [3, 11]]
]);
})
});
describe("when three words are selected", () => {
it("unselects words in order", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([0, 0]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[3, 8], [3, 11]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]]
]);
})
});
describe("when starting at the bottom word", () => {
it("unselects words in order", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([5, 7]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]],
[[0, 0], [0, 3]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]]
]);});
it("doesn't stack previously selected", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([5, 7]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-undo');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]],
[[0, 0], [0, 3]]
]);
});
});
});
describe("find-and-replace:select-skip", () => {
describe("when there is no selection", () => {
it("does nothing", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 0]]
]);
})
});
describe("when a word is selected", () => {
it("unselects current word and selects next match", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([3, 8]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[3, 8], [3, 11]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]]
]);
})
});
describe("when two words are selected", () => {
it("unselects second word and selects next match", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([0, 0]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]],
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
expect(editor.getSelectedBufferRanges()).toEqual([
[[0, 0], [0, 3]]
]);
});
});
describe("when starting at the bottom word", () => {
it("unselects second word and selects next match", () => {
editor.setText(dedent`
for
information
format
another for
fork
a 3rd for is here
`);
editor.setCursorBufferPosition([5, 7]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]]
]);
atom.commands.dispatch(editorElement, 'find-and-replace:select-next');
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip');
expect(editor.getSelectedBufferRanges()).toEqual([
[[5, 6], [5, 9]],
[[3, 8], [3, 11]]
]);
});
});
it('honors the reversed orientation of previous selections', () => {
editor.setText('ab ab ab ab')
editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: true})
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
expect(editor.getSelections().length).toBe(1)
expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
expect(editor.getSelections().length).toBe(2)
expect(editor.getSelections().every(s => s.isReversed())).toBe(true)
editor.setSelectedBufferRange([[0, 0], [0, 2]], {reversed: false})
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
expect(editor.getSelections().length).toBe(1)
expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
atom.commands.dispatch(editorElement, 'find-and-replace:select-next')
atom.commands.dispatch(editorElement, 'find-and-replace:select-skip')
expect(editor.getSelections().length).toBe(2)
expect(editor.getSelections().every(s => !s.isReversed())).toBe(true)
})
});
});

View File

@ -0,0 +1,19 @@
require('etch').setScheduler({
updateDocument(callback) { callback(); },
getNextUpdatePromise() { return Promise.resolve(); }
});
// The CI on Atom has been failing with this package. Experiments
// indicate the dev tools were being opened, which caused test
// failures. Dev tools are meant to be triggered on an uncaught
// exception.
//
// These failures are flaky though, so an exact cause
// has not yet been found. For now the following is added
// to reduce the number of flaky failures, which have been
// causing false failures on unrelated Atom PRs.
//
// See more in https://github.com/atom/atom/pull/21335
global.beforeEach(() => {
spyOn(atom, 'openDevTools').andReturn((console.error("ERROR: Dev tools attempted to open"), Promise.resolve()));
});

View File

@ -0,0 +1,376 @@
@import "ui-variables";
@import "syntax-variables";
// result markers
atom-text-editor {
.find-result .region {
background-color: transparent;
border-radius: @component-border-radius;
border: 1px solid @syntax-result-marker-color;
box-sizing: border-box;
z-index: 0;
}
.current-result .region {
border-radius: @component-border-radius;
border: 1px solid @syntax-result-marker-color-selected;
box-sizing: border-box;
z-index: 0;
}
.find-result,
.current-result {
display: none;
}
}
atom-workspace.find-visible {
atom-text-editor {
.find-result,
.current-result {
display: block;
}
}
}
// Both project and buffer FNR styles
.find-and-replace,
.preview-pane,
.project-find {
@min-width: 200px; // min width before it starts scrolling
-webkit-user-select: none;
padding: @component-padding/2;
overflow-x: auto;
.header {
padding: @component-padding/4 @component-padding/2;
min-width: @min-width;
line-height: 1.75;
}
.header-item {
margin: @component-padding/4 0;
}
.input-block {
display: flex;
flex-wrap: wrap;
width: 100%;
min-width: @min-width;
}
.input-block-item {
display: flex;
flex: 1;
padding: @component-padding / 2;
}
.btn-group {
display: flex;
flex: 1;
.btn {
flex: 1;
}
& + .btn-group {
margin-left: @component-padding;
}
}
.btn > .icon {
width: 20px;
height: 16px;
vertical-align: middle;
fill: currentColor;
stroke: currentColor;
pointer-events: none;
}
.close-button {
margin-left: @component-padding;
cursor: pointer;
color: @text-color-subtle;
&:hover {
color: @text-color-highlight;
}
.icon::before {
margin-right: 0;
text-align: center;
vertical-align: middle;
}
}
.description {
display: inline-block;
.subtle-info-message {
padding-left: 5px;
color: @text-color-subtle;
.highlight {
color: @text-color;
font-weight: normal;
}
}
}
.options-label {
color: @text-color-subtle;
position: relative;
.options {
margin-right: .5em;
color: @text-color;
}
}
.btn-group-options {
display: inline-flex;
margin-top: -.1em;
.btn {
width: 36px;
padding: 0;
line-height: 1.75;
}
}
.editor-container {
position: relative;
atom-text-editor {
width: 100%;
}
}
}
// Buffer find and replace
.find-and-replace {
@input-width: 260px;
@block-width: 260px;
.input-block-item {
flex: 1 1 @block-width;
}
.input-block-item--flex {
flex: 100 1 @input-width;
}
.btn-group-find,
.btn-group-replace {
flex: 1;
}
.btn-group-find-all,
.btn-group-replace-all {
flex: 2;
}
.find-container atom-text-editor {
padding-right: 64px; // leave some room for the results count
}
// results count
.find-meta-container {
position: absolute;
top: 1px;
right: 0;
margin: @component-padding/2 @component-padding/2 0 0;
z-index: 2;
font-size: .9em;
line-height: @component-line-height;
pointer-events: none;
.result-counter {
margin-right: @component-padding;
}
}
}
.find-wrap-icon {
@wrap-size: @font-size * 10;
opacity: 0;
transition: opacity 0.5s;
&.visible { opacity: 1; }
position: absolute;
// These are getting placed in the DOM as a pane item, so override the pane
// item positioning styles. :/
top: 50% !important;
left: 50% !important;
right: initial !important;
bottom: initial !important;
margin-top: @wrap-size * -0.5;
margin-left: @wrap-size * -0.5;
background: fadeout(darken(@syntax-background-color, 4%), 55%);
border-radius: @component-border-radius * 2;
text-align: center;
pointer-events: none;
&:before {
// Octicons look best in sizes that are multiples of 16px
font-size: @wrap-size - mod(@wrap-size, 16px) - 32px;
line-height: @wrap-size;
height: @wrap-size;
width: @wrap-size;
color: @syntax-text-color;
opacity: .5;
}
}
// Project find and replace
.project-find {
@project-input-width: 260px;
@project-block-width: 160px;
.input-block-item {
flex: 1 1 @project-block-width;
}
.input-block-item--flex {
flex: 100 1 @project-input-width;
}
.loading,
.preview-block,
.error-messages,
.filter-container {
display: none;
}
}
.preview-pane {
position: relative;
display: flex;
flex-direction: column;
padding: 0;
.preview-header {
display: flex;
flex-wrap: wrap;
padding: @component-padding/2;
align-items: center;
justify-content: space-between;
overflow: hidden;
font-weight: normal;
border-bottom: 1px solid @panel-heading-border-color;
background-color: @panel-heading-background-color;
}
.preview-count {
margin: @component-padding/2;
}
.preview-controls {
display: flex;
flex-wrap: wrap;
.btn-group {
margin: @component-padding/2;
}
}
.loading-spinner-tiny,
.loading-spinner-tiny + .inline-block {
vertical-align: middle;
}
.no-results-overlay {
visibility: hidden;
}
&.no-results .no-results-overlay {
visibility: visible;
}
.results-view {
overflow: auto;
position: relative;
flex: 1;
&-container {
// adds some padding
// so the last item can be clicked
// when there is a horizontal scrollbar -> #943
padding-bottom: @component-padding;
}
.list-item {
padding: 0 0 0 @component-padding;
}
.context-row, .match-row {
padding: 0 0 0 @component-padding;
margin-left: 8px;
box-shadow: inset 0 1px 0 mix(@base-border-color, @base-background-color);
// box-shadow over a border is used to not affect height calculation
&.separator {
box-shadow: inset 0 1px 0 @base-border-color;
}
}
.line-number {
margin-right: 1ex;
text-align: right;
display: inline-block;
}
.match-row.selected .line-number {
color: @text-color-selected;
}
.path-match-number {
padding-left: @component-padding;
color: @text-color-subtle;
}
.preview {
word-break: break-all;
white-space: pre;
color: @text-color-subtle;
}
.match-row .preview {
color: @text-color-highlight;
}
.match-row.selected .preview {
color: @text-color-selected;
}
.selected {
.highlight-info {
box-shadow: inset 0 0 1px lighten(@background-color-info, 50%);
}
.highlight-error {
box-shadow: inset 0 0 1px lighten(@background-color-error, 25%);
}
.highlight-success {
box-shadow: inset 0 0 1px lighten(@background-color-success, 25%);
}
}
}
}
.find-container atom-text-editor, .replace-container atom-text-editor {
// Styles for regular expression highlighting
.syntax--regexp {
.syntax--escape {
color: @text-color-info;
}
.syntax--range, .syntax--character-class, .syntax--wildcard {
color: @text-color-success;
}
.syntax--wildcard {
font-weight: bold;
}
.syntax--set {
color: inherit;
}
.syntax--keyword, .syntax--punctuation {
color: @text-color-error;
font-weight: normal;
}
.syntax--replacement.syntax--variable {
color: @text-color-warning;
}
}
}

View File

@ -4553,9 +4553,8 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
"find-and-replace@https://github.com/atom-community/find-and-replace/archive/refs/tags/v0.220.1.tar.gz":
version "0.220.1"
resolved "https://github.com/atom-community/find-and-replace/archive/refs/tags/v0.220.1.tar.gz#d7a0f56511e38ee72a89895a795bbbcab4a1a405"
"find-and-replace@file:packages/find-and-replace":
version "0.219.8"
dependencies:
binary-search "^1.3.3"
etch "0.9.3"