Merge branch 'main' into feat-configure-tab-size

This commit is contained in:
Jonatan Heyman 2025-04-22 17:06:38 +02:00
commit 4d08748b41
55 changed files with 7148 additions and 3502 deletions

View File

@ -32,7 +32,7 @@ jobs:
- name: Build/release Electron app - name: Build/release Electron app
#continue-on-error: true #continue-on-error: true
uses: samuelmeuli/action-electron-builder@v1 uses: johannesjo/action-electron-builder@v1
with: with:
# Specify electron-builder config file # Specify electron-builder config file
args: -c electron-builder.json5 args: -c electron-builder.json5
@ -56,7 +56,7 @@ jobs:
# macOS notarization API key # macOS notarization API key
APPLE_API_KEY: ~/private_keys/AuthKey.p8 APPLE_API_KEY: ~/private_keys/AuthKey.p8
APPLE_API_KEY_ID: ${{ secrets.apple_api_key_id }} APPLE_API_KEY_ID: ${{ secrets.apple_api_key_id }}
APPLE_API_KEY_ISSUER: ${{ secrets.apple_api_key_issuer_id }} APPLE_API_ISSUER: ${{ secrets.apple_api_key_issuer_id }}
#- name: Print notarization-error.log #- name: Print notarization-error.log
# if: ${{ matrix.os == 'macos-latest' }} # if: ${{ matrix.os == 'macos-latest' }}

View File

@ -7,9 +7,9 @@
## General Information ## General Information
- Website: [heynote.com](https://heynote.com) - [Website](https://heynote.com)
- Documentation: [heynote.com](https://heynote.com/docs/) - [Documentation](https://heynote.com/docs/)
- Changelog: [heynote.com](https://heynote.com/docs/changelog/) - [Changelog](https://heynote.com/docs/changelog/)
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. 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.

View File

@ -0,0 +1,80 @@
/* BEGIN Light */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/Light/OpenSans-Light.woff2?v=1.1.0") format("woff2"), url("./fonts/Light/OpenSans-Light.woff?v=1.1.0") format("woff");
font-weight: 300;
font-style: normal;
}
/* END Light */
/* BEGIN Light Italic */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/LightItalic/OpenSans-LightItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/LightItalic/OpenSans-LightItalic.woff?v=1.1.0") format("woff");
font-weight: 300;
font-style: italic;
}
/* END Light Italic */
/* BEGIN Regular */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/Regular/OpenSans-Regular.woff2?v=1.1.0") format("woff2"), url("./fonts/Regular/OpenSans-Regular.woff?v=1.1.0") format("woff");
font-weight: normal;
font-style: normal;
}
/* END Regular */
/* BEGIN Italic */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/Italic/OpenSans-Italic.woff2?v=1.1.0") format("woff2"), url("./fonts/Italic/OpenSans-Italic.woff?v=1.1.0") format("woff");
font-weight: normal;
font-style: italic;
}
/* END Italic */
/* BEGIN Semibold */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/Semibold/OpenSans-Semibold.woff2?v=1.1.0") format("woff2"), url("./fonts/Semibold/OpenSans-Semibold.woff?v=1.1.0") format("woff");
font-weight: 600;
font-style: normal;
}
/* END Semibold */
/* BEGIN Semibold Italic */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/SemiboldItalic/OpenSans-SemiboldItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/SemiboldItalic/OpenSans-SemiboldItalic.woff?v=1.1.0") format("woff");
font-weight: 600;
font-style: italic;
}
/* END Semibold Italic */
/* BEGIN Bold */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/Bold/OpenSans-Bold.woff2?v=1.1.0") format("woff2"), url("./fonts/Bold/OpenSans-Bold.woff?v=1.1.0") format("woff");
font-weight: bold;
font-style: normal;
}
/* END Bold */
/* BEGIN Bold Italic */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/BoldItalic/OpenSans-BoldItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/BoldItalic/OpenSans-BoldItalic.woff?v=1.1.0") format("woff");
font-weight: bold;
font-style: italic;
}
/* END Bold Italic */
/* BEGIN Extrabold */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/ExtraBold/OpenSans-ExtraBold.woff2?v=1.1.0") format("woff2"), url("./fonts/ExtraBold/OpenSans-ExtraBold.woff?v=1.1.0") format("woff");
font-weight: 800;
font-style: normal;
}
/* END Extrabold */
/* BEGIN Extrabold Italic */
@font-face {
font-family: 'Open Sans';
src: url("./fonts/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff?v=1.1.0") format("woff");
font-weight: 800;
font-style: italic;
}
/* END Extrabold Italic */

View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -2,6 +2,22 @@
Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases). Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases).
## 2.2.0-beta.2
### New Features
- Added support for custom key bindings. See [the documentation](https://heynote.com/docs/#user-content-custom-key-bindings) for more info.
- Added a "command palette" that can be accessed by pressing `Ctrl/Cmd+Shift+P`, or just typing `>` in the buffer selector. The command palette allows you to discover all available commands in the app, and to quickly execute them.
### Other changes
- Upgraded to latest version of Electron, Vue, electron-builder and other dependencies.
## 2.1.4
- Fix issue with positioning and size of todo list checkboxes in Markdown blocks when using a non-default font size, or a non-monospaced font.
- Fix issue when pressing `Ctrl/Cmd+A` in a text input inside a modal dialog (e.g. the buffer selector). Previously the select all command would be sent to the editor.
## 2.1.3 ## 2.1.3
- Fix escaping issue in buffer selector (properly this time, hopefully) - Fix escaping issue in buffer selector (properly this time, hopefully)

View File

@ -68,6 +68,12 @@ Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, C
Alt Show menu Alt Show menu
``` ```
## Custom Key Bindings
Heynote supports custom key bindings which you can configure in the settings. The key bindings are evaluated from top to bottom, so a binding that comes before another one will take precedence. Most commands will stop the event from propagating, but some commands only applies in certain contexts and might not stop the event from propagating to a later key binding.
To disable one of the built in key bindings, you can add a new key binding with the same key combination for the command "Do nothing". This will stop the event from propagating to the built in key binding.
## Download/Installation ## 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). 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).

View File

@ -13,7 +13,6 @@
"dist-electron", "dist-electron",
"dist" "dist"
], ],
"afterSign": "electron-builder-notarize",
"mac": { "mac": {
"artifactName": "${productName}_${version}_${arch}.${ext}", "artifactName": "${productName}_${version}_${arch}.${ext}",
"target": [ "target": [

View File

@ -24,6 +24,19 @@ const schema = {
properties: { properties: {
"keymap": { "enum": ["default", "emacs"], default:"default" }, "keymap": { "enum": ["default", "emacs"], default:"default" },
"emacsMetaKey": { "enum": [null, "alt", "meta"], default: null }, "emacsMetaKey": { "enum": [null, "alt", "meta"], default: null },
"keyBindings": {
"type": "array",
"items": {
"type": "object",
"required": ["key", "command"],
"properties": {
"key": { "type": "string" },
"command": { "type": "string" }
},
"additionalProperties": false
}
},
"showLineNumberGutter": {type: "boolean", default:true}, "showLineNumberGutter": {type: "boolean", default:true},
"showFoldGutter": {type: "boolean", default:true}, "showFoldGutter": {type: "boolean", default:true},
"autoUpdate": {type: "boolean", default: true}, "autoUpdate": {type: "boolean", default: true},
@ -62,6 +75,7 @@ const defaults = {
settings: { settings: {
keymap: "default", keymap: "default",
emacsMetaKey: isMac ? "meta" : "alt", emacsMetaKey: isMac ? "meta" : "alt",
keyBindings: [],
showLineNumberGutter: true, showLineNumberGutter: true,
showFoldGutter: true, showFoldGutter: true,
autoUpdate: true, autoUpdate: true,

View File

@ -64,8 +64,6 @@ const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html') const indexHtml = join(process.env.DIST, 'index.html')
let currentKeymap = CONFIG.get("settings.keymap")
// if this version is a beta version, set the release channel to beta // if this version is a beta version, set the release channel to beta
const isBetaVersion = app.getVersion().includes("beta") const isBetaVersion = app.getVersion().includes("beta")
if (isBetaVersion) { if (isBetaVersion) {
@ -85,8 +83,8 @@ export function quit() {
async function createWindow() { async function createWindow() {
// read any stored window settings from config, or use defaults // read any stored window settings from config, or use defaults
let windowConfig = { let windowConfig = {
width: CONFIG.get("windowConfig.width", 900) as number, width: CONFIG.get("windowConfig.width", 940) as number,
height: CONFIG.get("windowConfig.height", 680) as number, height: CONFIG.get("windowConfig.height", 720) as number,
isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean, isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean,
isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean, isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean,
x: CONFIG.get("windowConfig.x"), x: CONFIG.get("windowConfig.x"),
@ -409,9 +407,6 @@ ipcMain.handle("getInitErrors", () => {
ipcMain.handle('settings:set', async (event, settings) => { ipcMain.handle('settings:set', async (event, settings) => {
if (settings.keymap !== CONFIG.get("settings.keymap")) {
currentKeymap = settings.keymap
}
let globalHotkeyChanged = settings.enableGlobalHotkey !== CONFIG.get("settings.enableGlobalHotkey") || settings.globalHotkey !== CONFIG.get("settings.globalHotkey") let globalHotkeyChanged = settings.enableGlobalHotkey !== CONFIG.get("settings.enableGlobalHotkey") || settings.globalHotkey !== CONFIG.get("settings.globalHotkey")
let showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock"); let showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock");
let showInMenuChanged = settings.showInMenu !== CONFIG.get("settings.showInMenu"); let showInMenuChanged = settings.showInMenu !== CONFIG.get("settings.showInMenu");

8626
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "Heynote", "name": "Heynote",
"version": "2.1.3", "version": "2.2.0-beta.2",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"description": "A dedicated scratch pad", "description": "A dedicated scratch pad",
"author": "Jonatan Heyman (https://heyman.info)", "author": "Jonatan Heyman (https://heyman.info)",
@ -50,34 +50,35 @@
"@codemirror/legacy-modes": "^6.3.3", "@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2", "@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5", "@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.3.3", "@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.22.2", "@codemirror/view": "^6.36.5",
"@electron/asar": "^3.2.2", "@electron/asar": "^3.2.2",
"@lezer/generator": "^1.5.1", "@lezer/generator": "^1.5.1",
"@lezer/markdown": "^1.1.2", "@lezer/markdown": "^1.1.2",
"@playwright/test": "^1.49.0", "@playwright/test": "^1.51.1",
"@replit/codemirror-lang-csharp": "^6.2.0", "@replit/codemirror-lang-csharp": "^6.2.0",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",
"@types/node": "^20.10.5", "@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.2.3",
"codemirror-lang-elixir": "^4.0.0", "codemirror-lang-elixir": "^4.0.0",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"electron": "^33.3.1", "electron": "^35.2.0",
"electron-builder": "^23.6.0", "electron-builder": "^26.0.12",
"electron-builder-notarize": "^1.5.1",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "^6.1.7", "electron-updater": "^6.6.2",
"fs-jetpack": "^5.1.0", "fs-jetpack": "^5.1.0",
"lezer-elixir": "^1.1.2", "lezer-elixir": "^1.1.2",
"prettier": "^3.3.2", "prettier": "^3.3.2",
"primevue": "^4.3.3",
"rollup-plugin-license": "^3.0.1", "rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1", "sass-embedded": "^1.87.0",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"vite": "^4.5.2", "vite": "^6.3.2",
"vite-plugin-electron": "^0.11.1", "vite-plugin-electron": "^0.11.1",
"vite-plugin-electron-renderer": "^0.11.4", "vite-plugin-electron-renderer": "^0.11.4",
"vue": "^3.2.45", "vue": "^3.5.13",
"vue-tsc": "^1.0.16" "vue-tsc": "^1.0.16",
"vuedraggable": "^4.1.0"
}, },
"dependencies": { "dependencies": {
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",

View File

@ -20,7 +20,8 @@ export default defineConfig({
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, //workers: process.env.CI ? 1 : undefined,
workers: process.env.CI ? undefined : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI ? [['github'], ['html']] : 'list', reporter: process.env.CI ? [['github'], ['html']] : 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

View File

@ -65,6 +65,7 @@
showCreateBuffer(value) { this.dialogWatcher(value) }, showCreateBuffer(value) { this.dialogWatcher(value) },
showEditBuffer(value) { this.dialogWatcher(value) }, showEditBuffer(value) { this.dialogWatcher(value) },
showMoveToBufferSelector(value) { this.dialogWatcher(value) }, showMoveToBufferSelector(value) { this.dialogWatcher(value) },
showCommandPalette(value) { this.dialogWatcher(value) },
currentBufferPath() { currentBufferPath() {
this.focusEditor() this.focusEditor()
@ -85,11 +86,15 @@
"showCreateBuffer", "showCreateBuffer",
"showEditBuffer", "showEditBuffer",
"showMoveToBufferSelector", "showMoveToBufferSelector",
"openMoveToBufferSelector", "showCommandPalette",
]), ]),
dialogVisible() {
return this.showLanguageSelector || this.showBufferSelector || this.showCreateBuffer || this.showEditBuffer || this.showMoveToBufferSelector || this.showCommandPalette || this.showSettings
},
editorInert() { editorInert() {
return this.showCreateBuffer || this.showSettings || this.showEditBuffer return this.dialogVisible
}, },
}, },
@ -173,7 +178,9 @@
@close="closeDialog" @close="closeDialog"
/> />
<BufferSelector <BufferSelector
v-if="showBufferSelector" v-if="showBufferSelector || showCommandPalette"
:initialFilter="showCommandPalette ? '>' : ''"
:commandsEnabled="true"
@openBuffer="openBuffer" @openBuffer="openBuffer"
@openCreateBuffer="(nameSuggestion) => openCreateBuffer('new', nameSuggestion)" @openCreateBuffer="(nameSuggestion) => openCreateBuffer('new', nameSuggestion)"
@close="closeBufferSelector" @close="closeBufferSelector"
@ -181,6 +188,7 @@
<BufferSelector <BufferSelector
v-if="showMoveToBufferSelector" v-if="showMoveToBufferSelector"
headline="Move block to..." headline="Move block to..."
:commandsEnabled="false"
@openBuffer="onMoveCurrentBlockToOtherEditor" @openBuffer="onMoveCurrentBlockToOtherEditor"
@openCreateBuffer="(nameSuggestion) => openCreateBuffer('currentBlock', nameSuggestion)" @openCreateBuffer="(nameSuggestion) => openCreateBuffer('currentBlock', nameSuggestion)"
@close="closeMoveToBufferSelector" @close="closeMoveToBufferSelector"

View File

@ -4,6 +4,7 @@
import { mapState, mapActions } from 'pinia' import { mapState, mapActions } from 'pinia'
import { SCRATCH_FILE_NAME } from "../common/constants" import { SCRATCH_FILE_NAME } from "../common/constants"
import { useHeynoteStore } from "../stores/heynote-store" import { useHeynoteStore } from "../stores/heynote-store"
import { HEYNOTE_COMMANDS } from '../editor/commands'
const pathSep = window.heynote.buffer.pathSeparator const pathSep = window.heynote.buffer.pathSeparator
@ -19,13 +20,15 @@
export default { export default {
props: { props: {
headline: String, headline: String,
initialFilter: String,
commandsEnabled: Boolean,
}, },
data() { data() {
return { return {
selected: 0, selected: 0,
actionButton: 0, actionButton: 0,
filter: "", filter: this.initialFilter || "",
items: [], items: [],
SCRATCH_FILE_NAME: SCRATCH_FILE_NAME, SCRATCH_FILE_NAME: SCRATCH_FILE_NAME,
deleteConfirm: false, deleteConfirm: false,
@ -48,6 +51,25 @@
"recentBufferPaths", "recentBufferPaths",
]), ]),
commands() {
const commands = Object.entries(HEYNOTE_COMMANDS)
// sort array first by category, then by description
commands.sort((a, b) => {
const aCategory = a[1].category || ""
const bCategory = b[1].category || ""
if (aCategory === bCategory) {
return a[1].description.localeCompare(b[1].description)
} else {
return aCategory.localeCompare(bCategory)
}
})
return commands.map(([cmdKey, cmd]) => ({
name: `${cmd.category}: ${cmd.description}`,
cmd: cmdKey,
isCommand: true,
}))
},
orderedItems() { orderedItems() {
const sortKeys = Object.fromEntries(this.recentBufferPaths.map((item, idx) => [item, idx])) const sortKeys = Object.fromEntries(this.recentBufferPaths.map((item, idx) => [item, idx]))
const getSortScore = (item) => sortKeys[item.path] !== undefined ? sortKeys[item.path] : 1000 const getSortScore = (item) => sortKeys[item.path] !== undefined ? sortKeys[item.path] : 1000
@ -74,32 +96,47 @@
}, },
filteredItems() { filteredItems() {
let items if (this.commandsEnabled && this.filter.startsWith(">")) {
if (this.filter === "") { // command mode if the first character is ">"
items = this.orderedItems if (this.filter.length < 2) {
return this.commands
} else { }
const searchResults = fuzzysort.go(this.filter, this.items, { const searchResults = fuzzysort.go(this.filter.slice(1), this.commands, {
keys: ["name", "folder"], keys: ["name"],
}) })
items = searchResults.map((result) => { return searchResults.map((result) => {
const obj = {...result.obj} const obj = {...result.obj}
const nameHighlight = result[0].highlight("<b>", "</b>") const nameHighlight = result[0].highlight("<b>", "</b>")
const folderHighlight = result[1].highlight("<b>", "</b>")
obj.name = nameHighlight !== "" ? nameHighlight : obj.name obj.name = nameHighlight !== "" ? nameHighlight : obj.name
obj.folder = folderHighlight !== "" ? folderHighlight : obj.folder
return obj return obj
}) })
} } else {
let items
if (this.filter === "") {
items = this.orderedItems
} else {
const searchResults = fuzzysort.go(this.filter, this.items, {
keys: ["name", "folder"],
})
items = searchResults.map((result) => {
const obj = {...result.obj}
const nameHighlight = result[0].highlight("<b>", "</b>")
const folderHighlight = result[1].highlight("<b>", "</b>")
obj.name = nameHighlight !== "" ? nameHighlight : obj.name
obj.folder = folderHighlight !== "" ? folderHighlight : obj.folder
return obj
})
}
const newNoteItem = { const newNoteItem = {
name: "Create new…", name: "Create new…",
createNew:true, createNew:true,
}
return [
...items,
newNoteItem,
]
} }
return [
...items,
newNoteItem,
]
}, },
}, },
@ -108,6 +145,7 @@
"updateBuffers", "updateBuffers",
"editBufferMetadata", "editBufferMetadata",
"deleteBuffer", "deleteBuffer",
"executeCommand",
]), ]),
buildItems() { buildItems() {
@ -187,13 +225,18 @@
} else { } else {
this.$emit("openCreateBuffer", "") this.$emit("openCreateBuffer", "")
} }
} else if (item.isCommand) {
this.$emit("close")
this.$nextTick(() => {
this.executeCommand(item.cmd)
})
} else { } else {
this.$emit("openBuffer", item.path) this.$emit("openBuffer", item.path)
} }
}, },
itemHasActionButtons(item) { itemHasActionButtons(item) {
return !item.createNew && item.path !== SCRATCH_FILE_NAME return !item.createNew && item.path !== SCRATCH_FILE_NAME && !item.isCommand
}, },
onInput(event) { onInput(event) {
@ -248,7 +291,7 @@
</script> </script>
<template> <template>
<form class="note-selector" tabindex="-1" @focusout="onFocusOut" ref="container"> <form class="note-selector" tabindex="-1" @focusout="onFocusOut" ref="container" @submit.prevent>
<div class="input-container"> <div class="input-container">
<h1 v-if="headline">{{headline}}</h1> <h1 v-if="headline">{{headline}}</h1>
<input <input
@ -308,12 +351,11 @@
<style scoped lang="sass"> <style scoped lang="sass">
.note-selector .note-selector
font-size: 13px font-size: 13px
//background: #48b57e
background: #efefef background: #efefef
position: absolute position: absolute
top: 0 top: 0
left: 50% left: 50%
width: 420px width: 440px
transform: translateX(-50%) transform: translateX(-50%)
max-height: 100% max-height: 100%
box-sizing: border-box box-sizing: border-box

View File

@ -120,6 +120,12 @@
}, },
onInputKeydown(event) { onInputKeydown(event) {
// support Ctrl/Cmd+A to select all
if (event.key === "a" && event[window.heynote.platform.isMac ? "metaKey" : "ctrlKey"]) {
event.preventDefault()
event.srcElement.select()
}
// redirect arrow keys and page up/down to folder selector // redirect arrow keys and page up/down to folder selector
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"] const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
if (redirectKeys.includes(event.key)) { if (redirectKeys.includes(event.key)) {

View File

@ -22,6 +22,10 @@
syntaxTreeDebugContent: null, syntaxTreeDebugContent: null,
editor: null, editor: null,
onWindowClose: null, onWindowClose: null,
onUndo: null,
onRedo: null,
onDeleteBlock: null,
onSelectAll: null,
} }
}, },
@ -39,29 +43,40 @@
} }
window.heynote.mainProcess.on(WINDOW_CLOSE_EVENT, this.onWindowClose) window.heynote.mainProcess.on(WINDOW_CLOSE_EVENT, this.onWindowClose)
window.heynote.mainProcess.on(UNDO_EVENT, () => { this.onUndo = () => {
if (this.editor) { if (this.editor) {
toRaw(this.editor).undo() toRaw(this.editor).undo()
} }
}) }
window.heynote.mainProcess.on(UNDO_EVENT, this.onUndo)
window.heynote.mainProcess.on(REDO_EVENT, () => { this.onRedo = () => {
if (this.editor) { if (this.editor) {
toRaw(this.editor).redo() toRaw(this.editor).redo()
} }
}) }
window.heynote.mainProcess.on(REDO_EVENT, this.onRedo)
window.heynote.mainProcess.on(DELETE_BLOCK_EVENT, () => { this.onDeleteBlock = () => {
if (this.editor) { if (this.editor) {
toRaw(this.editor).deleteActiveBlock() toRaw(this.editor).deleteActiveBlock()
} }
}) }
window.heynote.mainProcess.on(DELETE_BLOCK_EVENT, this.onDeleteBlock)
window.heynote.mainProcess.on(SELECT_ALL_EVENT, () => { this.onSelectAll = () => {
if (this.editor) { const activeEl = document.activeElement
toRaw(this.editor).selectAll() if (activeEl && activeEl.tagName === "INPUT") {
// if the active element is an input, select all text in it
activeEl.select()
} else if (this.editor) {
// make sure the editor is focused
if (this.$refs.editor.contains(activeEl)) {
toRaw(this.editor).selectAll()
}
} }
}) }
window.heynote.mainProcess.on(SELECT_ALL_EVENT, this.onSelectAll)
// if debugSyntaxTree prop is set, display syntax tree for debugging // if debugSyntaxTree prop is set, display syntax tree for debugging
if (this.debugSyntaxTree) { if (this.debugSyntaxTree) {
@ -85,10 +100,10 @@
beforeUnmount() { beforeUnmount() {
window.heynote.mainProcess.off(WINDOW_CLOSE_EVENT, this.onWindowClose) window.heynote.mainProcess.off(WINDOW_CLOSE_EVENT, this.onWindowClose)
window.heynote.mainProcess.off(UNDO_EVENT) window.heynote.mainProcess.off(UNDO_EVENT, this.onUndo)
window.heynote.mainProcess.off(REDO_EVENT) window.heynote.mainProcess.off(REDO_EVENT, this.onRedo)
window.heynote.mainProcess.off(DELETE_BLOCK_EVENT) window.heynote.mainProcess.off(DELETE_BLOCK_EVENT, this.onDeleteBlock)
window.heynote.mainProcess.off(SELECT_ALL_EVENT) window.heynote.mainProcess.off(SELECT_ALL_EVENT, this.onSelectAll)
this.editorCacheStore.tearDown(); this.editorCacheStore.tearDown();
}, },

View File

@ -154,7 +154,7 @@
border: 1px solid #ccc border: 1px solid #ccc
box-sizing: border-box box-sizing: border-box
border-radius: 2px border-radius: 2px
width: 400px width: 300px
margin-bottom: 10px margin-bottom: 10px
&:focus &:focus
outline: none outline: none

View File

@ -128,6 +128,12 @@
}, },
onInputKeydown(event) { onInputKeydown(event) {
// support Ctrl/Cmd+A to select all
if (event.key === "a" && event[window.heynote.platform.isMac ? "metaKey" : "ctrlKey"]) {
event.preventDefault()
event.srcElement.select()
}
// redirect arrow keys and page up/down to folder selector // redirect arrow keys and page up/down to folder selector
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"] const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
if (redirectKeys.includes(event.key)) { if (redirectKeys.includes(event.key)) {

View File

@ -0,0 +1,211 @@
<script>
import fuzzysort from 'fuzzysort'
import AutoComplete from 'primevue/autocomplete'
import { HEYNOTE_COMMANDS } from '@/src/editor/commands'
import RecordKeyInput from './RecordKeyInput.vue'
export default {
name: "AddKeyBind",
components: {
AutoComplete,
RecordKeyInput,
},
data() {
return {
key: "",
command: "",
commandSuggestions: [],
}
},
computed: {
commands() {
return Object.entries(HEYNOTE_COMMANDS).map(([key, cmd]) => {
const description = cmd.category + ": " + cmd.description
return {
name: key,
category: cmd.category,
description: description,
key: cmd.description,
label: description,
}
})
},
},
mounted() {
window.addEventListener("keydown", this.onKeyDown)
this.$refs.keys.$el.focus()
},
beforeUnmount() {
window.removeEventListener("keydown", this.onKeyDown)
},
methods: {
onKeyDown(event) {
if (event.key === "Escape" && document.activeElement !== this.$refs.keys.$el) {
this.$emit("close")
}
},
onCommandSearch(event) {
if (event.query === "") {
this.commandSuggestions = [...this.commands]
} else {
const searchResults = fuzzysort.go(event.query, this.commands, {
keys: ["description", "name"],
})
this.commandSuggestions = searchResults.map((result) => {
const obj = {...result.obj}
const nameHighlight = result[0].highlight("<b>", "</b>")
obj.label = nameHighlight !== "" ? nameHighlight : obj.description
return obj
})
}
},
onSave() {
if (this.key === "" || this.command === "") {
return
}
this.$emit("save", {
key: this.key,
command: this.command.name,
})
},
focusCommandSelector() {
this.$refs.autocomplete.$el.querySelector("input").focus()
},
},
}
</script>
<template>
<div class="container add-key-binding-dialog">
<div class="dialog">
<div class="dialog-content">
<h3>Add key binding</h3>
<div class="form">
<div class="field">
<label>Key</label>
<RecordKeyInput
v-model="key"
@enter="focusCommandSelector"
@close="$emit('close')"
ref="keys"
/>
</div>
<div class="field">
<label>Command</label>
<AutoComplete
dropdown
forceSelection
v-model="command"
:suggestions="commandSuggestions"
:autoOptionFocus="true"
optionLabel="key"
:delay="0"
@complete="onCommandSearch"
ref="autocomplete"
emptySearchMessage="No commands found"
class="command-autocomplete"
>
<template #option="slotProps">
<div class="command-option">
<span v-html="slotProps.option.label" />
</div>
</template>
</AutoComplete>
</div>
</div>
</div>
<div class="footer">
<button
@click="onSave"
class="save"
>Save</button>
<button
@click="$emit('close')"
class="cancel"
>Cancel</button>
</div>
</div>
</div>
</template>
<style lang="sass" scoped>
.container
position: absolute
top: 0
left: 0
right: 0
bottom: 0
background: rgba(255,255,255, 0.7)
+dark-mode
background: rgba(51,51,51, 0.7)
.dialog
width: 400px
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
display: flex
flex-direction: column
background: #fff
border-radius: 5px
box-shadow: 0 5px 30px rgba(0,0,0, 0.3)
border: 2px solid #c0c0c0
+dark-mode
background: #333
box-shadow: 0 5px 30px rgba(0,0,0, 0.7)
border: 2px solid #555
.dialog-content
flex-grow: 1
padding: 20px
h3
font-size: 14px
font-weight: 600
text-align: center
margin: 0
margin-bottom: 30px
.form
//display: flex
.field
//width: 50%
margin-bottom: 20px
&:last-child
margin-bottom: 0
label
display: block
margin-bottom: 8px
input.keys
width: 100%
padding: 4px
border-radius: 2px
border: 1px solid #ccc
&:focus
border: 1px solid rgba(0,0,0, 0)
outline: 2px solid var(--highlight-color)
//border: 1px solid var(--highlight-color)
+dark-mode
background: #202020
color: #fff
border: 1px solid #5a5a5a
&:focus
border: 1px solid rgba(0,0,0, 0)
.command-autocomplete
width: 100%
.footer
padding: 10px
background: #f1f1f1
border-radius: 0 0 5px 5px
text-align: right
+dark-mode
background: #2c2c2c
.cancel
float: left
</style>

View File

@ -0,0 +1,105 @@
<script>
import { HEYNOTE_COMMANDS } from '@/src/editor/commands'
export default {
props: [
"keys",
"command",
"isDefault",
"source",
],
computed: {
formattedKeys() {
return this.keys.replaceAll(
"Mod",
window.heynote.platform.isMac ? "⌘" : "Ctrl",
)
},
commandLabel() {
const cmd = HEYNOTE_COMMANDS[this.command]
if (cmd) {
return `${cmd.category}: ${cmd.description}`
}
return HEYNOTE_COMMANDS[this.command]?.description ||this.command
},
className() {
return this.isDefault ? "keybind-default" : "keybind-user"
},
},
}
</script>
<template>
<tr :class="className">
<td class="source">
{{ source }}
</td>
<td class="key">
<template v-if="keys">
{{ formattedKeys }}
</template>
</td>
<td class="command">
<span class="command-name">{{ commandLabel }}</span>
</td>
<td class="actions">
<button
v-if="!isDefault"
@click="$emit('delete')"
class="delete"
>Delete</button>
</td>
<td v-if="!isDefault" class="drag-handle"></td>
<td v-else></td>
</tr>
</template>
<style lang="sass" scoped>
tr
&.overridden
text-decoration: line-through
color: rgba(0,0,0, 0.4)
+dark-mode
color: rgba(255,255,255, 0.4)
td
&.key
//letter-spacing: 1px
&.command
//
&.drag-handle
width: 24px
padding: 0
cursor: ns-resize
background-color: rgba(0,0,0, 0.02)
background-size: 20px
background-repeat: no-repeat
background-position: center center
background-image: url(@/assets/icons/drag-vertical-light.svg)
+dark-mode
background-color: rgba(0,0,0, 0.08)
background-image: url(@/assets/icons/drag-vertical-dark.svg)
&:hover
background-color: rgba(0,0,0, 0.05)
+dark-mode
background-color: rgba(0,0,0, 0.25)
button.delete
padding: 0 10px
height: 22px
font-size: 12px
background: none
border: none
border-radius: 2px
cursor: pointer
background: #ddd
&:hover
background: #ccc
+dark-mode
background: #555
color: #fff
&:hover
background: #666
</style>

View File

@ -0,0 +1,200 @@
<script>
import { mapState} from 'pinia'
import draggable from 'vuedraggable'
import { DEFAULT_KEYMAP, EMACS_KEYMAP } from "@/src/editor/keymap"
import { useSettingsStore } from "@/src/stores/settings-store"
import KeyBindRow from "./KeyBindRow.vue"
import AddKeyBind from "./AddKeyBind.vue"
export default {
props: [
"userKeys",
"modelValue",
],
components: {
draggable,
KeyBindRow,
AddKeyBind,
},
data() {
return {
keymap: this.modelValue,
addKeyBinding: false,
}
},
mounted() {
},
watch: {
addKeyBinding(newValue) {
this.$emit("addKeyBindingDialogVisible", newValue)
},
},
computed: {
...mapState(useSettingsStore, [
"settings",
]),
fixedKeymap() {
const defaultKeymap = (this.settings.keymap === "emacs" ? EMACS_KEYMAP : []).map((km) => ({
key: km.key,
command: km.command,
isDefault: true,
source: "Emacs",
}))
return defaultKeymap.concat(
DEFAULT_KEYMAP.map((km) => ({
key: km.key,
command: km.command,
isDefault: true,
source: "Default",
}))
)
},
},
methods: {
onDragEnd(event) {
this.$emit("update:modelValue", this.keymap)
},
onSaveKeyBinding(event) {
this.keymap = [
{
key: event.key,
command: event.command,
},
...(this.keymap ? this.keymap : []),
]
//console.log("keymap", this.keymap)
this.$emit("update:modelValue", this.keymap)
this.addKeyBinding = false
},
deleteKeyBinding(index) {
this.keymap = this.keymap.toSpliced(index, 1)
this.$emit("update:modelValue", this.keymap)
},
},
}
</script>
<template>
<div class="container">
<div class="header" :inert="addKeyBinding">
<h2>Keyboard Bindings</h2>
<!--<p>User key bindings can be reordered. Bindings that appear first take precedence</p>-->
<div class="button-container">
<button
class="add-keybinding"
@click="addKeyBinding = !addKeyBinding"
>Add Keybinding</button>
</div>
</div>
<AddKeyBind
v-if="addKeyBinding"
@close="addKeyBinding = false"
@save="onSaveKeyBinding"
/>
<table :inert="addKeyBinding">
<thead>
<tr>
<th>Source</th>
<th>Key</th>
<th>Command</th>
<th></th>
<th></th>
</tr>
</thead>
<draggable
v-model="keymap"
tag="tbody"
group="keymap"
ghost-class="ghost"
handle=".drag-handle"
@start="drag=true"
@end="onDragEnd"
item-key="key"
>
<template #item="{element, index}">
<KeyBindRow
:keys="element.key"
:command="element.command"
:isDefault="element.isDefault"
:index="index"
@delete="deleteKeyBinding(index)"
source="User"
/>
</template>
</draggable>
<tbody>
<KeyBindRow
v-for="key in fixedKeymap"
:key="key.source + '_' + key.key"
:keys="key.key"
:command="key.command"
:isDefault="key.isDefault"
:source="key.source"
/>
</tbody>
</table>
</div>
</template>
<style lang="sass" scoped>
.header
display: flex
margin-bottom: 12px
h2
flex-grow: 1
font-weight: 600
font-size: 14px
margin: 0
.button-container
.add-keybinding
font-size: 12px
height: 26px
cursor: pointer
table
width: 100%
background: #f1f1f1
border: 2px solid #f1f1f1
+dark-mode
background: #3c3c3c
background: #333
border: 2px solid #3c3c3c
::v-deep(tr)
background: #fff
border-bottom: 2px solid #f1f1f1
+dark-mode
background: #333
border-bottom: 2px solid #3c3c3c
&.ghost
background: #48b57e
color: #fff
+dark-mode
background: #1b6540
th
text-align: left
font-weight: 600
th, td
padding: 8px
&.actions
padding: 6px
button
height: 20px
font-size: 11px
tbody
margin-bottom: 20px
</style>

View File

@ -0,0 +1,88 @@
<script>
import { keyName, base } from "w3c-keyname"
export default {
props: [
"modelValue",
],
data() {
return {
keys: this.modelValue ? this.modelValue.split(" ") : [],
}
},
computed: {
key() {
return this.keys.join(" ")
},
},
watch: {
modelValue(newValue) {
this.keys = this.modelValue ? this.modelValue.split(" ") : []
},
key(newValue) {
this.$emit("update:model-value", newValue)
},
},
methods: {
onKeyDown(event) {
event.preventDefault()
//console.log("event", event, event.code, keyName(event))
if (event.key === "Enter") {
this.$emit("enter")
} else if (event.key === "Escape") {
if (this.keys.length > 0) {
this.keys = []
} else {
// setTimeout is used to ensure that the settings dialog's keydown listener
// doesn't close the whole settings dialog
setTimeout(() => {
this.$emit("close")
}, 0)
}
} else if (["Alt", "Control", "Meta", "Shift"].includes(event.key)) {
} else {
if (this.keys.length >= 2) {
this.keys = []
}
let keyCombo = ""
if (event.altKey) {
keyCombo += "Alt-"
}
if (event.ctrlKey) {
keyCombo += "Control-"
}
if (event.metaKey) {
keyCombo += "Meta-"
}
if (event.shiftKey) {
keyCombo += "Shift-"
}
let key = base[event.keyCode]
if (key) {
if (key === " ") {
key = "Space"
}
keyCombo += key
this.keys.push(keyCombo)
}
}
},
},
}
</script>
<template>
<input
type="text"
:value="key"
@keydown.prevent="onKeyDown"
class="keys"
readonly
>
</template>

View File

@ -1,9 +1,11 @@
<script> <script>
import { toRaw} from 'vue';
import { LANGUAGES } from '../../editor/languages.js' import { LANGUAGES } from '../../editor/languages.js'
import KeyboardHotkey from "./KeyboardHotkey.vue" import KeyboardHotkey from "./KeyboardHotkey.vue"
import TabListItem from "./TabListItem.vue" import TabListItem from "./TabListItem.vue"
import TabContent from "./TabContent.vue" import TabContent from "./TabContent.vue"
import KeyboardBindings from './KeyboardBindings.vue'
const defaultFontFamily = window.heynote.defaultFontFamily const defaultFontFamily = window.heynote.defaultFontFamily
const defaultFontSize = window.heynote.defaultFontSize const defaultFontSize = window.heynote.defaultFontSize
@ -20,15 +22,18 @@
KeyboardHotkey, KeyboardHotkey,
TabListItem, TabListItem,
TabContent, TabContent,
KeyboardBindings,
}, },
data() { data() {
//console.log("settings:", this.initialSettings)
return { return {
keymaps: [ keymaps: [
{ name: "Default", value: "default" }, { name: "Default", value: "default" },
{ name: "Emacs", value: "emacs" }, { name: "Emacs", value: "emacs" },
], ],
keymap: this.initialSettings.keymap, keymap: this.initialSettings.keymap,
keyBindings: this.initialSettings.keyBindings,
metaKey: this.initialSettings.emacsMetaKey, metaKey: this.initialSettings.emacsMetaKey,
isMac: window.heynote.platform.isMac, isMac: window.heynote.platform.isMac,
showLineNumberGutter: this.initialSettings.showLineNumberGutter, showLineNumberGutter: this.initialSettings.showLineNumberGutter,
@ -63,28 +68,36 @@
defaultFontSize: defaultFontSize, defaultFontSize: defaultFontSize,
appVersion: "", appVersion: "",
theme: this.themeSetting, theme: this.themeSetting,
// tracks if the add key binding dialog is visible (so that we can set inert on the save button)
addKeyBindingDialogVisible: false,
} }
}, },
async mounted() { async mounted() {
window.addEventListener("keydown", this.onKeyDown);
this.appVersion = await window.heynote.getVersion()
if (window.queryLocalFonts !== undefined) { if (window.queryLocalFonts !== undefined) {
let localFonts = [... new Set((await window.queryLocalFonts()).map(f => f.family))].filter(f => f !== "Hack") let localFonts = [... new Set((await window.queryLocalFonts()).map(f => f.family))].filter(f => f !== "Hack")
localFonts = [...new Set(localFonts)].map(f => [f, f]) localFonts = [...new Set(localFonts)].map(f => [f, f])
this.systemFonts = [[defaultFontFamily, defaultFontFamily + " (default)"], ...localFonts] this.systemFonts = [[defaultFontFamily, defaultFontFamily + " (default)"], ...localFonts]
} }
window.addEventListener("keydown", this.onKeyDown);
this.$refs.keymapSelector.focus()
this.appVersion = await window.heynote.getVersion()
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener("keydown", this.onKeyDown); window.removeEventListener("keydown", this.onKeyDown);
}, },
watch: {
keyBindings(newKeyBindings) {
this.updateSettings()
}
},
methods: { methods: {
onKeyDown(event) { onKeyDown(event) {
if (event.key === "Escape") { if (event.key === "Escape" && !this.addKeyBindingDialogVisible) {
this.$emit("closeSettings") this.$emit("closeSettings")
} }
}, },
@ -94,6 +107,7 @@
showLineNumberGutter: this.showLineNumberGutter, showLineNumberGutter: this.showLineNumberGutter,
showFoldGutter: this.showFoldGutter, showFoldGutter: this.showFoldGutter,
keymap: this.keymap, keymap: this.keymap,
keyBindings: this.keyBindings.map((kb) => toRaw(kb)),
emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt", emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt",
allowBetaVersions: this.allowBetaVersions, allowBetaVersions: this.allowBetaVersions,
enableGlobalHotkey: this.enableGlobalHotkey, enableGlobalHotkey: this.enableGlobalHotkey,
@ -161,6 +175,12 @@
:activeTab="activeTab" :activeTab="activeTab"
@click="activeTab = 'appearance'" @click="activeTab = 'appearance'"
/> />
<TabListItem
name="Key Bindings"
tab="keyboard-bindings"
:activeTab="activeTab"
@click="activeTab = 'keyboard-bindings'"
/>
<TabListItem <TabListItem
:name="isWebApp ? 'Version' : 'Updates'" :name="isWebApp ? 'Version' : 'Updates'"
tab="updates" tab="updates"
@ -171,23 +191,6 @@
</nav> </nav>
<div class="settings-content"> <div class="settings-content">
<TabContent tab="general" :activeTab="activeTab"> <TabContent tab="general" :activeTab="activeTab">
<div class="row">
<div class="entry">
<h2>Keymap</h2>
<select ref="keymapSelector" v-model="keymap" @change="updateSettings" class="keymap">
<template v-for="km in keymaps" :key="km.value">
<option :selected="km.value === keymap" :value="km.value">{{ km.name }}</option>
</template>
</select>
</div>
<div class="entry" v-if="keymap === 'emacs' && isMac">
<h2>Meta Key</h2>
<select v-model="metaKey" @change="updateSettings" class="metaKey">
<option :selected="metaKey === 'meta'" value="meta">Command</option>
<option :selected="metaKey === 'alt'" value="alt">Option</option>
</select>
</div>
</div>
<div class="row" v-if="!isWebApp"> <div class="row" v-if="!isWebApp">
<div class="entry"> <div class="entry">
<h2>Global Keyboard Shortcut</h2> <h2>Global Keyboard Shortcut</h2>
@ -244,14 +247,14 @@
</div> </div>
<div class="row" v-if="!isWebApp"> <div class="row" v-if="!isWebApp">
<div class="entry buffer-location"> <div class="entry buffer-location">
<h2>Buffer File Path</h2> <h2>Buffer Files Path</h2>
<label class="keyboard-shortcut-label"> <label class="keyboard-shortcut-label">
<input <input
type="checkbox" type="checkbox"
v-model="customBufferLocation" v-model="customBufferLocation"
@change="onCustomBufferLocationChange" @change="onCustomBufferLocationChange"
/> />
Use custom buffer file location Use custom location for the buffer files
</label> </label>
<div class="file-path"> <div class="file-path">
<button <button
@ -369,6 +372,31 @@
</div> </div>
</TabContent> </TabContent>
<TabContent tab="keyboard-bindings" :activeTab="activeTab">
<div class="row">
<div class="entry">
<h2>Keymap</h2>
<select v-model="keymap" @change="updateSettings" class="keymap">
<template v-for="km in keymaps" :key="km.value">
<option :selected="km.value === keymap" :value="km.value">{{ km.name }}</option>
</template>
</select>
</div>
<div class="entry" v-if="keymap === 'emacs' && isMac">
<h2>Meta Key</h2>
<select v-model="metaKey" @change="updateSettings" class="metaKey">
<option :selected="metaKey === 'meta'" value="meta">Command</option>
<option :selected="metaKey === 'alt'" value="alt">Option</option>
</select>
</div>
</div>
<KeyboardBindings
:userKeys="keyBindings ? keyBindings : {}"
v-model="keyBindings"
@addKeyBindingDialogVisible="addKeyBindingDialogVisible = $event"
/>
</TabContent>
<TabContent tab="updates" :activeTab="activeTab"> <TabContent tab="updates" :activeTab="activeTab">
<div class="row"> <div class="row">
<div class="entry"> <div class="entry">
@ -407,7 +435,7 @@
</div> </div>
</div> </div>
<div class="bottom-bar"> <div class="bottom-bar" :inert="addKeyBindingDialogVisible">
<button <button
@click="$emit('closeSettings')" @click="$emit('closeSettings')"
class="close" class="close"
@ -436,14 +464,16 @@
background: rgba(0, 0, 0, 0.5) background: rgba(0, 0, 0, 0.5)
.dialog .dialog
--dialog-height: 600px
--bottom-bar-height: 48px
box-sizing: border-box box-sizing: border-box
z-index: 2 z-index: 2
position: absolute position: absolute
left: 50% left: 50%
top: 50% top: 50%
transform: translate(-50%, -50%) transform: translate(-50%, -50%)
width: 700px width: 820px
height: 560px height: var(--dialog-height)
max-width: 100% max-width: 100%
max-height: 100% max-height: 100%
display: flex display: flex
@ -463,6 +493,7 @@
.dialog-content .dialog-content
flex-grow: 1 flex-grow: 1
display: flex display: flex
height: calc(var(--dialog-height) - var(--bottom-bar-height))
.sidebar .sidebar
box-sizing: border-box box-sizing: border-box
width: 140px width: 140px
@ -480,6 +511,7 @@
flex-grow: 1 flex-grow: 1
padding: 40px padding: 40px
overflow-y: auto overflow-y: auto
position: relative
select select
height: 22px height: 22px
margin: 4px 0 margin: 4px 0
@ -536,6 +568,8 @@
background: #222 background: #222
color: #aaa color: #aaa
.bottom-bar .bottom-bar
box-sizing: border-box
height: var(--bottom-bar-height)
border-radius: 0 0 5px 5px border-radius: 0 0 5px 5px
background: #eee background: #eee
text-align: right text-align: right
@ -544,4 +578,5 @@
background: #222 background: #222
.close .close
height: 28px height: 28px
cursor: pointer
</style> </style>

View File

@ -18,6 +18,7 @@
li li
padding: 9px 20px padding: 9px 20px
font-size: 13px font-size: 13px
line-height: 1.3
user-select: none user-select: none
cursor: pointer cursor: pointer
&:hover &:hover
@ -25,10 +26,11 @@
+dark-mode +dark-mode
background: #292929 background: #292929
&.active &.active
background: #48b57e background: var(--highlight-color)
color: #fff color: #fff
cursor: default cursor: default
+dark-mode +dark-mode
background: #1b6540 // needed for specificity (to not be overridden by :hover in dark mode)
background: var(--highlight-color)
</style> </style>

View File

@ -1,3 +1,4 @@
@import "reset" @use "reset"
@import "font" @use "font"
@import "base" @use "base"
@use "autocomplete"

45
src/css/autocomplete.sass Normal file
View File

@ -0,0 +1,45 @@
@use "@/src/css/include.sass" as *
.p-component
--p-inputtext-background: #fff
--p-inputtext-border-color: #ccc
--p-inputtext-border-radius: 3px
--p-inputtext-padding-y: 4px
--p-inputtext-padding-x: 4px
--p-inputtext-focus-border-color: var(--p-inputtext-border-color)
--p-inputtext-hover-border-color: var(--p-inputtext-border-color)
--p-inputtext-active-border-color: var(--p-inputtext-border-color)
--p-autocomplete-dropdown-border-color: var(--p-inputtext-border-color)
--p-autocomplete-dropdown-border-radius: var(--p-inputtext-border-radius)
--p-autocomplete-dropdown-hover-border-color: var(--p-inputtext-hover-border-color)
--p-autocomplete-dropdown-active-border-color: var(--p-inputtext-active-border-color)
--p-autocomplete-dropdown-hover-background: #f1f1f1
--p-autocomplete-option-focus-background: var(--highlight-color)
--p-autocomplete-option-focus-color: #fff
+dark-mode
--p-inputtext-background: #202020
--p-inputtext-border-color: #444
--p-autocomplete-dropdown-hover-background: #2a2a2a
.p-inputtext.p-inputtext
font-size: 12px
.p-autocomplete-list-container
background: #fff
border: 1px solid #f1f1f1
padding: 6px 0
border-radius: 3px
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)
+dark-mode
color: rgba(255,255,255, 0.8)
background: #333
border: 1px solid #2a2a2a
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3)
.p-autocomplete-list > li
padding: 6px 10px
b
font-weight: bold

View File

@ -1,10 +1,14 @@
@use "include" as *
:root[theme='light'] :root[theme='light']
--status-bar-background: #48b57e --status-bar-background: #48b57e
--status-bar-color: #fff --status-bar-color: #fff
--highlight-color: #48b57e
:root[theme='dark'] :root[theme='dark']
--status-bar-background: #0e1217 --status-bar-background: #0e1217
--status-bar-color: rgba(255, 255, 255, 0.75) --status-bar-color: rgba(255, 255, 255, 0.75)
--highlight-color: #1b6540
html html
margin: 0 margin: 0

View File

@ -4,7 +4,6 @@
font-weight: 400 font-weight: 400
font-style: normal font-style: normal
@font-face @font-face
font-family: 'Hack' font-family: 'Hack'
src: url('@/assets/font/hack/hack-bold.woff2') format('woff2'), url('@/assets/font/hack/hack-bold.woff') format('woff') src: url('@/assets/font/hack/hack-bold.woff2') format('woff2'), url('@/assets/font/hack/hack-bold.woff') format('woff')
@ -23,5 +22,3 @@
font-weight: 700 font-weight: 700
font-style: italic font-style: italic
@import "@/assets/font/open-sans/open-sans"

View File

@ -8,3 +8,10 @@ export const ADD_NEW_BLOCK = "heynote-add-new-block"
export const DELETE_BLOCK = "heynote-delete-block" export const DELETE_BLOCK = "heynote-delete-block"
export const CURSOR_CHANGE = "heynote-cursor-change" export const CURSOR_CHANGE = "heynote-cursor-change"
export const APPEND_BLOCK = "heynote-append-block" export const APPEND_BLOCK = "heynote-append-block"
export const SET_FONT = "heynote-set-font"
// This function checks if any of the transactions has the given Heynote annotation
export function transactionsHasAnnotation(transactions, annotation) {
return transactions.some(tr => tr.annotation(heynoteEvent) === annotation)
}

View File

@ -360,6 +360,7 @@ export const deleteBlock = (editor) => ({state, dispatch}) => {
selection: EditorSelection.cursor(newSelection), selection: EditorSelection.cursor(newSelection),
annotations: [heynoteEvent.of(DELETE_BLOCK)], annotations: [heynoteEvent.of(DELETE_BLOCK)],
})) }))
return true
} }
export const deleteBlockSetCursorPreviousBlock = (editor) => ({state, dispatch}) => { export const deleteBlockSetCursorPreviousBlock = (editor) => ({state, dispatch}) => {
@ -380,4 +381,5 @@ export const deleteBlockSetCursorPreviousBlock = (editor) => ({state, dispatch})
selection: EditorSelection.cursor(newSelection), selection: EditorSelection.cursor(newSelection),
annotations: [heynoteEvent.of(DELETE_BLOCK)], annotations: [heynoteEvent.of(DELETE_BLOCK)],
})) }))
return true
} }

View File

@ -1,5 +1,5 @@
import { EditorSelection } from "@codemirror/state" import { EditorSelection } from "@codemirror/state"
import { getActiveNoteBlock } from "./block" import { getNoteBlockFromPos } from "./block"
function updateSel(sel, by) { function updateSel(sel, by) {
return EditorSelection.create(sel.ranges.map(by), sel.mainIndex); return EditorSelection.create(sel.ranges.map(by), sel.mainIndex);
@ -28,10 +28,10 @@ export const deleteLine = (view) => {
const { state } = view const { state } = view
const block = getActiveNoteBlock(view.state)
const selectedLines = selectedLineBlocks(state) const selectedLines = selectedLineBlocks(state)
const changes = state.changes(selectedLines.map(({ from, to }) => { const changes = state.changes(selectedLines.map(({ from, to }) => {
const block = getNoteBlockFromPos(state, from)
if(from !== block.content.from || to !== block.content.to) { if(from !== block.content.from || to !== block.content.to) {
if (from > 0) from-- if (from > 0) from--
else if (to < state.doc.length) to++ else if (to < state.doc.length) to++

View File

@ -4,7 +4,7 @@ import { RangeSetBuilder } from "@codemirror/state"
import { WidgetType } from "@codemirror/view" import { WidgetType } from "@codemirror/view"
import { getNoteBlockFromPos } from "./block" import { getNoteBlockFromPos } from "./block"
import { CURRENCIES_LOADED } from "../annotation" import { transactionsHasAnnotation, CURRENCIES_LOADED } from "../annotation"
class MathResult extends WidgetType { class MathResult extends WidgetType {
@ -107,12 +107,6 @@ function mathDeco(view) {
return builder.finish() return builder.finish()
} }
// This function checks if any of the transactions has the given annotation
const transactionsHasAnnotation = (transactions, annotation) => {
return transactions.some(tr => tr.annotations.some(a => a.value === annotation))
}
export const mathBlock = ViewPlugin.fromClass(class { export const mathBlock = ViewPlugin.fromClass(class {
decorations decorations

View File

@ -0,0 +1,30 @@
import { EditorSelection, findClusterBreak} from "@codemirror/state";
import { getNoteBlockFromPos } from "./block"
/**
Flip the characters before and after the cursor(s).
*/
export const transposeChars = ({ state, dispatch }) => {
if (state.readOnly)
return false;
let changes = state.changeByRange(range => {
// prevent transposing characters if we're at the start or end of a block, since it'll break the block syntax
const block = getNoteBlockFromPos(state, range.from)
if (range.from === block.content.from || range.from === block.content.to) {
return { range }
}
if (!range.empty || range.from == 0 || range.from == state.doc.length)
return { range };
let pos = range.from, line = state.doc.lineAt(pos);
let from = pos == line.from ? pos - 1 : findClusterBreak(line.text, pos - line.from, false) + line.from;
let to = pos == line.to ? pos + 1 : findClusterBreak(line.text, pos - line.from, true) + line.from;
return { changes: { from, to, insert: state.doc.slice(pos, to).append(state.doc.slice(from, pos)) },
range: EditorSelection.cursor(to) };
});
if (changes.changes.empty)
return false;
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "move.character" }));
return true;
};

View File

@ -0,0 +1,10 @@
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'
import { Prec } from "@codemirror/state"
import { keymap } from "@codemirror/view"
export function getCloseBracketsExtensions() {
return [
closeBrackets(),
Prec.highest(keymap.of(closeBracketsKeymap)),
]
}

167
src/editor/commands.js Normal file
View File

@ -0,0 +1,167 @@
import * as codeMirrorCommands from "@codemirror/commands"
import {
undo, redo,
indentMore, indentLess,
deleteCharBackward, deleteCharForward,
deleteGroupBackward, deleteGroupForward,
deleteLineBoundaryBackward, deleteLineBoundaryForward,
deleteToLineEnd, deleteToLineStart,
simplifySelection,
splitLine,
insertNewlineAndIndent,
} from "@codemirror/commands"
import { foldCode, unfoldCode } from "@codemirror/language"
import { selectNextOccurrence } from "@codemirror/search"
import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown"
import {
addNewBlockAfterCurrent, addNewBlockBeforeCurrent, addNewBlockAfterLast, addNewBlockBeforeFirst, insertNewBlockAtCursor,
gotoPreviousBlock, gotoNextBlock, selectNextBlock, selectPreviousBlock,
gotoPreviousParagraph, gotoNextParagraph, selectNextParagraph, selectPreviousParagraph,
moveLineUp, moveLineDown,
selectAll,
deleteBlock, deleteBlockSetCursorPreviousBlock,
newCursorBelow, newCursorAbove,
} from "./block/commands.js"
import { deleteLine } from "./block/delete-line.js"
import { formatBlockContent } from "./block/format-code.js"
import { transposeChars } from "./block/transpose-chars.js"
import { cutCommand, copyCommand, pasteCommand } from "./copy-paste.js"
import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./mark-mode.js"
const cursorPreviousBlock = markModeMoveCommand(gotoPreviousBlock, selectPreviousBlock)
const cursorNextBlock = markModeMoveCommand(gotoNextBlock, selectNextBlock)
const cursorPreviousParagraph = markModeMoveCommand(gotoPreviousParagraph, selectPreviousParagraph)
const cursorNextParagraph = markModeMoveCommand(gotoNextParagraph, selectNextParagraph)
const openLanguageSelector = (editor) => () => {
editor.openLanguageSelector()
return true
}
const openBufferSelector = (editor) => () => {
editor.openBufferSelector()
return true
}
const openCommandPalette = (editor) => () => {
editor.openCommandPalette()
return true
}
const openMoveToBuffer = (editor) => () => {
editor.openMoveToBufferSelector()
return true
}
const openCreateNewBuffer = (editor) => () => {
editor.openCreateBuffer("new")
return true
}
const nothing = (view) => {
return true
}
const cmd = (f, category, description) => ({
run: f,
name: f.name,
description: description,
category: category,
})
const cmdLessContext = (f, category, description) => ({
run: (editor) => f,
name: f.name,
description: description,
category: category,
})
const HEYNOTE_COMMANDS = {
addNewBlockAfterCurrent: cmd(addNewBlockAfterCurrent, "Block", "Add new block after current block"),
addNewBlockBeforeCurrent: cmd(addNewBlockBeforeCurrent, "Block", "Add new block before current block"),
addNewBlockAfterLast: cmd(addNewBlockAfterLast, "Block", "Add new block after last block"),
addNewBlockBeforeFirst: cmd(addNewBlockBeforeFirst, "Block", "Add new block before first block"),
insertNewBlockAtCursor: cmd(insertNewBlockAtCursor, "Block", "Insert new block at cursor"),
deleteBlock: cmd(deleteBlock, "Block", "Delete block"),
deleteBlockSetCursorPreviousBlock: cmd(deleteBlockSetCursorPreviousBlock, "Block", "Delete block and set cursor to previous block"),
cursorPreviousBlock: cmd(cursorPreviousBlock, "Cursor", "Move cursor to previous block"),
cursorNextBlock: cmd(cursorNextBlock, "Cursor", "Move cursor to next block"),
cursorPreviousParagraph: cmd(cursorPreviousParagraph, "Cursor", "Move cursor to previous paragraph"),
cursorNextParagraph: cmd(cursorNextParagraph, "Cursor", "Move cursor to next paragraph"),
toggleSelectionMarkMode: cmd(toggleSelectionMarkMode, "Cursor", "Toggle selection mark mode"),
selectionMarkModeCancel: cmd(selectionMarkModeCancel, "Cursor", "Cancel selection mark mode"),
openLanguageSelector: cmd(openLanguageSelector, "Block", "Select block language"),
openBufferSelector: cmd(openBufferSelector, "Buffer", "Buffer selector"),
openCommandPalette: cmd(openCommandPalette, "Editor", "Open command palette"),
openMoveToBuffer: cmd(openMoveToBuffer, "Block", "Move block to another buffer"),
openCreateNewBuffer: cmd(openCreateNewBuffer, "Buffer", "Create new buffer"),
cut: cmd(cutCommand, "Clipboard", "Cut selection"),
copy: cmd(copyCommand, "Clipboard", "Copy selection"),
// commands without editor context
paste: cmdLessContext(pasteCommand, "Clipboard", "Paste from clipboard"),
selectAll: cmdLessContext(selectAll, "Selection", "Select all"),
moveLineUp: cmdLessContext(moveLineUp, "Edit", "Move line up"),
moveLineDown: cmdLessContext(moveLineDown, "Edit", "Move line down"),
deleteLine: cmdLessContext(deleteLine, "Edit", "Delete line"),
formatBlockContent: cmdLessContext(formatBlockContent, "Block", "Format block content"),
newCursorAbove: cmdLessContext(newCursorAbove, "Cursor", "Add cursor above"),
newCursorBelow: cmdLessContext(newCursorBelow, "Cursor", "Add cursor below"),
selectPreviousParagraph: cmdLessContext(selectPreviousParagraph, "Selection", "Select to previous paragraph"),
selectNextParagraph: cmdLessContext(selectNextParagraph, "Selection", "Select to next paragraph"),
selectPreviousBlock: cmdLessContext(selectPreviousBlock, "Selection", "Select to previous block"),
selectNextBlock: cmdLessContext(selectNextBlock, "Selection", "Select to next block"),
nothing: cmdLessContext(nothing, "Misc", "Do nothing"),
// directly from CodeMirror
undo: cmdLessContext(undo, "Edit", "Undo"),
redo: cmdLessContext(redo, "Edit", "Redo"),
indentMore: cmdLessContext(indentMore, "Edit", "Indent more"),
indentLess: cmdLessContext(indentLess, "Edit", "Indent less"),
foldCode: cmdLessContext(foldCode, "Edit", "Fold code"),
unfoldCode: cmdLessContext(unfoldCode, "Edit", "Unfold code"),
selectNextOccurrence: cmdLessContext(selectNextOccurrence, "Cursor", "Select next occurrence"),
deleteCharBackward: cmdLessContext(deleteCharBackward, "Edit", "Delete character backward"),
deleteCharForward: cmdLessContext(deleteCharForward, "Edit", "Delete character forward"),
deleteGroupBackward: cmdLessContext(deleteGroupBackward, "Edit", "Delete group backward"),
deleteGroupForward: cmdLessContext(deleteGroupForward, "Edit", "Delete group forward"),
deleteLineBoundaryBackward: cmdLessContext(deleteLineBoundaryBackward, "Edit", "Delete from start of wrapped line"),
deleteLineBoundaryForward: cmdLessContext(deleteLineBoundaryForward, "Edit", "Delete to end of wrapped line"),
deleteToLineEnd: cmdLessContext(deleteToLineEnd, "Edit", "Delete to end of line"),
deleteToLineStart: cmdLessContext(deleteToLineStart, "Edit", "Delete from start of line"),
simplifySelection: cmdLessContext(simplifySelection, "Cursor", "Simplify selection"),
splitLine: cmdLessContext(splitLine, "Edit", "Split line"),
transposeChars: cmdLessContext(transposeChars, "Edit", "Transpose characters"),
insertNewlineAndIndent: cmdLessContext(insertNewlineAndIndent, "Edit", "Insert newline and indent"),
insertNewlineContinueMarkup: cmdLessContext(insertNewlineContinueMarkup, "Markdown", "Insert newline and continue todo lists/block quotes"),
}
// selection mark-mode:ify all cursor/select commands from CodeMirror
for (let commandSuffix of [
"CharLeft", "CharRight",
"CharBackward", "CharForward",
"LineUp", "LineDown",
"LineStart", "LineEnd",
"GroupLeft", "GroupRight",
"GroupForward", "GroupBackward",
"PageUp", "PageDown",
"SyntaxLeft", "SyntaxRight",
"SubwordBackward", "SubwordForward",
"LineBoundaryBackward", "LineBoundaryForward",
]) {
HEYNOTE_COMMANDS[`cursor${commandSuffix}`] = {
run: markModeMoveCommand(codeMirrorCommands[`cursor${commandSuffix}`], codeMirrorCommands[`select${commandSuffix}`]),
name: `cursor${commandSuffix}`,
description: `cursor${commandSuffix}`,
category: "Cursor",
}
HEYNOTE_COMMANDS[`select${commandSuffix}`] = {
run: (editor) => codeMirrorCommands[`select${commandSuffix}`],
name: `select${commandSuffix}`,
description: `select${commandSuffix}`,
category: "Cursor",
}
}
export { HEYNOTE_COMMANDS }

View File

@ -2,7 +2,6 @@ import { EditorState, EditorSelection } from "@codemirror/state"
import { EditorView } from "@codemirror/view" import { EditorView } from "@codemirror/view"
import { LANGUAGES } from './languages.js'; import { LANGUAGES } from './languages.js';
import { setEmacsMarkMode } from "./emacs.js"
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|") const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
@ -61,7 +60,7 @@ export const heynoteCopyCut = (editor) => {
} }
// if we're in Emacs mode, we want to exit mark mode in case we're in it // if we're in Emacs mode, we want to exit mark mode in case we're in it
setEmacsMarkMode(false) editor.selectionMarkMode = false
// if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text // if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text
if (editor.deselectOnCopy && event.type == "copy") { if (editor.deselectOnCopy && event.type == "copy") {
@ -95,7 +94,7 @@ const copyCut = (view, cut, editor) => {
} }
// if we're in Emacs mode, we want to exit mark mode in case we're in it // if we're in Emacs mode, we want to exit mark mode in case we're in it
setEmacsMarkMode(false) editor.selectionMarkMode = false
// if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text // if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text
if (editor.deselectOnCopy && !cut) { if (editor.deselectOnCopy && !cut) {
@ -107,6 +106,7 @@ const copyCut = (view, cut, editor) => {
selection: newSelection, selection: newSelection,
})) }))
} }
return true
} }

View File

@ -1,7 +1,7 @@
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction } from "@codemirror/state" import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction, Prec } from "@codemirror/state"
import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view" import { EditorView, keymap as cmKeymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language" import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language"
import { markdown } from "@codemirror/lang-markdown" import { markdown, markdownKeymap } from "@codemirror/lang-markdown"
import { closeBrackets } from "@codemirror/autocomplete"; import { closeBrackets } from "@codemirror/autocomplete";
import { undo, redo } from "@codemirror/commands" import { undo, redo } from "@codemirror/commands"
@ -11,32 +11,24 @@ import { heynoteBase } from "./theme/base.js"
import { getFontTheme } from "./theme/font-theme.js"; import { getFontTheme } from "./theme/font-theme.js";
import { customSetup } from "./setup.js" import { customSetup } from "./setup.js"
import { heynoteLang } from "./lang-heynote/heynote.js" import { heynoteLang } from "./lang-heynote/heynote.js"
import { getCloseBracketsExtensions } from "./close-brackets.js"
import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, triggerCursorChange } from "./block/block.js" import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, triggerCursorChange } from "./block/block.js"
import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK } from "./annotation.js"; import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK, SET_FONT } from "./annotation.js";
import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.js" import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.js"
import { formatBlockContent } from "./block/format-code.js" import { formatBlockContent } from "./block/format-code.js"
import { heynoteKeymap } from "./keymap.js" import { getKeymapExtensions } from "./keymap.js"
import { emacsKeymap } from "./emacs.js"
import { heynoteCopyCut } from "./copy-paste" import { heynoteCopyCut } from "./copy-paste"
import { languageDetection } from "./language-detection/autodetect.js" import { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js" import { autoSaveContent } from "./save.js"
import { todoCheckboxPlugin} from "./todo-checkbox.ts" import { todoCheckboxPlugin} from "./todo-checkbox.ts"
import { links } from "./links.js" import { links } from "./links.js"
import { HEYNOTE_COMMANDS } from "./commands.js";
import { NoteFormat } from "../common/note-format.js" import { NoteFormat } from "../common/note-format.js"
import { AUTO_SAVE_INTERVAL } from "../common/constants.js" import { AUTO_SAVE_INTERVAL } from "../common/constants.js"
import { useHeynoteStore } from "../stores/heynote-store.js"; import { useHeynoteStore } from "../stores/heynote-store.js";
import { useErrorStore } from "../stores/error-store.js"; import { useErrorStore } from "../stores/error-store.js";
function getKeymapExtensions(editor, keymap) {
if (keymap === "emacs") {
return emacsKeymap(editor)
} else {
return heynoteKeymap(editor)
}
}
export class HeynoteEditor { export class HeynoteEditor {
constructor({ constructor({
element, element,
@ -54,6 +46,7 @@ export class HeynoteEditor {
tabSize=4, tabSize=4,
defaultBlockToken, defaultBlockToken,
defaultBlockAutoDetect, defaultBlockAutoDetect,
keyBindings,
}) { }) {
this.element = element this.element = element
this.path = path this.path = path
@ -73,20 +66,20 @@ export class HeynoteEditor {
this.notesStore = useHeynoteStore() this.notesStore = useHeynoteStore()
this.errorStore = useErrorStore() this.errorStore = useErrorStore()
this.name = "" this.name = ""
this.selectionMarkMode = false
const state = EditorState.create({ const state = EditorState.create({
doc: "", doc: "",
extensions: [ extensions: [
this.keymapCompartment.of(getKeymapExtensions(this, keymap)), this.keymapCompartment.of(getKeymapExtensions(this, keymap, keyBindings)),
heynoteCopyCut(this), heynoteCopyCut(this),
//minimalSetup, //minimalSetup,
this.lineNumberCompartment.of(showLineNumberGutter ? [lineNumbers(), blockLineNumbers] : []), this.lineNumberCompartment.of(showLineNumberGutter ? [lineNumbers(), blockLineNumbers] : []),
customSetup, customSetup,
this.foldGutterCompartment.of(showFoldGutter ? [foldGutter()] : []), this.foldGutterCompartment.of(showFoldGutter ? [foldGutter()] : []),
this.closeBracketsCompartment.of(bracketClosing ? [getCloseBracketsExtensions()] : []),
this.closeBracketsCompartment.of(bracketClosing ? [closeBrackets()] : []),
this.readOnlyCompartment.of([]), this.readOnlyCompartment.of([]),
@ -111,8 +104,12 @@ export class HeynoteEditor {
autoSaveContent(this, AUTO_SAVE_INTERVAL), autoSaveContent(this, AUTO_SAVE_INTERVAL),
// Markdown extensions, we need to add markdownKeymap manually with the highest precedence
// so that it takes precedence over the default keymap
todoCheckboxPlugin, todoCheckboxPlugin,
markdown(), markdown({addKeymap: false}),
Prec.highest(cmKeymap.of(markdownKeymap)),
links, links,
], ],
}) })
@ -137,6 +134,11 @@ export class HeynoteEditor {
if (focus) { if (focus) {
this.view.focus() this.view.focus()
} }
// trigger setFont once the fonts has loaded
document.fonts.ready.then(() => {
this.setFont(fontFamily, fontSize)
})
} }
async save() { async save() {
@ -260,6 +262,7 @@ export class HeynoteEditor {
setFont(fontFamily, fontSize) { setFont(fontFamily, fontSize) {
this.view.dispatch({ this.view.dispatch({
effects: this.fontTheme.reconfigure(getFontTheme(fontFamily, fontSize)), effects: this.fontTheme.reconfigure(getFontTheme(fontFamily, fontSize)),
annotations: [heynoteEvent.of(SET_FONT), Transaction.addToHistory.of(false)],
}) })
} }
@ -269,11 +272,11 @@ export class HeynoteEditor {
}) })
} }
setKeymap(keymap, emacsMetaKey) { setKeymap(keymap, emacsMetaKey, keyBindings) {
this.deselectOnCopy = keymap === "emacs" this.deselectOnCopy = keymap === "emacs"
this.emacsMetaKey = emacsMetaKey this.emacsMetaKey = emacsMetaKey
this.view.dispatch({ this.view.dispatch({
effects: this.keymapCompartment.reconfigure(getKeymapExtensions(this, keymap)), effects: this.keymapCompartment.reconfigure(getKeymapExtensions(this, keymap, keyBindings)),
}) })
} }
@ -285,6 +288,10 @@ export class HeynoteEditor {
this.notesStore.openBufferSelector() this.notesStore.openBufferSelector()
} }
openCommandPalette() {
this.notesStore.openCommandPalette()
}
openCreateBuffer(createMode) { openCreateBuffer(createMode) {
this.notesStore.openCreateBuffer(createMode) this.notesStore.openCreateBuffer(createMode)
} }
@ -367,7 +374,7 @@ export class HeynoteEditor {
setBracketClosing(value) { setBracketClosing(value) {
this.view.dispatch({ this.view.dispatch({
effects: this.closeBracketsCompartment.reconfigure(value ? [closeBrackets()] : []), effects: this.closeBracketsCompartment.reconfigure(value ? [getCloseBracketsExtensions()] : []),
}) })
} }
@ -425,6 +432,15 @@ export class HeynoteEditor {
effects: this.indentUnitCompartment.reconfigure(indentUnit.of(" ".repeat(tabSize))) effects: this.indentUnitCompartment.reconfigure(indentUnit.of(" ".repeat(tabSize)))
}) })
} }
executeCommand(command) {
const cmd = HEYNOTE_COMMANDS[command]
if (!cmd) {
console.error(`Command not found: ${command}`)
return
}
cmd.run(this)(this.view)
}
} }

View File

@ -1,119 +0,0 @@
import { Direction} from "@codemirror/view"
import { EditorSelection, EditorState, Prec } from "@codemirror/state"
import {
undo, redo,
cursorGroupLeft, cursorGroupRight, selectGroupLeft, selectGroupRight,
simplifySelection,
deleteCharForward, deleteCharBackward, deleteToLineEnd,
splitLine,
transposeChars,
cursorPageDown,
cursorCharLeft, selectCharLeft,
cursorCharRight, selectCharRight,
cursorLineUp, selectLineUp,
cursorLineDown, selectLineDown,
cursorLineStart, selectLineStart,
cursorLineEnd, selectLineEnd,
} from "@codemirror/commands"
import { heynoteKeymap, keymapFromSpec } from "./keymap.js"
import {
gotoPreviousBlock, gotoNextBlock,
selectNextBlock, selectPreviousBlock,
gotoPreviousParagraph, gotoNextParagraph,
selectNextParagraph, selectPreviousParagraph,
selectAll,
} from "./block/commands.js"
import { pasteCommand, copyCommand, cutCommand } from "./copy-paste.js"
// if set to true, all keybindings for moving around is changed to their corresponding select commands
let emacsMarkMode = false
export function setEmacsMarkMode(value) {
emacsMarkMode = value
}
/**
* Return a command that will conditionally execute either the default command or the mark mode command
*
* @param defaultCmd Default command to execute
* @param {*} markModeCmd Command to execute if mark mode is active
*/
function emacsMoveCommand(defaultCmd, markModeCmd) {
return (view) => emacsMarkMode ? markModeCmd(view) : defaultCmd(view)
}
/**
* C-g command that exits mark mode and simplifies selection
*/
function emacsCancel(view) {
simplifySelection(view)
setEmacsMarkMode(false)
}
/**
* Exit mark mode before executing selectAll command
*/
function emacsSelectAll(view) {
setEmacsMarkMode(false)
return selectAll(view)
}
function emacsMetaKeyCommand(key, editor, command) {
const handler = (view, event) => {
if (editor.emacsMetaKey === "meta" && event.metaKey || editor.emacsMetaKey === "alt" && event.altKey) {
event.preventDefault()
return command(view)
} else {
return false
}
}
return [
{key, run:handler, preventDefault:false},
{key:key.replace("Meta", "Alt"), run:handler, preventDefault:false},
]
}
export function emacsKeymap(editor) {
return [
heynoteKeymap(editor),
Prec.highest(keymapFromSpec([
["Ctrl-Shift--", undo],
["Ctrl-.", redo],
["Ctrl-g", emacsCancel],
["ArrowLeft", emacsMoveCommand(cursorCharLeft, selectCharLeft)],
["ArrowRight", emacsMoveCommand(cursorCharRight, selectCharRight)],
["ArrowUp", emacsMoveCommand(cursorLineUp, selectLineUp)],
["ArrowDown", emacsMoveCommand(cursorLineDown, selectLineDown)],
{key: "Ctrl-ArrowLeft", run: emacsMoveCommand(cursorGroupLeft, selectGroupLeft), shift: selectGroupLeft},
{key: "Ctrl-ArrowRight", run: emacsMoveCommand(cursorGroupRight, selectGroupRight), shift: selectGroupRight},
["Ctrl-d", deleteCharForward],
["Ctrl-h", deleteCharBackward],
["Ctrl-k", deleteToLineEnd],
["Ctrl-o", splitLine],
["Ctrl-t", transposeChars],
["Ctrl-v", cursorPageDown],
["Ctrl-y", pasteCommand],
["Ctrl-w", cutCommand(editor)],
...emacsMetaKeyCommand("Meta-w", editor, copyCommand(editor)),
{ key: "Ctrl-b", run: emacsMoveCommand(cursorCharLeft, selectCharLeft), shift: selectCharLeft },
{ key: "Ctrl-f", run: emacsMoveCommand(cursorCharRight, selectCharRight), shift: selectCharRight },
{ key: "Ctrl-a", run: emacsMoveCommand(cursorLineStart, selectLineStart), shift: selectLineStart },
{ key: "Ctrl-e", run: emacsMoveCommand(cursorLineEnd, selectLineEnd), shift: selectLineEnd },
])),
Prec.highest(keymapFromSpec([
["Ctrl-Space", (view) => { emacsMarkMode = !emacsMarkMode }],
["Mod-a", emacsSelectAll],
{key:"Mod-ArrowUp", run:emacsMoveCommand(gotoPreviousBlock, selectPreviousBlock), shift:selectPreviousBlock},
{key:"Mod-ArrowDown", run:emacsMoveCommand(gotoNextBlock, selectNextBlock), shift:selectNextBlock},
{key:"Ctrl-ArrowUp", run:emacsMoveCommand(gotoPreviousParagraph, selectPreviousParagraph), shift:selectPreviousParagraph},
{key:"Ctrl-ArrowDown", run:emacsMoveCommand(gotoNextParagraph, selectNextParagraph), shift:selectNextParagraph},
])),
]
}

View File

@ -1,75 +1,165 @@
import { keymap } from "@codemirror/view" import { keymap } from "@codemirror/view"
//import { EditorSelection, EditorState } from "@codemirror/state" import { Prec } from "@codemirror/state"
import {
indentLess, indentMore, redo,
} from "@codemirror/commands"
import { import { HEYNOTE_COMMANDS } from "./commands.js"
insertNewBlockAtCursor,
addNewBlockBeforeCurrent, addNewBlockAfterCurrent,
addNewBlockBeforeFirst, addNewBlockAfterLast,
moveLineUp, moveLineDown,
selectAll,
gotoPreviousBlock, gotoNextBlock,
selectNextBlock, selectPreviousBlock,
gotoPreviousParagraph, gotoNextParagraph,
selectNextParagraph, selectPreviousParagraph,
newCursorBelow, newCursorAbove,
deleteBlock,
} from "./block/commands.js"
import { pasteCommand, copyCommand, cutCommand } from "./copy-paste.js"
import { formatBlockContent } from "./block/format-code.js" const cmd = (key, command) => ({key, command})
import { deleteLine } from "./block/delete-line.js" const cmdShift = (key, command, shiftCommand) => {
return [
cmd(key, command),
cmd(`Shift-${key}`, shiftCommand),
]
}
const isMac = window.heynote.platform.isMac
const isLinux = window.heynote.platform.isLinux
const isWindows = window.heynote.platform.isWindows
export const DEFAULT_KEYMAP = [
cmd("Enter", "insertNewlineAndIndent"),
cmd("Mod-a", "selectAll"),
cmd("Mod-Enter", "addNewBlockAfterCurrent"),
cmd("Mod-Shift-Enter", "addNewBlockAfterLast"),
cmd("Alt-Enter", "addNewBlockBeforeCurrent"),
cmd("Alt-Shift-Enter", "addNewBlockBeforeFirst"),
cmd("Mod-Alt-Enter", "insertNewBlockAtCursor"),
...cmdShift("ArrowLeft", "cursorCharLeft", "selectCharLeft"),
...cmdShift("ArrowRight", "cursorCharRight", "selectCharRight"),
...cmdShift("ArrowUp", "cursorLineUp", "selectLineUp"),
...cmdShift("ArrowDown", "cursorLineDown", "selectLineDown"),
...cmdShift("Ctrl-ArrowLeft", "cursorGroupLeft", "selectGroupLeft"),
...cmdShift("Ctrl-ArrowRight", "cursorGroupRight", "selectGroupRight"),
...cmdShift("Alt-ArrowLeft", "cursorGroupLeft", "selectGroupLeft"),
...cmdShift("Alt-ArrowRight", "cursorGroupRight", "selectGroupRight"),
...cmdShift("Mod-ArrowUp", "cursorPreviousBlock", "selectPreviousBlock"),
...cmdShift("Mod-ArrowDown", "cursorNextBlock", "selectNextBlock"),
...cmdShift("Ctrl-ArrowUp", "cursorPreviousParagraph", "selectPreviousParagraph"),
...cmdShift("Ctrl-ArrowDown", "cursorNextParagraph", "selectNextParagraph"),
...cmdShift("PageUp", "cursorPageUp", "selectPageUp"),
...cmdShift("PageDown", "cursorPageDown", "selectPageDown"),
...cmdShift("Home", "cursorLineBoundaryBackward", "selectLineBoundaryBackward"),
...cmdShift("End", "cursorLineBoundaryForward", "selectLineBoundaryForward"),
cmd("Backspace", "deleteCharBackward"),
cmd("Delete", "deleteCharForward"),
cmd("Escape", "simplifySelection"),
cmd("Ctrl-Backspace", "deleteGroupBackward"),
cmd("Ctrl-Delete", "deleteGroupForward"),
...(isMac ? [
cmd("Alt-Backspace", "deleteGroupBackward"),
cmd("Alt-Delete", "deleteGroupForward"),
cmd("Mod-Backspace", "deleteLineBoundaryBackward"),
cmd("Mod-Delete", "deleteLineBoundaryForward"),
] : []),
cmd("Alt-ArrowUp", "moveLineUp"),
cmd("Alt-ArrowDown", "moveLineDown"),
cmd("Mod-Shift-k", "deleteLine"),
cmd("Mod-Alt-ArrowDown", "newCursorBelow"),
cmd("Mod-Alt-ArrowUp", "newCursorAbove"),
cmd("Mod-Shift-d", "deleteBlock"),
cmd("Mod-d", "selectNextOccurrence"),
cmd(isMac ? "Cmd-Alt-[" : "Ctrl-Shift-[", "foldCode"),
cmd(isMac ? "Cmd-Alt-]" : "Ctrl-Shift-]", "unfoldCode"),
cmd("Mod-c", "copy"),
cmd("Mod-v", "paste"),
cmd("Mod-x", "cut"),
cmd("Mod-z", "undo"),
cmd("Mod-Shift-z", "redo"),
...(isWindows || isLinux ? [
cmd("Mod-y", "redo"),
] : []),
cmd("Tab", "indentMore"),
cmd("Shift-Tab", "indentLess"),
//cmd("Alt-ArrowLeft", "cursorSubwordBackward"),
//cmd("Alt-ArrowRight", "cursorSubwordForward"),
cmd("Mod-l", "openLanguageSelector"),
cmd("Mod-p", "openBufferSelector"),
cmd("Mod-Shift-p", "openCommandPalette"),
cmd("Mod-s", "openMoveToBuffer"),
cmd("Mod-n", "openCreateNewBuffer"),
cmd("Alt-Shift-f", "formatBlockContent"),
// search
//cmd("Mod-f", "openSearchPanel"),
//cmd("F3", "findNext"),
//cmd("Mod-g", "findNext"),
//cmd("Shift-F3", "findPrevious"),
//cmd("Shift-Mod-g", "findPrevious"),
//cmd("Mod-Alt-g", "gotoLine"),
//cmd("Mod-d", "selectNextOccurrence"),
/*
- Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel)
- F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext)
- Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious)
- Mod-Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine)
- Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence)
*/
]
export const EMACS_KEYMAP = [
cmd("Ctrl-w", "cut"),
cmd("Ctrl-y", "paste"),
cmd("EmacsMeta-w", "copy"),
cmd("Ctrl-Space", "toggleSelectionMarkMode"),
cmd("Ctrl-g", "selectionMarkModeCancel"),
cmd("Escape", "selectionMarkModeCancel"),
cmd("Ctrl-o", "splitLine"),
cmd("Ctrl-d", "deleteCharForward"),
cmd("Ctrl-h", "deleteCharBackward"),
cmd("Ctrl-k", "deleteToLineEnd"),
cmd("Ctrl-t", "transposeChars"),
cmd("Ctrl-Shift--", "undo"),
cmd("Ctrl-.", "redo"),
...cmdShift("Ctrl-v", "cursorPageDown", "selectPageDown"),
...cmdShift("Ctrl-b", "cursorCharLeft", "selectCharLeft"),
...cmdShift("Ctrl-f", "cursorCharRight", "selectCharRight"),
...cmdShift("Ctrl-a", "cursorLineStart", "selectLineStart"),
...cmdShift("Ctrl-e", "cursorLineEnd", "selectLineEnd"),
]
export function keymapFromSpec(specs) { function keymapFromSpec(specs, editor) {
return keymap.of(specs.map((spec) => { return keymap.of(specs.map((spec) => {
if (spec.run) { let key = spec.key
if ("preventDefault" in spec) { if (key.indexOf("EmacsMeta") != -1) {
return spec key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta")
} else { }
return {...spec, preventDefault: true} return {
} key: key,
} else { //preventDefault: true,
const [key, run] = spec preventDefault: false,
return { run: (view) => {
key, //console.log("run()", spec.key, spec.command)
run, const command = HEYNOTE_COMMANDS[spec.command]
preventDefault: true, if (!command) {
} console.error(`Command not found: ${spec.command} (${spec.key})`)
return false
}
return command.run(editor)(view)
},
} }
})) }))
} }
export function heynoteKeymap(editor) {
return keymapFromSpec([ export function heynoteKeymap(editor, keymap, userKeymap) {
["Mod-c", copyCommand(editor)], return [
["Mod-v", pasteCommand], keymapFromSpec([
["Mod-x", cutCommand(editor)], ...userKeymap,
["Tab", indentMore], ...keymap,
["Shift-Tab", indentLess], ], editor),
["Alt-Shift-Enter", addNewBlockBeforeFirst(editor)], ]
["Mod-Shift-Enter", addNewBlockAfterLast(editor)], }
["Alt-Enter", addNewBlockBeforeCurrent(editor)],
["Mod-Enter", addNewBlockAfterCurrent(editor)], export function getKeymapExtensions(editor, keymap, keyBindings) {
["Mod-Alt-Enter", insertNewBlockAtCursor(editor)], return heynoteKeymap(
["Mod-a", selectAll], editor,
["Alt-ArrowUp", moveLineUp], keymap === "emacs" ? EMACS_KEYMAP.concat(DEFAULT_KEYMAP) : DEFAULT_KEYMAP,
["Alt-ArrowDown", moveLineDown], keyBindings || [],
["Mod-l", () => editor.openLanguageSelector()], )
["Mod-p", () => editor.openBufferSelector()],
["Mod-s", () => editor.openMoveToBufferSelector()],
["Mod-n", () => editor.openCreateBuffer("new")],
["Mod-Shift-d", deleteBlock(editor)],
["Alt-Shift-f", formatBlockContent],
["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},
{key:"Ctrl-ArrowDown", run:gotoNextParagraph, shift:selectNextParagraph},
])
} }

39
src/editor/mark-mode.js Normal file
View File

@ -0,0 +1,39 @@
import {
simplifySelection,
} from "@codemirror/commands"
/**
* Takes a command that moves the cursor and a command that marks the selection, and returns a new command that
* will run the mark command if we're in Emacs mark mode, or the move command otherwise.
*/
export function markModeMoveCommand(defaultCmd, markModeCmd) {
return (editor) => {
if (editor.selectionMarkMode) {
return (view) => {
markModeCmd(view)
// we need to return true here instead of returning what the default command returns, since the default
// codemirror select commands will return false if the selection doesn't change, which in turn will
// make the default *move* command run which will kill the selection if we're in mark mode
return true
}
} else {
return (view) => defaultCmd(view)
}
}
}
export function toggleSelectionMarkMode(editor) {
return (view) => {
editor.selectionMarkMode = !editor.selectionMarkMode
return true
}
}
export function selectionMarkModeCancel(editor) {
return (view) => {
simplifySelection(view)
editor.selectionMarkMode = false
return true
}
}

View File

@ -68,11 +68,12 @@ const customSetup = /*@__PURE__*/(() => [
EditorView.lineWrapping, EditorView.lineWrapping,
scrollPastEnd(), scrollPastEnd(),
keymap.of([ keymap.of([
...closeBracketsKeymap, //...closeBracketsKeymap,
...defaultKeymap, //...defaultKeymap,
...searchKeymap, ...searchKeymap,
...historyKeymap, //...historyKeymap,
...foldKeymap, //...foldKeymap,
//...completionKeymap, //...completionKeymap,
//...lintKeymap //...lintKeymap
]) ])

View File

@ -1,11 +1,39 @@
import { EditorView } from "@codemirror/view" import { EditorView } from "@codemirror/view"
import { Facet } from "@codemirror/state"
/**
* Check if the given font family is monospace by drawing test characters on a canvas
*/
function isMonospace(fontFamily) {
const testCharacters = ['i', 'W', 'm', ' ']
const testSize = '72px'
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = `${testSize} ${fontFamily}`
const widths = testCharacters.map(char => context.measureText(char).width)
return widths.every(width => width === widths[0])
}
export const isMonospaceFont = Facet.define({
combine: values => values.length ? values[0] : true,
})
export function getFontTheme(fontFamily, fontSize) { export function getFontTheme(fontFamily, fontSize) {
fontSize = fontSize || window.heynote.defaultFontSize fontSize = fontSize || window.heynote.defaultFontSize
return EditorView.theme({ const computedFontFamily = fontFamily || window.heynote.defaultFontFamily
'.cm-scroller': { return [
fontFamily: fontFamily || window.heynote.defaultFontFamily, EditorView.theme({
fontSize: (fontSize) + "px", '.cm-scroller': {
}, fontFamily: computedFontFamily,
}) fontSize: (fontSize) + "px",
},
}),
// in order to avoid a short flicker when the program is loaded with the default font (Hack),
// we hardcode Hack to be monospace
isMonospaceFont.of(computedFontFamily === "Hack" ? true : isMonospace(computedFontFamily)),
]
} }

View File

@ -3,25 +3,45 @@ import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
import { WidgetType } from "@codemirror/view" import { WidgetType } from "@codemirror/view"
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view" import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
import { isMonospaceFont } from "./theme/font-theme"
import { transactionsHasAnnotation, SET_FONT } from "./annotation"
class CheckboxWidget extends WidgetType { class CheckboxWidget extends WidgetType {
constructor(readonly checked: boolean) { super() } constructor(readonly checked: boolean, readonly monospace: boolean) { super() }
eq(other: CheckboxWidget) { return other.checked == this.checked } eq(other: CheckboxWidget) { return other.checked == this.checked && other.monospace == this.monospace }
toDOM() { toDOM() {
let wrap = document.createElement("span") let wrap = document.createElement("span")
wrap.setAttribute("aria-hidden", "true") wrap.setAttribute("aria-hidden", "true")
wrap.className = "cm-taskmarker-toggle" wrap.className = "cm-taskmarker-toggle"
wrap.style.position = "relative"
// Three spaces since it's the same width as [ ] and [x] let box = document.createElement("input")
wrap.appendChild(document.createTextNode(" "))
let box = wrap.appendChild(document.createElement("input"))
box.type = "checkbox" box.type = "checkbox"
box.checked = this.checked box.checked = this.checked
box.style.position = "absolute" box.tabIndex = -1
box.style.top = "-3px" box.style.margin = "0"
box.style.left = "0" box.style.padding = "0"
if (this.monospace) {
// if the font is monospaced, we'll set the content of the wrapper to " " and the
// position of the checkbox to absolute, since three spaces will be the same width
// as "[ ]" and "[x]" so that characters on different lines will line up
wrap.appendChild(document.createTextNode(" "))
wrap.style.position = "relative"
box.style.position = "absolute"
box.style.top = "0"
box.style.left = "0.25em"
box.style.width = "1.1em"
box.style.height = "1.1em"
} else {
// if the font isn't monospaced, we'll let the checkbox take up as much space as needed
box.style.position = "relative"
box.style.top = "0.1em"
box.style.marginRight = "0.5em"
}
wrap.appendChild(box)
return wrap return wrap
} }
@ -52,7 +72,7 @@ function checkboxes(view: EditorView) {
if (view.state.doc.sliceString(nodeRef.to, nodeRef.to+1) === " ") { if (view.state.doc.sliceString(nodeRef.to, nodeRef.to+1) === " ") {
let isChecked = view.state.doc.sliceString(nodeRef.from, nodeRef.to).toLowerCase() === "[x]" let isChecked = view.state.doc.sliceString(nodeRef.from, nodeRef.to).toLowerCase() === "[x]"
let deco = Decoration.replace({ let deco = Decoration.replace({
widget: new CheckboxWidget(isChecked), widget: new CheckboxWidget(isChecked, view.state.facet(isMonospaceFont)),
inclusive: false, inclusive: false,
}) })
widgets.push(deco.range(nodeRef.from, nodeRef.to)) widgets.push(deco.range(nodeRef.from, nodeRef.to))
@ -92,8 +112,9 @@ export const todoCheckboxPlugin = [
} }
update(update: ViewUpdate) { update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) if (update.docChanged || update.viewportChanged || transactionsHasAnnotation(update.transactions, SET_FONT)) {
this.decorations = checkboxes(update.view) this.decorations = checkboxes(update.view)
}
} }
}, { }, {
decorations: v => v.decorations, decorations: v => v.decorations,

View File

@ -1,7 +1,9 @@
import './css/application.sass' import './css/application.sass'
import '../assets/font/open-sans/open-sans.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config';
import App from './components/App.vue' import App from './components/App.vue'
import { loadCurrencies } from './currency' import { loadCurrencies } from './currency'
@ -13,6 +15,7 @@ const pinia = createPinia()
const app = createApp(App) const app = createApp(App)
app.use(pinia) app.use(pinia)
app.use(PrimeVue)
app.mount('#app').$nextTick(() => { app.mount('#app').$nextTick(() => {
// hide loading screen // hide loading screen
postMessage({ payload: 'removeLoading' }, '*') postMessage({ payload: 'removeLoading' }, '*')

View File

@ -39,6 +39,7 @@ export const useEditorCacheStore = defineStore("editorCache", {
tabSize: settingsStore.settings.tabSize, tabSize: settingsStore.settings.tabSize,
defaultBlockToken: settingsStore.settings.defaultBlockLanguage, defaultBlockToken: settingsStore.settings.defaultBlockLanguage,
defaultBlockAutoDetect: settingsStore.settings.defaultBlockLanguageAutoDetect, defaultBlockAutoDetect: settingsStore.settings.defaultBlockLanguageAutoDetect,
keyBindings: settingsStore.settings.keyBindings,
}) })
} catch (e) { } catch (e) {
errorStore.addError("Error! " + e.message) errorStore.addError("Error! " + e.message)
@ -123,7 +124,8 @@ export const useEditorCacheStore = defineStore("editorCache", {
switch (key) { switch (key) {
case "keymap": case "keymap":
case "emacsMetaKey": case "emacsMetaKey":
editor.setKeymap(newSettings.keymap, newSettings.emacsMetaKey) case "keyBindings":
editor.setKeymap(newSettings.keymap, newSettings.emacsMetaKey, newSettings.keyBindings)
break break
case "showLineNumberGutter": case "showLineNumberGutter":
editor.setLineNumberGutter(newSettings.showLineNumberGutter) editor.setLineNumberGutter(newSettings.showLineNumberGutter)
@ -168,6 +170,9 @@ export const useEditorCacheStore = defineStore("editorCache", {
} }
window.document.removeEventListener("currenciesLoaded", this.onCurrenciesLoaded) window.document.removeEventListener("currenciesLoaded", this.onCurrenciesLoaded)
this.editorCache.lru = []
this.editorCache.cache = {}
}, },
moveCurrentBlockToOtherEditor(targetPath) { moveCurrentBlockToOtherEditor(targetPath) {

View File

@ -28,6 +28,7 @@ export const useHeynoteStore = defineStore("heynote", {
showCreateBuffer: false, showCreateBuffer: false,
showEditBuffer: false, showEditBuffer: false,
showMoveToBufferSelector: false, showMoveToBufferSelector: false,
showCommandPalette: false,
}), }),
actions: { actions: {
@ -58,6 +59,11 @@ export const useHeynoteStore = defineStore("heynote", {
openBufferSelector() { openBufferSelector() {
this.closeDialog() this.closeDialog()
this.showBufferSelector = true this.showBufferSelector = true
},
openCommandPalette() {
this.closeDialog()
this.showCommandPalette = true
}, },
openMoveToBufferSelector() { openMoveToBufferSelector() {
this.closeDialog() this.closeDialog()
@ -78,10 +84,12 @@ export const useHeynoteStore = defineStore("heynote", {
this.showLanguageSelector = false this.showLanguageSelector = false
this.showEditBuffer = false this.showEditBuffer = false
this.showMoveToBufferSelector = false this.showMoveToBufferSelector = false
this.showCommandPalette = false
}, },
closeBufferSelector() { closeBufferSelector() {
this.showBufferSelector = false this.showBufferSelector = false
this.showCommandPalette = false
}, },
closeMoveToBufferSelector() { closeMoveToBufferSelector() {
@ -96,6 +104,13 @@ export const useHeynoteStore = defineStore("heynote", {
this.showEditBuffer = true this.showEditBuffer = true
}, },
executeCommand(command) {
if (this.currentEditor) {
toRaw(this.currentEditor).executeCommand(command)
}
},
/** /**
* Create a new note file at `path` with name `name` from the current block of the current open editor, * Create a new note file at `path` with name `name` from the current block of the current open editor,
* and switch to it * and switch to it

View File

@ -22,7 +22,7 @@ test("test default font is Hack", async ({ page }) => {
})).toBeLessThan(20) })).toBeLessThan(20)
}) })
test("test custom font", async ({ page, browserName }) => { test("test custom font", async ({ page }) => {
// monkey patch window.queryLocalFonts because it's not available in Playwright // monkey patch window.queryLocalFonts because it's not available in Playwright
await page.evaluate(() => { await page.evaluate(() => {
window.queryLocalFonts = async () => { window.queryLocalFonts = async () => {
@ -64,3 +64,55 @@ test("test custom font", async ({ page, browserName }) => {
return el.clientHeight return el.clientHeight
})).toBeGreaterThan(20) })).toBeGreaterThan(20)
}) })
test("markdown todo checkbox position with monospaced font", async ({ page }) => {
await heynotePage.setContent(`
markdown
- [ ] Test
- [x] Test 2
`)
await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first().waitFor()
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]")).toHaveCount(2)
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first()).toHaveCSS("position", "absolute")
})
test("markdown todo checkbox position with variable width font", async ({ page }) => {
await page.evaluate(() => {
window.queryLocalFonts = async () => {
return [
{
family: "Arial",
style: "Regular",
},
{
family: "Hack",
fullName: "Hack Regular",
style: "Regular",
postscriptName: "Hack-Regular",
},
{
family: "Hack",
fullName: "Hack Italic",
style: "Italic",
postscriptName: "Hack-Italic",
},
]
}
})
await page.locator("css=.status-block.settings").click()
await page.locator("css=li.tab-appearance").click()
await page.locator("css=select.font-family").selectOption("Arial")
await page.locator("css=select.font-size").selectOption("20")
await page.locator("body").press("Escape")
await heynotePage.setContent(`
markdown
- [ ] Test
- [x] Test 2
`)
await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first().waitFor()
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]")).toHaveCount(2)
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first()).toHaveCSS("position", "relative")
})

View File

@ -0,0 +1,73 @@
import { expect, test } from "@playwright/test";
import { HeynotePage } from "./test-utils.js";
let heynotePage
test.beforeEach(async ({page}) => {
heynotePage = new HeynotePage(page)
await heynotePage.goto()
await heynotePage.setContent(`
text
`)
})
test("add custom key binding", async ({page}) => {
await page.locator("css=.status-block.settings").click()
await page.locator("css=.overlay .settings .dialog .sidebar li.tab-keyboard-bindings").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings")).toBeVisible()
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-keybinding").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog")).toBeVisible()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog input.keys")).toBeFocused()
await page.locator("body").press("Control+Shift+H")
await page.locator("body").press("Enter")
await page.locator("body").pressSequentially("language")
await page.locator(".p-autocomplete-list li.p-autocomplete-option.p-focus").click()
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog .save").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user")).toHaveCount(1)
expect((await heynotePage.getSettings()).keyBindings).toEqual([{key:"Control-Shift-h", command:"openLanguageSelector"}])
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
await page.locator("body").press("Control+Shift+H")
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
})
test("delete custom key binding", async ({page}) => {
await page.locator("css=.status-block.settings").click()
await page.locator("css=.overlay .settings .dialog .sidebar li.tab-keyboard-bindings").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings")).toBeVisible()
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-keybinding").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog")).toBeVisible()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog input.keys")).toBeFocused()
await page.locator("body").press("Control+Shift+H")
await page.locator("body").press("Enter")
await page.locator("body").pressSequentially("language")
await page.locator(".p-autocomplete-list li.p-autocomplete-option.p-focus").click()
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog .save").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user")).toHaveCount(1)
expect((await heynotePage.getSettings()).keyBindings).toEqual([{key:"Control-Shift-h", command:"openLanguageSelector"}])
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
await page.locator("body").press("Control+Shift+H")
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
await page.locator("css=.status-block.settings").click()
await page.locator("css=.overlay .settings .dialog .sidebar li.tab-keyboard-bindings").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings")).toBeVisible()
await page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user .delete").click()
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user")).toHaveCount(0)
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
await page.locator("body").press("Control+Shift+H")
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
})
test("disable default key binding", async ({page}) => {
const langKey = heynotePage.isMac ? "Meta+L" : "Control+L"
await page.locator("body").press(langKey)
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
await page.locator("body").press("Escape")
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
const settings = await heynotePage.getSettings()
settings.keyBindings = [{key:"Mod-L", command:"nothing"}]
await heynotePage.setSettings(settings)
await page.locator("body").press(langKey)
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
})

View File

@ -13,6 +13,7 @@ test.beforeEach(async ({ page, browserName }) => {
test.skip() test.skip()
} }
await page.locator("css=.status-block.settings").click() await page.locator("css=.status-block.settings").click()
await page.locator("css=li.tab-keyboard-bindings").click()
//await page.locator("css=li.tab-editing").click() //await page.locator("css=li.tab-editing").click()
await page.locator("css=select.keymap").selectOption("emacs") await page.locator("css=select.keymap").selectOption("emacs")
if (heynotePage.isMac) { if (heynotePage.isMac) {

View File

@ -20,6 +20,7 @@ export class HeynotePage {
async goto() { async goto() {
await this.page.goto("/") await this.page.goto("/")
await expect(this.page).toHaveTitle(/Heynote/) await expect(this.page).toHaveTitle(/Heynote/)
await expect(this.page.locator(".cm-editor")).toBeVisible()
expect(this.getErrors()).toStrictEqual([]) expect(this.getErrors()).toStrictEqual([])
} }
@ -78,6 +79,16 @@ export class HeynotePage {
await this.page.evaluate(({path, content}) => window.heynote.buffer.save(path, content), {path, content:format.serialize()}) await this.page.evaluate(({path, content}) => window.heynote.buffer.save(path, content), {path, content:format.serialize()})
} }
async getSettings() {
return await this.page.evaluate(() => {
return JSON.parse(window.localStorage.getItem("settings") || "{}")
})
}
async setSettings(settings) {
await this.page.evaluate((settings) => window.heynote.setSettings(settings), settings)
}
agnosticKey(key) { agnosticKey(key) {
return key.replace("Mod", this.isMac ? "Meta" : "Control") return key.replace("Mod", this.isMac ? "Meta" : "Control")
} }

View File

@ -136,8 +136,9 @@ export default defineConfig({
css: { css: {
preprocessorOptions: { preprocessorOptions: {
sass: { sass: {
api: "modern-compiler",
additionalData: ` additionalData: `
@import "./src/css/include.sass" @use "@/src/css/include.sass" as *
` `
} }
} }

View File

@ -89,6 +89,7 @@ let initialSettings = {
showLineNumberGutter: true, showLineNumberGutter: true,
showFoldGutter: true, showFoldGutter: true,
bracketClosing: false, bracketClosing: false,
keyBindings: [],
} }
if (settingsData !== null) { if (settingsData !== null) {
initialSettings = Object.assign(initialSettings, JSON.parse(settingsData)) initialSettings = Object.assign(initialSettings, JSON.parse(settingsData))

View File

@ -2,6 +2,7 @@ import '../src/css/application.sass'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config';
import App from '../src/components/App.vue' import App from '../src/components/App.vue'
import { loadCurrencies } from '../src/currency' import { loadCurrencies } from '../src/currency'
@ -9,6 +10,7 @@ import { loadCurrencies } from '../src/currency'
const pinia = createPinia() const pinia = createPinia()
const app = createApp(App) const app = createApp(App)
app.use(pinia) app.use(pinia)
app.use(PrimeVue)
app.mount('#app') app.mount('#app')
//console.log("test:", app.hej.test) //console.log("test:", app.hej.test)

View File

@ -37,8 +37,9 @@ export default defineConfig({
css: { css: {
preprocessorOptions: { preprocessorOptions: {
sass: { sass: {
api: "modern-compiler",
additionalData: ` additionalData: `
@import "../src/css/include.sass" @use "@/src/css/include.sass" as *
` `
}, },
}, },