mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-10-26 19:24:31 +03:00
306 lines
9.3 KiB
JavaScript
306 lines
9.3 KiB
JavaScript
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
|
|
}
|
|
}
|