From b368810ecc9c4688ccf0f54137577720457e1497 Mon Sep 17 00:00:00 2001 From: Ariel Barreiro Date: Fri, 16 Feb 2024 11:03:35 -0300 Subject: [PATCH 001/228] Document how to format number with local Relates to #201 --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 228dd3f..7463fef 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,13 @@ _format = format # store reference to the built in format format(x) = _format(x, {notation:"exponential"}) ``` +You can also do something like this to show the number with your default locale or providing a [custom one](https://www.w3.org/International/articles/language-tags/): + +``` +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. From 302ed1a0b895a713176325d0d82eb02e9897afdf Mon Sep 17 00:00:00 2001 From: Ariel Barreiro Date: Fri, 16 Feb 2024 11:04:57 -0300 Subject: [PATCH 002/228] Fix language --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7463fef..884cfd4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ _format = format # store reference to the built in format format(x) = _format(x, {notation:"exponential"}) ``` -You can also do something like this to show the number with your default locale or providing a [custom one](https://www.w3.org/International/articles/language-tags/): +You can also do something like this to show the number with your default locale or provide a [custom one](https://www.w3.org/International/articles/language-tags/): ``` format(x) = x.toLocaleString(); From 93e805152ef22a05f25f4b292a69030182cf8251 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 26 Feb 2024 18:18:36 +0100 Subject: [PATCH 003/228] Add .webmanifest to web app --- public/site.webmanifest | 11 +++++++++++ webapp/index.html | 1 + 2 files changed, 12 insertions(+) create mode 100644 public/site.webmanifest diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..a4b2915 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,11 @@ +{ + "name": "Heynote", + "short_name": "Heynote", + "icons": [ + { + "src": "/icon.ico", + "sizes": "256x256" + } + ], + "display": "standalone" +} diff --git a/webapp/index.html b/webapp/index.html index af26a93..fd0ecd0 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -5,6 +5,7 @@ + Heynote From 78e2bb2ec3f1928715b6c688f2c7362011da153c Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 25 Mar 2024 13:29:23 +0100 Subject: [PATCH 004/228] Add link to website and logo Signed-off-by: Jonatan Heyman --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 228dd3f..50569f2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/heyman/heynote)](https://github.com/heyman/heynote/releases) [![Build Status](https://github.com/heyman/heynote/workflows/Tests/badge.svg)](https://github.com/heyman/heynote/actions?query=workflow%3ATests) + + +**Website:** [heynote.com](https://heynote.com) 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 d943fc8015b063c38cedf492a86ba07839938963 Mon Sep 17 00:00:00 2001 From: Donghee Lee Date: Wed, 12 Jun 2024 07:47:45 +0200 Subject: [PATCH 005/228] Support Dart Syntax --- README.md | 2 +- public/langdetect-worker.js | 2 +- src/editor/lang-heynote/heynote.grammar | 2 +- src/editor/lang-heynote/parser.js | 2 +- src/editor/languages.js | 8 +++++++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 50569f2..1bc4a38 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Available for Mac, Windows, and Linux. - Block-based - Syntax highlighting: - C++, C#, Clojure, CSS, Erlang, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Shell, SQL, Swift, XML, YAML + C++, C#, Clojure, CSS, Erlang, Dart, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Shell, SQL, Swift, XML, YAML - Language auto-detection - Auto-formatting diff --git a/public/langdetect-worker.js b/public/langdetect-worker.js index 95ca257..a519b4a 100644 --- a/public/langdetect-worker.js +++ b/public/langdetect-worker.js @@ -1,6 +1,6 @@ importScripts("guesslang.min.js") -GUESSLANG_LANGUAGES = ["json","py","html","sql","md","java","php","css","xml","cpp","rs","cs","rb","sh","yaml","toml","go","clj","erl","js","ts","swift","kt","groovy","ps1"] +GUESSLANG_LANGUAGES = ["json","py","html","sql","md","java","php","css","xml","cpp","rs","cs","rb","sh","yaml","toml","go","clj","erl","js","ts","swift","kt","groovy","ps1","dart"] const guessLang = new self.GuessLang() diff --git a/src/editor/lang-heynote/heynote.grammar b/src/editor/lang-heynote/heynote.grammar index afaa8e6..77aa679 100644 --- a/src/editor/lang-heynote/heynote.grammar +++ b/src/editor/lang-heynote/heynote.grammar @@ -11,7 +11,7 @@ NoteDelimiter { @tokens { noteDelimiterMark { "∞∞∞" } - NoteLanguage { "text" | "math" | "javascript" | "typescript" | "jsx" | "tsx" | "json" | "python" | "html" | "sql" | "markdown" | "java" | "php" | "css" | "xml" | "cpp" | "rust" | "csharp" | "ruby" | "shell" | "yaml" | "golang" | "clojure" | "erlang" | "lezer" | "toml" | "swift" | "kotlin" | "groovy" | "diff" | "powershell" } + NoteLanguage { "text" | "math" | "javascript" | "typescript" | "jsx" | "tsx" | "json" | "python" | "html" | "sql" | "markdown" | "java" | "php" | "css" | "xml" | "cpp" | "rust" | "csharp" | "ruby" | "shell" | "yaml" | "golang" | "clojure" | "erlang" | "lezer" | "toml" | "swift" | "kotlin" | "groovy" | "diff" | "powershell" | "dart" } Auto { "-a" } noteDelimiterEnter { "\n" } //NoteContent { String } diff --git a/src/editor/lang-heynote/parser.js b/src/editor/lang-heynote/parser.js index e24be27..8091b82 100644 --- a/src/editor/lang-heynote/parser.js +++ b/src/editor/lang-heynote/parser.js @@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({ maxTerm: 10, skippedNodes: [0], repeatNodeCount: 1, - tokenData: "-x~RbYZ!Z}!O!`#V#W!k#W#X$X#X#Y$k#Z#[%Z#[#]%|#^#_&`#_#`'|#`#a(f#a#b)O#d#e)}#f#g+i#g#h+x#h#i,b#l#m&S#m#n-a%&x%&y-g~!`OX~~!cP#T#U!f~!kOU~~!nR#`#a!w#d#e#l#g#h#r~!zP#c#d!}~#QP#^#_#T~#WP#i#j#Z~#^P#f#g#a~#dP#X#Y#g~#lOT~~#oP#d#e#g~#uQ#[#]#{#g#h#g~$OP#T#U$R~$UP#f#g#l~$[P#]#^$_~$bP#Y#Z$e~$hP#Y#Z#g~$nP#f#g$q~$tP#`#a$w~$zP#T#U$}~%QP#b#c%T~%WP#Z#[#g~%^Q#c#d$q#f#g%d~%gP#c#d%j~%mP#c#d%p~%sP#j#k%v~%yP#m#n#g~&PP#h#i&S~&VP#a#b&Y~&]P#`#a#g~&cQ#T#U&i#g#h'm~&lP#j#k&o~&rP#T#U&u~&zPT~#g#h&}~'QP#V#W'T~'WP#f#g'Z~'^P#]#^'a~'dP#d#e'g~'jP#h#i#g~'pQ#c#d'v#l#m#g~'yP#b#c#g~(PP#c#d(S~(VP#h#i(Y~(]P#`#a(`~(cP#]#^'v~(iP#X#Y(l~(oP#n#o(r~(uP#X#Y(x~({P#f#g#g~)RP#T#U)U~)XQ#f#g)_#h#i)w~)bP#_#`)e~)hP#W#X)k~)nP#c#d)q~)tP#k#l'v~)zP#[#]#g~*QR#[#]#l#c#d*Z#m#n+V~*^P#k#l*a~*dP#X#Y*g~*jP#f#g*m~*pP#g#h*s~*vP#[#]*y~*|P#X#Y+P~+SP#`#a&Y~+YP#h#i+]~+`P#[#]+c~+fP#c#d'v~+lP#i#j+o~+rQ#U#V%v#g#h'g~+{R#[#]*y#e#f&Y#k#l,U~,XP#]#^,[~,_P#Y#Z'g~,eS#X#Y,q#c#d&S#g#h,w#m#n,}~,tP#l#m'g~,zP#l#m#g~-QP#d#e-T~-WP#X#Y-Z~-^P#g#h&}~-dP#T#U&S~-jP%&x%&y-m~-pP%&x%&y-s~-xOY~", + tokenData: ".R~RbYZ!Z}!O!`#V#W!k#W#X$X#X#Y$z#Z#[%j#[#]&]#^#_&o#_#`(V#`#a(o#a#b)X#d#e*W#f#g+r#g#h,R#h#i,k#l#m&c#m#n-j%&x%&y-p~!`OX~~!cP#T#U!f~!kOU~~!nR#`#a!w#d#e#l#g#h#r~!zP#c#d!}~#QP#^#_#T~#WP#i#j#Z~#^P#f#g#a~#dP#X#Y#g~#lOT~~#oP#d#e#g~#uQ#[#]#{#g#h#g~$OP#T#U$R~$UP#f#g#l~$[Q#T#U$b#]#^$n~$eP#f#g$h~$kP#h#i#g~$qP#Y#Z$t~$wP#Y#Z#g~$}P#f#g%Q~%TP#`#a%W~%ZP#T#U%^~%aP#b#c%d~%gP#Z#[#g~%mQ#c#d%Q#f#g%s~%vP#c#d%y~%|P#c#d&P~&SP#j#k&V~&YP#m#n#g~&`P#h#i&c~&fP#a#b&i~&lP#`#a#g~&rQ#T#U&x#g#h'v~&{P#j#k'O~'RP#T#U'U~'ZPT~#g#h'^~'aP#V#W'd~'gP#f#g'j~'mP#]#^'p~'sP#d#e$h~'yQ#c#d(P#l#m#g~(SP#b#c#g~(YP#c#d(]~(`P#h#i(c~(fP#`#a(i~(lP#]#^(P~(rP#X#Y(u~(xP#n#o({~)OP#X#Y)R~)UP#f#g#g~)[P#T#U)_~)bQ#f#g)h#h#i*Q~)kP#_#`)n~)qP#W#X)t~)wP#c#d)z~)}P#k#l(P~*TP#[#]#g~*ZR#[#]#l#c#d*d#m#n+`~*gP#k#l*j~*mP#X#Y*p~*sP#f#g*v~*yP#g#h*|~+PP#[#]+S~+VP#X#Y+Y~+]P#`#a&i~+cP#h#i+f~+iP#[#]+l~+oP#c#d(P~+uP#i#j+x~+{Q#U#V&V#g#h$h~,UR#[#]+S#e#f&i#k#l,_~,bP#]#^,e~,hP#Y#Z$h~,nS#X#Y,z#c#d&c#g#h-Q#m#n-W~,}P#l#m$h~-TP#l#m#g~-ZP#d#e-^~-aP#X#Y-d~-gP#g#h'^~-mP#T#U&c~-sP%&x%&y-v~-yP%&x%&y-|~.ROY~", tokenizers: [0, noteContent], topRules: {"Document":[0,2]}, tokenPrec: 0 diff --git a/src/editor/languages.js b/src/editor/languages.js index a7d85ea..605c630 100644 --- a/src/editor/languages.js +++ b/src/editor/languages.js @@ -22,7 +22,7 @@ import { clojure } from "@codemirror/legacy-modes/mode/clojure" import { erlang } from "@codemirror/legacy-modes/mode/erlang" import { toml } from "@codemirror/legacy-modes/mode/toml" import { swift } from "@codemirror/legacy-modes/mode/swift" -import { kotlin } from "@codemirror/legacy-modes/mode/clike" +import { kotlin, dart } from "@codemirror/legacy-modes/mode/clike" import { groovy } from "@codemirror/legacy-modes/mode/groovy" import { diff } from "@codemirror/legacy-modes/mode/diff"; import { powerShell } from "@codemirror/legacy-modes/mode/powershell"; @@ -253,6 +253,12 @@ export const LANGUAGES = [ parser: StreamLanguage.define(powerShell).parser, guesslang: "ps1", }), + new Language({ + token: "dart", + name: "Dart", + parser: StreamLanguage.define(dart).parser, + guesslang: "dart", + }), ] From 387c842d631f1de3d83a9bc2b9b91b89124ae21b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 30 Jun 2024 14:17:14 +0200 Subject: [PATCH 006/228] Upgrade to latest version of Electron --- package-lock.json | 19 +++++-------------- package.json | 2 +- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ed82f3..552da9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "@types/node": "^20.10.5", "@vitejs/plugin-vue": "^4.0.0", "debounce": "^1.2.1", - "electron": "^28.0.0", + "electron": "^31.1.0", "electron-builder": "^23.6.0", "electron-builder-notarize": "^1.5.1", "electron-store": "^8.1.0", @@ -3078,14 +3078,14 @@ } }, "node_modules/electron": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.0.0.tgz", - "integrity": "sha512-eDhnCFBvG0PGFVEpNIEdBvyuGUBsFdlokd+CtuCe2ER3P+17qxaRfWRxMmksCOKgDHb5Wif5UxqOkZSlA4snlw==", + "version": "31.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz", + "integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -3474,15 +3474,6 @@ "node": ">= 10.0.0" } }, - "node_modules/electron/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index 2730a80..cc1a611 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@types/node": "^20.10.5", "@vitejs/plugin-vue": "^4.0.0", "debounce": "^1.2.1", - "electron": "^28.0.0", + "electron": "^31.1.0", "electron-builder": "^23.6.0", "electron-builder-notarize": "^1.5.1", "electron-store": "^8.1.0", From 11df8ea86463d8952d356d86d7a39fcc651fb4cf Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 30 Jun 2024 14:18:44 +0200 Subject: [PATCH 007/228] Bump version to 1.7.1-beta --- 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 552da9e..9135ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Heynote", - "version": "1.7.0", + "version": "1.7.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Heynote", - "version": "1.7.0", + "version": "1.7.1-beta", "license": "Commons Clause MIT", "dependencies": { "electron-log": "^5.0.1" diff --git a/package.json b/package.json index cc1a611..ab7e9d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Heynote", - "version": "1.7.0", + "version": "1.7.1-beta", "main": "dist-electron/main/index.js", "description": "A dedicated scratch pad", "author": "Jonatan Heyman (https://heyman.info)", From 48a29c75524723fcdd1c72263efed5ce57605399 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 2 Jul 2024 19:00:34 +0200 Subject: [PATCH 008/228] Bump version to 1.7.1 --- 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 9135ee8..d3a68a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Heynote", - "version": "1.7.1-beta", + "version": "1.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Heynote", - "version": "1.7.1-beta", + "version": "1.7.1", "license": "Commons Clause MIT", "dependencies": { "electron-log": "^5.0.1" diff --git a/package.json b/package.json index ab7e9d6..65577ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Heynote", - "version": "1.7.1-beta", + "version": "1.7.1", "main": "dist-electron/main/index.js", "description": "A dedicated scratch pad", "author": "Jonatan Heyman (https://heyman.info)", From 29d4eb26cc974b88831404872722ede765bc4e18 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 7 Jul 2024 15:50:57 +0200 Subject: [PATCH 009/228] Optimize block parsing when syntax tree isn't available. If the syntax tree isn't available, use String.indexOf to parse the blocks. --- src/editor/block/block.js | 92 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/src/editor/block/block.js b/src/editor/block/block.js index bb86ecb..623c16e 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -1,7 +1,7 @@ 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 { syntaxTree, ensureSyntaxTree } from "@codemirror/language" +import { syntaxTree, ensureSyntaxTree, syntaxTreeAvailable } from "@codemirror/language" import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js" import { IterMode } from "@lezer/common"; import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js"; @@ -10,12 +10,28 @@ import { mathBlock } from "./math.js" import { emptyBlockSelected } from "./select-all.js"; +function startTimer() { + const timeStart = performance.now(); + return function () { + return Math.round(performance.now() - timeStart); + }; +} + + // tracks the size of the first delimiter let firstBlockDelimiterSize -function getBlocks(state, timeout=50) { +/** + * Return a list of blocks in the document from the syntax tree. + * syntaxTreeAvailable() should have been called before this function to ensure the syntax tree is available. + * @param {*} state + * @param {*} timeout + * @returns + */ +function getBlocksFromSyntaxTree(state, timeout=50) { + //const timer = startTimer() const blocks = []; - const tree = ensureSyntaxTree(state, state.doc.length, timeout) + const tree = syntaxTree(state, state.doc.length, timeout) if (tree) { tree.iterate({ enter: (type) => { @@ -52,9 +68,79 @@ function getBlocks(state, timeout=50) { }); firstBlockDelimiterSize = blocks[0]?.delimiter.to } + //console.log("getBlocksSyntaxTree took", timer(), "ms") return blocks } +/** + * Get the blocks for the document state. + * If the syntax tree is available, we'll extract the blocks from that. Otherwise + * the blocks are parsed from the string contents of the document, which is much faster + * than waiting for the tree parsing to finnish. + */ +function getBlocks(state) { + if (syntaxTreeAvailable(state, state.doc.length)) { + return getBlocksFromSyntaxTree(state) + } + //const timer = startTimer() + const blocks = [] + const doc = state.doc + if (doc.length === 0) { + return []; + } + const content = doc.sliceString(0, doc.length) + const delim = "\n∞∞∞" + let pos = 0 + while (pos < doc.length) { + const blockStart = content.indexOf(delim, pos); + if (blockStart != pos) { + console.error("Error parsing blocks, expected delimiter at", pos) + break; + } + const langStart = blockStart + delim.length; + const delimiterEnd = content.indexOf("\n", langStart) + if (delimiterEnd < 0) { + console.error("Error parsing blocks. Delimiter didn't end with newline") + break + } + const langFull = content.substring(langStart, delimiterEnd); + let auto = false; + let lang = langFull; + if (langFull.endsWith("-a")) { + auto = true; + lang = langFull.substring(0, langFull.length - 2); + } + const contentFrom = delimiterEnd + 1; + let blockEnd = content.indexOf(delim, contentFrom); + if (blockEnd < 0) { + blockEnd = doc.length; + } + + const block = { + language: { + name: lang, + auto: auto, + }, + content: { + from: contentFrom, + to: blockEnd, + }, + delimiter: { + from: blockStart, + to: delimiterEnd + 1, + }, + range: { + from: blockStart, + to: blockEnd, + }, + }; + blocks.push(block); + pos = blockEnd; + } + //console.log("getBlocks (string parsing) took", timer(), "ms") + return blocks; +} + export const blockState = StateField.define({ create(state) { return getBlocks(state, 1000); From c3892163afeaf69275f7223ba149384b58f2e24f Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 7 Jul 2024 16:04:54 +0200 Subject: [PATCH 010/228] Remove timeout parameter for getBlocksFromSyntaxTree() since we now expect the syntax tree to already be available. --- src/editor/block/block.js | 133 ++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/src/editor/block/block.js b/src/editor/block/block.js index 623c16e..c93d98f 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -24,14 +24,11 @@ let firstBlockDelimiterSize /** * Return a list of blocks in the document from the syntax tree. * syntaxTreeAvailable() should have been called before this function to ensure the syntax tree is available. - * @param {*} state - * @param {*} timeout - * @returns */ -function getBlocksFromSyntaxTree(state, timeout=50) { +function getBlocksFromSyntaxTree(state) { //const timer = startTimer() const blocks = []; - const tree = syntaxTree(state, state.doc.length, timeout) + const tree = syntaxTree(state, state.doc.length) if (tree) { tree.iterate({ enter: (type) => { @@ -73,7 +70,70 @@ function getBlocksFromSyntaxTree(state, timeout=50) { } /** - * Get the blocks for the document state. + * Parse blocks from document's string contents using String.indexOf() + */ +function getBlocksFromString(state) { + //const timer = startTimer() + const blocks = [] + const doc = state.doc + if (doc.length === 0) { + return []; + } + const content = doc.sliceString(0, doc.length) + const delim = "\n∞∞∞" + let pos = 0 + while (pos < doc.length) { + const blockStart = content.indexOf(delim, pos); + if (blockStart != pos) { + console.error("Error parsing blocks, expected delimiter at", pos) + break; + } + const langStart = blockStart + delim.length; + const delimiterEnd = content.indexOf("\n", langStart) + if (delimiterEnd < 0) { + console.error("Error parsing blocks. Delimiter didn't end with newline") + break + } + const langFull = content.substring(langStart, delimiterEnd); + let auto = false; + let lang = langFull; + if (langFull.endsWith("-a")) { + auto = true; + lang = langFull.substring(0, langFull.length - 2); + } + const contentFrom = delimiterEnd + 1; + let blockEnd = content.indexOf(delim, contentFrom); + if (blockEnd < 0) { + blockEnd = doc.length; + } + + const block = { + language: { + name: lang, + auto: auto, + }, + content: { + from: contentFrom, + to: blockEnd, + }, + delimiter: { + from: blockStart, + to: delimiterEnd + 1, + }, + range: { + from: blockStart, + to: blockEnd, + }, + }; + blocks.push(block); + pos = blockEnd; + } + //console.log("getBlocksFromString() took", timer(), "ms") + return blocks; +} + +/** + * Get the blocks from the document state. * If the syntax tree is available, we'll extract the blocks from that. Otherwise * the blocks are parsed from the string contents of the document, which is much faster * than waiting for the tree parsing to finnish. @@ -81,69 +141,14 @@ function getBlocksFromSyntaxTree(state, timeout=50) { function getBlocks(state) { if (syntaxTreeAvailable(state, state.doc.length)) { return getBlocksFromSyntaxTree(state) + } else { + return getBlocksFromString(state) } - //const timer = startTimer() - const blocks = [] - const doc = state.doc - if (doc.length === 0) { - return []; - } - const content = doc.sliceString(0, doc.length) - const delim = "\n∞∞∞" - let pos = 0 - while (pos < doc.length) { - const blockStart = content.indexOf(delim, pos); - if (blockStart != pos) { - console.error("Error parsing blocks, expected delimiter at", pos) - break; - } - const langStart = blockStart + delim.length; - const delimiterEnd = content.indexOf("\n", langStart) - if (delimiterEnd < 0) { - console.error("Error parsing blocks. Delimiter didn't end with newline") - break - } - const langFull = content.substring(langStart, delimiterEnd); - let auto = false; - let lang = langFull; - if (langFull.endsWith("-a")) { - auto = true; - lang = langFull.substring(0, langFull.length - 2); - } - const contentFrom = delimiterEnd + 1; - let blockEnd = content.indexOf(delim, contentFrom); - if (blockEnd < 0) { - blockEnd = doc.length; - } - - const block = { - language: { - name: lang, - auto: auto, - }, - content: { - from: contentFrom, - to: blockEnd, - }, - delimiter: { - from: blockStart, - to: delimiterEnd + 1, - }, - range: { - from: blockStart, - to: blockEnd, - }, - }; - blocks.push(block); - pos = blockEnd; - } - //console.log("getBlocks (string parsing) took", timer(), "ms") - return blocks; } export const blockState = StateField.define({ create(state) { - return getBlocks(state, 1000); + return getBlocks(state); }, update(blocks, transaction) { // if blocks are empty it likely means we didn't get a parsed syntax tree, and then we want to update From 9bcf03d00c791602e3b86c5dbb4e298e8a3773b3 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 7 Jul 2024 19:29:17 +0200 Subject: [PATCH 011/228] Typo --- src/editor/block/block.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/block/block.js b/src/editor/block/block.js index c93d98f..2afd1cb 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -136,7 +136,7 @@ function getBlocksFromString(state) { * Get the blocks from the document state. * If the syntax tree is available, we'll extract the blocks from that. Otherwise * the blocks are parsed from the string contents of the document, which is much faster - * than waiting for the tree parsing to finnish. + * than waiting for the tree parsing to finish. */ function getBlocks(state) { if (syntaxTreeAvailable(state, state.doc.length)) { From a5252e12c48953233b5da22dc6d53c451c2e2215 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 7 Jul 2024 21:58:43 +0200 Subject: [PATCH 012/228] Update prettier to latest version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3a68a0..c42ab1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.7", "fs-jetpack": "^5.1.0", - "prettier": "^3.1.1", + "prettier": "^3.3.2", "rollup-plugin-license": "^3.0.1", "sass": "^1.57.1", "typescript": "^4.9.4", @@ -4996,9 +4996,9 @@ } }, "node_modules/prettier": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", - "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index 65577ea..e4eef4e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.7", "fs-jetpack": "^5.1.0", - "prettier": "^3.1.1", + "prettier": "^3.3.2", "rollup-plugin-license": "^3.0.1", "sass": "^1.57.1", "typescript": "^4.9.4", From 016422e7db8cea26d6c1980d4a352224237e9528 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 7 Jul 2024 21:59:47 +0200 Subject: [PATCH 013/228] Update imports so that that they also work in playwright tests --- src/editor/languages.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/editor/languages.js b/src/editor/languages.js index a7d85ea..804e62f 100644 --- a/src/editor/languages.js +++ b/src/editor/languages.js @@ -27,13 +27,13 @@ import { groovy } from "@codemirror/legacy-modes/mode/groovy" import { diff } from "@codemirror/legacy-modes/mode/diff"; import { powerShell } from "@codemirror/legacy-modes/mode/powershell"; -import typescriptPlugin from "prettier/plugins/typescript.mjs" -import babelPrettierPlugin from "prettier/plugins/babel.mjs" -import htmlPrettierPlugin from "prettier/esm/parser-html.mjs" -import cssPrettierPlugin from "prettier/esm/parser-postcss.mjs" -import markdownPrettierPlugin from "prettier/esm/parser-markdown.mjs" -import yamlPrettierPlugin from "prettier/plugins/yaml.mjs" -import * as prettierPluginEstree from "prettier/plugins/estree.mjs"; +import typescriptPlugin from "prettier/plugins/typescript" +import babelPrettierPlugin from "prettier/plugins/babel" +import htmlPrettierPlugin from "prettier/plugins/html" +import cssPrettierPlugin from "prettier/plugins/postcss" +import markdownPrettierPlugin from "prettier/plugins/markdown" +import yamlPrettierPlugin from "prettier/plugins/yaml" +import * as prettierPluginEstree from "prettier/plugins/estree"; class Language { From bc3a9b66a19141258a60fed00eb60e9428952bb9 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 7 Jul 2024 22:01:19 +0200 Subject: [PATCH 014/228] Add test that ensures that getBlocksFromSyntaxTree() and getBlocksFromString() functions produces the same results --- src/editor/block/block.js | 6 +++--- tests/block-parsing.spec.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/block-parsing.spec.js diff --git a/src/editor/block/block.js b/src/editor/block/block.js index 2afd1cb..57e2bb1 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -25,7 +25,7 @@ let firstBlockDelimiterSize * Return a list of blocks in the document from the syntax tree. * syntaxTreeAvailable() should have been called before this function to ensure the syntax tree is available. */ -function getBlocksFromSyntaxTree(state) { +export function getBlocksFromSyntaxTree(state) { //const timer = startTimer() const blocks = []; const tree = syntaxTree(state, state.doc.length) @@ -72,7 +72,7 @@ function getBlocksFromSyntaxTree(state) { /** * Parse blocks from document's string contents using String.indexOf() */ -function getBlocksFromString(state) { +export function getBlocksFromString(state) { //const timer = startTimer() const blocks = [] const doc = state.doc @@ -138,7 +138,7 @@ function getBlocksFromString(state) { * the blocks are parsed from the string contents of the document, which is much faster * than waiting for the tree parsing to finish. */ -function getBlocks(state) { +export function getBlocks(state) { if (syntaxTreeAvailable(state, state.doc.length)) { return getBlocksFromSyntaxTree(state) } else { diff --git a/tests/block-parsing.spec.js b/tests/block-parsing.spec.js new file mode 100644 index 0000000..ee55478 --- /dev/null +++ b/tests/block-parsing.spec.js @@ -0,0 +1,28 @@ +import { expect, test } from "@playwright/test" +import { EditorState } from "@codemirror/state" + +import { heynoteLang } from "../src/editor/lang-heynote/heynote.js" +import { getBlocksFromSyntaxTree, getBlocksFromString } from "../src/editor/block/block.js" + +test("parse blocks from both syntax tree and string contents", async ({page}) => { + const contents = ` +∞∞∞text +Text Block A +∞∞∞text-a +Text Block B +∞∞∞json-a +{ +"key": "value" +} +∞∞∞python +print("Hello, World!") +` + const state = EditorState.create({ + doc: contents, + extensions: heynoteLang(), + }) + const treeBlocks = getBlocksFromSyntaxTree(state) + const stringBlocks = getBlocksFromString(state) + + expect(treeBlocks).toEqual(stringBlocks) +}) From 478b78780dde1aeadb6888fd77e79f2563ba4999 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 8 Jul 2024 12:07:27 +0200 Subject: [PATCH 015/228] Add documentation, and update code that injects key bindings to also inject it into the docs --- README.md | 2 ++ docs/index.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.mjs | 7 +++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 docs/index.md diff --git a/README.md b/README.md index 50569f2..c7a28b9 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ I can totally see the usefulness of such a feature, and it's definitely somethin ### What are the default keyboard shortcuts? + + **On Mac** ``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ab7990d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,89 @@ +# 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. + +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. + +## Default Key Bindings + + + +**On Mac** + +``` +⌘ + Enter Add new block below the current block +⌥ + Enter Add new block before the current block +⌘ + Shift + Enter Add new block at the end of the buffer +⌥ + Shift + Enter Add new block at the start of the buffer +⌘ + ⌥ + Enter Split the current block at cursor position +⌘ + L Change block language +⌘ + Down Goto next block +⌘ + Up Goto previous block +⌘ + A Select all text in a note block. Press again to select the whole buffer +⌘ + ⌥ + Up/Down Add additional cursor above/below +⌥ + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown) +``` + +**On Windows and Linux** + +``` +Ctrl + Enter Add new block below the current block +Alt + Enter Add new block before the current block +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 + 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 +Ctrl + Alt + Up/Down Add additional cursor above/below +Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown) +Alt Show menu +``` + +## Download/Installation + +Download the appropriate (Mac, Windows or Linux) version from [heynote.com](https://heynote.com). The Windows build is not signed, so you might see some scary warning (I can not justify paying a yearly fee for a certificate just to get rid of that). + +### Notes on Linux installation + +It's been reported [(#48)](https://github.com/heyman/heynote/issues/48) that ChromeOS's Debian VM need the following packages installed to run the Heynote AppImage: + +``` +libfuse2 +libnss3 +libnspr4 +``` + +## Math Blocks + +Heynote's Math blocks are powered by [Math.js expressions](https://mathjs.org/docs/expressions). Checkout their [documentation](https://mathjs.org/docs/) to see what [syntax](https://mathjs.org/docs/expressions/syntax.html), [functions](https://mathjs.org/docs/reference/functions.html), and [constants](https://mathjs.org/docs/reference/constants.html) are available. + +### Accessing the previous result + +The variable `prev` can be used to access the previous result. For example: + +``` +128 +prev * 2 # 256 +``` + +### Changing how the results of Math blocks are formatted + +You can define a custom `format` function within the Math block like this: + +``` +_format = format # store reference to the built in format +format(x) = _format(x, {notation:"exponential"}) +``` + +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: + +- Mac: `~/Library/Application Support/Heynote/buffer.txt` +- Windows: `%APPDATA%\Heynote\buffer.txt` +- Linux: `~/.config/Heynote/buffer.txt` + diff --git a/vite.config.mjs b/vite.config.mjs index a359119..3e2ccef 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -20,7 +20,7 @@ const isProduction = process.env.NODE_ENV === "production" const updateReadmeKeybinds = async () => { const readmePath = path.resolve(__dirname, 'README.md') let readme = fs.readFileSync(readmePath, 'utf-8') - const keybindsRegex = /^(### What are the default keyboard shortcuts\?\s*).*?^(```\s+#)/gms + const keybindsRegex = /^(\s*).*?^(```\s+#)/gms const shortcuts = `$1**On Mac** \`\`\` @@ -34,6 +34,11 @@ ${keyHelpStr('win32')} $2` readme = readme.replace(keybindsRegex, shortcuts) fs.writeFileSync(readmePath, readme) + + const docsPath = path.resolve(__dirname, 'docs', 'index.md') + let docs = fs.readFileSync(docsPath, 'utf-8') + docs = docs.replace(keybindsRegex, shortcuts) + fs.writeFileSync(docsPath, docs) } const updateGuesslangLanguagesInWebWorker = async () => { From 4890cd6a6bcc35e9adc592a8f46b3fc5e164ad7d Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 8 Jul 2024 14:14:21 +0200 Subject: [PATCH 016/228] Add menu item to Help menu that opens documentation page --- electron/main/menu.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/electron/main/menu.ts b/electron/main/menu.ts index 39f942c..9e4a2a4 100644 --- a/electron/main/menu.ts +++ b/electron/main/menu.ts @@ -131,7 +131,14 @@ const template = [ role: 'help', submenu: [ { - label: 'Learn More', + label: 'Documentation', + click: async () => { + const { shell } = require('electron') + await shell.openExternal('https://heynote.com/docs/') + } + }, + { + label: 'Website', click: async () => { const { shell } = require('electron') await shell.openExternal('https://heynote.com') From d6420e65e89669058eca3ea2e022a0643dbfbf73 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 8 Jul 2024 14:48:45 +0200 Subject: [PATCH 017/228] Add links to documentation in Readme --- README.md | 81 +++---------------------------------------------- vite.config.mjs | 9 ++---- 2 files changed, 6 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index c7a28b9..4b1b0b0 100644 --- a/README.md +++ b/README.md @@ -31,19 +31,9 @@ Available for Mac, Windows, and Linux. - Default or Emacs-like key bindings -## Installation +## Documentation -Download the appropriate (Mac, Windows or Linux) version from the latest Github release (or from [heynote.com](https://heynote.com)). The Windows build is not signed, so you might see some scary warning (I can not justify paying a yearly fee for a certificate just to get rid of that). - -### Notes on Linux installation - -It's been reported [(#48)](https://github.com/heyman/heynote/issues/48) that ChromeOS's Debian VM need the following packages installed to run the Heynote AppImage: - -``` -libfuse2 -libnss3 -libnspr4 -``` +[Documentation](https://heynote.com/docs/) is available on the Heynote website. ## Development @@ -73,42 +63,12 @@ To run the tests in the Playwright UI: I'm happy to merge contributions that fit my vision for the app. Bug fixes are always welcome. -## Math Blocks - -Heynote's Math blocks are powered by [Math.js expressions](https://mathjs.org/docs/expressions). Checkout their [documentation](https://mathjs.org/docs/) to see what [syntax](https://mathjs.org/docs/expressions/syntax.html), [functions](https://mathjs.org/docs/reference/functions.html), and [constants](https://mathjs.org/docs/reference/constants.html) are available. - -### Accessing the previous result - -The variable `prev` can be used to access the previous result. For example: - -``` -128 -prev * 2 # 256 -``` - -### Changing how the results of Math blocks are formatted? - -You can define a custom `format` function within the Math block like this: - -``` -_format = format # store reference to the built in format -format(x) = _format(x, {notation:"exponential"}) -``` - -See the [Math.js format()](https://mathjs.org/docs/reference/functions/format.html) function for more info on what's supported. - ## FAQ ### Where is the buffer data stored? -The default paths for the buffer data for the respective OS are: - -- Mac: `~/Library/Application Support/Heynote/buffer.txt` -- Windows: `%APPDATA%\Heynote\buffer.txt` -- Linux: `~/.config/Heynote/buffer.txt` - -From version >=1.5.0, symlinks will be supported and you'll be able to configure the path where `buffer.txt` is stored. +See the [documentation](https://heynote.com/docs/#user-content-the-buffer-file). ### Can you make a mobile app? @@ -122,40 +82,7 @@ I can totally see the usefulness of such a feature, and it's definitely somethin ### What are the default keyboard shortcuts? - - -**On Mac** - -``` -⌘ + Enter Add new block below the current block -⌥ + Enter Add new block before the current block -⌘ + Shift + Enter Add new block at the end of the buffer -⌥ + Shift + Enter Add new block at the start of the buffer -⌘ + ⌥ + Enter Split the current block at cursor position -⌘ + L Change block language -⌘ + Down Goto next block -⌘ + Up Goto previous block -⌘ + A Select all text in a note block. Press again to select the whole buffer -⌘ + ⌥ + Up/Down Add additional cursor above/below -⌥ + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown) -``` - -**On Windows and Linux** - -``` -Ctrl + Enter Add new block below the current block -Alt + Enter Add new block before the current block -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 + 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 -Ctrl + Alt + Up/Down Add additional cursor above/below -Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown) -Alt Show menu -``` +See the [documentation](https://heynote.com/docs/#user-content-default-key-bindings). ## Thanks! diff --git a/vite.config.mjs b/vite.config.mjs index 3e2ccef..084827c 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -17,9 +17,7 @@ rmSync('dist-electron', { recursive: true, force: true }) const isDevelopment = process.env.NODE_ENV === "development" || !!process.env.VSCODE_DEBUG const isProduction = process.env.NODE_ENV === "production" -const updateReadmeKeybinds = async () => { - const readmePath = path.resolve(__dirname, 'README.md') - let readme = fs.readFileSync(readmePath, 'utf-8') +const injectKeybindsInDocs = async () => { const keybindsRegex = /^(\s*).*?^(```\s+#)/gms const shortcuts = `$1**On Mac** @@ -32,9 +30,6 @@ ${keyHelpStr('darwin')} \`\`\` ${keyHelpStr('win32')} $2` - readme = readme.replace(keybindsRegex, shortcuts) - fs.writeFileSync(readmePath, readme) - const docsPath = path.resolve(__dirname, 'docs', 'index.md') let docs = fs.readFileSync(docsPath, 'utf-8') docs = docs.replace(keybindsRegex, shortcuts) @@ -61,7 +56,7 @@ export default defineConfig({ plugins: [ vue(), - updateReadmeKeybinds(), + injectKeybindsInDocs(), updateGuesslangLanguagesInWebWorker(), electron([ { From 417394f0593c2162ad8cefee319de80bbaef2b52 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 8 Jul 2024 14:52:00 +0200 Subject: [PATCH 018/228] Add link to documentation to the top of the Readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b1b0b0..d946672 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ -**Website:** [heynote.com](https://heynote.com) +## General Information + +- Website: [heynote.com](https://heynote.com) +- Documentation: [heynote.com](https://heynote.com/docs/) 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 a6746167d624e0785efd121f1b58f195f0f70855 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 8 Jul 2024 14:59:10 +0200 Subject: [PATCH 019/228] Bump version to 1.8.0-beta --- 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 c42ab1e..2e492ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Heynote", - "version": "1.7.1", + "version": "1.8.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Heynote", - "version": "1.7.1", + "version": "1.8.0-beta", "license": "Commons Clause MIT", "dependencies": { "electron-log": "^5.0.1" diff --git a/package.json b/package.json index e4eef4e..ab1662a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Heynote", - "version": "1.7.1", + "version": "1.8.0-beta", "main": "dist-electron/main/index.js", "description": "A dedicated scratch pad", "author": "Jonatan Heyman (https://heyman.info)", From e8971a6733678238b2ba29a407bd033e64c03248 Mon Sep 17 00:00:00 2001 From: Cris Date: Mon, 8 Jul 2024 13:00:31 -0700 Subject: [PATCH 020/228] Add default redo cmd that works on all Platforms. Mod+Shift+Z --- src/editor/keymap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 745aaf3..fff0dbc 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -1,7 +1,7 @@ import { keymap } from "@codemirror/view" //import { EditorSelection, EditorState } from "@codemirror/state" import { - indentLess, indentMore, + indentLess, indentMore, redo, } from "@codemirror/commands" import { @@ -61,6 +61,7 @@ export function heynoteKeymap(editor) { ["Mod-Alt-ArrowDown", newCursorBelow], ["Mod-Alt-ArrowUp", newCursorAbove], ["Mod-Shift-k", deleteLine], + ["Mod-Shift-z", redo], {key:"Mod-ArrowUp", run:gotoPreviousBlock, shift:selectPreviousBlock}, {key:"Mod-ArrowDown", run:gotoNextBlock, shift:selectNextBlock}, {key:"Ctrl-ArrowUp", run:gotoPreviousParagraph, shift:selectPreviousParagraph}, From f33be7f3a4b0415be87941cf8510a459ac387a94 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 11 Jul 2024 14:30:07 +0200 Subject: [PATCH 021/228] Fix bug causing editing to break when there are empty blocks of a language that uses a StreamParser --- src/editor/lang-heynote/nested-parser.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/editor/lang-heynote/nested-parser.js b/src/editor/lang-heynote/nested-parser.js index 4b5aa8e..3a1c338 100644 --- a/src/editor/lang-heynote/nested-parser.js +++ b/src/editor/lang-heynote/nested-parser.js @@ -22,7 +22,13 @@ export function configureNesting() { if (id == NoteContent) { let noteLang = node.node.parent.firstChild.getChildren(NoteLanguage)[0] let langName = input.read(noteLang?.from, noteLang?.to) - //console.log("langName:", langName) + + // if the NoteContent is empty, we don't want to return a parser, since that seems to cause an + // error for StreamLanguage parsers when the buffer size is large (e.g >300 kb) + if (node.node.from == node.node.to) { + return null + } + if (langName in languageMapping && languageMapping[langName] !== null) { //console.log("found parser for language:", langName) return { From 71f6a033cff4bfca522e41e661186213737edbdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:50:27 +0000 Subject: [PATCH 022/228] Bump braces from 3.0.2 to 3.0.3 Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e492ca..2f9f30c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2262,12 +2262,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3675,9 +3675,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" From e5d4d31ca2456d37fd46037515f4caa1c9c8e607 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:53:19 +0000 Subject: [PATCH 023/228] Bump ejs from 3.1.9 to 3.1.10 Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f9f30c..0e1b376 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3063,9 +3063,9 @@ "dev": true }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "dependencies": { "jake": "^10.8.5" From d82b3920d7678e837de9da10c9850c1fe39167e2 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 12 Jul 2024 14:33:26 +0200 Subject: [PATCH 024/228] Add setting for configuring the default block language and language auto detection --- electron/config.js | 2 ++ src/components/App.vue | 2 ++ src/components/Editor.vue | 11 ++++++- src/components/settings/Settings.vue | 35 +++++++++++++++++++++ src/editor/block/commands.js | 29 ++++++++++------- src/editor/editor.js | 11 +++++-- src/editor/keymap.js | 10 +++--- src/editor/language-detection/autodetect.js | 14 +++++---- 8 files changed, 89 insertions(+), 25 deletions(-) diff --git a/electron/config.js b/electron/config.js index 7c443ea..f3ecd97 100644 --- a/electron/config.js +++ b/electron/config.js @@ -35,6 +35,8 @@ const schema = { "showInMenu": {type: "boolean", default: false}, "alwaysOnTop": {type: "boolean", default: false}, "bracketClosing": {type: "boolean", default: false}, + "defaultBlockLanguage": {type: "string"}, + "defaultBlockLanguageAutoDetect": {type: "boolean"}, // when default font settings are used, fontFamily and fontSize is not specified in the // settings file, so that it's possible for us to change the default settings in the diff --git a/src/components/App.vue b/src/components/App.vue index 4f400bc..08234d5 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -122,6 +122,8 @@ :bracketClosing="settings.bracketClosing" :fontFamily="settings.fontFamily" :fontSize="settings.fontSize" + :defaultBlockLanguage="settings.defaultBlockLanguage || 'text'" + :defaultBlockLanguageAutoDetect="settings.defaultBlockLanguageAutoDetect === undefined ? true : settings.defaultBlockLanguageAutoDetect" class="editor" ref="editor" @openLanguageSelector="openLanguageSelector" diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 7171a80..b3bc938 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -29,6 +29,8 @@ }, fontFamily: String, fontSize: Number, + defaultBlockLanguage: String, + defaultBlockLanguageAutoDetect: Boolean, }, components: {}, @@ -78,6 +80,7 @@ }) 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) => { @@ -145,12 +148,18 @@ fontSize() { this.editor.setFont(this.fontFamily, this.fontSize) }, + defaultBlockLanguage() { + this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + }, + defaultBlockLanguageAutoDetect() { + this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect) + }, }, methods: { setLanguage(language) { if (language === "auto") { - this.editor.setCurrentLanguage("text", true) + this.editor.setCurrentLanguage(null, true) } else { this.editor.setCurrentLanguage(language, false) } diff --git a/src/components/settings/Settings.vue b/src/components/settings/Settings.vue index 42c9ee6..3a7a45b 100644 --- a/src/components/settings/Settings.vue +++ b/src/components/settings/Settings.vue @@ -1,10 +1,14 @@ + + + + + + diff --git a/src/main.js b/src/main.js index 7836648..456d02e 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 0000000..cc3aa1c --- /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 06ef81b..57d9098 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 01d18da..f02d793 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 d01c19fd727e2b372279a1bf77374535507e40c2 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 24 Jul 2024 13:52:44 +0200 Subject: [PATCH 048/228] 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 | 147 +++++++------- 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 | 45 +++-- src/editor/editor.js | 92 +++++++-- src/editor/event.js | 9 - src/editor/keymap.js | 2 + src/editor/language-detection/autodetect.js | 181 +++++++++-------- src/editor/save.js | 8 +- src/main.js | 4 + src/stores/notes-store.js | 65 ++++++ webapp/bridge.js | 30 ++- 20 files changed, 1179 insertions(+), 295 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 b8799dd..0ecc5a7 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 0000000..e1bb7f1 --- /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 01393fe..93a24f5 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 7d5f242..45baa60 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 save(content) { - return await ipcRenderer.invoke("buffer-content:save", content) + async getList() { + return await ipcRenderer.invoke("buffer:getList") }, - async saveAndQuit(content) { - return await ipcRenderer.invoke("buffer-content:saveAndQuit", content) + async load(path) { + return await ipcRenderer.invoke("buffer:load", path) }, - onChangeCallback(callback) { - ipcRenderer.on("buffer-content:change", callback) + async save(path, content) { + return await ipcRenderer.invoke("buffer:save", path, content) + }, + + async saveAndQuit(contents) { + return await ipcRenderer.invoke("buffer:saveAndQuit", contents) + }, + + 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 a519b4a..15d736f 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 08234d5..7e51dfe 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 0000000..126b14b --- /dev/null +++ b/src/components/NoteSelector.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index 0676dd9..a6b4df1 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 57e2bb1..2c5341b 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 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 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 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 8df85a9..484176d 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 34f5960..0000000 --- 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 a7e32ed..64b4cea 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 771170e..4e08237 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 + + //const plugin = EditorView.updateListener.of(update => { + const plugin = ViewPlugin.fromClass( + class { + update(update) { + if (update.docChanged) { + if (idleCallbackId !== null) { + cancelIdleCallbackCompat(idleCallbackId) + idleCallbackId = null + } + + 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 + } + }) + } } - 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, - }) - 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 415a8ee..4763f6f 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 456d02e..9994a49 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 0000000..7f4bbb8 --- /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 57d9098..107a955 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 ecea69f6bbde0f8e726dcf3c99a5a16c69dab42a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 24 Jul 2024 14:30:14 +0200 Subject: [PATCH 049/228] 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 ac91760..d0ca3d1 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 0000000..f321095 --- /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 0000000..f1e4dbe --- /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 0000000..5cafd14 --- /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 0000000..7693e7c --- /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 e3d2881..0000000 --- a/src/components/form/FolderSelect.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - - diff --git a/webapp/bridge.js b/webapp/bridge.js index 107a955..2cf646f 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 5b61a0a234ddcef19977abe00d731b6394506297 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 26 Jul 2024 11:30:25 +0200 Subject: [PATCH 053/228] 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 4a8dd94..b00edc9 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 5bd74c3..9d5bd8b 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 34812c2..030fdab 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 1688cc1..f75dd77 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 b81c6aa..e384b43 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 4122970..c348554 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 6b4e83c..fae768e 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 2c5341b..0f3141c 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 b55c50b..42192c3 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 8bfc2aa..80801b9 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 64b4cea..9b077b1 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 7f4bbb8..c8cb662 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 2cf646f..00db2e9 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 edcf33a606773c6efa1735557e56158153163df0 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sat, 27 Jul 2024 10:26:51 +0200 Subject: [PATCH 054/228] 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 22fa759..78f69a9 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 059/228] 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 5058c1c..7da8c34 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 7c9c975..8eb7d82 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 ea821a5..30417e3 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 5cafd14..06e2840 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 156cc8b1bbc04f83fbaf7134aa04e97d83e16638 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 4 Aug 2024 17:42:53 +0200 Subject: [PATCH 060/228] 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 e384b43..68a9ce5 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 706a567e76920abb5043a289cdb923c57a03b5c2 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 4 Aug 2024 17:53:31 +0200 Subject: [PATCH 061/228] 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 f39fd87..b579ef2 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 7da8c34..7cf8f20 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 00af47d..c667a6e 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 0da3e32171d1f199770417457a7570fcc00779e0 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 4 Aug 2024 17:58:20 +0200 Subject: [PATCH 062/228] 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 8eb7d82..860d709 100644 --- a/src/components/folder-selector/FolderItem.vue +++ b/src/components/folder-selector/FolderItem.vue @@ -53,7 +53,7 @@ :style="style" > {{ name }} - + From 7e1f01471a1873b979465712e3a18bdb851a3f85 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Sep 2024 15:22:06 +0200 Subject: [PATCH 063/228] 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 | 102 +++++++++++- src/editor/editor.js | 6 + src/main.js | 2 + src/stores/editor-cache.js | 48 ++++++ src/stores/notes-store.js | 57 +++++-- 11 files changed, 513 insertions(+), 53 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 0000000..b3110a4 --- /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 b00edc9..bc06a18 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 9d5bd8b..bb1e553 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 7e51dfe..982b80a 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 0000000..cfbd5d8 --- /dev/null +++ b/src/components/EditNote.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 68a9ce5..93c52af 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 c667a6e..61e9d80 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"}) } - } else if (event.key === "Enter") { - this.selectItem(this.filteredItems[this.selected].path) + 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") { + 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 80801b9..754e5c8 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 9994a49..fa49f2b 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 0000000..26bb186 --- /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 4da1e8f..bdd1187 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 982f95a2c7dafbe0baf03d28c4c94cfdb03cc621 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sat, 7 Sep 2024 12:15:35 +0200 Subject: [PATCH 064/228] 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 61e9d80..a08ac5e 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 29facb478759e533afbe9387a146c562ae3afe92 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 10 Sep 2024 09:55:14 +0200 Subject: [PATCH 065/228] 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 26bb186..5cf492c 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 7be0a304d01d436e12bd625ec2cde412e75df529 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 10 Sep 2024 13:34:23 +0200 Subject: [PATCH 066/228] 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 | 29 ++- src/components/Editor.vue | 9 +- src/components/NewNote.vue | 23 ++- src/components/NoteSelector.vue | 125 +++++++++---- 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, 341 insertions(+), 269 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 0000000..27ad162 --- /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 0000000..9130299 --- /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 bef7a83..52f7c2f 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 d120afe..374f812 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 0ecc5a7..0000000 --- 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 bc06a18..6b3649d 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 93a24f5..c78a57f 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 bb1e553..9e5da29 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 9930042..900a78f 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 0000000..42c1766 --- /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 982b80a..b9683b0 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 93c52af..5358d16 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 7cf8f20..224b9e5 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 a08ac5e..a5d6e4b 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 - &.selected - background: #48b57e - color: #fff - .action-buttons .show-actions - display: inline-block - &.scratch - font-weight: 600 + 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 + &.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 + +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: - &.selected - background: #1b6540 &:hover - background: #1f7449 + background-color: rgba(255,255,255, 0.1) + &.selected + background: #48b57e + color: #fff + &:hover + background: #3ea471 &.delete - background: #ae1e1e + background: #e95050 &:hover - background: #bf2222 + background: #ce4848 + +dark-mode + background: #1b6540 + &:hover + 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 a6b4df1..4c7cdef 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 754e5c8..fb23d9c 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 5cf492c..a8b41e9 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 bdd1187..5ac1c11 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 f9acfb9..9822d03 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 b2760cf..aa59fb6 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 f0e299122f27bc7ba6c109e595d19b33a134bf63 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 10 Sep 2024 16:22:30 +0200 Subject: [PATCH 067/228] 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 6b3649d..987b8ff 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 9e5da29..1dd2eb1 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 a5d6e4b..58058e6 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 42192c3..c59ce51 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 5ac1c11..5f815d8 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 9863501b70270fc68c769bda196789fc607a9ed4 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 11:50:21 +0200 Subject: [PATCH 068/228] Add Changelog to docs --- docs/index.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/docs/index.md b/docs/index.md index 52f7c2f..de26c9d 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 2ec4fecd1ac147a42b47a3688e4fe45276d5a5e3 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 11:57:03 +0200 Subject: [PATCH 069/228] 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 0000000..ade702a --- /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 de26c9d..52f7c2f 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 b0161836aa065e05134c3b0ff365831b15958a85 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 12:05:08 +0200 Subject: [PATCH 070/228] 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 52f7c2f..aa5c085 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 bd4ebdfec07451ca0205831ac0797202dffb8eaa Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 11 Sep 2024 12:06:11 +0200 Subject: [PATCH 071/228] 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 aa5c085..1088941 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 e46a014dd7e2669caf9bb69e3e780ca3ef21ecc5 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 12 Sep 2024 11:42:38 +0200 Subject: [PATCH 072/228] 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 b002233..53fdfb2 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 a924d3345377e4a44b3dc7cebfa2228f69416d0f Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 12 Sep 2024 12:05:31 +0200 Subject: [PATCH 073/228] 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 987b8ff..4009dd8 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 e34a1c8ca3a99e5c1b3f01dda4fefb01c1edf1c5 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 3 Oct 2024 09:47:48 +0200 Subject: [PATCH 074/228] 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 4009dd8..b387c26 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 b7d2c26ce3e6699b42ed7f68017ee12deccc63b0 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 6 Oct 2024 12:01:17 +0200 Subject: [PATCH 075/228] 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 e5c6e6f..520d4ea 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 5896911..1a4b2bc 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 d1f87b062cabf4b9d6822c59c4596fbb60b23b4b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 6 Oct 2024 12:01:55 +0200 Subject: [PATCH 076/228] 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 d477a35..6fd8517 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 72f0bb5e21471e4b1186d7cb930bf6b52906ae4a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 6 Oct 2024 16:57:09 +0200 Subject: [PATCH 077/228] 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 6fd8517..eaf16bc 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 2f9b6b8aab351689dc261051fa8b63440bc5ee15 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 7 Oct 2024 13:01:40 +0200 Subject: [PATCH 078/228] 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 eaf16bc..fc4cbf6 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 c47d824111d6da4a1a2778642af50621857dbd3d Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 28 Oct 2024 13:56:27 +0100 Subject: [PATCH 079/228] 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 b387c26..7dc6143 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 6c950055440d99e6e31abcf5dd0e361cc65b1836 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 28 Oct 2024 13:57:20 +0100 Subject: [PATCH 080/228] #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 7dc6143..b8420aa 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 374e2b4e5deab9195a428ca0635b60762bee35e6 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 12:27:17 +0100 Subject: [PATCH 081/228] 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 b8420aa..a92662c 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 c78a57f..7c06d46 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 4833db9928e07211a66c5376f4e40e702818f334 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 13:34:59 +0100 Subject: [PATCH 082/228] 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 ade702a..0a39990 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 1088941..dda262b 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 57d4ac49e0d4192203db13180e329069ec929023 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 13:43:03 +0100 Subject: [PATCH 083/228] 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 94c55b3..26cfc94 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 dda262b..0e94646 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 75b7eac5ca2b3248fdcc7c383cf2f07e088773c9 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 13:48:36 +0100 Subject: [PATCH 084/228] Fix typo --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 0e94646..2aaa473 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 d590c625bb6339ed291322a3a73ac5360573e095 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 14:08:33 +0100 Subject: [PATCH 085/228] 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 0a39990..bf27f98 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 f49ef4bec8070164117a3081dcdbe72a5cdd1d17 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 14:12:18 +0100 Subject: [PATCH 086/228] 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 fb23d9c..3d4d77d 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 d12f6577bfc695b7cabd6242dd463b4c7e6c4acb Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 14:14:20 +0100 Subject: [PATCH 087/228] 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 5358d16..6003e5b 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 3d4d77d..4a3f3ac 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 f6b086b7e65da01f1006e7d0983dfd0161d9a5a6 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 15:30:10 +0100 Subject: [PATCH 088/228] 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 a92662c..a21e7e6 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 946728060de05e33630a8844c377ec0d7cd9203e Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 16:19:40 +0100 Subject: [PATCH 089/228] 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 224b9e5..6581a35 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 58058e6..1bbe95a 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 090/228] 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 1bbe95a..ca63783 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 2c31f0170ac67caecd56e1a4d4318f7cdd36d280 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 16:47:55 +0100 Subject: [PATCH 091/228] 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 00db2e9..8978935 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 3e84850147aa5c82de7143d458801ad3544899ae Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 16:48:15 +0100 Subject: [PATCH 092/228] 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 a21e7e6..6999523 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 b7ffd5a2ce181a8c86be992dbb0a5810709f6a02 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 17:03:36 +0100 Subject: [PATCH 093/228] 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 8978935..41e1af8 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 45c2df5c281a2e90d637969358e976dbf320a0b6 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 17:19:14 +0100 Subject: [PATCH 094/228] 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 41e1af8..0d8adb3 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 7191747..748e3ea 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 42c71dad7b5e6fde85e967dd5d6c1ea69e794b4b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 29 Oct 2024 18:37:42 +0100 Subject: [PATCH 095/228] 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 ca63783..8b8b474 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 fb58bb6f365db51130b0a7958fc81830ec5ebd70 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Dec 2024 16:49:01 +0100 Subject: [PATCH 096/228] 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 6581a35..96bc77a 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 d54a3ba633fb67d8a39d0692394124209aa93504 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Dec 2024 16:52:35 +0100 Subject: [PATCH 097/228] 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 42c1766..a1359aa 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 c59ce51..fe9bf3b 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 4a3f3ac..0199c14 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 0000000..a42705c --- /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 aa59fb6..209701a 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 0d8adb3..48bf02e 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 9c057b120a578534742282cad06b96a892ecb946 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 4 Dec 2024 20:58:24 +0100 Subject: [PATCH 098/228] 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 96bc77a..b05a1d0 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 9b077b1..eb7a229 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 5f815d8..4f46437 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 a42705c..61b94b6 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 fe6a410e95c36d45978a1800fd096a80bd798b93 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 5 Dec 2024 23:20:13 +0100 Subject: [PATCH 099/228] 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 b05a1d0..8998ae7 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 8b8b474..de36b5d 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 @@ diff --git a/src/components/settings/KeyboardBindings.vue b/src/components/settings/KeyboardBindings.vue new file mode 100644 index 0000000..a4a584e --- /dev/null +++ b/src/components/settings/KeyboardBindings.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/settings/Settings.vue b/src/components/settings/Settings.vue index f142b70..9e9e6aa 100644 --- a/src/components/settings/Settings.vue +++ b/src/components/settings/Settings.vue @@ -1,9 +1,11 @@ @@ -111,16 +139,31 @@ border: 2px solid #f1f1f1 +dark-mode background: #3c3c3c + background: #333 border: 2px solid #3c3c3c ::v-deep(tr) - &:nth-child(2n) - background: #fff + background: #fff + border-bottom: 2px solid #f1f1f1 + +dark-mode + background: #333 + border-bottom: 2px solid #3c3c3c + &.ghost + background: #48b57e + color: #fff +dark-mode - background: #333 + background: #1b6540 th text-align: left font-weight: 600 th, td padding: 8px + &.actions + padding: 6px + button + height: 20px + font-size: 11px + + tbody + margin-bottom: 20px diff --git a/src/components/settings/Settings.vue b/src/components/settings/Settings.vue index 84ed5fa..cb399c6 100644 --- a/src/components/settings/Settings.vue +++ b/src/components/settings/Settings.vue @@ -86,6 +86,12 @@ window.removeEventListener("keydown", this.onKeyDown); }, + watch: { + keyBindings(newKeyBindings) { + this.updateSettings() + } + }, + methods: { onKeyDown(event) { if (event.key === "Escape") { @@ -98,7 +104,7 @@ showLineNumberGutter: this.showLineNumberGutter, showFoldGutter: this.showFoldGutter, keymap: this.keymap, - keyBindings: toRaw(this.keyBindings), + keyBindings: this.keyBindings.map((kb) => toRaw(kb)), emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt", allowBetaVersions: this.allowBetaVersions, enableGlobalHotkey: this.enableGlobalHotkey, @@ -369,6 +375,7 @@
      diff --git a/src/editor/editor.js b/src/editor/editor.js index 23d1535..b5484d1 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -16,7 +16,7 @@ import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, t import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK, SET_FONT } from "./annotation.js"; import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.js" import { formatBlockContent } from "./block/format-code.js" -import { heynoteKeymap, DEFAULT_KEYMAP, EMACS_KEYMAP } from "./keymap.js" +import { getKeymapExtensions } from "./keymap.js" import { heynoteCopyCut } from "./copy-paste" import { languageDetection } from "./language-detection/autodetect.js" import { autoSaveContent } from "./save.js" @@ -29,15 +29,6 @@ import { useHeynoteStore } from "../stores/heynote-store.js"; import { useErrorStore } from "../stores/error-store.js"; -function getKeymapExtensions(editor, keymap, keyBindings) { - return heynoteKeymap( - editor, - keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP, - keyBindings, - ) -} - - export class HeynoteEditor { constructor({ element, diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 8389d32..211a0d3 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -3,30 +3,6 @@ import { Prec } from "@codemirror/state" import { HEYNOTE_COMMANDS } from "./commands.js" - -function keymapFromSpec(specs, editor) { - return keymap.of(specs.map((spec) => { - let key = spec.key - if (key.indexOf("EmacsMeta") != -1) { - key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta") - } - return { - key: key, - //preventDefault: true, - preventDefault: false, - run: (view) => { - //console.log("run()", spec.key, spec.command) - const command = HEYNOTE_COMMANDS[spec.command] - if (!command) { - console.error(`Command not found: ${spec.command} (${spec.key})`) - return false - } - return command(editor)(view) - }, - } - })) -} - const cmd = (key, command) => ({key, command}) const cmdShift = (key, command, shiftCommand) => { return [ @@ -99,8 +75,6 @@ export const DEFAULT_KEYMAP = [ cmd("Shift-Tab", "indentLess"), //cmd("Alt-ArrowLeft", "cursorSubwordBackward"), //cmd("Alt-ArrowRight", "cursorSubwordForward"), - cmd("Ctrl-Space", "toggleEmacsMarkMode"), - cmd("Ctrl-g", "emacsCancel"), cmd("Mod-l", "openLanguageSelector"), cmd("Mod-p", "openBufferSelector"), @@ -146,27 +120,46 @@ export const EMACS_KEYMAP = [ ...cmdShift("Ctrl-f", "cursorCharRight", "selectCharRight"), ...cmdShift("Ctrl-a", "cursorLineStart", "selectLineStart"), ...cmdShift("Ctrl-e", "cursorLineEnd", "selectLineEnd"), - ...DEFAULT_KEYMAP, ] +function keymapFromSpec(specs, editor) { + return keymap.of(specs.map((spec) => { + let key = spec.key + if (key.indexOf("EmacsMeta") != -1) { + key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta") + } + return { + key: key, + //preventDefault: true, + preventDefault: false, + run: (view) => { + //console.log("run()", spec.key, spec.command) + const command = HEYNOTE_COMMANDS[spec.command] + if (!command) { + console.error(`Command not found: ${spec.command} (${spec.key})`) + return false + } + return command(editor)(view) + }, + } + })) +} + export function heynoteKeymap(editor, keymap, userKeymap) { - //return [ - // keymapFromSpec([ - // ...Object.entries(userKeymap).map(([key, command]) => cmd(key, command)), - // ...keymap, - // ], editor), - //] - - // merge the default keymap with the custom keymap - const defaultKeys = Object.fromEntries(keymap.map(km => [km.key, km.command])) - //let mergedKeys = Object.entries({...defaultKeys, ...Object.fromEntries(userKeymap.map(km => [km.key, km.command]))}).map(([key, command]) => cmd(key, command)) - let mergedKeys = Object.entries({...defaultKeys, ...userKeymap}).map(([key, command]) => cmd(key, command)) - //console.log("userKeys:", userKeymap) - //console.log("mergedKeys:", mergedKeys) - return [ - Prec.high(keymapFromSpec(mergedKeys, editor)), + keymapFromSpec([ + ...userKeymap, + ...keymap, + ], editor), ] } + +export function getKeymapExtensions(editor, keymap, keyBindings) { + return heynoteKeymap( + editor, + keymap === "emacs" ? EMACS_KEYMAP.concat(DEFAULT_KEYMAP) : DEFAULT_KEYMAP, + keyBindings, + ) +} From 89f883e5e9b18313a6048e881b13b44209489cf8 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 13 Apr 2025 12:39:50 +0200 Subject: [PATCH 191/228] Make emacs mark mode commands return true to stop other key bindings from executing --- src/editor/emacs-mode.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/editor/emacs-mode.js b/src/editor/emacs-mode.js index d560b2f..7d957f5 100644 --- a/src/editor/emacs-mode.js +++ b/src/editor/emacs-mode.js @@ -26,6 +26,7 @@ export function emacsMoveCommand(defaultCmd, markModeCmd) { export function toggleEmacsMarkMode(editor) { return (view) => { editor.emacsMarkMode = !editor.emacsMarkMode + return true } } @@ -33,5 +34,6 @@ export function emacsCancel(editor) { return (view) => { simplifySelection(view) editor.emacsMarkMode = false + return true } } From 01aba1fb9f7ebc756b72f5bbe3aede2ec41f4813 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Sun, 13 Apr 2025 12:40:31 +0200 Subject: [PATCH 192/228] Add command that does nothing except preventing further commands to execute for a specific keybinding (can be used to remove a default key binding) --- src/editor/commands.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/editor/commands.js b/src/editor/commands.js index d1d4772..94248ce 100644 --- a/src/editor/commands.js +++ b/src/editor/commands.js @@ -58,6 +58,9 @@ const openCreateNewBuffer = (editor) => () => { editor.openCreateBuffer("new") return true } +const nothing = (view) => { + return true +} const HEYNOTE_COMMANDS = { //undo, @@ -107,6 +110,7 @@ const NON_EDITOR_CONTEXT_COMMANDS = { selectPreviousParagraph, selectNextParagraph, selectPreviousBlock, selectNextBlock, paste: pasteCommand, + nothing, // directly from CodeMirror undo, redo, From 166855c6f5fd614d78fad070a34b8a1fd147f086 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 14 Apr 2025 10:31:43 +0200 Subject: [PATCH 193/228] Fix tests and webapp --- src/editor/keymap.js | 2 +- webapp/bridge.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor/keymap.js b/src/editor/keymap.js index 211a0d3..ca52817 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -160,6 +160,6 @@ export function getKeymapExtensions(editor, keymap, keyBindings) { return heynoteKeymap( editor, keymap === "emacs" ? EMACS_KEYMAP.concat(DEFAULT_KEYMAP) : DEFAULT_KEYMAP, - keyBindings, + keyBindings || [], ) } diff --git a/webapp/bridge.js b/webapp/bridge.js index 1d5f09f..48944bc 100644 --- a/webapp/bridge.js +++ b/webapp/bridge.js @@ -89,6 +89,7 @@ let initialSettings = { showLineNumberGutter: true, showFoldGutter: true, bracketClosing: false, + keyBindings: [], } if (settingsData !== null) { initialSettings = Object.assign(initialSettings, JSON.parse(settingsData)) From 4241a9d6ce34c5e626951f61191f68f5b5583f71 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 14 Apr 2025 14:53:36 +0200 Subject: [PATCH 194/228] Rename Emacs mark mode -> Selection mark mode --- src/editor/commands.js | 16 ++++++++-------- src/editor/copy-paste.js | 4 ++-- src/editor/editor.js | 2 +- src/editor/emacs-mode.js | 12 ++++++------ src/editor/keymap.js | 6 +++--- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/editor/commands.js b/src/editor/commands.js index 94248ce..890174d 100644 --- a/src/editor/commands.js +++ b/src/editor/commands.js @@ -29,13 +29,13 @@ import { transposeChars } from "./block/transpose-chars.js" import { cutCommand, copyCommand, pasteCommand } from "./copy-paste.js" -import { emacsMoveCommand, toggleEmacsMarkMode, emacsCancel } from "./emacs-mode.js" +import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./emacs-mode.js" -const cursorPreviousBlock = emacsMoveCommand(gotoPreviousBlock, selectPreviousBlock) -const cursorNextBlock = emacsMoveCommand(gotoNextBlock, selectNextBlock) -const cursorPreviousParagraph = emacsMoveCommand(gotoPreviousParagraph, selectPreviousParagraph) -const cursorNextParagraph = emacsMoveCommand(gotoNextParagraph, selectNextParagraph) +const cursorPreviousBlock = markModeMoveCommand(gotoPreviousBlock, selectPreviousBlock) +const cursorNextBlock = markModeMoveCommand(gotoNextBlock, selectNextBlock) +const cursorPreviousParagraph = markModeMoveCommand(gotoPreviousParagraph, selectPreviousParagraph) +const cursorNextParagraph = markModeMoveCommand(gotoNextParagraph, selectNextParagraph) const openLanguageSelector = (editor) => () => { @@ -71,8 +71,8 @@ const HEYNOTE_COMMANDS = { cursorPreviousParagraph, cursorNextParagraph, deleteBlock, deleteBlockSetCursorPreviousBlock, - toggleEmacsMarkMode, - emacsCancel, + toggleSelectionMarkMode, + selectionMarkModeCancel, openLanguageSelector, openBufferSelector, @@ -97,7 +97,7 @@ for (let commandSuffix of [ "SubwordBackward", "SubwordForward", "LineBoundaryBackward", "LineBoundaryForward", ]) { - HEYNOTE_COMMANDS[`cursor${commandSuffix}`] = emacsMoveCommand(codeMirrorCommands[`cursor${commandSuffix}`], codeMirrorCommands[`select${commandSuffix}`]) + HEYNOTE_COMMANDS[`cursor${commandSuffix}`] = markModeMoveCommand(codeMirrorCommands[`cursor${commandSuffix}`], codeMirrorCommands[`select${commandSuffix}`]) HEYNOTE_COMMANDS[`select${commandSuffix}`] = (editor) => codeMirrorCommands[`select${commandSuffix}`] } diff --git a/src/editor/copy-paste.js b/src/editor/copy-paste.js index cfac1d1..1974a71 100644 --- a/src/editor/copy-paste.js +++ b/src/editor/copy-paste.js @@ -60,7 +60,7 @@ export const heynoteCopyCut = (editor) => { } // if we're in Emacs mode, we want to exit mark mode in case we're in it - editor.emacsMarkMode = false + editor.selectionMarkMode = false // if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text if (editor.deselectOnCopy && event.type == "copy") { @@ -94,7 +94,7 @@ const copyCut = (view, cut, editor) => { } // if we're in Emacs mode, we want to exit mark mode in case we're in it - editor.emacsMarkMode = false + editor.selectionMarkMode = false // if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text if (editor.deselectOnCopy && !cut) { diff --git a/src/editor/editor.js b/src/editor/editor.js index b5484d1..f08927d 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -64,7 +64,7 @@ export class HeynoteEditor { this.notesStore = useHeynoteStore() this.errorStore = useErrorStore() this.name = "" - this.emacsMarkMode = false + this.selectionMarkMode = false const state = EditorState.create({ diff --git a/src/editor/emacs-mode.js b/src/editor/emacs-mode.js index 7d957f5..03e48a2 100644 --- a/src/editor/emacs-mode.js +++ b/src/editor/emacs-mode.js @@ -6,9 +6,9 @@ import { * Takes a command that moves the cursor and a command that marks the selection, and returns a new command that * will run the mark command if we're in Emacs mark mode, or the move command otherwise. */ -export function emacsMoveCommand(defaultCmd, markModeCmd) { +export function markModeMoveCommand(defaultCmd, markModeCmd) { return (editor) => { - if (editor.emacsMarkMode) { + if (editor.selectionMarkMode) { return (view) => { markModeCmd(view) // we need to return true here instead of returning what the default command returns, since the default @@ -23,17 +23,17 @@ export function emacsMoveCommand(defaultCmd, markModeCmd) { } -export function toggleEmacsMarkMode(editor) { +export function toggleSelectionMarkMode(editor) { return (view) => { - editor.emacsMarkMode = !editor.emacsMarkMode + editor.selectionMarkMode = !editor.selectionMarkMode return true } } -export function emacsCancel(editor) { +export function selectionMarkModeCancel(editor) { return (view) => { simplifySelection(view) - editor.emacsMarkMode = false + editor.selectionMarkMode = false return true } } diff --git a/src/editor/keymap.js b/src/editor/keymap.js index ca52817..b2d104b 100644 --- a/src/editor/keymap.js +++ b/src/editor/keymap.js @@ -105,9 +105,9 @@ export const EMACS_KEYMAP = [ cmd("Ctrl-w", "cut"), cmd("Ctrl-y", "paste"), cmd("EmacsMeta-w", "copy"), - cmd("Ctrl-Space", "toggleEmacsMarkMode"), - cmd("Ctrl-g", "emacsCancel"), - cmd("Escape", "emacsCancel"), + cmd("Ctrl-Space", "toggleSelectionMarkMode"), + cmd("Ctrl-g", "selectionMarkModeCancel"), + cmd("Escape", "selectionMarkModeCancel"), cmd("Ctrl-o", "splitLine"), cmd("Ctrl-d", "deleteCharForward"), cmd("Ctrl-h", "deleteCharBackward"), From c6cae175228cce945651890db392a2f9b3f59d5e Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 14 Apr 2025 14:55:11 +0200 Subject: [PATCH 195/228] Rename Emacs mark mode -> Selection mark mode --- src/editor/commands.js | 2 +- src/editor/{emacs-mode.js => mark-mode.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/editor/{emacs-mode.js => mark-mode.js} (100%) diff --git a/src/editor/commands.js b/src/editor/commands.js index 890174d..1844ec2 100644 --- a/src/editor/commands.js +++ b/src/editor/commands.js @@ -84,7 +84,7 @@ const HEYNOTE_COMMANDS = { copy: copyCommand, } -// emacs-mode:ify all cursor/select commands from CodeMirror +// selection mark-mode:ify all cursor/select commands from CodeMirror for (let commandSuffix of [ "CharLeft", "CharRight", "CharBackward", "CharForward", diff --git a/src/editor/emacs-mode.js b/src/editor/mark-mode.js similarity index 100% rename from src/editor/emacs-mode.js rename to src/editor/mark-mode.js From 28fb986250c0084c77919358efc0b25b573f5a6a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 14 Apr 2025 15:08:49 +0200 Subject: [PATCH 196/228] Rename Emacs mark mode -> Selection mark mode --- src/editor/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/commands.js b/src/editor/commands.js index 1844ec2..b4df7df 100644 --- a/src/editor/commands.js +++ b/src/editor/commands.js @@ -29,7 +29,7 @@ import { transposeChars } from "./block/transpose-chars.js" import { cutCommand, copyCommand, pasteCommand } from "./copy-paste.js" -import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./emacs-mode.js" +import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./mark-mode.js" const cursorPreviousBlock = markModeMoveCommand(gotoPreviousBlock, selectPreviousBlock) From bcaa2d300657a82a0d5ae6cb6e8ceb47402d7a9f Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 14 Apr 2025 16:18:23 +0200 Subject: [PATCH 197/228] Increase default window size slightly, and increase size of Settings dialog --- electron/main/index.ts | 4 ++-- src/components/settings/Settings.vue | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 31bad25..07dcb1a 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -83,8 +83,8 @@ export function quit() { async function createWindow() { // read any stored window settings from config, or use defaults let windowConfig = { - width: CONFIG.get("windowConfig.width", 900) as number, - height: CONFIG.get("windowConfig.height", 680) as number, + width: CONFIG.get("windowConfig.width", 940) as number, + height: CONFIG.get("windowConfig.height", 720) as number, isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean, isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean, x: CONFIG.get("windowConfig.x"), diff --git a/src/components/settings/Settings.vue b/src/components/settings/Settings.vue index cb399c6..7b21a6b 100644 --- a/src/components/settings/Settings.vue +++ b/src/components/settings/Settings.vue @@ -446,7 +446,7 @@ background: rgba(0, 0, 0, 0.5) .dialog - --dialog-height: 560px + --dialog-height: 600px --bottom-bar-height: 48px box-sizing: border-box z-index: 2 @@ -454,7 +454,7 @@ left: 50% top: 50% transform: translate(-50%, -50%) - width: 700px + width: 820px height: var(--dialog-height) max-width: 100% max-height: 100% From a080b627e0a97fbbc7ac0c6d6bb7a00bbaa2a317 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Mon, 14 Apr 2025 16:19:43 +0200 Subject: [PATCH 198/228] Add descriptions and categories to commands Use the descriptions and categories in the Command palette and Settings dialog --- src/components/BufferSelector.vue | 17 ++- src/components/settings/KeyBindRow.vue | 25 ++-- src/components/settings/KeyboardBindings.vue | 2 +- src/editor/commands.js | 132 ++++++++++++------- src/editor/editor.js | 2 +- src/editor/keymap.js | 2 +- 6 files changed, 115 insertions(+), 65 deletions(-) diff --git a/src/components/BufferSelector.vue b/src/components/BufferSelector.vue index 246c1d7..1a9b24a 100644 --- a/src/components/BufferSelector.vue +++ b/src/components/BufferSelector.vue @@ -52,9 +52,20 @@ ]), commands() { - return Object.keys(HEYNOTE_COMMANDS).map(cmd => ({ - name: cmd, - cmd: cmd, + const commands = Object.entries(HEYNOTE_COMMANDS) + // sort array first by category, then by description + commands.sort((a, b) => { + const aCategory = a[1].category || "" + const bCategory = b[1].category || "" + if (aCategory === bCategory) { + return a[1].description.localeCompare(b[1].description) + } else { + return aCategory.localeCompare(bCategory) + } + }) + return commands.map(([cmdKey, cmd]) => ({ + name: `${cmd.category}: ${cmd.description}`, + cmd: cmdKey, isCommand: true, })) }, diff --git a/src/components/settings/KeyBindRow.vue b/src/components/settings/KeyBindRow.vue index f4e304d..c436e43 100644 --- a/src/components/settings/KeyBindRow.vue +++ b/src/components/settings/KeyBindRow.vue @@ -1,4 +1,6 @@ @@ -29,8 +39,7 @@ - Unbound - {{ command }} + {{ commandLabel }}