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
},