diff --git a/src/editor/block/block.js b/src/editor/block/block.js index 8de117f..f930b72 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -8,6 +8,7 @@ import { IterMode } from "@lezer/common"; import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js"; import { SelectionChangeEvent } from "../event.js" import { mathBlock } from "./math.js" +import { emptyBlockSelected } from "./select-all.js"; // tracks the size of the first delimiter @@ -325,5 +326,6 @@ export const noteBlockExtension = (editor) => { preventSelectionBeforeFirstBlock, emitCursorChange(editor), mathBlock, + emptyBlockSelected, ] } diff --git a/src/editor/block/commands.js b/src/editor/block/commands.js index 1a17bff..c52d5fa 100644 --- a/src/editor/block/commands.js +++ b/src/editor/block/commands.js @@ -1,13 +1,10 @@ import { EditorSelection } from "@codemirror/state" -import { - selectAll as defaultSelectAll, - moveLineUp as defaultMoveLineUp, -} from "@codemirror/commands" import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED } from "../annotation.js"; import { blockState, getActiveNoteBlock, getNoteBlockFromPos } from "./block" import { moveLineDown, moveLineUp } from "./move-lines.js"; +import { selectAll } from "./select-all.js"; -export { moveLineDown, moveLineUp } +export { moveLineDown, moveLineUp, selectAll } export const insertNewBlockAtCursor = ({ state, dispatch }) => { @@ -50,37 +47,6 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => { return true; } -export const selectAll = ({ state, dispatch }) => { - const range = state.selection.asSingle().ranges[0] - const block = getActiveNoteBlock(state) - - // handle empty blocks separately - if (block.content.from === block.content.to) { - // check if C-a has already been pressed - if (range.from === block.content.from-1 && range.to === block.content.to) { - return defaultSelectAll({state, dispatch}) - } - dispatch(state.update({ - selection: {anchor: block.content.from-1, head: block.content.to}, - userEvent: "select" - })) - return true - } - - // check if all the text of the note is already selected, in which case we want to select all the text of the whole document - if (range.from === block.content.from && range.to === block.content.to) { - return defaultSelectAll({state, dispatch}) - } - - dispatch(state.update({ - selection: {anchor: block.content.from, head: block.content.to}, - userEvent: "select" - })) - - return true -} - - export function changeLanguageTo(state, dispatch, block, language, auto) { if (state.readOnly) return false diff --git a/src/editor/block/select-all.js b/src/editor/block/select-all.js new file mode 100644 index 0000000..2aef87b --- /dev/null +++ b/src/editor/block/select-all.js @@ -0,0 +1,102 @@ +import { ViewPlugin, Decoration } from "@codemirror/view" +import { StateField, StateEffect, RangeSetBuilder } from "@codemirror/state" +import { selectAll as defaultSelectAll } from "@codemirror/commands" + +import { getActiveNoteBlock } from "./block" + + + +/** + * When the user presses C-a, we want to first select the whole block. But if the whole block is already selected, + * we want to instead select the whole document. This doesn't work for empty block, since the whole block is already + * selected (since it's empty). Therefore we use a StateField to keep track of whether the empty block is selected, + * and add a manual line decoration to visually indicate that the empty block is selected. + */ + + +export const emptyBlockSelected = StateField.define({ + create: () => { + return null + }, + update(value, tr) { + if (tr.selection) { + // if selection changes, reset the state + return null + } else { + for (let e of tr.effects) { + if (e.is(setEmptyBlockSelected)) { + // toggle the state to true + return e.value + } + } + } + }, + provide() { + return ViewPlugin.fromClass(class { + constructor(view) { + this.decorations = emptyBlockSelectedDecorations(view) + } + update(update) { + this.decorations = emptyBlockSelectedDecorations(update.view) + } + }, { + decorations: v => v.decorations + }) + } +}) + +/** + * Effect that can be dispatched to set the empty block selected state + */ + const setEmptyBlockSelected = StateEffect.define() + +const decoration = Decoration.line({ + attributes: {class: "heynote-empty-block-selected"} +}) +function emptyBlockSelectedDecorations(view) { + const selectionPos = view.state.field(emptyBlockSelected) + const builder = new RangeSetBuilder() + if (selectionPos) { + const line = view.state.doc.lineAt(selectionPos) + builder.add(line.from, line.from, decoration) + } + return builder.finish() +} + + +export const selectAll = ({ state, dispatch }) => { + const range = state.selection.asSingle().ranges[0] + const block = getActiveNoteBlock(state) + + // handle empty blocks separately + if (block.content.from === block.content.to) { + // check if C-a has already been pressed, + if (state.field(emptyBlockSelected)) { + // if the active block is already marked as selected we want to select the whole buffer + return defaultSelectAll({state, dispatch}) + } else if (range.empty) { + // if the empty block is not selected mark it as selected + // the reason we check for range.empty is if there is a an empty block at the end of the document + // and the users presses C-a twice so that the whole buffer gets selected, the active block will + // still be empty but we don't want to mark it as selected + dispatch({ + effects: setEmptyBlockSelected.of(block.content.from) + }) + } + return true + } + + // check if all the text of the note is already selected, in which case we want to select all the text of the whole document + if (range.from === block.content.from && range.to === block.content.to) { + return defaultSelectAll({state, dispatch}) + } + + dispatch(state.update({ + selection: {anchor: block.content.from, head: block.content.to}, + userEvent: "select" + })) + + return true +} + + diff --git a/src/editor/theme/base.js b/src/editor/theme/base.js index 45b8012..7c7f88f 100644 --- a/src/editor/theme/base.js +++ b/src/editor/theme/base.js @@ -122,5 +122,5 @@ export const heynoteBase = EditorView.theme({ }, '.heynote-link': { textDecoration: "underline", - } + }, }) diff --git a/src/editor/theme/dark.js b/src/editor/theme/dark.js index f0589fd..542fc92 100644 --- a/src/editor/theme/dark.js +++ b/src/editor/theme/dark.js @@ -29,8 +29,8 @@ const highlightBackground = 'rgba(255,255,255,0.04)'; const lineNumberColor = 'rgba(255,255,255, 0.15)'; const commentColor = '#888d97'; const matchingBracket = 'rgba(255,255,255,0.1)'; -const selection = "#0865a9"; -const selectionBlur = "#225377"; +const selection = "#0865a9aa"; +const selectionBlur = "#225377aa"; const darkTheme = EditorView.theme({ @@ -48,6 +48,9 @@ const darkTheme = EditorView.theme({ '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { backgroundColor: selection, }, + '.cm-activeLine.heynote-empty-block-selected': { + "background-color": selection, + }, '.cm-panels': { backgroundColor: "#474747", color: "#9c9c9c", diff --git a/src/editor/theme/light.js b/src/editor/theme/light.js index dfbe3ac..5baf85e 100644 --- a/src/editor/theme/light.js +++ b/src/editor/theme/light.js @@ -49,6 +49,9 @@ const lightTheme = EditorView.theme({ "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground": { background: selection, }, + '.cm-activeLine.heynote-empty-block-selected': { + "background-color": selection, + }, ".heynote-blocks-layer .block-even": { background: "#ffffff",