zadam / trilium

Build your personal knowledge base with Trilium Notes
GNU Affero General Public License v3.0
27.2k stars 1.9k forks source link

(Bug report) Frontend script API getActiveContextCodeEditor() times out #4174

Open mm21 opened 1 year ago

mm21 commented 1 year ago

Trilium Version

d93e00a

What operating system are you using?

Ubuntu

What is your setup?

Server access only

Operating System Version

Ubuntu 22.04

Description

Hi,

I'm developing a widget to render Markdown code notes. I'd like to toggle the visibility of the code editor, but getActiveContextCodeEditor() times out when awaited in refreshWithNote():

Uncaught (in promise) Error: Time limit failed on TriliumMarkdownWidget with noteSwitchedEvent
    at Object.timeLimit (utils.js:305:19)
    at TriliumMarkdownWidget.callMethod (component.js:113:26)
    at TriliumMarkdownWidget.handleEvent (component.js:48:24)
    at NoteWrapperWidget.handleEventInChildren (component.js:74:31)
    at NoteWrapperWidget.handleEvent (component.js:50:42)
    at SplitNoteContainer.handleEventInChildren (split_note_container.js:187:28)
    at SplitNoteContainer.handleEvent (component.js:50:42)
    at FlexContainer.handleEventInChildren (component.js:74:31)
    at FlexContainer.handleEvent (component.js:50:42)
    at FlexContainer.handleEventInChildren (component.js:74:31)

And when called in doRender() it returns undefined.

Error logs

Backend log:

Error: Loading note contexts '[{"ntxId":"MdtQvM","mainNtxId":null,"notePath":"root/wRwPDkjcK5lF","hoistedNoteId":"root","active":true,"viewScope":{"viewMode":"default"}}]' failed: Time limit failed on SplitNoteContainer with newNoteContextCreatedEvent Error: Time limit failed on SplitNoteContainer with newNoteContextCreatedEvent
    at Object.timeLimit (http://vm-dev-1:8080/assets/v0.61.4-beta/app/services/utils.js:305:19)
    at SplitNoteContainer.callMethod (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:113:26)
    at SplitNoteContainer.handleEvent (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:48:24)
    at FlexContainer.handleEventInChildren (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:74:31)
    at FlexContainer.handleEvent (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:50:42)
    at FlexContainer.handleEventInChildren (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:74:31)
    at FlexContainer.handleEvent (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:50:42)
    at FlexContainer.handleEventInChildren (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:74:31)
    at FlexContainer.handleEvent (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:50:42)
    at RootContainer.handleEventInChildren (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/component.js:74:31)
Stack: Error
    at logError (http://vm-dev-1:8080/assets/v0.61.4-beta/app/services/ws.js:24:20)
    at TabManager.loadTabs (http://vm-dev-1:8080/assets/v0.61.4-beta/app/components/tab_manager.js:127:13)
zadam commented 1 year ago

Hello, this is expected to happen when there isn't any code editor in the active context. Is that the case?

I've just committed to master a change with a more reasonable timeout returning null instead of the current behavior.

mm21 commented 1 year ago

Thanks for checking. There is a code editor in the context where I'd like to toggle it, which I think is the active context. Here's a screenshot of my current progress:

image

But maybe I'm misunderstanding the purpose of this API. Ideally I'd like to get a reference to the code editor in my widget's doRender(), then hide/show it when the toggle button is clicked. Is this use case supported?

zerebos commented 1 year ago

If it just needs to be visually hidden, you don't necessarily need the editor instance to do so. You could use css with the right selector (.CodeMirror-wrap) to directly hide it. I also worked on a project like this, but hiding the editor wasn't necessary in my case https://github.com/rauenzi/Trilium-MarkdownPreview

Where in your code are you calling getActiveContextCodeEditor?

mm21 commented 1 year ago

Thanks for the suggestion. This does work, but then it shows/hides the code editor on other notes as well. I had figured there would be a separate editor object for each note, but now I realize they all share the same DOM element. One problem is if the editor is hidden, the content doesn't seem to get refreshed when switching to another note - when shown again, it shows the content of the previous note.

That's a really great project, thanks for sharing! I'd like to write most of my notes in Markdown so the only thing missing for me would be the ability to hide the editor by default. I have a plan to eventually sync a folder of Markdown files and use a custom renderer for links to other notes and images. (When syncing the folder I'll generate a noteId from the relative path of the .md/image file, so the renderer can use the same algorithm to lookup the target note/image.)

Where in your code are you calling getActiveContextCodeEditor?

I tried calling it in doRender() (handling it with .then()) and awaiting it in refreshWithNote().

zerebos commented 1 year ago

I'd like to write most of my notes in Markdown so the only thing missing for me would be the ability to hide the editor by default.

A markdown-based editor may or may not be a plugin I'm working on in secret 🤫

I tried calling it in doRender() (handling it with .then()) and awaiting it in refreshWithNote().

As far as I know, doRender() is only called on the initial render and it's up to the widget to update their elements on refreshWithNote. Without seeing more of the code, it's hard to diagnose exactly what's going on. You can try calling it in the noteSwitch event, or make sure you're awaiting note.getNoteComplement in refreshWithNote. Without awaiting the complement you might get the previous editor.

This seemed to work for me:

    async refreshWithNote(note) {
        await note.getNoteComplement();
        const editor = await api.getActiveContextCodeEditor();
        console.log(editor?.doc?.getValue())
    }
mm21 commented 1 year ago

Thanks for the tips. For some reason it still timed out when awaiting note.getNoteComplement first in refreshWithNote. However I was able to get the intended behavior by selecting .note-detail-code, handling the case where it isn't created yet upon initial load.

Also I found out the editor DOM wasn't get updated when the containing div had display: none, and this seems to be the expected behavior. Positioning it off the screen allowed it to still get content updates.

The next challenge was making it hidden by default for Markdown code notes while keeping other code types visible, but that was easy enough by implementing noteSwitched. Here's the complete code for reference:

```javascript const TPL = `

`; class TriliumMarkdownWidget extends api.NoteContextAwareWidget { get position() { return 20; } get parentWidget() { return "note-detail-pane"; } isEnabled() { return super.isEnabled() && this.note.type === "code" && this.note.mime === "text/x-markdown"; } doRender() { this.$widget = $(TPL); this.$render = this.$widget.find(".rendered-markdown"); this.$toggle = this.$widget.find(".toggle"); this.$handle = this.$widget.find(".handle"); var $widget = this; this.$toggle.click(function() { $widget.$toggle.toggleClass("on"); if ($widget.$toggle.hasClass("on")) { $widget.$handle.animate({ left: "16px" }, 200); $(".note-detail-code").removeClass("editor-hidden"); } else { $widget.$handle.animate({ left: "1px" }, 200); $(".note-detail-code").addClass("editor-hidden"); } }); return this.$widget; } async refreshWithNote(note) { /* render content */ const {content} = await note.getNoteComplement(); this.$render.html(marked.parse(content, {mangle: false, headerIds: false})); /* set visibility of editor */ if (this.$toggle.hasClass("on")) { $(".note-detail-code").removeClass("editor-hidden"); } else { /* check if editor is present (not present for initial load) */ var editor = document.querySelector(".note-detail-code"); if (editor === null) { /* detect when editor is added to DOM so it can be initially hidden */ function elementAddedCallback(mutationsList, observer) { for (let mutation of mutationsList) { if (mutation.type === "childList") { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && $(node).hasClass("note-detail-code")) { $(node).addClass("editor-hidden"); observer.disconnect(); } }); } } } const observerConfig = {childList: true, subtree: true}; const observer = new MutationObserver(elementAddedCallback); observer.observe(document.body, observerConfig); } else { editor.classList.add("editor-hidden"); } } } async entitiesReloadedEvent({loadResults}) { if (loadResults.isNoteContentReloaded(this.noteId)) { this.refresh(); } } async noteSwitched() { /* reset widget content */ this.$render.html(""); /* if not switching to another Markdown note, restore editor */ if (this.note.type !== "code" || this.note.mime !== "text/x-markdown") { $(".note-detail-code").removeClass("editor-hidden"); } super.noteSwitched(); } } module.exports = new TriliumMarkdownWidget(); ```

And the #appCss note:

```css div.toggle { width: 32px; height: 16px; background-color: #ccc; border-radius: 8px; position: relative; } div.handle { width: 14px; height: 14px; background-color: white; border-radius: 50%; position: absolute; top: 1px; left: 1px; transition: left 0.1s ease; } div.editor-hidden { /* with display: none, content doesn't get updated * with visibility: hidden, scroll bar persists */ position: absolute; left: -9999px; } ```