From bc0a6fb897ea90379b89677c7969134adfba2cb8 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 15 Jul 2024 10:45:25 +0200 Subject: [PATCH 01/65] Add metadata to the beginning of the serialized buffer. Store the cursors' positions in the buffer metadata and restore the cursors when loading the buffer content. --- src/editor/editor.js | 53 ++++++++++++++++++++++++++------------- src/editor/note-format.js | 35 ++++++++++++++++++++++++++ src/editor/save.js | 8 +++--- 3 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 src/editor/note-format.js diff --git a/src/editor/editor.js b/src/editor/editor.js index ecc1f5ba..1c070ac5 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -1,4 +1,4 @@ -import { Annotation, EditorState, Compartment, Facet } from "@codemirror/state" +import { Annotation, EditorState, Compartment, Facet, EditorSelection } from "@codemirror/state" import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view" import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language" import { markdown } from "@codemirror/lang-markdown" @@ -21,6 +21,7 @@ import { languageDetection } from "./language-detection/autodetect.js" import { autoSaveContent } from "./save.js" import { todoCheckboxPlugin} from "./todo-checkbox.ts" import { links } from "./links.js" +import { NoteFormat } from "./note-format.js" export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector" @@ -61,9 +62,10 @@ export class HeynoteEditor { this.fontTheme = new Compartment this.defaultBlockToken = "text" this.defaultBlockAutoDetect = true + this.saveFunction = saveFunction const state = EditorState.create({ - doc: content || "", + doc: "", extensions: [ this.keymapCompartment.of(getKeymapExtensions(this, keymap)), heynoteCopyCut(this), @@ -96,7 +98,7 @@ export class HeynoteEditor { return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"} }), - saveFunction ? autoSaveContent(saveFunction, 2000) : [], + this.saveFunction ? autoSaveContent(this, 2000) : [], todoCheckboxPlugin, markdown(), @@ -107,7 +109,7 @@ export class HeynoteEditor { // make sure saveFunction is called when page is unloaded if (saveFunction) { window.addEventListener("beforeunload", () => { - saveFunction(this.getContent()) + this.save() }) } @@ -116,36 +118,53 @@ export class HeynoteEditor { parent: element, }) - // Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers - // when moving the cursor to the end of the buffer when the program starts - ensureSyntaxTree(state, state.doc.length, 5000) + this.setContent(content) if (focus) { - this.view.dispatch({ - selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length}, - scrollIntoView: true, - }) this.view.focus() } } + save() { + this.saveFunction(this.getContent()) + } + getContent() { - return this.view.state.sliceDoc() + this.note.content = this.view.state.sliceDoc() + this.note.cursors = this.view.state.selection.toJSON() + return this.note.serialize() } setContent(content) { + this.note = NoteFormat.load(content) + + // set buffer content this.view.dispatch({ changes: { from: 0, to: this.view.state.doc.length, - insert: content, + insert: this.note.content, }, annotations: [heynoteEvent.of(SET_CONTENT)], }) - this.view.dispatch({ - selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length}, - scrollIntoView: true, - }) + + // Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers + // when moving the cursor to the end of the buffer when the program starts + ensureSyntaxTree(this.view.state, this.view.state.doc.length, 5000) + + // set cursor positions + if (this.note.cursors) { + this.view.dispatch({ + selection: EditorSelection.fromJSON(this.note.cursors), + scrollIntoView: true, + }) + } else { + // if metadata doesn't contain cursor position, we set the cursor to the end of the buffer + this.view.dispatch({ + selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length}, + scrollIntoView: true, + }) + } } getBlocks() { diff --git a/src/editor/note-format.js b/src/editor/note-format.js new file mode 100644 index 00000000..0d1313b4 --- /dev/null +++ b/src/editor/note-format.js @@ -0,0 +1,35 @@ +export class NoteFormat { + constructor() { + this.content = ''; + this.metadata = {}; + } + + static load(data) { + const note = new NoteFormat(); + + note.content = data + const firstSeparator = data.indexOf("\n∞∞∞") + if (firstSeparator !== -1) { + const metadataContent = data.slice(0, firstSeparator).trim() + if (metadataContent !== "") { + note.metadata = JSON.parse(metadataContent) + } + note.content = data.slice(firstSeparator) + } + + return note + } + + serialize() { + this.metadata.formatVersion = "1.0" + return JSON.stringify(this.metadata) + this.content + } + + set cursors(cursors) { + this.metadata.cursors = cursors + } + + get cursors() { + return this.metadata.cursors + } +} diff --git a/src/editor/save.js b/src/editor/save.js index 81b74220..415a8eec 100644 --- a/src/editor/save.js +++ b/src/editor/save.js @@ -2,17 +2,17 @@ import { ViewPlugin } from "@codemirror/view" import { debounce } from "debounce" -export const autoSaveContent = (saveFunction, interval) => { - const save = debounce((view) => { +export const autoSaveContent = (editor, interval) => { + const save = debounce(() => { //console.log("saving buffer") - saveFunction(view.state.sliceDoc()) + editor.save() }, interval); return ViewPlugin.fromClass( class { update(update) { if (update.docChanged) { - save(update.view) + save() } } } From 00ec8d53accf58716af25e03d66a139ac3df5e85 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 15 Jul 2024 13:13:25 +0200 Subject: [PATCH 02/65] Fix broken tests --- tests/test-utils.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test-utils.js b/tests/test-utils.js index 9e841c47..b2760cf3 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import { NoteFormat } from '../src/editor/note-format.js'; export function pageErrorGetter(page) { let messages = []; @@ -26,10 +27,15 @@ export class HeynotePage { return await this.page.evaluate(() => window._heynote_editor.getBlocks()) } - async getContent() { + async getBufferData() { return await this.page.evaluate(() => window._heynote_editor.getContent()) } + async getContent() { + const note = NoteFormat.load(await this.getBufferData()) + return note.content + } + async setContent(content) { await expect(this.page.locator("css=.cm-editor")).toBeVisible() await this.page.evaluate((content) => window._heynote_editor.setContent(content), content) From e465696959531e900000ebccf6fdb279e099cdb3 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 15 Jul 2024 13:32:14 +0200 Subject: [PATCH 03/65] Add test for saving and restoring cursor positions --- tests/note-format.spec.js | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/note-format.spec.js diff --git a/tests/note-format.spec.js b/tests/note-format.spec.js new file mode 100644 index 00000000..33d5655a --- /dev/null +++ b/tests/note-format.spec.js @@ -0,0 +1,48 @@ +import { test, expect } from "@playwright/test"; +import { HeynotePage } from "./test-utils.js"; +import { NoteFormat } from "../src/editor/note-format.js"; + +let heynotePage + +test.beforeEach(async ({ page }) => { + heynotePage = new HeynotePage(page) + await heynotePage.goto() +}); + + +test("test restore cursor position", async ({ page, browserName }) => { + heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}} +∞∞∞text +Textblock`) + await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+Enter") + expect(await heynotePage.getContent()).toBe(` +∞∞∞text +Text +∞∞∞text +block`) +}) + + +test("test save cursor positions", async ({ page, browserName }) => { + heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}} +∞∞∞text +this +is +a +text +block`) + await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+ArrowDown") + await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+ArrowDown") + await page.locator("body").press("Delete") + expect(await heynotePage.getContent()).toBe(` +∞∞∞text +his +s + +text +block`) + + const bufferData = await heynotePage.getBufferData() + const note = NoteFormat.load(bufferData) + expect(note.cursors.ranges.length).toBe(3) +}) From 3bf7108fa2e06eab259e184e5df387790c08b472 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 15 Jul 2024 20:23:17 +0200 Subject: [PATCH 04/65] Fix race condition that could cause the editor to not scroll the cursor(s) into the viewport upon loading --- src/editor/editor.js | 58 ++++++++++++++++++++---------------- tests/block-creation.spec.js | 2 +- tests/formatting.spec.js | 4 +-- tests/note-format.spec.js | 4 +-- 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/editor/editor.js b/src/editor/editor.js index 1c070ac5..cc86622e 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -136,35 +136,41 @@ export class HeynoteEditor { } setContent(content) { - this.note = NoteFormat.load(content) - - // set buffer content - this.view.dispatch({ - changes: { - from: 0, - to: this.view.state.doc.length, - insert: this.note.content, - }, - annotations: [heynoteEvent.of(SET_CONTENT)], - }) - - // Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers - // when moving the cursor to the end of the buffer when the program starts - ensureSyntaxTree(this.view.state, this.view.state.doc.length, 5000) - - // set cursor positions - if (this.note.cursors) { + return new Promise((resolve) => { + this.note = NoteFormat.load(content) + + // set buffer content this.view.dispatch({ - selection: EditorSelection.fromJSON(this.note.cursors), - scrollIntoView: true, + changes: { + from: 0, + to: this.view.state.doc.length, + insert: this.note.content, + }, + annotations: [heynoteEvent.of(SET_CONTENT)], }) - } else { - // if metadata doesn't contain cursor position, we set the cursor to the end of the buffer - this.view.dispatch({ - selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length}, - scrollIntoView: true, + + // Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers + // when moving the cursor to the end of the buffer when the program starts + ensureSyntaxTree(this.view.state, this.view.state.doc.length, 5000) + + // Set cursor positions + // We use requestAnimationFrame to avoid a race condition causing the scrollIntoView to sometimes not work + requestAnimationFrame(() => { + if (this.note.cursors) { + this.view.dispatch({ + selection: EditorSelection.fromJSON(this.note.cursors), + scrollIntoView: true, + }) + } else { + // if metadata doesn't contain cursor position, we set the cursor to the end of the buffer + this.view.dispatch({ + selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length}, + scrollIntoView: true, + }) + } + resolve() }) - } + }) } getBlocks() { diff --git a/tests/block-creation.spec.js b/tests/block-creation.spec.js index 2d9e54c4..b00a55f1 100644 --- a/tests/block-creation.spec.js +++ b/tests/block-creation.spec.js @@ -8,7 +8,7 @@ test.beforeEach(async ({page}) => { await heynotePage.goto() expect((await heynotePage.getBlocks()).length).toBe(1) - heynotePage.setContent(` + await heynotePage.setContent(` ∞∞∞text Block A ∞∞∞text diff --git a/tests/formatting.spec.js b/tests/formatting.spec.js index 5b5bbac4..4a633100 100644 --- a/tests/formatting.spec.js +++ b/tests/formatting.spec.js @@ -10,7 +10,7 @@ test.beforeEach(async ({ page }) => { test("JSON formatting", async ({ page }) => { - heynotePage.setContent(` + await heynotePage.setContent(` ∞∞∞json {"test": 1, "key2": "hey!"} `) @@ -25,7 +25,7 @@ test("JSON formatting", async ({ page }) => { }) test("JSON formatting (cursor at start)", async ({ page }) => { - heynotePage.setContent(` + await heynotePage.setContent(` ∞∞∞json {"test": 1, "key2": "hey!"} `) diff --git a/tests/note-format.spec.js b/tests/note-format.spec.js index 33d5655a..70c26f30 100644 --- a/tests/note-format.spec.js +++ b/tests/note-format.spec.js @@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => { test("test restore cursor position", async ({ page, browserName }) => { - heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}} + await heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}} ∞∞∞text Textblock`) await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+Enter") @@ -24,7 +24,7 @@ block`) test("test save cursor positions", async ({ page, browserName }) => { - heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}} + await heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}} ∞∞∞text this is From af9c93017c29c638bb32d4189162eb0307989056 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 16 Jul 2024 10:24:15 +0200 Subject: [PATCH 05/65] Add test that checks that existing keys are kept in buffer files' metadata --- tests/note-format.spec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/note-format.spec.js b/tests/note-format.spec.js index 70c26f30..ec68b5a7 100644 --- a/tests/note-format.spec.js +++ b/tests/note-format.spec.js @@ -46,3 +46,17 @@ block`) const note = NoteFormat.load(bufferData) expect(note.cursors.ranges.length).toBe(3) }) + +test("unknown note metadata keys is kept", async ({ page, browserName }) => { + await heynotePage.setContent(`{"yoda":[123], "formatVersion":"1.0", "cursors":{"ranges":[{"anchor":15,"head":15}],"main":0}} +∞∞∞text +block 1`) + await page.locator("body").pressSequentially("hello") + expect(await heynotePage.getContent()).toBe(` +∞∞∞text +block hello1`) + + const bufferData = await heynotePage.getBufferData() + const note = NoteFormat.load(bufferData) + expect(note.metadata.yoda).toStrictEqual([123]) +}) \ No newline at end of file From 8f2d48d6eaef1e1a3c4ecca2101829d8c0f808a7 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 17 Jul 2024 15:20:21 +0200 Subject: [PATCH 06/65] Throw error if major format version is greater than the currently supported version --- package-lock.json | 98 +++++++-------------------------------- package.json | 3 +- src/components/Editor.vue | 61 +++++++++++++----------- src/editor/editor.js | 13 ++++-- src/editor/note-format.js | 14 +++++- 5 files changed, 73 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a376c0c..810c8fbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.8.0", "license": "Commons Clause MIT", "dependencies": { - "electron-log": "^5.0.1" + "electron-log": "^5.0.1", + "semver": "^7.6.3" }, "devDependencies": { "@codemirror/autocomplete": "^6.11.1", @@ -572,6 +573,15 @@ "global-agent": "^3.0.0" } }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@electron/universal": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", @@ -2089,21 +2099,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/app-builder-lib/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2721,21 +2716,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/conf/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -3465,21 +3445,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/electron-updater/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -3914,22 +3879,6 @@ "node": ">=10.0" } }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -5367,12 +5316,14 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-compare": { @@ -6388,21 +6339,6 @@ "typescript": "*" } }, - "node_modules/vue-tsc/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", diff --git a/package.json b/package.json index 48493c7c..d3186170 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "vue-tsc": "^1.0.16" }, "dependencies": { - "electron-log": "^5.0.1" + "electron-log": "^5.0.1", + "semver": "^7.6.3" } } diff --git a/src/components/Editor.vue b/src/components/Editor.vue index b3bc9389..83f56ffe 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -58,35 +58,40 @@ // load buffer content and create editor window.heynote.buffer.load().then((content) => { - let diskContent = content - this.editor = new HeynoteEditor({ - element: this.$refs.editor, - content: content, - theme: this.theme, - saveFunction: (content) => { - if (content === diskContent) { - return - } + try { + let diskContent = content + this.editor = new HeynoteEditor({ + element: this.$refs.editor, + content: content, + theme: this.theme, + saveFunction: (content) => { + if (content === diskContent) { + return + } + diskContent = content + window.heynote.buffer.save(content) + }, + keymap: this.keymap, + emacsMetaKey: this.emacsMetaKey, + showLineNumberGutter: this.showLineNumberGutter, + showFoldGutter: this.showFoldGutter, + bracketClosing: this.bracketClosing, + fontFamily: this.fontFamily, + fontSize: this.fontSize, + }) + window._heynote_editor = this.editor + window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded) + this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + + // set up buffer change listener + window.heynote.buffer.onChangeCallback((event, content) => { diskContent = content - window.heynote.buffer.save(content) - }, - keymap: this.keymap, - emacsMetaKey: this.emacsMetaKey, - showLineNumberGutter: this.showLineNumberGutter, - showFoldGutter: this.showFoldGutter, - bracketClosing: this.bracketClosing, - fontFamily: this.fontFamily, - fontSize: this.fontSize, - }) - window._heynote_editor = this.editor - window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded) - this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) - - // set up buffer change listener - window.heynote.buffer.onChangeCallback((event, content) => { - diskContent = content - this.editor.setContent(content) - }) + this.editor.setContent(content) + }) + } catch (e) { + alert("Error! " + e.message) + throw e + } }) // set up window close handler that will save the buffer and quit window.heynote.onWindowClose(() => { diff --git a/src/editor/editor.js b/src/editor/editor.js index cc86622e..8df85a96 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -117,9 +117,9 @@ export class HeynoteEditor { state: state, parent: element, }) - + this.setContent(content) - + if (focus) { this.view.focus() } @@ -136,9 +136,14 @@ export class HeynoteEditor { } setContent(content) { - return new Promise((resolve) => { + try { this.note = NoteFormat.load(content) - + this.setReadOnly(false) + } catch (e) { + this.setReadOnly(true) + throw e + } + return new Promise((resolve) => { // set buffer content this.view.dispatch({ changes: { diff --git a/src/editor/note-format.js b/src/editor/note-format.js index 0d1313b4..cb5ceb26 100644 --- a/src/editor/note-format.js +++ b/src/editor/note-format.js @@ -1,7 +1,13 @@ +import { major } from "semver"; + + +const FORMAT_VERSION = "1.0.0" + + export class NoteFormat { constructor() { this.content = ''; - this.metadata = {}; + this.metadata = {formatVersion: "0.0.0"}; } static load(data) { @@ -16,12 +22,16 @@ export class NoteFormat { } note.content = data.slice(firstSeparator) } + + if (major(note.metadata.formatVersion) > major(FORMAT_VERSION)) { + throw new Error(`Unsupported Heynote format version: ${note.metadata.formatVersion}. You probably need to update Heynote.`) + } return note } serialize() { - this.metadata.formatVersion = "1.0" + this.metadata.formatVersion = FORMAT_VERSION return JSON.stringify(this.metadata) + this.content } From 67c43df34dbe5cd131e36f4425e370264f82cc41 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 17 Jul 2024 15:30:08 +0200 Subject: [PATCH 07/65] Fix tests --- tests/note-format.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/note-format.spec.js b/tests/note-format.spec.js index ec68b5a7..f9acfb93 100644 --- a/tests/note-format.spec.js +++ b/tests/note-format.spec.js @@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => { test("test restore cursor position", async ({ page, browserName }) => { - await heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}} + await heynotePage.setContent(`{"formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}} ∞∞∞text Textblock`) await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+Enter") @@ -24,7 +24,7 @@ block`) test("test save cursor positions", async ({ page, browserName }) => { - await heynotePage.setContent(`{"formatVersion":"1.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}} + await heynotePage.setContent(`{"formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}} ∞∞∞text this is @@ -48,7 +48,7 @@ block`) }) test("unknown note metadata keys is kept", async ({ page, browserName }) => { - await heynotePage.setContent(`{"yoda":[123], "formatVersion":"1.0", "cursors":{"ranges":[{"anchor":15,"head":15}],"main":0}} + await heynotePage.setContent(`{"yoda":[123], "formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":15,"head":15}],"main":0}} ∞∞∞text block 1`) await page.locator("body").pressSequentially("hello") From 7d7f6803775a55dfa192cd57c7809d4a8d9f7165 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 24 Jul 2024 12:31:19 +0200 Subject: [PATCH 08/65] Add a way to display error dialogs --- electron/main/index.ts | 4 ++ electron/preload/index.ts | 6 +- package-lock.json | 88 +++++++++++++++++------- package.json | 1 + src/components/ErrorMessages.vue | 114 +++++++++++++++++++++++++++++++ src/main.js | 10 +++ src/stores/error-store.js | 21 ++++++ webapp/bridge.js | 4 ++ webapp/main.js | 4 ++ 9 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 src/components/ErrorMessages.vue create mode 100644 src/stores/error-store.js diff --git a/electron/main/index.ts b/electron/main/index.ts index 6d98db32..01393fe3 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -50,6 +50,7 @@ Menu.setApplicationMenu(menu) export let win: BrowserWindow | null = null let tray: Tray | null = null; +let initErrors: string[] = [] // Here, you can also use other preload const preload = join(__dirname, '../preload/index.js') const url = process.env.VITE_DEV_SERVER_URL @@ -350,6 +351,9 @@ ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource) // load buffer on app start loadBuffer() +ipcMain.handle("getInitErrors", () => { + return initErrors +}) ipcMain.handle('settings:set', async (event, settings) => { diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 64e82629..7d5f2427 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -103,7 +103,11 @@ contextBridge.exposeInMainWorld("heynote", { async getVersion() { return await ipcRenderer.invoke("getVersion") - } + }, + + async getInitErrors() { + return await ipcRenderer.invoke("getInitErrors") + }, }) diff --git a/package-lock.json b/package-lock.json index 810c8fbf..34812c29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Commons Clause MIT", "dependencies": { "electron-log": "^5.0.1", + "pinia": "^2.1.7", "semver": "^7.6.3" }, "devDependencies": { @@ -242,7 +243,6 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -1013,8 +1013,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@lezer/common": { "version": "1.2.1", @@ -1732,7 +1731,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.11.tgz", "integrity": "sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w==", - "dev": true, "dependencies": { "@babel/parser": "^7.23.5", "@vue/shared": "3.3.11", @@ -1744,7 +1742,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.11.tgz", "integrity": "sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw==", - "dev": true, "dependencies": { "@vue/compiler-core": "3.3.11", "@vue/shared": "3.3.11" @@ -1754,7 +1751,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.11.tgz", "integrity": "sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA==", - "dev": true, "dependencies": { "@babel/parser": "^7.23.5", "@vue/compiler-core": "3.3.11", @@ -1772,12 +1768,16 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.11.tgz", "integrity": "sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg==", - "dev": true, "dependencies": { "@vue/compiler-dom": "3.3.11", "@vue/shared": "3.3.11" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, "node_modules/@vue/language-core": { "version": "1.8.25", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.25.tgz", @@ -1831,7 +1831,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.11.tgz", "integrity": "sha512-D5tcw091f0nuu+hXq5XANofD0OXnBmaRqMYl5B3fCR+mX+cXJIGNw/VNawBqkjLNWETrFW0i+xH9NvDbTPVh7g==", - "dev": true, "dependencies": { "@vue/shared": "3.3.11" } @@ -1840,7 +1839,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.11.tgz", "integrity": "sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg==", - "dev": true, "dependencies": { "@babel/parser": "^7.23.5", "@vue/compiler-core": "3.3.11", @@ -1853,7 +1851,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.11.tgz", "integrity": "sha512-g9ztHGwEbS5RyWaOpXuyIVFTschclnwhqEbdy5AwGhYOgc7m/q3NFwr50MirZwTTzX55JY8pSkeib9BX04NIpw==", - "dev": true, "dependencies": { "@vue/reactivity": "3.3.11", "@vue/shared": "3.3.11" @@ -1863,7 +1860,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.11.tgz", "integrity": "sha512-OlhtV1PVpbgk+I2zl+Y5rQtDNcCDs12rsRg71XwaA2/Rbllw6mBLMi57VOn8G0AjOJ4Mdb4k56V37+g8ukShpQ==", - "dev": true, "dependencies": { "@vue/runtime-core": "3.3.11", "@vue/shared": "3.3.11", @@ -1874,7 +1870,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.11.tgz", "integrity": "sha512-AIWk0VwwxCAm4wqtJyxBylRTXSy1wCLOKbWxHaHiu14wjsNYtiRCSgVuqEPVuDpErOlRdNnuRgipQfXRLjLN5A==", - "dev": true, "dependencies": { "@vue/compiler-ssr": "3.3.11", "@vue/shared": "3.3.11" @@ -1886,8 +1881,7 @@ "node_modules/@vue/shared": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.11.tgz", - "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==", - "dev": true + "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==" }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", @@ -2756,8 +2750,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/de-indent": { "version": "1.0.2", @@ -3570,8 +3563,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/extract-zip": { "version": "2.0.1", @@ -4444,7 +4436,6 @@ "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -4611,7 +4602,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -4846,8 +4836,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4861,6 +4850,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz", + "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", @@ -4935,7 +4974,6 @@ "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5443,7 +5481,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5749,7 +5786,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6295,7 +6332,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.11.tgz", "integrity": "sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==", - "dev": true, "dependencies": { "@vue/compiler-dom": "3.3.11", "@vue/compiler-sfc": "3.3.11", diff --git a/package.json b/package.json index d3186170..1688cc12 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ }, "dependencies": { "electron-log": "^5.0.1", + "pinia": "^2.1.7", "semver": "^7.6.3" } } diff --git a/src/components/ErrorMessages.vue b/src/components/ErrorMessages.vue new file mode 100644 index 00000000..a5a9424d --- /dev/null +++ b/src/components/ErrorMessages.vue @@ -0,0 +1,114 @@ + + + + + + + diff --git a/src/main.js b/src/main.js index 7836648a..456d02ec 100644 --- a/src/main.js +++ b/src/main.js @@ -1,17 +1,27 @@ import './css/application.sass' import { createApp } from 'vue' +import { createPinia } from 'pinia' + import App from './components/App.vue' import { loadCurrencies } from './currency' +import { useErrorStore } from './stores/error-store' +const pinia = createPinia() const app = createApp(App) +app.use(pinia) app.mount('#app').$nextTick(() => { // hide loading screen postMessage({ payload: 'removeLoading' }, '*') }) +const errorStore = useErrorStore() +window.heynote.getInitErrors().then((errors) => { + errors.forEach((e) => errorStore.addError(e)) +}) + diff --git a/src/stores/error-store.js b/src/stores/error-store.js new file mode 100644 index 00000000..cc3aa1c6 --- /dev/null +++ b/src/stores/error-store.js @@ -0,0 +1,21 @@ +import { defineStore } from 'pinia' + +export const useErrorStore = defineStore("errors", { + state: () => ({ + errors: [], + }), + + actions: { + setErrors(errors) { + this.errors = errors + }, + + addError(error) { + this.errors.push(error) + }, + + popError() { + this.errors.splice(0, 1) + }, + }, +}) diff --git a/webapp/bridge.js b/webapp/bridge.js index 06ef81b2..57d9098d 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -152,6 +152,10 @@ const Heynote = { async getVersion() { return __APP_VERSION__ + " (" + __GIT_HASH__ + ")" }, + + async getInitErrors() { + + }, } export { Heynote, ipcRenderer} diff --git a/webapp/main.js b/webapp/main.js index 01d18da9..f02d7936 100644 --- a/webapp/main.js +++ b/webapp/main.js @@ -1,10 +1,14 @@ import '../src/css/application.sass' import { createApp } from 'vue' +import { createPinia } from 'pinia' + import App from '../src/components/App.vue' import { loadCurrencies } from '../src/currency' +const pinia = createPinia() const app = createApp(App) +app.use(pinia) app.mount('#app') //console.log("test:", app.hej.test) From a9ea48954d18a424d31d5dae73648cddd534173b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 24 Jul 2024 13:52:44 +0200 Subject: [PATCH 09/65] WIP: Implement support for multiple notes Refactor Vue <-> Editor <-> CodeMirror code. Introduce Pinia store to keep global state, in order to get rid of a lot of event juggling between Editor class/child components and the root App component. --- electron/main/buffer.js | 48 +++-- electron/main/file-library.js | 210 ++++++++++++++++++++ electron/main/index.ts | 17 +- electron/preload/index.ts | 60 +++++- public/langdetect-worker.js | 3 + src/components/App.vue | 109 +++++++--- src/components/Editor.vue | 141 ++++++------- src/components/NewNote.vue | 158 +++++++++++++++ src/components/NoteSelector.vue | 186 +++++++++++++++++ src/components/StatusBar.vue | 47 +++-- src/components/form/FolderSelect.vue | 53 +++++ src/editor/block/block.js | 49 ++--- src/editor/editor.js | 92 +++++++-- src/editor/event.js | 9 - src/editor/keymap.js | 2 + src/editor/language-detection/autodetect.js | 175 ++++++++-------- src/editor/save.js | 8 +- src/main.js | 4 + src/stores/notes-store.js | 65 ++++++ webapp/bridge.js | 30 ++- 20 files changed, 1175 insertions(+), 291 deletions(-) create mode 100644 electron/main/file-library.js create mode 100644 src/components/NewNote.vue create mode 100644 src/components/NoteSelector.vue create mode 100644 src/components/form/FolderSelect.vue delete mode 100644 src/editor/event.js create mode 100644 src/stores/notes-store.js diff --git a/electron/main/buffer.js b/electron/main/buffer.js index b8799dd3..0ecc5a75 100644 --- a/electron/main/buffer.js +++ b/electron/main/buffer.js @@ -16,15 +16,15 @@ const untildify = (pathWithTilde) => { : pathWithTilde; } -export function constructBufferFilePath(directoryPath) { - return join(untildify(directoryPath), isDev ? "buffer-dev.txt" : "buffer.txt") +export function constructBufferFilePath(directoryPath, path) { + return join(untildify(directoryPath), path) } -export function getBufferFilePath() { +export function getFullBufferFilePath(path) { let defaultPath = app.getPath("userData") let configPath = CONFIG.get("settings.bufferPath") let bufferPath = configPath.length ? configPath : defaultPath - let bufferFilePath = constructBufferFilePath(bufferPath) + let bufferFilePath = constructBufferFilePath(bufferPath, path) try { // use realpathSync to resolve a potential symlink return fs.realpathSync(bufferFilePath) @@ -103,39 +103,45 @@ export class Buffer { // Buffer -let buffer -export function loadBuffer() { - if (buffer) { - buffer.close() +let buffers = {} +export function loadBuffer(path) { + if (buffers[path]) { + buffers[path].close() } - buffer = new Buffer({ - filePath: getBufferFilePath(), + buffers[path] = new Buffer({ + filePath: getFullBufferFilePath(path), onChange: (content) => { - win?.webContents.send("buffer-content:change", content) + console.log("Old buffer.js onChange") + win?.webContents.send("buffer-content:change", path, content) }, }) - return buffer + return buffers[path] } -ipcMain.handle('buffer-content:load', async () => { - if (buffer.exists() && !(eraseInitialContent && isDev)) { - return await buffer.load() +ipcMain.handle('buffer-content:load', async (event, path) => { + if (!buffers[path]) { + loadBuffer(path) + } + if (buffers[path].exists() && !(eraseInitialContent && isDev)) { + return await buffers[path].load() } else { return isDev ? initialDevContent : initialContent } }); -async function save(content) { - return await buffer.save(content) +async function save(path, content) { + return await buffers[path].save(content) } -ipcMain.handle('buffer-content:save', async (event, content) => { - return await save(content) +ipcMain.handle('buffer-content:save', async (event, path, content) => { + return await save(path, content) }); export let contentSaved = false -ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => { - await save(content) +ipcMain.handle('buffer-content:saveAndQuit', async (event, contents) => { + for (const [path, content] of contents) { + await save(path, content) + } contentSaved = true app.quit() }) diff --git a/electron/main/file-library.js b/electron/main/file-library.js new file mode 100644 index 00000000..e1bb7f12 --- /dev/null +++ b/electron/main/file-library.js @@ -0,0 +1,210 @@ +import fs from "fs" +import os from "node:os" +import { join, dirname, basename } from "path" + +import * as jetpack from "fs-jetpack"; +import { app, ipcMain, dialog } from "electron" + + +const untildify = (pathWithTilde) => { + const homeDir = os.homedir() + return homeDir ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDir) : pathWithTilde +} + +async function readNoteMetadata(filePath) { + const chunks = [] + for await (let chunk of fs.createReadStream(filePath, { start: 0, end:4000 })) { + chunks.push(chunk) + } + const headContent = Buffer.concat(chunks).toString("utf8") + const firstSeparator = headContent.indexOf("\n∞∞∞") + if (firstSeparator === -1) { + return null + } + try { + const metadata = JSON.parse(headContent.slice(0, firstSeparator).trim()) + return {"name": metadata.name, "tags": metadata.tags} + } catch (e) { + return {} + } +} + + +export class FileLibrary { + constructor(basePath) { + basePath = untildify(basePath) + if (jetpack.exists(basePath) !== "dir") { + throw new Error(`Path directory does not exist: ${basePath}`) + } + this.basePath = fs.realpathSync(basePath) + this.jetpack = jetpack.cwd(this.basePath) + this.files = {}; + this.watcher = null; + this.contentSaved = false + this.onChangeCallback = null + } + + async exists(path) { + return this.jetpack.exists(path) === "file" + } + + async load(path) { + if (this.files[path]) { + return this.files[path].read() + } + const fullPath = fs.realpathSync(join(this.basePath, path)) + this.files[path] = new NoteBuffer({fullPath, library:this}) + return await this.files[path].read() + } + + async save(path, content) { + if (!this.files[path]) { + throw new Error(`File not loaded: ${path}`) + } + return await this.files[path].save(content) + } + + async getList() { + console.log("Loading notes") + const notes = {} + const files = await this.jetpack.findAsync(this.basePath, { + matching: "*.txt", + recursive: true, + }) + const promises = [] + for (const file of files) { + promises.push(readNoteMetadata(join(this.basePath, file))) + } + const metadataList = await Promise.all(promises) + metadataList.forEach((metadata, i) => { + const path = files[i] + notes[path] = metadata + }) + return notes + } + + setupWatcher(win) { + if (!this.watcher) { + this.watcher = fs.watch( + this.basePath, + { + persistent: true, + recursive: true, + encoding: "utf8", + }, + async (eventType, changedPath) => { + console.log("File changed", eventType, changedPath) + //if (changedPath.toLowerCase().endsWith(".txt")) { + // console.log("txt", this.notes) + // if (await this.exists(changedPath)) { + // console.log("file exists!") + // const newMetadata = await readNoteMetadata(join(this.basePath, changedPath)) + // if (!(changedPath in this.notes) || newMetadata.name !== this.notes[changedPath].name) { + // this.notes[changedPath] = newMetadata + // win.webContents.send("buffer:noteMetadataChanged", changedPath, newMetadata) + // console.log("metadata changed") + // } else { + // console.log("no metadata change") + // } + // } else if (changedPath in this.notes) { + // console.log("note removed", changedPath) + // delete this.notes[changedPath] + // win.webContents.send("buffer:noteRemoved", changedPath) + // } + //} + for (const [path, buffer] of Object.entries(this.files)) { + if (changedPath === basename(path)) { + const content = await buffer.read() + if (buffer._lastSavedContent !== content) { + win.webContents.send("buffer:change", path, content) + } + } + } + } + ) + } + } + + closeFile(path) { + if (this.files[path]) { + delete this.files[path] + } + } + + close() { + for (const buffer of Object.values(this.files)) { + this.closeFile(buffer.filePath) + } + this.stopWatcher() + } + + stopWatcher() { + if (this.watcher) { + this.watcher.close() + this.watcher = null + } + } +} + + + +export class NoteBuffer { + constructor({fullPath, library}) { + this.fullPath = fullPath + this._lastSavedContent = null + this.library = library + } + + async read() { + return await this.library.jetpack.read(this.fullPath, 'utf8') + } + + async save(content) { + this._lastSavedContent = content + const saveResult = await this.library.jetpack.write(this.fullPath, content, { + atomic: true, + mode: '600', + }) + return saveResult + } + + exists() { + return jetpack.exists(this.fullPath) === "file" + } +} + + +export function setupFileLibraryEventHandlers(library, win) { + ipcMain.handle('buffer:load', async (event, path) => { + console.log("buffer:load", path) + return await library.load(path) + }); + + + ipcMain.handle('buffer:save', async (event, path, content) => { + return await library.save(path, content) + }); + + ipcMain.handle('buffer:getList', async (event) => { + return await library.getList() + }); + + ipcMain.handle('buffer:exists', async (event, path) => { + return await library.exists(path) + }); + + ipcMain.handle('buffer:close', async (event, path) => { + return await library.closeFile(path) + }); + + ipcMain.handle('buffer:saveAndQuit', async (event, contents) => { + library.stopWatcher() + for (const [path, content] of contents) { + await library.save(path, content) + } + library.contentSaved = true + app.quit() + }) + + library.setupWatcher(win) +} diff --git a/electron/main/index.ts b/electron/main/index.ts index 01393fe3..93a24f57 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -10,6 +10,7 @@ import { isDev, isLinux, isMac, isWindows } from '../detect-platform'; import { initializeAutoUpdate, checkForUpdates } from './auto-update'; import { fixElectronCors } from './cors'; import { loadBuffer, contentSaved } from './buffer'; +import { FileLibrary, setupFileLibraryEventHandlers } from './file-library'; // The built directory structure @@ -49,6 +50,7 @@ Menu.setApplicationMenu(menu) // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' export let win: BrowserWindow | null = null +let fileLibrary: FileLibrary | null = null let tray: Tray | null = null; let initErrors: string[] = [] // Here, you can also use other preload @@ -139,7 +141,7 @@ async function createWindow() { } // Prevent the window from closing, and send a message to the renderer which will in turn // send a message to the main process to save the current buffer and close the window. - if (!contentSaved) { + if (!!fileLibrary && !fileLibrary.contentSaved) { event.preventDefault() win?.webContents.send(WINDOW_CLOSE_EVENT) } else { @@ -308,6 +310,7 @@ function registerAlwaysOnTop() { } app.whenReady().then(createWindow).then(async () => { + setupFileLibraryEventHandlers(fileLibrary, win) initializeAutoUpdate(win) registerGlobalHotkey() registerShowInDock() @@ -349,8 +352,16 @@ ipcMain.handle('dark-mode:set', (event, mode) => { ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource) -// load buffer on app start -loadBuffer() +// Initialize note/file library +const customLibraryPath = CONFIG.get("settings.bufferPath") +const libraryPath = customLibraryPath ? customLibraryPath : join(app.getPath("userData"), "notes") +console.log("libraryPath", libraryPath) +try { + fileLibrary = new FileLibrary(libraryPath) +} catch (error) { + initErrors.push(`Error: ${error.message}`) +} + ipcMain.handle("getInitErrors", () => { return initErrors }) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 7d5f2427..45baa608 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,6 +1,6 @@ const { contextBridge } = require('electron') import themeMode from "./theme-mode" -import { isMac, isWindows, isLinux } from "../detect-platform" +import { isMac, isWindows, isLinux, isDev } from "../detect-platform" import { ipcRenderer } from "electron" import { WINDOW_CLOSE_EVENT, @@ -29,9 +29,20 @@ contextBridge.exposeInMainWorld("heynote", { isLinux, isWebApp: false, }, - + + isDev: isDev, themeMode: themeMode, + init() { + ipcRenderer.on("buffer:change", (event, path, content) => { + // called on all changes to open buffer files + // go through all registered callbacks for this path and call them + if (this.buffer._onChangeCallbacks[path]) { + this.buffer._onChangeCallbacks[path].forEach(callback => callback(content)) + } + }) + }, + quit() { console.log("quitting") //ipcRenderer.invoke("app_quit") @@ -46,25 +57,52 @@ contextBridge.exposeInMainWorld("heynote", { }, buffer: { - async load() { - return await ipcRenderer.invoke("buffer-content:load") + async exists(path) { + return await ipcRenderer.invoke("buffer:exists", path) + }, + + async getList() { + return await ipcRenderer.invoke("buffer:getList") + }, + + async load(path) { + return await ipcRenderer.invoke("buffer:load", path) }, - async save(content) { - return await ipcRenderer.invoke("buffer-content:save", content) + async save(path, content) { + return await ipcRenderer.invoke("buffer:save", path, content) }, - async saveAndQuit(content) { - return await ipcRenderer.invoke("buffer-content:saveAndQuit", content) + async saveAndQuit(contents) { + return await ipcRenderer.invoke("buffer:saveAndQuit", contents) }, - onChangeCallback(callback) { - ipcRenderer.on("buffer-content:change", callback) + async close(path) { + return await ipcRenderer.invoke("buffer:close", path) + }, + + _onChangeCallbacks: {}, + addOnChangeCallback(path, callback) { + // register a callback to be called when the buffer content changes for a specific file + if (!this._onChangeCallbacks[path]) { + this._onChangeCallbacks[path] = [] + } + this._onChangeCallbacks[path].push(callback) + }, + removeOnChangeCallback(path, callback) { + if (this._onChangeCallbacks[path]) { + this._onChangeCallbacks[path] = this._onChangeCallbacks[path].filter(cb => cb !== callback) + } }, async selectLocation() { return await ipcRenderer.invoke("buffer-content:selectLocation") - } + }, + + callbacks(callbacks) { + ipcRenderer.on("buffer:noteMetadataChanged", (event, path, info) => callbacks?.noteMetadataChanged(path, info)) + ipcRenderer.on("buffer:noteRemoved", (event, path) => callbacks?.noteRemoved(path)) + }, }, settings: CONFIG.get("settings"), diff --git a/public/langdetect-worker.js b/public/langdetect-worker.js index a519b4af..15d736f9 100644 --- a/public/langdetect-worker.js +++ b/public/langdetect-worker.js @@ -28,6 +28,7 @@ onmessage = (event) => { }, content: content, idx: event.data.idx, + path: event.data.path, }) return } @@ -53,6 +54,7 @@ onmessage = (event) => { }, content: content, idx: event.data.idx, + path: event.data.path, }) return } @@ -66,6 +68,7 @@ onmessage = (event) => { }, content: content, idx: event.data.idx, + path: event.data.path, }) return } diff --git a/src/components/App.vue b/src/components/App.vue index 08234d50..7e51dfef 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -1,8 +1,17 @@ + + + + diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue new file mode 100644 index 00000000..126b14b7 --- /dev/null +++ b/src/components/NoteSelector.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index 0676dd91..a6b4df16 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -1,17 +1,14 @@ + + + + diff --git a/src/editor/block/block.js b/src/editor/block/block.js index 57e2bb11..2c5341b0 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -4,8 +4,8 @@ import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet import { syntaxTree, ensureSyntaxTree, syntaxTreeAvailable } from "@codemirror/language" import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js" import { IterMode } from "@lezer/common"; +import { useNotesStore } from "../../stores/notes-store.js" import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js"; -import { SelectionChangeEvent } from "../event.js" import { mathBlock } from "./math.js" import { emptyBlockSelected } from "./select-all.js"; @@ -404,32 +404,33 @@ function getSelectionSize(state, sel) { return count } -const emitCursorChange = (editor) => ViewPlugin.fromClass( - class { - update(update) { - // if the selection changed or the language changed (can happen without selection change), - // emit a selection change event - const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE)) - if (update.selectionSet || langChange) { - const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head) - - const selectionSize = update.state.selection.ranges.map( - (sel) => getSelectionSize(update.state, sel) - ).reduce((a, b) => a + b, 0) - - const block = getActiveNoteBlock(update.state) - if (block && cursorLine) { - editor.element.dispatchEvent(new SelectionChangeEvent({ - cursorLine, - selectionSize, - language: block.language.name, - languageAuto: block.language.auto, - })) +const emitCursorChange = (editor) => { + const notesStore = useNotesStore() + return ViewPlugin.fromClass( + class { + update(update) { + // if the selection changed or the language changed (can happen without selection change), + // emit a selection change event + const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE)) + if (update.selectionSet || langChange) { + const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head) + + const selectionSize = update.state.selection.ranges.map( + (sel) => getSelectionSize(update.state, sel) + ).reduce((a, b) => a + b, 0) + + const block = getActiveNoteBlock(update.state) + if (block && cursorLine) { + notesStore.currentCursorLine = cursorLine + notesStore.currentSelectionSize = selectionSize + notesStore.currentLanguage = block.language.name + notesStore.currentLanguageAuto = block.language.auto + } } } } - } -) + ) +} export const noteBlockExtension = (editor) => { return [ diff --git a/src/editor/editor.js b/src/editor/editor.js index 8df85a96..484176d2 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -1,4 +1,4 @@ -import { Annotation, EditorState, Compartment, Facet, EditorSelection } from "@codemirror/state" +import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction } from "@codemirror/state" import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view" import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language" import { markdown } from "@codemirror/lang-markdown" @@ -22,8 +22,8 @@ import { autoSaveContent } from "./save.js" import { todoCheckboxPlugin} from "./todo-checkbox.ts" import { links } from "./links.js" import { NoteFormat } from "./note-format.js" +import { useNotesStore } from "../stores/notes-store.js"; -export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector" function getKeymapExtensions(editor, keymap) { if (keymap === "emacs") { @@ -37,10 +37,10 @@ function getKeymapExtensions(editor, keymap) { export class HeynoteEditor { constructor({ element, + path, content, focus=true, theme="light", - saveFunction=null, keymap="default", emacsMetaKey, showLineNumberGutter=true, @@ -48,8 +48,11 @@ export class HeynoteEditor { bracketClosing=false, fontFamily, fontSize, + defaultBlockToken, + defaultBlockAutoDetect, }) { this.element = element + this.path = path this.themeCompartment = new Compartment this.keymapCompartment = new Compartment this.lineNumberCompartmentPre = new Compartment @@ -60,9 +63,10 @@ export class HeynoteEditor { this.deselectOnCopy = keymap === "emacs" this.emacsMetaKey = emacsMetaKey this.fontTheme = new Compartment - this.defaultBlockToken = "text" - this.defaultBlockAutoDetect = true - this.saveFunction = saveFunction + this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect) + this.contentLoaded = false + this.notesStore = useNotesStore() + const state = EditorState.create({ doc: "", @@ -88,7 +92,7 @@ export class HeynoteEditor { }), heynoteLang(), noteBlockExtension(this), - languageDetection(() => this), + languageDetection(path, () => this), // set cursor blink rate to 1 second drawSelection({cursorBlinkRate:1000}), @@ -98,7 +102,7 @@ export class HeynoteEditor { return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"} }), - this.saveFunction ? autoSaveContent(this, 2000) : [], + autoSaveContent(this, 2000), todoCheckboxPlugin, markdown(), @@ -107,34 +111,66 @@ export class HeynoteEditor { }) // make sure saveFunction is called when page is unloaded - if (saveFunction) { - window.addEventListener("beforeunload", () => { - this.save() - }) - } + window.addEventListener("beforeunload", () => { + this.save() + }) this.view = new EditorView({ state: state, parent: element, }) - this.setContent(content) - + //this.setContent(content) + this.setReadOnly(true) + this.loadContent().then(() => { + this.setReadOnly(false) + }) + if (focus) { this.view.focus() } } - save() { - this.saveFunction(this.getContent()) + async save() { + if (!this.contentLoaded) { + return + } + const content = this.getContent() + if (content === this.diskContent) { + return + } + console.log("saving:", this.path) + this.diskContent = content + await window.heynote.buffer.save(this.path, content) } getContent() { this.note.content = this.view.state.sliceDoc() this.note.cursors = this.view.state.selection.toJSON() + + const ranges = this.note.cursors.ranges + if (ranges.length == 1 && ranges[0].anchor == 0 && ranges[0].head == 0) { + console.log("DEBUG!! Cursor is at 0,0") + console.trace() + } return this.note.serialize() } + async loadContent() { + console.log("loading content", this.path) + const content = await window.heynote.buffer.load(this.path) + this.diskContent = content + this.contentLoaded = true + this.setContent(content) + + // set up content change listener + this.onChange = (content) => { + this.diskContent = content + this.setContent(content) + } + window.heynote.buffer.addOnChangeCallback(this.path, this.onChange) + } + setContent(content) { try { this.note = NoteFormat.load(content) @@ -143,6 +179,7 @@ export class HeynoteEditor { this.setReadOnly(true) throw e } + this.notesStore.currentNoteName = this.note.metadata?.name || this.path return new Promise((resolve) => { // set buffer content this.view.dispatch({ @@ -151,7 +188,7 @@ export class HeynoteEditor { to: this.view.state.doc.length, insert: this.note.content, }, - annotations: [heynoteEvent.of(SET_CONTENT)], + annotations: [heynoteEvent.of(SET_CONTENT), Transaction.addToHistory.of(false)], }) // Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers @@ -217,7 +254,15 @@ export class HeynoteEditor { } openLanguageSelector() { - this.element.dispatchEvent(new Event(LANGUAGE_SELECTOR_EVENT)) + this.notesStore.openLanguageSelector() + } + + openNoteSelector() { + this.notesStore.openNoteSelector() + } + + openCreateNote() { + this.notesStore.openCreateNote() } setCurrentLanguage(lang, auto=false) { @@ -257,6 +302,15 @@ export class HeynoteEditor { currenciesLoaded() { triggerCurrenciesLoaded(this.view.state, this.view.dispatch) } + + destroy() { + if (this.onChange) { + window.heynote.buffer.removeOnChangeCallback(this.path, this.onChange) + } + this.save() + this.view.destroy() + window.heynote.buffer.close(this.path) + } } diff --git a/src/editor/event.js b/src/editor/event.js deleted file mode 100644 index 34f59601..00000000 --- a/src/editor/event.js +++ /dev/null @@ -1,9 +0,0 @@ -export class SelectionChangeEvent extends Event { - constructor({cursorLine, language, languageAuto, selectionSize}) { - super("selectionChange") - this.cursorLine = cursorLine - this.selectionSize = selectionSize - this.language = language - this.languageAuto = languageAuto - } -} diff --git a/src/editor/keymap.js b/src/editor/keymap.js index a7e32ed1..64b4cea8 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -57,6 +57,8 @@ export function heynoteKeymap(editor) { ["Alt-ArrowUp", moveLineUp], ["Alt-ArrowDown", moveLineDown], ["Mod-l", () => editor.openLanguageSelector()], + ["Mod-p", () => editor.openNoteSelector()], + ["Mod-s", () => editor.openCreateNote()], ["Alt-Shift-f", formatBlockContent], ["Mod-Alt-ArrowDown", newCursorBelow], ["Mod-Alt-ArrowUp", newCursorAbove], diff --git a/src/editor/language-detection/autodetect.js b/src/editor/language-detection/autodetect.js index 771170ea..4e08237f 100644 --- a/src/editor/language-detection/autodetect.js +++ b/src/editor/language-detection/autodetect.js @@ -1,5 +1,5 @@ import { EditorState } from "@codemirror/state"; -import { EditorView } from "@codemirror/view"; +import { EditorView, ViewPlugin } from "@codemirror/view"; import { redoDepth } from "@codemirror/commands"; import { getActiveNoteBlock, blockState } from "../block/block"; import { levenshtein_distance } from "./levenshtein"; @@ -25,95 +25,112 @@ function cancelIdleCallbackCompat(id) { } } -export function languageDetection(getEditor) { - const previousBlockContent = {} - let idleCallbackId = null - - const detectionWorker = new Worker('langdetect-worker.js?worker'); - detectionWorker.onmessage = (event) => { - //console.log("event:", event.data) - if (!event.data.guesslang.language) { - return - } - const editor = getEditor() - const view = editor.view - const state = view.state - const block = getActiveNoteBlock(state) - const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language] - if (block.language.auto === true && block.language.name !== newLang) { - console.log("New auto detected language:", newLang, "Confidence:", event.data.guesslang.confidence) - let content = state.doc.sliceString(block.content.from, block.content.to) - const threshold = content.length * 0.1 - if (levenshtein_distance(content, event.data.content) <= threshold) { - // the content has not changed significantly so it's safe to change the language - if (redoDepth(state) === 0) { - console.log("Changing language to", newLang) - changeLanguageTo(state, view.dispatch, block, newLang, true) - } else { - console.log("Not changing language because the user has undo:ed and has redo history") - } +// we'll use a shared global web worker for the language detection, for multiple Editor instances +const editorInstances = {} +const detectionWorker = new Worker('langdetect-worker.js?worker'); +detectionWorker.onmessage = (event) => { + //console.log("event:", event.data) + if (!event.data.guesslang.language) { + return + } + + const editor = editorInstances[event.data.path] + //const editor = getEditor() + const view = editor.view + const state = view.state + const block = getActiveNoteBlock(state) + const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language] + if (block.language.auto === true && block.language.name !== newLang) { + console.log("New auto detected language:", newLang, "Confidence:", event.data.guesslang.confidence) + let content = state.doc.sliceString(block.content.from, block.content.to) + const threshold = content.length * 0.1 + if (levenshtein_distance(content, event.data.content) <= threshold) { + // the content has not changed significantly so it's safe to change the language + if (redoDepth(state) === 0) { + console.log("Changing language to", newLang) + changeLanguageTo(state, view.dispatch, block, newLang, true) } else { - console.log("Content has changed significantly, not setting new language") + console.log("Not changing language because the user has undo:ed and has redo history") } + } else { + console.log("Content has changed significantly, not setting new language") } } +} - const plugin = EditorView.updateListener.of(update => { - if (update.docChanged) { - if (idleCallbackId !== null) { - cancelIdleCallbackCompat(idleCallbackId) - idleCallbackId = null - } +export function languageDetection(path, getEditor) { + const previousBlockContent = {} + let idleCallbackId = null + const editor = getEditor() + editorInstances[path] = editor - idleCallbackId = requestIdleCallbackCompat(() => { - idleCallbackId = null - - const range = update.state.selection.asSingle().ranges[0] - const blocks = update.state.facet(blockState) - let block = null, idx = null; - for (let i=0; i= range.from) { - block = blocks[i] - idx = i - break + //const plugin = EditorView.updateListener.of(update => { + const plugin = ViewPlugin.fromClass( + class { + update(update) { + if (update.docChanged) { + if (idleCallbackId !== null) { + cancelIdleCallbackCompat(idleCallbackId) + idleCallbackId = null } - } - if (block === null) { - return - } else if (block.language.auto === false) { - // if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection - // immediately if the user changes the language to auto - delete previousBlockContent[idx] - return - } - const content = update.state.doc.sliceString(block.content.from, block.content.to) - if (content === "" && redoDepth(update.state) === 0) { - // if content is cleared, set language to default - const editor = getEditor() - const view = editor.view - const block = getActiveNoteBlock(view.state) - if (block.language.name !== editor.defaultBlockToken) { - changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true) - } - delete previousBlockContent[idx] - } - if (content.length <= 8) { - return - } - const threshold = content.length * 0.1 - if (!previousBlockContent[idx] || levenshtein_distance(previousBlockContent[idx], content) >= threshold) { - // the content has changed significantly, so schedule a language detection - //console.log("Scheduling language detection for block", idx, "with threshold", threshold) - detectionWorker.postMessage({ - content: content, - idx: idx, + idleCallbackId = requestIdleCallbackCompat(() => { + idleCallbackId = null + + const range = update.state.selection.asSingle().ranges[0] + const blocks = update.state.facet(blockState) + let block = null, idx = null; + for (let i=0; i= range.from) { + block = blocks[i] + idx = i + break + } + } + if (block === null) { + return + } else if (block.language.auto === false) { + // if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection + // immediately if the user changes the language to auto + delete previousBlockContent[idx] + return + } + + const content = update.state.doc.sliceString(block.content.from, block.content.to) + if (content === "" && redoDepth(update.state) === 0) { + // if content is cleared, set language to default + //const editor = getEditor() + const view = editor.view + const block = getActiveNoteBlock(view.state) + if (block.language.name !== editor.defaultBlockToken) { + changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true) + } + delete previousBlockContent[idx] + } + if (content.length <= 8) { + return + } + const threshold = content.length * 0.1 + if (!previousBlockContent[idx] || levenshtein_distance(previousBlockContent[idx], content) >= threshold) { + // the content has changed significantly, so schedule a language detection + //console.log("Scheduling language detection for block", idx, "with threshold", threshold) + detectionWorker.postMessage({ + content: content, + idx: idx, + path: path, + }) + previousBlockContent[idx] = content + } }) - previousBlockContent[idx] = content } - }) + } + + destroy() { + console.log("Removing editorInstance for:", path) + delete editorInstances[path] + } } - }) + ) return plugin } diff --git a/src/editor/save.js b/src/editor/save.js index 415a8eec..4763f6f3 100644 --- a/src/editor/save.js +++ b/src/editor/save.js @@ -1,5 +1,6 @@ import { ViewPlugin } from "@codemirror/view" import { debounce } from "debounce" +import { SET_CONTENT }  from "./annotation" export const autoSaveContent = (editor, interval) => { @@ -12,9 +13,12 @@ export const autoSaveContent = (editor, interval) => { class { update(update) { if (update.docChanged) { - save() + const initialSetContent = update.transactions.flatMap(t => t.annotations).some(a => a.value === SET_CONTENT) + if (!initialSetContent) { + save() + } } } } ) -} \ No newline at end of file +} diff --git a/src/main.js b/src/main.js index 456d02ec..9994a49b 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import { createPinia } from 'pinia' import App from './components/App.vue' import { loadCurrencies } from './currency' import { useErrorStore } from './stores/error-store' +import { useNotesStore, initNotesStore } from './stores/notes-store' const pinia = createPinia() @@ -18,10 +19,12 @@ app.mount('#app').$nextTick(() => { }) const errorStore = useErrorStore() +//errorStore.addError("test error") window.heynote.getInitErrors().then((errors) => { errors.forEach((e) => errorStore.addError(e)) }) +initNotesStore() @@ -29,3 +32,4 @@ window.heynote.getInitErrors().then((errors) => { loadCurrencies() setInterval(loadCurrencies, 1000 * 3600 * 4) +window.heynote.init() diff --git a/src/stores/notes-store.js b/src/stores/notes-store.js new file mode 100644 index 00000000..7f4bbb85 --- /dev/null +++ b/src/stores/notes-store.js @@ -0,0 +1,65 @@ +import { defineStore } from "pinia" + +export const useNotesStore = defineStore("notes", { + state: () => ({ + notes: {}, + currentNotePath: window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt", + currentNoteName: null, + currentLanguage: null, + currentLanguageAuto: null, + currentCursorLine: null, + currentSelectionSize: null, + + showNoteSelector: false, + showLanguageSelector: false, + showCreateNote: false, + }), + + actions: { + async updateNotes() { + this.setNotes(await window.heynote.buffer.getList()) + }, + + setNotes(notes) { + this.notes = notes + }, + + createNewNote(path, content) { + //window.heynote.buffer.save(path, content) + this.updateNotes() + }, + + openNote(path) { + this.showNoteSelector = false + this.showLanguageSelector = false + this.showCreateNote = false + this.currentNotePath = path + }, + + openLanguageSelector() { + this.showLanguageSelector = true + this.showNoteSelector = false + this.showCreateNote = false + }, + openNoteSelector() { + this.showNoteSelector = true + this.showLanguageSelector = false + this.showCreateNote = false + }, + openCreateNote() { + this.showCreateNote = true + this.showNoteSelector = false + this.showLanguageSelector = false + }, + closeDialog() { + this.showCreateNote = false + this.showNoteSelector = false + this.showLanguageSelector = false + }, + }, +}) + +export async function initNotesStore() { + const notesStore = useNotesStore() + await notesStore.updateNotes() +} diff --git a/webapp/bridge.js b/webapp/bridge.js index 57d9098d..107a9550 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -80,20 +80,38 @@ const Heynote = { defaultFontSize: isMobileDevice ? 16 : 12, buffer: { - async load() { - const content = localStorage.getItem("buffer") + async load(path) { + const content = localStorage.getItem(path) return content === null ? "\n∞∞∞text-a\n" : content }, - async save(content) { - localStorage.setItem("buffer", content) + async save(path, content) { + console.log("saving", path, content) + localStorage.setItem(path, content) }, - async saveAndQuit(content) { + async saveAndQuit(contents) { }, - onChangeCallback(callback) { + + async exists(path) { + return true + }, + + async getList(path) { + return [{"path":"buffer.txt", "metadata":{}}] + }, + + async close(path) { + + }, + + _onChangeCallbacks: {}, + addOnChangeCallback(path, callback) { + + }, + removeOnChangeCallback(path, callback) { }, }, From 76b116567c85e9683722f744a7293c8fac644db6 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 24 Jul 2024 14:30:14 +0200 Subject: [PATCH 10/65] Place scrollbar for Language selector and Note selector within the list instead of scrolling the whole "screen" --- src/components/LanguageSelector.vue | 17 +++++++++++------ src/components/NewNote.vue | 5 ++--- src/components/NoteSelector.vue | 17 +++++++++++------ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/components/LanguageSelector.vue b/src/components/LanguageSelector.vue index ac91760a..d0ca3d10 100644 --- a/src/components/LanguageSelector.vue +++ b/src/components/LanguageSelector.vue @@ -106,12 +106,12 @@ +./folder-selector/FolderSelector.vue \ No newline at end of file diff --git a/src/components/folder-selector/FolderItem.vue b/src/components/folder-selector/FolderItem.vue new file mode 100644 index 00000000..f321095d --- /dev/null +++ b/src/components/folder-selector/FolderItem.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/components/folder-selector/FolderSelector.vue b/src/components/folder-selector/FolderSelector.vue new file mode 100644 index 00000000..f1e4dbe5 --- /dev/null +++ b/src/components/folder-selector/FolderSelector.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/src/components/folder-selector/NewFolderItem.vue b/src/components/folder-selector/NewFolderItem.vue new file mode 100644 index 00000000..5cafd14f --- /dev/null +++ b/src/components/folder-selector/NewFolderItem.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/components/folder-selector/sanitize-filename.js b/src/components/folder-selector/sanitize-filename.js new file mode 100644 index 00000000..7693e7c3 --- /dev/null +++ b/src/components/folder-selector/sanitize-filename.js @@ -0,0 +1,14 @@ +const illegalRe = /[\/\?<>\\:\*\|"]/g; +const controlRe = /[\x00-\x1f\x80-\x9f]/g; +const reservedRe = /^\.+$/; +const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; +const windowsTrailingRe = /[\. ]+$/; + +export default function sanitizeFilename(input, replacement) { + return input.trim() + .replace(illegalRe, replacement) + .replace(controlRe, replacement) + .replace(reservedRe, replacement) + .replace(windowsReservedRe, replacement) + .replace(windowsTrailingRe, replacement) +} diff --git a/src/components/form/FolderSelect.vue b/src/components/form/FolderSelect.vue deleted file mode 100644 index e3d2881b..00000000 --- a/src/components/form/FolderSelect.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - - diff --git a/webapp/bridge.js b/webapp/bridge.js index 107a9550..2cf646f9 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -103,6 +103,10 @@ const Heynote = { return [{"path":"buffer.txt", "metadata":{}}] }, + async getDirectoryList() { + return [] + }, + async close(path) { }, From 0a18360fc81e7c303176e20ca1b57b39d044dd94 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 26 Jul 2024 11:30:25 +0200 Subject: [PATCH 14/65] WIP: Implement ability to create new notes. Support cache of multiple Editor instances. Change so that current note name is included in the event data dispatched by emitCursorChange. --- electron/main/file-library.js | 12 +++++ electron/preload/index.ts | 4 ++ package-lock.json | 52 ++++++++++++++++++++ package.json | 1 + src/components/Editor.vue | 85 ++++++++++++++++++++++++--------- src/components/NewNote.vue | 49 ++++++++++++++++--- src/components/NoteSelector.vue | 24 +++++++++- src/editor/annotation.js | 2 + src/editor/block/block.js | 18 +++++-- src/editor/block/commands.js | 26 ++++++++-- src/editor/editor.js | 39 +++++++++++++-- src/editor/keymap.js | 2 + src/stores/notes-store.js | 28 +++++++++-- webapp/bridge.js | 6 ++- 14 files changed, 298 insertions(+), 50 deletions(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index 4a8dd943..b00edc9d 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -64,6 +64,14 @@ export class FileLibrary { return await this.files[path].save(content) } + async create(path, content) { + if (await this.exists(path)) { + throw new Error(`File already exists: ${path}`) + } + const fullPath = join(this.basePath, path) + await this.jetpack.writeAsync(fullPath, content) + } + async getList() { console.log("Loading notes") const notes = {} @@ -194,6 +202,10 @@ export function setupFileLibraryEventHandlers(library, win) { return await library.save(path, content) }); + ipcMain.handle('buffer:create', async (event, path, content) => { + return await library.create(path, content) + }); + ipcMain.handle('buffer:getList', async (event) => { return await library.getList() }); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 5bd74c3f..9d5bd8b2 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -77,6 +77,10 @@ contextBridge.exposeInMainWorld("heynote", { return await ipcRenderer.invoke("buffer:save", path, content) }, + async create(path, content) { + return await ipcRenderer.invoke("buffer:create", path, content) + }, + async saveAndQuit(contents) { return await ipcRenderer.invoke("buffer:saveAndQuit", contents) }, diff --git a/package-lock.json b/package-lock.json index 34812c29..030fdab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.8.0", "license": "Commons Clause MIT", "dependencies": { + "@sindresorhus/slugify": "^2.2.1", "electron-log": "^5.0.1", "pinia": "^2.1.7", "semver": "^7.6.3" @@ -1517,6 +1518,57 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", diff --git a/package.json b/package.json index 1688cc12..f75dd775 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "vue-tsc": "^1.0.16" }, "dependencies": { + "@sindresorhus/slugify": "^2.2.1", "electron-log": "^5.0.1", "pinia": "^2.1.7", "semver": "^7.6.3" diff --git a/src/components/Editor.vue b/src/components/Editor.vue index b81c6aa3..e384b439 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -2,9 +2,12 @@ import { HeynoteEditor } from '../editor/editor.js' import { syntaxTree } from "@codemirror/language" import { toRaw } from 'vue'; - import { mapState } from 'pinia' + import { mapState, mapWritableState, mapActions } from 'pinia' + import { useErrorStore } from "../stores/error-store" import { useNotesStore } from "../stores/notes-store" + const NUM_EDITOR_INSTANCES = 5 + export default { props: { theme: String, @@ -41,8 +44,11 @@ data() { return { syntaxTreeDebugContent: null, - bufferFilePath: null, editor: null, + editorCache: { + lru: [], + cache: {} + }, } }, @@ -130,34 +136,67 @@ ...mapState(useNotesStore, [ "currentNotePath", ]), + ...mapWritableState(useNotesStore, [ + "currentEditor", + "currentNoteName", + ]), }, methods: { + ...mapActions(useErrorStore, ["addError"]), + loadBuffer(path) { if (this.editor) { - this.editor.destroy() + this.editor.hide() } - // load buffer content and create editor - this.bufferFilePath = path - try { - this.editor = new HeynoteEditor({ - element: this.$refs.editor, - path: this.bufferFilePath, - theme: this.theme, - keymap: this.keymap, - emacsMetaKey: this.emacsMetaKey, - showLineNumberGutter: this.showLineNumberGutter, - showFoldGutter: this.showFoldGutter, - bracketClosing: this.bracketClosing, - fontFamily: this.fontFamily, - fontSize: this.fontSize, - defaultBlockToken: this.defaultBlockLanguage, - defaultBlockAutoDetect: this.defaultBlockLanguageAutoDetect, - }) + + if (this.editorCache.cache[path]) { + // editor is already loaded, just switch to it + console.log("Switching to cached editor", path) + toRaw(this.editor).hide() + this.editor = this.editorCache.cache[path] + toRaw(this.editor).show() + //toRaw(this.editor).currenciesLoaded() + this.currentEditor = toRaw(this.editor) window._heynote_editor = toRaw(this.editor) - } catch (e) { - alert("Error! " + e.message) - throw e + // move to end of LRU + this.editorCache.lru = this.editorCache.lru.filter(p => p !== path) + this.editorCache.lru.push(path) + } else { + // check if we need to free up a slot + if (this.editorCache.lru.length >= NUM_EDITOR_INSTANCES) { + const pathToFree = this.editorCache.lru.shift() + console.log("Freeing up editor slot", pathToFree) + this.editorCache.cache[pathToFree].destroy() + delete this.editorCache.cache[pathToFree] + this.editorCache.lru = this.editorCache.lru.filter(p => p !== pathToFree) + } + + // create new Editor instance + console.log("Loading new editor", path) + try { + this.editor = new HeynoteEditor({ + element: this.$refs.editor, + path: path, + theme: this.theme, + keymap: this.keymap, + emacsMetaKey: this.emacsMetaKey, + showLineNumberGutter: this.showLineNumberGutter, + showFoldGutter: this.showFoldGutter, + bracketClosing: this.bracketClosing, + fontFamily: this.fontFamily, + fontSize: this.fontSize, + defaultBlockToken: this.defaultBlockLanguage, + defaultBlockAutoDetect: this.defaultBlockLanguageAutoDetect, + }) + this.currentEditor = toRaw(this.editor) + window._heynote_editor = toRaw(this.editor) + this.editorCache.cache[path] = this.editor + this.editorCache.lru.push(path) + } catch (e) { + this.addError("Error! " + e.message) + throw e + } } }, diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 41229707..c3485544 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -1,4 +1,6 @@ @@ -100,12 +109,12 @@
  • {{ item.name }} - {{ item.path }} + {{ item.folder }}
  • @@ -128,6 +137,7 @@ position: absolute top: 0 left: 50% + width: 420px transform: translateX(-50%) max-height: 100% box-sizing: border-box @@ -176,6 +186,8 @@ &.selected background: #48b57e color: #fff + &.scratch + font-weight: 600 +dark-mode color: rgba(255,255,255, 0.53) &:hover @@ -185,7 +197,15 @@ color: rgba(255,255,255, 0.87) .name margin-right: 12px + flex-shrink: 0 + overflow: hidden + text-overflow: ellipsis + text-wrap: nowrap .path opacity: 0.6 font-size: 12px + flex-shrink: 1 + overflow: hidden + text-overflow: ellipsis + text-wrap: nowrap diff --git a/src/editor/annotation.js b/src/editor/annotation.js index 6b4e83cb..fae768ef 100644 --- a/src/editor/annotation.js +++ b/src/editor/annotation.js @@ -5,3 +5,5 @@ export const LANGUAGE_CHANGE = "heynote-change" export const CURRENCIES_LOADED = "heynote-currencies-loaded" export const SET_CONTENT = "heynote-set-content" export const ADD_NEW_BLOCK = "heynote-add-new-block" +export const DELETE_BLOCK = "heynote-delete-block" +export const CURSOR_CHANGE = "heynote-cursor-change" diff --git a/src/editor/block/block.js b/src/editor/block/block.js index 2c5341b0..0f3141cf 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -1,11 +1,11 @@ import { ViewPlugin, EditorView, Decoration, WidgetType, lineNumbers } from "@codemirror/view" import { layer, RectangleMarker } from "@codemirror/view" -import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet} from "@codemirror/state"; +import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet, Transaction} from "@codemirror/state"; import { syntaxTree, ensureSyntaxTree, syntaxTreeAvailable } from "@codemirror/language" import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js" import { IterMode } from "@lezer/common"; import { useNotesStore } from "../../stores/notes-store.js" -import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js"; +import { heynoteEvent, LANGUAGE_CHANGE, CURSOR_CHANGE } from "../annotation.js"; import { mathBlock } from "./math.js" import { emptyBlockSelected } from "./select-all.js"; @@ -404,6 +404,15 @@ function getSelectionSize(state, sel) { return count } +export function triggerCursorChange({state, dispatch}) { + // Trigger empty change transaction that is annotated with CURRENCIES_LOADED + // This will make Math blocks re-render so that currency conversions are applied + dispatch(state.update({ + changes:{from: 0, to: 0, insert:""}, + annotations: [heynoteEvent.of(CURSOR_CHANGE), Transaction.addToHistory.of(false)], + })) +} + const emitCursorChange = (editor) => { const notesStore = useNotesStore() return ViewPlugin.fromClass( @@ -411,8 +420,8 @@ const emitCursorChange = (editor) => { update(update) { // if the selection changed or the language changed (can happen without selection change), // emit a selection change event - const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE)) - if (update.selectionSet || langChange) { + const shouldUpdate = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE || a.value == CURSOR_CHANGE)) + if (update.selectionSet || shouldUpdate) { const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head) const selectionSize = update.state.selection.ranges.map( @@ -425,6 +434,7 @@ const emitCursorChange = (editor) => { notesStore.currentSelectionSize = selectionSize notesStore.currentLanguage = block.language.name notesStore.currentLanguageAuto = block.language.auto + notesStore.currentNoteName = editor.name } } } diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js index b55c50bf..42192c34 100644 --- a/src/editor/block/commands.js +++ b/src/editor/block/commands.js @@ -1,5 +1,5 @@ -import { EditorSelection } from "@codemirror/state" -import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK } from "../annotation.js"; +import { EditorSelection, Transaction } from "@codemirror/state" +import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, DELETE_BLOCK } from "../annotation.js"; import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block" import { moveLineDown, moveLineUp } from "./move-lines.js"; import { selectAll } from "./select-all.js"; @@ -7,7 +7,7 @@ import { selectAll } from "./select-all.js"; export { moveLineDown, moveLineUp, selectAll } -function getBlockDelimiter(defaultToken, autoDetect) { +export function getBlockDelimiter(defaultToken, autoDetect) { return `\n∞∞∞${autoDetect ? defaultToken + '-a' : defaultToken}\n` } @@ -317,6 +317,24 @@ export function triggerCurrenciesLoaded(state, dispatch) { // This will make Math blocks re-render so that currency conversions are applied dispatch(state.update({ changes:{from: 0, to: 0, insert:""}, - annotations: [heynoteEvent.of(CURRENCIES_LOADED)], + annotations: [heynoteEvent.of(CURRENCIES_LOADED), Transaction.addToHistory.of(false)], + })) +} + +export const deleteBlock = (editor) => ({state, dispatch}) => { + const block = getActiveNoteBlock(state) + const blocks = state.facet(blockState) + let replace = "" + if (blocks.length == 1) { + replace = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect) + } + dispatch(state.update({ + changes: { + from: block.range.from, + to: block.range.to, + insert: replace, + }, + selection: EditorSelection.cursor(block.delimiter.from), + annotations: [heynoteEvent.of(DELETE_BLOCK)], })) } diff --git a/src/editor/editor.js b/src/editor/editor.js index 8bfc2aaa..80801b98 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -10,9 +10,9 @@ import { heynoteBase } from "./theme/base.js" import { getFontTheme } from "./theme/font-theme.js"; import { customSetup } from "./setup.js" import { heynoteLang } from "./lang-heynote/heynote.js" -import { noteBlockExtension, blockLineNumbers, blockState } from "./block/block.js" -import { heynoteEvent, SET_CONTENT } from "./annotation.js"; -import { changeCurrentBlockLanguage, triggerCurrenciesLoaded } from "./block/commands.js" +import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, triggerCursorChange } from "./block/block.js" +import { heynoteEvent, SET_CONTENT, DELETE_BLOCK } from "./annotation.js"; +import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock } from "./block/commands.js" import { formatBlockContent } from "./block/format-code.js" import { heynoteKeymap } from "./keymap.js" import { emacsKeymap } from "./emacs.js" @@ -66,6 +66,7 @@ export class HeynoteEditor { this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect) this.contentLoaded = false this.notesStore = useNotesStore() + this.name = "" const state = EditorState.create({ @@ -179,7 +180,8 @@ export class HeynoteEditor { this.setReadOnly(true) throw new Error(`Failed to load note: ${e.message}`) } - this.notesStore.currentNoteName = this.note.metadata?.name || this.path + this.name = this.note.metadata?.name || this.path + return new Promise((resolve) => { // set buffer content this.view.dispatch({ @@ -262,7 +264,24 @@ export class HeynoteEditor { } openCreateNote() { - this.notesStore.openCreateNote() + this.notesStore.openCreateNote(this) + } + + async createNewNoteFromActiveBlock(path, name) { + const block = getActiveNoteBlock(this.view.state) + if (!block) { + return + } + const data = this.view.state.sliceDoc(block.range.from, block.range.to) + await this.notesStore.saveNewNote(path, name, data) + deleteBlock(this)(this.view) + + // by using requestAnimationFrame we avoid a race condition where rendering the block backgrounds + // would fail if we immediately opened the new note (since the block UI wouldn't have time to update + // after the block was deleted) + requestAnimationFrame(() => { + this.notesStore.openNote(path) + }) } setCurrentLanguage(lang, auto=false) { @@ -311,6 +330,16 @@ export class HeynoteEditor { this.view.destroy() window.heynote.buffer.close(this.path) } + + hide() { + console.log("hiding element", this.view.dom) + this.view.dom.style.setProperty("display", "none", "important") + } + show() { + console.log("showing element", this.view.dom) + this.view.dom.style.setProperty("display", "") + triggerCursorChange(this.view) + } } diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 64b4cea8..9b077b13 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -15,6 +15,7 @@ import { gotoPreviousParagraph, gotoNextParagraph, selectNextParagraph, selectPreviousParagraph, newCursorBelow, newCursorAbove, + deleteBlock, } from "./block/commands.js" import { pasteCommand, copyCommand, cutCommand } from "./copy-paste.js" @@ -59,6 +60,7 @@ export function heynoteKeymap(editor) { ["Mod-l", () => editor.openLanguageSelector()], ["Mod-p", () => editor.openNoteSelector()], ["Mod-s", () => editor.openCreateNote()], + ["Mod-Shift-d", deleteBlock(editor)], ["Alt-Shift-f", formatBlockContent], ["Mod-Alt-ArrowDown", newCursorBelow], ["Mod-Alt-ArrowUp", newCursorAbove], diff --git a/src/stores/notes-store.js b/src/stores/notes-store.js index 7f4bbb85..c8cb6622 100644 --- a/src/stores/notes-store.js +++ b/src/stores/notes-store.js @@ -1,8 +1,11 @@ +import { toRaw } from 'vue'; import { defineStore } from "pinia" +import { NoteFormat } from "../editor/note-format" export const useNotesStore = defineStore("notes", { state: () => ({ notes: {}, + currentEditor: null, currentNotePath: window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt", currentNoteName: null, currentLanguage: null, @@ -24,11 +27,6 @@ export const useNotesStore = defineStore("notes", { this.notes = notes }, - createNewNote(path, content) { - //window.heynote.buffer.save(path, content) - this.updateNotes() - }, - openNote(path) { this.showNoteSelector = false this.showLanguageSelector = false @@ -56,6 +54,26 @@ export const useNotesStore = defineStore("notes", { this.showNoteSelector = false this.showLanguageSelector = false }, + + async createNewNoteFromActiveBlock(path, name) { + await toRaw(this.currentEditor).createNewNoteFromActiveBlock(path, name) + }, + + async saveNewNote(path, name, content) { + //window.heynote.buffer.save(path, content) + //this.updateNotes() + + if (this.notes[path]) { + throw new Error(`Note already exists: ${path}`) + } + + const note = new NoteFormat() + note.content = content + note.metadata.name = name + console.log("saving", path, note.serialize()) + await window.heynote.buffer.create(path, note.serialize()) + this.updateNotes() + }, }, }) diff --git a/webapp/bridge.js b/webapp/bridge.js index 2cf646f9..00db2e93 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -1,3 +1,4 @@ +import { Exception } from "sass"; import { SETTINGS_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from "../electron/constants"; const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)') @@ -90,11 +91,14 @@ const Heynote = { localStorage.setItem(path, content) }, + async create(path, content) { + throw Exception("Not implemented") + }, + async saveAndQuit(contents) { }, - async exists(path) { return true }, From 2dcd25747db1fe49940651b5cd5d92054e098cd5 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sat, 27 Jul 2024 10:26:51 +0200 Subject: [PATCH 15/65] Sort notes in notes selector by how recent they were opened --- src/components/NoteSelector.vue | 32 +++++++++++++++++++++++++++++++- src/stores/notes-store.js | 10 +++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index 22fa7597..78f69a93 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -1,5 +1,6 @@ @@ -201,7 +217,8 @@ :level="item.level" :selected="idx === selected && !item.createNewFolder" :newFolder="item.newFolder" - @click="selected = idx" + :open="item.open" + @click="folderClick(idx)" @new-folder="newFolderDialog(item.path)" /> Date: Sun, 4 Aug 2024 17:36:47 +0200 Subject: [PATCH 20/65] Dark mode styling of New Note dialog --- src/components/NewNote.vue | 7 ++++--- src/components/folder-selector/FolderItem.vue | 16 ++++++++++++++++ .../folder-selector/FolderSelector.vue | 3 +++ src/components/folder-selector/NewFolderItem.vue | 6 ++++-- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 5058c1cf..7da8c343 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -192,7 +192,7 @@ &:focus outline: none +dark-mode - background: #333 + background: #151516 box-shadow: 0 0 10px rgba(0,0,0,0.5) color: rgba(255,255,255, 0.7) +webapp-mobile @@ -247,8 +247,6 @@ padding-top: 0 display: flex justify-content: flex-end - +dark-mode - background: #222 button font-size: 12px height: 28px @@ -258,5 +256,8 @@ padding-right: 10px &:focus outline-color: #48b57e + +dark-mode + background: #444 + border: none diff --git a/src/components/folder-selector/FolderItem.vue b/src/components/folder-selector/FolderItem.vue index 7c9c9750..8eb7d828 100644 --- a/src/components/folder-selector/FolderItem.vue +++ b/src/components/folder-selector/FolderItem.vue @@ -70,10 +70,17 @@ background-repeat: no-repeat background-position-y: 5px background-position-x: calc(2px + var(--indent-level) * 16px) + +dark-mode + background-image: url('@/assets/icons/caret-right-white.svg') + color: rgba(255,255,255, 0.87) &:hover background-color: #f1f1f1 + +dark-mode + background-color: #39393a &.open background-image: url('@/assets/icons/caret-down.svg') + +dark-mode + background-image: url('@/assets/icons/caret-down-white.svg') &.selected background-color: #48b57e color: #fff @@ -82,6 +89,11 @@ background-image: url('@/assets/icons/caret-down-white.svg') &:hover background-color: #40a773 + +dark-mode + background-color: #1b6540 + color: rgba(255,255,255, 0.87) + &:hover + background-color: #1f6f47 .new-folder display: block color: rgba(255,255,255, 0.9) @@ -90,6 +102,10 @@ color: rgba(0,0,0, 0.5) &.selected color: rgba(255,255,255, 0.8) + +dark-mode + color: rgba(255,255,255, 0.5) + &.selected + color: rgba(255,255,255, 0.8) .name diff --git a/src/components/folder-selector/FolderSelector.vue b/src/components/folder-selector/FolderSelector.vue index ea821a57..30417e31 100644 --- a/src/components/folder-selector/FolderSelector.vue +++ b/src/components/folder-selector/FolderSelector.vue @@ -245,4 +245,7 @@ outline: none border: 1px solid #fff outline: 2px solid #48b57e + +dark-mode + background: #262626 + border: 1px solid #363636 diff --git a/src/components/folder-selector/NewFolderItem.vue b/src/components/folder-selector/NewFolderItem.vue index 5cafd14f..06e28403 100644 --- a/src/components/folder-selector/NewFolderItem.vue +++ b/src/components/folder-selector/NewFolderItem.vue @@ -86,8 +86,8 @@ padding-left: calc(0px + var(--indent-level) * 16px) display: flex background: #f1f1f1 - &:hover - background: #f1f1f1 + +dark-mode + background-color: #39393a input @@ -104,5 +104,7 @@ outline: none &::placeholder font-size: 12px + +dark-mode + background: #3b3b3b From 93ac05d448bbb4d4d3e76f36d3a11f72d781ac7e Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 4 Aug 2024 17:42:53 +0200 Subject: [PATCH 21/65] Propagate settings changes to all cached Editor instances (and not just the active one) --- src/components/Editor.vue | 44 ++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/components/Editor.vue b/src/components/Editor.vue index e384b439..68a9ce58 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -95,40 +95,60 @@ }, theme(newTheme) { - toRaw(this.editor).setTheme(newTheme) + this.eachEditor(editor => { + editor.setTheme(newTheme) + }) }, keymap() { - toRaw(this.editor).setKeymap(this.keymap, this.emacsMetaKey) + this.eachEditor(editor => { + editor.setKeymap(this.keymap, this.emacsMetaKey) + }) }, emacsMetaKey() { - toRaw(this.editor).setKeymap(this.keymap, this.emacsMetaKey) + this.eachEditor(editor => { + editor.setKeymap(this.keymap, this.emacsMetaKey) + }) }, showLineNumberGutter(show) { - toRaw(this.editor).setLineNumberGutter(show) + this.eachEditor(editor => { + editor.setLineNumberGutter(show) + }) }, showFoldGutter(show) { - toRaw(this.editor).setFoldGutter(show) + this.eachEditor(editor => { + editor.setFoldGutter(show) + }) }, bracketClosing(value) { - toRaw(this.editor).setBracketClosing(value) + this.eachEditor(editor => { + editor.setBracketClosing(value) + }) }, fontFamily() { - toRaw(this.editor).setFont(this.fontFamily, this.fontSize) + this.eachEditor(editor => { + editor.setFont(this.fontFamily, this.fontSize) + }) }, fontSize() { - toRaw(this.editor).setFont(this.fontFamily, this.fontSize) + this.eachEditor(editor => { + editor.setFont(this.fontFamily, this.fontSize) + }) }, defaultBlockLanguage() { - toRaw(this.editor).setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + this.eachEditor(editor => { + editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + }) }, defaultBlockLanguageAutoDetect() { - toRaw(this.editor).setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + this.eachEditor(editor => { + editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + }) }, }, @@ -225,6 +245,10 @@ focus() { toRaw(this.editor).focus() }, + + eachEditor(fn) { + Object.values(toRaw(this.editorCache).cache).forEach(fn) + }, }, } From c635812747788380df0e26bf466852a4933087fd Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 4 Aug 2024 17:53:31 +0200 Subject: [PATCH 22/65] Minor color tweak in dark mode --- src/components/LanguageSelector.vue | 2 +- src/components/NewNote.vue | 1 + src/components/NoteSelector.vue | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/LanguageSelector.vue b/src/components/LanguageSelector.vue index f39fd877..b579ef2a 100644 --- a/src/components/LanguageSelector.vue +++ b/src/components/LanguageSelector.vue @@ -180,7 +180,7 @@ background: #48b57e color: #fff +dark-mode - color: rgba(255,255,255, 0.53) + color: rgba(255,255,255, 0.65) &:hover background: #29292a &.selected diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 7da8c343..7cf8f20e 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -259,5 +259,6 @@ +dark-mode background: #444 border: none + color: rgba(255,255,255, 0.75) diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index 00af47db..c667a6e4 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -241,7 +241,7 @@ &.scratch font-weight: 600 +dark-mode - color: rgba(255,255,255, 0.53) + color: rgba(255,255,255, 0.65) &:hover background: #29292a &.selected From a5440b4f3656206d5bbf5d20cd497044a5699e60 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 4 Aug 2024 17:58:20 +0200 Subject: [PATCH 23/65] Don't close/close folder when New folder button is clicked --- src/components/folder-selector/FolderItem.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/folder-selector/FolderItem.vue b/src/components/folder-selector/FolderItem.vue index 8eb7d828..860d709b 100644 --- a/src/components/folder-selector/FolderItem.vue +++ b/src/components/folder-selector/FolderItem.vue @@ -53,7 +53,7 @@ :style="style" > {{ name }} - + From fcec9ece3b55043f248e54913f657db52bf57851 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Sep 2024 15:22:06 +0200 Subject: [PATCH 24/65] Implement support for editing notes' metadata, and ability to move notes into other directories. Create separate pinia store for the editor cache functionality. --- assets/icons/arrow-right.svg | 1 + electron/main/file-library.js | 13 ++ electron/preload/index.ts | 4 + src/components/App.vue | 13 +- src/components/EditNote.vue | 274 ++++++++++++++++++++++++++++++++ src/components/Editor.vue | 46 ++---- src/components/NoteSelector.vue | 100 +++++++++++- src/editor/editor.js | 6 + src/main.js | 2 + src/stores/editor-cache.js | 48 ++++++ src/stores/notes-store.js | 57 +++++-- 11 files changed, 512 insertions(+), 52 deletions(-) create mode 100644 assets/icons/arrow-right.svg create mode 100644 src/components/EditNote.vue create mode 100644 src/stores/editor-cache.js diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 00000000..b3110a43 --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron/main/file-library.js b/electron/main/file-library.js index b00edc9d..bc06a186 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -72,6 +72,15 @@ export class FileLibrary { await this.jetpack.writeAsync(fullPath, content) } + async move(path, newPath) { + if (await this.exists(newPath)) { + throw new Error(`File already exists: ${newPath}`) + } + const fullOldPath = join(this.basePath, path) + const fullNewPath = join(this.basePath, newPath) + await this.jetpack.moveAsync(fullOldPath, fullNewPath) + } + async getList() { console.log("Loading notes") const notes = {} @@ -231,5 +240,9 @@ export function setupFileLibraryEventHandlers(library, win) { app.quit() }) + ipcMain.handle('buffer:move', async (event, path, newPath) => { + return await library.move(path, newPath) + }); + library.setupWatcher(win) } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 9d5bd8b2..bb1e5536 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -77,6 +77,10 @@ contextBridge.exposeInMainWorld("heynote", { return await ipcRenderer.invoke("buffer:save", path, content) }, + async move(path, newPath) { + return await ipcRenderer.invoke("buffer:move", path, newPath) + }, + async create(path, content) { return await ipcRenderer.invoke("buffer:create", path, content) }, diff --git a/src/components/App.vue b/src/components/App.vue index 7e51dfef..982b80ae 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -12,6 +12,7 @@ import Settings from './settings/Settings.vue' import ErrorMessages from './ErrorMessages.vue' import NewNote from './NewNote.vue' + import EditNote from './EditNote.vue' export default { components: { @@ -22,6 +23,7 @@ NoteSelector, ErrorMessages, NewNote, + EditNote, }, data() { @@ -67,6 +69,7 @@ showLanguageSelector(value) { this.dialogWatcher(value) }, showNoteSelector(value) { this.dialogWatcher(value) }, showCreateNote(value) { this.dialogWatcher(value) }, + showEditNote(value) { this.dialogWatcher(value) }, currentNotePath() { this.focusEditor() @@ -79,10 +82,11 @@ "showLanguageSelector", "showNoteSelector", "showCreateNote", + "showEditNote", ]), editorInert() { - return this.showCreateNote || this.showSettings + return this.showCreateNote || this.showSettings || this.showEditNote }, }, @@ -92,6 +96,7 @@ "openNoteSelector", "openCreateNote", "closeDialog", + "closeNoteSelector", "openNote", ]), @@ -186,7 +191,7 @@ + diff --git a/src/components/EditNote.vue b/src/components/EditNote.vue new file mode 100644 index 00000000..cfbd5d88 --- /dev/null +++ b/src/components/EditNote.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 68a9ce58..93c52af7 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -5,6 +5,7 @@ import { mapState, mapWritableState, mapActions } from 'pinia' import { useErrorStore } from "../stores/error-store" import { useNotesStore } from "../stores/notes-store" + import { useEditorCacheStore } from "../stores/editor-cache" const NUM_EDITOR_INSTANCES = 5 @@ -45,10 +46,6 @@ return { syntaxTreeDebugContent: null, editor: null, - editorCache: { - lru: [], - cache: {} - }, } }, @@ -164,36 +161,21 @@ methods: { ...mapActions(useErrorStore, ["addError"]), + ...mapActions(useEditorCacheStore, ["getEditor", "addEditor", "eachEditor"]), loadBuffer(path) { + console.log("loadBuffer", path) if (this.editor) { this.editor.hide() } - if (this.editorCache.cache[path]) { - // editor is already loaded, just switch to it - console.log("Switching to cached editor", path) - toRaw(this.editor).hide() - this.editor = this.editorCache.cache[path] + let cachedEditor = this.getEditor(path) + if (cachedEditor) { + console.log("show cached editor") + this.editor = cachedEditor toRaw(this.editor).show() - //toRaw(this.editor).currenciesLoaded() - this.currentEditor = toRaw(this.editor) - window._heynote_editor = toRaw(this.editor) - // move to end of LRU - this.editorCache.lru = this.editorCache.lru.filter(p => p !== path) - this.editorCache.lru.push(path) } else { - // check if we need to free up a slot - if (this.editorCache.lru.length >= NUM_EDITOR_INSTANCES) { - const pathToFree = this.editorCache.lru.shift() - console.log("Freeing up editor slot", pathToFree) - this.editorCache.cache[pathToFree].destroy() - delete this.editorCache.cache[pathToFree] - this.editorCache.lru = this.editorCache.lru.filter(p => p !== pathToFree) - } - - // create new Editor instance - console.log("Loading new editor", path) + console.log("create new editor") try { this.editor = new HeynoteEditor({ element: this.$refs.editor, @@ -209,15 +191,15 @@ defaultBlockToken: this.defaultBlockLanguage, defaultBlockAutoDetect: this.defaultBlockLanguageAutoDetect, }) - this.currentEditor = toRaw(this.editor) - window._heynote_editor = toRaw(this.editor) - this.editorCache.cache[path] = this.editor - this.editorCache.lru.push(path) } catch (e) { this.addError("Error! " + e.message) throw e } + this.addEditor(path, toRaw(this.editor)) } + + this.currentEditor = toRaw(this.editor) + window._heynote_editor = toRaw(this.editor) }, setLanguage(language) { @@ -245,10 +227,6 @@ focus() { toRaw(this.editor).focus() }, - - eachEditor(fn) { - Object.values(toRaw(this.editorCache).cache).forEach(fn) - }, }, } diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index c667a6e4..61e9d80e 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -3,14 +3,16 @@ import { mapState, mapActions } from 'pinia' import { toRaw } from 'vue'; - import { useNotesStore } from "../stores/notes-store" + import { useNotesStore, SCRATCH_FILE } from "../stores/notes-store" export default { data() { return { selected: 0, + actionButton: 0, filter: "", items: [], + SCRATCH_FILE: SCRATCH_FILE, } }, @@ -23,7 +25,7 @@ "path": path, "name": metadata?.name || path, "folder": path.split("/").slice(0, -1).join("/"), - "scratch": path === "buffer-dev.txt", + "scratch": path === SCRATCH_FILE, } }) if (this.items.length > 1) { @@ -84,9 +86,11 @@ methods: { ...mapActions(useNotesStore, [ "updateNotes", + "editNote", ]), onKeydown(event) { + const path = this.filteredItems[this.selected].path if (event.key === "ArrowDown") { if (this.selected === this.filteredItems.length - 1) { this.selected = 0 @@ -99,7 +103,7 @@ } else { this.$refs.item[this.selected].scrollIntoView({block: "nearest"}) } - + this.actionButton = 0 } else if (event.key === "ArrowUp") { if (this.selected === 0) { this.selected = this.filteredItems.length - 1 @@ -112,9 +116,23 @@ } else { this.$refs.item[this.selected].scrollIntoView({block: "nearest"}) } + this.actionButton = 0 + } else if (event.key === "ArrowRight" && path !== SCRATCH_FILE) { + event.preventDefault() + this.actionButton = Math.min(2, this.actionButton + 1) + } else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE) { + event.preventDefault() + this.actionButton = Math.max(0, this.actionButton - 1) } else if (event.key === "Enter") { - this.selectItem(this.filteredItems[this.selected].path) event.preventDefault() + if (this.actionButton === 1) { + console.log("edit file:", path) + this.editNote(path) + } else if (this.actionButton === 2) { + console.log("delete file:", path) + } else { + this.selectItem(path) + } } else if (event.key === "Escape") { this.$emit("close") event.preventDefault() @@ -140,9 +158,16 @@ getItemClass(item, idx) { return { "selected": idx === this.selected, + "action-buttons-visible": this.actionButton > 0, "scratch": item.scratch, } - } + }, + + showActionButtons(idx) { + this.selected = idx + this.actionButton = 1 + this.$refs.input.focus() + }, } } @@ -167,6 +192,21 @@ > + + + + + @@ -228,16 +268,20 @@ .items overflow-y: auto > li + position: relative border-radius: 3px padding: 5px 12px - cursor: pointer display: flex align-items: center &:hover background: #e2e2e2 + .action-buttons .show-actions + display: inline-block &.selected background: #48b57e color: #fff + .action-buttons .show-actions + display: inline-block &.scratch font-weight: 600 +dark-mode @@ -247,6 +291,10 @@ &.selected background: #1b6540 color: rgba(255,255,255, 0.87) + &.action-buttons-visible + background: none + border: 1px solid #1b6540 + padding: 4px 11px .name margin-right: 12px flex-shrink: 0 @@ -264,4 +312,44 @@ text-wrap: nowrap ::v-deep(b) font-weight: 700 + .action-buttons + position: absolute + top: 1px + right: 1px + button + padding: 1px 10px + font-size: 12px + background: none + border: none + border-radius: 2px + margin-right: 2px + cursor: pointer + &:last-child + margin-right: 0 + &:hover + background: rgba(255,255,255, 0.1) + +dark-mode + //background: #1b6540 + //&:hover + // background: + &.selected + background: #1b6540 + &:hover + background: #1f7449 + &.delete + background: #ae1e1e + &:hover + background: #bf2222 + &.show-actions + display: none + position: relative + top: 1px + padding: 1px 8px + //cursor: default + background-image: url(@/assets/icons/arrow-right.svg) + width: 22px + height: 19px + background-size: 19px + background-position: center center + background-repeat: no-repeat diff --git a/src/editor/editor.js b/src/editor/editor.js index 80801b98..754e5c80 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -217,6 +217,12 @@ export class HeynoteEditor { }) } + setName(name) { + this.note.metadata.name = name + this.name = name + triggerCursorChange(this.view) + } + getBlocks() { return this.view.state.facet(blockState) } diff --git a/src/main.js b/src/main.js index 9994a49b..fa49f2bf 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import App from './components/App.vue' import { loadCurrencies } from './currency' import { useErrorStore } from './stores/error-store' import { useNotesStore, initNotesStore } from './stores/notes-store' +import { useEditorCacheStore } from './stores/editor-cache' const pinia = createPinia() @@ -19,6 +20,7 @@ app.mount('#app').$nextTick(() => { }) const errorStore = useErrorStore() +const editorCacheStore = useEditorCacheStore() //errorStore.addError("test error") window.heynote.getInitErrors().then((errors) => { errors.forEach((e) => errorStore.addError(e)) diff --git a/src/stores/editor-cache.js b/src/stores/editor-cache.js new file mode 100644 index 00000000..26bb1869 --- /dev/null +++ b/src/stores/editor-cache.js @@ -0,0 +1,48 @@ +import { toRaw } from 'vue'; +import { defineStore } from "pinia" +import { NoteFormat } from "../editor/note-format" + +const NUM_EDITOR_INSTANCES = 5 + +export const useEditorCacheStore = defineStore("editorCache", { + state: () => ({ + editorCache: { + lru: [], + cache: {}, + }, + }), + + actions: { + getEditor(path) { + // move to end of LRU + this.editorCache.lru = this.editorCache.lru.filter(p => p !== path) + this.editorCache.lru.push(path) + + if (this.editorCache.cache[path]) { + return this.editorCache.cache[path] + } + }, + + addEditor(path, editor) { + if (this.editorCache.lru.length >= NUM_EDITOR_INSTANCES) { + const pathToFree = this.editorCache.lru.shift() + this.freeEditor(pathToFree) + } + + this.editorCache.cache[path] = editor + }, + + freeEditor(pathToFree) { + if (!this.editorCache.cache[pathToFree]) { + return + } + this.editorCache.cache[pathToFree].destroy() + delete this.editorCache.cache[pathToFree] + this.editorCache.lru = this.editorCache.lru.filter(p => p !== pathToFree) + }, + + eachEditor(fn) { + Object.values(this.editorCache.cache).forEach(fn) + }, + }, +}) diff --git a/src/stores/notes-store.js b/src/stores/notes-store.js index 4da1e8fe..bdd1187d 100644 --- a/src/stores/notes-store.js +++ b/src/stores/notes-store.js @@ -1,8 +1,9 @@ import { toRaw } from 'vue'; import { defineStore } from "pinia" import { NoteFormat } from "../editor/note-format" +import { useEditorCacheStore } from "./editor-cache" -const SCRATCH_FILE = window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt" +export const SCRATCH_FILE = window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt" export const useNotesStore = defineStore("notes", { state: () => ({ @@ -20,6 +21,7 @@ export const useNotesStore = defineStore("notes", { showNoteSelector: false, showLanguageSelector: false, showCreateNote: false, + showEditNote: false, }), actions: { @@ -32,9 +34,7 @@ export const useNotesStore = defineStore("notes", { }, openNote(path) { - this.showNoteSelector = false - this.showLanguageSelector = false - this.showCreateNote = false + this.closeDialog() this.currentNotePath = path const recent = this.recentNotePaths.filter((p) => p !== path) @@ -43,30 +43,49 @@ export const useNotesStore = defineStore("notes", { }, openLanguageSelector() { + this.closeDialog() this.showLanguageSelector = true - this.showNoteSelector = false - this.showCreateNote = false }, openNoteSelector() { + this.closeDialog() this.showNoteSelector = true - this.showLanguageSelector = false - this.showCreateNote = false }, openCreateNote() { + this.closeDialog() this.showCreateNote = true - this.showNoteSelector = false - this.showLanguageSelector = false }, closeDialog() { this.showCreateNote = false this.showNoteSelector = false this.showLanguageSelector = false + this.showEditNote = false + }, + + closeNoteSelector() { + this.showNoteSelector = false + }, + + editNote(path) { + if (this.currentNotePath !== path) { + this.openNote(path) + } + this.closeDialog() + this.showEditNote = true }, + /** + * Create a new note file at `path` with name `name` from the current block of the current open editor + */ async createNewNoteFromActiveBlock(path, name) { await toRaw(this.currentEditor).createNewNoteFromActiveBlock(path, name) }, + /** + * Create a new note file at path, with name `name`, and content content + * @param {*} path: File path relative to Heynote root + * @param {*} name Name of the note + * @param {*} content Contents (without metadata) + */ async saveNewNote(path, name, content) { //window.heynote.buffer.save(path, content) //this.updateNotes() @@ -82,6 +101,24 @@ export const useNotesStore = defineStore("notes", { await window.heynote.buffer.create(path, note.serialize()) this.updateNotes() }, + + async updateNoteMetadata(path, name, newPath) { + const editorCacheStore = useEditorCacheStore() + + if (this.currentEditor.path !== path) { + throw new Error(`Can't update note (${path}) since it's not the active one (${this.currentEditor.path})`) + } + console.log("currentEditor", this.currentEditor) + toRaw(this.currentEditor).setName(name) + await (toRaw(this.currentEditor)).save() + if (newPath && path !== newPath) { + console.log("moving note", path, newPath) + editorCacheStore.freeEditor(path) + await window.heynote.buffer.move(path, newPath) + this.openNote(newPath) + this.updateNotes() + } + }, }, }) From f647d9c01548d0eeb38d83bf9d86021995170248 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sat, 7 Sep 2024 12:15:35 +0200 Subject: [PATCH 25/65] Fix error when NoteSelector has zero results --- src/components/NoteSelector.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index 61e9d80e..a08ac5e7 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -90,6 +90,9 @@ ]), onKeydown(event) { + if (this.filteredItems.length === 0) { + return + } const path = this.filteredItems[this.selected].path if (event.key === "ArrowDown") { if (this.selected === this.filteredItems.length - 1) { From 7bbe1f8a7c47da1dd950fa4bb83c778b3ce7fa71 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 10 Sep 2024 09:55:14 +0200 Subject: [PATCH 26/65] Fix issue with changing theme and other editor settings not propagating down to the editor instances --- src/stores/editor-cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/editor-cache.js b/src/stores/editor-cache.js index 26bb1869..5cf492cf 100644 --- a/src/stores/editor-cache.js +++ b/src/stores/editor-cache.js @@ -42,7 +42,7 @@ export const useEditorCacheStore = defineStore("editorCache", { }, eachEditor(fn) { - Object.values(this.editorCache.cache).forEach(fn) + Object.values(toRaw(this.editorCache.cache)).forEach(fn) }, }, }) From aefe44fd2acffbb0967b17a96f0aea5c4de9d300 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 10 Sep 2024 13:34:23 +0200 Subject: [PATCH 27/65] WIP: Multiple notes support Add support for migrating old buffer file to new library. Add support for changing location for the notes library. Replace theme toggle in status bar with a dropdown in Appearance settings. Improve New Note and Update Note dialogs. Implement UI for confirming note delete (the actualal deltion is still to be implemented). --- assets/icons/arrow-right-black.svg | 1 + .../{arrow-right.svg => arrow-right-grey.svg} | 0 assets/icons/arrow-right-white.svg | 1 + docs/index.md | 4 + electron/initial-content.ts | 3 +- electron/main/buffer.js | 172 ------------------ electron/main/file-library.js | 92 +++++++++- electron/main/index.ts | 50 +++-- electron/preload/index.ts | 7 +- shared-utils/key-helper.ts | 2 + src/common/constants.js | 1 + src/{editor => common}/note-format.js | 0 src/components/App.vue | 10 +- src/components/EditNote.vue | 27 ++- src/components/Editor.vue | 9 +- src/components/NewNote.vue | 23 ++- src/components/NoteSelector.vue | 121 ++++++++---- src/components/StatusBar.vue | 18 -- src/components/settings/Settings.vue | 15 ++ src/editor/editor.js | 12 +- src/stores/editor-cache.js | 11 +- src/stores/notes-store.js | 21 ++- tests/note-format.spec.js | 2 +- tests/test-utils.js | 2 +- 24 files changed, 338 insertions(+), 266 deletions(-) create mode 100644 assets/icons/arrow-right-black.svg rename assets/icons/{arrow-right.svg => arrow-right-grey.svg} (100%) create mode 100644 assets/icons/arrow-right-white.svg delete mode 100644 electron/main/buffer.js create mode 100644 src/common/constants.js rename src/{editor => common}/note-format.js (100%) diff --git a/assets/icons/arrow-right-black.svg b/assets/icons/arrow-right-black.svg new file mode 100644 index 00000000..27ad162e --- /dev/null +++ b/assets/icons/arrow-right-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right-grey.svg similarity index 100% rename from assets/icons/arrow-right.svg rename to assets/icons/arrow-right-grey.svg diff --git a/assets/icons/arrow-right-white.svg b/assets/icons/arrow-right-white.svg new file mode 100644 index 00000000..9130299a --- /dev/null +++ b/assets/icons/arrow-right-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index bef7a83d..52f7c2fe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,6 +36,8 @@ Available for Mac, Windows, and Linux. ⌥ + Shift + Enter Add new block at the start of the buffer ⌘ + ⌥ + Enter Split the current block at cursor position ⌘ + L Change block language +⌘ + S Create a new note from the current block +⌘ + P Open note selector ⌘ + Down Goto next block ⌘ + Up Goto previous block ⌘ + A Select all text in a note block. Press again to select the whole buffer @@ -52,6 +54,8 @@ Ctrl + Shift + Enter Add new block at the end of the buffer Alt + Shift + Enter Add new block at the start of the buffer Ctrl + Alt + Enter Split the current block at cursor position Ctrl + L Change block language +Ctrl + S Create a new note from the current block +Ctrl + P Open note selector Ctrl + Down Goto next block Ctrl + Up Goto previous block Ctrl + A Select all text in a note block. Press again to select the whole buffer diff --git a/electron/initial-content.ts b/electron/initial-content.ts index d120afec..374f8124 100644 --- a/electron/initial-content.ts +++ b/electron/initial-content.ts @@ -1,9 +1,8 @@ import os from "os"; import { keyHelpStr } from "../shared-utils/key-helper"; -export const eraseInitialContent = !!process.env.ERASE_INITIAL_CONTENT - export const initialContent = ` +{"formatVersion":"1.0.0","name":"Scratch"} ∞∞∞markdown Welcome to Heynote! 👋 diff --git a/electron/main/buffer.js b/electron/main/buffer.js deleted file mode 100644 index 0ecc5a75..00000000 --- a/electron/main/buffer.js +++ /dev/null @@ -1,172 +0,0 @@ -import fs from "fs" -import os from "node:os" -import { join, dirname, basename } from "path" -import { app, ipcMain, dialog } from "electron" -import * as jetpack from "fs-jetpack"; - -import CONFIG from "../config" -import { isDev } from "../detect-platform" -import { win } from "./index" -import { eraseInitialContent, initialContent, initialDevContent } from '../initial-content' - -const untildify = (pathWithTilde) => { - const homeDirectory = os.homedir(); - return homeDirectory - ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) - : pathWithTilde; -} - -export function constructBufferFilePath(directoryPath, path) { - return join(untildify(directoryPath), path) -} - -export function getFullBufferFilePath(path) { - let defaultPath = app.getPath("userData") - let configPath = CONFIG.get("settings.bufferPath") - let bufferPath = configPath.length ? configPath : defaultPath - let bufferFilePath = constructBufferFilePath(bufferPath, path) - try { - // use realpathSync to resolve a potential symlink - return fs.realpathSync(bufferFilePath) - } catch (err) { - // realpathSync will fail if the file does not exist, but that doesn't matter since the file will be created - if (err.code !== "ENOENT") { - throw err - } - return bufferFilePath - } -} - - -export class Buffer { - constructor({filePath, onChange}) { - this.filePath = filePath - this.onChange = onChange - this.watcher = null - this.setupWatcher() - this._lastSavedContent = null - } - - async load() { - const content = await jetpack.read(this.filePath, 'utf8') - this.setupWatcher() - return content - } - - async save(content) { - this._lastSavedContent = content - const saveResult = await jetpack.write(this.filePath, content, { - atomic: true, - mode: '600', - }) - return saveResult - } - - exists() { - return jetpack.exists(this.filePath) === "file" - } - - setupWatcher() { - if (!this.watcher && this.exists()) { - this.watcher = fs.watch( - dirname(this.filePath), - { - persistent: true, - recursive: false, - encoding: "utf8", - }, - async (eventType, filename) => { - if (filename !== basename(this.filePath)) { - return - } - - // read the file content and compare it to the last saved content - // (if the content is the same, then we can ignore the event) - const content = await jetpack.read(this.filePath, 'utf8') - - if (this._lastSavedContent !== content) { - // file has changed on disk, trigger onChange - this.onChange(content) - } - } - ) - } - } - - close() { - if (this.watcher) { - this.watcher.close() - this.watcher = null - } - } -} - - -// Buffer -let buffers = {} -export function loadBuffer(path) { - if (buffers[path]) { - buffers[path].close() - } - buffers[path] = new Buffer({ - filePath: getFullBufferFilePath(path), - onChange: (content) => { - console.log("Old buffer.js onChange") - win?.webContents.send("buffer-content:change", path, content) - }, - }) - return buffers[path] -} - -ipcMain.handle('buffer-content:load', async (event, path) => { - if (!buffers[path]) { - loadBuffer(path) - } - if (buffers[path].exists() && !(eraseInitialContent && isDev)) { - return await buffers[path].load() - } else { - return isDev ? initialDevContent : initialContent - } -}); - -async function save(path, content) { - return await buffers[path].save(content) -} - -ipcMain.handle('buffer-content:save', async (event, path, content) => { - return await save(path, content) -}); - -export let contentSaved = false -ipcMain.handle('buffer-content:saveAndQuit', async (event, contents) => { - for (const [path, content] of contents) { - await save(path, content) - } - contentSaved = true - app.quit() -}) - -ipcMain.handle("buffer-content:selectLocation", async () => { - let result = await dialog.showOpenDialog({ - title: "Select directory to store buffer", - properties: [ - "openDirectory", - "createDirectory", - "noResolveAliases", - ], - }) - if (result.canceled) { - return - } - const filePath = result.filePaths[0] - if (fs.existsSync(constructBufferFilePath(filePath))) { - if (dialog.showMessageBoxSync({ - type: "question", - message: "The selected directory already contains a buffer file. It will be loaded. Do you want to continue?", - buttons: ["Cancel", "Continue"], - }) === 0) { - return - } - } - return filePath -}) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index bc06a186..6b3649d3 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -5,6 +5,16 @@ import { join, dirname, basename } from "path" import * as jetpack from "fs-jetpack"; import { app, ipcMain, dialog } from "electron" +import CONFIG from "../config" +import { SCRATCH_FILE_NAME } from "../../src/common/constants" +import { NoteFormat } from "../../src/common/note-format" +import { isDev } from '../detect-platform'; +import { initialContent, initialDevContent } from '../initial-content' + +export const NOTES_DIR_NAME = isDev ? "notes-dev" : "notes" + + +let library const untildify = (pathWithTilde) => { const homeDir = os.homedir() @@ -42,6 +52,11 @@ export class FileLibrary { this.watcher = null; this.contentSaved = false this.onChangeCallback = null + + // create scratch.txt if it doesn't exist + if (!this.jetpack.exists(SCRATCH_FILE_NAME)) { + this.jetpack.write(SCRATCH_FILE_NAME, isDev ? initialDevContent : initialContent) + } } async exists(path) { @@ -82,7 +97,7 @@ export class FileLibrary { } async getList() { - console.log("Loading notes") + //console.log("Listing notes") const notes = {} const files = await this.jetpack.findAsync(".", { matching: "*.txt", @@ -199,10 +214,13 @@ export class NoteBuffer { } } +export function setCurrentFileLibrary(lib) { + library = lib +} -export function setupFileLibraryEventHandlers(library, win) { +export function setupFileLibraryEventHandlers(win) { ipcMain.handle('buffer:load', async (event, path) => { - console.log("buffer:load", path) + //console.log("buffer:load", path) return await library.load(path) }); @@ -244,5 +262,71 @@ export function setupFileLibraryEventHandlers(library, win) { return await library.move(path, newPath) }); - library.setupWatcher(win) + ipcMain.handle("library:selectLocation", async () => { + let result = await dialog.showOpenDialog({ + title: "Select directory to store buffer", + properties: [ + "openDirectory", + "createDirectory", + "noResolveAliases", + ], + }) + if (result.canceled) { + return + } + const filePath = result.filePaths[0] + return filePath + }) } + + +export async function migrateBufferFileToLibrary(app) { + async function ensureBufferFileMetadata(filePath) { + const metadata = await readNoteMetadata(filePath) + //console.log("Metadata", metadata) + if (!metadata || !metadata.name) { + console.log("Adding metadata to", filePath) + const note = NoteFormat.load(jetpack.read(filePath)) + note.metadata.name = "Scratch" + jetpack.write(filePath, note.serialize()) + } else { + console.log("Metadata already exists for", filePath) + } + } + + const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME) + const customBufferPath = CONFIG.get("settings.bufferPath") + const oldBufferFile = isDev ? "buffer-dev.txt" : "buffer.txt" + if (customBufferPath) { + // if the new buffer file exists, no need to migrate + if (jetpack.exists(join(customBufferPath, SCRATCH_FILE_NAME)) === "file") { + return + } + const oldBufferFileFullPath = join(customBufferPath, oldBufferFile) + if (jetpack.exists(oldBufferFileFullPath) === "file") { + const newFileFullPath = join(customBufferPath, SCRATCH_FILE_NAME); + console.log(`Migrating file ${oldBufferFileFullPath} to ${newFileFullPath}`) + // rename buffer file to scratch.txt + jetpack.move(oldBufferFileFullPath, newFileFullPath) + // add metadata to scratch.txt (just to be sure, we'll double check that it's needed first) + await ensureBufferFileMetadata(newFileFullPath) + } + } else { + // if the new buffer file exists, no need to migrate + if (jetpack.exists(join(defaultLibraryPath, SCRATCH_FILE_NAME)) === "file") { + return + } + // check if the old buffer file exists, while the default *library* path doesn't exist + const oldBufferFileFullPath = join(app.getPath("userData"), oldBufferFile) + if (jetpack.exists(oldBufferFileFullPath) === "file" && jetpack.exists(defaultLibraryPath) !== "dir") { + const newFileFullPath = join(defaultLibraryPath, SCRATCH_FILE_NAME); + console.log(`Migrating buffer file ${oldBufferFileFullPath} to ${newFileFullPath}`) + // create the default library path + jetpack.dir(defaultLibraryPath) + // move the buffer file to the library path + jetpack.move(oldBufferFileFullPath, newFileFullPath) + // add metadata to scratch.txt + await ensureBufferFileMetadata(newFileFullPath) + } + } +} \ No newline at end of file diff --git a/electron/main/index.ts b/electron/main/index.ts index 93a24f57..c78a57f6 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -9,8 +9,13 @@ import CONFIG from "../config" import { isDev, isLinux, isMac, isWindows } from '../detect-platform'; import { initializeAutoUpdate, checkForUpdates } from './auto-update'; import { fixElectronCors } from './cors'; -import { loadBuffer, contentSaved } from './buffer'; -import { FileLibrary, setupFileLibraryEventHandlers } from './file-library'; +import { + FileLibrary, + setupFileLibraryEventHandlers, + setCurrentFileLibrary, + migrateBufferFileToLibrary, + NOTES_DIR_NAME +} from './file-library'; // The built directory structure @@ -310,7 +315,9 @@ function registerAlwaysOnTop() { } app.whenReady().then(createWindow).then(async () => { - setupFileLibraryEventHandlers(fileLibrary, win) + initFileLibrary(win).then(() => { + setupFileLibraryEventHandlers(win) + }) initializeAutoUpdate(win) registerGlobalHotkey() registerShowInDock() @@ -352,14 +359,28 @@ ipcMain.handle('dark-mode:set', (event, mode) => { ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource) + // Initialize note/file library -const customLibraryPath = CONFIG.get("settings.bufferPath") -const libraryPath = customLibraryPath ? customLibraryPath : join(app.getPath("userData"), "notes") -console.log("libraryPath", libraryPath) -try { - fileLibrary = new FileLibrary(libraryPath) -} catch (error) { - initErrors.push(`Error: ${error.message}`) +async function initFileLibrary(win) { + await migrateBufferFileToLibrary(app) + + const customLibraryPath = CONFIG.get("settings.bufferPath") + const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME) + const libraryPath = customLibraryPath ? customLibraryPath : defaultLibraryPath + //console.log("libraryPath", libraryPath) + + // if we're using the default library path, and it doesn't exist (e.g. first time run), create it + if (!customLibraryPath && !fs.existsSync(defaultLibraryPath)) { + fs.mkdirSync(defaultLibraryPath) + } + + try { + fileLibrary = new FileLibrary(libraryPath) + fileLibrary.setupWatcher(win) + } catch (error) { + initErrors.push(`Error: ${error.message}`) + } + setCurrentFileLibrary(fileLibrary) } ipcMain.handle("getInitErrors", () => { @@ -393,9 +414,10 @@ ipcMain.handle('settings:set', async (event, settings) => { registerAlwaysOnTop() } if (bufferPathChanged) { - const buffer = loadBuffer() - if (buffer.exists()) { - win?.webContents.send("buffer-content:change", await buffer.load()) - } + console.log("bufferPath changed, closing existing file library") + fileLibrary.close() + console.log("initializing new file library") + initFileLibrary(win) + await win.webContents.send("library:pathChanged") } }) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index bb1e5536..9e5da29d 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -108,12 +108,11 @@ contextBridge.exposeInMainWorld("heynote", { }, async selectLocation() { - return await ipcRenderer.invoke("buffer-content:selectLocation") + return await ipcRenderer.invoke("library:selectLocation") }, - callbacks(callbacks) { - ipcRenderer.on("buffer:noteMetadataChanged", (event, path, info) => callbacks?.noteMetadataChanged(path, info)) - ipcRenderer.on("buffer:noteRemoved", (event, path) => callbacks?.noteRemoved(path)) + setLibraryPathChangeCallback(callback) { + ipcRenderer.on("library:pathChanged", callback) }, }, diff --git a/shared-utils/key-helper.ts b/shared-utils/key-helper.ts index 99300427..900a78f8 100644 --- a/shared-utils/key-helper.ts +++ b/shared-utils/key-helper.ts @@ -9,6 +9,8 @@ export const keyHelpStr = (platform: string) => { [`${altChar} + Shift + Enter`, "Add new block at the start of the buffer"], [`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"], [`${modChar} + L`, "Change block language"], + [`${modChar} + S`, "Create a new note from the current block"], + [`${modChar} + P`, "Open note selector"], [`${modChar} + Down`, "Goto next block"], [`${modChar} + Up`, "Goto previous block"], [`${modChar} + A`, "Select all text in a note block. Press again to select the whole buffer"], diff --git a/src/common/constants.js b/src/common/constants.js new file mode 100644 index 00000000..42c17667 --- /dev/null +++ b/src/common/constants.js @@ -0,0 +1 @@ +export const SCRATCH_FILE_NAME = "scratch.txt" diff --git a/src/editor/note-format.js b/src/common/note-format.js similarity index 100% rename from src/editor/note-format.js rename to src/common/note-format.js diff --git a/src/components/App.vue b/src/components/App.vue index 982b80ae..b9683b03 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -137,6 +137,11 @@ this.$refs.editor.focus() }, + setTheme(theme) { + window.heynote.themeMode.set(theme) + this.themeSetting = theme + }, + onSelectLanguage(language) { this.closeDialog() this.$refs.editor.setLanguage(language) @@ -170,11 +175,8 @@ ref="editor" />
    - + +
    @@ -256,7 +273,7 @@ padding: 10px padding-top: 0 display: flex - justify-content: flex-end + justify-content: space-between button font-size: 12px height: 28px @@ -270,5 +287,9 @@ background: #444 border: none color: rgba(255,255,255, 0.75) + &[type="submit"] + order: 1 + &.cancel + order: 0 diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 93c52af7..5358d16b 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -86,9 +86,9 @@ }, watch: { - currentNotePath(path) { + loadNewEditor() { //console.log("currentNotePath changed to", path) - this.loadBuffer(path) + this.loadBuffer(this.currentNotePath) }, theme(newTheme) { @@ -152,11 +152,16 @@ computed: { ...mapState(useNotesStore, [ "currentNotePath", + "libraryId", ]), ...mapWritableState(useNotesStore, [ "currentEditor", "currentNoteName", ]), + + loadNewEditor() { + return `${this.currentNotePath}|${this.libraryId}` + }, }, methods: { diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 7cf8f20e..224b9e54 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -96,6 +96,18 @@ } }, + onCancelKeydown(event) { + if (event.key === "Enter") { + event.preventDefault() + event.stopPropagation() + this.cancel() + } + }, + + cancel() { + this.$emit("close") + }, + onInputKeydown(event) { // redirect arrow keys and page up/down to folder selector const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"] @@ -162,6 +174,11 @@
    +
    @@ -246,7 +263,7 @@ padding: 10px padding-top: 0 display: flex - justify-content: flex-end + justify-content: space-between button font-size: 12px height: 28px @@ -260,5 +277,9 @@ background: #444 border: none color: rgba(255,255,255, 0.75) + &[type="submit"] + order: 1 + &.cancel + order: 0 diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index a08ac5e7..a5d6e4b1 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -3,7 +3,8 @@ import { mapState, mapActions } from 'pinia' import { toRaw } from 'vue'; - import { useNotesStore, SCRATCH_FILE } from "../stores/notes-store" + import { SCRATCH_FILE_NAME } from "../common/constants" + import { useNotesStore } from "../stores/notes-store" export default { data() { @@ -12,7 +13,8 @@ actionButton: 0, filter: "", items: [], - SCRATCH_FILE: SCRATCH_FILE, + SCRATCH_FILE_NAME: SCRATCH_FILE_NAME, + deleteConfirm: false, } }, @@ -25,7 +27,7 @@ "path": path, "name": metadata?.name || path, "folder": path.split("/").slice(0, -1).join("/"), - "scratch": path === SCRATCH_FILE, + "scratch": path === SCRATCH_FILE_NAME, } }) if (this.items.length > 1) { @@ -120,25 +122,30 @@ this.$refs.item[this.selected].scrollIntoView({block: "nearest"}) } this.actionButton = 0 - } else if (event.key === "ArrowRight" && path !== SCRATCH_FILE) { + } else if (event.key === "ArrowRight" && path !== SCRATCH_FILE_NAME) { event.preventDefault() this.actionButton = Math.min(2, this.actionButton + 1) - } else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE) { + } else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE_NAME) { event.preventDefault() this.actionButton = Math.max(0, this.actionButton - 1) + this.deleteConfirm = false } else if (event.key === "Enter") { event.preventDefault() if (this.actionButton === 1) { console.log("edit file:", path) this.editNote(path) } else if (this.actionButton === 2) { - console.log("delete file:", path) + this.deleteConfirmNote(path) } else { this.selectItem(path) } } else if (event.key === "Escape") { - this.$emit("close") event.preventDefault() + if (this.actionButton !== 0) { + this.hideActionButtons() + } else { + this.$emit("close") + } } }, @@ -169,8 +176,24 @@ showActionButtons(idx) { this.selected = idx this.actionButton = 1 + this.deleteConfirm = false this.$refs.input.focus() }, + + hideActionButtons() { + this.actionButton = 0 + this.deleteConfirm = false + }, + + deleteConfirmNote(path) { + if (this.deleteConfirm) { + console.log("delete file:", path) + } else { + this.deleteConfirm = true + this.actionButton = 2 + this.$refs.input.focus() + } + }, } } @@ -195,18 +218,27 @@ > - + + :class="{'delete':true, 'selected':actionButton === 2, 'confirm':deleteConfirm}" + @click.stop.prevent="deleteConfirmNote(item.path)" + > + + + @@ -273,31 +305,40 @@ > li position: relative border-radius: 3px - padding: 5px 12px + padding: 3px 12px + line-height: 18px display: flex align-items: center &:hover background: #e2e2e2 .action-buttons .show-actions display: inline-block + background-image: url(@/assets/icons/arrow-right-black.svg) + &.selected .action-buttons .show-actions + background-image: url(@/assets/icons/arrow-right-white.svg) + +dark-mode + color: rgba(255,255,255, 0.65) + &:hover + background: #29292a &.selected background: #48b57e color: #fff + &.action-buttons-visible + background: none + border: 1px solid #48b57e + padding: 2px 11px + color: #444 .action-buttons .show-actions display: inline-block - &.scratch - font-weight: 600 - +dark-mode - color: rgba(255,255,255, 0.65) - &:hover - background: #29292a - &.selected + +dark-mode background: #1b6540 color: rgba(255,255,255, 0.87) &.action-buttons-visible background: none border: 1px solid #1b6540 - padding: 4px 11px + color: rgba(255,255,255, 0.65) + &.scratch + font-weight: 600 .name margin-right: 12px flex-shrink: 0 @@ -318,9 +359,15 @@ .action-buttons position: absolute top: 1px - right: 1px + right: 0px + padding: 0 1px + &.visible + background: #efefef + +dark-mode + background: #151516 button - padding: 1px 10px + padding: 0 10px + height: 20px font-size: 12px background: none border: none @@ -330,29 +377,41 @@ &:last-child margin-right: 0 &:hover - background: rgba(255,255,255, 0.1) + background: rgba(0,0,0, 0.1) +dark-mode - //background: #1b6540 - //&:hover - // background: + &:hover + background-color: rgba(255,255,255, 0.1) &.selected - background: #1b6540 + background: #48b57e + color: #fff &:hover - background: #1f7449 + background: #3ea471 &.delete - background: #ae1e1e + background: #e95050 + &:hover + background: #ce4848 + +dark-mode + background: #1b6540 &:hover - background: #bf2222 + background: #1f7449 + &.delete + background: #ae1e1e + &:hover + background: #bf2222 + &.confirm + font-weight: 600 &.show-actions display: none position: relative top: 1px padding: 1px 8px //cursor: default - background-image: url(@/assets/icons/arrow-right.svg) + background-image: url(@/assets/icons/arrow-right-white.svg) width: 22px height: 19px background-size: 19px background-position: center center background-repeat: no-repeat + +dark-mode + background-image: url(@/assets/icons/arrow-right-grey.svg) diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index a6b4df16..4c7cdefc 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -9,8 +9,6 @@ export default { props: [ - "theme", - "themeSetting", "autoUpdate", "allowBetaVersions", ], @@ -113,9 +111,6 @@ :autoUpdate="autoUpdate" :allowBetaVersions="allowBetaVersions" /> -
    - -
    +
    +
    +

    Color Theme

    + +
    +

    Gutters

    diff --git a/src/editor/editor.js b/src/editor/editor.js index 754e5c80..fb23d9c8 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -21,7 +21,7 @@ import { languageDetection } from "./language-detection/autodetect.js" import { autoSaveContent } from "./save.js" import { todoCheckboxPlugin} from "./todo-checkbox.ts" import { links } from "./links.js" -import { NoteFormat } from "./note-format.js" +import { NoteFormat } from "../common/note-format.js" import { useNotesStore } from "../stores/notes-store.js"; @@ -140,7 +140,7 @@ export class HeynoteEditor { if (content === this.diskContent) { return } - console.log("saving:", this.path) + //console.log("saving:", this.path) this.diskContent = content await window.heynote.buffer.save(this.path, content) } @@ -158,7 +158,7 @@ export class HeynoteEditor { } async loadContent() { - console.log("loading content", this.path) + //console.log("loading content", this.path) const content = await window.heynote.buffer.load(this.path) this.diskContent = content this.contentLoaded = true @@ -328,11 +328,13 @@ export class HeynoteEditor { triggerCurrenciesLoaded(this.view.state, this.view.dispatch) } - destroy() { + destroy(save=true) { if (this.onChange) { window.heynote.buffer.removeOnChangeCallback(this.path, this.onChange) } - this.save() + if (save) { + this.save() + } this.view.destroy() window.heynote.buffer.close(this.path) } diff --git a/src/stores/editor-cache.js b/src/stores/editor-cache.js index 5cf492cf..a8b41e9c 100644 --- a/src/stores/editor-cache.js +++ b/src/stores/editor-cache.js @@ -1,6 +1,6 @@ import { toRaw } from 'vue'; import { defineStore } from "pinia" -import { NoteFormat } from "../editor/note-format" +import { NoteFormat } from "../common/note-format" const NUM_EDITOR_INSTANCES = 5 @@ -44,5 +44,14 @@ export const useEditorCacheStore = defineStore("editorCache", { eachEditor(fn) { Object.values(toRaw(this.editorCache.cache)).forEach(fn) }, + + clearCache(save=true) { + console.log("Clearing editor cache") + this.eachEditor((editor) => { + editor.destroy(save=save) + }) + this.editorCache.cache = {} + this.editorCache.lru = [] + }, }, }) diff --git a/src/stores/notes-store.js b/src/stores/notes-store.js index bdd1187d..5ac1c11d 100644 --- a/src/stores/notes-store.js +++ b/src/stores/notes-store.js @@ -1,22 +1,23 @@ import { toRaw } from 'vue'; import { defineStore } from "pinia" -import { NoteFormat } from "../editor/note-format" +import { NoteFormat } from "../common/note-format" import { useEditorCacheStore } from "./editor-cache" +import { SCRATCH_FILE_NAME } from "../common/constants" -export const SCRATCH_FILE = window.heynote.isDev ? "buffer-dev.txt" : "buffer.txt" export const useNotesStore = defineStore("notes", { state: () => ({ notes: {}, - recentNotePaths: [SCRATCH_FILE], + recentNotePaths: [SCRATCH_FILE_NAME], currentEditor: null, - currentNotePath: SCRATCH_FILE, + currentNotePath: SCRATCH_FILE_NAME, currentNoteName: null, currentLanguage: null, currentLanguageAuto: null, currentCursorLine: null, currentSelectionSize: null, + libraryId: 0, showNoteSelector: false, showLanguageSelector: false, @@ -119,10 +120,22 @@ export const useNotesStore = defineStore("notes", { this.updateNotes() } }, + + async reloadLibrary() { + const editorCacheStore = useEditorCacheStore() + await this.updateNotes() + editorCacheStore.clearCache(false) + this.currentEditor = null + this.currentNotePath = SCRATCH_FILE_NAME + this.libraryId++ + }, }, }) export async function initNotesStore() { const notesStore = useNotesStore() + window.heynote.buffer.setLibraryPathChangeCallback(() => { + notesStore.reloadLibrary() + }) await notesStore.updateNotes() } diff --git a/tests/note-format.spec.js b/tests/note-format.spec.js index f9acfb93..9822d039 100644 --- a/tests/note-format.spec.js +++ b/tests/note-format.spec.js @@ -1,6 +1,6 @@ import { test, expect } from "@playwright/test"; import { HeynotePage } from "./test-utils.js"; -import { NoteFormat } from "../src/editor/note-format.js"; +import { NoteFormat } from "../src/common/note-format.js"; let heynotePage diff --git a/tests/test-utils.js b/tests/test-utils.js index b2760cf3..aa59fb61 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { NoteFormat } from '../src/editor/note-format.js'; +import { NoteFormat } from '../src/common/note-format.js'; export function pageErrorGetter(page) { let messages = []; From 6772a4579d7a3488bc034d4e02ed1507f3bd28d7 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 10 Sep 2024 16:22:30 +0200 Subject: [PATCH 28/65] Implement functionality for deleting notes --- electron/main/file-library.js | 12 ++++++++++++ electron/preload/index.ts | 4 ++++ src/components/NoteSelector.vue | 28 +++++++++++++++++++--------- src/editor/block/commands.js | 4 +++- src/stores/notes-store.js | 18 ++++++++++++++++-- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index 6b3649d3..987b8ffa 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -96,6 +96,14 @@ export class FileLibrary { await this.jetpack.moveAsync(fullOldPath, fullNewPath) } + async delete(path) { + if (path === SCRATCH_FILE_NAME) { + throw new Error("Can't delete scratch file") + } + const fullPath = join(this.basePath, path) + await this.jetpack.removeAsync(fullPath) + } + async getList() { //console.log("Listing notes") const notes = {} @@ -262,6 +270,10 @@ export function setupFileLibraryEventHandlers(win) { return await library.move(path, newPath) }); + ipcMain.handle('buffer:delete', async (event, path) => { + return await library.delete(path) + }); + ipcMain.handle("library:selectLocation", async () => { let result = await dialog.showOpenDialog({ title: "Select directory to store buffer", diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 9e5da29d..1dd2eb15 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -77,6 +77,10 @@ contextBridge.exposeInMainWorld("heynote", { return await ipcRenderer.invoke("buffer:save", path, content) }, + async delete(path) { + return await ipcRenderer.invoke("buffer:delete", path) + }, + async move(path, newPath) { return await ipcRenderer.invoke("buffer:move", path, newPath) }, diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index a5d6e4b1..58058e69 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -22,14 +22,7 @@ await this.updateNotes() this.$refs.container.focus() this.$refs.input.focus() - this.items = Object.entries(this.notes).map(([path, metadata]) => { - return { - "path": path, - "name": metadata?.name || path, - "folder": path.split("/").slice(0, -1).join("/"), - "scratch": path === SCRATCH_FILE_NAME, - } - }) + this.buildItems() if (this.items.length > 1) { this.selected = 1 } @@ -89,8 +82,21 @@ ...mapActions(useNotesStore, [ "updateNotes", "editNote", + "deleteNote", ]), + buildItems() { + //console.log("buildItems", Object.entries(this.notes)) + this.items = Object.entries(this.notes).map(([path, metadata]) => { + return { + "path": path, + "name": metadata?.name || path, + "folder": path.split("/").slice(0, -1).join("/"), + "scratch": path === SCRATCH_FILE_NAME, + } + }) + }, + onKeydown(event) { if (this.filteredItems.length === 0) { return @@ -185,9 +191,13 @@ this.deleteConfirm = false }, - deleteConfirmNote(path) { + async deleteConfirmNote(path) { if (this.deleteConfirm) { console.log("delete file:", path) + await this.deleteNote(path) + this.hideActionButtons() + this.buildItems() + this.selected = Math.min(this.selected, this.items.length - 1) } else { this.deleteConfirm = true this.actionButton = 2 diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js index 42192c34..c59ce51d 100644 --- a/src/editor/block/commands.js +++ b/src/editor/block/commands.js @@ -325,8 +325,10 @@ export const deleteBlock = (editor) => ({state, dispatch}) => { const block = getActiveNoteBlock(state) const blocks = state.facet(blockState) let replace = "" + let newSelection = block.delimiter.from if (blocks.length == 1) { replace = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect) + newSelection = replace.length } dispatch(state.update({ changes: { @@ -334,7 +336,7 @@ export const deleteBlock = (editor) => ({state, dispatch}) => { to: block.range.to, insert: replace, }, - selection: EditorSelection.cursor(block.delimiter.from), + selection: EditorSelection.cursor(newSelection), annotations: [heynoteEvent.of(DELETE_BLOCK)], })) } diff --git a/src/stores/notes-store.js b/src/stores/notes-store.js index 5ac1c11d..5f815d8b 100644 --- a/src/stores/notes-store.js +++ b/src/stores/notes-store.js @@ -109,11 +109,11 @@ export const useNotesStore = defineStore("notes", { if (this.currentEditor.path !== path) { throw new Error(`Can't update note (${path}) since it's not the active one (${this.currentEditor.path})`) } - console.log("currentEditor", this.currentEditor) + //console.log("currentEditor", this.currentEditor) toRaw(this.currentEditor).setName(name) await (toRaw(this.currentEditor)).save() if (newPath && path !== newPath) { - console.log("moving note", path, newPath) + //console.log("moving note", path, newPath) editorCacheStore.freeEditor(path) await window.heynote.buffer.move(path, newPath) this.openNote(newPath) @@ -121,6 +121,20 @@ export const useNotesStore = defineStore("notes", { } }, + async deleteNote(path) { + if (path === SCRATCH_FILE_NAME) { + throw new Error("Can't delete scratch file") + } + const editorCacheStore = useEditorCacheStore() + if (this.currentEditor.path === path) { + this.currentEditor = null + this.currentNotePath = SCRATCH_FILE_NAME + } + editorCacheStore.freeEditor(path) + await window.heynote.buffer.delete(path) + await this.updateNotes() + }, + async reloadLibrary() { const editorCacheStore = useEditorCacheStore() await this.updateNotes() From b5db0208d6a16c63da38d17fa1bfa13a33884ebc Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 11:50:21 +0200 Subject: [PATCH 29/65] Add Changelog to docs --- docs/index.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/docs/index.md b/docs/index.md index 52f7c2fe..de26c9df 100644 --- a/docs/index.md +++ b/docs/index.md @@ -148,3 +148,89 @@ registerShortcut('toggleHeynote', 'Toggle Heynote', 'Ctrl+Shift+H', toggleHeynot See the [KWin scripting tutorial](https://develop.kde.org/docs/plasma/kwin/) for instructions on how to install the script. Remember to enable the script in the KDE System Settings. It may also be necessary to go into the KDE System Settings and bind the "Toggle Heynote" key manually. + + +## Changelog + +Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases). + +### 2.0.0-beta (not yet released) + +#### Support for multiple note buffers. + +Apart from the default scratch buffer, you can now create and switch between multiple note buffers. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New note buffers are saved to the note library which is basically a directories (with sub dirs) on the disk with `.txt` files. You switch between note buffers by pressing `Ctrl/Cmd+P`. + +The first time you start the new version of Heynote, your existing buffer file will be migrated to the new note library. If you're using the default buffer location, that means that the existing scratch buffer file will be moved from `%APP_DIR%/buffer.txt` to `%APP_DIR%/notes/scratch.txt`. If you are using a custom buffer location the file will be moved from `%CUSTOM_DIR%/buffer.txt` to `%CUSTOM_DIR%/scratch.txt`. + +#### Other changes + +- The file format for the buffer files has been updated to include some JSON metadata at the top of the file. +- The cursor(s) location is saved between sessions. +- The setting for changing the color theme is now located in the program settings, instead of in the status bar. + +### 1.8.0 + +- Performance optimizations +- Add default redo cmd that works on all Platforms. Mod+Shift+Z +- Fix bug causing editing to break for empty blocks in some cases +- Add setting for configuring the default block language +- Vue language support +- Dart Syntax +- Fix error on startup for large buffers + +### 1.7.1 + +- Update to latest version of Electron. Fixes crash on MacOS 15 Developer Preview + +### 1.7.0 + +- Fix "white flash" effect when resizing window in dark mode +- Add prev variable to Math blocks that holds the previous value +- Add settings button to status bar +- Add version number to settings dialog +- Persist window location when opening the app +- Copy whole current line(s) when selection(s) are empty +- Fix block corruption when deleting block content using deleteLine command +- Add PowerShell and Diff language modes +- "Always on top" setting which makes Heynote stay on top of other programs + +### 1.6.0 + +- Added support for having Heynote in the Mac Menu Bar / Tray icon +- Ability to specify file system location of Heynote's buffer file. The buffer will automatically be reloaded if changed on disk, so this should make it possible to have the buffer automatically synced between machines using a file-syncing service such as Dropbox. +- Custom font and font size support. +- More key-binding for creating new blocks +- Syntax hightlighting support for new languages: + * Swift + * Kotlin + * Groovy +- Auto-close brackets functionality that can be turned on in settings +- Ability to change how calculations are formatted in Math blocks. See the [Docs](https://heynote.com/docs/#user-content-changing-how-the-results-of-math-blocks-are-formatted) for info on how to do this. +- There's now a Heynote webapp at [app.heynote.com](https://app.heynote.com). It's still work-in-progress, but should be usable. The buffer is stored in localStorage. +- Multiple bug fixes and minor improvement. + + +### 1.5.0 + +- Add support for the following languages + * TypeScript + * JSX + * TSX + * TOML + * C# + * Clojure + * Erlang + * Golang + * Lezer + * Ruby + * Shell + * YAML +- Various bug fixes and improvements + +### 1.4.1 + +- Fixed issue that would sometimes cause auto formatting to freeze the app for long periods. + +### 1.4.0 + +- Added ability to set a global hotkey for showing/hiding Heynote. From 5564c054632f99e7734c3bb048d6b462b8e8eb2c Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 11:57:03 +0200 Subject: [PATCH 30/65] Move Changelog into separate file --- docs/changelog.md | 84 +++++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 86 ----------------------------------------------- 2 files changed, 84 insertions(+), 86 deletions(-) create mode 100644 docs/changelog.md diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..ade702aa --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,84 @@ +# Changelog + +Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases). + +## 2.0.0-beta (not yet released) + +### Support for multiple note buffers. + +Apart from the default scratch buffer, you can now create and switch between multiple note buffers. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New note buffers are saved to the note library which is basically a directories (with sub dirs) on the disk with `.txt` files. You switch between note buffers by pressing `Ctrl/Cmd+P`. + +The first time you start the new version of Heynote, your existing buffer file will be migrated to the new note library. If you're using the default buffer location, that means that the existing scratch buffer file will be moved from `%APP_DIR%/buffer.txt` to `%APP_DIR%/notes/scratch.txt`. If you are using a custom buffer location the file will be moved from `%CUSTOM_DIR%/buffer.txt` to `%CUSTOM_DIR%/scratch.txt`. + +### Other changes + +- The file format for the buffer files has been updated to include some JSON metadata at the top of the file. +- The cursor(s) location is saved between sessions. +- The setting for changing the color theme is now located in the program settings, instead of in the status bar. + +## 1.8.0 + +- Performance optimizations +- Add default redo cmd that works on all Platforms. Mod+Shift+Z +- Fix bug causing editing to break for empty blocks in some cases +- Add setting for configuring the default block language +- Vue language support +- Dart Syntax +- Fix error on startup for large buffers + +## 1.7.1 + +- Update to latest version of Electron. Fixes crash on MacOS 15 Developer Preview + +## 1.7.0 + +- Fix "white flash" effect when resizing window in dark mode +- Add prev variable to Math blocks that holds the previous value +- Add settings button to status bar +- Add version number to settings dialog +- Persist window location when opening the app +- Copy whole current line(s) when selection(s) are empty +- Fix block corruption when deleting block content using deleteLine command +- Add PowerShell and Diff language modes +- "Always on top" setting which makes Heynote stay on top of other programs + +## 1.6.0 + +- Added support for having Heynote in the Mac Menu Bar / Tray icon +- Ability to specify file system location of Heynote's buffer file. The buffer will automatically be reloaded if changed on disk, so this should make it possible to have the buffer automatically synced between machines using a file-syncing service such as Dropbox. +- Custom font and font size support. +- More key-binding for creating new blocks +- Syntax hightlighting support for new languages: + * Swift + * Kotlin + * Groovy +- Auto-close brackets functionality that can be turned on in settings +- Ability to change how calculations are formatted in Math blocks. See the [Docs](https://heynote.com/docs/#user-content-changing-how-the-results-of-math-blocks-are-formatted) for info on how to do this. +- There's now a Heynote webapp at [app.heynote.com](https://app.heynote.com). It's still work-in-progress, but should be usable. The buffer is stored in localStorage. +- Multiple bug fixes and minor improvement. + + +## 1.5.0 + +- Add support for the following languages + * TypeScript + * JSX + * TSX + * TOML + * C# + * Clojure + * Erlang + * Golang + * Lezer + * Ruby + * Shell + * YAML +- Various bug fixes and improvements + +## 1.4.1 + +- Fixed issue that would sometimes cause auto formatting to freeze the app for long periods. + +## 1.4.0 + +- Added ability to set a global hotkey for showing/hiding Heynote. diff --git a/docs/index.md b/docs/index.md index de26c9df..52f7c2fe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -148,89 +148,3 @@ registerShortcut('toggleHeynote', 'Toggle Heynote', 'Ctrl+Shift+H', toggleHeynot See the [KWin scripting tutorial](https://develop.kde.org/docs/plasma/kwin/) for instructions on how to install the script. Remember to enable the script in the KDE System Settings. It may also be necessary to go into the KDE System Settings and bind the "Toggle Heynote" key manually. - - -## Changelog - -Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases). - -### 2.0.0-beta (not yet released) - -#### Support for multiple note buffers. - -Apart from the default scratch buffer, you can now create and switch between multiple note buffers. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New note buffers are saved to the note library which is basically a directories (with sub dirs) on the disk with `.txt` files. You switch between note buffers by pressing `Ctrl/Cmd+P`. - -The first time you start the new version of Heynote, your existing buffer file will be migrated to the new note library. If you're using the default buffer location, that means that the existing scratch buffer file will be moved from `%APP_DIR%/buffer.txt` to `%APP_DIR%/notes/scratch.txt`. If you are using a custom buffer location the file will be moved from `%CUSTOM_DIR%/buffer.txt` to `%CUSTOM_DIR%/scratch.txt`. - -#### Other changes - -- The file format for the buffer files has been updated to include some JSON metadata at the top of the file. -- The cursor(s) location is saved between sessions. -- The setting for changing the color theme is now located in the program settings, instead of in the status bar. - -### 1.8.0 - -- Performance optimizations -- Add default redo cmd that works on all Platforms. Mod+Shift+Z -- Fix bug causing editing to break for empty blocks in some cases -- Add setting for configuring the default block language -- Vue language support -- Dart Syntax -- Fix error on startup for large buffers - -### 1.7.1 - -- Update to latest version of Electron. Fixes crash on MacOS 15 Developer Preview - -### 1.7.0 - -- Fix "white flash" effect when resizing window in dark mode -- Add prev variable to Math blocks that holds the previous value -- Add settings button to status bar -- Add version number to settings dialog -- Persist window location when opening the app -- Copy whole current line(s) when selection(s) are empty -- Fix block corruption when deleting block content using deleteLine command -- Add PowerShell and Diff language modes -- "Always on top" setting which makes Heynote stay on top of other programs - -### 1.6.0 - -- Added support for having Heynote in the Mac Menu Bar / Tray icon -- Ability to specify file system location of Heynote's buffer file. The buffer will automatically be reloaded if changed on disk, so this should make it possible to have the buffer automatically synced between machines using a file-syncing service such as Dropbox. -- Custom font and font size support. -- More key-binding for creating new blocks -- Syntax hightlighting support for new languages: - * Swift - * Kotlin - * Groovy -- Auto-close brackets functionality that can be turned on in settings -- Ability to change how calculations are formatted in Math blocks. See the [Docs](https://heynote.com/docs/#user-content-changing-how-the-results-of-math-blocks-are-formatted) for info on how to do this. -- There's now a Heynote webapp at [app.heynote.com](https://app.heynote.com). It's still work-in-progress, but should be usable. The buffer is stored in localStorage. -- Multiple bug fixes and minor improvement. - - -### 1.5.0 - -- Add support for the following languages - * TypeScript - * JSX - * TSX - * TOML - * C# - * Clojure - * Erlang - * Golang - * Lezer - * Ruby - * Shell - * YAML -- Various bug fixes and improvements - -### 1.4.1 - -- Fixed issue that would sometimes cause auto formatting to freeze the app for long periods. - -### 1.4.0 - -- Added ability to set a global hotkey for showing/hiding Heynote. From 122558e70aa9b3bab5474d0729f7dfae7a96799a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 12:05:08 +0200 Subject: [PATCH 31/65] Add link to Changelog in the docs --- docs/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 52f7c2fe..aa5c085d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,8 @@ # Heynote Documentation -Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc. +[Changelog](https://heynote.com/docs/changelog/) + +Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc. The Heynote buffer is divided into blocks, and each block can have its own Language set (e.g. JavaScript, JSON, Markdown, etc.). This gives you syntax highlighting and lets you auto-format that JSON response. From 53cfcdb3a9052d34379cc597aed73f31a79c7ef5 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 12:06:11 +0200 Subject: [PATCH 32/65] Use relative URL for changelog link --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index aa5c085d..10889413 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Heynote Documentation -[Changelog](https://heynote.com/docs/changelog/) +[Changelog](/docs/changelog/) Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc. From dd8f1aecb863d90b7a3aee047d03437a16aef0f0 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 12 Sep 2024 11:42:38 +0200 Subject: [PATCH 33/65] Remove C-p and C-n key bindings from Emacs mode (since they interfer with new key bindings for multiple notes support) --- src/editor/emacs.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/editor/emacs.js b/src/editor/emacs.js index b0022330..53fdfb23 100644 --- a/src/editor/emacs.js +++ b/src/editor/emacs.js @@ -103,8 +103,6 @@ export function emacsKeymap(editor) { { key: "Ctrl-b", run: emacsMoveCommand(cursorCharLeft, selectCharLeft), shift: selectCharLeft }, { key: "Ctrl-f", run: emacsMoveCommand(cursorCharRight, selectCharRight), shift: selectCharRight }, - { key: "Ctrl-p", run: emacsMoveCommand(cursorLineUp, selectLineUp), shift: selectLineUp }, - { key: "Ctrl-n", run: emacsMoveCommand(cursorLineDown, selectLineDown), shift: selectLineDown }, { key: "Ctrl-a", run: emacsMoveCommand(cursorLineStart, selectLineStart), shift: selectLineStart }, { key: "Ctrl-e", run: emacsMoveCommand(cursorLineEnd, selectLineEnd), shift: selectLineEnd }, ])), From 1102b89b429d316fca0ded7dd999a62b76d8ae21 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 12 Sep 2024 12:05:31 +0200 Subject: [PATCH 34/65] Remove debug log --- electron/main/file-library.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index 987b8ffa..4009dd84 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -142,7 +142,7 @@ export class FileLibrary { encoding: "utf8", }, async (eventType, changedPath) => { - console.log("File changed", eventType, changedPath) + //console.log("File changed", eventType, changedPath) //if (changedPath.toLowerCase().endsWith(".txt")) { // console.log("txt", this.notes) // if (await this.exists(changedPath)) { From 03dd4d29342c267658edd7e6ac6038a75264a513 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 3 Oct 2024 09:47:48 +0200 Subject: [PATCH 35/65] Remove unused import --- electron/main/file-library.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index 4009dd84..b387c262 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -1,6 +1,6 @@ import fs from "fs" import os from "node:os" -import { join, dirname, basename } from "path" +import { join, basename } from "path" import * as jetpack from "fs-jetpack"; import { app, ipcMain, dialog } from "electron" From bd93ecc92dbefd50403e6b2e312d539cb85f5ee6 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 6 Oct 2024 12:01:17 +0200 Subject: [PATCH 36/65] Bump version to 2.0.0-alpha --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5c6e6f7..520d4ea5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Heynote", - "version": "1.8.0", + "version": "2.0.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Heynote", - "version": "1.8.0", + "version": "2.0.0-alpha", "license": "Commons Clause MIT", "dependencies": { "@sindresorhus/slugify": "^2.2.1", diff --git a/package.json b/package.json index 58969118..1a4b2bc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Heynote", - "version": "1.8.0", + "version": "2.0.0-alpha", "main": "dist-electron/main/index.js", "description": "A dedicated scratch pad", "author": "Jonatan Heyman (https://heyman.info)", From 2081b85613c3db746a67729f4aa4e0b6128f9ac1 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 6 Oct 2024 12:01:55 +0200 Subject: [PATCH 37/65] Build app and upload artifacts when commit message contains #build --- .github/workflows/build.yml | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d477a358..6fd85174 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: push jobs: publish: runs-on: ${{ matrix.os }} - if: ${{ startsWith(github.ref, 'refs/tags/v') }} + if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.head_commit.message, '#build') }} permissions: contents: write @@ -50,8 +50,8 @@ jobs: # If the commit is tagged with a version (e.g. "v1.0.0"), # release the app after building - release: true - #release: ${{ startsWith(github.ref, 'refs/tags/v') }} + #release: true + release: ${{ startsWith(github.ref, 'refs/tags/v') }} env: # macOS notarization API key APPLE_API_KEY: ~/private_keys/AuthKey.p8 @@ -61,3 +61,26 @@ jobs: #- name: Print notarization-error.log # run: cat notarization-error.log + - name: Upload Linux artifact + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: actions/upload-artifact@v3 + with: + name: heynote-linux-${{ github.sha }} + path: release/*/Heynote_*.AppImage + retention-days: 30 + + - name: Upload Mac artifact + if: ${{ matrix.os == 'macos-latest' }} + uses: actions/upload-artifact@v3 + with: + name: heynote-mac-universal-${{ github.sha }} + path: release/*/mac-universal/Heynote.app + retention-days: 30 + + - name: Upload Windows artifact + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/upload-artifact@v3 + with: + name: heynote-windows-${{ github.sha }} + path: release/*/Heynote_*.exe + retention-days: 30 From 9b6a884258ac0105bf7ff95134ee3525a6541d7b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 6 Oct 2024 16:57:09 +0200 Subject: [PATCH 38/65] Change which Mac file is put into build artifact #build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fd85174..eaf16bc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,7 +74,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: heynote-mac-universal-${{ github.sha }} - path: release/*/mac-universal/Heynote.app + path: release/*/Heynote_*_arm64.dmg retention-days: 30 - name: Upload Windows artifact From a99a9ff032084f0521df626201b501964cf4342e Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 7 Oct 2024 13:01:40 +0200 Subject: [PATCH 39/65] Fix artifact file name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eaf16bc4..fc4cbf61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,7 +73,7 @@ jobs: if: ${{ matrix.os == 'macos-latest' }} uses: actions/upload-artifact@v3 with: - name: heynote-mac-universal-${{ github.sha }} + name: heynote-mac-arm64-${{ github.sha }} path: release/*/Heynote_*_arm64.dmg retention-days: 30 From 47d8da190d3a9d443c051104a9f74725417465de Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 28 Oct 2024 13:56:27 +0100 Subject: [PATCH 40/65] Don't trigger buffer:change event when file is removed --- electron/main/file-library.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index b387c262..7dc61432 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -164,7 +164,8 @@ export class FileLibrary { for (const [path, buffer] of Object.entries(this.files)) { if (changedPath === basename(path)) { const content = await buffer.read() - if (buffer._lastSavedContent !== content) { + // if the file was removed (e.g. during a atomic save) the content will be undefined + if (content !== undefined && buffer._lastSavedContent !== content) { win.webContents.send("buffer:change", path, content) } } From 5cf766b7fc0c5e1d4c58904a6bccc9cdceabd7cd Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 28 Oct 2024 13:57:20 +0100 Subject: [PATCH 41/65] #build --- electron/main/file-library.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index 7dc61432..b8420aa8 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -342,4 +342,4 @@ export async function migrateBufferFileToLibrary(app) { await ensureBufferFileMetadata(newFileFullPath) } } -} \ No newline at end of file +} From d11e90d0759c50617455e591167b8500458a9cd4 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 12:27:17 +0100 Subject: [PATCH 42/65] Check all open buffer files for changes when window gets focus The reason we do this is because fs.watch() is unreliable in some case. #build --- electron/main/file-library.js | 80 +++++++++++++++++++++++------------ electron/main/index.ts | 6 +-- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index b8420aa8..a92662c6 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -41,7 +41,8 @@ async function readNoteMetadata(filePath) { export class FileLibrary { - constructor(basePath) { + constructor(basePath, win) { + this.win = win basePath = untildify(basePath) if (jetpack.exists(basePath) !== "dir") { throw new Error(`Path directory does not exist: ${basePath}`) @@ -52,6 +53,7 @@ export class FileLibrary { this.watcher = null; this.contentSaved = false this.onChangeCallback = null + this._onWindowFocus = null // create scratch.txt if it doesn't exist if (!this.jetpack.exists(SCRATCH_FILE_NAME)) { @@ -69,7 +71,7 @@ export class FileLibrary { } const fullPath = fs.realpathSync(join(this.basePath, path)) this.files[path] = new NoteBuffer({fullPath, library:this}) - return await this.files[path].read() + return await this.files[path].load() } async save(path, content) { @@ -132,7 +134,7 @@ export class FileLibrary { return directories } - setupWatcher(win) { + setupWatcher() { if (!this.watcher) { this.watcher = fs.watch( this.basePath, @@ -143,35 +145,28 @@ export class FileLibrary { }, async (eventType, changedPath) => { //console.log("File changed", eventType, changedPath) - //if (changedPath.toLowerCase().endsWith(".txt")) { - // console.log("txt", this.notes) - // if (await this.exists(changedPath)) { - // console.log("file exists!") - // const newMetadata = await readNoteMetadata(join(this.basePath, changedPath)) - // if (!(changedPath in this.notes) || newMetadata.name !== this.notes[changedPath].name) { - // this.notes[changedPath] = newMetadata - // win.webContents.send("buffer:noteMetadataChanged", changedPath, newMetadata) - // console.log("metadata changed") - // } else { - // console.log("no metadata change") - // } - // } else if (changedPath in this.notes) { - // console.log("note removed", changedPath) - // delete this.notes[changedPath] - // win.webContents.send("buffer:noteRemoved", changedPath) - // } - //} for (const [path, buffer] of Object.entries(this.files)) { if (changedPath === basename(path)) { - const content = await buffer.read() - // if the file was removed (e.g. during a atomic save) the content will be undefined - if (content !== undefined && buffer._lastSavedContent !== content) { - win.webContents.send("buffer:change", path, content) + const content = await buffer.loadIfChanged() + if (content !== null) { + this.win.webContents.send("buffer:change", path, content) } } } } ) + + // fs.watch() is unreliable in some cases, e.g. OneDrive on Windows. Therefor we'll load the open buffer files + // and check for changes when the window gets focus. + this._onWindowFocus = async (event) => { + for (const [path, buffer] of Object.entries(this.files)) { + const content = await buffer.loadIfChanged() + if (content !== null) { + this.win.webContents.send("buffer:change", path, content) + } + } + } + this.win.on("focus", this._onWindowFocus) } } @@ -193,6 +188,10 @@ export class FileLibrary { this.watcher.close() this.watcher = null } + if (this._onWindowFocus) { + this.win.off("focus", this._onWindowFocus) + this._onWindowFocus = null + } } } @@ -201,7 +200,7 @@ export class FileLibrary { export class NoteBuffer { constructor({fullPath, library}) { this.fullPath = fullPath - this._lastSavedContent = null + this._lastKnownContent = null this.library = library } @@ -209,8 +208,33 @@ export class NoteBuffer { return await this.library.jetpack.read(this.fullPath, 'utf8') } + /** + * load() assumes that the actual note buffer is actually updated with the new content, otherwise + * _lastKnownContent will be out of sync. If you just want to read the content, use read() instead. + */ + async load() { + const content = await this.read() + this._lastKnownContent = content + return content + } + + /** + * loadIfChanged() will only return the content if it has changed since the last time it was loaded. + * If content is returned, the note buffer must be updated with the new content in order to keep the + * _lastKnownContent in sync. + */ + async loadIfChanged() { + const content = await this.read() + // if the file was removed (e.g. during an atomic save) the content will be undefined + if (content !== undefined && this._lastKnownContent !== content) { + this._lastKnownContent = content + return content + } + return null + } + async save(content) { - this._lastSavedContent = content + this._lastKnownContent = content const saveResult = await this.library.jetpack.write(this.fullPath, content, { atomic: true, mode: '600', @@ -227,7 +251,7 @@ export function setCurrentFileLibrary(lib) { library = lib } -export function setupFileLibraryEventHandlers(win) { +export function setupFileLibraryEventHandlers() { ipcMain.handle('buffer:load', async (event, path) => { //console.log("buffer:load", path) return await library.load(path) diff --git a/electron/main/index.ts b/electron/main/index.ts index c78a57f6..7c06d463 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -316,7 +316,7 @@ function registerAlwaysOnTop() { app.whenReady().then(createWindow).then(async () => { initFileLibrary(win).then(() => { - setupFileLibraryEventHandlers(win) + setupFileLibraryEventHandlers() }) initializeAutoUpdate(win) registerGlobalHotkey() @@ -375,8 +375,8 @@ async function initFileLibrary(win) { } try { - fileLibrary = new FileLibrary(libraryPath) - fileLibrary.setupWatcher(win) + fileLibrary = new FileLibrary(libraryPath, win) + fileLibrary.setupWatcher() } catch (error) { initErrors.push(`Error: ${error.message}`) } From 64331582e03dce6279f96e86ad7a4066be462b1f Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 13:34:59 +0100 Subject: [PATCH 43/65] Update docs and changelog --- docs/changelog.md | 5 +++-- docs/index.md | 25 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index ade702aa..0a399909 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,14 +6,15 @@ Here are the most notable changes in each release. For a more detailed list of c ### Support for multiple note buffers. -Apart from the default scratch buffer, you can now create and switch between multiple note buffers. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New note buffers are saved to the note library which is basically a directories (with sub dirs) on the disk with `.txt` files. You switch between note buffers by pressing `Ctrl/Cmd+P`. +Apart from the default Scratch note, you can now create and switch between multiple notes. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New notes are saved to the note library which is basically a directory (with sub dirs) on the disk with a `.txt` file for each note. You switch between Notes by pressing `Ctrl/Cmd+P`. -The first time you start the new version of Heynote, your existing buffer file will be migrated to the new note library. If you're using the default buffer location, that means that the existing scratch buffer file will be moved from `%APP_DIR%/buffer.txt` to `%APP_DIR%/notes/scratch.txt`. If you are using a custom buffer location the file will be moved from `%CUSTOM_DIR%/buffer.txt` to `%CUSTOM_DIR%/scratch.txt`. +The first time you start the new version of Heynote, your existing buffer file will be migrated to the new note library. If you're using the default buffer location, that means that the existing Scratch note file will be moved from `%APP_DIR%/buffer.txt` to `%APP_DIR%/notes/scratch.txt`. If you are using a custom buffer location the existing scratch file will be moved from `%CUSTOM_DIR%/buffer.txt` to `%CUSTOM_DIR%/scratch.txt`. ### Other changes - The file format for the buffer files has been updated to include some JSON metadata at the top of the file. - The cursor(s) location is saved between sessions. +- Improvements when using a file syncing service (e.g. Dropbox, OneDrive) to sync the note library between machines. - The setting for changing the color theme is now located in the program settings, instead of in the status bar. ## 1.8.0 diff --git a/docs/index.md b/docs/index.md index 10889413..dda262b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -101,16 +101,27 @@ format(x) = x.toLocaleString(); format(x) = x.toLocaleString('en-GB'); ``` - See the [Math.js format()](https://mathjs.org/docs/reference/functions/format.html) function for more info on what's supported. -## The buffer file -The default paths for the buffer data for the respective operating systems are: +## The notes library (only for Heynote 2.0, not yet released) + +The notes library is a directory (with sub dirs) on the disk with a `.txt` file for each note. It's created the first time you start Heynote, with the default note file `scratch.txt` in it. The default location for the library is: + +- Mac: `~/Library/Application Support/Heynote/notes/` +- Windows: `%APPDATA%\Heynote\notes\` +- Linux: `~/.config/Heynote/notes/` + +You can change the path of the notes library in the settings. Heynote expects reasonably fast disk access to the notes library, so it's not recommended to use a network drive, though file syncing services like Dropbox, OneDrive, etc. should work (see below). + +## Synchronizing the notes library + +Heynote is built to support synchronizing the notes library (or buffer file in the case of Heynote 1.x) through file-syncing services like Dropbox, OneDrive, etc. However, note that the synchronization logic is quite simple, so editing the same note on two different machines at the same time might lead to conflicts and unexpected results. + +When using a file synching service that support "offloading" of files in the cloud (removing them from the disk), it's recommended to mark the notes library as "always available offline". + +As always, backups things that are important. -- Mac: `~/Library/Application Support/Heynote/buffer.txt` -- Windows: `%APPDATA%\Heynote\buffer.txt` -- Linux: `~/.config/Heynote/buffer.txt` ## Linux @@ -124,7 +135,7 @@ libnss3 libnspr4 ``` -#### Wayland +### Wayland Due to [an issue in Electron](https://github.com/electron/electron/issues/38288), the global hotkey will not work in all applications running under Wayland. In KDE it is possible to work around this limitation by adding this Kwin script: From cff192c3ed413ddf77dd6df84728be932682de83 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 13:43:03 +0100 Subject: [PATCH 44/65] Add link to changelog and minor formatting change --- README.md | 1 + docs/index.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94c55b38..26cfc944 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - Website: [heynote.com](https://heynote.com) - Documentation: [heynote.com](https://heynote.com/docs/) +- Changelog: [heynote.com](https://heynote.com/docs/changelog/) Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc. diff --git a/docs/index.md b/docs/index.md index dda262b2..0e946469 100644 --- a/docs/index.md +++ b/docs/index.md @@ -114,7 +114,7 @@ The notes library is a directory (with sub dirs) on the disk with a `.txt` file You can change the path of the notes library in the settings. Heynote expects reasonably fast disk access to the notes library, so it's not recommended to use a network drive, though file syncing services like Dropbox, OneDrive, etc. should work (see below). -## Synchronizing the notes library +### Synchronizing the notes library Heynote is built to support synchronizing the notes library (or buffer file in the case of Heynote 1.x) through file-syncing services like Dropbox, OneDrive, etc. However, note that the synchronization logic is quite simple, so editing the same note on two different machines at the same time might lead to conflicts and unexpected results. From 60e9ec7265266d4e6ad5fefe3e5fcda97fc66316 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 13:48:36 +0100 Subject: [PATCH 45/65] Fix typo --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 0e946469..2aaa4739 100644 --- a/docs/index.md +++ b/docs/index.md @@ -120,7 +120,7 @@ Heynote is built to support synchronizing the notes library (or buffer file in t When using a file synching service that support "offloading" of files in the cloud (removing them from the disk), it's recommended to mark the notes library as "always available offline". -As always, backups things that are important. +As always, backup things that are important. ## Linux @@ -161,3 +161,4 @@ registerShortcut('toggleHeynote', 'Toggle Heynote', 'Ctrl+Shift+H', toggleHeynot See the [KWin scripting tutorial](https://develop.kde.org/docs/plasma/kwin/) for instructions on how to install the script. Remember to enable the script in the KDE System Settings. It may also be necessary to go into the KDE System Settings and bind the "Toggle Heynote" key manually. + From c153e95ba1ba388cf8cf975bddd3f3c36f01cc34 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 14:08:33 +0100 Subject: [PATCH 46/65] Add note about breaking change (default scratch file path changed) to changelog --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 0a399909..bf27f981 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,10 @@ Here are the most notable changes in each release. For a more detailed list of c ## 2.0.0-beta (not yet released) +### IMPORTANT (breaking change) + +The default path of the scratch file has changed. If you are running a previous version of Heynote with the buffer file synchronized across multiple machines using a file synching service such as Dropbox or OneDrive, you should make sure to upgrade all machines to Heynote 2.0 at the same time (closing Heynote before) in order for the file to stay synched, since the file path for the buffer file has changed. See below for more info. + ### Support for multiple note buffers. Apart from the default Scratch note, you can now create and switch between multiple notes. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New notes are saved to the note library which is basically a directory (with sub dirs) on the disk with a `.txt` file for each note. You switch between Notes by pressing `Ctrl/Cmd+P`. From 43e5613527e092d90fbf17aac82cf7c534f479d1 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 14:12:18 +0100 Subject: [PATCH 47/65] Show error message if failing to unserialize note data (e.g. if a note's format has a major version that is unsupported) --- src/editor/editor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/editor.js b/src/editor/editor.js index fb23d9c8..3d4d77d4 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -23,6 +23,7 @@ import { todoCheckboxPlugin} from "./todo-checkbox.ts" import { links } from "./links.js" import { NoteFormat } from "../common/note-format.js" import { useNotesStore } from "../stores/notes-store.js"; +import { useErrorStore } from "../stores/error-store.js"; function getKeymapExtensions(editor, keymap) { @@ -66,6 +67,7 @@ export class HeynoteEditor { this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect) this.contentLoaded = false this.notesStore = useNotesStore() + this.errorStore = useErrorStore() this.name = "" @@ -178,6 +180,7 @@ export class HeynoteEditor { this.setReadOnly(false) } catch (e) { this.setReadOnly(true) + this.errorStore.addError(`Failed to load note: ${e.message}`) throw new Error(`Failed to load note: ${e.message}`) } this.name = this.note.metadata?.name || this.path From 94eb33ce9238ca06bccf8bd5084405f19d25f347 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 14:14:20 +0100 Subject: [PATCH 48/65] Remove debug logging #build --- src/components/Editor.vue | 6 +++--- src/editor/editor.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 5358d16b..6003e5b1 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -169,18 +169,18 @@ ...mapActions(useEditorCacheStore, ["getEditor", "addEditor", "eachEditor"]), loadBuffer(path) { - console.log("loadBuffer", path) + //console.log("loadBuffer", path) if (this.editor) { this.editor.hide() } let cachedEditor = this.getEditor(path) if (cachedEditor) { - console.log("show cached editor") + //console.log("show cached editor") this.editor = cachedEditor toRaw(this.editor).show() } else { - console.log("create new editor") + //console.log("create new editor") try { this.editor = new HeynoteEditor({ element: this.$refs.editor, diff --git a/src/editor/editor.js b/src/editor/editor.js index 3d4d77d4..4a3f3ac9 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -343,11 +343,11 @@ export class HeynoteEditor { } hide() { - console.log("hiding element", this.view.dom) + //console.log("hiding element", this.view.dom) this.view.dom.style.setProperty("display", "none", "important") } show() { - console.log("showing element", this.view.dom) + //console.log("showing element", this.view.dom) this.view.dom.style.setProperty("display", "") triggerCursorChange(this.view) } From 0a963dd1b9a8c23076cb6883a9eff9aeeeaad6d4 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 15:30:10 +0100 Subject: [PATCH 49/65] Use FileLibrary.load() method to make sure tracking of current buffer content works --- electron/main/file-library.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index a92662c6..a21e7e61 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -67,7 +67,7 @@ export class FileLibrary { async load(path) { if (this.files[path]) { - return this.files[path].read() + return this.files[path].load() } const fullPath = fs.realpathSync(join(this.basePath, path)) this.files[path] = new NoteBuffer({fullPath, library:this}) From 0c84e11340d0828713223a5b8c9a6d61b8456b38 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 16:19:40 +0100 Subject: [PATCH 50/65] Add attributes to prevent auto completion (for web app version) --- src/components/NewNote.vue | 2 ++ src/components/NoteSelector.vue | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 224b9e54..6581a359 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -160,6 +160,8 @@ ref="nameInput" @keydown="onInputKeydown" @input="errors.name = false" + autocomplete="off" + data-1p-ignore /> diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index 58058e69..1bbe95a1 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -217,6 +217,7 @@ @keydown="onKeydown" @input="onInput" v-model="filter" + autocomplete="off" />
    • Date: Tue, 29 Oct 2024 16:20:20 +0100 Subject: [PATCH 51/65] Fix issue where Escape key wouldn't work in Note selector when there was text in the input causing no Notes to match --- src/components/NoteSelector.vue | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index 1bbe95a1..ca637838 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -98,9 +98,21 @@ }, onKeydown(event) { + if (event.key === "Escape") { + console.log("escape") + event.preventDefault() + if (this.actionButton !== 0) { + this.hideActionButtons() + } else { + this.$emit("close") + } + return + } + if (this.filteredItems.length === 0) { return } + const path = this.filteredItems[this.selected].path if (event.key === "ArrowDown") { if (this.selected === this.filteredItems.length - 1) { @@ -145,13 +157,6 @@ } else { this.selectItem(path) } - } else if (event.key === "Escape") { - event.preventDefault() - if (this.actionButton !== 0) { - this.hideActionButtons() - } else { - this.$emit("close") - } } }, From e242dc506cf61cdf6c7275bce188c29196a6e5a4 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 16:47:55 +0100 Subject: [PATCH 52/65] Add support for mutiple notes to web app --- webapp/bridge.js | 70 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/webapp/bridge.js b/webapp/bridge.js index 00db2e93..89789358 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -75,6 +75,26 @@ if (settingsData !== null) { } +const NOTE_KEY_PREFIX = "heynote-library__" + +function noteKey(path) { + return NOTE_KEY_PREFIX + path +} + +function getNoteMetadata(content) { + const firstSeparator = content.indexOf("\n∞∞∞") + if (firstSeparator === -1) { + return null + } + try { + const metadata = JSON.parse(content.slice(0, firstSeparator).trim()) + return {"name": metadata.name} + } catch (e) { + return {} + } +} + + const Heynote = { platform: platform, defaultFontFamily: "Hack", @@ -82,17 +102,28 @@ const Heynote = { buffer: { async load(path) { - const content = localStorage.getItem(path) - return content === null ? "\n∞∞∞text-a\n" : content + //console.log("loading", path) + const content = localStorage.getItem(noteKey(path)) + return content === null ? '{"formatVersion":"1.0.0","name":"Scratch"}\n∞∞∞text-a\n' : content }, async save(path, content) { - console.log("saving", path, content) - localStorage.setItem(path, content) + //console.log("saving", path, content) + localStorage.setItem(noteKey(path), content) }, async create(path, content) { - throw Exception("Not implemented") + localStorage.setItem(noteKey(path), content) + }, + + async delete(path) { + localStorage.removeItem(noteKey(path)) + }, + + async move(path, newPath) { + const content = localStorage.getItem(noteKey(path)) + localStorage.setItem(noteKey(newPath), content) + localStorage.removeItem(noteKey(path)) }, async saveAndQuit(contents) { @@ -100,15 +131,36 @@ const Heynote = { }, async exists(path) { - return true + return localStorage.getItem(noteKey(path)) !== null }, async getList(path) { - return [{"path":"buffer.txt", "metadata":{}}] + //return {"scratch.txt": {name:"Scratch"}} + const notes = {} + for (let [key, content] of Object.entries(localStorage)) { + if (key.startsWith(NOTE_KEY_PREFIX)) { + const path = key.slice(NOTE_KEY_PREFIX.length) + notes[path] = getNoteMetadata(content) + } + } + return notes }, async getDirectoryList() { - return [] + const directories = new Set() + for (let key in localStorage) { + if (key.startsWith(NOTE_KEY_PREFIX)) { + const path = key.slice(NOTE_KEY_PREFIX.length) + const parts = path.split("/") + if (parts.length > 1) { + for (let i = 1; i < parts.length; i++) { + directories.add(parts.slice(0, i).join("/")) + } + } + } + } + //console.log("directories", directories) + return [...directories] }, async close(path) { @@ -147,7 +199,7 @@ const Heynote = { set: (mode) => { localStorage.setItem("theme", mode) themeCallback(mode) - console.log("set theme to", mode) + //console.log("set theme to", mode) }, get: async () => { const theme = localStorage.getItem("theme") || "system" From 87246e2cd3fb9a90578811f1afbb03c1a0368279 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 16:48:15 +0100 Subject: [PATCH 53/65] Add code comment --- electron/main/file-library.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electron/main/file-library.js b/electron/main/file-library.js index a21e7e61..69995234 100644 --- a/electron/main/file-library.js +++ b/electron/main/file-library.js @@ -125,6 +125,9 @@ export class FileLibrary { return notes } + /** + * @returns {Array} List of path to all directories, but not the root directory. + */ async getDirectoryList() { const directories = await this.jetpack.findAsync("", { files: false, From 1db631e68e95e1f3b3e9b680ba1c44033460e943 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 17:03:36 +0100 Subject: [PATCH 54/65] Web app: Migrate existing localStorage single buffer to buffer library --- webapp/bridge.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/webapp/bridge.js b/webapp/bridge.js index 89789358..41e1af89 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -1,5 +1,6 @@ import { Exception } from "sass"; import { SETTINGS_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from "../electron/constants"; +import { NoteFormat } from "../src/common/note-format"; const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)') let themeCallback = null @@ -94,6 +95,33 @@ function getNoteMetadata(content) { } } +// Migrate single buffer (Heynote pre 2.0) in localStorage to notes library +// At some point we can remove this migration code +function migrateBufferFileToLibrary() { + if (!("buffer" in localStorage)) { + // nothing to migrate + return + } + if (Object.keys(localStorage).filter(key => key.startsWith(NOTE_KEY_PREFIX)).length > 0) { + // already migrated + return + } + + console.log("Migrating single buffer to notes library") + + let content = localStorage.getItem("buffer") + const metadata = getNoteMetadata(content) + if (!metadata || !metadata.name) { + console.log("Adding metadata to Scratch note") + const note = NoteFormat.load(content) + note.metadata.name = "Scratch" + content = note.serialize() + } + localStorage.setItem("heynote-library__scratch.txt", content) + localStorage.removeItem("buffer") +} +migrateBufferFileToLibrary() + const Heynote = { platform: platform, From 8ab16ea21e1c00feae6ee391be3699f54b8f5379 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 17:19:14 +0100 Subject: [PATCH 55/65] Add __TESTS__ variable to web app when the tests are running --- webapp/bridge.js | 1 - webapp/vite.config.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/bridge.js b/webapp/bridge.js index 41e1af89..0d8adb36 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -122,7 +122,6 @@ function migrateBufferFileToLibrary() { } migrateBufferFileToLibrary() - const Heynote = { platform: platform, defaultFontFamily: "Hack", diff --git a/webapp/vite.config.js b/webapp/vite.config.js index 7191747b..748e3ead 100644 --- a/webapp/vite.config.js +++ b/webapp/vite.config.js @@ -26,7 +26,6 @@ const middleware = () => { } } - // https://vitejs.dev/config/ export default defineConfig({ publicDir: "../public", @@ -54,5 +53,6 @@ export default defineConfig({ define: { '__APP_VERSION__': JSON.stringify(process.env.npm_package_version), '__GIT_HASH__': JSON.stringify(child.execSync('git rev-parse --short HEAD').toString().trim()), + '__TESTS__': process.env.HEYNOTE_TESTS, }, }) From 2f6ba8c91af68add35053e7c7e24cb148bb42f08 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 18:37:42 +0100 Subject: [PATCH 56/65] Fix UI bug in web app For some reason, the color of action buttons in NoteSelector was black in dark mode --- src/components/NoteSelector.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index ca637838..8b8b474c 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -336,6 +336,8 @@ color: rgba(255,255,255, 0.65) &:hover background: #29292a + .action-buttons button + color: #fff &.selected background: #48b57e color: #fff From 4223937fbf3e46e00e265eb5412b08afe0bbcbfa Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Dec 2024 16:49:01 +0100 Subject: [PATCH 57/65] Fix issue of Enter keystroke within New Note dialog being picked up by Editor (in web app) --- src/components/NewNote.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 6581a359..96bc77ac 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -92,6 +92,8 @@ this.$emit("close") event.preventDefault() } if (event.key === "Enter") { + // without preventDefault, the editor will receive a Enter keydown event on webapp (not in Electron) + event.preventDefault() this.submit() } }, From cf389c791c6be2a0467a457f02679cec263db070 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Dec 2024 16:52:35 +0100 Subject: [PATCH 58/65] Add tests for creating new Note buffers --- src/common/constants.js | 1 + src/editor/block/commands.js | 1 + src/editor/editor.js | 3 +- tests/buffer-creation.spec.js | 69 +++++++++++++++++++++++++++++++++++ tests/test-utils.js | 12 ++++++ webapp/bridge.js | 7 ++-- 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 tests/buffer-creation.spec.js diff --git a/src/common/constants.js b/src/common/constants.js index 42c17667..a1359aa6 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -1 +1,2 @@ export const SCRATCH_FILE_NAME = "scratch.txt" +export const AUTO_SAVE_INTERVAL = 2000 diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js index c59ce51d..fe9bf3bb 100644 --- a/src/editor/block/commands.js +++ b/src/editor/block/commands.js @@ -1,4 +1,5 @@ import { EditorSelection, Transaction } from "@codemirror/state" + import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, DELETE_BLOCK } from "../annotation.js"; import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block" import { moveLineDown, moveLineUp } from "./move-lines.js"; diff --git a/src/editor/editor.js b/src/editor/editor.js index 4a3f3ac9..0199c140 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -22,6 +22,7 @@ import { autoSaveContent } from "./save.js" import { todoCheckboxPlugin} from "./todo-checkbox.ts" import { links } from "./links.js" import { NoteFormat } from "../common/note-format.js" +import { AUTO_SAVE_INTERVAL } from "../common/constants.js" import { useNotesStore } from "../stores/notes-store.js"; import { useErrorStore } from "../stores/error-store.js"; @@ -105,7 +106,7 @@ export class HeynoteEditor { return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"} }), - autoSaveContent(this, 2000), + autoSaveContent(this, AUTO_SAVE_INTERVAL), todoCheckboxPlugin, markdown(), diff --git a/tests/buffer-creation.spec.js b/tests/buffer-creation.spec.js new file mode 100644 index 00000000..a42705c0 --- /dev/null +++ b/tests/buffer-creation.spec.js @@ -0,0 +1,69 @@ +import {expect, test} from "@playwright/test"; +import {HeynotePage} from "./test-utils.js"; + +import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js" +import { NoteFormat } from "../src/common/note-format.js" +import exp from "constants"; + +let heynotePage + +test.beforeEach(async ({page}) => { + heynotePage = new HeynotePage(page) + await heynotePage.goto() + + expect((await heynotePage.getBlocks()).length).toBe(1) + await heynotePage.setContent(` +∞∞∞text +Block A +∞∞∞text +Block B +∞∞∞text +Block C`) + await page.waitForTimeout(100); + // check that blocks are created + expect((await heynotePage.getBlocks()).length).toBe(3) + + // check that visual block layers are created + await expect(page.locator("css=.heynote-blocks-layer > div")).toHaveCount(3) +}); + + +test("default buffer saved", async ({page}) => { + // make some change and make sure content is auto saved in default scratch buffer + await page.locator("body").pressSequentially("YAY") + await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50); + const bufferList = await heynotePage.getStoredBufferList() + expect(Object.keys(bufferList).length).toBe(1) + expect(bufferList["scratch.txt"]).toBeTruthy() +}) + +test("create new buffer from block", async ({page}) => { + await page.locator("body").press(heynotePage.agnosticKey("Mod+S")) + await page.waitForTimeout(50) + await page.locator("body").pressSequentially("My New Buffer") + await page.locator("body").press("Enter") + await page.waitForTimeout(50) + await page.locator("body").press("Enter") + await page.locator("body").pressSequentially("New buffer content") + await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50); + + const buffers = Object.keys(await heynotePage.getStoredBufferList()) + expect(buffers).toContain("scratch.txt") + expect(buffers).toContain("my-new-buffer.txt") + + const defaultBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("scratch.txt")) + const newBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("my-new-buffer.txt")) + + + expect(defaultBuffer.content).toBe(` +∞∞∞text +Block A +∞∞∞text +Block B`) + + expect(newBuffer.content).toBe(` +∞∞∞text +Block C +New buffer content`) + +}) diff --git a/tests/test-utils.js b/tests/test-utils.js index aa59fb61..209701ac 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -56,4 +56,16 @@ export class HeynotePage { async getStoredSettings() { return await this.page.evaluate(() => JSON.parse(window.localStorage.getItem("settings"))) } + + async getStoredBufferList() { + return await this.page.evaluate(() => window.heynote.buffer.getList()) + } + + async getStoredBuffer(path) { + return await this.page.evaluate((path) => window.heynote.buffer.load(path), path) + } + + agnosticKey(key) { + return key.replace("Mod", this.isMac ? "Meta" : "Control") + } } diff --git a/webapp/bridge.js b/webapp/bridge.js index 0d8adb36..48bf02e0 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -2,6 +2,8 @@ import { Exception } from "sass"; import { SETTINGS_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from "../electron/constants"; import { NoteFormat } from "../src/common/note-format"; +const NOTE_KEY_PREFIX = "heynote-library__" + const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)') let themeCallback = null mediaMatch.addEventListener("change", async (event) => { @@ -75,9 +77,6 @@ if (settingsData !== null) { initialSettings = Object.assign(initialSettings, JSON.parse(settingsData)) } - -const NOTE_KEY_PREFIX = "heynote-library__" - function noteKey(path) { return NOTE_KEY_PREFIX + path } @@ -161,7 +160,7 @@ const Heynote = { return localStorage.getItem(noteKey(path)) !== null }, - async getList(path) { + async getList() { //return {"scratch.txt": {name:"Scratch"}} const notes = {} for (let [key, content] of Object.entries(localStorage)) { From 418f2fc4eef6edca070a7a7248ced9b3b3797634 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Dec 2024 20:58:24 +0100 Subject: [PATCH 59/65] Add support for creating new empty note buffers --- src/components/NewNote.vue | 19 ++++++++++++++++--- src/editor/editor.js | 16 ++++++++++++++-- src/editor/keymap.js | 3 ++- src/stores/notes-store.js | 15 +++++++++++++-- tests/buffer-creation.spec.js | 30 +++++++++++++++++++++++++++++- 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index 96bc77ac..b05a1d0d 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -67,6 +67,7 @@ ...mapState(useNotesStore, [ "notes", "currentNotePath", + "createNoteMode", ]), currentNoteDirectory() { @@ -78,12 +79,17 @@ "name-input": true, "error": this.errors.name, } - } + }, + + dialogTitle() { + return this.createNoteMode === "currentBlock" ? "New Note from Block" : "New Note" + }, }, methods: { ...mapActions(useNotesStore, [ "updateNotes", + "createNewNote", "createNewNoteFromActiveBlock", ]), @@ -141,7 +147,14 @@ return } console.log("Creating note", path) - this.createNewNoteFromActiveBlock(path, this.name) + if (this.createNoteMode === "currentBlock") { + this.createNewNoteFromActiveBlock(path, this.name) + } else if (this.createNoteMode === "new") { + this.createNewNote(path, this.name) + } else { + throw new Error("Unknown createNoteMode: " + this.createNoteMode) + } + this.$emit("close") //this.$emit("create", this.$refs.input.value) }, @@ -153,7 +166,7 @@
      -

      New Note from Block

      +

      {{ dialogTitle }}

      { + this.notesStore.openNote(path) + }) } async createNewNoteFromActiveBlock(path, name) { diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 9b077b13..eb7a229d 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -59,7 +59,8 @@ export function heynoteKeymap(editor) { ["Alt-ArrowDown", moveLineDown], ["Mod-l", () => editor.openLanguageSelector()], ["Mod-p", () => editor.openNoteSelector()], - ["Mod-s", () => editor.openCreateNote()], + ["Mod-s", () => editor.openCreateNote("currentBlock")], + ["Mod-n", () => editor.openCreateNote("new")], ["Mod-Shift-d", deleteBlock(editor)], ["Alt-Shift-f", formatBlockContent], ["Mod-Alt-ArrowDown", newCursorBelow], diff --git a/src/stores/notes-store.js b/src/stores/notes-store.js index 5f815d8b..4f464378 100644 --- a/src/stores/notes-store.js +++ b/src/stores/notes-store.js @@ -18,6 +18,7 @@ export const useNotesStore = defineStore("notes", { currentCursorLine: null, currentSelectionSize: null, libraryId: 0, + createNoteMode: "new", showNoteSelector: false, showLanguageSelector: false, @@ -51,8 +52,10 @@ export const useNotesStore = defineStore("notes", { this.closeDialog() this.showNoteSelector = true }, - openCreateNote() { + openCreateNote(createMode) { + createMode = createMode || "new" this.closeDialog() + this.createNoteMode = createMode this.showCreateNote = true }, closeDialog() { @@ -75,12 +78,20 @@ export const useNotesStore = defineStore("notes", { }, /** - * Create a new note file at `path` with name `name` from the current block of the current open editor + * Create a new note file at `path` with name `name` from the current block of the current open editor, + * and switch to it */ async createNewNoteFromActiveBlock(path, name) { await toRaw(this.currentEditor).createNewNoteFromActiveBlock(path, name) }, + /** + * Create a new empty note file at `path` with name `name`, and switch to it + */ + async createNewNote(path, name) { + await toRaw(this.currentEditor).createNewNote(path, name) + }, + /** * Create a new note file at path, with name `name`, and content content * @param {*} path: File path relative to Heynote root diff --git a/tests/buffer-creation.spec.js b/tests/buffer-creation.spec.js index a42705c0..61b94b6e 100644 --- a/tests/buffer-creation.spec.js +++ b/tests/buffer-creation.spec.js @@ -54,7 +54,6 @@ test("create new buffer from block", async ({page}) => { const defaultBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("scratch.txt")) const newBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("my-new-buffer.txt")) - expect(defaultBuffer.content).toBe(` ∞∞∞text Block A @@ -67,3 +66,32 @@ Block C New buffer content`) }) + + +test("create new empty note", async ({page}) => { + await page.locator("body").press("Enter") + await page.locator("body").press("Backspace") + await page.locator("body").press(heynotePage.agnosticKey("Mod+N")) + await page.locator("body").pressSequentially("New Empty Buffer") + await page.locator("body").press("Enter") + await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50); + + const buffers = Object.keys(await heynotePage.getStoredBufferList()) + expect(buffers).toContain("scratch.txt") + expect(buffers).toContain("new-empty-buffer.txt") + + const defaultBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("scratch.txt")) + const newBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("new-empty-buffer.txt")) + + expect(defaultBuffer.content).toBe(` +∞∞∞text +Block A +∞∞∞text +Block B +∞∞∞text +Block C`) + + expect(newBuffer.content).toBe(` +∞∞∞text-a +`) +}) From 13292ab83596f62a30eb6fbf4cf4d9fa6ab68334 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 5 Dec 2024 23:20:13 +0100 Subject: [PATCH 60/65] Add "Create new..." item to the note selector dialog --- src/components/NewNote.vue | 21 +++- src/components/NoteSelector.vue | 212 ++++++++++++++++++-------------- src/stores/notes-store.js | 12 +- 3 files changed, 147 insertions(+), 98 deletions(-) diff --git a/src/components/NewNote.vue b/src/components/NewNote.vue index b05a1d0d..8998ae7b 100644 --- a/src/components/NewNote.vue +++ b/src/components/NewNote.vue @@ -24,7 +24,16 @@ }, async mounted() { - this.$refs.nameInput.focus() + if (!!this.createNoteParams.name) { + this.name = this.createNoteParams.name + this.$refs.nameInput.focus() + this.$nextTick(() => { + this.$refs.nameInput.select() + }) + } else { + this.$refs.nameInput.focus() + } + this.updateNotes() // build directory tree @@ -67,7 +76,7 @@ ...mapState(useNotesStore, [ "notes", "currentNotePath", - "createNoteMode", + "createNoteParams", ]), currentNoteDirectory() { @@ -82,7 +91,7 @@ }, dialogTitle() { - return this.createNoteMode === "currentBlock" ? "New Note from Block" : "New Note" + return this.createNoteParams.mode === "currentBlock" ? "New Note from Block" : "New Note" }, }, @@ -147,12 +156,12 @@ return } console.log("Creating note", path) - if (this.createNoteMode === "currentBlock") { + if (this.createNoteParams.mode === "currentBlock") { this.createNewNoteFromActiveBlock(path, this.name) - } else if (this.createNoteMode === "new") { + } else if (this.createNoteParams.mode === "new") { this.createNewNote(path, this.name) } else { - throw new Error("Unknown createNoteMode: " + this.createNoteMode) + throw new Error("Unknown createNote Mode: " + this.createNoteParams.mode) } this.$emit("close") diff --git a/src/components/NoteSelector.vue b/src/components/NoteSelector.vue index 8b8b474c..de36b5d2 100644 --- a/src/components/NoteSelector.vue +++ b/src/components/NoteSelector.vue @@ -60,13 +60,15 @@ }, filteredItems() { + let items if (this.filter === "") { - return this.orderedItems + items = this.orderedItems + } else { const searchResults = fuzzysort.go(this.filter, this.items, { keys: ["name", "folder"], }) - return searchResults.map((result) => { + items = searchResults.map((result) => { const obj = {...result.obj} const nameHighlight = result[0].highlight("", "") const folderHighlight = result[1].highlight("", "") @@ -75,6 +77,15 @@ return obj }) } + + const newNoteItem = { + name: "Create new…", + createNew:true, + } + return [ + ...items, + newNoteItem, + ] }, }, @@ -83,6 +94,7 @@ "updateNotes", "editNote", "deleteNote", + "openCreateNote", ]), buildItems() { @@ -99,7 +111,6 @@ onKeydown(event) { if (event.key === "Escape") { - console.log("escape") event.preventDefault() if (this.actionButton !== 0) { this.hideActionButtons() @@ -112,8 +123,8 @@ if (this.filteredItems.length === 0) { return } - - const path = this.filteredItems[this.selected].path + + const item = this.filteredItems[this.selected] if (event.key === "ArrowDown") { if (this.selected === this.filteredItems.length - 1) { this.selected = 0 @@ -121,11 +132,9 @@ this.selected = Math.min(this.selected + 1, this.filteredItems.length - 1) } event.preventDefault() - if (this.selected === this.filteredItems.length - 1) { - this.$refs.container.scrollIntoView({block: "end"}) - } else { - this.$refs.item[this.selected].scrollIntoView({block: "nearest"}) - } + this.$nextTick(() => { + this.$refs.container.querySelector(".selected").scrollIntoView({block: "nearest"}) + }) this.actionButton = 0 } else if (event.key === "ArrowUp") { if (this.selected === 0) { @@ -134,28 +143,32 @@ this.selected = Math.max(this.selected - 1, 0) } event.preventDefault() - if (this.selected === 0) { - this.$refs.container.scrollIntoView({block: "start"}) - } else { - this.$refs.item[this.selected].scrollIntoView({block: "nearest"}) - } + this.$nextTick(() => { + this.$refs.container.querySelector(".selected").scrollIntoView({block: "nearest"}) + }) this.actionButton = 0 - } else if (event.key === "ArrowRight" && path !== SCRATCH_FILE_NAME) { + } else if (event.key === "ArrowRight" && this.itemHasActionButtons(item)) { event.preventDefault() this.actionButton = Math.min(2, this.actionButton + 1) - } else if (event.key === "ArrowLeft" && path !== SCRATCH_FILE_NAME) { + } else if (event.key === "ArrowLeft" && this.itemHasActionButtons(item)) { event.preventDefault() this.actionButton = Math.max(0, this.actionButton - 1) this.deleteConfirm = false } else if (event.key === "Enter") { event.preventDefault() - if (this.actionButton === 1) { - console.log("edit file:", path) - this.editNote(path) + if (item.createNew) { + if (this.filteredItems.length === 1) { + this.openCreateNote("new", this.filter) + } else { + this.openCreateNote("new", "") + } + } else if (this.actionButton === 1) { + //console.log("edit file:", path) + this.editNote(item.path) } else if (this.actionButton === 2) { - this.deleteConfirmNote(path) + this.deleteConfirmNote(item.path) } else { - this.selectItem(path) + this.selectItem(item.path) } } }, @@ -164,6 +177,10 @@ this.$emit("openNote", path) }, + itemHasActionButtons(item) { + return !item.createNew && item.path !== SCRATCH_FILE_NAME + }, + onInput(event) { // reset selection this.selected = 0 @@ -178,9 +195,11 @@ getItemClass(item, idx) { return { + "item": true, "selected": idx === this.selected, "action-buttons-visible": this.actionButton > 0, "scratch": item.scratch, + "new-note": item.createNew, } }, @@ -198,7 +217,7 @@ async deleteConfirmNote(path) { if (this.deleteConfirm) { - console.log("delete file:", path) + //console.log("delete file:", path) await this.deleteNote(path) this.hideActionButtons() this.buildItems() @@ -214,8 +233,8 @@