Merge branch 'main' into patch-1

This commit is contained in:
Jonatan Heyman 2024-07-14 13:16:15 +02:00
commit e84b36273a
24 changed files with 446 additions and 170 deletions

View File

@ -3,6 +3,12 @@
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/heyman/heynote)](https://github.com/heyman/heynote/releases)
[![Build Status](https://github.com/heyman/heynote/workflows/Tests/badge.svg)](https://github.com/heyman/heynote/actions?query=workflow%3ATests)
<img src="https://heynote.com/img/logo.png" style="width:79px;">
## General Information
- Website: [heynote.com](https://heynote.com)
- Documentation: [heynote.com](https://heynote.com/docs/)
Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc.
@ -16,7 +22,7 @@ Available for Mac, Windows, and Linux.
- Block-based
- Syntax highlighting:
C++, C#, Clojure, CSS, Erlang, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Shell, SQL, Swift, XML, YAML
C++, C#, Clojure, CSS, Erlang, Dart, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Shell, SQL, Swift, XML, YAML
- Language auto-detection
- Auto-formatting
@ -28,19 +34,9 @@ Available for Mac, Windows, and Linux.
- Default or Emacs-like key bindings
## Installation
## Documentation
Download the appropriate (Mac, Windows or Linux) version from the latest Github release (or from [heynote.com](https://heynote.com)). The Windows build is not signed, so you might see some scary warning (I can not justify paying a yearly fee for a certificate just to get rid of that).
### Notes on Linux installation
It's been reported [(#48)](https://github.com/heyman/heynote/issues/48) that ChromeOS's Debian VM need the following packages installed to run the Heynote AppImage:
```
libfuse2
libnss3
libnspr4
```
[Documentation](https://heynote.com/docs/) is available on the Heynote website.
## Development
@ -70,49 +66,12 @@ To run the tests in the Playwright UI:
I'm happy to merge contributions that fit my vision for the app. Bug fixes are always welcome.
## Math Blocks
Heynote's Math blocks are powered by [Math.js expressions](https://mathjs.org/docs/expressions). Checkout their [documentation](https://mathjs.org/docs/) to see what [syntax](https://mathjs.org/docs/expressions/syntax.html), [functions](https://mathjs.org/docs/reference/functions.html), and [constants](https://mathjs.org/docs/reference/constants.html) are available.
### Accessing the previous result
The variable `prev` can be used to access the previous result. For example:
```
128
prev * 2 # 256
```
### Changing how the results of Math blocks are formatted?
You can define a custom `format` function within the Math block like this:
```
_format = format # store reference to the built in format
format(x) = _format(x, {notation:"exponential"})
```
You can also do something like this to show the number with your default locale or provide a [custom one](https://www.w3.org/International/articles/language-tags/):
```
format(x) = x.toLocaleString();
format(x) = x.toLocaleString('en-GB');
```
See the [Math.js format()](https://mathjs.org/docs/reference/functions/format.html) function for more info on what's supported.
## FAQ
### Where is the buffer data stored?
The default paths for the buffer data for the respective OS are:
- Mac: `~/Library/Application Support/Heynote/buffer.txt`
- Windows: `%APPDATA%\Heynote\buffer.txt`
- Linux: `~/.config/Heynote/buffer.txt`
From version >=1.5.0, symlinks will be supported and you'll be able to configure the path where `buffer.txt` is stored.
See the [documentation](https://heynote.com/docs/#user-content-the-buffer-file).
### Can you make a mobile app?
@ -126,38 +85,7 @@ I can totally see the usefulness of such a feature, and it's definitely somethin
### What are the default keyboard shortcuts?
**On Mac**
```
⌘ + Enter Add new block below the current block
⌥ + Enter Add new block before the current block
⌘ + Shift + Enter Add new block at the end of the buffer
⌥ + Shift + Enter Add new block at the start of the buffer
⌘ + ⌥ + Enter Split the current block at cursor position
⌘ + L Change block language
⌘ + Down Goto next block
⌘ + Up Goto previous block
⌘ + A Select all text in a note block. Press again to select the whole buffer
⌘ + ⌥ + Up/Down Add additional cursor above/below
⌥ + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
```
**On Windows and Linux**
```
Ctrl + Enter Add new block below the current block
Alt + Enter Add new block before the current block
Ctrl + Shift + Enter Add new block at the end of the buffer
Alt + Shift + Enter Add new block at the start of the buffer
Ctrl + Alt + Enter Split the current block at cursor position
Ctrl + L Change block language
Ctrl + Down Goto next block
Ctrl + Up Goto previous block
Ctrl + A Select all text in a note block. Press again to select the whole buffer
Ctrl + Alt + Up/Down Add additional cursor above/below
Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
Alt Show menu
```
See the [documentation](https://heynote.com/docs/#user-content-default-key-bindings).
## Thanks!

89
docs/index.md Normal file
View File

@ -0,0 +1,89 @@
# Heynote Documentation
Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc.
The Heynote buffer is divided into blocks, and each block can have its own Language set (e.g. JavaScript, JSON, Markdown, etc.). This gives you syntax highlighting and lets you auto-format that JSON response.
## Default Key Bindings
<!-- keyboard_shortcuts -->
**On Mac**
```
⌘ + Enter Add new block below the current block
⌥ + Enter Add new block before the current block
⌘ + Shift + Enter Add new block at the end of the buffer
⌥ + Shift + Enter Add new block at the start of the buffer
⌘ + ⌥ + Enter Split the current block at cursor position
⌘ + L Change block language
⌘ + Down Goto next block
⌘ + Up Goto previous block
⌘ + A Select all text in a note block. Press again to select the whole buffer
⌘ + ⌥ + Up/Down Add additional cursor above/below
⌥ + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
```
**On Windows and Linux**
```
Ctrl + Enter Add new block below the current block
Alt + Enter Add new block before the current block
Ctrl + Shift + Enter Add new block at the end of the buffer
Alt + Shift + Enter Add new block at the start of the buffer
Ctrl + Alt + Enter Split the current block at cursor position
Ctrl + L Change block language
Ctrl + Down Goto next block
Ctrl + Up Goto previous block
Ctrl + A Select all text in a note block. Press again to select the whole buffer
Ctrl + Alt + Up/Down Add additional cursor above/below
Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
Alt Show menu
```
## Download/Installation
Download the appropriate (Mac, Windows or Linux) version from [heynote.com](https://heynote.com). The Windows build is not signed, so you might see some scary warning (I can not justify paying a yearly fee for a certificate just to get rid of that).
### Notes on Linux installation
It's been reported [(#48)](https://github.com/heyman/heynote/issues/48) that ChromeOS's Debian VM need the following packages installed to run the Heynote AppImage:
```
libfuse2
libnss3
libnspr4
```
## Math Blocks
Heynote's Math blocks are powered by [Math.js expressions](https://mathjs.org/docs/expressions). Checkout their [documentation](https://mathjs.org/docs/) to see what [syntax](https://mathjs.org/docs/expressions/syntax.html), [functions](https://mathjs.org/docs/reference/functions.html), and [constants](https://mathjs.org/docs/reference/constants.html) are available.
### Accessing the previous result
The variable `prev` can be used to access the previous result. For example:
```
128
prev * 2 # 256
```
### Changing how the results of Math blocks are formatted
You can define a custom `format` function within the Math block like this:
```
_format = format # store reference to the built in format
format(x) = _format(x, {notation:"exponential"})
```
See the [Math.js format()](https://mathjs.org/docs/reference/functions/format.html) function for more info on what's supported.
## The buffer file
The default paths for the buffer data for the respective operating systems are:
- Mac: `~/Library/Application Support/Heynote/buffer.txt`
- Windows: `%APPDATA%\Heynote\buffer.txt`
- Linux: `~/.config/Heynote/buffer.txt`

View File

@ -35,6 +35,8 @@ const schema = {
"showInMenu": {type: "boolean", default: false},
"alwaysOnTop": {type: "boolean", default: false},
"bracketClosing": {type: "boolean", default: false},
"defaultBlockLanguage": {type: "string"},
"defaultBlockLanguageAutoDetect": {type: "boolean"},
// when default font settings are used, fontFamily and fontSize is not specified in the
// settings file, so that it's possible for us to change the default settings in the

View File

@ -131,7 +131,14 @@ const template = [
role: 'help',
submenu: [
{
label: 'Learn More',
label: 'Documentation',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://heynote.com/docs/')
}
},
{
label: 'Website',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://heynote.com')

72
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "Heynote",
"version": "1.7.0",
"version": "1.8.0-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Heynote",
"version": "1.7.0",
"version": "1.8.0-beta",
"license": "Commons Clause MIT",
"dependencies": {
"electron-log": "^5.0.1"
@ -25,6 +25,7 @@
"@codemirror/lang-python": "^6.1.3",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sql": "^6.5.4",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.3",
"@codemirror/legacy-modes": "^6.3.3",
@ -41,13 +42,13 @@
"@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^4.0.0",
"debounce": "^1.2.1",
"electron": "^28.0.0",
"electron": "^31.1.0",
"electron-builder": "^23.6.0",
"electron-builder-notarize": "^1.5.1",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"fs-jetpack": "^5.1.0",
"prettier": "^3.1.1",
"prettier": "^3.3.2",
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",
@ -427,6 +428,20 @@
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-vue": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz",
"integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==",
"dev": true,
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/language": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.1"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz",
@ -992,9 +1007,9 @@
"dev": true
},
"node_modules/@lezer/common": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.2.tgz",
"integrity": "sha512-V+GqBsga5+cQJMfM0GdnHmg4DgWvLzgMWjbldBg0+jC3k9Gu6nJNZDLJxXEBT1Xj8KhRN4jmbC5CY7SIL++sVw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==",
"dev": true
},
"node_modules/@lezer/cpp": {
@ -2262,12 +2277,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -3063,9 +3078,9 @@
"dev": true
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"dependencies": {
"jake": "^10.8.5"
@ -3078,14 +3093,14 @@
}
},
"node_modules/electron": {
"version": "28.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-28.0.0.tgz",
"integrity": "sha512-eDhnCFBvG0PGFVEpNIEdBvyuGUBsFdlokd+CtuCe2ER3P+17qxaRfWRxMmksCOKgDHb5Wif5UxqOkZSlA4snlw==",
"version": "31.1.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz",
"integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"@types/node": "^20.9.0",
"extract-zip": "^2.0.1"
},
"bin": {
@ -3474,15 +3489,6 @@
"node": ">= 10.0.0"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "18.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz",
"integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -3684,9 +3690,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@ -5005,9 +5011,9 @@
}
},
"node_modules/prettier": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"

View File

@ -1,6 +1,6 @@
{
"name": "Heynote",
"version": "1.7.0",
"version": "1.8.0-beta",
"main": "dist-electron/main/index.js",
"description": "A dedicated scratch pad",
"author": "Jonatan Heyman (https://heyman.info)",
@ -44,6 +44,7 @@
"@codemirror/lang-python": "^6.1.3",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-sql": "^6.5.4",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.3",
"@codemirror/legacy-modes": "^6.3.3",
@ -60,13 +61,13 @@
"@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^4.0.0",
"debounce": "^1.2.1",
"electron": "^28.0.0",
"electron": "^31.1.0",
"electron-builder": "^23.6.0",
"electron-builder-notarize": "^1.5.1",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"fs-jetpack": "^5.1.0",
"prettier": "^3.1.1",
"prettier": "^3.3.2",
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",

View File

@ -1,6 +1,6 @@
importScripts("guesslang.min.js")
GUESSLANG_LANGUAGES = ["json","py","html","sql","md","java","php","css","xml","cpp","rs","cs","rb","sh","yaml","toml","go","clj","erl","js","ts","swift","kt","groovy","ps1"]
GUESSLANG_LANGUAGES = ["json","py","html","sql","md","java","php","css","xml","cpp","rs","cs","rb","sh","yaml","toml","go","clj","erl","js","ts","swift","kt","groovy","ps1","dart"]
const guessLang = new self.GuessLang()

11
public/site.webmanifest Normal file
View File

@ -0,0 +1,11 @@
{
"name": "Heynote",
"short_name": "Heynote",
"icons": [
{
"src": "/icon.ico",
"sizes": "256x256"
}
],
"display": "standalone"
}

View File

@ -122,6 +122,8 @@
:bracketClosing="settings.bracketClosing"
:fontFamily="settings.fontFamily"
:fontSize="settings.fontSize"
:defaultBlockLanguage="settings.defaultBlockLanguage || 'text'"
:defaultBlockLanguageAutoDetect="settings.defaultBlockLanguageAutoDetect === undefined ? true : settings.defaultBlockLanguageAutoDetect"
class="editor"
ref="editor"
@openLanguageSelector="openLanguageSelector"

View File

@ -29,6 +29,8 @@
},
fontFamily: String,
fontSize: Number,
defaultBlockLanguage: String,
defaultBlockLanguageAutoDetect: Boolean,
},
components: {},
@ -78,6 +80,7 @@
})
window._heynote_editor = this.editor
window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded)
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
// set up buffer change listener
window.heynote.buffer.onChangeCallback((event, content) => {
@ -145,12 +148,18 @@
fontSize() {
this.editor.setFont(this.fontFamily, this.fontSize)
},
defaultBlockLanguage() {
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
},
defaultBlockLanguageAutoDetect() {
this.editor.setDefaultBlockLanguage(this.defaultBlockLanguage, this.defaultBlockLanguageAutoDetect)
},
},
methods: {
setLanguage(language) {
if (language === "auto") {
this.editor.setCurrentLanguage("text", true)
this.editor.setCurrentLanguage(null, true)
} else {
this.editor.setCurrentLanguage(language, false)
}

View File

@ -1,10 +1,14 @@
<script>
import { LANGUAGES } from '../../editor/languages.js'
import KeyboardHotkey from "./KeyboardHotkey.vue"
import TabListItem from "./TabListItem.vue"
import TabContent from "./TabContent.vue"
const defaultFontFamily = window.heynote.defaultFontFamily
const defaultFontSize = window.heynote.defaultFontSize
const defaultDefaultBlockLanguage = "text"
const defaultDefaultBlockLanguageAutoDetect = true
export default {
props: {
@ -39,6 +43,16 @@
bufferPath: this.initialSettings.bufferPath,
fontFamily: this.initialSettings.fontFamily || defaultFontFamily,
fontSize: this.initialSettings.fontSize || defaultFontSize,
languageOptions: LANGUAGES.map(l => {
return {
"value": l.token,
"name": l.token == "text" ? l.name + " (default)" : l.name,
}
}).sort((a, b) => {
return a.name.localeCompare(b.name)
}),
defaultBlockLanguage: this.initialSettings.defaultBlockLanguage || defaultDefaultBlockLanguage,
defaultBlockLanguageAutoDetect: this.initialSettings.defaultBlockLanguageAutoDetect === false ? false : defaultDefaultBlockLanguageAutoDetect,
activeTab: "general",
isWebApp: window.heynote.platform.isWebApp,
@ -89,6 +103,8 @@
bufferPath: this.bufferPath,
fontFamily: this.fontFamily === defaultFontFamily ? undefined : this.fontFamily,
fontSize: this.fontSize === defaultFontSize ? undefined : this.fontSize,
defaultBlockLanguage: this.defaultBlockLanguage === "text" ? undefined : this.defaultBlockLanguage,
defaultBlockLanguageAutoDetect: this.defaultBlockLanguageAutoDetect === true ? undefined : this.defaultBlockLanguageAutoDetect,
})
if (!this.showInDock) {
this.showInMenu = true
@ -255,6 +271,25 @@
</label>
</div>
</div>
<div class="row">
<div class="entry">
<h2>Default Block Language</h2>
<select v-model="defaultBlockLanguage" @change="updateSettings" class="block-language">
<template v-for="lang in languageOptions" :key="lang.value">
<option :selected="lang.value === defaultBlockLanguage" :value="lang.value">{{ lang.name }}</option>
</template>
</select>
<label>
<input
type="checkbox"
v-model="defaultBlockLanguageAutoDetect"
@change="updateSettings"
class="language-auto-detect"
/>
Auto-detection (default: on)
</label>
</div>
</div>
</TabContent>
<TabContent tab="appearance" :activeTab="activeTab">
@ -417,6 +452,7 @@
overflow-y: auto
select
height: 22px
margin: 4px 0
.row
display: flex
.entry

View File

@ -1,7 +1,7 @@
import { ViewPlugin, EditorView, Decoration, WidgetType, lineNumbers } from "@codemirror/view"
import { layer, RectangleMarker } from "@codemirror/view"
import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet} from "@codemirror/state";
import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
import { syntaxTree, ensureSyntaxTree, syntaxTreeAvailable } from "@codemirror/language"
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
import { IterMode } from "@lezer/common";
import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
@ -10,12 +10,25 @@ import { mathBlock } from "./math.js"
import { emptyBlockSelected } from "./select-all.js";
function startTimer() {
const timeStart = performance.now();
return function () {
return Math.round(performance.now() - timeStart);
};
}
// tracks the size of the first delimiter
let firstBlockDelimiterSize
function getBlocks(state, timeout=50) {
/**
* Return a list of blocks in the document from the syntax tree.
* syntaxTreeAvailable() should have been called before this function to ensure the syntax tree is available.
*/
export function getBlocksFromSyntaxTree(state) {
//const timer = startTimer()
const blocks = [];
const tree = ensureSyntaxTree(state, state.doc.length, timeout)
const tree = syntaxTree(state, state.doc.length)
if (tree) {
tree.iterate({
enter: (type) => {
@ -52,12 +65,90 @@ function getBlocks(state, timeout=50) {
});
firstBlockDelimiterSize = blocks[0]?.delimiter.to
}
//console.log("getBlocksSyntaxTree took", timer(), "ms")
return blocks
}
/**
* Parse blocks from document's string contents using String.indexOf()
*/
export function getBlocksFromString(state) {
//const timer = startTimer()
const blocks = []
const doc = state.doc
if (doc.length === 0) {
return [];
}
const content = doc.sliceString(0, doc.length)
const delim = "\n∞∞∞"
let pos = 0
while (pos < doc.length) {
const blockStart = content.indexOf(delim, pos);
if (blockStart != pos) {
console.error("Error parsing blocks, expected delimiter at", pos)
break;
}
const langStart = blockStart + delim.length;
const delimiterEnd = content.indexOf("\n", langStart)
if (delimiterEnd < 0) {
console.error("Error parsing blocks. Delimiter didn't end with newline")
break
}
const langFull = content.substring(langStart, delimiterEnd);
let auto = false;
let lang = langFull;
if (langFull.endsWith("-a")) {
auto = true;
lang = langFull.substring(0, langFull.length - 2);
}
const contentFrom = delimiterEnd + 1;
let blockEnd = content.indexOf(delim, contentFrom);
if (blockEnd < 0) {
blockEnd = doc.length;
}
const block = {
language: {
name: lang,
auto: auto,
},
content: {
from: contentFrom,
to: blockEnd,
},
delimiter: {
from: blockStart,
to: delimiterEnd + 1,
},
range: {
from: blockStart,
to: blockEnd,
},
};
blocks.push(block);
pos = blockEnd;
}
//console.log("getBlocksFromString() took", timer(), "ms")
return blocks;
}
/**
* Get the blocks from the document state.
* If the syntax tree is available, we'll extract the blocks from that. Otherwise
* the blocks are parsed from the string contents of the document, which is much faster
* than waiting for the tree parsing to finish.
*/
export function getBlocks(state) {
if (syntaxTreeAvailable(state, state.doc.length)) {
return getBlocksFromSyntaxTree(state)
} else {
return getBlocksFromString(state)
}
}
export const blockState = StateField.define({
create(state) {
return getBlocks(state, 1000);
return getBlocks(state);
},
update(blocks, transaction) {
// if blocks are empty it likely means we didn't get a parsed syntax tree, and then we want to update

View File

@ -7,7 +7,11 @@ import { selectAll } from "./select-all.js";
export { moveLineDown, moveLineUp, selectAll }
export const insertNewBlockAtCursor = ({ state, dispatch }) => {
function getBlockDelimiter(defaultToken, autoDetect) {
return `\n∞∞∞${autoDetect ? defaultToken + '-a' : defaultToken}\n`
}
export const insertNewBlockAtCursor = (editor) => ({ state, dispatch }) => {
if (state.readOnly)
return false
@ -16,7 +20,7 @@ export const insertNewBlockAtCursor = ({ state, dispatch }) => {
if (currentBlock) {
delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}\n`
} else {
delimText = "\n∞∞∞text-a\n"
delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
}
dispatch(state.replaceSelection(delimText),
{
@ -28,13 +32,12 @@ export const insertNewBlockAtCursor = ({ state, dispatch }) => {
return true;
}
export const addNewBlockBeforeCurrent = ({ state, dispatch }) => {
console.log("addNewBlockBeforeCurrent")
export const addNewBlockBeforeCurrent = (editor) => ({ state, dispatch }) => {
if (state.readOnly)
return false
const block = getActiveNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
dispatch(state.update({
changes: {
@ -50,12 +53,12 @@ export const addNewBlockBeforeCurrent = ({ state, dispatch }) => {
return true;
}
export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
export const addNewBlockAfterCurrent = (editor) => ({ state, dispatch }) => {
if (state.readOnly)
return false
const block = getActiveNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
dispatch(state.update({
changes: {
@ -70,12 +73,12 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
return true;
}
export const addNewBlockBeforeFirst = ({ state, dispatch }) => {
export const addNewBlockBeforeFirst = (editor) => ({ state, dispatch }) => {
if (state.readOnly)
return false
const block = getFirstNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
dispatch(state.update({
changes: {
@ -91,11 +94,11 @@ export const addNewBlockBeforeFirst = ({ state, dispatch }) => {
return true;
}
export const addNewBlockAfterLast = ({ state, dispatch }) => {
export const addNewBlockAfterLast = (editor) => ({ state, dispatch }) => {
if (state.readOnly)
return false
const block = getLastNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
dispatch(state.update({
changes: {
@ -131,6 +134,10 @@ export function changeLanguageTo(state, dispatch, block, language, auto) {
export function changeCurrentBlockLanguage(state, dispatch, language, auto) {
const block = getActiveNoteBlock(state)
// if language is null, we only want to change the auto-detect flag
if (language === null) {
language = block.language.name
}
changeLanguageTo(state, dispatch, block, language, auto)
}

View File

@ -1,4 +1,4 @@
import { Annotation, EditorState, Compartment } from "@codemirror/state"
import { Annotation, EditorState, Compartment, Facet } from "@codemirror/state"
import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
import { indentUnit, forceParsing, foldGutter } from "@codemirror/language"
import { markdown } from "@codemirror/lang-markdown"
@ -59,6 +59,8 @@ export class HeynoteEditor {
this.deselectOnCopy = keymap === "emacs"
this.emacsMetaKey = emacsMetaKey
this.fontTheme = new Compartment
this.defaultBlockToken = "text"
this.defaultBlockAutoDetect = true
const state = EditorState.create({
doc: content || "",
@ -84,7 +86,7 @@ export class HeynoteEditor {
}),
heynoteLang(),
noteBlockExtension(this),
languageDetection(() => this.view),
languageDetection(() => this),
// set cursor blink rate to 1 second
drawSelection({cursorBlinkRate:1000}),
@ -206,6 +208,11 @@ export class HeynoteEditor {
})
}
setDefaultBlockLanguage(token, autoDetect) {
this.defaultBlockToken = token
this.defaultBlockAutoDetect = autoDetect
}
formatCurrentBlock() {
formatBlockContent({
state: this.view.state,

View File

@ -1,7 +1,7 @@
import { keymap } from "@codemirror/view"
//import { EditorSelection, EditorState } from "@codemirror/state"
import {
indentLess, indentMore,
indentLess, indentMore, redo,
} from "@codemirror/commands"
import {
@ -48,11 +48,11 @@ export function heynoteKeymap(editor) {
["Mod-x", cutCommand(editor)],
["Tab", indentMore],
["Shift-Tab", indentLess],
["Alt-Shift-Enter", addNewBlockBeforeFirst],
["Mod-Shift-Enter", addNewBlockAfterLast],
["Alt-Enter", addNewBlockBeforeCurrent],
["Mod-Enter", addNewBlockAfterCurrent],
["Mod-Alt-Enter", insertNewBlockAtCursor],
["Alt-Shift-Enter", addNewBlockBeforeFirst(editor)],
["Mod-Shift-Enter", addNewBlockAfterLast(editor)],
["Alt-Enter", addNewBlockBeforeCurrent(editor)],
["Mod-Enter", addNewBlockAfterCurrent(editor)],
["Mod-Alt-Enter", insertNewBlockAtCursor(editor)],
["Mod-a", selectAll],
["Alt-ArrowUp", moveLineUp],
["Alt-ArrowDown", moveLineDown],
@ -61,6 +61,7 @@ export function heynoteKeymap(editor) {
["Mod-Alt-ArrowDown", newCursorBelow],
["Mod-Alt-ArrowUp", newCursorAbove],
["Mod-Shift-k", deleteLine],
["Mod-Shift-z", redo],
{key:"Mod-ArrowUp", run:gotoPreviousBlock, shift:selectPreviousBlock},
{key:"Mod-ArrowDown", run:gotoNextBlock, shift:selectNextBlock},
{key:"Ctrl-ArrowUp", run:gotoPreviousParagraph, shift:selectPreviousParagraph},

View File

@ -11,7 +11,7 @@ NoteDelimiter {
@tokens {
noteDelimiterMark { "∞∞∞" }
NoteLanguage { "text" | "math" | "javascript" | "typescript" | "jsx" | "tsx" | "json" | "python" | "html" | "sql" | "markdown" | "java" | "php" | "css" | "xml" | "cpp" | "rust" | "csharp" | "ruby" | "shell" | "yaml" | "golang" | "clojure" | "erlang" | "lezer" | "toml" | "swift" | "kotlin" | "groovy" | "diff" | "powershell" }
NoteLanguage { "text" | "math" | "javascript" | "typescript" | "jsx" | "tsx" | "json" | "python" | "html" | "sql" | "markdown" | "java" | "php" | "css" | "xml" | "cpp" | "rust" | "csharp" | "ruby" | "shell" | "yaml" | "golang" | "clojure" | "erlang" | "lezer" | "toml" | "swift" | "kotlin" | "groovy" | "diff" | "powershell" | "vue" | "dart" }
Auto { "-a" }
noteDelimiterEnter { "\n" }
//NoteContent { String }

View File

@ -22,7 +22,13 @@ export function configureNesting() {
if (id == NoteContent) {
let noteLang = node.node.parent.firstChild.getChildren(NoteLanguage)[0]
let langName = input.read(noteLang?.from, noteLang?.to)
//console.log("langName:", langName)
// if the NoteContent is empty, we don't want to return a parser, since that seems to cause an
// error for StreamLanguage parsers when the buffer size is large (e.g >300 kb)
if (node.node.from == node.node.to) {
return null
}
if (langName in languageMapping && languageMapping[langName] !== null) {
//console.log("found parser for language:", langName)
return {

View File

@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({
maxTerm: 10,
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "-x~RbYZ!Z}!O!`#V#W!k#W#X$X#X#Y$k#Z#[%Z#[#]%|#^#_&`#_#`'|#`#a(f#a#b)O#d#e)}#f#g+i#g#h+x#h#i,b#l#m&S#m#n-a%&x%&y-g~!`OX~~!cP#T#U!f~!kOU~~!nR#`#a!w#d#e#l#g#h#r~!zP#c#d!}~#QP#^#_#T~#WP#i#j#Z~#^P#f#g#a~#dP#X#Y#g~#lOT~~#oP#d#e#g~#uQ#[#]#{#g#h#g~$OP#T#U$R~$UP#f#g#l~$[P#]#^$_~$bP#Y#Z$e~$hP#Y#Z#g~$nP#f#g$q~$tP#`#a$w~$zP#T#U$}~%QP#b#c%T~%WP#Z#[#g~%^Q#c#d$q#f#g%d~%gP#c#d%j~%mP#c#d%p~%sP#j#k%v~%yP#m#n#g~&PP#h#i&S~&VP#a#b&Y~&]P#`#a#g~&cQ#T#U&i#g#h'm~&lP#j#k&o~&rP#T#U&u~&zPT~#g#h&}~'QP#V#W'T~'WP#f#g'Z~'^P#]#^'a~'dP#d#e'g~'jP#h#i#g~'pQ#c#d'v#l#m#g~'yP#b#c#g~(PP#c#d(S~(VP#h#i(Y~(]P#`#a(`~(cP#]#^'v~(iP#X#Y(l~(oP#n#o(r~(uP#X#Y(x~({P#f#g#g~)RP#T#U)U~)XQ#f#g)_#h#i)w~)bP#_#`)e~)hP#W#X)k~)nP#c#d)q~)tP#k#l'v~)zP#[#]#g~*QR#[#]#l#c#d*Z#m#n+V~*^P#k#l*a~*dP#X#Y*g~*jP#f#g*m~*pP#g#h*s~*vP#[#]*y~*|P#X#Y+P~+SP#`#a&Y~+YP#h#i+]~+`P#[#]+c~+fP#c#d'v~+lP#i#j+o~+rQ#U#V%v#g#h'g~+{R#[#]*y#e#f&Y#k#l,U~,XP#]#^,[~,_P#Y#Z'g~,eS#X#Y,q#c#d&S#g#h,w#m#n,}~,tP#l#m'g~,zP#l#m#g~-QP#d#e-T~-WP#X#Y-Z~-^P#g#h&}~-dP#T#U&S~-jP%&x%&y-m~-pP%&x%&y-s~-xOY~",
tokenData: ".[~RcYZ!^}!O!c#V#W!n#W#X$[#X#Y$}#Z#[%m#[#]&`#^#_&r#_#`(Y#`#a(r#a#b)[#d#e*Z#f#g+u#g#h,U#h#i,n#j#k-m#l#m&f#m#n-s%&x%&y-y~!cOX~~!fP#T#U!i~!nOU~~!qR#`#a!z#d#e#o#g#h#u~!}P#c#d#Q~#TP#^#_#W~#ZP#i#j#^~#aP#f#g#d~#gP#X#Y#j~#oOT~~#rP#d#e#j~#xQ#[#]$O#g#h#j~$RP#T#U$U~$XP#f#g#o~$_Q#T#U$e#]#^$q~$hP#f#g$k~$nP#h#i#j~$tP#Y#Z$w~$zP#Y#Z#j~%QP#f#g%T~%WP#`#a%Z~%^P#T#U%a~%dP#b#c%g~%jP#Z#[#j~%pQ#c#d%T#f#g%v~%yP#c#d%|~&PP#c#d&S~&VP#j#k&Y~&]P#m#n#j~&cP#h#i&f~&iP#a#b&l~&oP#`#a#j~&uQ#T#U&{#g#h'y~'OP#j#k'R~'UP#T#U'X~'^PT~#g#h'a~'dP#V#W'g~'jP#f#g'm~'pP#]#^'s~'vP#d#e$k~'|Q#c#d(S#l#m#j~(VP#b#c#j~(]P#c#d(`~(cP#h#i(f~(iP#`#a(l~(oP#]#^(S~(uP#X#Y(x~({P#n#o)O~)RP#X#Y)U~)XP#f#g#j~)_P#T#U)b~)eQ#f#g)k#h#i*T~)nP#_#`)q~)tP#W#X)w~)zP#c#d)}~*QP#k#l(S~*WP#[#]#j~*^R#[#]#o#c#d*g#m#n+c~*jP#k#l*m~*pP#X#Y*s~*vP#f#g*y~*|P#g#h+P~+SP#[#]+V~+YP#X#Y+]~+`P#`#a&l~+fP#h#i+i~+lP#[#]+o~+rP#c#d(S~+xP#i#j+{~,OQ#U#V&Y#g#h$k~,XR#[#]+V#e#f&l#k#l,b~,eP#]#^,h~,kP#Y#Z$k~,qS#X#Y,}#c#d&f#g#h-T#m#n-Z~-QP#l#m$k~-WP#l#m#j~-^P#d#e-a~-dP#X#Y-g~-jP#g#h'a~-pP#i#j#d~-vP#T#U&f~-|P%&x%&y.P~.SP%&x%&y.V~.[OY~",
tokenizers: [0, noteContent],
topRules: {"Document":[0,2]},
tokenPrec: 0

View File

@ -25,7 +25,7 @@ function cancelIdleCallbackCompat(id) {
}
}
export function languageDetection(getView) {
export function languageDetection(getEditor) {
const previousBlockContent = {}
let idleCallbackId = null
@ -35,7 +35,8 @@ export function languageDetection(getView) {
if (!event.data.guesslang.language) {
return
}
const view = getView()
const editor = getEditor()
const view = editor.view
const state = view.state
const block = getActiveNoteBlock(state)
const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
@ -88,11 +89,12 @@ export function languageDetection(getView) {
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 plaintext
const view = getView()
// if content is cleared, set language to default
const editor = getEditor()
const view = editor.view
const block = getActiveNoteBlock(view.state)
if (block.language.name !== "text") {
changeLanguageTo(view.state, view.dispatch, block, "text", true)
if (block.language.name !== editor.defaultBlockToken) {
changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true)
}
delete previousBlockContent[idx]
}

View File

@ -12,6 +12,7 @@ import { cppLanguage } from "@codemirror/lang-cpp"
import { xmlLanguage } from "@codemirror/lang-xml"
import { rustLanguage } from "@codemirror/lang-rust"
import { csharpLanguage } from "@replit/codemirror-lang-csharp"
import { vueLanguage } from "@codemirror/lang-vue";
import { StreamLanguage } from "@codemirror/language"
import { ruby } from "@codemirror/legacy-modes/mode/ruby"
@ -22,18 +23,18 @@ import { clojure } from "@codemirror/legacy-modes/mode/clojure"
import { erlang } from "@codemirror/legacy-modes/mode/erlang"
import { toml } from "@codemirror/legacy-modes/mode/toml"
import { swift } from "@codemirror/legacy-modes/mode/swift"
import { kotlin } from "@codemirror/legacy-modes/mode/clike"
import { kotlin, dart } from "@codemirror/legacy-modes/mode/clike"
import { groovy } from "@codemirror/legacy-modes/mode/groovy"
import { diff } from "@codemirror/legacy-modes/mode/diff";
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
import typescriptPlugin from "prettier/plugins/typescript.mjs"
import babelPrettierPlugin from "prettier/plugins/babel.mjs"
import htmlPrettierPlugin from "prettier/esm/parser-html.mjs"
import cssPrettierPlugin from "prettier/esm/parser-postcss.mjs"
import markdownPrettierPlugin from "prettier/esm/parser-markdown.mjs"
import yamlPrettierPlugin from "prettier/plugins/yaml.mjs"
import * as prettierPluginEstree from "prettier/plugins/estree.mjs";
import typescriptPlugin from "prettier/plugins/typescript"
import babelPrettierPlugin from "prettier/plugins/babel"
import htmlPrettierPlugin from "prettier/plugins/html"
import cssPrettierPlugin from "prettier/plugins/postcss"
import markdownPrettierPlugin from "prettier/plugins/markdown"
import yamlPrettierPlugin from "prettier/plugins/yaml"
import * as prettierPluginEstree from "prettier/plugins/estree";
class Language {
@ -253,6 +254,18 @@ export const LANGUAGES = [
parser: StreamLanguage.define(powerShell).parser,
guesslang: "ps1",
}),
new Language({
token: "vue",
name: "Vue",
parser: vueLanguage.parser,
guesslang: null,
}),
new Language({
token: "dart",
name: "Dart",
parser: StreamLanguage.define(dart).parser,
guesslang: "dart",
}),
]

View File

@ -114,3 +114,32 @@ const runTest = async (page, key, expectedBlocks) => {
await expect(await page.locator("css=.heynote-block-start.first")).toHaveCount(1)
}
test("test custom default block language", async ({ page, browserName }) => {
heynotePage.setContent(`
text
Text block`)
await page.locator("css=.status-block.settings").click()
await page.locator("css=li.tab-editing").click()
await page.locator("css=select.block-language").selectOption("Rust")
await page.locator("body").press("Escape")
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Enter")
expect(await heynotePage.getContent()).toBe(`
text
Text block
rust-a
`)
await page.locator("css=.status-block.settings").click()
await page.locator("css=li.tab-editing").click()
await page.locator("css=input.language-auto-detect").click()
await page.locator("body").press("Escape")
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Enter")
expect(await heynotePage.getContent()).toBe(`
text
Text block
rust-a
rust
`)
})

View File

@ -0,0 +1,28 @@
import { expect, test } from "@playwright/test"
import { EditorState } from "@codemirror/state"
import { heynoteLang } from "../src/editor/lang-heynote/heynote.js"
import { getBlocksFromSyntaxTree, getBlocksFromString } from "../src/editor/block/block.js"
test("parse blocks from both syntax tree and string contents", async ({page}) => {
const contents = `
text
Text Block A
text-a
Text Block B
json-a
{
"key": "value"
}
python
print("Hello, World!")
`
const state = EditorState.create({
doc: contents,
extensions: heynoteLang(),
})
const treeBlocks = getBlocksFromSyntaxTree(state)
const stringBlocks = getBlocksFromString(state)
expect(treeBlocks).toEqual(stringBlocks)
})

View File

@ -17,10 +17,8 @@ rmSync('dist-electron', { recursive: true, force: true })
const isDevelopment = process.env.NODE_ENV === "development" || !!process.env.VSCODE_DEBUG
const isProduction = process.env.NODE_ENV === "production"
const updateReadmeKeybinds = async () => {
const readmePath = path.resolve(__dirname, 'README.md')
let readme = fs.readFileSync(readmePath, 'utf-8')
const keybindsRegex = /^(### What are the default keyboard shortcuts\?\s*).*?^(```\s+#)/gms
const injectKeybindsInDocs = async () => {
const keybindsRegex = /^(<!-- keyboard_shortcuts -->\s*).*?^(```\s+#)/gms
const shortcuts = `$1**On Mac**
\`\`\`
@ -32,8 +30,10 @@ ${keyHelpStr('darwin')}
\`\`\`
${keyHelpStr('win32')}
$2`
readme = readme.replace(keybindsRegex, shortcuts)
fs.writeFileSync(readmePath, readme)
const docsPath = path.resolve(__dirname, 'docs', 'index.md')
let docs = fs.readFileSync(docsPath, 'utf-8')
docs = docs.replace(keybindsRegex, shortcuts)
fs.writeFileSync(docsPath, docs)
}
const updateGuesslangLanguagesInWebWorker = async () => {
@ -56,7 +56,7 @@ export default defineConfig({
plugins: [
vue(),
updateReadmeKeybinds(),
injectKeybindsInDocs(),
updateGuesslangLanguagesInWebWorker(),
electron([
{

View File

@ -5,6 +5,7 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark">
<link rel="manifest" href="/site.webmanifest">
<title>Heynote</title>
</head>
<body>