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
#continue-on-error: true
uses: samuelmeuli/action-electron-builder@v1
uses: johannesjo/action-electron-builder@v1
with:
# Specify electron-builder config file
args: -c electron-builder.json5
@ -56,7 +56,7 @@ jobs:
# macOS notarization API key
APPLE_API_KEY: ~/private_keys/AuthKey.p8
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
# if: ${{ matrix.os == 'macos-latest' }}

View File

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

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).
## 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
- 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
```
## 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 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"
],
"afterSign": "electron-builder-notarize",
"mac": {
"artifactName": "${productName}_${version}_${arch}.${ext}",
"target": [

View File

@ -24,6 +24,19 @@ const schema = {
properties: {
"keymap": { "enum": ["default", "emacs"], default:"default" },
"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},
"showFoldGutter": {type: "boolean", default:true},
"autoUpdate": {type: "boolean", default: true},
@ -62,6 +75,7 @@ const defaults = {
settings: {
keymap: "default",
emacsMetaKey: isMac ? "meta" : "alt",
keyBindings: [],
showLineNumberGutter: true,
showFoldGutter: 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 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
const isBetaVersion = app.getVersion().includes("beta")
if (isBetaVersion) {
@ -85,8 +83,8 @@ export function quit() {
async function createWindow() {
// read any stored window settings from config, or use defaults
let windowConfig = {
width: CONFIG.get("windowConfig.width", 900) as number,
height: CONFIG.get("windowConfig.height", 680) as number,
width: CONFIG.get("windowConfig.width", 940) as number,
height: CONFIG.get("windowConfig.height", 720) as number,
isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean,
isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean,
x: CONFIG.get("windowConfig.x"),
@ -409,9 +407,6 @@ ipcMain.handle("getInitErrors", () => {
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 showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock");
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",
"version": "2.1.3",
"version": "2.2.0-beta.2",
"main": "dist-electron/main/index.js",
"description": "A dedicated scratch pad",
"author": "Jonatan Heyman (https://heyman.info)",
@ -50,34 +50,35 @@
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.2",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.5",
"@electron/asar": "^3.2.2",
"@lezer/generator": "^1.5.1",
"@lezer/markdown": "^1.1.2",
"@playwright/test": "^1.49.0",
"@playwright/test": "^1.51.1",
"@replit/codemirror-lang-csharp": "^6.2.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue": "^5.2.3",
"codemirror-lang-elixir": "^4.0.0",
"debounce": "^1.2.1",
"electron": "^33.3.1",
"electron-builder": "^23.6.0",
"electron-builder-notarize": "^1.5.1",
"electron": "^35.2.0",
"electron-builder": "^26.0.12",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"electron-updater": "^6.6.2",
"fs-jetpack": "^5.1.0",
"lezer-elixir": "^1.1.2",
"prettier": "^3.3.2",
"primevue": "^4.3.3",
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"sass-embedded": "^1.87.0",
"typescript": "^4.9.4",
"vite": "^4.5.2",
"vite": "^6.3.2",
"vite-plugin-electron": "^0.11.1",
"vite-plugin-electron-renderer": "^0.11.4",
"vue": "^3.2.45",
"vue-tsc": "^1.0.16"
"vue": "^3.5.13",
"vue-tsc": "^1.0.16",
"vuedraggable": "^4.1.0"
},
"dependencies": {
"@sindresorhus/slugify": "^2.2.1",

View File

@ -20,7 +20,8 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* 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: process.env.CI ? [['github'], ['html']] : 'list',
/* 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) },
showEditBuffer(value) { this.dialogWatcher(value) },
showMoveToBufferSelector(value) { this.dialogWatcher(value) },
showCommandPalette(value) { this.dialogWatcher(value) },
currentBufferPath() {
this.focusEditor()
@ -85,11 +86,15 @@
"showCreateBuffer",
"showEditBuffer",
"showMoveToBufferSelector",
"openMoveToBufferSelector",
"showCommandPalette",
]),
dialogVisible() {
return this.showLanguageSelector || this.showBufferSelector || this.showCreateBuffer || this.showEditBuffer || this.showMoveToBufferSelector || this.showCommandPalette || this.showSettings
},
editorInert() {
return this.showCreateBuffer || this.showSettings || this.showEditBuffer
return this.dialogVisible
},
},
@ -173,7 +178,9 @@
@close="closeDialog"
/>
<BufferSelector
v-if="showBufferSelector"
v-if="showBufferSelector || showCommandPalette"
:initialFilter="showCommandPalette ? '>' : ''"
:commandsEnabled="true"
@openBuffer="openBuffer"
@openCreateBuffer="(nameSuggestion) => openCreateBuffer('new', nameSuggestion)"
@close="closeBufferSelector"
@ -181,6 +188,7 @@
<BufferSelector
v-if="showMoveToBufferSelector"
headline="Move block to..."
:commandsEnabled="false"
@openBuffer="onMoveCurrentBlockToOtherEditor"
@openCreateBuffer="(nameSuggestion) => openCreateBuffer('currentBlock', nameSuggestion)"
@close="closeMoveToBufferSelector"

View File

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

View File

@ -120,6 +120,12 @@
},
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
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
if (redirectKeys.includes(event.key)) {

View File

@ -22,6 +22,10 @@
syntaxTreeDebugContent: null,
editor: 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(UNDO_EVENT, () => {
this.onUndo = () => {
if (this.editor) {
toRaw(this.editor).undo()
}
})
}
window.heynote.mainProcess.on(UNDO_EVENT, this.onUndo)
window.heynote.mainProcess.on(REDO_EVENT, () => {
this.onRedo = () => {
if (this.editor) {
toRaw(this.editor).redo()
}
})
}
window.heynote.mainProcess.on(REDO_EVENT, this.onRedo)
window.heynote.mainProcess.on(DELETE_BLOCK_EVENT, () => {
this.onDeleteBlock = () => {
if (this.editor) {
toRaw(this.editor).deleteActiveBlock()
}
})
}
window.heynote.mainProcess.on(DELETE_BLOCK_EVENT, this.onDeleteBlock)
window.heynote.mainProcess.on(SELECT_ALL_EVENT, () => {
if (this.editor) {
toRaw(this.editor).selectAll()
this.onSelectAll = () => {
const activeEl = document.activeElement
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 (this.debugSyntaxTree) {
@ -85,10 +100,10 @@
beforeUnmount() {
window.heynote.mainProcess.off(WINDOW_CLOSE_EVENT, this.onWindowClose)
window.heynote.mainProcess.off(UNDO_EVENT)
window.heynote.mainProcess.off(REDO_EVENT)
window.heynote.mainProcess.off(DELETE_BLOCK_EVENT)
window.heynote.mainProcess.off(SELECT_ALL_EVENT)
window.heynote.mainProcess.off(UNDO_EVENT, this.onUndo)
window.heynote.mainProcess.off(REDO_EVENT, this.onRedo)
window.heynote.mainProcess.off(DELETE_BLOCK_EVENT, this.onDeleteBlock)
window.heynote.mainProcess.off(SELECT_ALL_EVENT, this.onSelectAll)
this.editorCacheStore.tearDown();
},

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
@import "reset"
@import "font"
@import "base"
@use "reset"
@use "font"
@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']
--status-bar-background: #48b57e
--status-bar-color: #fff
--highlight-color: #48b57e
:root[theme='dark']
--status-bar-background: #0e1217
--status-bar-color: rgba(255, 255, 255, 0.75)
--highlight-color: #1b6540
html
margin: 0

View File

@ -4,7 +4,6 @@
font-weight: 400
font-style: normal
@font-face
font-family: 'Hack'
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-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 CURSOR_CHANGE = "heynote-cursor-change"
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),
annotations: [heynoteEvent.of(DELETE_BLOCK)],
}))
return true
}
export const deleteBlockSetCursorPreviousBlock = (editor) => ({state, dispatch}) => {
@ -380,4 +381,5 @@ export const deleteBlockSetCursorPreviousBlock = (editor) => ({state, dispatch})
selection: EditorSelection.cursor(newSelection),
annotations: [heynoteEvent.of(DELETE_BLOCK)],
}))
return true
}

View File

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

View File

@ -4,7 +4,7 @@ import { RangeSetBuilder } from "@codemirror/state"
import { WidgetType } from "@codemirror/view"
import { getNoteBlockFromPos } from "./block"
import { CURRENCIES_LOADED } from "../annotation"
import { transactionsHasAnnotation, CURRENCIES_LOADED } from "../annotation"
class MathResult extends WidgetType {
@ -107,12 +107,6 @@ function mathDeco(view) {
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 {
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 { LANGUAGES } from './languages.js';
import { setEmacsMarkMode } from "./emacs.js"
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
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 && 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
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 && !cut) {
@ -107,6 +106,7 @@ const copyCut = (view, cut, editor) => {
selection: newSelection,
}))
}
return true
}

View File

@ -1,7 +1,7 @@
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction } from "@codemirror/state"
import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction, Prec } from "@codemirror/state"
import { EditorView, keymap as cmKeymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
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 { undo, redo } from "@codemirror/commands"
@ -11,32 +11,24 @@ import { heynoteBase } from "./theme/base.js"
import { getFontTheme } from "./theme/font-theme.js";
import { customSetup } from "./setup.js"
import { heynoteLang } from "./lang-heynote/heynote.js"
import { getCloseBracketsExtensions } from "./close-brackets.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 { formatBlockContent } from "./block/format-code.js"
import { heynoteKeymap } from "./keymap.js"
import { emacsKeymap } from "./emacs.js"
import { getKeymapExtensions } from "./keymap.js"
import { heynoteCopyCut } from "./copy-paste"
import { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js"
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
import { links } from "./links.js"
import { HEYNOTE_COMMANDS } from "./commands.js";
import { NoteFormat } from "../common/note-format.js"
import { AUTO_SAVE_INTERVAL } from "../common/constants.js"
import { useHeynoteStore } from "../stores/heynote-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 {
constructor({
element,
@ -54,6 +46,7 @@ export class HeynoteEditor {
tabSize=4,
defaultBlockToken,
defaultBlockAutoDetect,
keyBindings,
}) {
this.element = element
this.path = path
@ -73,20 +66,20 @@ export class HeynoteEditor {
this.notesStore = useHeynoteStore()
this.errorStore = useErrorStore()
this.name = ""
this.selectionMarkMode = false
const state = EditorState.create({
doc: "",
extensions: [
this.keymapCompartment.of(getKeymapExtensions(this, keymap)),
this.keymapCompartment.of(getKeymapExtensions(this, keymap, keyBindings)),
heynoteCopyCut(this),
//minimalSetup,
this.lineNumberCompartment.of(showLineNumberGutter ? [lineNumbers(), blockLineNumbers] : []),
customSetup,
this.foldGutterCompartment.of(showFoldGutter ? [foldGutter()] : []),
this.closeBracketsCompartment.of(bracketClosing ? [closeBrackets()] : []),
this.closeBracketsCompartment.of(bracketClosing ? [getCloseBracketsExtensions()] : []),
this.readOnlyCompartment.of([]),
@ -111,8 +104,12 @@ export class HeynoteEditor {
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,
markdown(),
markdown({addKeymap: false}),
Prec.highest(cmKeymap.of(markdownKeymap)),
links,
],
})
@ -137,6 +134,11 @@ export class HeynoteEditor {
if (focus) {
this.view.focus()
}
// trigger setFont once the fonts has loaded
document.fonts.ready.then(() => {
this.setFont(fontFamily, fontSize)
})
}
async save() {
@ -260,6 +262,7 @@ export class HeynoteEditor {
setFont(fontFamily, fontSize) {
this.view.dispatch({
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.emacsMetaKey = emacsMetaKey
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()
}
openCommandPalette() {
this.notesStore.openCommandPalette()
}
openCreateBuffer(createMode) {
this.notesStore.openCreateBuffer(createMode)
}
@ -367,7 +374,7 @@ export class HeynoteEditor {
setBracketClosing(value) {
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)))
})
}
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 { EditorSelection, EditorState } from "@codemirror/state"
import {
indentLess, indentMore, redo,
} from "@codemirror/commands"
import { Prec } from "@codemirror/state"
import {
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 { HEYNOTE_COMMANDS } from "./commands.js"
import { formatBlockContent } from "./block/format-code.js"
import { deleteLine } from "./block/delete-line.js"
const cmd = (key, command) => ({key, command})
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) => {
if (spec.run) {
if ("preventDefault" in spec) {
return spec
} else {
return {...spec, preventDefault: true}
}
} else {
const [key, run] = spec
return {
key,
run,
preventDefault: true,
}
let key = spec.key
if (key.indexOf("EmacsMeta") != -1) {
key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta")
}
return {
key: key,
//preventDefault: true,
preventDefault: false,
run: (view) => {
//console.log("run()", spec.key, spec.command)
const command = HEYNOTE_COMMANDS[spec.command]
if (!command) {
console.error(`Command not found: ${spec.command} (${spec.key})`)
return false
}
return command.run(editor)(view)
},
}
}))
}
export function heynoteKeymap(editor) {
return keymapFromSpec([
["Mod-c", copyCommand(editor)],
["Mod-v", pasteCommand],
["Mod-x", cutCommand(editor)],
["Tab", indentMore],
["Shift-Tab", indentLess],
["Alt-Shift-Enter", addNewBlockBeforeFirst(editor)],
["Mod-Shift-Enter", addNewBlockAfterLast(editor)],
["Alt-Enter", addNewBlockBeforeCurrent(editor)],
["Mod-Enter", addNewBlockAfterCurrent(editor)],
["Mod-Alt-Enter", insertNewBlockAtCursor(editor)],
["Mod-a", selectAll],
["Alt-ArrowUp", moveLineUp],
["Alt-ArrowDown", moveLineDown],
["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},
])
export function heynoteKeymap(editor, keymap, userKeymap) {
return [
keymapFromSpec([
...userKeymap,
...keymap,
], editor),
]
}
export function getKeymapExtensions(editor, keymap, keyBindings) {
return heynoteKeymap(
editor,
keymap === "emacs" ? EMACS_KEYMAP.concat(DEFAULT_KEYMAP) : DEFAULT_KEYMAP,
keyBindings || [],
)
}

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,
scrollPastEnd(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
//...closeBracketsKeymap,
//...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
//...historyKeymap,
//...foldKeymap,
//...completionKeymap,
//...lintKeymap
])

View File

@ -1,11 +1,39 @@
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) {
fontSize = fontSize || window.heynote.defaultFontSize
return EditorView.theme({
'.cm-scroller': {
fontFamily: fontFamily || window.heynote.defaultFontFamily,
fontSize: (fontSize) + "px",
},
})
const computedFontFamily = fontFamily || window.heynote.defaultFontFamily
return [
EditorView.theme({
'.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 { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
import { isMonospaceFont } from "./theme/font-theme"
import { transactionsHasAnnotation, SET_FONT } from "./annotation"
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() {
let wrap = document.createElement("span")
wrap.setAttribute("aria-hidden", "true")
wrap.className = "cm-taskmarker-toggle"
wrap.style.position = "relative"
// Three spaces since it's the same width as [ ] and [x]
wrap.appendChild(document.createTextNode(" "))
let box = wrap.appendChild(document.createElement("input"))
let box = document.createElement("input")
box.type = "checkbox"
box.checked = this.checked
box.style.position = "absolute"
box.style.top = "-3px"
box.style.left = "0"
box.tabIndex = -1
box.style.margin = "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
}
@ -52,7 +72,7 @@ function checkboxes(view: EditorView) {
if (view.state.doc.sliceString(nodeRef.to, nodeRef.to+1) === " ") {
let isChecked = view.state.doc.sliceString(nodeRef.from, nodeRef.to).toLowerCase() === "[x]"
let deco = Decoration.replace({
widget: new CheckboxWidget(isChecked),
widget: new CheckboxWidget(isChecked, view.state.facet(isMonospaceFont)),
inclusive: false,
})
widgets.push(deco.range(nodeRef.from, nodeRef.to))
@ -92,8 +112,9 @@ export const todoCheckboxPlugin = [
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged)
if (update.docChanged || update.viewportChanged || transactionsHasAnnotation(update.transactions, SET_FONT)) {
this.decorations = checkboxes(update.view)
}
}
}, {
decorations: v => v.decorations,

View File

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

View File

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

View File

@ -28,6 +28,7 @@ export const useHeynoteStore = defineStore("heynote", {
showCreateBuffer: false,
showEditBuffer: false,
showMoveToBufferSelector: false,
showCommandPalette: false,
}),
actions: {
@ -58,6 +59,11 @@ export const useHeynoteStore = defineStore("heynote", {
openBufferSelector() {
this.closeDialog()
this.showBufferSelector = true
},
openCommandPalette() {
this.closeDialog()
this.showCommandPalette = true
},
openMoveToBufferSelector() {
this.closeDialog()
@ -78,10 +84,12 @@ export const useHeynoteStore = defineStore("heynote", {
this.showLanguageSelector = false
this.showEditBuffer = false
this.showMoveToBufferSelector = false
this.showCommandPalette = false
},
closeBufferSelector() {
this.showBufferSelector = false
this.showCommandPalette = false
},
closeMoveToBufferSelector() {
@ -96,6 +104,13 @@ export const useHeynoteStore = defineStore("heynote", {
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,
* and switch to it

View File

@ -22,7 +22,7 @@ test("test default font is Hack", async ({ page }) => {
})).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
await page.evaluate(() => {
window.queryLocalFonts = async () => {
@ -64,3 +64,55 @@ test("test custom font", async ({ page, browserName }) => {
return el.clientHeight
})).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()
}
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=select.keymap").selectOption("emacs")
if (heynotePage.isMac) {

View File

@ -20,6 +20,7 @@ export class HeynotePage {
async goto() {
await this.page.goto("/")
await expect(this.page).toHaveTitle(/Heynote/)
await expect(this.page.locator(".cm-editor")).toBeVisible()
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()})
}
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) {
return key.replace("Mod", this.isMac ? "Meta" : "Control")
}

View File

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

View File

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

View File

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

View File

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