2025-03-19 22:00:41 +02:00
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 ;
} ;
}
2022-05-17 23:53:35 +02:00
export default class FindInText {
2025-03-17 22:46:00 +02:00
private parent : FindWidget ;
2025-03-19 22:00:41 +02:00
private findResult? : CKFindResult | null ;
private editingState? : EditingState ;
2025-03-17 22:46:00 +02:00
constructor ( parent : FindWidget ) {
2022-05-17 23:53:35 +02:00
this . parent = parent ;
}
2022-05-16 23:56:43 +02:00
2022-05-17 23:53:35 +02:00
async getTextEditor() {
2025-03-17 22:46:00 +02:00
return this . parent ? . noteContext ? . getTextEditor ( ) ;
2022-05-17 23:53:35 +02:00
}
2022-05-16 23:56:43 +02:00
2025-03-19 22:00:41 +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
2022-05-17 23:53:35 +02:00
const textEditor = await this . getTextEditor ( ) ;
2025-03-19 22:00:41 +02:00
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 ( ) ;
2024-11-11 18:19:19 +08:00
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 } ;
2025-03-19 22:00:41 +02:00
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 ( ) ;
2025-03-19 22:00:41 +02:00
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 ) {
2022-05-17 23:53:35 +02:00
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 ( ) ;
2025-03-19 22:00:41 +02:00
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 ( ) ;
}
2024-11-11 18:11:31 +08:00
2025-03-17 22:46:00 +02:00
async replace ( replaceText : string ) {
2024-11-11 18:59:03 +08:00
if ( this . editingState !== undefined && this . editingState . highlightedResult !== null ) {
2024-11-11 18:11:31 +08:00
const textEditor = await this . getTextEditor ( ) ;
2025-03-17 22:46:00 +02:00
textEditor ? . execute ( "replace" , replaceText , this . editingState . highlightedResult ) ;
2024-11-11 18:11:31 +08:00
}
}
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 ) {
2024-11-11 18:11:31 +08:00
const textEditor = await this . getTextEditor ( ) ;
2025-03-17 22:46:00 +02:00
textEditor ? . execute ( "replaceAll" , replaceText , this . editingState . results ) ;
2024-11-11 18:11:31 +08:00
}
}
2022-05-16 23:56:43 +02:00
}