mirror of
https://github.com/heyman/heynote.git
synced 2024-11-22 07:54:11 +01:00
Replace block separators with "\n\n" when copying text.
Add Settings dialog. Started implementing Emacs-like keymap.
This commit is contained in:
parent
6c1e89c5b0
commit
924fd4b226
21
electron/config.js
Normal file
21
electron/config.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Store from "electron-store"
|
||||
|
||||
const schema = {
|
||||
additionalProperties: false,
|
||||
|
||||
windowConfig: {
|
||||
type: "object",
|
||||
properties: {
|
||||
width: {type: "number"},
|
||||
height: {type: "number"},
|
||||
x: {type: "number"},
|
||||
y: {type: "number"},
|
||||
isMaximized: {type: "boolean"},
|
||||
isFullScreen: {type: "boolean"},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
keymap: { "enum": ["default", "emacs"] },
|
||||
}
|
||||
|
||||
export default new Store({schema})
|
@ -1 +1,3 @@
|
||||
export const WINDOW_CLOSE_EVENT = "window-close"
|
||||
export const KEYMAP_CHANGE_EVENT = "keymap-change"
|
||||
export const OPEN_SETTINGS_EVENT = "open-settings"
|
||||
|
27
electron/keymap.js
Normal file
27
electron/keymap.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { isMac } from "./detect-platform"
|
||||
|
||||
|
||||
export function onBeforeInputEvent({win, event, input, currentKeymap}) {
|
||||
//console.log("keyboard event", input)
|
||||
let metaKey = "alt"
|
||||
if (isMac) {
|
||||
metaKey = "meta"
|
||||
}
|
||||
if (currentKeymap === "emacs") {
|
||||
/**
|
||||
* When using Emacs keymap, we can't bind shortcuts for copy, cut and paste in the the renderer process
|
||||
* using Codemirror's bind function. Therefore we have to bind them in electron land, and send
|
||||
* cut, paste and copy to window.webContents
|
||||
*/
|
||||
if (input.key === "y" && input.control) {
|
||||
event.preventDefault()
|
||||
win.webContents.paste()
|
||||
} else if (input.key === "w" && input.control) {
|
||||
event.preventDefault()
|
||||
win.webContents.cut()
|
||||
} else if (input.key === "w" && input[metaKey]) {
|
||||
event.preventDefault()
|
||||
win.webContents.copy()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { app, BrowserWindow, shell, ipcMain, Menu, nativeTheme } from 'electron'
|
||||
import { app, BrowserWindow, shell, ipcMain, Menu, nativeTheme, globalShortcut } from 'electron'
|
||||
import { release } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import * as jetpack from "fs-jetpack";
|
||||
import Store from "electron-store"
|
||||
import menu from './menu'
|
||||
import { initialContent, initialDevContent } from '../initial-content'
|
||||
import { WINDOW_CLOSE_EVENT } from '../constants';
|
||||
import { WINDOW_CLOSE_EVENT, KEYMAP_CHANGE_EVENT } from '../constants';
|
||||
import CONFIG from "../config"
|
||||
import { onBeforeInputEvent } from "../keymap"
|
||||
import { isMac } from '../detect-platform';
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
@ -37,8 +39,6 @@ if (!process.env.VITE_DEV_SERVER_URL && !app.requestSingleInstanceLock()) {
|
||||
// Set custom application menu
|
||||
Menu.setApplicationMenu(menu)
|
||||
|
||||
const CONFIG = new Store()
|
||||
|
||||
|
||||
// Remove electron security warnings
|
||||
// This warning only shows in development mode
|
||||
@ -51,6 +51,8 @@ const preload = join(__dirname, '../preload/index.js')
|
||||
const url = process.env.VITE_DEV_SERVER_URL
|
||||
const indexHtml = join(process.env.DIST, 'index.html')
|
||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||
|
||||
let currentKeymap = CONFIG.get("keymap", "default")
|
||||
let contentSaved = false
|
||||
|
||||
|
||||
@ -114,6 +116,11 @@ async function createWindow() {
|
||||
win.loadFile(indexHtml)
|
||||
//win.webContents.openDevTools()
|
||||
}
|
||||
|
||||
// custom keyboard shortcuts for Emacs keybindings
|
||||
win.webContents.on("before-input-event", function (event, input) {
|
||||
onBeforeInputEvent({event, input, win, currentKeymap})
|
||||
})
|
||||
|
||||
// Test actively push message to the Electron-Renderer
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
@ -183,3 +190,9 @@ ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => {
|
||||
contentSaved = true
|
||||
app.quit()
|
||||
})
|
||||
|
||||
ipcMain.handle('keymap:set', (event, keymap) => {
|
||||
currentKeymap = keymap
|
||||
win?.webContents.send(KEYMAP_CHANGE_EVENT, keymap)
|
||||
CONFIG.set("keymap", keymap)
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
const { app, Menu } = require('electron')
|
||||
import { WINDOW_CLOSE_EVENT, KEYMAP_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from '../constants';
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
@ -9,6 +10,14 @@ const template = [
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Preferences',
|
||||
click: (menuItem, window, event) => {
|
||||
window?.webContents.send(OPEN_SETTINGS_EVENT)
|
||||
},
|
||||
accelerator: isMac ? 'Command+,': null,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
|
@ -2,7 +2,8 @@ const { contextBridge } = require('electron')
|
||||
import darkMode from "./theme-mode"
|
||||
import { isMac, isWindows, isLinux } from "../detect-platform"
|
||||
import { ipcRenderer } from "electron"
|
||||
import { WINDOW_CLOSE_EVENT } from "../constants"
|
||||
import { WINDOW_CLOSE_EVENT, KEYMAP_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from "../constants"
|
||||
import CONFIG from "../config"
|
||||
|
||||
contextBridge.exposeInMainWorld("platform", {
|
||||
isMac,
|
||||
@ -21,6 +22,10 @@ contextBridge.exposeInMainWorld("heynote", {
|
||||
ipcRenderer.on(WINDOW_CLOSE_EVENT, callback)
|
||||
},
|
||||
|
||||
onOpenSettings(callback) {
|
||||
ipcRenderer.on(OPEN_SETTINGS_EVENT, callback)
|
||||
},
|
||||
|
||||
buffer: {
|
||||
async load() {
|
||||
return await ipcRenderer.invoke("buffer-content:load")
|
||||
@ -33,7 +38,17 @@ contextBridge.exposeInMainWorld("heynote", {
|
||||
async saveAndQuit(content) {
|
||||
return await ipcRenderer.invoke("buffer-content:saveAndQuit", content)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
keymap: {
|
||||
set(keymap) {
|
||||
ipcRenderer.invoke("keymap:set", keymap);
|
||||
},
|
||||
initial: CONFIG.get("keymap", "default"),
|
||||
onKeymapChange(callback) {
|
||||
ipcRenderer.on(KEYMAP_CHANGE_EVENT, (event, keymap) => callback(keymap))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
@ -2,12 +2,14 @@
|
||||
import StatusBar from './StatusBar.vue'
|
||||
import Editor from './Editor.vue'
|
||||
import LanguageSelector from './LanguageSelector.vue'
|
||||
import Settings from './settings/Settings.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Editor,
|
||||
StatusBar,
|
||||
LanguageSelector,
|
||||
Settings,
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -21,6 +23,8 @@
|
||||
systemTheme: 'system',
|
||||
development: window.location.href.indexOf("dev=1") !== -1,
|
||||
showLanguageSelector: false,
|
||||
showSettings: false,
|
||||
keymap: window.heynote.keymap.initial,
|
||||
}
|
||||
},
|
||||
|
||||
@ -32,6 +36,12 @@
|
||||
window.darkMode.onChange((theme) => {
|
||||
this.theme = theme
|
||||
})
|
||||
window.heynote.keymap.onKeymapChange((keymap) => {
|
||||
this.keymap = keymap
|
||||
})
|
||||
window.heynote.onOpenSettings(() => {
|
||||
this.showSettings = true
|
||||
})
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@ -39,6 +49,14 @@
|
||||
},
|
||||
|
||||
methods: {
|
||||
openSettings() {
|
||||
this.showSettings = true
|
||||
},
|
||||
closeSettings() {
|
||||
this.showSettings = false
|
||||
this.$refs.editor.focus()
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
let newTheme
|
||||
// when the "system" theme is used, make sure that the first click always results in amn actual theme change
|
||||
@ -85,6 +103,7 @@
|
||||
:theme="theme"
|
||||
:development="development"
|
||||
:debugSyntaxTree="false"
|
||||
:keymap="keymap"
|
||||
class="editor"
|
||||
ref="editor"
|
||||
@openLanguageSelector="openLanguageSelector"
|
||||
@ -106,6 +125,11 @@
|
||||
@selectLanguage="onSelectLanguage"
|
||||
@close="closeLanguageSelector"
|
||||
/>
|
||||
<Settings
|
||||
v-if="showSettings"
|
||||
:keymap="keymap"
|
||||
@closeSettings="closeSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -3,11 +3,15 @@
|
||||
import { syntaxTree } from "@codemirror/language"
|
||||
|
||||
export default {
|
||||
props: [
|
||||
"theme",
|
||||
"development",
|
||||
"debugSyntaxTree",
|
||||
],
|
||||
props: {
|
||||
theme: String,
|
||||
development: Boolean,
|
||||
debugSyntaxTree: Boolean,
|
||||
keymap: {
|
||||
type: String,
|
||||
default: "default",
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
@ -38,6 +42,7 @@
|
||||
saveFunction: (content) => {
|
||||
window.heynote.buffer.save(content)
|
||||
},
|
||||
keymap: this.keymap,
|
||||
})
|
||||
})
|
||||
// set up window close handler that will save the buffer and quit
|
||||
@ -69,6 +74,10 @@
|
||||
theme(newTheme) {
|
||||
this.editor.setTheme(newTheme)
|
||||
},
|
||||
|
||||
keymap(keymap) {
|
||||
this.editor.setKeymap(keymap)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
113
src/components/settings/Settings.vue
Normal file
113
src/components/settings/Settings.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
keymap: String,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
this.$refs.keymapSelector.focus()
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeymapChange(event) {
|
||||
window.heynote.keymap.set(event.target.value)
|
||||
},
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.$emit("closeSettings")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings">
|
||||
<div class="dialog">
|
||||
<div>
|
||||
<h1>Settings</h1>
|
||||
<div class="entry">
|
||||
<h2>Keymap:</h2>
|
||||
<select ref="keymapSelector" @change="onKeymapChange">
|
||||
<option :selected="keymap==='default'" value="default">Default</option>
|
||||
<option :selected="keymap==='emacs'" value="emacs">Emacs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('closeSettings')"
|
||||
class="close"
|
||||
>Close</button>
|
||||
</div>
|
||||
<div class="shader"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
=dark-mode()
|
||||
@media (prefers-color-scheme: dark)
|
||||
@content
|
||||
|
||||
.settings
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
|
||||
.shader
|
||||
z-index: 1
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
background: rgba(0, 0, 0, 0.5)
|
||||
|
||||
.dialog
|
||||
box-sizing: border-box
|
||||
z-index: 2
|
||||
position: absolute
|
||||
left: 50%
|
||||
top: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
width: 100%
|
||||
height: 100%
|
||||
max-width: 700px
|
||||
max-height: 500px
|
||||
border-radius: 5px
|
||||
padding: 40px
|
||||
background: #fff
|
||||
color: #333
|
||||
&:active, &:selected, &:focus, &:focus-visible
|
||||
border: none
|
||||
outline: none
|
||||
+dark-mode
|
||||
background: #333
|
||||
color: #eee
|
||||
h1
|
||||
font-size: 20px
|
||||
font-weight: 600
|
||||
margin-bottom: 20px
|
||||
|
||||
.entry
|
||||
margin-bottom: 20px
|
||||
h2
|
||||
font-weight: 600
|
||||
margin-bottom: 10px
|
||||
select
|
||||
width: 200px
|
||||
&:focus
|
||||
outline: none
|
||||
|
||||
.close
|
||||
height: 32px
|
||||
position: absolute
|
||||
bottom: 30px
|
||||
right: 30px
|
||||
</style>
|
57
src/editor/copy-paste.js
Normal file
57
src/editor/copy-paste.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { EditorState, EditorSelection } from "@codemirror/state"
|
||||
import { EditorView } from "@codemirror/view"
|
||||
|
||||
import { LANGUAGES } from './languages.js';
|
||||
|
||||
|
||||
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
|
||||
const blockSeparatorRegex = new RegExp(`\\n∞∞∞(${languageTokensMatcher})(-a)?\\n`, "g")
|
||||
|
||||
|
||||
function copiedRange(state) {
|
||||
let content = [], ranges = []
|
||||
for (let range of state.selection.ranges) if (!range.empty) {
|
||||
content.push(state.sliceDoc(range.from, range.to))
|
||||
ranges.push(range)
|
||||
}
|
||||
return { text: content.join(state.lineBreak), ranges }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const heynoteCopyPaste = (editor) => {
|
||||
let copy, cut
|
||||
copy = cut = (event, view) => {
|
||||
let { text, ranges } = copiedRange(view.state)
|
||||
text = text.replaceAll(blockSeparatorRegex, "\n\n")
|
||||
let data = event.clipboardData
|
||||
if (data) {
|
||||
event.preventDefault()
|
||||
data.clearData()
|
||||
data.setData("text/plain", text)
|
||||
}
|
||||
if (event.type == "cut" && !view.state.readOnly) {
|
||||
view.dispatch({
|
||||
changes: ranges,
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete.cut"
|
||||
})
|
||||
}
|
||||
// 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") {
|
||||
const newSelection = EditorSelection.create(
|
||||
view.state.selection.ranges.map(r => EditorSelection.cursor(r.head)),
|
||||
view.state.selection.mainIndex,
|
||||
)
|
||||
view.dispatch(view.state.update({
|
||||
selection: newSelection,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return EditorView.domEventHandlers({
|
||||
copy,
|
||||
cut,
|
||||
})
|
||||
}
|
@ -9,27 +9,39 @@ import { customSetup } from "./setup.js"
|
||||
import { heynoteLang } from "./lang-heynote/heynote.js"
|
||||
import { noteBlockExtension } from "./block/block.js"
|
||||
import { changeCurrentBlockLanguage } from "./block/commands.js"
|
||||
import { heynoteKeymap } from "./keymap.js"
|
||||
import { heynoteKeymap, emacsKeymap } from "./keymap.js"
|
||||
import { heynoteCopyPaste } from "./copy-paste"
|
||||
import { languageDetection } from "./language-detection/autodetect.js"
|
||||
import { autoSaveContent } from "./save.js"
|
||||
|
||||
export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector"
|
||||
|
||||
function getKeymapExtensions(editor, keymap) {
|
||||
if (keymap === "emacs") {
|
||||
return emacsKeymap(editor)
|
||||
} else {
|
||||
return heynoteKeymap(editor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class HeynoteEditor {
|
||||
constructor({element, content, focus=true, theme="light", saveFunction=null}) {
|
||||
constructor({element, content, focus=true, theme="light", saveFunction=null, keymap="default"}) {
|
||||
this.element = element
|
||||
this.theme = new Compartment
|
||||
this.themeCompartment = new Compartment
|
||||
this.keymapCompartment = new Compartment
|
||||
this.deselectOnCopy = keymap === "emacs"
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: content || "",
|
||||
extensions: [
|
||||
heynoteKeymap(this),
|
||||
this.keymapCompartment.of(getKeymapExtensions(this, keymap)),
|
||||
heynoteCopyPaste(this),
|
||||
|
||||
//minimalSetup,
|
||||
customSetup,
|
||||
|
||||
this.theme.of(theme === "dark" ? heynoteDark : heynoteLight),
|
||||
this.themeCompartment.of(theme === "dark" ? heynoteDark : heynoteLight),
|
||||
heynoteBase,
|
||||
indentUnit.of(" "),
|
||||
EditorView.scrollMargins.of(f => {
|
||||
@ -71,7 +83,14 @@ export class HeynoteEditor {
|
||||
|
||||
setTheme(theme) {
|
||||
this.view.dispatch({
|
||||
effects: this.theme.reconfigure(theme === "dark" ? heynoteDark : heynoteLight),
|
||||
effects: this.themeCompartment.reconfigure(theme === "dark" ? heynoteDark : heynoteLight),
|
||||
})
|
||||
}
|
||||
|
||||
setKeymap(keymap) {
|
||||
this.deselectOnCopy = keymap === "emacs"
|
||||
this.view.dispatch({
|
||||
effects: this.keymapCompartment.reconfigure(getKeymapExtensions(this, keymap)),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,54 +1,40 @@
|
||||
import { keymap } from "@codemirror/view"
|
||||
import { indentWithTab, insertTab, indentLess, indentMore } from "@codemirror/commands"
|
||||
import { EditorView, keymap } from "@codemirror/view"
|
||||
import { EditorSelection } from "@codemirror/state"
|
||||
import { indentWithTab, insertTab, indentLess, indentMore, undo, redo } from "@codemirror/commands"
|
||||
import { insertNewBlockAtCursor, addNewBlockAfterCurrent, moveLineUp, selectAll, gotoPreviousBlock, gotoNextBlock } from "./block/commands.js";
|
||||
|
||||
export function heynoteKeymap(editor) {
|
||||
return keymap.of([
|
||||
{
|
||||
key: "Tab",
|
||||
["Tab", indentMore],
|
||||
["Shift-Tab", indentLess],
|
||||
["Mod-Enter", addNewBlockAfterCurrent],
|
||||
["Mod-Shift-Enter", insertNewBlockAtCursor],
|
||||
["Mod-a", selectAll],
|
||||
["Alt-ArrowUp", moveLineUp],
|
||||
["Mod-ArrowUp", gotoPreviousBlock],
|
||||
["Mod-ArrowDown", gotoNextBlock],
|
||||
["Mod-l", () => editor.openLanguageSelector()],
|
||||
].map(([key, run]) => {
|
||||
return {
|
||||
key,
|
||||
run,
|
||||
preventDefault: true,
|
||||
//run: insertTab,
|
||||
run: indentMore,
|
||||
},
|
||||
{
|
||||
key: 'Shift-Tab',
|
||||
preventDefault: true,
|
||||
run: indentLess,
|
||||
},
|
||||
{
|
||||
key: "Mod-Enter",
|
||||
preventDefault: true,
|
||||
run: addNewBlockAfterCurrent,
|
||||
},
|
||||
{
|
||||
key: "Mod-Shift-Enter",
|
||||
preventDefault: true,
|
||||
run: insertNewBlockAtCursor,
|
||||
},
|
||||
{
|
||||
key: "Mod-a",
|
||||
preventDefault: true,
|
||||
run: selectAll,
|
||||
},
|
||||
{
|
||||
key: "Alt-ArrowUp",
|
||||
preventDefault: true,
|
||||
run: moveLineUp,
|
||||
},
|
||||
{
|
||||
key: "Mod-ArrowUp",
|
||||
preventDefault: true,
|
||||
run: gotoPreviousBlock,
|
||||
},
|
||||
{
|
||||
key: "Mod-ArrowDown",
|
||||
preventDefault: true,
|
||||
run: gotoNextBlock,
|
||||
},
|
||||
{
|
||||
key: "Mod-l",
|
||||
preventDefault: true,
|
||||
run: () => editor.openLanguageSelector(),
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export function emacsKeymap(editor) {
|
||||
return [
|
||||
heynoteKeymap(editor),
|
||||
keymap.of([
|
||||
["Ctrl-Shift--", undo],
|
||||
["Ctrl-.", redo],
|
||||
].map(([key, run]) => {
|
||||
return {
|
||||
key,
|
||||
run,
|
||||
preventDefault: true,
|
||||
}
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user