Notes/src/public/app/widgets/find_in_text.ts

147 lines
5.6 KiB
TypeScript
Raw Normal View History

import type { FindResult } from "./find.js";
2025-03-17 22:46:00 +02:00
import type FindWidget from "./find.js";
// TODO: Deduplicate.
interface Match {
className: string;
clear(): void;
find(): {
from: number;
to: number;
};
}
export default class FindInText {
2025-03-17 22:46:00 +02:00
private parent: FindWidget;
private findResult?: CKFindResult | null;
private editingState?: EditingState;
2025-03-17 22:46:00 +02:00
constructor(parent: FindWidget) {
this.parent = parent;
}
2022-05-16 23:56:43 +02:00
async getTextEditor() {
2025-03-17 22:46:00 +02:00
return this.parent?.noteContext?.getTextEditor();
}
2022-05-16 23:56:43 +02:00
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean): Promise<FindResult> {
2022-05-16 23:56:43 +02:00
// Do this even if the searchTerm is empty so the markers are cleared and
// the counters updated
const textEditor = await this.getTextEditor();
if (!textEditor) {
return { currentFound: 0, totalFound: 0 };
}
2022-05-16 23:56:43 +02:00
const model = textEditor.model;
let findResult = null;
let totalFound = 0;
let currentFound = -1;
// Clear
2025-01-09 18:07:02 +02:00
const findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
2022-05-16 23:56:43 +02:00
findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.stop();
this.editingState = findAndReplaceEditing.state;
2022-05-16 23:56:43 +02:00
if (searchTerm !== "") {
// Parameters are callback/text, options.matchCase=false, options.wholeWords=false
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
// XXX Need to use the callback version for regexp
// searchTerm = escapeRegExp(searchTerm);
// let re = new RegExp(searchTerm, 'gi');
// let m = text.match(re);
// totalFound = m ? m.length : 0;
2025-01-09 18:07:02 +02:00
const options = { matchCase: matchCase, wholeWords: wholeWord };
findResult = textEditor.execute<CKFindResult>("find", searchTerm, options);
2022-05-16 23:56:43 +02:00
totalFound = findResult.results.length;
// Find the result beyond the cursor
const cursorPos = model.document.selection.getLastPosition();
for (let i = 0; i < findResult.results.length; ++i) {
const marker = findResult.results.get(i).marker;
const fromPos = marker.getStart();
if (cursorPos && fromPos.compareWith(cursorPos) !== "before") {
2022-05-16 23:56:43 +02:00
currentFound = i;
break;
}
}
}
this.findResult = findResult;
// Calculate curfound if not already, highlight it as
// selected
if (totalFound > 0) {
currentFound = Math.max(0, currentFound);
// XXX Do this accessing the private data?
2023-05-05 23:41:11 +02:00
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
2025-01-09 18:07:02 +02:00
for (let i = 0; i < currentFound; ++i) {
2025-03-17 22:46:00 +02:00
textEditor?.execute("findNext", searchTerm);
2022-05-16 23:56:43 +02:00
}
}
return {
totalFound,
2023-09-05 21:00:24 +02:00
currentFound: Math.min(currentFound + 1, totalFound)
2022-05-16 23:56:43 +02:00
};
}
2025-03-17 22:46:00 +02:00
async findNext(direction: number, currentFound: number, nextFound: number) {
const textEditor = await this.getTextEditor();
2022-05-16 23:56:43 +02:00
// There are no parameters for findNext/findPrev
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js#L57
// curFound wrap around above assumes findNext and
// findPrevious wraparound, which is what they do
if (direction > 0) {
2025-03-17 22:46:00 +02:00
textEditor?.execute("findNext");
2022-05-16 23:56:43 +02:00
} else {
2025-03-17 22:46:00 +02:00
textEditor?.execute("findPrevious");
2022-05-16 23:56:43 +02:00
}
}
2025-03-17 22:46:00 +02:00
async findBoxClosed(totalFound: number, currentFound: number) {
2022-05-26 16:29:54 +02:00
const textEditor = await this.getTextEditor();
2025-03-17 22:46:00 +02:00
if (!textEditor) {
return;
}
2022-05-26 16:29:54 +02:00
2022-05-16 23:56:43 +02:00
if (totalFound > 0) {
// Clear the markers and set the caret to the
// current occurrence
const model = textEditor.model;
2025-03-17 22:46:00 +02:00
const range = this.findResult?.results?.get(currentFound).marker.getRange();
2022-05-16 23:56:43 +02:00
// From
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92
// XXX Roll our own since already done for codeEditor and
// will probably allow more refactoring?
2025-01-09 18:07:02 +02:00
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
2022-05-16 23:56:43 +02:00
findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.stop();
if (range) {
model.change((writer) => {
writer.setSelection(range, 0);
});
}
2022-05-16 23:56:43 +02:00
textEditor.editing.view.scrollToTheSelection();
}
this.findResult = null;
textEditor.focus();
}
2025-03-17 22:46:00 +02:00
async replace(replaceText: string) {
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
const textEditor = await this.getTextEditor();
2025-03-17 22:46:00 +02:00
textEditor?.execute("replace", replaceText, this.editingState.highlightedResult);
}
}
2025-03-17 22:46:00 +02:00
async replaceAll(replaceText: string) {
2025-01-09 18:07:02 +02:00
if (this.editingState !== undefined && this.editingState.results.length > 0) {
const textEditor = await this.getTextEditor();
2025-03-17 22:46:00 +02:00
textEditor?.execute("replaceAll", replaceText, this.editingState.results);
}
}
2022-05-16 23:56:43 +02:00
}