trilium/src/public/app/widgets/find_in_code.js

199 lines
7.5 KiB
JavaScript

// ck-find-result and ck-find-result_selected are the styles ck-editor
// uses for highlighting matches, use the same one on CodeMirror
// for consistency
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export default class FindInCode {
constructor(parent) {
this.parent = parent;
}
async getCodeEditor() {
return this.parent.noteContext.getCodeEditor();
}
async getInitialSearchTerm() {
const codeEditor = await this.getCodeEditor();
// highlightSelectionMatches is the overlay that highlights
// the words under the cursor. This occludes the search
// markers style, save it, disable it. Will be restored when
// the focus is back into the note
this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches");
codeEditor.setOption("highlightSelectionMatches", false);
// Fill in the findbox with the current selection if any
const selectedText = codeEditor.getSelection()
if (selectedText !== "") {
return selectedText;
}
}
async performFind(searchTerm, matchCase, wholeWord) {
let findResult = null;
let totalFound = 0;
let currentFound = -1;
// See https://codemirror.net/addon/search/searchcursor.js for tips
const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc;
const text = doc.getValue();
// Clear all markers
if (this.findResult != null) {
codeEditor.operation(() => {
for (let i = 0; i < this.findResult.length; ++i) {
const marker = this.findResult[i];
marker.clear();
}
});
}
if (searchTerm !== "") {
searchTerm = escapeRegExp(searchTerm);
// Find and highlight matches
// Find and highlight matches
// XXX Using \\b and not using the unicode flag probably doesn't
// work with non ascii alphabets, findAndReplace uses a more
// complicated regexp, see
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145
const wholeWordChar = wholeWord ? "\\b" : "";
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar,
'g' + (matchCase ? '' : 'i'));
let curLine = 0;
let curChar = 0;
let curMatch = null;
findResult = [];
// All those markText take several seconds on eg this ~500-line
// script, batch them inside an operation so they become
// unnoticeable. Alternatively, an overlay could be used, see
// https://codemirror.net/addon/search/match-highlighter.js ?
codeEditor.operation(() => {
for (let i = 0; i < text.length; ++i) {
// Fetch next match if it's the first time or
// if past the current match start
if ((curMatch == null) || (curMatch.index < i)) {
curMatch = re.exec(text);
if (curMatch == null) {
// No more matches
break;
}
}
// Create a non-selected highlight marker for the match, the
// selected marker highlight will be done later
if (i === curMatch.index) {
let fromPos = { "line" : curLine, "ch" : curChar };
// XXX If multiline is supported, this needs to
// recalculate curLine since the match may span
// lines
let toPos = { "line" : curLine, "ch" : curChar + curMatch[0].length};
// XXX or css = "color: #f3"
let marker = doc.markText( fromPos, toPos, { "className" : FIND_RESULT_CSS_CLASSNAME });
findResult.push(marker);
// Set the first match beyond the cursor as current
// match
if (currentFound === -1) {
const cursorPos = codeEditor.getCursor();
if ((fromPos.line > cursorPos.line) ||
((fromPos.line === cursorPos.line) &&
(fromPos.ch >= cursorPos.ch))){
currentFound = totalFound;
}
}
totalFound++;
}
// Do line and char position tracking
if (text[i] === "\n") {
curLine++;
curChar = 0;
} else {
curChar++;
}
}
});
}
this.findResult = findResult;
// Calculate curfound if not already, highlight it as selected
if (totalFound > 0) {
currentFound = Math.max(0, currentFound)
let marker = findResult[currentFound];
let pos = marker.find();
codeEditor.scrollIntoView(pos.to);
marker.clear();
findResult[currentFound] = doc.markText( pos.from, pos.to,
{ "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME }
);
}
return {
totalFound,
currentFound: currentFound + 1
};
}
async findNext(direction, currentFound, nextFound) {
const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc;
//
// Dehighlight current, highlight & scrollIntoView next
//
let marker = this.findResult[currentFound];
let pos = marker.find();
marker.clear();
marker = doc.markText(
pos.from, pos.to,
{ "className" : FIND_RESULT_CSS_CLASSNAME }
);
this.findResult[currentFound] = marker;
marker = this.findResult[nextFound];
pos = marker.find();
marker.clear();
marker = doc.markText(
pos.from, pos.to,
{ "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME }
);
this.findResult[nextFound] = marker;
codeEditor.scrollIntoView(pos.from);
}
async cleanup(totalFound, currentFound) {
const codeEditor = await this.getCodeEditor();
if (totalFound > 0) {
const doc = codeEditor.doc;
const pos = this.findResult[currentFound].find();
// Note setting the selection sets the cursor to
// the end of the selection and scrolls it into
// view
doc.setSelection(pos.from, pos.to);
// Clear all markers
codeEditor.operation(() => {
for (let i = 0; i < this.findResult.length; ++i) {
let marker = this.findResult[i];
marker.clear();
}
});
}
// Restore the highlightSelectionMatches setting
codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches);
this.findResult = null;
}
async close() {
const codeEditor = await this.getCodeEditor();
codeEditor.focus();
}
}