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)}` +}