Gutters
diff --git a/src/editor/annotation.js b/src/editor/annotation.js
index 6b4e83cb..fae768ef 100644
--- a/src/editor/annotation.js
+++ b/src/editor/annotation.js
@@ -5,3 +5,5 @@ export const LANGUAGE_CHANGE = "heynote-change"
export const CURRENCIES_LOADED = "heynote-currencies-loaded"
export const SET_CONTENT = "heynote-set-content"
export const ADD_NEW_BLOCK = "heynote-add-new-block"
+export const DELETE_BLOCK = "heynote-delete-block"
+export const CURSOR_CHANGE = "heynote-cursor-change"
diff --git a/src/editor/block/block.js b/src/editor/block/block.js
index 57e2bb11..cb382969 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 { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
-import { SelectionChangeEvent } from "../event.js"
+import { useHeynoteStore } from "../../stores/heynote-store.js"
+import { heynoteEvent, LANGUAGE_CHANGE, CURSOR_CHANGE } from "../annotation.js";
import { mathBlock } from "./math.js"
import { emptyBlockSelected } from "./select-all.js";
@@ -404,32 +404,43 @@ function getSelectionSize(state, sel) {
return count
}
-const emitCursorChange = (editor) => ViewPlugin.fromClass(
- class {
- update(update) {
- // if the selection changed or the language changed (can happen without selection change),
- // emit a selection change event
- const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))
- if (update.selectionSet || langChange) {
- const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
-
- const selectionSize = update.state.selection.ranges.map(
- (sel) => getSelectionSize(update.state, sel)
- ).reduce((a, b) => a + b, 0)
-
- const block = getActiveNoteBlock(update.state)
- if (block && cursorLine) {
- editor.element.dispatchEvent(new SelectionChangeEvent({
- cursorLine,
- selectionSize,
- language: block.language.name,
- languageAuto: block.language.auto,
- }))
+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 heynoteStore = useHeynoteStore()
+ 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 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(
+ (sel) => getSelectionSize(update.state, sel)
+ ).reduce((a, b) => a + b, 0)
+
+ const block = getActiveNoteBlock(update.state)
+ if (block && cursorLine) {
+ heynoteStore.currentCursorLine = cursorLine
+ heynoteStore.currentSelectionSize = selectionSize
+ heynoteStore.currentLanguage = block.language.name
+ heynoteStore.currentLanguageAuto = block.language.auto
+ heynoteStore.currentBufferName = editor.name
+ }
}
}
}
- }
-)
+ )
+}
export const noteBlockExtension = (editor) => {
return [
diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js
index b55c50bf..fe9bf3bb 100644
--- a/src/editor/block/commands.js
+++ b/src/editor/block/commands.js
@@ -1,5 +1,6 @@
-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 +8,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 +318,26 @@ 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 = ""
+ let newSelection = block.delimiter.from
+ if (blocks.length == 1) {
+ replace = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
+ newSelection = replace.length
+ }
+ dispatch(state.update({
+ changes: {
+ from: block.range.from,
+ to: block.range.to,
+ insert: replace,
+ },
+ selection: EditorSelection.cursor(newSelection),
+ annotations: [heynoteEvent.of(DELETE_BLOCK)],
}))
}
diff --git a/src/editor/editor.js b/src/editor/editor.js
index ecc1f5ba..85317f68 100644
--- a/src/editor/editor.js
+++ b/src/editor/editor.js
@@ -1,4 +1,4 @@
-import { Annotation, EditorState, Compartment, Facet } from "@codemirror/state"
+import { Annotation, EditorState, Compartment, Facet, EditorSelection, 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"
@@ -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"
@@ -21,8 +21,11 @@ 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 "../common/note-format.js"
+import { AUTO_SAVE_INTERVAL } from "../common/constants.js"
+import { useHeynoteStore } from "../stores/heynote-store.js";
+import { useErrorStore } from "../stores/error-store.js";
-export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector"
function getKeymapExtensions(editor, keymap) {
if (keymap === "emacs") {
@@ -36,10 +39,10 @@ function getKeymapExtensions(editor, keymap) {
export class HeynoteEditor {
constructor({
element,
+ path,
content,
focus=true,
theme="light",
- saveFunction=null,
keymap="default",
emacsMetaKey,
showLineNumberGutter=true,
@@ -47,8 +50,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
@@ -59,11 +65,15 @@ export class HeynoteEditor {
this.deselectOnCopy = keymap === "emacs"
this.emacsMetaKey = emacsMetaKey
this.fontTheme = new Compartment
- this.defaultBlockToken = "text"
- this.defaultBlockAutoDetect = true
+ this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect)
+ this.contentLoaded = false
+ this.notesStore = useHeynoteStore()
+ this.errorStore = useErrorStore()
+ this.name = ""
+
const state = EditorState.create({
- doc: content || "",
+ doc: "",
extensions: [
this.keymapCompartment.of(getKeymapExtensions(this, keymap)),
heynoteCopyCut(this),
@@ -86,7 +96,7 @@ export class HeynoteEditor {
}),
heynoteLang(),
noteBlockExtension(this),
- languageDetection(() => this),
+ languageDetection(path, () => this),
// set cursor blink rate to 1 second
drawSelection({cursorBlinkRate:1000}),
@@ -96,7 +106,7 @@ export class HeynoteEditor {
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
}),
- saveFunction ? autoSaveContent(saveFunction, 2000) : [],
+ autoSaveContent(this, AUTO_SAVE_INTERVAL),
todoCheckboxPlugin,
markdown(),
@@ -105,49 +115,118 @@ export class HeynoteEditor {
})
// make sure saveFunction is called when page is unloaded
- if (saveFunction) {
- window.addEventListener("beforeunload", () => {
- saveFunction(this.getContent())
- })
- }
+ window.addEventListener("beforeunload", () => {
+ this.save()
+ })
this.view = new EditorView({
state: state,
parent: element,
})
-
- // Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers
- // when moving the cursor to the end of the buffer when the program starts
- ensureSyntaxTree(state, state.doc.length, 5000)
+
+ //this.setContent(content)
+ this.setReadOnly(true)
+ this.loadContent().then(() => {
+ this.setReadOnly(false)
+ })
if (focus) {
- this.view.dispatch({
- selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length},
- scrollIntoView: true,
- })
this.view.focus()
}
}
+ 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() {
- return this.view.state.sliceDoc()
+ 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) {
- this.view.dispatch({
- changes: {
- from: 0,
- to: this.view.state.doc.length,
- insert: content,
- },
- annotations: [heynoteEvent.of(SET_CONTENT)],
- })
- this.view.dispatch({
- selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length},
- scrollIntoView: true,
+ try {
+ this.note = NoteFormat.load(content)
+ 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
+
+ return new Promise((resolve) => {
+ // set buffer content
+ this.view.dispatch({
+ changes: {
+ from: 0,
+ to: this.view.state.doc.length,
+ insert: this.note.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
+ // when moving the cursor to the end of the buffer when the program starts
+ ensureSyntaxTree(this.view.state, this.view.state.doc.length, 5000)
+
+ // Set cursor positions
+ // We use requestAnimationFrame to avoid a race condition causing the scrollIntoView to sometimes not work
+ requestAnimationFrame(() => {
+ if (this.note.cursors) {
+ this.view.dispatch({
+ selection: EditorSelection.fromJSON(this.note.cursors),
+ scrollIntoView: true,
+ })
+ } else {
+ // if metadata doesn't contain cursor position, we set the cursor to the end of the buffer
+ this.view.dispatch({
+ selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length},
+ scrollIntoView: true,
+ })
+ }
+ resolve()
+ })
})
}
+ setName(name) {
+ this.note.metadata.name = name
+ this.name = name
+ triggerCursorChange(this.view)
+ }
+
getBlocks() {
return this.view.state.facet(blockState)
}
@@ -187,7 +266,44 @@ export class HeynoteEditor {
}
openLanguageSelector() {
- this.element.dispatchEvent(new Event(LANGUAGE_SELECTOR_EVENT))
+ this.notesStore.openLanguageSelector()
+ }
+
+ openBufferSelector() {
+ this.notesStore.openBufferSelector()
+ }
+
+ openCreateBuffer(createMode) {
+ this.notesStore.openCreateBuffer(createMode)
+ }
+
+ async createNewBuffer(path, name) {
+ const data = getBlockDelimiter(this.defaultBlockToken, this.defaultBlockAutoDetect)
+ await this.notesStore.saveNewBuffer(path, name, data)
+
+ // 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.openBuffer(path)
+ })
+ }
+
+ async createNewBufferFromActiveBlock(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.saveNewBuffer(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.openBuffer(path)
+ })
}
setCurrentLanguage(lang, auto=false) {
@@ -227,6 +343,27 @@ export class HeynoteEditor {
currenciesLoaded() {
triggerCurrenciesLoaded(this.view.state, this.view.dispatch)
}
+
+ destroy(save=true) {
+ if (this.onChange) {
+ window.heynote.buffer.removeOnChangeCallback(this.path, this.onChange)
+ }
+ if (save) {
+ this.save()
+ }
+ 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/emacs.js b/src/editor/emacs.js
index b0022330..53fdfb23 100644
--- a/src/editor/emacs.js
+++ b/src/editor/emacs.js
@@ -103,8 +103,6 @@ export function emacsKeymap(editor) {
{ key: "Ctrl-b", run: emacsMoveCommand(cursorCharLeft, selectCharLeft), shift: selectCharLeft },
{ key: "Ctrl-f", run: emacsMoveCommand(cursorCharRight, selectCharRight), shift: selectCharRight },
- { key: "Ctrl-p", run: emacsMoveCommand(cursorLineUp, selectLineUp), shift: selectLineUp },
- { key: "Ctrl-n", run: emacsMoveCommand(cursorLineDown, selectLineDown), shift: selectLineDown },
{ key: "Ctrl-a", run: emacsMoveCommand(cursorLineStart, selectLineStart), shift: selectLineStart },
{ key: "Ctrl-e", run: emacsMoveCommand(cursorLineEnd, selectLineEnd), shift: selectLineEnd },
])),
diff --git a/src/editor/event.js b/src/editor/event.js
deleted file mode 100644
index 34f59601..00000000
--- a/src/editor/event.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export class SelectionChangeEvent extends Event {
- constructor({cursorLine, language, languageAuto, selectionSize}) {
- super("selectionChange")
- this.cursorLine = cursorLine
- this.selectionSize = selectionSize
- this.language = language
- this.languageAuto = languageAuto
- }
-}
diff --git a/src/editor/keymap.js b/src/editor/keymap.js
index a7e32ed1..15914ef3 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"
@@ -57,6 +58,10 @@ export function heynoteKeymap(editor) {
["Alt-ArrowUp", moveLineUp],
["Alt-ArrowDown", moveLineDown],
["Mod-l", () => editor.openLanguageSelector()],
+ ["Mod-p", () => editor.openBufferSelector()],
+ ["Mod-s", () => editor.openCreateBuffer("currentBlock")],
+ ["Mod-n", () => editor.openCreateBuffer("new")],
+ ["Mod-Shift-d", deleteBlock(editor)],
["Alt-Shift-f", formatBlockContent],
["Mod-Alt-ArrowDown", newCursorBelow],
["Mod-Alt-ArrowUp", newCursorAbove],
diff --git a/src/editor/language-detection/autodetect.js b/src/editor/language-detection/autodetect.js
index 771170ea..4e08237f 100644
--- a/src/editor/language-detection/autodetect.js
+++ b/src/editor/language-detection/autodetect.js
@@ -1,5 +1,5 @@
import { EditorState } from "@codemirror/state";
-import { EditorView } from "@codemirror/view";
+import { EditorView, ViewPlugin } from "@codemirror/view";
import { redoDepth } from "@codemirror/commands";
import { getActiveNoteBlock, blockState } from "../block/block";
import { levenshtein_distance } from "./levenshtein";
@@ -25,95 +25,112 @@ function cancelIdleCallbackCompat(id) {
}
}
-export function languageDetection(getEditor) {
- const previousBlockContent = {}
- let idleCallbackId = null
-
- const detectionWorker = new Worker('langdetect-worker.js?worker');
- detectionWorker.onmessage = (event) => {
- //console.log("event:", event.data)
- if (!event.data.guesslang.language) {
- return
- }
- const editor = getEditor()
- const view = editor.view
- const state = view.state
- const block = getActiveNoteBlock(state)
- const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
- if (block.language.auto === true && block.language.name !== newLang) {
- console.log("New auto detected language:", newLang, "Confidence:", event.data.guesslang.confidence)
- let content = state.doc.sliceString(block.content.from, block.content.to)
- const threshold = content.length * 0.1
- if (levenshtein_distance(content, event.data.content) <= threshold) {
- // the content has not changed significantly so it's safe to change the language
- if (redoDepth(state) === 0) {
- console.log("Changing language to", newLang)
- changeLanguageTo(state, view.dispatch, block, newLang, true)
- } else {
- console.log("Not changing language because the user has undo:ed and has redo history")
- }
+// we'll use a shared global web worker for the language detection, for multiple Editor instances
+const editorInstances = {}
+const detectionWorker = new Worker('langdetect-worker.js?worker');
+detectionWorker.onmessage = (event) => {
+ //console.log("event:", event.data)
+ if (!event.data.guesslang.language) {
+ return
+ }
+
+ const editor = editorInstances[event.data.path]
+ //const editor = getEditor()
+ const view = editor.view
+ const state = view.state
+ const block = getActiveNoteBlock(state)
+ const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
+ if (block.language.auto === true && block.language.name !== newLang) {
+ console.log("New auto detected language:", newLang, "Confidence:", event.data.guesslang.confidence)
+ let content = state.doc.sliceString(block.content.from, block.content.to)
+ const threshold = content.length * 0.1
+ if (levenshtein_distance(content, event.data.content) <= threshold) {
+ // the content has not changed significantly so it's safe to change the language
+ if (redoDepth(state) === 0) {
+ console.log("Changing language to", newLang)
+ changeLanguageTo(state, view.dispatch, block, newLang, true)
} else {
- console.log("Content has changed significantly, not setting new language")
+ console.log("Not changing language because the user has undo:ed and has redo history")
}
+ } else {
+ console.log("Content has changed significantly, not setting new language")
}
}
+}
- const plugin = EditorView.updateListener.of(update => {
- if (update.docChanged) {
- if (idleCallbackId !== null) {
- cancelIdleCallbackCompat(idleCallbackId)
- idleCallbackId = null
- }
+export function languageDetection(path, getEditor) {
+ const previousBlockContent = {}
+ let idleCallbackId = null
+ const editor = getEditor()
+ editorInstances[path] = editor
- idleCallbackId = requestIdleCallbackCompat(() => {
- idleCallbackId = null
-
- const range = update.state.selection.asSingle().ranges[0]
- const blocks = update.state.facet(blockState)
- let block = null, idx = null;
- for (let i=0; i= range.from) {
- block = blocks[i]
- idx = i
- break
+ //const plugin = EditorView.updateListener.of(update => {
+ const plugin = ViewPlugin.fromClass(
+ class {
+ update(update) {
+ if (update.docChanged) {
+ if (idleCallbackId !== null) {
+ cancelIdleCallbackCompat(idleCallbackId)
+ idleCallbackId = null
}
- }
- if (block === null) {
- return
- } else if (block.language.auto === false) {
- // if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection
- // immediately if the user changes the language to auto
- delete previousBlockContent[idx]
- return
- }
- const content = update.state.doc.sliceString(block.content.from, block.content.to)
- if (content === "" && redoDepth(update.state) === 0) {
- // if content is cleared, set language to default
- const editor = getEditor()
- const view = editor.view
- const block = getActiveNoteBlock(view.state)
- if (block.language.name !== editor.defaultBlockToken) {
- changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true)
- }
- delete previousBlockContent[idx]
- }
- if (content.length <= 8) {
- return
- }
- const threshold = content.length * 0.1
- if (!previousBlockContent[idx] || levenshtein_distance(previousBlockContent[idx], content) >= threshold) {
- // the content has changed significantly, so schedule a language detection
- //console.log("Scheduling language detection for block", idx, "with threshold", threshold)
- detectionWorker.postMessage({
- content: content,
- idx: idx,
+ idleCallbackId = requestIdleCallbackCompat(() => {
+ idleCallbackId = null
+
+ const range = update.state.selection.asSingle().ranges[0]
+ const blocks = update.state.facet(blockState)
+ let block = null, idx = null;
+ for (let i=0; i= range.from) {
+ block = blocks[i]
+ idx = i
+ break
+ }
+ }
+ if (block === null) {
+ return
+ } else if (block.language.auto === false) {
+ // if language is not auto, set it's previousBlockContent to null so that we'll trigger a language detection
+ // immediately if the user changes the language to auto
+ delete previousBlockContent[idx]
+ return
+ }
+
+ const content = update.state.doc.sliceString(block.content.from, block.content.to)
+ if (content === "" && redoDepth(update.state) === 0) {
+ // if content is cleared, set language to default
+ //const editor = getEditor()
+ const view = editor.view
+ const block = getActiveNoteBlock(view.state)
+ if (block.language.name !== editor.defaultBlockToken) {
+ changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true)
+ }
+ delete previousBlockContent[idx]
+ }
+ if (content.length <= 8) {
+ return
+ }
+ const threshold = content.length * 0.1
+ if (!previousBlockContent[idx] || levenshtein_distance(previousBlockContent[idx], content) >= threshold) {
+ // the content has changed significantly, so schedule a language detection
+ //console.log("Scheduling language detection for block", idx, "with threshold", threshold)
+ detectionWorker.postMessage({
+ content: content,
+ idx: idx,
+ path: path,
+ })
+ previousBlockContent[idx] = content
+ }
})
- previousBlockContent[idx] = content
}
- })
+ }
+
+ destroy() {
+ console.log("Removing editorInstance for:", path)
+ delete editorInstances[path]
+ }
}
- })
+ )
return plugin
}
diff --git a/src/editor/save.js b/src/editor/save.js
index 81b74220..4763f6f3 100644
--- a/src/editor/save.js
+++ b/src/editor/save.js
@@ -1,20 +1,24 @@
import { ViewPlugin } from "@codemirror/view"
import { debounce } from "debounce"
+import { SET_CONTENT } from "./annotation"
-export const autoSaveContent = (saveFunction, interval) => {
- const save = debounce((view) => {
+export const autoSaveContent = (editor, interval) => {
+ const save = debounce(() => {
//console.log("saving buffer")
- saveFunction(view.state.sliceDoc())
+ editor.save()
}, interval);
return ViewPlugin.fromClass(
class {
update(update) {
if (update.docChanged) {
- save(update.view)
+ 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 7836648a..dd29cd10 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,17 +1,32 @@
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'
+import { useHeynoteStore, initHeynoteStore } from './stores/heynote-store'
+import { useEditorCacheStore } from './stores/editor-cache'
+const pinia = createPinia()
const app = createApp(App)
+app.use(pinia)
app.mount('#app').$nextTick(() => {
// hide loading screen
postMessage({ payload: 'removeLoading' }, '*')
})
+const errorStore = useErrorStore()
+const editorCacheStore = useEditorCacheStore()
+//errorStore.addError("test error")
+window.heynote.getInitErrors().then((errors) => {
+ errors.forEach((e) => errorStore.addError(e))
+})
+
+initHeynoteStore()
@@ -19,3 +34,4 @@ app.mount('#app').$nextTick(() => {
loadCurrencies()
setInterval(loadCurrencies, 1000 * 3600 * 4)
+window.heynote.init()
diff --git a/src/stores/editor-cache.js b/src/stores/editor-cache.js
new file mode 100644
index 00000000..a8b41e9c
--- /dev/null
+++ b/src/stores/editor-cache.js
@@ -0,0 +1,57 @@
+import { toRaw } from 'vue';
+import { defineStore } from "pinia"
+import { NoteFormat } from "../common/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(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/error-store.js b/src/stores/error-store.js
new file mode 100644
index 00000000..cc3aa1c6
--- /dev/null
+++ b/src/stores/error-store.js
@@ -0,0 +1,21 @@
+import { defineStore } from 'pinia'
+
+export const useErrorStore = defineStore("errors", {
+ state: () => ({
+ errors: [],
+ }),
+
+ actions: {
+ setErrors(errors) {
+ this.errors = errors
+ },
+
+ addError(error) {
+ this.errors.push(error)
+ },
+
+ popError() {
+ this.errors.splice(0, 1)
+ },
+ },
+})
diff --git a/src/stores/heynote-store.js b/src/stores/heynote-store.js
new file mode 100644
index 00000000..589d5fc4
--- /dev/null
+++ b/src/stores/heynote-store.js
@@ -0,0 +1,169 @@
+import { toRaw } from 'vue';
+import { defineStore } from "pinia"
+import { NoteFormat } from "../common/note-format"
+import { useEditorCacheStore } from "./editor-cache"
+import { SCRATCH_FILE_NAME } from "../common/constants"
+
+
+export const useHeynoteStore = defineStore("heynote", {
+ state: () => ({
+ buffers: {},
+ recentBufferPaths: [SCRATCH_FILE_NAME],
+
+ currentEditor: null,
+ currentBufferPath: SCRATCH_FILE_NAME,
+ currentBufferName: null,
+ currentLanguage: null,
+ currentLanguageAuto: null,
+ currentCursorLine: null,
+ currentSelectionSize: null,
+ libraryId: 0,
+ createBufferParams: {
+ mode: "new",
+ nameSuggestion: ""
+ },
+
+ showBufferSelector: false,
+ showLanguageSelector: false,
+ showCreateBuffer: false,
+ showEditBuffer: false,
+ }),
+
+ actions: {
+ async updateBuffers() {
+ this.setBuffers(await window.heynote.buffer.getList())
+ },
+
+ setBuffers(buffers) {
+ this.buffers = buffers
+ },
+
+ openBuffer(path) {
+ this.closeDialog()
+ this.currentBufferPath = path
+
+ const recent = this.recentBufferPaths.filter((p) => p !== path)
+ recent.unshift(path)
+ this.recentBufferPaths = recent.slice(0, 100)
+ },
+
+ openLanguageSelector() {
+ this.closeDialog()
+ this.showLanguageSelector = true
+ },
+ openBufferSelector() {
+ this.closeDialog()
+ this.showBufferSelector = true
+ },
+ openCreateBuffer(createMode, nameSuggestion) {
+ createMode = createMode || "new"
+ this.closeDialog()
+ this.createBufferParams = {
+ mode: createMode || "new",
+ name: nameSuggestion || ""
+ }
+ this.showCreateBuffer = true
+ },
+ closeDialog() {
+ this.showCreateBuffer = false
+ this.showBufferSelector = false
+ this.showLanguageSelector = false
+ this.showEditBuffer = false
+ },
+
+ closeBufferSelector() {
+ this.showBufferSelector = false
+ },
+
+ editBufferMetadata(path) {
+ if (this.currentBufferPath !== path) {
+ this.openBuffer(path)
+ }
+ this.closeDialog()
+ this.showEditBuffer = true
+ },
+
+ /**
+ * Create a new note file at `path` with name `name` from the current block of the current open editor,
+ * and switch to it
+ */
+ async createNewBufferFromActiveBlock(path, name) {
+ await toRaw(this.currentEditor).createNewBufferFromActiveBlock(path, name)
+ },
+
+ /**
+ * Create a new empty note file at `path` with name `name`, and switch to it
+ */
+ async createNewBuffer(path, name) {
+ await toRaw(this.currentEditor).createNewBuffer(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 saveNewBuffer(path, name, content) {
+ if (this.buffers[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.updateBuffers()
+ },
+
+ async updateBufferMetadata(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.openBuffer(newPath)
+ this.updateBuffers()
+ }
+ },
+
+ async deleteBuffer(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.currentBufferPath = SCRATCH_FILE_NAME
+ }
+ editorCacheStore.freeEditor(path)
+ await window.heynote.buffer.delete(path)
+ await this.updateBuffers()
+ },
+
+ async reloadLibrary() {
+ const editorCacheStore = useEditorCacheStore()
+ await this.updateBuffers()
+ editorCacheStore.clearCache(false)
+ this.currentEditor = null
+ this.currentBufferPath = SCRATCH_FILE_NAME
+ this.libraryId++
+ },
+ },
+})
+
+export async function initHeynoteStore() {
+ const heynoteStore = useHeynoteStore()
+ window.heynote.buffer.setLibraryPathChangeCallback(() => {
+ heynoteStore.reloadLibrary()
+ })
+ await heynoteStore.updateBuffers()
+}
diff --git a/tests/block-creation.spec.js b/tests/block-creation.spec.js
index 2d9e54c4..b00a55f1 100644
--- a/tests/block-creation.spec.js
+++ b/tests/block-creation.spec.js
@@ -8,7 +8,7 @@ test.beforeEach(async ({page}) => {
await heynotePage.goto()
expect((await heynotePage.getBlocks()).length).toBe(1)
- heynotePage.setContent(`
+ await heynotePage.setContent(`
∞∞∞text
Block A
∞∞∞text
diff --git a/tests/buffer-creation.spec.js b/tests/buffer-creation.spec.js
new file mode 100644
index 00000000..61b94b6e
--- /dev/null
+++ b/tests/buffer-creation.spec.js
@@ -0,0 +1,97 @@
+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`)
+
+})
+
+
+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
+`)
+})
diff --git a/tests/formatting.spec.js b/tests/formatting.spec.js
index 5b5bbac4..4a633100 100644
--- a/tests/formatting.spec.js
+++ b/tests/formatting.spec.js
@@ -10,7 +10,7 @@ test.beforeEach(async ({ page }) => {
test("JSON formatting", async ({ page }) => {
- heynotePage.setContent(`
+ await heynotePage.setContent(`
∞∞∞json
{"test": 1, "key2": "hey!"}
`)
@@ -25,7 +25,7 @@ test("JSON formatting", async ({ page }) => {
})
test("JSON formatting (cursor at start)", async ({ page }) => {
- heynotePage.setContent(`
+ await heynotePage.setContent(`
∞∞∞json
{"test": 1, "key2": "hey!"}
`)
diff --git a/tests/note-format.spec.js b/tests/note-format.spec.js
new file mode 100644
index 00000000..9822d039
--- /dev/null
+++ b/tests/note-format.spec.js
@@ -0,0 +1,62 @@
+import { test, expect } from "@playwright/test";
+import { HeynotePage } from "./test-utils.js";
+import { NoteFormat } from "../src/common/note-format.js";
+
+let heynotePage
+
+test.beforeEach(async ({ page }) => {
+ heynotePage = new HeynotePage(page)
+ await heynotePage.goto()
+});
+
+
+test("test restore cursor position", async ({ page, browserName }) => {
+ await heynotePage.setContent(`{"formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}}
+∞∞∞text
+Textblock`)
+ await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+Enter")
+ expect(await heynotePage.getContent()).toBe(`
+∞∞∞text
+Text
+∞∞∞text
+block`)
+})
+
+
+test("test save cursor positions", async ({ page, browserName }) => {
+ await heynotePage.setContent(`{"formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}}
+∞∞∞text
+this
+is
+a
+text
+block`)
+ await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+ArrowDown")
+ await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+ArrowDown")
+ await page.locator("body").press("Delete")
+ expect(await heynotePage.getContent()).toBe(`
+∞∞∞text
+his
+s
+
+text
+block`)
+
+ const bufferData = await heynotePage.getBufferData()
+ const note = NoteFormat.load(bufferData)
+ expect(note.cursors.ranges.length).toBe(3)
+})
+
+test("unknown note metadata keys is kept", async ({ page, browserName }) => {
+ await heynotePage.setContent(`{"yoda":[123], "formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":15,"head":15}],"main":0}}
+∞∞∞text
+block 1`)
+ await page.locator("body").pressSequentially("hello")
+ expect(await heynotePage.getContent()).toBe(`
+∞∞∞text
+block hello1`)
+
+ const bufferData = await heynotePage.getBufferData()
+ const note = NoteFormat.load(bufferData)
+ expect(note.metadata.yoda).toStrictEqual([123])
+})
\ No newline at end of file
diff --git a/tests/test-utils.js b/tests/test-utils.js
index 9e841c47..209701ac 100644
--- a/tests/test-utils.js
+++ b/tests/test-utils.js
@@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test';
+import { NoteFormat } from '../src/common/note-format.js';
export function pageErrorGetter(page) {
let messages = [];
@@ -26,10 +27,15 @@ export class HeynotePage {
return await this.page.evaluate(() => window._heynote_editor.getBlocks())
}
- async getContent() {
+ async getBufferData() {
return await this.page.evaluate(() => window._heynote_editor.getContent())
}
+ async getContent() {
+ const note = NoteFormat.load(await this.getBufferData())
+ return note.content
+ }
+
async setContent(content) {
await expect(this.page.locator("css=.cm-editor")).toBeVisible()
await this.page.evaluate((content) => window._heynote_editor.setContent(content), content)
@@ -50,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 06ef81b2..21866c57 100644
--- a/webapp/bridge.js
+++ b/webapp/bridge.js
@@ -1,4 +1,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
@@ -73,6 +77,49 @@ if (settingsData !== null) {
initialSettings = Object.assign(initialSettings, JSON.parse(settingsData))
}
+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 {}
+ }
+}
+
+// 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,
@@ -80,20 +127,77 @@ const Heynote = {
defaultFontSize: isMobileDevice ? 16 : 12,
buffer: {
- async load() {
- const content = localStorage.getItem("buffer")
- return content === null ? "\n∞∞∞text-a\n" : content
+ async load(path) {
+ //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(noteKey(path), content)
+ },
+
+ async create(path, content) {
+ 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) {
+
+ },
+
+ async exists(path) {
+ return localStorage.getItem(noteKey(path)) !== null
+ },
+
+ async getList() {
+ //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 save(content) {
- localStorage.setItem("buffer", content)
+ async getDirectoryList() {
+ 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 saveAndQuit(content) {
+ async close(path) {
},
- onChangeCallback(callback) {
+ _onChangeCallbacks: {},
+ addOnChangeCallback(path, callback) {
+
+ },
+ removeOnChangeCallback(path, callback) {
},
},
@@ -121,7 +225,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"
@@ -152,6 +256,14 @@ const Heynote = {
async getVersion() {
return __APP_VERSION__ + " (" + __GIT_HASH__ + ")"
},
+
+ async getInitErrors() {
+
+ },
+
+ setWindowTitle(title) {
+ document.title = title + " - Heynote"
+ },
}
export { Heynote, ipcRenderer}
diff --git a/webapp/main.js b/webapp/main.js
index 01d18da9..f02d7936 100644
--- a/webapp/main.js
+++ b/webapp/main.js
@@ -1,10 +1,14 @@
import '../src/css/application.sass'
import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
import App from '../src/components/App.vue'
import { loadCurrencies } from '../src/currency'
+const pinia = createPinia()
const app = createApp(App)
+app.use(pinia)
app.mount('#app')
//console.log("test:", app.hej.test)
diff --git a/webapp/vite.config.js b/webapp/vite.config.js
index 7191747b..748e3ead 100644
--- a/webapp/vite.config.js
+++ b/webapp/vite.config.js
@@ -26,7 +26,6 @@ const middleware = () => {
}
}
-
// https://vitejs.dev/config/
export default defineConfig({
publicDir: "../public",
@@ -54,5 +53,6 @@ export default defineConfig({
define: {
'__APP_VERSION__': JSON.stringify(process.env.npm_package_version),
'__GIT_HASH__': JSON.stringify(child.execSync('git rev-parse --short HEAD').toString().trim()),
+ '__TESTS__': process.env.HEYNOTE_TESTS,
},
})