improve cv2

This commit is contained in:
2025-12-17 19:42:15 +01:00
parent 1aff1703ac
commit a403bf1ac0
9 changed files with 232 additions and 103 deletions

View File

@@ -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",

25
pnpm-lock.yaml generated
View File

@@ -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': {}

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { actionTooltip } from "$lib/title";
let {
onchange,
value,
variant,
}: {
value: boolean;
variant: "start" | "end";
onchange: (
event: Event & { currentTarget: EventTarget & HTMLInputElement },
) => void;
} = $props();
</script>
{#snippet tooltip()}
{#if value}
{#if variant === "start"}
<b>Remove</b> preceding space
{:else}
<b>Add</b> trailing space
{/if}
{:else if variant === "start"}
<b>Keep</b> preceding space
{:else}
<b>Add</b> trailing space
{/if}
{/snippet}
<label class="autospace" {@attach actionTooltip(tooltip)}
><span class="icon">space_bar</span><input
checked={!value}
{onchange}
type="checkbox"
/></label
>
<style lang="scss">
label.autospace {
display: inline-flex;
vertical-align: middle;
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: var(--auto-space-show, 0);
}
}
</style>

View File

@@ -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);
}
}
}

View File

@@ -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 = "&emsp;⇛&emsp;";
element.style.scale = "1.8";
element.style.opacity = "0.5";
return element;
if (!this.element) {
this.element = document.createElement("span");
this.element.innerHTML =
"&emsp;⇛" + (this.hasConcatenator ? "" : "&emsp;");
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: "<JOIN>",
},
selection: { anchor: delimNode.to + "<JOIN>".length },
});
}
},
},
},
);

View File

@@ -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;
}
}

View File

@@ -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)}
<kbd
class:in-text={inText}
class:icon={!!info.icon}
class:left={info.variant === "left"}
class:right={info.variant === "right"}
@@ -83,7 +85,7 @@
{#if !info.icon && dynamicMapping?.length === 1}
<span
{@attach hasPopover ? actionTooltip(popover) : null}
class:in-text={display === "inline-text"}
class:in-text={inText}
class:error={info.code > 1023}
class:warn={!retrievedInfo}
class:left={info.variant === "left"}
@@ -92,7 +94,7 @@
{:else if !info.icon && info.id?.length === 1}
<span
{@attach hasPopover ? actionTooltip(popover) : null}
class:in-text={display === "inline-text"}
class:in-text={inText}
class:error={info.code > 1023}
class:warn={!retrievedInfo}
class:left={info.variant === "left"}
@@ -101,7 +103,7 @@
{:else}
<kbd
class="inline-kbd"
class:in-text={display === "inline-text"}
class:in-text={inText}
class:left={info.variant === "left"}
class:right={info.variant === "right"}
class:icon={!!info.icon}

View File

@@ -12,6 +12,7 @@
import { action, actionTooltip } from "$lib/title";
import semverGte from "semver/functions/gte";
import Action from "$lib/components/Action.svelte";
import AutospaceSelector from "$lib/chord-editor/AutospaceSelector.svelte";
let { chord }: { chord: ChordInfo } = $props();
@@ -208,36 +209,27 @@
}}
>
{#if supportsAutospace}
{#snippet tooltip()}
{#if chord.phrase[0] === JOIN_ACTION}
<b>Remove</b> preceding space
{:else}
<b>Keep</b> preceding space
{/if}
{/snippet}
<label class="auto-space-edit" {@attach actionTooltip(tooltip)}
><span class="icon">space_bar</span><input
checked={chord.phrase[0] !== JOIN_ACTION}
onchange={async (event) => {
const autospace = hasAutospace;
if ((event.target as HTMLInputElement).checked) {
if (chord.phrase[0] === JOIN_ACTION) {
deleteAction(0, 1);
await tick();
moveCursor(cursorPosition - 1, true);
}
} else {
if (chord.phrase[0] !== JOIN_ACTION) {
insertAction(0, JOIN_ACTION);
moveCursor(cursorPosition + 1, true);
}
<AutospaceSelector
variant="start"
value={chord.phrase[0] === JOIN_ACTION}
onchange={async (event) => {
const autospace = hasAutospace;
if ((event.target as HTMLInputElement).checked) {
if (chord.phrase[0] === JOIN_ACTION) {
deleteAction(0, 1);
await tick();
moveCursor(cursorPosition - 1, true);
}
await tick();
resolveAutospace(autospace);
}}
type="checkbox"
/></label
>
} else {
if (chord.phrase[0] !== JOIN_ACTION) {
insertAction(0, JOIN_ACTION);
moveCursor(cursorPosition + 1, true);
}
}
await tick();
resolveAutospace(autospace);
}}
/>
{/if}
<div
onkeydown={keypress}
@@ -268,21 +260,12 @@
{/each}
</div>
{#if supportsAutospace}
{#snippet tooltip()}
{#if hasAutospace}
<b>Add</b> trailing space
{:else}
<b>Don't add</b> trailing space
{/if}
{/snippet}
<label class="auto-space-edit" {@attach actionTooltip(tooltip)}
><span class="icon">space_bar</span><input
checked={hasAutospace}
onchange={(event) =>
resolveAutospace((event.target as HTMLInputElement).checked)}
type="checkbox"
/></label
>
<AutospaceSelector
variant="end"
value={!hasAutospace}
onchange={(event) =>
resolveAutospace((event.target as HTMLInputElement).checked)}
/>
{/if}
<sup></sup>
</div>
@@ -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 {

View File

@@ -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(