mirror of https://github.com/heyman/heynote.git
Refactor the way we handle copy/cut/paste in Emacs mode
Previously we listened for the key bindings for copy, cut and paste in the Electron main process, and triggered the event using copy(), paste() and cut() methods on win.webContent. Now this is fully handled within the renderer process using the window.navigator.clipboard API. This will make it simpler to implement fully customizable key bindings.
This commit is contained in:
parent
1b0b2d55b1
commit
957b22c70e
|
@ -1,28 +0,0 @@
|
|||
import CONFIG from "./config"
|
||||
import { isMac } from "./detect-platform"
|
||||
|
||||
|
||||
export function onBeforeInputEvent({win, event, input, currentKeymap}) {
|
||||
//console.log("keyboard event", input)
|
||||
let metaKey = "alt"
|
||||
if (isMac) {
|
||||
metaKey = CONFIG.get("settings.emacsMetaKey", "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.code === "KeyY" && input.control) {
|
||||
event.preventDefault()
|
||||
win.webContents.paste()
|
||||
} else if (input.code === "KeyW" && input.control) {
|
||||
event.preventDefault()
|
||||
win.webContents.cut()
|
||||
} else if (input.code === "KeyW" && input[metaKey]) {
|
||||
event.preventDefault()
|
||||
win.webContents.copy()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import fs from "fs"
|
|||
import { menu, getTrayMenu } from './menu'
|
||||
import { WINDOW_CLOSE_EVENT, SETTINGS_CHANGE_EVENT } from '../constants';
|
||||
import CONFIG from "../config"
|
||||
import { onBeforeInputEvent } from "../keymap"
|
||||
import { isDev, isMac, isWindows } from '../detect-platform';
|
||||
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
|
||||
import { fixElectronCors } from './cors';
|
||||
|
@ -126,11 +125,6 @@ async function createWindow() {
|
|||
//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', () => {
|
||||
win?.webContents.send('main-process-message', new Date().toLocaleString())
|
||||
|
|
|
@ -34,7 +34,12 @@ export default defineConfig({
|
|||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
contextOptions: {
|
||||
permissions: ['clipboard-read','clipboard-write'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
:development="development"
|
||||
:debugSyntaxTree="false"
|
||||
:keymap="settings.keymap"
|
||||
:emacsMetaKey="settings.emacsMetaKey"
|
||||
:showLineNumberGutter="settings.showLineNumberGutter"
|
||||
:showFoldGutter="settings.showFoldGutter"
|
||||
:bracketClosing="settings.bracketClosing"
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
type: String,
|
||||
default: "default",
|
||||
},
|
||||
emacsMetaKey: {
|
||||
type: String,
|
||||
default: "Meta",
|
||||
},
|
||||
showLineNumberGutter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -63,6 +67,7 @@
|
|||
window.heynote.buffer.save(content)
|
||||
},
|
||||
keymap: this.keymap,
|
||||
emacsMetaKey: this.emacsMetaKey,
|
||||
showLineNumberGutter: this.showLineNumberGutter,
|
||||
showFoldGutter: this.showFoldGutter,
|
||||
bracketClosing: this.bracketClosing,
|
||||
|
@ -110,8 +115,12 @@
|
|||
this.editor.setTheme(newTheme)
|
||||
},
|
||||
|
||||
keymap(keymap) {
|
||||
this.editor.setKeymap(keymap)
|
||||
keymap() {
|
||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
||||
},
|
||||
|
||||
emacsMetaKey() {
|
||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
||||
},
|
||||
|
||||
showLineNumberGutter(show) {
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
<div class="row">
|
||||
<div class="entry">
|
||||
<h2>Keymap</h2>
|
||||
<select ref="keymapSelector" v-model="keymap" @change="updateSettings">
|
||||
<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>
|
||||
|
@ -137,7 +137,7 @@
|
|||
</div>
|
||||
<div class="entry" v-if="keymap === 'emacs' && isMac">
|
||||
<h2>Meta Key</h2>
|
||||
<select v-model="metaKey" @change="updateSettings">
|
||||
<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>
|
||||
|
|
|
@ -21,7 +21,10 @@ function copiedRange(state) {
|
|||
|
||||
|
||||
|
||||
export const heynoteCopyPaste = (editor) => {
|
||||
/**
|
||||
* Set up event handlers for the browser's copy & cut events, that will replace block separators with newlines
|
||||
*/
|
||||
export const heynoteCopyCut = (editor) => {
|
||||
let copy, cut
|
||||
copy = cut = (event, view) => {
|
||||
let { text, ranges } = copiedRange(view.state)
|
||||
|
@ -60,3 +63,76 @@ export const heynoteCopyPaste = (editor) => {
|
|||
cut,
|
||||
})
|
||||
}
|
||||
|
||||
const copyCut = (view, cut, editor) => {
|
||||
let { text, ranges } = copiedRange(view.state)
|
||||
text = text.replaceAll(blockSeparatorRegex, "\n\n")
|
||||
navigator.clipboard.writeText(text)
|
||||
|
||||
if (cut && !view.state.readOnly) {
|
||||
view.dispatch({
|
||||
changes: ranges,
|
||||
scrollIntoView: true,
|
||||
userEvent: "delete.cut"
|
||||
})
|
||||
}
|
||||
|
||||
// if we're in Emacs mode, we want to exit mark mode in case we're in it
|
||||
setEmacsMarkMode(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) {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function doPaste(view, input) {
|
||||
let { state } = view, changes, i = 1, text = state.toText(input)
|
||||
let byLine = text.lines == state.selection.ranges.length
|
||||
if (byLine) {
|
||||
changes = state.changeByRange(range => {
|
||||
let line = text.line(i++)
|
||||
return {
|
||||
changes: { from: range.from, to: range.to, insert: line.text },
|
||||
range: EditorSelection.cursor(range.from + line.length)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
changes = state.replaceSelection(text)
|
||||
}
|
||||
view.dispatch(changes, {
|
||||
userEvent: "input.paste",
|
||||
scrollIntoView: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param editor Editor instance
|
||||
* @returns CodeMirror command that copies the current selection to the clipboard
|
||||
*/
|
||||
export function copyCommand(editor) {
|
||||
return (view) => copyCut(view, false, editor)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param editor Editor instance
|
||||
* @returns CodeMirror command that cuts the current selection to the clipboard
|
||||
*/
|
||||
export function cutCommand(editor) {
|
||||
return (view) => copyCut(view, true, editor)
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeMirror command that pastes the clipboard content into the editor
|
||||
*/
|
||||
export async function pasteCommand(view) {
|
||||
return doPaste(view, await navigator.clipboard.readText())
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { changeCurrentBlockLanguage, triggerCurrenciesLoaded } from "./block/com
|
|||
import { formatBlockContent } from "./block/format-code.js"
|
||||
import { heynoteKeymap } from "./keymap.js"
|
||||
import { emacsKeymap } from "./emacs.js"
|
||||
import { heynoteCopyPaste } from "./copy-paste"
|
||||
import { heynoteCopyCut } from "./copy-paste"
|
||||
import { languageDetection } from "./language-detection/autodetect.js"
|
||||
import { autoSaveContent } from "./save.js"
|
||||
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
|
||||
|
@ -40,6 +40,7 @@ export class HeynoteEditor {
|
|||
theme="light",
|
||||
saveFunction=null,
|
||||
keymap="default",
|
||||
emacsMetaKey="Meta",
|
||||
showLineNumberGutter=true,
|
||||
showFoldGutter=true,
|
||||
bracketClosing=false,
|
||||
|
@ -53,12 +54,13 @@ export class HeynoteEditor {
|
|||
this.readOnlyCompartment = new Compartment
|
||||
this.closeBracketsCompartment = new Compartment
|
||||
this.deselectOnCopy = keymap === "emacs"
|
||||
this.emacsMetaKey = emacsMetaKey
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: content || "",
|
||||
extensions: [
|
||||
this.keymapCompartment.of(getKeymapExtensions(this, keymap)),
|
||||
heynoteCopyPaste(this),
|
||||
heynoteCopyCut(this),
|
||||
|
||||
//minimalSetup,
|
||||
this.lineNumberCompartment.of(showLineNumberGutter ? [lineNumbers(), blockLineNumbers] : []),
|
||||
|
@ -159,8 +161,9 @@ export class HeynoteEditor {
|
|||
})
|
||||
}
|
||||
|
||||
setKeymap(keymap) {
|
||||
setKeymap(keymap, emacsMetaKey) {
|
||||
this.deselectOnCopy = keymap === "emacs"
|
||||
this.emacsMetaKey = emacsMetaKey
|
||||
this.view.dispatch({
|
||||
effects: this.keymapCompartment.reconfigure(getKeymapExtensions(this, keymap)),
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
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
|
||||
|
@ -60,6 +61,21 @@ function emacsSelectAll(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),
|
||||
|
@ -81,7 +97,11 @@ export function emacsKeymap(editor) {
|
|||
["Ctrl-t", transposeChars],
|
||||
["Ctrl-v", cursorPageDown],
|
||||
|
||||
{ key: "Ctrl-b", run: emacsMoveCommand(cursorCharLeft, selectCharLeft), shift: selectCharLeft, preventDefault: true },
|
||||
["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-p", run: emacsMoveCommand(cursorLineUp, selectLineUp), shift: selectLineUp },
|
||||
{ key: "Ctrl-n", run: emacsMoveCommand(cursorLineDown, selectLineDown), shift: selectLineDown },
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { HeynotePage } from "./test-utils.js";
|
||||
|
||||
let heynotePage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
heynotePage = new HeynotePage(page)
|
||||
await heynotePage.goto()
|
||||
});
|
||||
|
||||
|
||||
test("test emacs copy/pase/cut key bindings", async ({ page, browserName }) => {
|
||||
if (browserName !== "chromium") {
|
||||
// This test only works in Chromium due to accessing the clipboard
|
||||
test.skip()
|
||||
}
|
||||
await page.locator("css=.status-block.settings").click()
|
||||
//await page.locator("css=li.tab-editing").click()
|
||||
await page.locator("css=select.keymap").selectOption("emacs")
|
||||
await page.locator("css=select.metaKey").selectOption("alt")
|
||||
await page.locator("body").press("Escape")
|
||||
|
||||
await page.locator("body").pressSequentially("test")
|
||||
await page.locator("body").press(heynotePage.isMac ? "Meta+A" : "Control+A")
|
||||
await page.locator("body").press("Alt+W")
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("test")
|
||||
await page.locator("body").press("Control+Y")
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("testtest")
|
||||
|
||||
await page.locator("body").press(heynotePage.isMac ? "Meta+A" : "Control+A")
|
||||
await page.locator("body").press("Control+W")
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("")
|
||||
await page.locator("body").press("Control+Y")
|
||||
await page.locator("body").press("Control+Y")
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("testtesttesttest")
|
||||
})
|
Loading…
Reference in New Issue