diff --git a/electron/initial-content.ts b/electron/initial-content.ts
index 4c4a8d86..6602f073 100644
--- a/electron/initial-content.ts
+++ b/electron/initial-content.ts
@@ -20,13 +20,23 @@ if (isWindows || isLinux) {
const keyMaxLength = keyHelp.map(([key, help]) => key.length).reduce((a, b) => Math.max(a, b))
const keyHelpStr = keyHelp.map(([key, help]) => `${key.padEnd(keyMaxLength)} ${help}`).join("\n")
+// see src/editor/time.js for templates
+const getTime = () => {
+ // Return time in ISO8601 string YYYY-MM-DDTHH:mm:ssZ
+ return (new Date()).toISOString().replace(/\.\d+Z/,'Z')
+}
+
+export const newCreatedUpdatedTime = () => {
+ return `-c${getTime()}-u${getTime()}`
+}
+
export const initialContent = `
-∞∞∞text
+∞∞∞text${newCreatedUpdatedTime()}
Welcome to Heynote! 👋
${keyHelpStr}
-∞∞∞math
+∞∞∞math${newCreatedUpdatedTime()}
This is a Math block. Here, rows are evaluated as math expressions.
radius = 5
@@ -40,16 +50,16 @@ time = 3900 seconds to minutes
time * 2
1 EUR in USD
-∞∞∞markdown
+∞∞∞markdown${newCreatedUpdatedTime()}
In Markdown blocks, lists with [x] and [ ] are rendered as checkboxes:
- [x] Download Heynote
- [ ] Try out Heynote
-∞∞∞text-a
+∞∞∞text-a${newCreatedUpdatedTime()}
`
export const initialDevContent = initialContent + `
-∞∞∞python-a
+∞∞∞python-a${newCreatedUpdatedTime()}
# hmm
def my_func():
print("hejsan")
@@ -60,7 +70,7 @@ import {EditorView, keymap} from "@codemirror/view"
import {javascript} from "@codemirror/lang-javascript"
import {indentWithTab, insertTab, indentLess, indentMore} from "@codemirror/commands"
import {nord} from "./nord.mjs"
-∞∞∞javascript-a
+∞∞∞javascript-a${newCreatedUpdatedTime()}
let editor = new EditorView({
//extensions: [basicSetup, javascript()],
extensions: [
@@ -84,7 +94,7 @@ let editor = new EditorView({
],
parent: document.getElementById("editor"),
})
-∞∞∞json
+∞∞∞json${newCreatedUpdatedTime()}
{
"name": "heynote-codemirror",
"type": "module",
@@ -112,7 +122,7 @@ let editor = new EditorView({
"typescript": "^4.9.4"
}
}
-∞∞∞html
+∞∞∞html${newCreatedUpdatedTime()}
Test
@@ -124,9 +134,9 @@ let editor = new EditorView({
-∞∞∞sql
+∞∞∞sql${newCreatedUpdatedTime()}
SELECT * FROM table WHERE id = 1;
-∞∞∞text
+∞∞∞text${newCreatedUpdatedTime()}
Shopping list:
- Milk
diff --git a/src/components/App.vue b/src/components/App.vue
index d689648f..4dd6804d 100644
--- a/src/components/App.vue
+++ b/src/components/App.vue
@@ -26,6 +26,8 @@
showLanguageSelector: false,
showSettings: false,
settings: window.heynote.settings,
+ createdTime: "",
+ updatedTime: "",
}
},
@@ -84,6 +86,8 @@
this.selectionSize = e.selectionSize
this.language = e.language
this.languageAuto = e.languageAuto
+ this.createdTime = e.createdTime
+ this.updatedTime = e.updatedTime
},
openLanguageSelector() {
@@ -132,6 +136,8 @@
:themeSetting="themeSetting"
:autoUpdate="settings.autoUpdate"
:allowBetaVersions="settings.allowBetaVersions"
+ :createdTime="createdTime"
+ :updatedTime="updatedTime"
@toggleTheme="toggleTheme"
@openLanguageSelector="openLanguageSelector"
@formatCurrentBlock="formatCurrentBlock"
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 95ca6838..ddb9bd1d 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -37,6 +37,8 @@
selectionSize: e.selectionSize,
language: e.language,
languageAuto: e.languageAuto,
+ createdTime: e.createdTime,
+ updatedTime: e.updatedTime,
})
})
diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue
index b671a786..08381c96 100644
--- a/src/components/StatusBar.vue
+++ b/src/components/StatusBar.vue
@@ -16,6 +16,8 @@
"themeSetting",
"autoUpdate",
"allowBetaVersions",
+ "createdTime",
+ "updatedTime",
],
components: {
@@ -68,6 +70,11 @@
Sel {{ selectionSize }}
+
+ Updated {{ updatedTime }}
+
+
+
l.token).join("|")
// tracks the size of the first delimiter
let firstBlockDelimiterSize
@@ -25,12 +28,19 @@ function getBlocks(state, timeout=50) {
const langNode = type.node.getChild("NoteLanguage")
const language = state.doc.sliceString(langNode.from, langNode.to)
const isAuto = !!type.node.getChild("Auto")
+ const createdAtNode = type.node.getChild("NoteCreated")
+ const updatedAtNode = type.node.getChild("NoteUpdated")
const contentNode = type.node.nextSibling
+
blocks.push({
language: {
name: language,
auto: isAuto,
},
+ time: {
+ created: createdAtNode ? state.doc.sliceString(createdAtNode.from, createdAtNode.to) : null,
+ updated: updatedAtNode ? state.doc.sliceString(updatedAtNode.from, updatedAtNode.to) : null,
+ },
content: {
from: contentNode.from,
to: contentNode.to,
@@ -326,6 +336,8 @@ const emitCursorChange = (editor) => ViewPlugin.fromClass(
selectionSize,
language: block.language.name,
languageAuto: block.language.auto,
+ createdTime: displayTime(block.time.created),
+ updatedTime: displayTime(block.time.updated),
}))
}
}
@@ -333,6 +345,35 @@ const emitCursorChange = (editor) => ViewPlugin.fromClass(
}
)
+const updateTimeOnChange = EditorState.transactionFilter.of((tr) => {
+ if (!tr.docChanged) return tr
+
+ const state = tr.startState
+ const block = getActiveNoteBlock(state)
+
+ // Block updates to time when deleting the last content in a block
+ if ((block.content.from === block.content.to) && !tr.changes.inserted.length) return tr
+
+ // this adds a slight debounce so the delimiter is only updated every second
+ const updatedTime = newUpdatedTime()
+ if (block.time.updated === updatedTime) return tr
+
+ const language = block.language.name
+ const auto = block.language.auto
+ const createdTimeStr = block.time.created || ""
+ const updatedTimeStr = block.time.updated ? updatedTime : ""
+
+ // return original transaction, with additional transaction to update time in delimiter
+ return [tr, {
+ changes: {
+ from: block.delimiter.from,
+ to: block.delimiter.to,
+ insert: `\n∞∞∞${language}${auto ? '-a' : ''}${createdTimeStr}${updatedTimeStr}\n`,
+ },
+ filter: false
+ }]
+})
+
export const noteBlockExtension = (editor) => {
return [
blockState,
@@ -344,5 +385,8 @@ export const noteBlockExtension = (editor) => {
emitCursorChange(editor),
mathBlock,
emptyBlockSelected,
+ updateTimeOnChange,
]
}
+
+
diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js
index c52d5fa0..c4e54a5a 100644
--- a/src/editor/block/commands.js
+++ b/src/editor/block/commands.js
@@ -1,11 +1,14 @@
import { EditorSelection } from "@codemirror/state"
+import { LANGUAGES } from '../languages.js';
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED } from "../annotation.js";
-import { blockState, getActiveNoteBlock, getNoteBlockFromPos } from "./block"
+import { blockState, getActiveNoteBlock, getNoteBlockFromPos} from "./block"
import { moveLineDown, moveLineUp } from "./move-lines.js";
import { selectAll } from "./select-all.js";
+import { newCreatedUpdatedTime, newUpdatedTime, timeMatcher } from "../time.js";
-export { moveLineDown, moveLineUp, selectAll }
+const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
+export { moveLineDown, moveLineUp, selectAll }
export const insertNewBlockAtCursor = ({ state, dispatch }) => {
if (state.readOnly)
@@ -14,9 +17,9 @@ export const insertNewBlockAtCursor = ({ state, dispatch }) => {
const currentBlock = getActiveNoteBlock(state)
let delimText;
if (currentBlock) {
- delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}\n`
+ delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}${newCreatedUpdatedTime()}\n`
} else {
- delimText = "\n∞∞∞text-a\n"
+ delimText = `\n∞∞∞text-a${newCreatedUpdatedTime()}\n`
}
dispatch(state.replaceSelection(delimText),
{
@@ -32,7 +35,7 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
if (state.readOnly)
return false
const block = getActiveNoteBlock(state)
- const delimText = "\n∞∞∞text-a\n"
+ const delimText = `\n∞∞∞text-a${newCreatedUpdatedTime()}\n`
dispatch(state.update({
changes: {
@@ -50,14 +53,16 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
export function changeLanguageTo(state, dispatch, block, language, auto) {
if (state.readOnly)
return false
- const delimRegex = /^\n∞∞∞[a-z]{0,16}(-a)?\n/g
+ const delimRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-a)?(-c${timeMatcher})?(-u${timeMatcher})?\\n`, "g")
if (state.doc.sliceString(block.delimiter.from, block.delimiter.to).match(delimRegex)) {
- //console.log("changing language to", language)
+ const createdTimeStr = block.time.created || ""
+ const updatedTimeStr = block.time.updated ? newUpdatedTime() : ""
+ // console.log("changing language to", language)
dispatch(state.update({
changes: {
from: block.delimiter.from,
to: block.delimiter.to,
- insert: `\n∞∞∞${language}${auto ? '-a' : ''}\n`,
+ insert: `\n∞∞∞${language}${auto ? '-a' : ''}${createdTimeStr}${updatedTimeStr}\n`,
},
annotations: [heynoteEvent.of(LANGUAGE_CHANGE)],
}))
diff --git a/src/editor/block/move-lines.js b/src/editor/block/move-lines.js
index 584732b6..72571fce 100644
--- a/src/editor/block/move-lines.js
+++ b/src/editor/block/move-lines.js
@@ -1,9 +1,10 @@
import { EditorSelection } from "@codemirror/state"
import { blockState } from "./block"
import { LANGUAGES } from '../languages.js';
+import { timeMatcher } from '../time.js';
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
-const tokenRegEx = new RegExp(`^∞∞∞(${languageTokensMatcher})(-a)?$`, "g")
+const tokenRegEx = new RegExp(`^∞∞∞(${languageTokensMatcher})(-a)?(-c${timeMatcher})?(-u${timeMatcher})?$`, "g")
function selectedLineBlocks(state) {
diff --git a/src/editor/copy-paste.js b/src/editor/copy-paste.js
index dc0f1d3a..da467ee1 100644
--- a/src/editor/copy-paste.js
+++ b/src/editor/copy-paste.js
@@ -2,11 +2,12 @@ import { EditorState, EditorSelection } from "@codemirror/state"
import { EditorView } from "@codemirror/view"
import { LANGUAGES } from './languages.js';
+import { timeMatcher } from './time.js';
import { setEmacsMarkMode } from "./emacs.js"
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
-const blockSeparatorRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-a)?\\n`, "g")
+const blockSeparatorRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-a)?(-c${timeMatcher})?(-u${timeMatcher})?\\n`, "g")
function copiedRange(state) {
diff --git a/src/editor/event.js b/src/editor/event.js
index 34f59601..96317c86 100644
--- a/src/editor/event.js
+++ b/src/editor/event.js
@@ -1,9 +1,11 @@
export class SelectionChangeEvent extends Event {
- constructor({cursorLine, language, languageAuto, selectionSize}) {
+ constructor({cursorLine, language, languageAuto, selectionSize, createdTime, updatedTime}) {
super("selectionChange")
this.cursorLine = cursorLine
this.selectionSize = selectionSize
this.language = language
this.languageAuto = languageAuto
+ this.createdTime = createdTime
+ this.updatedTime = updatedTime
}
}
diff --git a/src/editor/lang-heynote/external-tokens.js b/src/editor/lang-heynote/external-tokens.js
index 7b44c412..bba9f727 100644
--- a/src/editor/lang-heynote/external-tokens.js
+++ b/src/editor/lang-heynote/external-tokens.js
@@ -1,6 +1,7 @@
import { ExternalTokenizer } from '@lezer/lr'
import { NoteContent } from "./parser.terms.js"
import { LANGUAGES } from '../languages.js';
+import { timeMatcher } from '../time.js';
const EOF = -1;
@@ -8,7 +9,7 @@ const FIRST_TOKEN_CHAR = "\n".charCodeAt(0)
const SECOND_TOKEN_CHAR = "∞".charCodeAt(0)
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
-const tokenRegEx = new RegExp(`^\\n∞∞∞(${languageTokensMatcher})(-a)?\\n`, "g")
+const tokenRegEx = new RegExp(`^\\n∞∞∞(${languageTokensMatcher})(-a)?(-c${timeMatcher})?(-u${timeMatcher})?\\n`, "g")
export const noteContent = new ExternalTokenizer((input) => {
let current = input.peek(0);
@@ -23,7 +24,7 @@ export const noteContent = new ExternalTokenizer((input) => {
// so we don't need to check for the rest of the token
if (current === FIRST_TOKEN_CHAR && next === SECOND_TOKEN_CHAR) {
let potentialLang = "";
- for (let i=0; i<18; i++) {
+ for (let i=0; i<62; i++) {
potentialLang += String.fromCharCode(input.peek(i));
}
if (potentialLang.match(tokenRegEx)) {
diff --git a/src/editor/lang-heynote/heynote.grammar b/src/editor/lang-heynote/heynote.grammar
index 8d2ca5cb..0f1fd268 100644
--- a/src/editor/lang-heynote/heynote.grammar
+++ b/src/editor/lang-heynote/heynote.grammar
@@ -5,15 +5,28 @@ Note {
}
NoteDelimiter {
- noteDelimiterEnter noteDelimiterMark NoteLanguage Auto? noteDelimiterEnter
+ noteDelimiterEnter noteDelimiterMark NoteLanguage Auto? NoteCreated? NoteUpdated? noteDelimiterEnter
}
+NoteCreated {
+ noteCreatedDelimiter time
+}
+
+NoteUpdated {
+ noteUpdatedDelimiter time
+}
@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" }
Auto { "-a" }
noteDelimiterEnter { "\n" }
+ noteCreatedDelimiter { "-c" }
+ noteUpdatedDelimiter { "-u" }
+ time {
+ @digit @digit @digit @digit "-" @digit @digit "-" @digit @digit "T"
+ @digit @digit ":" @digit @digit ":" @digit @digit "Z"
+ }
//NoteContent { String }
//String { (![∞])+ }
}
diff --git a/src/editor/lang-heynote/parser.js b/src/editor/lang-heynote/parser.js
index 4bc9d2ec..7aa86a7d 100644
--- a/src/editor/lang-heynote/parser.js
+++ b/src/editor/lang-heynote/parser.js
@@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr"
import {noteContent} from "./external-tokens.js"
export const parser = LRParser.deserialize({
version: 14,
- states: "!jQQOPOOOVOPO'#C`O[OQO'#C_OOOO'#Cc'#CcQQOPOOOaOPO,58zOOOO,58y,58yOOOO-E6a-E6aOfOPO1G.fOOOQ7+$Q7+$QOnOPO7+$QOOOQ< {
+ // Return time in ISO8601 string YYYY-MM-DDTHH:mm:ssZ
+ return (new Date()).toISOString().replace(/\.\d+Z/,'Z')
+}
+
+export const newCreatedTime = () => {
+ return `-c${getTime()}`
+}
+
+export const newUpdatedTime = () => {
+ return `-u${getTime()}`
+}
+
+export const newCreatedUpdatedTime = () => {
+ return `${newCreatedTime()}${newUpdatedTime()}`
+}
+
+export const timeMatcher = '\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z)'
+
+export const displayTime = (t) => {
+ if (!t) return ""
+
+ // create Date object from delimiter time string
+ const dt = new Date(t.slice(2,))
+
+ // Present year if its not equal to this one
+ if (dt.getFullYear() !== THIS_YEAR) {
+ return `${dt.toTimeString().slice(0,5)} ${dt.toDateString().slice(4, 10)}, ${dt.getFullYear()}`
+ }
+
+ return `${dt.toTimeString().slice(0,5)} ${dt.toDateString().slice(4, 10)}`
+}