pulsar/packages/find-and-replace/lib/buffer-search.js
2023-05-05 19:38:34 -03:00

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
}
}