mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-11 11:15:24 +03:00
Merge pull request #517 from pulsar-edit/bundle-find-and-replace
Bundle find and replace
This commit is contained in:
commit
7bc10280c2
@ -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",
|
||||
|
@ -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
|
||||
|
20
packages/find-and-replace/LICENSE.md
Normal file
20
packages/find-and-replace/LICENSE.md
Normal 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.
|
17
packages/find-and-replace/README.md
Normal file
17
packages/find-and-replace/README.md
Normal 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.
|
85
packages/find-and-replace/keymaps/find-and-replace.cson
Normal file
85
packages/find-and-replace/keymaps/find-and-replace.cson
Normal 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'
|
305
packages/find-and-replace/lib/buffer-search.js
Normal file
305
packages/find-and-replace/lib/buffer-search.js
Normal 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
|
||||
}
|
||||
}
|
26
packages/find-and-replace/lib/default-file-icons.js
Normal file
26
packages/find-and-replace/lib/default-file-icons.js
Normal 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()
|
17
packages/find-and-replace/lib/escape-helper.js
Normal file
17
packages/find-and-replace/lib/escape-helper.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
103
packages/find-and-replace/lib/find-options.js
Normal file
103
packages/find-and-replace/lib/find-options.js
Normal 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, '\\$&');
|
805
packages/find-and-replace/lib/find-view.js
Normal file
805
packages/find-and-replace/lib/find-view.js
Normal 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);
|
||||
}
|
||||
};
|
292
packages/find-and-replace/lib/find.js
Normal file
292
packages/find-and-replace/lib/find.js
Normal 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()
|
||||
};
|
||||
}
|
||||
};
|
66
packages/find-and-replace/lib/get-icon-services.js
Normal file
66
packages/find-and-replace/lib/get-icon-services.js
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
76
packages/find-and-replace/lib/history.coffee
Normal file
76
packages/find-and-replace/lib/history.coffee
Normal 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}
|
566
packages/find-and-replace/lib/project-find-view.js
Normal file
566
packages/find-and-replace/lib/project-find-view.js
Normal 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});
|
||||
}
|
||||
};
|
103
packages/find-and-replace/lib/project/list-view.js
Normal file
103
packages/find-and-replace/lib/project/list-view.js
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
286
packages/find-and-replace/lib/project/result-row-view.js
Normal file
286
packages/find-and-replace/lib/project/result-row-view.js
Normal 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);
|
||||
}
|
||||
};
|
154
packages/find-and-replace/lib/project/result-row.js
Normal file
154
packages/find-and-replace/lib/project/result-row.js
Normal 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
|
||||
}
|
388
packages/find-and-replace/lib/project/results-model.js
Normal file
388
packages/find-and-replace/lib/project/results-model.js
Normal 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
|
362
packages/find-and-replace/lib/project/results-pane.js
Normal file
362
packages/find-and-replace/lib/project/results-pane.js
Normal 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";
|
711
packages/find-and-replace/lib/project/results-view.js
Normal file
711
packages/find-and-replace/lib/project/results-view.js
Normal 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);
|
||||
}
|
||||
};
|
65
packages/find-and-replace/lib/project/util.coffee
Normal file
65
packages/find-and-replace/lib/project/util.coffee
Normal 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
|
||||
}
|
164
packages/find-and-replace/lib/select-next.js
Normal file
164
packages/find-and-replace/lib/select-next.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
36
packages/find-and-replace/menus/find-and-replace.cson
Normal file
36
packages/find-and-replace/menus/find-and-replace.cson
Normal 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' }
|
||||
]
|
146
packages/find-and-replace/package.json
Normal file
146
packages/find-and-replace/package.json
Normal 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'."
|
||||
}
|
||||
}
|
||||
}
|
23
packages/find-and-replace/spec/async-spec-helpers.js
Normal file
23
packages/find-and-replace/spec/async-spec-helpers.js
Normal 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)
|
||||
})
|
||||
}
|
856
packages/find-and-replace/spec/buffer-search-spec.js
Normal file
856
packages/find-and-replace/spec/buffer-search-spec.js
Normal 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
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
49
packages/find-and-replace/spec/find-spec.js
Normal file
49
packages/find-and-replace/spec/find-spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
1847
packages/find-and-replace/spec/find-view-spec.js
Normal file
1847
packages/find-and-replace/spec/find-view-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/find-and-replace/spec/fixtures/one-long-line.coffee
vendored
Normal file
1
packages/find-and-replace/spec/fixtures/one-long-line.coffee
vendored
Normal 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
|
90
packages/find-and-replace/spec/fixtures/project/long-match.js
vendored
Normal file
90
packages/find-and-replace/spec/fixtures/project/long-match.js
vendored
Normal 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
|
1
packages/find-and-replace/spec/fixtures/project/one-long-line.coffee
vendored
Normal file
1
packages/find-and-replace/spec/fixtures/project/one-long-line.coffee
vendored
Normal 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
|
23
packages/find-and-replace/spec/fixtures/project/sample.coffee
vendored
Normal file
23
packages/find-and-replace/spec/fixtures/project/sample.coffee
vendored
Normal 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
|
13
packages/find-and-replace/spec/fixtures/project/sample.js
vendored
Normal file
13
packages/find-and-replace/spec/fixtures/project/sample.js
vendored
Normal 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));
|
||||
};
|
23
packages/find-and-replace/spec/fixtures/sample.coffee
vendored
Normal file
23
packages/find-and-replace/spec/fixtures/sample.coffee
vendored
Normal 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
|
13
packages/find-and-replace/spec/fixtures/sample.js
vendored
Normal file
13
packages/find-and-replace/spec/fixtures/sample.js
vendored
Normal 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));
|
||||
};
|
9
packages/find-and-replace/spec/helpers.js
Normal file
9
packages/find-and-replace/spec/helpers.js
Normal 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 }
|
71
packages/find-and-replace/spec/item/deferred-editor-item.js
Normal file
71
packages/find-and-replace/spec/item/deferred-editor-item.js
Normal 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
|
38
packages/find-and-replace/spec/item/embedded-editor-item.js
Normal file
38
packages/find-and-replace/spec/item/embedded-editor-item.js
Normal 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
|
30
packages/find-and-replace/spec/item/unrecognized-item.js
Normal file
30
packages/find-and-replace/spec/item/unrecognized-item.js
Normal 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
|
1679
packages/find-and-replace/spec/project-find-view-spec.js
Normal file
1679
packages/find-and-replace/spec/project-find-view-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
98
packages/find-and-replace/spec/result-row-spec.js
Normal file
98
packages/find-and-replace/spec/result-row-spec.js
Normal 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]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
130
packages/find-and-replace/spec/results-model-spec.js
Normal file
130
packages/find-and-replace/spec/results-model-spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
1074
packages/find-and-replace/spec/results-view-spec.js
Normal file
1074
packages/find-and-replace/spec/results-view-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
636
packages/find-and-replace/spec/select-next-spec.js
Normal file
636
packages/find-and-replace/spec/select-next-spec.js
Normal 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)
|
||||
})
|
||||
});
|
||||
});
|
19
packages/find-and-replace/spec/setup-spec.js
Normal file
19
packages/find-and-replace/spec/setup-spec.js
Normal 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()));
|
||||
});
|
376
packages/find-and-replace/styles/find-and-replace.less
Normal file
376
packages/find-and-replace/styles/find-and-replace.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user