From a403bf1ac0923390eedc7e719e6b0597c3c86469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 17 Dec 2025 19:42:15 +0100 Subject: [PATCH] improve cv2 --- package.json | 1 + pnpm-lock.yaml | 25 ++--- src/lib/chord-editor/AutospaceSelector.svelte | 54 +++++++++++ src/lib/chord-editor/action-plugin.ts | 24 +++-- src/lib/chord-editor/chord-delim-plugin.ts | 95 ++++++++++++++++-- .../concatenator-button.module.scss | 13 +++ src/lib/components/Action.svelte | 12 ++- .../config/chords/ChordPhraseEdit.svelte | 97 +++++++------------ src/routes/(app)/config/cv2/+page.svelte | 14 ++- 9 files changed, 232 insertions(+), 103 deletions(-) create mode 100644 src/lib/chord-editor/AutospaceSelector.svelte create mode 100644 src/lib/chord-editor/concatenator-button.module.scss diff --git a/package.json b/package.json index 0363f1be..cc98f6fa 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@codemirror/view": "^6.38.1", "@fontsource-variable/material-symbols-rounded": "^5.2.17", "@fontsource-variable/noto-sans-mono": "^5.2.7", + "@lezer/common": "^1.4.0", "@lezer/generator": "^1.8.0", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 952fee69..d447fc22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@fontsource-variable/noto-sans-mono': specifier: ^5.2.7 version: 5.2.7 + '@lezer/common': + specifier: ^1.4.0 + version: 1.4.0 '@lezer/generator': specifier: ^1.8.0 version: 1.8.0 @@ -1084,8 +1087,8 @@ packages: '@keyv/serialize@1.1.0': resolution: {integrity: sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==} - '@lezer/common@1.2.1': - resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + '@lezer/common@1.4.0': + resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} '@lezer/generator@1.8.0': resolution: {integrity: sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==} @@ -5148,14 +5151,14 @@ snapshots: '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@codemirror/commands@6.8.1': dependencies: '@codemirror/language': 6.11.2 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@codemirror/lang-javascript@6.2.4': dependencies: @@ -5164,14 +5167,14 @@ snapshots: '@codemirror/lint': 6.8.1 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@lezer/javascript': 1.4.17 '@codemirror/language@6.11.2': dependencies: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.1 - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.5 style-mod: 4.1.2 @@ -5407,26 +5410,26 @@ snapshots: '@keyv/serialize@1.1.0': {} - '@lezer/common@1.2.1': {} + '@lezer/common@1.4.0': {} '@lezer/generator@1.8.0': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@lezer/lr': 1.4.5 '@lezer/highlight@1.2.1': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@lezer/javascript@1.4.17': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.5 '@lezer/lr@1.4.5': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.4.0 '@marijn/find-cluster-break@1.0.2': {} diff --git a/src/lib/chord-editor/AutospaceSelector.svelte b/src/lib/chord-editor/AutospaceSelector.svelte new file mode 100644 index 00000000..30613721 --- /dev/null +++ b/src/lib/chord-editor/AutospaceSelector.svelte @@ -0,0 +1,54 @@ + + +{#snippet tooltip()} + {#if value} + {#if variant === "start"} + Remove preceding space + {:else} + Add trailing space + {/if} + {:else if variant === "start"} + Keep preceding space + {:else} + Add trailing space + {/if} +{/snippet} + + + diff --git a/src/lib/chord-editor/action-plugin.ts b/src/lib/chord-editor/action-plugin.ts index 0514097f..cbf8a10a 100644 --- a/src/lib/chord-editor/action-plugin.ts +++ b/src/lib/chord-editor/action-plugin.ts @@ -11,19 +11,12 @@ import { syntaxTree } from "@codemirror/language"; import type { Range } from "@codemirror/state"; export class ActionWidget extends WidgetType { - component: {}; - element: HTMLElement; + component?: {}; + element?: HTMLElement; constructor(readonly id: string | number) { super(); this.id = id; - this.element = document.createElement("span"); - this.element.style.paddingInline = "2px"; - - this.component = mount(Action, { - target: this.element, - props: { action: id, display: "keys" }, - }); } override eq(other: ActionWidget) { @@ -31,6 +24,15 @@ export class ActionWidget extends WidgetType { } toDOM() { + if (!this.element) { + this.element = document.createElement("span"); + this.element.style.paddingInline = "2px"; + + this.component = mount(Action, { + target: this.element, + props: { action: this.id, display: "keys", inText: true }, + }); + } return this.element; } @@ -39,7 +41,9 @@ export class ActionWidget extends WidgetType { } override destroy() { - unmount(this.component); + if (this.component) { + unmount(this.component); + } } } diff --git a/src/lib/chord-editor/chord-delim-plugin.ts b/src/lib/chord-editor/chord-delim-plugin.ts index 1cdb774b..089d35b4 100644 --- a/src/lib/chord-editor/chord-delim-plugin.ts +++ b/src/lib/chord-editor/chord-delim-plugin.ts @@ -7,29 +7,71 @@ import { } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; import type { Range } from "@codemirror/state"; +import { mount, unmount } from "svelte"; +import Action from "../components/Action.svelte"; +import type { SyntaxNodeRef } from "@lezer/common"; +import classNames from "./concatenator-button.module.scss"; export class DelimWidget extends WidgetType { - constructor() { + component?: {}; + element?: HTMLElement; + + constructor(readonly hasConcatenator: boolean) { super(); } override eq(other: DelimWidget) { - return true; + return this.hasConcatenator == other.hasConcatenator; } toDOM() { - const element = document.createElement("span"); - element.innerHTML = " ⇛ "; - element.style.scale = "1.8"; - element.style.opacity = "0.5"; - return element; + if (!this.element) { + this.element = document.createElement("span"); + this.element.innerHTML = + " ⇛" + (this.hasConcatenator ? "" : " "); + this.element.style.scale = "1.8"; + this.element.style.color = + "color-mix(in srgb, currentColor 50%, transparent)"; + + if (this.hasConcatenator) { + const button = document.createElement("button"); + button.className = classNames["concatenator-button"]!; + this.component = mount(Action, { + target: button, + props: { action: 574, display: "keys", inText: true, ghost: true }, + }); + this.element.appendChild(button); + } + } + return this.element; } override ignoreEvent() { return false; } - override destroy() {} + override destroy() { + if (this.component) { + unmount(this.component); + } + } +} + +function getJoinNode( + view: EditorView, + phraseDelimNode: SyntaxNodeRef, +): SyntaxNodeRef | null | undefined { + const firstPhraseAction = phraseDelimNode.node.nextSibling + ?.getChild("ActionString") + ?.node.firstChild?.node.getChild("ExplicitAction"); + const idNode = firstPhraseAction?.node.getChild("ActionId"); + const actionId = idNode + ? view.state.doc.sliceString(idNode.from, idNode.to) + : null; + const isJoinAction = + actionId === "JOIN" && + !!firstPhraseAction!.node.getChild("ExplicitDelimEnd"); + return isJoinAction ? firstPhraseAction : null; } function actionWidgets(view: EditorView) { @@ -40,8 +82,10 @@ function actionWidgets(view: EditorView) { to, enter: (node) => { if (node.name !== "PhraseDelim") return; + const joinNode = getJoinNode(view, node); + let deco = Decoration.replace({ - widget: new DelimWidget(), + widget: new DelimWidget(!joinNode), }); widgets.push(deco.range(node.from, node.to)); }, @@ -76,5 +120,38 @@ export const delimPlugin = ViewPlugin.fromClass( (view) => view.plugin(plugin)?.decorations ?? Decoration.none, ); }, + eventHandlers: { + click: (event, view) => { + if (!(event.target instanceof HTMLElement)) return; + if ( + !( + event.target instanceof HTMLButtonElement || + (event.target as HTMLElement).parentElement instanceof + HTMLButtonElement + ) + ) + return; + + const chordNode = syntaxTree(view.state).resolve( + view.posAtDOM(event.target), + ); + const delimNode = ( + chordNode.name === "ActionString" + ? chordNode.parent?.parent + : chordNode + )?.getChild("PhraseDelim"); + if (!delimNode) return; + const joinNode = getJoinNode(view, delimNode); + if (!event.target.checked && !joinNode) { + view.dispatch({ + changes: { + from: delimNode.to, + insert: "", + }, + selection: { anchor: delimNode.to + "".length }, + }); + } + }, + }, }, ); diff --git a/src/lib/chord-editor/concatenator-button.module.scss b/src/lib/chord-editor/concatenator-button.module.scss new file mode 100644 index 00000000..a8f00e1e --- /dev/null +++ b/src/lib/chord-editor/concatenator-button.module.scss @@ -0,0 +1,13 @@ +.concatenator-button { + display: inline; + opacity: calc(var(--auto-space-show, 0) * 0.7); + margin: 0; + padding: 4px; + height: auto; + + > :global(kbd) { + outline: 1px dashed var(--md-sys-color-outline); + outline-offset: -1px; + background: none; + } +} diff --git a/src/lib/components/Action.svelte b/src/lib/components/Action.svelte index 385acb75..2977fd88 100644 --- a/src/lib/components/Action.svelte +++ b/src/lib/components/Action.svelte @@ -2,16 +2,17 @@ import { KEYMAP_CODES, KEYMAP_IDS } from "$lib/serial/keymap-codes"; import type { KeyInfo } from "$lib/serial/keymap-codes"; import { osLayout } from "$lib/os-layout"; - import { tooltip } from "$lib/hover-popover"; import { isVerbose } from "./verbose-action"; import { actionTooltip } from "$lib/title"; let { action, display, + inText = false, }: { action: string | number | KeyInfo; - display: "inline-text" | "inline-keys" | "keys" | "verbose"; + display: "inline-keys" | "keys" | "verbose"; + inText?: boolean; } = $props(); let retrievedInfo = $derived( @@ -69,6 +70,7 @@ {/snippet} {#snippet kbdSnippet(withPopover = true)} 1023} class:warn={!retrievedInfo} class:left={info.variant === "left"} @@ -92,7 +94,7 @@ {:else if !info.icon && info.id?.length === 1} 1023} class:warn={!retrievedInfo} class:left={info.variant === "left"} @@ -101,7 +103,7 @@ {:else} {#if supportsAutospace} - {#snippet tooltip()} - {#if chord.phrase[0] === JOIN_ACTION} - Remove preceding space - {:else} - Keep preceding space - {/if} - {/snippet} - + } else { + if (chord.phrase[0] !== JOIN_ACTION) { + insertAction(0, JOIN_ACTION); + moveCursor(cursorPosition + 1, true); + } + } + await tick(); + resolveAutospace(autospace); + }} + /> {/if}
{#if supportsAutospace} - {#snippet tooltip()} - {#if hasAutospace} - Add trailing space - {:else} - Don't add trailing space - {/if} - {/snippet} - + + resolveAutospace((event.target as HTMLInputElement).checked)} + /> {/if}
@@ -330,24 +313,6 @@ } } - .auto-space-edit { - margin-inline: 8px; - border-radius: 4px; - background: var(--md-sys-color-tertiary-container); - padding-inline: 0; - height: 1em; - color: var(--md-sys-color-on-tertiary-container); - font-size: 1.3em; - - &:has(:checked) { - opacity: 0; - } - } - - .wrapper:hover .auto-space-edit { - opacity: 1; - } - .wrapper { display: flex; @@ -380,8 +345,12 @@ transition-duration: 250ms; } - &:hover::before { - opacity: 0.3; + &:hover { + --auto-space-show: 1; + + &::before { + opacity: 0.3; + } } &:has(> :focus-within)::after { diff --git a/src/routes/(app)/config/cv2/+page.svelte b/src/routes/(app)/config/cv2/+page.svelte index 77439fe9..26cd3e28 100644 --- a/src/routes/(app)/config/cv2/+page.svelte +++ b/src/routes/(app)/config/cv2/+page.svelte @@ -26,15 +26,16 @@ const showEdits = persistentWritable("chord-editor-show-edits", true); let originalDoc = $derived( $chords - .map( - (chord) => + .map((chord) => { + return ( chord.actions .filter((it) => it !== 0) .map((it) => actionToValue(it)) .join("") + "=>" + - chord.phrase.map((it) => actionToValue(it)).join(""), - ) + chord.phrase.map((it) => actionToValue(it)).join("") + ); + }) .join("\n"), ); let editor: HTMLDivElement | undefined = $state(undefined); @@ -175,6 +176,11 @@ ) !important; } + :global(.cm-activeLine), + :global(.cm-line:hover) { + --auto-space-show: 1; + } + :global(.cm-activeLine) { border-bottom: 1px solid var(--md-sys-color-surface-variant); /*background-color: color-mix(