From 87f436c6eadeaca0527e596f927922a469dcf599 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 9 May 2022 23:13:34 +0200 Subject: [PATCH] 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(); + } + } +}