Skip to content

Commit

Permalink
Add key bindings for inserting new blocks at the end/top of the buffe…
Browse files Browse the repository at this point in the history
…r, as well as before the current block (#85)

* Add functionality to insert new block after the last block

- Update key bindings in `initial-content.ts` to include `Alt + Enter` for adding a new block after the last block.
- Implement `getLastNoteBlock` function in `block.js` to retrieve the last block in the note.
- Add `addNewBlockAfterLast` command in `commands.js` to handle the insertion of a new block after the last one.
- Integrate `addNewBlockAfterLast` command into the keymap in `keymap.js`.

* Add block insertion before/after current, before first and after last. Also, tests.

- Added `getFirstNoteBlock` in `block.js` for accessing the first text block.
- Implemented new functions in `commands.js` like `addNewBlockBeforeCurrent` and `addNewBlockBeforeFirst`.
- Updated `keymap.js` with new key bindings to facilitate block creation.
- Introduced `block-creation.spec.js` for testing the new block manipulation features.

* Fix visual bug when inserting new block at the top

* Update help text and Readme

* Fix wrong cursor position after inserting new blocks at the top of the buffer, when the previous first block's delimiter is long (e.g. Markdown)

* Make RegEx more generic

* Fix import

* Auto-generate the README.md and initial-content documentation

- Add a documentation generator
- Add an option to force the initial content to be erased with an env variable

* Add more specific tests

* Fix Mod key on Mac in test

---------

Co-authored-by: Jonatan Heyman <[email protected]>
  • Loading branch information
flolbr and heyman authored Jan 4, 2024
1 parent b802304 commit d0d8f87
Show file tree
Hide file tree
Showing 15 changed files with 290 additions and 54 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ I can totally see the usefulness of such a feature, and it's definitely somethin
**On Mac**

```
⌥ + Shift + Enter Add new block at the start of the buffer
⌘ + Shift + Enter Add new block at the end of the buffer
⌥ + Enter Add new block before the current block
⌘ + Enter Add new block below the current block
⌘ + Shift + Enter Split the current block at cursor position
⌘ + + Enter Split the current block at cursor position
⌘ + L Change block language
⌘ + Down Goto next block
⌘ + Up Goto previous block
Expand All @@ -110,8 +113,11 @@ I can totally see the usefulness of such a feature, and it's definitely somethin
**On Windows and Linux**

```
Alt + Shift + Enter Add new block at the start of the buffer
Ctrl + Shift + Enter Add new block at the end of the buffer
Alt + Enter Add new block before the current block
Ctrl + Enter Add new block below the current block
Ctrl + Shift + Enter Split the current block at cursor position
Ctrl + Alt + Enter Split the current block at cursor position
Ctrl + L Change block language
Ctrl + Down Goto next block
Ctrl + Up Goto previous block
Expand Down
2 changes: 1 addition & 1 deletion electron/detect-platform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const os = require('os');
import os from 'os';

export const isDev = !!process.env.VITE_DEV_SERVER_URL

Expand Down
32 changes: 7 additions & 25 deletions electron/initial-content.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import { isLinux, isMac, isWindows } from "./detect-platform.js"

const modChar = isMac ? "⌘" : "Ctrl"
const altChar = isMac ? "⌥" : "Alt"

const keyHelp = [
[`${modChar} + Enter`, "Add new block below the current block"],
[`${modChar} + Shift + Enter`, "Split the current block at cursor position"],
[`${modChar} + L`, "Change block language"],
[`${modChar} + Down`, "Goto next block"],
[`${modChar} + Up`, "Goto previous block"],
[`${modChar} + A`, "Select all text in a note block. Press again to select the whole buffer"],
[`${modChar} + ${altChar} + Up/Down`, "Add additional cursor above/below"],
[`${altChar} + Shift + F`, "Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)"],
]
if (isWindows || isLinux) {
keyHelp.push([altChar, "Show menu"])
}

const keyMaxLength = keyHelp.map(([key, help]) => key.length).reduce((a, b) => Math.max(a, b))
const keyHelpStr = keyHelp.map(([key, help]) => `${key.padEnd(keyMaxLength)} ${help}`).join("\n")
import os from "os";
import { keyHelpStr } from "../shared-utils/key-helper";

export const eraseInitialContent = !!process.env.ERASE_INITIAL_CONTENT

export const initialContent = `
∞∞∞text
∞∞∞markdown
Welcome to Heynote! 👋
${keyHelpStr}
${keyHelpStr(os.platform())}
∞∞∞math
This is a Math block. Here, rows are evaluated as math expressions.
Expand Down Expand Up @@ -54,13 +36,13 @@ export const initialDevContent = initialContent + `
def my_func():
print("hejsan")
∞∞∞javascript-a
import {basicSetup} from "codemirror"
import {EditorView, keymap} from "@codemirror/view"
import {javascript} from "@codemirror/lang-javascript"
import {indentWithTab, insertTab, indentLess, indentMore} from "@codemirror/commands"
import {nord} from "./nord.mjs"
∞∞∞javascript-a
let editor = new EditorView({
//extensions: [basicSetup, javascript()],
extensions: [
Expand Down
21 changes: 10 additions & 11 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { app, BrowserWindow, Tray, shell, ipcMain, Menu, nativeTheme, globalShortcut, nativeImage } from 'electron'
import { release } from 'node:os'
import { join } from 'node:path'
import * as jetpack from "fs-jetpack";

import { menu, getTrayMenu } from './menu'
import { initialContent, initialDevContent } from '../initial-content'
import { eraseInitialContent, initialContent, initialDevContent } from '../initial-content'
import { WINDOW_CLOSE_EVENT, SETTINGS_CHANGE_EVENT } from '../constants';
import CONFIG from "../config"
import { onBeforeInputEvent } from "../keymap"
Expand Down Expand Up @@ -127,7 +126,7 @@ 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})
Expand All @@ -139,11 +138,11 @@ async function createWindow() {
})

// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
win.webContents.setWindowOpenHandler(({url}) => {
if (url.startsWith('https:') || url.startsWith('http:')) {
shell.openExternal(url)
}
return { action: 'deny' }
return {action: 'deny'}
})

fixElectronCors(win)
Expand Down Expand Up @@ -253,25 +252,25 @@ ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)


const buffer = new Buffer({
filePath: getBufferFilePath(),
filePath: getBufferFilePath(),
onChange: (eventData) => {
win?.webContents.send("buffer-content:change", eventData)
},
})

ipcMain.handle('buffer-content:load', async () => {
if (buffer.exists()) {
if (buffer.exists() && !(eraseInitialContent && isDev)) {
return await buffer.load()
} else {
return isDev? initialDevContent : initialContent
return isDev ? initialDevContent : initialContent
}
});

async function save(content) {
return await buffer.save(content)
}

ipcMain.handle('buffer-content:save', async (event, content) =>  {
ipcMain.handle('buffer-content:save', async (event, content) => {
return await save(content)
});

Expand All @@ -281,7 +280,7 @@ ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => {
app.quit()
})

ipcMain.handle('settings:set', (event, settings) => {
ipcMain.handle('settings:set', (event, settings) => {
if (settings.keymap !== CONFIG.get("settings.keymap")) {
currentKeymap = settings.keymap
}
Expand All @@ -291,7 +290,7 @@ ipcMain.handle('settings:set', (event, settings) => {
CONFIG.set("settings", settings)

win?.webContents.send(SETTINGS_CHANGE_EVENT, settings)

if (globalHotkeyChanged) {
registerGlobalHotkey()
}
Expand Down
25 changes: 25 additions & 0 deletions shared-utils/key-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const keyHelpStr = (platform: string) => {
const modChar = platform === "darwin" ? "⌘" : "Ctrl"
const altChar = platform === "darwin" ? "⌥" : "Alt"

const keyHelp = [
[`${altChar} + Shift + Enter`, "Add new block at the start of the buffer"],
[`${modChar} + Shift + Enter`, "Add new block at the end of the buffer"],
[`${altChar} + Enter`, "Add new block before the current block"],
[`${modChar} + Enter`, "Add new block below the current block"],
[`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"],
[`${modChar} + L`, "Change block language"],
[`${modChar} + Down`, "Goto next block"],
[`${modChar} + Up`, "Goto previous block"],
[`${modChar} + A`, "Select all text in a note block. Press again to select the whole buffer"],
[`${modChar} + ${altChar} + Up/Down`, "Add additional cursor above/below"],
[`${altChar} + Shift + F`, "Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)"],
]

if (platform === "win32" || platform === "linux") {
keyHelp.push([altChar, "Show menu"])
}
const keyMaxLength = keyHelp.map(([key]) => key.length).reduce((a, b) => Math.max(a, b))

return keyHelp.map(([key, help]) => `${key.padEnd(keyMaxLength)} ${help}`).join("\n")
}
2 changes: 1 addition & 1 deletion src/editor/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export const heynoteEvent = Annotation.define()
export const LANGUAGE_CHANGE = "heynote-change"
export const CURRENCIES_LOADED = "heynote-currencies-loaded"
export const SET_CONTENT = "heynote-set-content"

export const ADD_NEW_BLOCK = "heynote-add-new-block"
13 changes: 10 additions & 3 deletions src/editor/block/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ export function getActiveNoteBlock(state) {
return state.facet(blockState).find(block => block.range.from <= range.head && block.range.to >= range.head)
}

export function getFirstNoteBlock(state) {
return state.facet(blockState)[0]
}

export function getLastNoteBlock(state) {
return state.facet(blockState)[state.facet(blockState).length - 1]
}

export function getNoteBlockFromPos(state, pos) {
return state.facet(blockState).find(block => block.range.from <= pos && block.range.to >= pos)
}
Expand All @@ -86,8 +94,7 @@ class NoteBlockStart extends WidgetType {
this.isFirst = isFirst
}
eq(other) {
//return other.checked == this.checked
return true
return this.isFirst === other.isFirst
}
toDOM() {
let wrap = document.createElement("div")
Expand Down Expand Up @@ -249,7 +256,7 @@ const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr) => {
* Transaction filter to prevent the selection from being before the first block
*/
const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr) => {
if (!firstBlockDelimiterSize) {
if (!firstBlockDelimiterSize || tr.annotations.some(a => a.type === heynoteEvent)) {
return tr
}
tr?.selection?.ranges.forEach(range => {
Expand Down
77 changes: 70 additions & 7 deletions src/editor/block/commands.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EditorSelection } from "@codemirror/state"
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED } from "../annotation.js";
import { blockState, getActiveNoteBlock, getNoteBlockFromPos } from "./block"
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK } from "../annotation.js";
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block"
import { moveLineDown, moveLineUp } from "./move-lines.js";
import { selectAll } from "./select-all.js";

Expand All @@ -10,27 +10,50 @@ export { moveLineDown, moveLineUp, selectAll }
export const insertNewBlockAtCursor = ({ state, dispatch }) => {
if (state.readOnly)
return false

const currentBlock = getActiveNoteBlock(state)
let delimText;
if (currentBlock) {
delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}\n`
} else {
delimText = "\n∞∞∞text-a\n"
}
dispatch(state.replaceSelection(delimText),
dispatch(state.replaceSelection(delimText),
{
scrollIntoView: true,
scrollIntoView: true,
userEvent: "input",
}
)

return true;
}

export const addNewBlockBeforeCurrent = ({ state, dispatch }) => {
console.log("addNewBlockBeforeCurrent")
if (state.readOnly)
return false

const block = getActiveNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"

dispatch(state.update({
changes: {
from: block.delimiter.from,
insert: delimText,
},
selection: EditorSelection.cursor(block.delimiter.from + delimText.length),
annotations: [heynoteEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
}))
return true;
}

export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
if (state.readOnly)
return false

const block = getActiveNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"

Expand All @@ -41,7 +64,47 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
},
selection: EditorSelection.cursor(block.content.to + delimText.length)
}, {
scrollIntoView: true,
scrollIntoView: true,
userEvent: "input",
}))
return true;
}

export const addNewBlockBeforeFirst = ({ state, dispatch }) => {
if (state.readOnly)
return false

const block = getFirstNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"

dispatch(state.update({
changes: {
from: block.delimiter.from,
insert: delimText,
},
selection: EditorSelection.cursor(delimText.length),
annotations: [heynoteEvent.of(ADD_NEW_BLOCK)],
}, {
scrollIntoView: true,
userEvent: "input",
}))
return true;
}

export const addNewBlockAfterLast = ({ state, dispatch }) => {
if (state.readOnly)
return false
const block = getLastNoteBlock(state)
const delimText = "\n∞∞∞text-a\n"

dispatch(state.update({
changes: {
from: block.content.to,
insert: delimText,
},
selection: EditorSelection.cursor(block.content.to + delimText.length)
}, {
scrollIntoView: true,
userEvent: "input",
}))
return true;
Expand All @@ -50,7 +113,7 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
export function changeLanguageTo(state, dispatch, block, language, auto) {
if (state.readOnly)
return false
const delimRegex = /^\n∞∞∞[a-z]{0,16}(-a)?\n/g
const delimRegex = /^\n∞∞∞[a-z]+?(-a)?\n/g
if (state.doc.sliceString(block.delimiter.from, block.delimiter.to).match(delimRegex)) {
//console.log("changing language to", language)
dispatch(state.update({
Expand Down
4 changes: 4 additions & 0 deletions src/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export class HeynoteEditor {
return this.view.state.facet(blockState)
}

getCursorPosition() {
return this.view.state.selection.main.head
}

focus() {
this.view.focus()
}
Expand Down
8 changes: 6 additions & 2 deletions src/editor/keymap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {

import {
insertNewBlockAtCursor,
addNewBlockAfterCurrent,
addNewBlockBeforeCurrent, addNewBlockAfterCurrent,
addNewBlockBeforeFirst, addNewBlockAfterLast,
moveLineUp, moveLineDown,
selectAll,
gotoPreviousBlock, gotoNextBlock,
Expand Down Expand Up @@ -38,8 +39,11 @@ export function heynoteKeymap(editor) {
return keymapFromSpec([
["Tab", indentMore],
["Shift-Tab", indentLess],
["Alt-Shift-Enter", addNewBlockBeforeFirst],
["Mod-Shift-Enter", addNewBlockAfterLast],
["Alt-Enter", addNewBlockBeforeCurrent],
["Mod-Enter", addNewBlockAfterCurrent],
["Mod-Shift-Enter", insertNewBlockAtCursor],
["Mod-Alt-Enter", insertNewBlockAtCursor],
["Mod-a", selectAll],
["Alt-ArrowUp", moveLineUp],
["Alt-ArrowDown", moveLineDown],
Expand Down
Loading

0 comments on commit d0d8f87

Please sign in to comment.