From 87f436c6eadeaca0527e596f927922a469dcf599 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 9 May 2022 23:13:34 +0200 Subject: [PATCH 01/12] search dialog WIP from custom widget from antoniotejada --- docs/frontend_api/FrontendScriptApi.html | 755 ++++++++++++++---- .../services_frontend_script_api.js.html | 59 +- package-lock.json | 14 +- package.json | 3 +- src/public/app/layouts/desktop_layout.js | 2 + src/public/app/services/entrypoints.js | 23 - .../app/services/frontend_script_api.js | 59 +- src/public/app/services/tab_manager.js | 8 + src/public/app/widgets/find.js | 556 +++++++++++++ 9 files changed, 1280 insertions(+), 199 deletions(-) create mode 100644 src/public/app/widgets/find.js diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html index 4a1c6b2da..6000fae0f 100644 --- a/docs/frontend_api/FrontendScriptApi.html +++ b/docs/frontend_api/FrontendScriptApi.html @@ -1671,7 +1671,7 @@ -

addTextToActiveTabEditor(text)

+

addTextToActiveContextEditor(text)

@@ -1772,7 +1772,146 @@
Source:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

addTextToActiveTabEditor(text)

+ + + + + + +
+ Adds given text to the editor cursor +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
text + + +string + + + + this must be clear text, HTML is not supported.
+ + + + + + +
+ + + + + + + + + + + + + + + + +
Deprecated:
  • use addTextToActiveContextEditor() instead
+ + + + + + + + + + + +
Source:
+
@@ -1928,7 +2067,7 @@
Source:
@@ -2479,114 +2618,7 @@ -

getActiveNoteDetailWidget() → {Promise.<NoteDetailWidget>}

- - - - - - -
- Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the -implementation of actual widget type. -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - -
Returns:
- - - - -
-
- Type -
-
- -Promise.<NoteDetailWidget> - - -
-
- - - - - - - - - - - - - -

getActiveTabCodeEditor() → {Promise.<CodeMirror>}

+

getActiveContextCodeEditor() → {Promise.<CodeMirror>}

@@ -2638,7 +2670,7 @@ implementation of actual widget type.
Source:
@@ -2696,7 +2728,7 @@ implementation of actual widget type. -

getActiveTabNote() → {NoteShort}

+

getActiveContextNote() → {NoteShort}

@@ -2744,7 +2776,438 @@ implementation of actual widget type.
Source:
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ active note (loaded into right pane) +
+ + + +
+
+ Type +
+
+ +NoteShort + + +
+
+ + + + + + + + + + + + + +

getActiveContextNotePath() → {Promise.<(string|null)>}

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ returns note path of active note or null if there isn't active note +
+ + + +
+
+ Type +
+
+ +Promise.<(string|null)> + + +
+
+ + + + + + + + + + + + + +

getActiveContextTextEditor() → {Promise.<CKEditor>}

+ + + + + + +
+ See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ instance of CKEditor +
+ + + +
+
+ Type +
+
+ +Promise.<CKEditor> + + +
+
+ + + + + + + + + + + + + +

getActiveNoteDetailWidget() → {Promise.<NoteDetailWidget>}

+ + + + + + +
+ Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the +implementation of actual widget type. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Promise.<NoteDetailWidget> + + +
+
+ + + + + + + + + + + + + +

getActiveTabNote() → {NoteShort}

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
Deprecated:
  • use getActiveContextNote() instead
+ + + + + + + + + + + +
Source:
+
@@ -2838,6 +3301,8 @@ implementation of actual widget type. +
Deprecated:
  • use getActiveContextNotePath() instead
+ @@ -2850,7 +3315,7 @@ implementation of actual widget type.
Source:
@@ -2908,7 +3373,7 @@ implementation of actual widget type. -

getActiveTabTextEditor(callbackopt) → {Promise.<CKEditor>}

+

getActiveTabTextEditor(callbackopt)

@@ -2975,7 +3440,7 @@ implementation of actual widget type. - deprecated (use returned promise): callback receiving "textEditor" instance + callback receiving "textEditor" instance @@ -3004,6 +3469,8 @@ implementation of actual widget type. +
Deprecated:
  • use getActiveContextTextEditor()
+ @@ -3016,7 +3483,7 @@ implementation of actual widget type.
Source:
@@ -3041,28 +3508,6 @@ implementation of actual widget type. -
Returns:
- - -
- instance of CKEditor -
- - - -
-
- Type -
-
- -Promise.<CKEditor> - - -
-
- - @@ -3175,7 +3620,7 @@ implementation of actual widget type.
Source:
@@ -3332,7 +3777,7 @@ implementation of actual widget type.
Source:
@@ -3487,7 +3932,7 @@ implementation of actual widget type.
Source:
@@ -3749,7 +4194,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4212,7 +4657,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4367,7 +4812,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4522,7 +4967,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4959,7 +5404,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5115,7 +5560,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5271,7 +5716,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5408,7 +5853,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5562,7 +6007,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6503,7 +6948,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6654,7 +7099,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7020,7 +7465,7 @@ Typical use case is when new note has been created, we should wait until it is s
Source:
diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html index 0f47ab437..8a4cbef6e 100644 --- a/docs/frontend_api/services_frontend_script_api.js.html +++ b/docs/frontend_api/services_frontend_script_api.js.html @@ -349,25 +349,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 @@ -375,7 +411,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 @@ -388,9 +424,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 626b48aab..1fd5ff38e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.1", + "dayjs": "1.11.2", "ejs": "3.1.7", "electron-debug": "3.2.0", "electron-dl": "3.3.1", @@ -3200,9 +3200,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", - "integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==" + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, "node_modules/debug": { "version": "4.3.3", @@ -13413,9 +13413,9 @@ } }, "dayjs": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", - "integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==" + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz", + "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" }, "debug": { "version": "4.3.3", diff --git a/package.json b/package.json index a140c3194..dc38d7816 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,10 @@ "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.1", + "dayjs": "1.11.2", "ejs": "3.1.7", "electron-debug": "3.2.0", "electron-dl": "3.3.1", - "electron-find": "1.0.7", "electron-window-state": "5.0.3", "@electron/remote": "2.0.8", "express": "4.18.1", diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 1b85cc71b..a5429521e 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) { @@ -164,6 +165,7 @@ export default class DesktopLayout { .child(...this.customWidgets.get('node-detail-pane')) ) ) + .child(new FindWidget()) .child(...this.customWidgets.get('center-pane')) ) .child(new RightPaneContainer() diff --git a/src/public/app/services/entrypoints.js b/src/public/app/services/entrypoints.js index 42e4f79ee..10292e2c9 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 2cd12f8bc..257fee0df 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -321,25 +321,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} 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 @@ -347,7 +383,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.getActiveContextCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the @@ -360,9 +396,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/tab_manager.js b/src/public/app/services/tab_manager.js index 15f939bfe..1250b6948 100644 --- a/src/public/app/services/tab_manager.js +++ b/src/public/app/services/tab_manager.js @@ -193,6 +193,14 @@ export default class TabManager extends Component { return activeNote ? activeNote.type : null; } + async getActiveContextTextEditor(callback) { + return new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + } + + async getActiveContextCodeEditor() { + return new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {resolve})); + } + async switchToNoteContext(ntxId, notePath) { const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId) || await this.openEmptyTab(); diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js new file mode 100644 index 000000000..99b7e2f0e --- /dev/null +++ b/src/public/app/widgets/find.js @@ -0,0 +1,556 @@ +/** + * Find in note replacement for Trilium ctrl+f search + * (c) Antonio Tejada 2022 + * + * Features: + * - Find in writeable using ctrl+f and F3 + * - Tested on Trilium Desktop 0.50.3 + * + * Installation: + * - Create a code note of language JS Frontend with the contents of this file + * - Set the owned attributes (alt-a) to #widget + * - Set the owned attributes of any note you don't want to enable finding to + * #noFindWidget + * - Disable Ctrl+f shorcut in Trilium options + * + * Todo: + * - Refactoring/code cleanup + * - Case-sensitive option + * - Regexp option + * - Full word option + * - Find & Replace + * + * Note that many times some common code is replicated between CodeMirror and + * CKEditor codepaths because the CKEditor update is done inside a callback that + * is deferred so the code cannot be put outside of the callback or it will + * execute too early. + * + * See https://github.com/zadam/trilium/discussions/2806 for discussions + */ + +import NoteContextAwareWidget from "./note_context_aware_widget.js"; +import appContext from "../services/app_context.js"; + +function getNoteAttributeValue(note, attributeType, attributeName, defaultValue) { + let attribute = note.getAttribute(attributeType, attributeName); + + let attributeValue = (attribute != null) ? attribute.value : defaultValue; + + return attributeValue; +} + +const findWidgetDelayMillis = 200; +const waitForEnter = (findWidgetDelayMillis < 0); + +const TEMPLATE = `
+
+ +  case +  regexp + 0/0 +
+
`; + +const tag = "FindWidget"; +const debugLevels = ["error", "warn", "info", "log", "debug"]; +const debugLevel = "info"; + +let warn = function() {}; +if (debugLevel >= debugLevels.indexOf("warn")) { + warn = console.warn.bind(console, tag + ": "); +} + +let info = function() {}; +if (debugLevel >= debugLevels.indexOf("info")) { + info = console.info.bind(console, tag + ": "); +} + +let log = function() {}; +if (debugLevel >= debugLevels.indexOf("log")) { + log = console.log.bind(console, tag + ": "); +} + +let dbg = function() {}; +if (debugLevel >= debugLevels.indexOf("debug")) { + dbg = console.debug.bind(console, tag + ": "); +} + +function assert(e, msg) { + console.assert(e, tag + ": " + msg); +} + +function debugbreak() { + debugger; +} + + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function getActiveContextCodeEditor() { + return await appContext.tabManager.getActiveContextCodeEditor(); +} + +async function getActiveContextTextEditor() { + return await appContext.tabManager.getActiveContextTextEditor(); +} + +// 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"; + +export default class FindWidget extends NoteContextAwareWidget { + constructor(...args) { + super(...args); + this.$widget = $(TEMPLATE); + this.$findBox = this.$widget.find('#findBox'); + this.$input = this.$widget.find('#input'); + this.$curFound = this.$widget.find('#curFound'); + this.$numFound = this.$widget.find('#numFound'); + this.findResult = null; + this.prevFocus = null; + this.nedle = null; + let findWidget = this; + + // XXX Use api.bindGlobalShortcut? + $(window).keydown(async function (e){ + dbg("keydown on window " + e.key); + if ((e.key == 'F3') || + // Note that for ctrl+f to work, needs to be disabled in Trilium's + // shortcut config menu + // XXX Maybe not if using bindShorcut? + ((e.metaKey || e.ctrlKey) && ((e.key == 'f') || (e.key == 'F')))) { + + const note = appContext.tabManager.getActiveContextNote(); + // Only writeable text and code supported + const readOnly = note.getAttribute("label", "readOnly"); + if (!readOnly && ((note.type == "code") || (note.type == "text"))) { + if (findWidget.$findBox.is(":hidden")) { + + findWidget.$findBox.show(); + findWidget.$input.focus(); + findWidget.$numFound.text(0); + findWidget.$curFound.text(0); + + // Initialize the input field to the text selection, if any + if (note.type == "code") { + let codeEditor = getActiveContextCodeEditor(); + + // 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 + findWidget.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); + codeEditor.setOption("highlightSelectionMatches", false); + + // Fill in the findbox with the current selection if any + const selectedText = codeEditor.getSelection() + if (selectedText != "") { + findWidget.$input.val(selectedText); + } + // Directly perform the search if there's some text to find, + // without delaying or waiting for enter + const needle = findWidget.$input.val(); + if (needle != "") { + findWidget.$input.select(); + await findWidget.performFind(needle); + } + } else { + const textEditor = await getActiveContextTextEditor(); + + 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 + findWidget.$input.val(item.data); + break; + } + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + const needle = findWidget.$input.val(); + if (needle != "") { + findWidget.$input.select(); + await findWidget.performFind(needle); + } + } + } + e.preventDefault(); + return false; + } + } + return true; + }); + + findWidget.$input.keydown(async function (e) { + dbg("keydown on input " + e.key); + 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 + findWidget.$input.select(); + } else if ((e.key == 'Enter') || (e.key == 'F3')) { + const needle = findWidget.$input.val(); + if (waitForEnter && (findWidget.needle != needle)) { + await findWidget.performFind(needle); + } + let numFound = parseInt(findWidget.$numFound.text()); + let curFound = parseInt(findWidget.$curFound.text()) - 1; + dbg("Finding " + curFound + "/" + numFound + " occurrence of " + findWidget.$input.val()); + if (numFound > 0) { + let delta = e.shiftKey ? -1 : 1; + let nextFound = curFound + delta; + // Wrap around + if (nextFound > numFound - 1) { + nextFound = 0; + } if (nextFound < 0) { + nextFound = numFound - 1; + } + + let needle = findWidget.$input.val(); + findWidget.$curFound.text(nextFound + 1); + + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + let codeEditor = getActiveContextCodeEditor(); + let doc = codeEditor.doc; + + // + // Dehighlight current, highlight & scrollIntoView next + // + + let marker = findWidget.findResult[curFound]; + let pos = marker.find(); + marker.clear(); + marker = doc.markText( + pos.from, pos.to, + { "className" : FIND_RESULT_CSS_CLASSNAME } + ); + findWidget.findResult[curFound] = marker; + + marker = findWidget.findResult[nextFound]; + pos = marker.find(); + marker.clear(); + marker = doc.markText( + pos.from, pos.to, + { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } + ); + findWidget.findResult[nextFound] = marker; + + codeEditor.scrollIntoView(pos.from); + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + const textEditor = await getActiveContextTextEditor(); + + const model = textEditor.model; + const doc = model.document; + const root = doc.getRoot(); + // See + // 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 + // needle = escapeRegExp(needle); + // cufFound wrap around assumes findNext and findPrevious + // wraparound, which is what they do + if (delta > 0) { + textEditor.execute('findNext', needle); + } else { + textEditor.execute('findPrevious', needle); + } + } + } + e.preventDefault(); + return false; + } else if (e.key == 'Escape') { + let numFound = parseInt(findWidget.$numFound.text()); + + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + let codeEditor = getActiveContextCodeEditor(); + + codeEditor.focus(); + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + const textEditor = await getActiveContextTextEditor(); + textEditor.focus(); + } + } + // e.preventDefault(); + }); + + findWidget.$input.on('input', function (e) { + // 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(findWidget.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 + const needle = findWidget.$input.val(); + findWidget.timeoutId = setTimeout(async function () { + findWidget.timeoutId = null; + await findWidget.performFind(needle); + }, findWidgetDelayMillis); + } + }); + + findWidget.$input.blur(async function () { + findWidget.$findBox.hide(); + + // Restore any state, if there's a current occurrence clear markers + // and scroll to and select the last occurrence + + // XXX Switching to a different tab with crl+tab doesn't invoke + // blur and leaves a stale search which then breaks when + // navigating it + let numFound = parseInt(findWidget.$numFound.text()); + let curFound = parseInt(findWidget.$curFound.text()) - 1; + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + let codeEditor = await getActiveContextCodeEditor(); + if (numFound > 0) { + let doc = codeEditor.doc; + let pos = findWidget.findResult[curFound].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(function() { + for (let i = 0; i < findWidget.findResult.length; ++i) { + let marker = findWidget.findResult[i]; + marker.clear(); + } + }); + } + // Restore the highlightSelectionMatches setting + codeEditor.setOption("highlightSelectionMatches", findWidget.oldHighlightSelectionMatches); + findWidget.findResult = null; + findWidget.needle = null; + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + if (numFound > 0) { + const textEditor = await getActiveContextTextEditor(); + // Clear the markers and set the caret to the + // current occurrence + const model = textEditor.model; + let range = findWidget.findResult.results.get(curFound).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(); + findWidget.findResult = null; + findWidget.needle = null; + } else { + findWidget.findResult = null; + findWidget.needle = null; + } + } + }); + } + + async performTextNoteFind(needle) { + // Do this even if the needle is empty so the markers are cleared and + // the counters updated + const textEditor = await getActiveContextTextEditor(); + const model = textEditor.model; + let findResult = null; + let numFound = 0; + let curFound = -1; + + // Clear + let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); + log("findAndReplace clearing"); + findAndReplaceEditing.state.clear(model); + log("findAndReplace stopping"); + findAndReplaceEditing.stop(); + if (needle != "") { + // 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 + // needle = escapeRegExp(needle); + // let re = new RegExp(needle, 'gi'); + // let m = text.match(re); + // numFound = m ? m.length : 0; + log("findAndReplace starts"); + findResult = textEditor.execute('find', needle); + log("findAndReplace ends"); + numFound = findResult.results.length; + // Find the result beyond the cursor + log("findAndReplace positioning"); + let cursorPos = model.document.selection.getLastPosition(); + for (let i = 0; i < findResult.results.length; ++i) { + let marker = findResult.results.get(i).marker; + let fromPos = marker.getStart(); + if (fromPos.compareWith(cursorPos) != "before") { + curFound = i; + break; + } + } + log("findAndReplace positioned"); + } + + this.findResult = findResult; + this.$numFound.text(numFound); + // Calculate curfound if not already, highlight it as + // selected + if (numFound > 0) { + curFound = Math.max(0, curFound); + // 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 < curFound; ++i) { + textEditor.execute('findNext', needle); + } + } + this.$curFound.text(curFound + 1); + this.needle = needle; + } + + async performCodeNoteFind(needle) { + let findResult = null; + let numFound = 0; + let curFound = -1; + + // See https://codemirror.net/addon/search/searchcursor.js for tips + let codeEditor = await getActiveContextCodeEditor(); + let doc = codeEditor.doc; + let text = doc.getValue(); + + // Clear all markers + if (this.findResult != null) { + const findWidget = this; + codeEditor.operation(function() { + for (let i = 0; i < findWidget.findResult.length; ++i) { + let marker = findWidget.findResult[i]; + marker.clear(); + } + }); + } + + if (needle != "") { + needle = escapeRegExp(needle); + + // Find and highlight matches + let re = new RegExp(needle, 'gi'); + 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(function() { + 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 (curFound == -1) { + let cursorPos = codeEditor.getCursor(); + if ((fromPos.line > cursorPos.line) || + ((fromPos.line == cursorPos.line) && + (fromPos.ch >= cursorPos.ch))){ + curFound = numFound; + } + } + + numFound++; + } + // Do line and char position tracking + if (text[i] == "\n") { + curLine++; + curChar = 0; + } else { + curChar++; + } + } + }); + } + + this.findResult = findResult; + this.$numFound.text(numFound); + // Calculate curfound if not already, highlight it as selected + if (numFound > 0) { + curFound = Math.max(0, curFound) + let marker = findResult[curFound]; + let pos = marker.find(); + codeEditor.scrollIntoView(pos.to); + marker.clear(); + findResult[curFound] = doc.markText( pos.from, pos.to, + { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } + ); + } + this.$curFound.text(curFound + 1); + this.needle = needle; + } + + async performFind(needle) { + const note = appContext.tabManager.getActiveContextNote(); + if (note.type == "code") { + await this.performCodeNoteFind(needle); + } else { + assert(note.type == "text", "Expected text note, found " + note.type); + await this.performTextNoteFind(needle); + } + } + + isEnabled() { + dbg("isEnabled"); + return super.isEnabled() + && ((this.note.type === 'text') || (this.note.type === 'code')) + && !this.note.hasLabel('noFindWidget'); + } + + doRender() { + dbg("doRender"); + this.$findBox.hide(); + return this.$widget; + } + + async refreshWithNote(note) { + dbg("refreshWithNote"); + } + + async entitiesReloadedEvent({loadResults}) { + dbg("entitiesReloadedEvent"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + this.refresh(); + } + } +} From 078fc420b0241313150cafb8c5b9ae0033ea0240 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 14 May 2022 21:06:14 +0200 Subject: [PATCH 02/12] findwidget merge from upstream --- src/public/app/widgets/find.js | 93 +++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 99b7e2f0e..80c7fdb6a 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -42,11 +42,15 @@ function getNoteAttributeValue(note, attributeType, attributeName, defaultValue) 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 TEMPLATE = `
-  case -  regexp + + + 0/0
`; @@ -110,9 +114,11 @@ export default class FindWidget extends NoteContextAwareWidget { this.$input = this.$widget.find('#input'); this.$curFound = this.$widget.find('#curFound'); this.$numFound = this.$widget.find('#numFound'); + this.$caseCheck = this.$widget.find("#caseCheck"); + this.$wordCheck = this.$widget.find("#wordCheck"); this.findResult = null; this.prevFocus = null; - this.nedle = null; + this.needle = null; let findWidget = this; // XXX Use api.bindGlobalShortcut? @@ -245,20 +251,14 @@ export default class FindWidget extends NoteContextAwareWidget { assert(note.type == "text", "Expected text note, found " + note.type); const textEditor = await getActiveContextTextEditor(); - const model = textEditor.model; - const doc = model.document; - const root = doc.getRoot(); - // See - // 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 - // needle = escapeRegExp(needle); - // cufFound wrap around assumes findNext and findPrevious - // wraparound, which is what they do + // 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 (delta > 0) { - textEditor.execute('findNext', needle); + textEditor.execute('findNext'); } else { - textEditor.execute('findPrevious', needle); + textEditor.execute('findPrevious'); } } } @@ -295,14 +295,37 @@ export default class FindWidget extends NoteContextAwareWidget { // one or two-char searchwords and long notes // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1 const needle = findWidget.$input.val(); + const matchCase = findWidget.$caseCheck.prop("checked"); + const wholeWord = findWidget.$wordCheck.prop("checked"); findWidget.timeoutId = setTimeout(async function () { findWidget.timeoutId = null; - await findWidget.performFind(needle); + await findWidget.performFind(needle, matchCase, wholeWord); }, findWidgetDelayMillis); } }); - findWidget.$input.blur(async function () { + findWidget.$caseCheck.change(function() { + log("caseCheck change"); + findWidget.performFind(); + }); + + findWidget.$wordCheck.change(function() { + log("wordCheck change"); + findWidget.performFind(); + }); + + // Note blur doesn't bubble to parent div, but the parent div needs to + // detect when any of the children are not focused and hide. Use + // focusout instead which does bubble to the parent div. + findWidget.$findBox.focusout(async function (e) { + // e.relatedTarget is the new focused element, note it can be null + // if nothing is being focused + log(`focusout ${e.target.id} related ${e.relatedTarget?.id}`); + if (findWidget.$findBox[0].contains(e.relatedTarget)) { + // The focused element is inside this div, ignore + log("focusout to child, ignoring"); + return; + } findWidget.$findBox.hide(); // Restore any state, if there's a current occurrence clear markers @@ -364,7 +387,7 @@ export default class FindWidget extends NoteContextAwareWidget { }); } - async performTextNoteFind(needle) { + async performTextNoteFind(needle, matchCase, wholeWord) { // Do this even if the needle is empty so the markers are cleared and // the counters updated const textEditor = await getActiveContextTextEditor(); @@ -388,7 +411,8 @@ export default class FindWidget extends NoteContextAwareWidget { // let m = text.match(re); // numFound = m ? m.length : 0; log("findAndReplace starts"); - findResult = textEditor.execute('find', needle); + const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; + findResult = textEditor.execute('find', needle, options); log("findAndReplace ends"); numFound = findResult.results.length; // Find the result beyond the cursor @@ -422,7 +446,7 @@ export default class FindWidget extends NoteContextAwareWidget { this.needle = needle; } - async performCodeNoteFind(needle) { + async performCodeNoteFind(needle, matchCase, wholeWord) { let findResult = null; let numFound = 0; let curFound = -1; @@ -447,7 +471,14 @@ export default class FindWidget extends NoteContextAwareWidget { needle = escapeRegExp(needle); // Find and highlight matches - let re = new RegExp(needle, 'gi'); + // 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" : ""; + let re = new RegExp(wholeWordChar + needle + wholeWordChar, + 'g' + (matchCase ? '' : 'i')); let curLine = 0; let curChar = 0; let curMatch = null; @@ -520,13 +551,27 @@ export default class FindWidget extends NoteContextAwareWidget { this.needle = needle; } - async performFind(needle) { + /** + * Perform the find and highlight the find results. + * + * @param needle {string} optional parameter, taken from the input box if + * missing. + * @param matchCase {boolean} optional parameter, taken from the checkbox + * state if missing. + * @param wholeWord {boolean} optional parameter, taken from the checkbox + * state if missing. + */ + async performFind(needle, matchCase, wholeWord) { + needle = (needle == undefined) ? this.$input.val() : needle; + matchCase = (matchCase === undefined) ? this.$caseCheck.prop("checked") : matchCase; + wholeWord = (wholeWord === undefined) ? this.$wordCheck.prop("checked") : wholeWord; + log(`performFind needle:${needle} case:${matchCase} word:${wholeWord}`); const note = appContext.tabManager.getActiveContextNote(); if (note.type == "code") { - await this.performCodeNoteFind(needle); + await this.performCodeNoteFind(needle, matchCase, wholeWord); } else { assert(note.type == "text", "Expected text note, found " + note.type); - await this.performTextNoteFind(needle); + await this.performTextNoteFind(needle, matchCase, wholeWord); } } From 36308c307ba11d7e1d144c8bea25c6af413889b0 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 14 May 2022 22:33:45 +0200 Subject: [PATCH 03/12] findwidget cleanup --- src/public/app/widgets/find.js | 158 ++++++++++----------------------- 1 file changed, 45 insertions(+), 113 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 80c7fdb6a..9ac090400 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -1,6 +1,7 @@ /** * Find in note replacement for Trilium ctrl+f search * (c) Antonio Tejada 2022 + * https://github.com/antoniotejada/Trilium-FindWidget * * Features: * - Find in writeable using ctrl+f and F3 @@ -31,74 +32,29 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import appContext from "../services/app_context.js"; -function getNoteAttributeValue(note, attributeType, attributeName, defaultValue) { - let attribute = note.getAttribute(attributeType, attributeName); - - let attributeValue = (attribute != null) ? attribute.value : defaultValue; - - return attributeValue; -} - 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 TEMPLATE = `
-
- - - - - 0/0 -
+// of undefined. It's -1 instead of > 0, so they don't tabstop +const TEMPLATE = ` +
+
+ + + + + 0/0 +
`; -const tag = "FindWidget"; -const debugLevels = ["error", "warn", "info", "log", "debug"]; -const debugLevel = "info"; - -let warn = function() {}; -if (debugLevel >= debugLevels.indexOf("warn")) { - warn = console.warn.bind(console, tag + ": "); -} - -let info = function() {}; -if (debugLevel >= debugLevels.indexOf("info")) { - info = console.info.bind(console, tag + ": "); -} - -let log = function() {}; -if (debugLevel >= debugLevels.indexOf("log")) { - log = console.log.bind(console, tag + ": "); -} - -let dbg = function() {}; -if (debugLevel >= debugLevels.indexOf("debug")) { - dbg = console.debug.bind(console, tag + ": "); -} - -function assert(e, msg) { - console.assert(e, tag + ": " + msg); -} - -function debugbreak() { - debugger; -} - - function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -async function getActiveContextCodeEditor() { - return await appContext.tabManager.getActiveContextCodeEditor(); -} - -async function getActiveContextTextEditor() { - return await appContext.tabManager.getActiveContextTextEditor(); -} +const getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); +const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); // ck-find-result and ck-find-result_selected are the styles ck-editor // uses for highlighting matches, use the same one on CodeMirror @@ -123,17 +79,16 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Use api.bindGlobalShortcut? $(window).keydown(async function (e){ - dbg("keydown on window " + e.key); - if ((e.key == 'F3') || + if ((e.key === 'F3') || // Note that for ctrl+f to work, needs to be disabled in Trilium's // shortcut config menu // XXX Maybe not if using bindShorcut? - ((e.metaKey || e.ctrlKey) && ((e.key == 'f') || (e.key == 'F')))) { + ((e.metaKey || e.ctrlKey) && ((e.key === 'f') || (e.key === 'F')))) { const note = appContext.tabManager.getActiveContextNote(); // Only writeable text and code supported const readOnly = note.getAttribute("label", "readOnly"); - if (!readOnly && ((note.type == "code") || (note.type == "text"))) { + if (!readOnly && (note.type === "code" || note.type === "text")) { if (findWidget.$findBox.is(":hidden")) { findWidget.$findBox.show(); @@ -142,7 +97,7 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.$curFound.text(0); // Initialize the input field to the text selection, if any - if (note.type == "code") { + if (note.type === "code") { let codeEditor = getActiveContextCodeEditor(); // highlightSelectionMatches is the overlay that highlights @@ -154,13 +109,13 @@ export default class FindWidget extends NoteContextAwareWidget { // Fill in the findbox with the current selection if any const selectedText = codeEditor.getSelection() - if (selectedText != "") { + if (selectedText !== "") { findWidget.$input.val(selectedText); } // Directly perform the search if there's some text to find, // without delaying or waiting for enter const needle = findWidget.$input.val(); - if (needle != "") { + if (needle !== "") { findWidget.$input.select(); await findWidget.performFind(needle); } @@ -179,7 +134,7 @@ export default class FindWidget extends NoteContextAwareWidget { // Directly perform the search if there's some text to // find, without delaying or waiting for enter const needle = findWidget.$input.val(); - if (needle != "") { + if (needle !== "") { findWidget.$input.select(); await findWidget.performFind(needle); } @@ -193,19 +148,18 @@ export default class FindWidget extends NoteContextAwareWidget { }); findWidget.$input.keydown(async function (e) { - dbg("keydown on input " + e.key); - if ((e.metaKey || e.ctrlKey) && ((e.key == 'F') || (e.key == 'f'))) { + 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 findWidget.$input.select(); - } else if ((e.key == 'Enter') || (e.key == 'F3')) { + } else if ((e.key === 'Enter') || (e.key === 'F3')) { const needle = findWidget.$input.val(); - if (waitForEnter && (findWidget.needle != needle)) { + if (waitForEnter && (findWidget.needle !== needle)) { await findWidget.performFind(needle); } let numFound = parseInt(findWidget.$numFound.text()); let curFound = parseInt(findWidget.$curFound.text()) - 1; - dbg("Finding " + curFound + "/" + numFound + " occurrence of " + findWidget.$input.val()); + if (numFound > 0) { let delta = e.shiftKey ? -1 : 1; let nextFound = curFound + delta; @@ -220,7 +174,7 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.$curFound.text(nextFound + 1); const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { let codeEditor = getActiveContextCodeEditor(); let doc = codeEditor.doc; @@ -248,7 +202,6 @@ export default class FindWidget extends NoteContextAwareWidget { codeEditor.scrollIntoView(pos.from); } else { - assert(note.type == "text", "Expected text note, found " + note.type); const textEditor = await getActiveContextTextEditor(); // There are no parameters for findNext/findPrev @@ -264,16 +217,15 @@ export default class FindWidget extends NoteContextAwareWidget { } e.preventDefault(); return false; - } else if (e.key == 'Escape') { + } else if (e.key === 'Escape') { let numFound = parseInt(findWidget.$numFound.text()); const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { let codeEditor = getActiveContextCodeEditor(); codeEditor.focus(); } else { - assert(note.type == "text", "Expected text note, found " + note.type); const textEditor = await getActiveContextTextEditor(); textEditor.focus(); } @@ -305,12 +257,10 @@ export default class FindWidget extends NoteContextAwareWidget { }); findWidget.$caseCheck.change(function() { - log("caseCheck change"); findWidget.performFind(); }); findWidget.$wordCheck.change(function() { - log("wordCheck change"); findWidget.performFind(); }); @@ -320,10 +270,8 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.$findBox.focusout(async function (e) { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused - log(`focusout ${e.target.id} related ${e.relatedTarget?.id}`); if (findWidget.$findBox[0].contains(e.relatedTarget)) { // The focused element is inside this div, ignore - log("focusout to child, ignoring"); return; } findWidget.$findBox.hide(); @@ -337,7 +285,7 @@ export default class FindWidget extends NoteContextAwareWidget { let numFound = parseInt(findWidget.$numFound.text()); let curFound = parseInt(findWidget.$curFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { let codeEditor = await getActiveContextCodeEditor(); if (numFound > 0) { let doc = codeEditor.doc; @@ -359,7 +307,6 @@ export default class FindWidget extends NoteContextAwareWidget { findWidget.findResult = null; findWidget.needle = null; } else { - assert(note.type == "text", "Expected text note, found " + note.type); if (numFound > 0) { const textEditor = await getActiveContextTextEditor(); // Clear the markers and set the caret to the @@ -397,12 +344,10 @@ export default class FindWidget extends NoteContextAwareWidget { let curFound = -1; // Clear - let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); - log("findAndReplace clearing"); + const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); findAndReplaceEditing.state.clear(model); - log("findAndReplace stopping"); findAndReplaceEditing.stop(); - if (needle != "") { + if (needle !== "") { // 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 @@ -410,23 +355,19 @@ export default class FindWidget extends NoteContextAwareWidget { // let re = new RegExp(needle, 'gi'); // let m = text.match(re); // numFound = m ? m.length : 0; - log("findAndReplace starts"); const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; findResult = textEditor.execute('find', needle, options); - log("findAndReplace ends"); numFound = findResult.results.length; // Find the result beyond the cursor - log("findAndReplace positioning"); - let cursorPos = model.document.selection.getLastPosition(); + const cursorPos = model.document.selection.getLastPosition(); for (let i = 0; i < findResult.results.length; ++i) { - let marker = findResult.results.get(i).marker; - let fromPos = marker.getStart(); - if (fromPos.compareWith(cursorPos) != "before") { + const marker = findResult.results.get(i).marker; + const fromPos = marker.getStart(); + if (fromPos.compareWith(cursorPos) !== "before") { curFound = i; break; } } - log("findAndReplace positioned"); } this.findResult = findResult; @@ -452,16 +393,16 @@ export default class FindWidget extends NoteContextAwareWidget { let curFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips - let codeEditor = await getActiveContextCodeEditor(); - let doc = codeEditor.doc; - let text = doc.getValue(); + const codeEditor = await getActiveContextCodeEditor(); + const doc = codeEditor.doc; + const text = doc.getValue(); // Clear all markers if (this.findResult != null) { const findWidget = this; codeEditor.operation(function() { for (let i = 0; i < findWidget.findResult.length; ++i) { - let marker = findWidget.findResult[i]; + const marker = findWidget.findResult[i]; marker.clear(); } }); @@ -477,7 +418,7 @@ export default class FindWidget extends NoteContextAwareWidget { // complicated regexp, see // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145 const wholeWordChar = wholeWord ? "\\b" : ""; - let re = new RegExp(wholeWordChar + needle + wholeWordChar, + const re = new RegExp(wholeWordChar + needle + wholeWordChar, 'g' + (matchCase ? '' : 'i')); let curLine = 0; let curChar = 0; @@ -500,7 +441,7 @@ export default class FindWidget extends NoteContextAwareWidget { } // Create a non-selected highlight marker for the match, the // selected marker highlight will be done later - if (i == curMatch.index) { + 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 @@ -512,8 +453,8 @@ export default class FindWidget extends NoteContextAwareWidget { // Set the first match beyond the cursor as current // match - if (curFound == -1) { - let cursorPos = codeEditor.getCursor(); + if (curFound === -1) { + const cursorPos = codeEditor.getCursor(); if ((fromPos.line > cursorPos.line) || ((fromPos.line == cursorPos.line) && (fromPos.ch >= cursorPos.ch))){ @@ -524,7 +465,7 @@ export default class FindWidget extends NoteContextAwareWidget { numFound++; } // Do line and char position tracking - if (text[i] == "\n") { + if (text[i] === "\n") { curLine++; curChar = 0; } else { @@ -562,38 +503,29 @@ export default class FindWidget extends NoteContextAwareWidget { * state if missing. */ async performFind(needle, matchCase, wholeWord) { - needle = (needle == undefined) ? this.$input.val() : needle; + needle = (needle === undefined) ? this.$input.val() : needle; matchCase = (matchCase === undefined) ? this.$caseCheck.prop("checked") : matchCase; wholeWord = (wholeWord === undefined) ? this.$wordCheck.prop("checked") : wholeWord; - log(`performFind needle:${needle} case:${matchCase} word:${wholeWord}`); const note = appContext.tabManager.getActiveContextNote(); - if (note.type == "code") { + if (note.type === "code") { await this.performCodeNoteFind(needle, matchCase, wholeWord); } else { - assert(note.type == "text", "Expected text note, found " + note.type); await this.performTextNoteFind(needle, matchCase, wholeWord); } } isEnabled() { - dbg("isEnabled"); return super.isEnabled() && ((this.note.type === 'text') || (this.note.type === 'code')) && !this.note.hasLabel('noFindWidget'); } doRender() { - dbg("doRender"); this.$findBox.hide(); return this.$widget; } - async refreshWithNote(note) { - dbg("refreshWithNote"); - } - async entitiesReloadedEvent({loadResults}) { - dbg("entitiesReloadedEvent"); if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); } From 6778e1e60e234b7072a8be336eee4edb8c4f302f Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 15 May 2022 12:09:30 +0200 Subject: [PATCH 04/12] findwidget cleanup --- src/public/app/widgets/find.js | 142 +++++++++++++++------------------ 1 file changed, 66 insertions(+), 76 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 9ac090400..2ea338797 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -38,7 +38,7 @@ 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 TEMPLATE = ` +const TPL = `
@@ -63,22 +63,20 @@ const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; export default class FindWidget extends NoteContextAwareWidget { - constructor(...args) { - super(...args); - this.$widget = $(TEMPLATE); + doRender() { + this.$widget = $(TPL); this.$findBox = this.$widget.find('#findBox'); + this.$findBox.hide(); this.$input = this.$widget.find('#input'); this.$curFound = this.$widget.find('#curFound'); this.$numFound = this.$widget.find('#numFound'); this.$caseCheck = this.$widget.find("#caseCheck"); this.$wordCheck = this.$widget.find("#wordCheck"); this.findResult = null; - this.prevFocus = null; this.needle = null; - let findWidget = this; // XXX Use api.bindGlobalShortcut? - $(window).keydown(async function (e){ + $(window).keydown(async (e) => { if ((e.key === 'F3') || // Note that for ctrl+f to work, needs to be disabled in Trilium's // shortcut config menu @@ -89,12 +87,12 @@ export default class FindWidget extends NoteContextAwareWidget { // Only writeable text and code supported const readOnly = note.getAttribute("label", "readOnly"); if (!readOnly && (note.type === "code" || note.type === "text")) { - if (findWidget.$findBox.is(":hidden")) { + if (this.$findBox.is(":hidden")) { - findWidget.$findBox.show(); - findWidget.$input.focus(); - findWidget.$numFound.text(0); - findWidget.$curFound.text(0); + this.$findBox.show(); + this.$input.focus(); + this.$numFound.text(0); + this.$curFound.text(0); // Initialize the input field to the text selection, if any if (note.type === "code") { @@ -104,20 +102,20 @@ export default class FindWidget extends NoteContextAwareWidget { // 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 - findWidget.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); + 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 !== "") { - findWidget.$input.val(selectedText); + this.$input.val(selectedText); } // Directly perform the search if there's some text to find, // without delaying or waiting for enter - const needle = findWidget.$input.val(); + const needle = this.$input.val(); if (needle !== "") { - findWidget.$input.select(); - await findWidget.performFind(needle); + this.$input.select(); + await this.performFind(needle); } } else { const textEditor = await getActiveContextTextEditor(); @@ -128,15 +126,15 @@ export default class FindWidget extends NoteContextAwareWidget { for (const item of range.getItems()) { // Fill in the findbox with the current selection if // any - findWidget.$input.val(item.data); + this.$input.val(item.data); break; } // Directly perform the search if there's some text to // find, without delaying or waiting for enter - const needle = findWidget.$input.val(); + const needle = this.$input.val(); if (needle !== "") { - findWidget.$input.select(); - await findWidget.performFind(needle); + this.$input.select(); + await this.performFind(needle); } } } @@ -147,18 +145,18 @@ export default class FindWidget extends NoteContextAwareWidget { return true; }); - findWidget.$input.keydown(async function (e) { + 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 - findWidget.$input.select(); + this.$input.select(); } else if ((e.key === 'Enter') || (e.key === 'F3')) { - const needle = findWidget.$input.val(); - if (waitForEnter && (findWidget.needle !== needle)) { - await findWidget.performFind(needle); + const needle = this.$input.val(); + if (waitForEnter && (this.needle !== needle)) { + await this.performFind(needle); } - let numFound = parseInt(findWidget.$numFound.text()); - let curFound = parseInt(findWidget.$curFound.text()) - 1; + const numFound = parseInt(this.$numFound.text()); + const curFound = parseInt(this.$curFound.text()) - 1; if (numFound > 0) { let delta = e.shiftKey ? -1 : 1; @@ -170,8 +168,8 @@ export default class FindWidget extends NoteContextAwareWidget { nextFound = numFound - 1; } - let needle = findWidget.$input.val(); - findWidget.$curFound.text(nextFound + 1); + let needle = this.$input.val(); + this.$curFound.text(nextFound + 1); const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { @@ -182,23 +180,23 @@ export default class FindWidget extends NoteContextAwareWidget { // Dehighlight current, highlight & scrollIntoView next // - let marker = findWidget.findResult[curFound]; + let marker = this.findResult[curFound]; let pos = marker.find(); marker.clear(); marker = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_CSS_CLASSNAME } ); - findWidget.findResult[curFound] = marker; + this.findResult[curFound] = marker; - marker = findWidget.findResult[nextFound]; + marker = this.findResult[nextFound]; pos = marker.find(); marker.clear(); marker = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } ); - findWidget.findResult[nextFound] = marker; + this.findResult[nextFound] = marker; codeEditor.scrollIntoView(pos.from); } else { @@ -218,7 +216,7 @@ export default class FindWidget extends NoteContextAwareWidget { e.preventDefault(); return false; } else if (e.key === 'Escape') { - let numFound = parseInt(findWidget.$numFound.text()); + let numFound = parseInt(this.$numFound.text()); const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { @@ -233,48 +231,43 @@ export default class FindWidget extends NoteContextAwareWidget { // e.preventDefault(); }); - findWidget.$input.on('input', function (e) { + this.$input.on('input', () => { // 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(findWidget.timeoutId); + 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 - const needle = findWidget.$input.val(); - const matchCase = findWidget.$caseCheck.prop("checked"); - const wholeWord = findWidget.$wordCheck.prop("checked"); - findWidget.timeoutId = setTimeout(async function () { - findWidget.timeoutId = null; - await findWidget.performFind(needle, matchCase, wholeWord); + const needle = this.$input.val(); + const matchCase = this.$caseCheck.prop("checked"); + const wholeWord = this.$wordCheck.prop("checked"); + this.timeoutId = setTimeout(async () => { + this.timeoutId = null; + await this.performFind(needle, matchCase, wholeWord); }, findWidgetDelayMillis); } }); - findWidget.$caseCheck.change(function() { - findWidget.performFind(); - }); - - findWidget.$wordCheck.change(function() { - findWidget.performFind(); - }); + this.$caseCheck.change(() => this.performFind()); + this.$wordCheck.change(() => this.performFind()); // Note blur doesn't bubble to parent div, but the parent div needs to // detect when any of the children are not focused and hide. Use // focusout instead which does bubble to the parent div. - findWidget.$findBox.focusout(async function (e) { + this.$findBox.focusout(async (e) => { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused - if (findWidget.$findBox[0].contains(e.relatedTarget)) { + if (this.$findBox[0].contains(e.relatedTarget)) { // The focused element is inside this div, ignore return; } - findWidget.$findBox.hide(); + this.$findBox.hide(); // Restore any state, if there's a current occurrence clear markers // and scroll to and select the last occurrence @@ -282,37 +275,37 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Switching to a different tab with crl+tab doesn't invoke // blur and leaves a stale search which then breaks when // navigating it - let numFound = parseInt(findWidget.$numFound.text()); - let curFound = parseInt(findWidget.$curFound.text()) - 1; + let numFound = parseInt(this.$numFound.text()); + let curFound = parseInt(this.$curFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { let codeEditor = await getActiveContextCodeEditor(); if (numFound > 0) { let doc = codeEditor.doc; - let pos = findWidget.findResult[curFound].find(); + let pos = this.findResult[curFound].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(function() { - for (let i = 0; i < findWidget.findResult.length; ++i) { - let marker = findWidget.findResult[i]; + 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", findWidget.oldHighlightSelectionMatches); - findWidget.findResult = null; - findWidget.needle = null; + codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); + this.findResult = null; + this.needle = null; } else { if (numFound > 0) { const textEditor = await getActiveContextTextEditor(); // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - let range = findWidget.findResult.results.get(curFound).marker.getRange(); + let range = this.findResult.results.get(curFound).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 @@ -324,14 +317,16 @@ export default class FindWidget extends NoteContextAwareWidget { writer.setSelection(range, 0); }); textEditor.editing.view.scrollToTheSelection(); - findWidget.findResult = null; - findWidget.needle = null; + this.findResult = null; + this.needle = null; } else { - findWidget.findResult = null; - findWidget.needle = null; + this.findResult = null; + this.needle = null; } } }); + + return this.$widget; } async performTextNoteFind(needle, matchCase, wholeWord) { @@ -400,9 +395,9 @@ export default class FindWidget extends NoteContextAwareWidget { // Clear all markers if (this.findResult != null) { const findWidget = this; - codeEditor.operation(function() { - for (let i = 0; i < findWidget.findResult.length; ++i) { - const marker = findWidget.findResult[i]; + codeEditor.operation(() => { + for (let i = 0; i < this.findResult.length; ++i) { + const marker = this.findResult[i]; marker.clear(); } }); @@ -428,7 +423,7 @@ export default class FindWidget extends NoteContextAwareWidget { // 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(function() { + 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 @@ -520,11 +515,6 @@ export default class FindWidget extends NoteContextAwareWidget { && !this.note.hasLabel('noFindWidget'); } - doRender() { - this.$findBox.hide(); - return this.$widget; - } - async entitiesReloadedEvent({loadResults}) { if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); From c50d8e85dc07c0075eba659b7a89e32136b6d652 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 15 May 2022 21:03:51 +0200 Subject: [PATCH 05/12] findwidget cleanup --- src/public/app/widgets/find.js | 186 +++++++++++++-------------------- 1 file changed, 71 insertions(+), 115 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 2ea338797..31769064a 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -1,32 +1,6 @@ /** - * Find in note replacement for Trilium ctrl+f search * (c) Antonio Tejada 2022 * https://github.com/antoniotejada/Trilium-FindWidget - * - * Features: - * - Find in writeable using ctrl+f and F3 - * - Tested on Trilium Desktop 0.50.3 - * - * Installation: - * - Create a code note of language JS Frontend with the contents of this file - * - Set the owned attributes (alt-a) to #widget - * - Set the owned attributes of any note you don't want to enable finding to - * #noFindWidget - * - Disable Ctrl+f shorcut in Trilium options - * - * Todo: - * - Refactoring/code cleanup - * - Case-sensitive option - * - Regexp option - * - Full word option - * - Find & Replace - * - * Note that many times some common code is replicated between CodeMirror and - * CKEditor codepaths because the CKEditor update is done inside a callback that - * is deferred so the code cannot be put outside of the callback or it will - * execute too early. - * - * See https://github.com/zadam/trilium/discussions/2806 for discussions */ import NoteContextAwareWidget from "./note_context_aware_widget.js"; @@ -44,7 +18,6 @@ const TPL = ` - 0/0
`; @@ -75,76 +48,6 @@ export default class FindWidget extends NoteContextAwareWidget { this.findResult = null; this.needle = null; - // XXX Use api.bindGlobalShortcut? - $(window).keydown(async (e) => { - if ((e.key === 'F3') || - // Note that for ctrl+f to work, needs to be disabled in Trilium's - // shortcut config menu - // XXX Maybe not if using bindShorcut? - ((e.metaKey || e.ctrlKey) && ((e.key === 'f') || (e.key === 'F')))) { - - const note = appContext.tabManager.getActiveContextNote(); - // Only writeable text and code supported - const readOnly = note.getAttribute("label", "readOnly"); - if (!readOnly && (note.type === "code" || note.type === "text")) { - if (this.$findBox.is(":hidden")) { - - this.$findBox.show(); - this.$input.focus(); - this.$numFound.text(0); - this.$curFound.text(0); - - // Initialize the input field to the text selection, if any - if (note.type === "code") { - let codeEditor = getActiveContextCodeEditor(); - - // 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 !== "") { - this.$input.val(selectedText); - } - // Directly perform the search if there's some text to find, - // without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { - this.$input.select(); - await this.performFind(needle); - } - } else { - const textEditor = await getActiveContextTextEditor(); - - 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 - this.$input.val(item.data); - break; - } - // Directly perform the search if there's some text to - // find, without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { - this.$input.select(); - await this.performFind(needle); - } - } - } - e.preventDefault(); - return false; - } - } - return true; - }); - 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 @@ -173,8 +76,8 @@ export default class FindWidget extends NoteContextAwareWidget { const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - let codeEditor = getActiveContextCodeEditor(); - let doc = codeEditor.doc; + const codeEditor = await getActiveContextCodeEditor(); + const doc = codeEditor.doc; // // Dehighlight current, highlight & scrollIntoView next @@ -216,19 +119,15 @@ export default class FindWidget extends NoteContextAwareWidget { e.preventDefault(); return false; } else if (e.key === 'Escape') { - let numFound = parseInt(this.$numFound.text()); - const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - let codeEditor = getActiveContextCodeEditor(); - + const codeEditor = await getActiveContextCodeEditor(); codeEditor.focus(); } else { const textEditor = await getActiveContextTextEditor(); textEditor.focus(); } } - // e.preventDefault(); }); this.$input.on('input', () => { @@ -275,14 +174,14 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Switching to a different tab with crl+tab doesn't invoke // blur and leaves a stale search which then breaks when // navigating it - let numFound = parseInt(this.$numFound.text()); - let curFound = parseInt(this.$curFound.text()) - 1; + const numFound = parseInt(this.$numFound.text()); + const curFound = parseInt(this.$curFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - let codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await getActiveContextCodeEditor(); if (numFound > 0) { - let doc = codeEditor.doc; - let pos = this.findResult[curFound].find(); + const doc = codeEditor.doc; + const pos = this.findResult[curFound].find(); // Note setting the selection sets the cursor to // the end of the selection and scrolls it into // view @@ -305,7 +204,7 @@ export default class FindWidget extends NoteContextAwareWidget { // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - let range = this.findResult.results.get(curFound).marker.getRange(); + const range = this.findResult.results.get(curFound).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 @@ -329,6 +228,65 @@ export default class FindWidget extends NoteContextAwareWidget { return this.$widget; } + async findInTextEvent() { + const note = appContext.tabManager.getActiveContextNote(); + // Only writeable text and code supported + const readOnly = note.getAttribute("label", "readOnly"); + if (!readOnly && (note.type === "code" || note.type === "text")) { + if (this.$findBox.is(":hidden")) { + + this.$findBox.show(); + this.$input.focus(); + this.$numFound.text(0); + this.$curFound.text(0); + + // Initialize the input field to the text selection, if any + if (note.type === "code") { + const codeEditor = await getActiveContextCodeEditor(); + + // 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 !== "") { + this.$input.val(selectedText); + } + // Directly perform the search if there's some text to find, + // without delaying or waiting for enter + const needle = this.$input.val(); + if (needle !== "") { + this.$input.select(); + await this.performFind(needle); + } + } else { + const textEditor = await getActiveContextTextEditor(); + + 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 + this.$input.val(item.data); + break; + } + // Directly perform the search if there's some text to + // find, without delaying or waiting for enter + const needle = this.$input.val(); + if (needle !== "") { + this.$input.select(); + await this.performFind(needle); + } + } + } + } + } + async performTextNoteFind(needle, matchCase, wholeWord) { // Do this even if the needle is empty so the markers are cleared and // the counters updated @@ -403,7 +361,7 @@ export default class FindWidget extends NoteContextAwareWidget { }); } - if (needle != "") { + if (needle !== "") { needle = escapeRegExp(needle); // Find and highlight matches @@ -451,7 +409,7 @@ export default class FindWidget extends NoteContextAwareWidget { if (curFound === -1) { const cursorPos = codeEditor.getCursor(); if ((fromPos.line > cursorPos.line) || - ((fromPos.line == cursorPos.line) && + ((fromPos.line === cursorPos.line) && (fromPos.ch >= cursorPos.ch))){ curFound = numFound; } @@ -510,9 +468,7 @@ export default class FindWidget extends NoteContextAwareWidget { } isEnabled() { - return super.isEnabled() - && ((this.note.type === 'text') || (this.note.type === 'code')) - && !this.note.hasLabel('noFindWidget'); + return super.isEnabled() && (this.note.type === 'text' || this.note.type === 'code'); } async entitiesReloadedEvent({loadResults}) { From bb7ad496bf9fe72cbb9d1531c3acb86974d83f93 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 15 May 2022 22:51:26 +0200 Subject: [PATCH 06/12] findwidget cleanup --- src/public/app/widgets/find.js | 229 +++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 97 deletions(-) diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 31769064a..115efce13 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -14,11 +14,48 @@ const waitForEnter = (findWidgetDelayMillis < 0); // of undefined. It's -1 instead of > 0, so they don't tabstop const TPL = `
-
- - - - 0/0 + + +
+ + +
+ +
+ +
+ +
+ +
+ 0 + / + 0 +
`; @@ -38,41 +75,40 @@ const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; export default class FindWidget extends NoteContextAwareWidget { doRender() { this.$widget = $(TPL); - this.$findBox = this.$widget.find('#findBox'); + this.$findBox = this.$widget.find('.find-widget-box'); this.$findBox.hide(); - this.$input = this.$widget.find('#input'); - this.$curFound = this.$widget.find('#curFound'); - this.$numFound = this.$widget.find('#numFound'); - this.$caseCheck = this.$widget.find("#caseCheck"); - this.$wordCheck = this.$widget.find("#wordCheck"); + 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.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); this.findResult = null; - this.needle = null; + this.searchTerm = null; this.$input.keydown(async e => { - if ((e.metaKey || e.ctrlKey) && ((e.key === 'F') || (e.key === 'f'))) { + 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')) { - const needle = this.$input.val(); - if (waitForEnter && (this.needle !== needle)) { - await this.performFind(needle); + } else if (e.key === 'Enter' || e.key === 'F3') { + const searchTerm = this.$input.val(); + if (waitForEnter && this.searchTerm !== searchTerm) { + await this.performFind(searchTerm); } - const numFound = parseInt(this.$numFound.text()); - const curFound = parseInt(this.$curFound.text()) - 1; + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; - if (numFound > 0) { - let delta = e.shiftKey ? -1 : 1; - let nextFound = curFound + delta; + if (totalFound > 0) { + const delta = e.shiftKey ? -1 : 1; + let nextFound = currentFound + delta; // Wrap around - if (nextFound > numFound - 1) { + if (nextFound > totalFound - 1) { nextFound = 0; - } if (nextFound < 0) { - nextFound = numFound - 1; + } else if (nextFound < 0) { + nextFound = totalFound - 1; } - let needle = this.$input.val(); - this.$curFound.text(nextFound + 1); + this.$currentFound.text(nextFound + 1); const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { @@ -83,14 +119,14 @@ export default class FindWidget extends NoteContextAwareWidget { // Dehighlight current, highlight & scrollIntoView next // - let marker = this.findResult[curFound]; + 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[curFound] = marker; + this.findResult[currentFound] = marker; marker = this.findResult[nextFound]; pos = marker.find(); @@ -143,23 +179,23 @@ export default class FindWidget extends NoteContextAwareWidget { // 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 - const needle = this.$input.val(); - const matchCase = this.$caseCheck.prop("checked"); - const wholeWord = this.$wordCheck.prop("checked"); + const searchTerm = this.$input.val(); + const matchCase = this.$caseSensitiveCheckbox.prop("checked"); + const wholeWord = this.$matchWordsCheckbox.prop("checked"); this.timeoutId = setTimeout(async () => { this.timeoutId = null; - await this.performFind(needle, matchCase, wholeWord); + await this.performFind(searchTerm, matchCase, wholeWord); }, findWidgetDelayMillis); } }); - this.$caseCheck.change(() => this.performFind()); - this.$wordCheck.change(() => this.performFind()); + this.$caseSensitiveCheckbox.change(() => this.performFind()); + this.$matchWordsCheckbox.change(() => this.performFind()); // Note blur doesn't bubble to parent div, but the parent div needs to // detect when any of the children are not focused and hide. Use // focusout instead which does bubble to the parent div. - this.$findBox.focusout(async (e) => { + this.$findBox.on('focusout', async (e) => { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused if (this.$findBox[0].contains(e.relatedTarget)) { @@ -174,14 +210,14 @@ export default class FindWidget extends NoteContextAwareWidget { // XXX Switching to a different tab with crl+tab doesn't invoke // blur and leaves a stale search which then breaks when // navigating it - const numFound = parseInt(this.$numFound.text()); - const curFound = parseInt(this.$curFound.text()) - 1; + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { const codeEditor = await getActiveContextCodeEditor(); - if (numFound > 0) { + if (totalFound > 0) { const doc = codeEditor.doc; - const pos = this.findResult[curFound].find(); + const pos = this.findResult[currentFound].find(); // Note setting the selection sets the cursor to // the end of the selection and scrolls it into // view @@ -197,14 +233,14 @@ export default class FindWidget extends NoteContextAwareWidget { // Restore the highlightSelectionMatches setting codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); this.findResult = null; - this.needle = null; + this.searchTerm = null; } else { - if (numFound > 0) { + if (totalFound > 0) { const textEditor = await getActiveContextTextEditor(); // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - const range = this.findResult.results.get(curFound).marker.getRange(); + 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 @@ -217,10 +253,10 @@ export default class FindWidget extends NoteContextAwareWidget { }); textEditor.editing.view.scrollToTheSelection(); this.findResult = null; - this.needle = null; + this.searchTerm = null; } else { this.findResult = null; - this.needle = null; + this.searchTerm = null; } } }); @@ -237,8 +273,8 @@ export default class FindWidget extends NoteContextAwareWidget { this.$findBox.show(); this.$input.focus(); - this.$numFound.text(0); - this.$curFound.text(0); + this.$totalFound.text(0); + this.$currentFound.text(0); // Initialize the input field to the text selection, if any if (note.type === "code") { @@ -258,10 +294,10 @@ export default class FindWidget extends NoteContextAwareWidget { } // Directly perform the search if there's some text to find, // without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { + const searchTerm = this.$input.val(); + if (searchTerm !== "") { this.$input.select(); - await this.performFind(needle); + await this.performFind(searchTerm); } } else { const textEditor = await getActiveContextTextEditor(); @@ -277,73 +313,73 @@ export default class FindWidget extends NoteContextAwareWidget { } // Directly perform the search if there's some text to // find, without delaying or waiting for enter - const needle = this.$input.val(); - if (needle !== "") { + const searchTerm = this.$input.val(); + if (searchTerm !== "") { this.$input.select(); - await this.performFind(needle); + await this.performFind(searchTerm); } } } } } - async performTextNoteFind(needle, matchCase, wholeWord) { - // Do this even if the needle is empty so the markers are cleared and + async performTextNoteFind(searchTerm, matchCase, wholeWord) { + // Do this even if the searchTerm is empty so the markers are cleared and // the counters updated const textEditor = await getActiveContextTextEditor(); const model = textEditor.model; let findResult = null; - let numFound = 0; - let curFound = -1; + let totalFound = 0; + let currentFound = -1; // Clear const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); findAndReplaceEditing.state.clear(model); findAndReplaceEditing.stop(); - if (needle !== "") { + 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 - // needle = escapeRegExp(needle); - // let re = new RegExp(needle, 'gi'); + // searchTerm = escapeRegExp(searchTerm); + // let re = new RegExp(searchTerm, 'gi'); // let m = text.match(re); - // numFound = m ? m.length : 0; + // totalFound = m ? m.length : 0; const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; - findResult = textEditor.execute('find', needle, options); - numFound = findResult.results.length; + 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") { - curFound = i; + currentFound = i; break; } } } this.findResult = findResult; - this.$numFound.text(numFound); + this.$totalFound.text(totalFound); // Calculate curfound if not already, highlight it as // selected - if (numFound > 0) { - curFound = Math.max(0, curFound); + 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 < curFound; ++i) { - textEditor.execute('findNext', needle); + for (let i = 0 ; i < currentFound; ++i) { + textEditor.execute('findNext', searchTerm); } } - this.$curFound.text(curFound + 1); - this.needle = needle; + this.$currentFound.text(currentFound + 1); + this.searchTerm = searchTerm; } - async performCodeNoteFind(needle, matchCase, wholeWord) { + async performCodeNoteFind(searchTerm, matchCase, wholeWord) { let findResult = null; - let numFound = 0; - let curFound = -1; + let totalFound = 0; + let currentFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips const codeEditor = await getActiveContextCodeEditor(); @@ -352,7 +388,6 @@ export default class FindWidget extends NoteContextAwareWidget { // Clear all markers if (this.findResult != null) { - const findWidget = this; codeEditor.operation(() => { for (let i = 0; i < this.findResult.length; ++i) { const marker = this.findResult[i]; @@ -361,8 +396,8 @@ export default class FindWidget extends NoteContextAwareWidget { }); } - if (needle !== "") { - needle = escapeRegExp(needle); + if (searchTerm !== "") { + searchTerm = escapeRegExp(searchTerm); // Find and highlight matches // Find and highlight matches @@ -371,7 +406,7 @@ export default class FindWidget extends NoteContextAwareWidget { // 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 + needle + wholeWordChar, + const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, 'g' + (matchCase ? '' : 'i')); let curLine = 0; let curChar = 0; @@ -406,16 +441,16 @@ export default class FindWidget extends NoteContextAwareWidget { // Set the first match beyond the cursor as current // match - if (curFound === -1) { + if (currentFound === -1) { const cursorPos = codeEditor.getCursor(); if ((fromPos.line > cursorPos.line) || ((fromPos.line === cursorPos.line) && (fromPos.ch >= cursorPos.ch))){ - curFound = numFound; + currentFound = totalFound; } } - numFound++; + totalFound++; } // Do line and char position tracking if (text[i] === "\n") { @@ -429,41 +464,41 @@ export default class FindWidget extends NoteContextAwareWidget { } this.findResult = findResult; - this.$numFound.text(numFound); + this.$totalFound.text(totalFound); // Calculate curfound if not already, highlight it as selected - if (numFound > 0) { - curFound = Math.max(0, curFound) - let marker = findResult[curFound]; + if (totalFound > 0) { + currentFound = Math.max(0, currentFound) + let marker = findResult[currentFound]; let pos = marker.find(); codeEditor.scrollIntoView(pos.to); marker.clear(); - findResult[curFound] = doc.markText( pos.from, pos.to, + findResult[currentFound] = doc.markText( pos.from, pos.to, { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } ); } - this.$curFound.text(curFound + 1); - this.needle = needle; + this.$currentFound.text(currentFound + 1); + this.searchTerm = searchTerm; } /** * Perform the find and highlight the find results. * - * @param needle {string} optional parameter, taken from the input box if + * @param [searchTerm] {string} optional parameter, taken from the input box if * missing. - * @param matchCase {boolean} optional parameter, taken from the checkbox + * @param [matchCase] {boolean} optional parameter, taken from the checkbox * state if missing. - * @param wholeWord {boolean} optional parameter, taken from the checkbox + * @param [wholeWord] {boolean} optional parameter, taken from the checkbox * state if missing. */ - async performFind(needle, matchCase, wholeWord) { - needle = (needle === undefined) ? this.$input.val() : needle; - matchCase = (matchCase === undefined) ? this.$caseCheck.prop("checked") : matchCase; - wholeWord = (wholeWord === undefined) ? this.$wordCheck.prop("checked") : wholeWord; + async performFind(searchTerm, matchCase, wholeWord) { + searchTerm = (searchTerm === undefined) ? this.$input.val() : searchTerm; + matchCase = (matchCase === undefined) ? this.$caseSensitiveCheckbox.prop("checked") : matchCase; + wholeWord = (wholeWord === undefined) ? this.$matchWordsCheckbox.prop("checked") : wholeWord; const note = appContext.tabManager.getActiveContextNote(); if (note.type === "code") { - await this.performCodeNoteFind(needle, matchCase, wholeWord); + await this.performCodeNoteFind(searchTerm, matchCase, wholeWord); } else { - await this.performTextNoteFind(needle, matchCase, wholeWord); + await this.performTextNoteFind(searchTerm, matchCase, wholeWord); } } From c51e6107a1fa31203a518aa6f487ccade87e9520 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 16 May 2022 23:56:43 +0200 Subject: [PATCH 07/12] findwidget cleanup --- src/public/app/widgets/find.js | 481 ++++++------------------- src/public/app/widgets/find_in_code.js | 193 ++++++++++ src/public/app/widgets/find_in_text.js | 116 ++++++ 3 files changed, 417 insertions(+), 373 deletions(-) create mode 100644 src/public/app/widgets/find_in_code.js create mode 100644 src/public/app/widgets/find_in_text.js diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 115efce13..b204cffb1 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -5,6 +5,8 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js"; import appContext from "../services/app_context.js"; +import FindInText from "./find_in_text.js"; +import FindInCode from "./find_in_code.js"; const findWidgetDelayMillis = 200; const waitForEnter = (findWidgetDelayMillis < 0); @@ -59,20 +61,14 @@ const TPL = `
`; -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -const getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); -const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); - -// 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"; - export default class FindWidget extends NoteContextAwareWidget { + constructor() { + super(); + + this.textHandler = new FindInText(); + this.codeHandler = new FindInCode(); + } + doRender() { this.$widget = $(TPL); this.$findBox = this.$widget.find('.find-widget-box'); @@ -81,7 +77,9 @@ export default class FindWidget extends NoteContextAwareWidget { 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.findResult = null; this.searchTerm = null; @@ -91,415 +89,142 @@ export default class FindWidget extends NoteContextAwareWidget { // whole input to find this.$input.select(); } else if (e.key === 'Enter' || e.key === 'F3') { - const searchTerm = this.$input.val(); - if (waitForEnter && this.searchTerm !== searchTerm) { - await this.performFind(searchTerm); - } - const totalFound = parseInt(this.$totalFound.text()); - const currentFound = parseInt(this.$currentFound.text()) - 1; - - if (totalFound > 0) { - const delta = e.shiftKey ? -1 : 1; - let nextFound = currentFound + delta; - // Wrap around - if (nextFound > totalFound - 1) { - nextFound = 0; - } else if (nextFound < 0) { - nextFound = totalFound - 1; - } - - this.$currentFound.text(nextFound + 1); - - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); - 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); - } else { - const textEditor = await getActiveContextTextEditor(); - - // 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 (delta > 0) { - textEditor.execute('findNext'); - } else { - textEditor.execute('findPrevious'); - } - } - } + await this.findNext(e); e.preventDefault(); return false; } else if (e.key === 'Escape') { - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); - codeEditor.focus(); - } else { - const textEditor = await getActiveContextTextEditor(); - textEditor.focus(); - } + await this.getHandler().close(); } }); - this.$input.on('input', () => { - // 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 - const searchTerm = this.$input.val(); - const matchCase = this.$caseSensitiveCheckbox.prop("checked"); - const wholeWord = this.$matchWordsCheckbox.prop("checked"); - this.timeoutId = setTimeout(async () => { - this.timeoutId = null; - await this.performFind(searchTerm, matchCase, wholeWord); - }, findWidgetDelayMillis); - } - }); - - this.$caseSensitiveCheckbox.change(() => this.performFind()); - this.$matchWordsCheckbox.change(() => this.performFind()); + this.$input.on('input', () => this.startSearch()); // Note blur doesn't bubble to parent div, but the parent div needs to // detect when any of the children are not focused and hide. Use // focusout instead which does bubble to the parent div. - this.$findBox.on('focusout', async (e) => { + this.$findBox.on('focusout', async e => { // e.relatedTarget is the new focused element, note it can be null // if nothing is being focused if (this.$findBox[0].contains(e.relatedTarget)) { // The focused element is inside this div, ignore return; } - this.$findBox.hide(); - // Restore any state, if there's a current occurrence clear markers - // and scroll to and select the last occurrence - - // XXX Switching to a different tab with crl+tab doesn't invoke - // blur and leaves a stale search which then breaks when - // navigating it - const totalFound = parseInt(this.$totalFound.text()); - const currentFound = parseInt(this.$currentFound.text()) - 1; - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); - 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; - this.searchTerm = null; - } else { - if (totalFound > 0) { - const textEditor = await getActiveContextTextEditor(); - // 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; - this.searchTerm = null; - } else { - this.findResult = null; - this.searchTerm = null; - } - } + await this.closeSearch(); }); 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 + const searchTerm = this.$input.val(); + const matchCase = this.$caseSensitiveCheckbox.prop("checked"); + const wholeWord = this.$matchWordsCheckbox.prop("checked"); + this.timeoutId = setTimeout(async () => { + this.timeoutId = null; + await this.performFind(searchTerm, matchCase, wholeWord); + }, findWidgetDelayMillis); + } + } + + async findNext(e) { + const searchTerm = this.$input.val(); + if (waitForEnter && this.searchTerm !== searchTerm) { + await this.performFind(searchTerm); + } + 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.getHandler().findNext(direction, currentFound, nextFound); + } + } + async findInTextEvent() { const note = appContext.tabManager.getActiveContextNote(); // Only writeable text and code supported const readOnly = note.getAttribute("label", "readOnly"); if (!readOnly && (note.type === "code" || note.type === "text")) { if (this.$findBox.is(":hidden")) { - this.$findBox.show(); this.$input.focus(); this.$totalFound.text(0); this.$currentFound.text(0); - // Initialize the input field to the text selection, if any - if (note.type === "code") { - const codeEditor = await getActiveContextCodeEditor(); + const searchTerm = await this.getHandler().getInitialSearchTerm(); - // 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); + this.$input.val(searchTerm || ""); - // Fill in the findbox with the current selection if any - const selectedText = codeEditor.getSelection() - if (selectedText !== "") { - this.$input.val(selectedText); - } - // Directly perform the search if there's some text to find, - // without delaying or waiting for enter - const searchTerm = this.$input.val(); - if (searchTerm !== "") { - this.$input.select(); - await this.performFind(searchTerm); - } - } else { - const textEditor = await getActiveContextTextEditor(); - - 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 - this.$input.val(item.data); - break; - } - // Directly perform the search if there's some text to - // find, without delaying or waiting for enter - const searchTerm = this.$input.val(); - if (searchTerm !== "") { - this.$input.select(); - await this.performFind(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(searchTerm); } } } } - async performTextNoteFind(searchTerm, matchCase, wholeWord) { - // Do this even if the searchTerm is empty so the markers are cleared and - // the counters updated - const textEditor = await getActiveContextTextEditor(); - 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; - this.$totalFound.text(totalFound); - // 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); - } - } - this.$currentFound.text(currentFound + 1); - this.searchTerm = searchTerm; - } - - async performCodeNoteFind(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 getActiveContextCodeEditor(); - 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; - this.$totalFound.text(totalFound); - // 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 } - ); - } - this.$currentFound.text(currentFound + 1); - this.searchTerm = searchTerm; - } - /** * Perform the find and highlight the find results. * - * @param [searchTerm] {string} optional parameter, taken from the input box if - * missing. - * @param [matchCase] {boolean} optional parameter, taken from the checkbox - * state if missing. - * @param [wholeWord] {boolean} optional parameter, taken from the checkbox - * state if missing. + * @param [searchTerm] {string} taken from the input box if missing. + * @param [matchCase] {boolean} taken from the checkbox state if missing. + * @param [wholeWord] {boolean} taken from the checkbox state if missing. */ async performFind(searchTerm, matchCase, wholeWord) { searchTerm = (searchTerm === undefined) ? this.$input.val() : searchTerm; matchCase = (matchCase === undefined) ? this.$caseSensitiveCheckbox.prop("checked") : matchCase; wholeWord = (wholeWord === undefined) ? this.$matchWordsCheckbox.prop("checked") : wholeWord; - const note = appContext.tabManager.getActiveContextNote(); - if (note.type === "code") { - await this.performCodeNoteFind(searchTerm, matchCase, wholeWord); - } else { - await this.performTextNoteFind(searchTerm, matchCase, wholeWord); + + const {totalFound, currentFound} = await this.getHandler().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 + + // XXX Switching to a different tab with crl+tab doesn't invoke + // blur and leaves a stale search which then breaks when + // navigating it + const totalFound = parseInt(this.$totalFound.text()); + const currentFound = parseInt(this.$currentFound.text()) - 1; + + if (totalFound > 0) { + await this.getHandler().cleanup(totalFound, currentFound); } + + this.searchTerm = null; } isEnabled() { @@ -511,4 +236,14 @@ export default class FindWidget extends NoteContextAwareWidget { this.refresh(); } } + + getHandler() { + const note = appContext.tabManager.getActiveContextNote(); + + if (note.type === "code") { + return this.codeHandler; + } else { + return 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..764f42f9b --- /dev/null +++ b/src/public/app/widgets/find_in_code.js @@ -0,0 +1,193 @@ +import appContext from "../services/app_context.js"; + +// 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 getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); +const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export default class FindInCode { + async getInitialSearchTerm() { + const codeEditor = await getActiveContextCodeEditor(); + + // 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 getActiveContextCodeEditor(); + 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 getActiveContextCodeEditor(); + 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 getActiveContextCodeEditor(); + + 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 getActiveContextCodeEditor(); + 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..5db1a8b63 --- /dev/null +++ b/src/public/app/widgets/find_in_text.js @@ -0,0 +1,116 @@ +import appContext from "../services/app_context.js"; + +const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); + +export default class FindInText { + async getInitialSearchTerm() { + const textEditor = await getActiveContextTextEditor(); + + 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 getActiveContextTextEditor(); + 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 getActiveContextTextEditor(); + + // 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 getActiveContextTextEditor(); + // 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 getActiveContextTextEditor(); + textEditor.focus(); + } +} From 04379b4e1f58e68087e890e4ccb649a7b21a4420 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 20:22:33 +0200 Subject: [PATCH 08/12] delay protected session expiration check after DB init, fixes #2855 --- src/services/protected_session.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/services/protected_session.js b/src/services/protected_session.js index ad125deca..22e531738 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -3,6 +3,7 @@ const log = require('./log'); const dataEncryptionService = require('./data_encryption'); const options = require("./options"); +const sqlInit = require("./sql_init"); let dataKey = null; @@ -63,17 +64,19 @@ function touchProtectedSession() { } } -setInterval(() => { - const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); - if (isProtectedSessionAvailable() - && lastProtectedSessionOperationDate - && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { +sqlInit.dbReady.then(() => { + setInterval(() => { + const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); + if (isProtectedSessionAvailable() + && lastProtectedSessionOperationDate + && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { - resetDataKey(); + resetDataKey(); - require('./ws').reloadFrontend(); - } -}, 30000); + require('./ws').reloadFrontend(); + } + }, 30000); +}); module.exports = { From 5bc629d1c7060d042abebdf0cfeba22ece97b953 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 20:39:21 +0200 Subject: [PATCH 09/12] moved protected session expiration scheduling #2855 --- src/services/protected_session.js | 25 ++++++++++++------------- src/services/scheduler.js | 3 +++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/services/protected_session.js b/src/services/protected_session.js index 22e531738..c6098cc2f 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -3,7 +3,6 @@ const log = require('./log'); const dataEncryptionService = require('./data_encryption'); const options = require("./options"); -const sqlInit = require("./sql_init"); let dataKey = null; @@ -64,20 +63,19 @@ function touchProtectedSession() { } } -sqlInit.dbReady.then(() => { - setInterval(() => { - const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); - if (isProtectedSessionAvailable() - && lastProtectedSessionOperationDate - && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { +function checkProtectedSessionExpiration() { + const protectedSessionTimeout = options.getOptionInt('protectedSessionTimeout'); + if (isProtectedSessionAvailable() + && lastProtectedSessionOperationDate + && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) { - resetDataKey(); + resetDataKey(); - require('./ws').reloadFrontend(); - } - }, 30000); -}); + log.info("Expiring protected session"); + require('./ws').reloadFrontend(); + } +} module.exports = { setDataKey, @@ -87,5 +85,6 @@ module.exports = { decrypt, decryptString, decryptNotes, - touchProtectedSession + touchProtectedSession, + checkProtectedSessionExpiration }; diff --git a/src/services/scheduler.js b/src/services/scheduler.js index f2d88e09a..3f253ba9b 100644 --- a/src/services/scheduler.js +++ b/src/services/scheduler.js @@ -6,6 +6,7 @@ const log = require('./log'); const sql = require("./sql"); const becca = require("../becca/becca"); const specialNotesService = require("../services/special_notes"); +const protectedSessionService = require("../services/protected_session"); function getRunAtHours(note) { try { @@ -59,4 +60,6 @@ sqlInit.dbReady.then(() => { setTimeout(cls.wrap(() => specialNotesService.createMissingSpecialNotes()), 10 * 1000); } + + setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000); }); From fca0b82610e2eed352f00da784851274a0fe7af2 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 22:11:45 +0200 Subject: [PATCH 10/12] find widget fixes --- package-lock.json | 25 ++++++------------- package.json | 2 +- src/public/app/widgets/find.js | 1 - .../app/widgets/type_widgets/editable_code.js | 4 +-- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa9248ab0..57db450ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,14 +24,13 @@ "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", "express-rate-limit": "6.4.0", "express-session": "1.17.3", "fs-extra": "10.1.0", - "helmet": "5.0.2", + "helmet": "5.1.0", "html": "1.0.0", "html2plaintext": "2.1.4", "http-proxy-agent": "5.0.0", @@ -3758,11 +3757,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", @@ -5940,9 +5934,9 @@ } }, "node_modules/helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", + "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==", "engines": { "node": ">=12.0.0" } @@ -14125,11 +14119,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", @@ -15684,9 +15673,9 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "helmet": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz", - "integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", + "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==" }, "hosted-git-info": { "version": "2.8.9", diff --git a/package.json b/package.json index 5b7042461..e1a373cc3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "express-rate-limit": "6.4.0", "express-session": "1.17.3", "fs-extra": "10.1.0", - "helmet": "5.0.2", + "helmet": "5.1.0", "html": "1.0.0", "html2plaintext": "2.1.4", "http-proxy-agent": "5.0.0", diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index b204cffb1..0c8975886 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -80,7 +80,6 @@ export default class FindWidget extends NoteContextAwareWidget { this.$caseSensitiveCheckbox.change(() => this.performFind()); this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); this.$matchWordsCheckbox.change(() => this.performFind()); - this.findResult = null; this.searchTerm = null; this.$input.keydown(async e => { diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js index 7bbc356c3..afffd9e8a 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}) { + async executeInActiveCodeEditorEvent({resolve}) { if (!this.isActive()) { return; } await this.initialized; - callback(this.codeEditor); + resolve(this.codeEditor); } } From 4978a3ff1a68ed8c516864916978456b57f6612c Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 23:22:28 +0200 Subject: [PATCH 11/12] find widget improvements --- src/public/app/layouts/desktop_layout.js | 2 +- src/public/app/widgets/find.js | 115 ++++++++++------------- 2 files changed, 50 insertions(+), 67 deletions(-) diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index a5429521e..2723076ce 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -162,10 +162,10 @@ export default class DesktopLayout { .child(new SearchResultWidget()) .child(new SqlResultWidget()) ) + .child(new FindWidget()) .child(...this.customWidgets.get('node-detail-pane')) ) ) - .child(new FindWidget()) .child(...this.customWidgets.get('center-pane')) ) .child(new RightPaneContainer() diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 0c8975886..0e5ad3b7a 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -4,7 +4,6 @@ */ import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import appContext from "../services/app_context.js"; import FindInText from "./find_in_text.js"; import FindInCode from "./find_in_code.js"; @@ -34,10 +33,18 @@ const TPL = ` .find-widget-found-wrapper { font-weight: bold; } + + .find-widget-search-term-input { + max-width: 250px; + } + + .find-widget-spacer { + flex-grow: 1; + }
- +
+ +
+ +
`; @@ -65,6 +76,8 @@ export default class FindWidget extends NoteContextAwareWidget { constructor() { super(); + this.searchTerm = null; + this.textHandler = new FindInText(); this.codeHandler = new FindInCode(); } @@ -80,7 +93,8 @@ export default class FindWidget extends NoteContextAwareWidget { this.$caseSensitiveCheckbox.change(() => this.performFind()); this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); this.$matchWordsCheckbox.change(() => this.performFind()); - this.searchTerm = null; + 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')) { @@ -92,26 +106,12 @@ export default class FindWidget extends NoteContextAwareWidget { e.preventDefault(); return false; } else if (e.key === 'Escape') { - await this.getHandler().close(); + await this.closeSearch(); } }); this.$input.on('input', () => this.startSearch()); - // Note blur doesn't bubble to parent div, but the parent div needs to - // detect when any of the children are not focused and hide. Use - // focusout instead which does bubble to the parent div. - this.$findBox.on('focusout', async e => { - // e.relatedTarget is the new focused element, note it can be null - // if nothing is being focused - if (this.$findBox[0].contains(e.relatedTarget)) { - // The focused element is inside this div, ignore - return; - } - - await this.closeSearch(); - }); - return this.$widget; } @@ -128,12 +128,9 @@ export default class FindWidget extends NoteContextAwareWidget { // 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 - const searchTerm = this.$input.val(); - const matchCase = this.$caseSensitiveCheckbox.prop("checked"); - const wholeWord = this.$matchWordsCheckbox.prop("checked"); this.timeoutId = setTimeout(async () => { this.timeoutId = null; - await this.performFind(searchTerm, matchCase, wholeWord); + await this.performFind(); }, findWidgetDelayMillis); } } @@ -141,7 +138,7 @@ export default class FindWidget extends NoteContextAwareWidget { async findNext(e) { const searchTerm = this.$input.val(); if (waitForEnter && this.searchTerm !== searchTerm) { - await this.performFind(searchTerm); + await this.performFind(); } const totalFound = parseInt(this.$totalFound.text()); const currentFound = parseInt(this.$currentFound.text()) - 1; @@ -163,41 +160,35 @@ export default class FindWidget extends NoteContextAwareWidget { } async findInTextEvent() { - const note = appContext.tabManager.getActiveContextNote(); // Only writeable text and code supported - const readOnly = note.getAttribute("label", "readOnly"); - if (!readOnly && (note.type === "code" || note.type === "text")) { - if (this.$findBox.is(":hidden")) { - this.$findBox.show(); - this.$input.focus(); - this.$totalFound.text(0); - this.$currentFound.text(0); + const readOnly = await this.noteContext.isReadOnly(); - const searchTerm = await this.getHandler().getInitialSearchTerm(); + if (readOnly || !['text', 'code'].includes(this.note.type) || !this.$findBox.is(":hidden")) { + return; + } - this.$input.val(searchTerm || ""); + this.$findBox.show(); + this.$input.focus(); + this.$totalFound.text(0); + this.$currentFound.text(0); - // 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(searchTerm); - } - } + const searchTerm = await this.getHandler().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. - * - * @param [searchTerm] {string} taken from the input box if missing. - * @param [matchCase] {boolean} taken from the checkbox state if missing. - * @param [wholeWord] {boolean} taken from the checkbox state if missing. - */ - async performFind(searchTerm, matchCase, wholeWord) { - searchTerm = (searchTerm === undefined) ? this.$input.val() : searchTerm; - matchCase = (matchCase === undefined) ? this.$caseSensitiveCheckbox.prop("checked") : matchCase; - wholeWord = (wholeWord === undefined) ? this.$matchWordsCheckbox.prop("checked") : wholeWord; + /** 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.getHandler().performFind(searchTerm, matchCase, wholeWord); @@ -212,10 +203,6 @@ export default class FindWidget extends NoteContextAwareWidget { // Restore any state, if there's a current occurrence clear markers // and scroll to and select the last occurrence - - // XXX Switching to a different tab with crl+tab doesn't invoke - // blur and leaves a stale search which then breaks when - // navigating it const totalFound = parseInt(this.$totalFound.text()); const currentFound = parseInt(this.$currentFound.text()) - 1; @@ -226,23 +213,19 @@ export default class FindWidget extends NoteContextAwareWidget { this.searchTerm = null; } - isEnabled() { - return super.isEnabled() && (this.note.type === 'text' || this.note.type === 'code'); - } - async entitiesReloadedEvent({loadResults}) { if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); } } - getHandler() { - const note = appContext.tabManager.getActiveContextNote(); + isEnabled() { + return super.isEnabled() && ['text', 'code'].includes(this.note.type); + } - if (note.type === "code") { - return this.codeHandler; - } else { - return this.textHandler; - } + getHandler() { + return this.note.type === "code" + ? this.codeHandler + : this.textHandler; } } From cd622cbdd7ad0a8ba7b4ad5e24c50077018d5b44 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 17 May 2022 23:53:35 +0200 Subject: [PATCH 12/12] find widget refactoring to use note context --- src/public/app/dialogs/markdown_import.js | 5 +++-- .../app/services/frontend_script_api.js | 6 ++--- src/public/app/services/note_context.js | 15 +++++++++++++ src/public/app/services/tab_manager.js | 8 ------- src/public/app/widgets/find.js | 18 +++++++++------ src/public/app/widgets/find_in_code.js | 21 +++++++++++------- src/public/app/widgets/find_in_text.js | 22 +++++++++++-------- .../app/widgets/type_widgets/editable_code.js | 4 ++-- .../app/widgets/type_widgets/editable_text.js | 4 ++-- 9 files changed, 62 insertions(+), 41 deletions(-) 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/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index dee3b7a7d..b2eabdc13 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -384,7 +384,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain this.getActiveTabTextEditor = callback => { console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); - return appContext.tabManager.getActiveContextTextEditor(callback); + return appContext.tabManager.getActiveContext()?.getTextEditor(callback); }; /** @@ -393,7 +393,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise} instance of CKEditor */ - this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContextTextEditor(); + this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); /** * See https://codemirror.net/doc/manual.html#api @@ -401,7 +401,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * @method * @returns {Promise} instance of CodeMirror */ - this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContextCodeEditor(); + this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); /** * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the 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/services/tab_manager.js b/src/public/app/services/tab_manager.js index 1250b6948..15f939bfe 100644 --- a/src/public/app/services/tab_manager.js +++ b/src/public/app/services/tab_manager.js @@ -193,14 +193,6 @@ export default class TabManager extends Component { return activeNote ? activeNote.type : null; } - async getActiveContextTextEditor(callback) { - return new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); - } - - async getActiveContextCodeEditor() { - return new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {resolve})); - } - async switchToNoteContext(ntxId, notePath) { const noteContext = this.noteContexts.find(nc => nc.ntxId === ntxId) || await this.openEmptyTab(); diff --git a/src/public/app/widgets/find.js b/src/public/app/widgets/find.js index 0e5ad3b7a..9e0590ce9 100644 --- a/src/public/app/widgets/find.js +++ b/src/public/app/widgets/find.js @@ -78,8 +78,8 @@ export default class FindWidget extends NoteContextAwareWidget { this.searchTerm = null; - this.textHandler = new FindInText(); - this.codeHandler = new FindInCode(); + this.textHandler = new FindInText(this); + this.codeHandler = new FindInCode(this); } doRender() { @@ -155,11 +155,15 @@ export default class FindWidget extends NoteContextAwareWidget { this.$currentFound.text(nextFound + 1); - await this.getHandler().findNext(direction, currentFound, nextFound); + 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(); @@ -172,7 +176,7 @@ export default class FindWidget extends NoteContextAwareWidget { this.$totalFound.text(0); this.$currentFound.text(0); - const searchTerm = await this.getHandler().getInitialSearchTerm(); + const searchTerm = await this.handler.getInitialSearchTerm(); this.$input.val(searchTerm || ""); @@ -190,7 +194,7 @@ export default class FindWidget extends NoteContextAwareWidget { const matchCase = this.$caseSensitiveCheckbox.prop("checked"); const wholeWord = this.$matchWordsCheckbox.prop("checked"); - const {totalFound, currentFound} = await this.getHandler().performFind(searchTerm, matchCase, wholeWord); + const {totalFound, currentFound} = await this.handler.performFind(searchTerm, matchCase, wholeWord); this.$totalFound.text(totalFound); this.$currentFound.text(currentFound); @@ -207,7 +211,7 @@ export default class FindWidget extends NoteContextAwareWidget { const currentFound = parseInt(this.$currentFound.text()) - 1; if (totalFound > 0) { - await this.getHandler().cleanup(totalFound, currentFound); + await this.handler.cleanup(totalFound, currentFound); } this.searchTerm = null; @@ -223,7 +227,7 @@ export default class FindWidget extends NoteContextAwareWidget { return super.isEnabled() && ['text', 'code'].includes(this.note.type); } - getHandler() { + 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 index 764f42f9b..7a6bcbced 100644 --- a/src/public/app/widgets/find_in_code.js +++ b/src/public/app/widgets/find_in_code.js @@ -1,17 +1,22 @@ -import appContext from "../services/app_context.js"; - // 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 getActiveContextCodeEditor = async () => await appContext.tabManager.getActiveContextCodeEditor(); 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 getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); // highlightSelectionMatches is the overlay that highlights // the words under the cursor. This occludes the search @@ -33,7 +38,7 @@ export default class FindInCode { let currentFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); const doc = codeEditor.doc; const text = doc.getValue(); @@ -135,7 +140,7 @@ export default class FindInCode { } async findNext(direction, currentFound, nextFound) { - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); const doc = codeEditor.doc; // @@ -164,7 +169,7 @@ export default class FindInCode { } async cleanup(totalFound, currentFound) { - const codeEditor = await getActiveContextCodeEditor(); + const codeEditor = await this.getCodeEditor(); if (totalFound > 0) { const doc = codeEditor.doc; @@ -187,7 +192,7 @@ export default class FindInCode { } async close() { - const codeEditor = await getActiveContextCodeEditor(); + 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 index 5db1a8b63..54f3bd091 100644 --- a/src/public/app/widgets/find_in_text.js +++ b/src/public/app/widgets/find_in_text.js @@ -1,10 +1,14 @@ -import appContext from "../services/app_context.js"; - -const getActiveContextTextEditor = async () => await appContext.tabManager.getActiveContextTextEditor(); - export default class FindInText { + constructor(parent) { + this.parent = parent; + } + + async getTextEditor() { + return this.parent.noteContext.getTextEditor(); + } + async getInitialSearchTerm() { - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); const selection = textEditor.model.document.selection; const range = selection.getFirstRange(); @@ -19,7 +23,7 @@ export default class FindInText { 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 getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); const model = textEditor.model; let findResult = null; let totalFound = 0; @@ -73,7 +77,7 @@ export default class FindInText { } async findNext(direction, currentFound, nextFound) { - const textEditor = await getActiveContextTextEditor(); + 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 @@ -88,7 +92,7 @@ export default class FindInText { async cleanup(totalFound, currentFound) { if (totalFound > 0) { - const textEditor = await getActiveContextTextEditor(); + const textEditor = await this.getTextEditor(); // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; @@ -110,7 +114,7 @@ export default class FindInText { } async close() { - const textEditor = await getActiveContextTextEditor(); + 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 afffd9e8a..2a4097f1c 100644 --- a/src/public/app/widgets/type_widgets/editable_code.js +++ b/src/public/app/widgets/type_widgets/editable_code.js @@ -171,8 +171,8 @@ export default class EditableCodeTypeWidget extends TypeWidget { } } - async executeInActiveCodeEditorEvent({resolve}) { - if (!this.isActive()) { + async executeInCodeEditorEvent({resolve, ntxId}) { + if (!this.isNoteContext(ntxId)) { return; } 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; }