diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html index 7cb97dfa4..fe896a184 100644 --- a/docs/frontend_api/services_frontend_script_api.js.html +++ b/docs/frontend_api/services_frontend_script_api.js.html @@ -367,25 +367,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * Adds given text to the editor cursor * + * @deprecated use addTextToActiveContextEditor() instead * @param {string} text - this must be clear text, HTML is not supported. * @method */ - this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + this.addTextToActiveTabEditor = text => { + console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead."); + + return appContext.triggerCommand('addTextToActiveEditor', {text}); + }; + + /** + * Adds given text to the editor cursor + * + * @param {string} text - this must be clear text, HTML is not supported. + * @method + */ + this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + + /** + * @method + * @deprecated use getActiveContextNote() instead + * @returns {NoteShort} active note (loaded into right pane) + */ + this.getActiveTabNote = () => { + console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead."); + + return appContext.tabManager.getActiveContextNote(); + }; /** * @method * @returns {NoteShort} active note (loaded into right pane) */ - this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote(); + this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); + + /** + * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. + * + * @deprecated use getActiveContextTextEditor() + * @method + * @param [callback] - callback receiving "textEditor" instance + */ + this.getActiveTabTextEditor = callback => { + console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); + + return appContext.tabManager.getActiveContextTextEditor(callback); + }; /** * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. * * @method - * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance * @returns {Promise<CKEditor>} instance of CKEditor */ - this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContextTextEditor(); /** * See https://codemirror.net/doc/manual.html#api @@ -393,7 +429,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise<CodeMirror>} instance of CodeMirror */ - this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); + this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContextCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the @@ -406,9 +442,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * @method + * @deprecated use getActiveContextNotePath() instead * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note */ - this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); + this.getActiveTabNotePath = () => { + console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead."); + + return appContext.tabManager.getActiveContextNotePath(); + }; + + /** + * @method + * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note + */ + this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); /** * Returns component which owns given DOM element (the nearest parent component in DOM tree) diff --git a/package-lock.json b/package-lock.json index 36bc60d66..0301aa51e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", - "electron-find": "1.0.7", "electron-window-state": "5.0.3", "express": "4.18.1", "express-partial-content": "1.0.2", @@ -3575,11 +3574,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-find": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.7.tgz", - "integrity": "sha512-C2FQJuk8567P2a2loBNwl5c8kwOTQVMB0capgHtPI7zKwZG16X0UxG+sNYZExQfnJ0PA+ecECA/4LcXxQa2TCA==" - }, "node_modules/electron-installer-common": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", @@ -13742,11 +13736,6 @@ "unused-filename": "^2.1.0" } }, - "electron-find": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.7.tgz", - "integrity": "sha512-C2FQJuk8567P2a2loBNwl5c8kwOTQVMB0capgHtPI7zKwZG16X0UxG+sNYZExQfnJ0PA+ecECA/4LcXxQa2TCA==" - }, "electron-installer-common": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", diff --git a/package.json b/package.json index 1cbd862b4..ed817a939 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", - "electron-find": "1.0.7", "electron-window-state": "5.0.3", "express": "4.18.1", "express-partial-content": "1.0.2", diff --git a/src/public/app/dialogs/markdown_import.js b/src/public/app/dialogs/markdown_import.js index e83522cf7..94712b28e 100644 --- a/src/public/app/dialogs/markdown_import.js +++ b/src/public/app/dialogs/markdown_import.js @@ -16,7 +16,7 @@ async function convertMarkdownToHtml(text) { const result = writer.render(parsed); - appContext.triggerCommand('executeInActiveTextEditor', { + appContext.triggerCommand('executeInTextEditor', { callback: textEditor => { const viewFragment = textEditor.data.processor.toView(result); const modelFragment = textEditor.data.toModel(viewFragment); @@ -24,7 +24,8 @@ async function convertMarkdownToHtml(text) { textEditor.model.insertContent(modelFragment, textEditor.model.document.selection); toastService.showMessage("Markdown content has been imported into the document."); - } + }, + ntxId: this.ntxId }); } diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 1b85cc71b..2723076ce 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -48,6 +48,7 @@ import BookmarkButtons from "../widgets/bookmark_buttons.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; import BacklinksWidget from "../widgets/backlinks.js"; import SharedInfoWidget from "../widgets/shared_info.js"; +import FindWidget from "../widgets/find.js"; export default class DesktopLayout { constructor(customWidgets) { @@ -161,6 +162,7 @@ export default class DesktopLayout { .child(new SearchResultWidget()) .child(new SqlResultWidget()) ) + .child(new FindWidget()) .child(...this.customWidgets.get('node-detail-pane')) ) ) diff --git a/src/public/app/services/entrypoints.js b/src/public/app/services/entrypoints.js index cd05a515c..a794232b7 100644 --- a/src/public/app/services/entrypoints.js +++ b/src/public/app/services/entrypoints.js @@ -39,29 +39,6 @@ export default class Entrypoints extends Component { } } - findInTextCommand() { - if (!utils.isElectron()) { - return; - } - - const remote = utils.dynamicRequire('@electron/remote'); - const {FindInPage} = utils.dynamicRequire('electron-find'); - const findInPage = new FindInPage(remote.getCurrentWebContents(), { - offsetTop: 10, - offsetRight: 10, - boxBgColor: 'var(--main-background-color)', - boxShadowColor: '#000', - inputColor: 'var(--input-text-color)', - inputBgColor: 'var(--input-background-color)', - inputFocusColor: '#555', - textColor: 'var(--main-text-color)', - textHoverBgColor: '#555', - caseSelectedColor: 'var(--main-border-color)' - }); - - findInPage.openFindWindow(); - } - async createNoteIntoInboxCommand() { const inboxNote = await dateNoteService.getInboxNote(); diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index b5e540344..b2eabdc13 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -339,25 +339,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * Adds given text to the editor cursor * + * @deprecated use addTextToActiveContextEditor() instead * @param {string} text - this must be clear text, HTML is not supported. * @method */ - this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + this.addTextToActiveTabEditor = text => { + console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead."); + + return appContext.triggerCommand('addTextToActiveEditor', {text}); + }; + + /** + * Adds given text to the editor cursor + * + * @param {string} text - this must be clear text, HTML is not supported. + * @method + */ + this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); + + /** + * @method + * @deprecated use getActiveContextNote() instead + * @returns {NoteShort} active note (loaded into right pane) + */ + this.getActiveTabNote = () => { + console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead."); + + return appContext.tabManager.getActiveContextNote(); + }; /** * @method * @returns {NoteShort} active note (loaded into right pane) */ - this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote(); + this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); + + /** + * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. + * + * @deprecated use getActiveContextTextEditor() + * @method + * @param [callback] - callback receiving "textEditor" instance + */ + this.getActiveTabTextEditor = callback => { + console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); + + return appContext.tabManager.getActiveContext()?.getTextEditor(callback); + }; /** * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. * * @method - * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance * @returns {Promise} instance of CKEditor */ - this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); /** * See https://codemirror.net/doc/manual.html#api @@ -365,7 +401,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise} instance of CodeMirror */ - this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); + this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the @@ -378,9 +414,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain /** * @method + * @deprecated use getActiveContextNotePath() instead * @returns {Promise} returns note path of active note or null if there isn't active note */ - this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); + this.getActiveTabNotePath = () => { + console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead."); + + return appContext.tabManager.getActiveContextNotePath(); + }; + + /** + * @method + * @returns {Promise} returns note path of active note or null if there isn't active note + */ + this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); /** * Returns component which owns given DOM element (the nearest parent component in DOM tree) diff --git a/src/public/app/services/note_context.js b/src/public/app/services/note_context.js index 2b7e71fa3..179fd1aea 100644 --- a/src/public/app/services/note_context.js +++ b/src/public/app/services/note_context.js @@ -226,6 +226,21 @@ class NoteContext extends Component { && this.note.mime !== 'text/x-sqlite;schema=trilium' && !this.note.hasLabel('hideChildrenOverview'); } + + async getTextEditor(callback) { + return new Promise(resolve => appContext.triggerCommand('executeInTextEditor', { + callback, + resolve, + ntxId: this.ntxId + })); + } + + async getCodeEditor() { + return new Promise(resolve => appContext.triggerCommand('executeInCodeEditor', { + resolve, + ntxId: this.ntxId + })); + } } export default NoteContext; diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js new file mode 100644 index 000000000..9e0590ce9 --- /dev/null +++ b/src/public/app/widgets/find.js @@ -0,0 +1,235 @@ +/** + * (c) Antonio Tejada 2022 + * https://github.com/antoniotejada/Trilium-FindWidget + */ + +import NoteContextAwareWidget from "./note_context_aware_widget.js"; +import FindInText from "./find_in_text.js"; +import FindInCode from "./find_in_code.js"; + +const findWidgetDelayMillis = 200; +const waitForEnter = (findWidgetDelayMillis < 0); + +// tabIndex=-1 on the checkbox labels is necessary so when clicking on the label +// the focusout handler is called with relatedTarget equal to the label instead +// of undefined. It's -1 instead of > 0, so they don't tabstop +const TPL = ` +
+ + +
+ + +
+ +
+ +
+ +
+ +
+ 0 + / + 0 +
+ +
+ +
+
+
`; + +export default class FindWidget extends NoteContextAwareWidget { + constructor() { + super(); + + this.searchTerm = null; + + this.textHandler = new FindInText(this); + this.codeHandler = new FindInCode(this); + } + + doRender() { + this.$widget = $(TPL); + this.$findBox = this.$widget.find('.find-widget-box'); + this.$findBox.hide(); + this.$input = this.$widget.find('.find-widget-search-term-input'); + this.$currentFound = this.$widget.find('.find-widget-current-found'); + this.$totalFound = this.$widget.find('.find-widget-total-found'); + this.$caseSensitiveCheckbox = this.$widget.find(".find-widget-case-sensitive-checkbox"); + this.$caseSensitiveCheckbox.change(() => this.performFind()); + this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); + this.$matchWordsCheckbox.change(() => this.performFind()); + this.$closeButton = this.$widget.find(".find-widget-close-button"); + this.$closeButton.on("click", () => this.closeSearch()); + + this.$input.keydown(async e => { + if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { + // If ctrl+f is pressed when the findbox is shown, select the + // whole input to find + this.$input.select(); + } else if (e.key === 'Enter' || e.key === 'F3') { + await this.findNext(e); + e.preventDefault(); + return false; + } else if (e.key === 'Escape') { + await this.closeSearch(); + } + }); + + this.$input.on('input', () => this.startSearch()); + + return this.$widget; + } + + startSearch() { + // XXX This should clear the previous search immediately in all cases + // (the search is stale when waitforenter but also while the + // delay is running for non waitforenter case) + if (!waitForEnter) { + // Clear the previous timeout if any, it's ok if timeoutId is + // null or undefined + clearTimeout(this.timeoutId); + + // Defer the search a few millis so the search doesn't start + // immediately, as this can cause search word typing lag with + // one or two-char searchwords and long notes + // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 + this.timeoutId = setTimeout(async () => { + this.timeoutId = null; + await this.performFind(); + }, findWidgetDelayMillis); + } + } + + async findNext(e) { + const searchTerm = this.$input.val(); + if (waitForEnter && this.searchTerm !== searchTerm) { + await this.performFind(); + } + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; + + if (totalFound > 0) { + const direction = e.shiftKey ? -1 : 1; + let nextFound = currentFound + direction; + // Wrap around + if (nextFound > totalFound - 1) { + nextFound = 0; + } else if (nextFound < 0) { + nextFound = totalFound - 1; + } + + this.$currentFound.text(nextFound + 1); + + await this.handler.findNext(direction, currentFound, nextFound); + } + } + + async findInTextEvent() { + if (!this.isActiveNoteContext()) { + return; + } + + // Only writeable text and code supported + const readOnly = await this.noteContext.isReadOnly(); + + if (readOnly || !['text', 'code'].includes(this.note.type) || !this.$findBox.is(":hidden")) { + return; + } + + this.$findBox.show(); + this.$input.focus(); + this.$totalFound.text(0); + this.$currentFound.text(0); + + const searchTerm = await this.handler.getInitialSearchTerm(); + + this.$input.val(searchTerm || ""); + + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + if (searchTerm !== "") { + this.$input.select(); + await this.performFind(); + } + } + + /** Perform the find and highlight the find results. */ + async performFind() { + const searchTerm = this.$input.val(); + const matchCase = this.$caseSensitiveCheckbox.prop("checked"); + const wholeWord = this.$matchWordsCheckbox.prop("checked"); + + const {totalFound, currentFound} = await this.handler.performFind(searchTerm, matchCase, wholeWord); + + this.$totalFound.text(totalFound); + this.$currentFound.text(currentFound); + + this.searchTerm = searchTerm; + } + + async closeSearch() { + this.$findBox.hide(); + + // Restore any state, if there's a current occurrence clear markers + // and scroll to and select the last occurrence + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; + + if (totalFound > 0) { + await this.handler.cleanup(totalFound, currentFound); + } + + this.searchTerm = null; + } + + async entitiesReloadedEvent({loadResults}) { + if (loadResults.isNoteContentReloaded(this.noteId)) { + this.refresh(); + } + } + + isEnabled() { + return super.isEnabled() && ['text', 'code'].includes(this.note.type); + } + + get handler() { + return this.note.type === "code" + ? this.codeHandler + : this.textHandler; + } +} diff --git a/src/public/app/widgets/find_in_code.js b/src/public/app/widgets/find_in_code.js new file mode 100644 index 000000000..7a6bcbced --- /dev/null +++ b/src/public/app/widgets/find_in_code.js @@ -0,0 +1,198 @@ +// 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(); + } +} diff --git a/src/public/app/widgets/find_in_text.js b/src/public/app/widgets/find_in_text.js new file mode 100644 index 000000000..54f3bd091 --- /dev/null +++ b/src/public/app/widgets/find_in_text.js @@ -0,0 +1,120 @@ +export default class FindInText { + constructor(parent) { + this.parent = parent; + } + + async getTextEditor() { + return this.parent.noteContext.getTextEditor(); + } + + async getInitialSearchTerm() { + const textEditor = await this.getTextEditor(); + + const selection = textEditor.model.document.selection; + const range = selection.getFirstRange(); + + for (const item of range.getItems()) { + // Fill in the findbox with the current selection if + // any + return item.data; + } + } + + async performFind(searchTerm, matchCase, wholeWord) { + // Do this even if the searchTerm is empty so the markers are cleared and + // the counters updated + const textEditor = await this.getTextEditor(); + const model = textEditor.model; + let findResult = null; + let totalFound = 0; + let currentFound = -1; + + // Clear + const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.stop(); + 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; + const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; + findResult = textEditor.execute('find', searchTerm, options); + 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 (fromPos.compareWith(cursorPos) !== "before") { + 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? + // See + // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js + for (let i = 0 ; i < currentFound; ++i) { + textEditor.execute('findNext', searchTerm); + } + } + + return { + totalFound, + currentFound: currentFound + 1 + }; + } + + async findNext(direction, currentFound, nextFound) { + const textEditor = await this.getTextEditor(); + + // 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) { + textEditor.execute('findNext'); + } else { + textEditor.execute('findPrevious'); + } + } + + async cleanup(totalFound, currentFound) { + if (totalFound > 0) { + const textEditor = await this.getTextEditor(); + // Clear the markers and set the caret to the + // current occurrence + const model = textEditor.model; + const range = this.findResult.results.get(currentFound).marker.getRange(); + // 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? + let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.stop(); + model.change(writer => { + writer.setSelection(range, 0); + }); + textEditor.editing.view.scrollToTheSelection(); + } + + this.findResult = null; + } + + async close() { + const textEditor = await this.getTextEditor(); + textEditor.focus(); + } +} diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js index 7bbc356c3..2a4097f1c 100644 --- a/src/public/app/widgets/type_widgets/editable_code.js +++ b/src/public/app/widgets/type_widgets/editable_code.js @@ -171,13 +171,13 @@ export default class EditableCodeTypeWidget extends TypeWidget { } } - async executeInActiveCodeEditorEvent({callback}) { - if (!this.isActive()) { + async executeInCodeEditorEvent({resolve, ntxId}) { + if (!this.isNoteContext(ntxId)) { return; } await this.initialized; - callback(this.codeEditor); + resolve(this.codeEditor); } } diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index 44469fdc6..f51138289 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -229,8 +229,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { return !selection.isCollapsed; } - async executeInActiveTextEditorEvent({callback, resolve}) { - if (!this.isActive()) { + async executeInTextEditorEvent({callback, resolve, ntxId}) { + if (!this.isNoteContext(ntxId)) { return; }