From 41a1ef0dbb53dbddf1e044ffd44ff28a728f8fd0 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Mon, 17 Mar 2025 16:31:11 +0100 Subject: [PATCH] stabalized the index map, now the selection is lost on rerenders again :/ --- .vscode/launch.json | 46 +++---------- .vscode/settings.json | 8 +-- .../{context.spec.tsx => context.spec.ts} | 0 src/features/editor/context.ts | 68 +++++++++++++------ src/features/editor/editor.tsx | 12 ++-- src/features/editor/map.ts | 14 ++-- src/routes/(editor)/experimental/editor.tsx | 28 +++++--- tsconfig.json | 1 - 8 files changed, 91 insertions(+), 86 deletions(-) rename src/features/editor/{context.spec.tsx => context.spec.ts} (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index a6d750e..f6ec67b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "request": "launch", "name": "Start dev", // The path to a JavaScript or TypeScript file to run. - "program": "${file}", + "program": "entry-server.tsx", // The arguments to pass to the program, if any. "args": [], // The working directory of the program. @@ -15,40 +15,9 @@ "env": {}, // If the environment variables should not be inherited from the parent process. "strictEnv": false, - // If the program should be run in watch mode. - // This is equivalent to passing `--watch` to the `bun` executable. - // You can also set this to "hot" to enable hot reloading using `--hot`. "watchMode": false, // If the debugger should stop on the first line of the program. - "stopOnEntry": false, - // If the debugger should be disabled. (for example, breakpoints will not be hit) - "noDebug": false, - // The path to the `bun` executable, defaults to your `PATH` environment variable. - "runtime": "bun", - // The arguments to pass to the `bun` executable, if any. - // Unlike `args`, these are passed to the executable itself, not the program. - "runtimeArgs": [], - }, - { - "type": "bun", - "request": "launch", - "name": "Run tests", - // The path to a JavaScript or TypeScript file to run. - "program": "${file}", - // The arguments to pass to the program, if any. - "args": [], - // The working directory of the program. - "cwd": "${workspaceFolder}", - // The environment variables to pass to the program. - "env": {}, - // If the environment variables should not be inherited from the parent process. - "strictEnv": false, - // If the program should be run in watch mode. - // This is equivalent to passing `--watch` to the `bun` executable. - // You can also set this to "hot" to enable hot reloading using `--hot`. - "watchMode": false, - // If the debugger should stop on the first line of the program. - "stopOnEntry": false, + "stopOnEntry": true, // If the debugger should be disabled. (for example, breakpoints will not be hit) "noDebug": false, // The path to the `bun` executable, defaults to your `PATH` environment variable. @@ -56,17 +25,18 @@ // The arguments to pass to the `bun` executable, if any. // Unlike `args`, these are passed to the executable itself, not the program. "runtimeArgs": [ - "run", - "test" + "--bun", + "--inspect", + "dev" ], }, { "type": "bun", + "internalConsoleOptions": "neverOpen", "request": "attach", - "name": "Attach to Bun", - // The URL of the WebSocket inspector to attach to. - // This value can be retreived by using `bun --inspect`. + "name": "Attach Bun", "url": "ws://localhost:6499/", + "stopOnEntry": true } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e57826f..be68592 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,6 @@ { // The path to the `bun` executable. "bun.runtime": "/path/to/bun", - "bun.debugTerminal": { - // If support for Bun should be added to the default "JavaScript Debug Terminal". - "enabled": true, - // If the debugger should stop on the first line of the program. - "stopOnEntry": false, - } + "bun.debugTerminal.enabled": true, + "bun.debugTerminal.stopOnEntry": true } \ No newline at end of file diff --git a/src/features/editor/context.spec.tsx b/src/features/editor/context.spec.ts similarity index 100% rename from src/features/editor/context.spec.tsx rename to src/features/editor/context.spec.ts diff --git a/src/features/editor/context.ts b/src/features/editor/context.ts index 1fcbfa9..b725c0a 100644 --- a/src/features/editor/context.ts +++ b/src/features/editor/context.ts @@ -1,6 +1,6 @@ import { createEventListenerMap, DocumentEventListener, WindowEventListener } from "@solid-primitives/event-listener"; -import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js"; -import { createStore } from "solid-js/store"; +import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount, Setter } from "solid-js"; +import { createStore, produce } from "solid-js/store"; import { isServer } from "solid-js/web"; import { createMap } from './map'; import { unified } from "unified"; @@ -10,6 +10,7 @@ export type SelectFunction = (range: Range) => void; type Editor = { select: SelectFunction, readonly selection: Accessor }; interface EditorStoreType { + text: string; isComposing: boolean; selection: Range | undefined; characterBounds: DOMRect[]; @@ -17,7 +18,7 @@ interface EditorStoreType { selectionBounds: DOMRect; } -export function createEditor(ref: Accessor, value: Accessor): Editor { +export function createEditor(ref: Accessor, value: Accessor, setValue: (next: string) => any): Editor { if (isServer) { return { select() { }, @@ -29,14 +30,8 @@ export function createEditor(ref: Accessor, value: Accessor throw new Error('`EditContext` is not implemented'); } - const context = new EditContext({ - text: value(), - }); - - const mutations = observe(ref); - const ast = createMemo(() => parse(value())); - const indexMap = createMap(ref, ast); const [store, setStore] = createStore({ + text: value(), isComposing: false, selection: undefined, @@ -46,20 +41,54 @@ export function createEditor(ref: Accessor, value: Accessor selectionBounds: new DOMRect(), }); - createEffect(on(mutations, () => { - const selection = store.selection; + const context = new EditContext({ + text: store.text, + }); - if (selection === undefined) { - return - } + const mutations = observe(ref); + const ast = createMemo(() => parse(store.text)); + const indexMap = createMap(ref, ast); + + createEffect(() => { + setValue(store.text); + }); + + // createEffect(() => { + // const selection = store.selection; + + // if (!selection) { + // return; + // } + + // console.log(indexMap.query(selection)); + // }); + + createEffect(on(() => [ref(), ast()], () => { + console.log('pre rerender?'); + const selection = store.selection; + const indices = selection ? indexMap.query(selection) : []; queueMicrotask(() => { - console.log(selection); - - updateSelection(selection); + console.log('post rerender?'); + console.log(indices); }); })); + createEffect(on(value, value => { + if (value !== store.text) { + setStore('text', value); + } + })); + + createEffect(on(mutations, ([root, mutations]) => { + const text = (root! as HTMLElement).innerHTML; + + if (text !== store.text) { + context.updateText(0, context.text.length, text); + setStore('text', context.text); + } + })); + createEventListenerMap(context, { textupdate(e: TextUpdateEvent) { const selection = store.selection; @@ -68,6 +97,7 @@ export function createEditor(ref: Accessor, value: Accessor return; } + selection.extractContents(); selection.insertNode(document.createTextNode(e.text)); selection.collapse(); }, @@ -100,8 +130,6 @@ export function createEditor(ref: Accessor, value: Accessor function updateSelection(range: Range) { const [start, end] = indexMap.query(range); - console.log(start, end, range); - if (!start || !end) { return; } diff --git a/src/features/editor/editor.tsx b/src/features/editor/editor.tsx index 67b3fab..eecc6dc 100644 --- a/src/features/editor/editor.tsx +++ b/src/features/editor/editor.tsx @@ -1,5 +1,5 @@ import { createContextProvider } from "@solid-primitives/context"; -import { Accessor, createEffect, createMemo, createSignal, on, ParentProps, Setter } from "solid-js"; +import { Accessor, createEffect, createMemo, createSignal, on, ParentProps, Setter, untrack } from "solid-js"; import { createEditor, SelectFunction } from "./context"; import { createSource, Source } from "../source"; import { getTextNodes } from "@solid-primitives/selection"; @@ -19,7 +19,7 @@ interface EditorContextProps extends Record { const [EditorProvider, useEditor] = createContextProvider((props) => { const source = createSource(() => props.value); - const { select, selection } = createEditor(props.ref, () => source.out); + const { select, selection } = createEditor(props.ref, () => source.out, next => source.out = next); createEffect(() => { props.oninput?.(source.in); @@ -38,7 +38,7 @@ const [EditorProvider, useEditor] = createContextProvider source.out), + text: () => source.out, select, source, selection, @@ -65,11 +65,7 @@ export function Editor(props: ParentProps<{ value: string, oninput?: (value: str function Content(props: { ref: Setter }) { const { text } = useEditor(); - createEffect(() => { - text(); - - console.error('rerendering'); - }); + createEffect(on(text, () => console.error('rerendering'))); return
; } diff --git a/src/features/editor/map.ts b/src/features/editor/map.ts index 9923233..8001e40 100644 --- a/src/features/editor/map.ts +++ b/src/features/editor/map.ts @@ -8,17 +8,21 @@ export type IndexMap = IndexNode[]; export type IndexRange = [IndexNode, IndexNode] | [undefined, undefined]; export function createMap(root: Accessor, ast: Accessor) { - const mapping = createMemo(() => { + const [mapping, setMapping] = createSignal(new WeakMap()); + + createEffect(() => { const node = root(); const tree = ast(); if (node === undefined) { - return new WeakMap(); + return; } - console.warn('recalculating map'); - - return createMapping(node, tree); + // Delay the recalculation a bit to give other code a chance to update the DOM. + // This -hopefully- prevents the map from getting out of sync + queueMicrotask(() => { + setMapping(createMapping(node, tree)); + }); }); return { diff --git a/src/routes/(editor)/experimental/editor.tsx b/src/routes/(editor)/experimental/editor.tsx index d7aa2a2..7553dfe 100644 --- a/src/routes/(editor)/experimental/editor.tsx +++ b/src/routes/(editor)/experimental/editor.tsx @@ -31,15 +31,15 @@ this is *a string* that contains italicized text export default function Formatter(props: {}) { const [value, setValue] = createSignal(tempVal); - const onInput = debounce((e: InputEvent) => { + const onInput = (e: InputEvent) => { setValue((e.target! as HTMLTextAreaElement).value); - }, 300); + }; return
- + @@ -48,10 +48,12 @@ export default function Formatter(props: {}) { } function Toolbar() { + const { selection } = useEditor(); + const bold = () => { - const range = window.getSelection()!.getRangeAt(0); - // const { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } = range; - // console.log(startContainer, startOffset, endContainer, endOffset, commonAncestorContainer); + const range = untrack(selection)!; + + console.log(range); if (range.startContainer.nodeType !== Node.TEXT_NODE) { return; @@ -61,6 +63,13 @@ function Toolbar() { return; } + // Trim whitespace + { + const text = range.toString(); + range.setStart(range.startContainer, range.startOffset + (text.match(/^\s+/)?.[0].length ?? 0)); + range.setEnd(range.endContainer, range.endOffset - (text.match(/\s+$/)?.[0].length ?? 0)); + } + const fragment = range.extractContents(); if (range.startContainer === range.commonAncestorContainer && range.endContainer === range.commonAncestorContainer && range.commonAncestorContainer.parentElement?.tagName === 'STRONG') { @@ -72,6 +81,7 @@ function Toolbar() { strong.append(fragment); range.insertNode(strong); + range.selectNode(strong); } }; @@ -87,7 +97,7 @@ function Toolbar() { } function SearchAndReplace() { - const { mutate, source } = useEditor(); + const { source } = useEditor(); const [replacement, setReplacement] = createSignal(''); const [term, setTerm] = createSignal(''); const [caseInsensitive, setCaseInsensitive] = createSignal(true); @@ -104,7 +114,9 @@ function SearchAndReplace() { const form = e.target as HTMLFormElement; form.reset(); - mutate(text => text.replaceAll(query(), replacement())); + console.log(source.queryResults); + + // mutate(text => text.replaceAll(query(), replacement())); }; return
diff --git a/tsconfig.json b/tsconfig.json index cec05f7..662f2f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,6 @@ "@vitest/browser/providers/playwright", "vinxi/types/client", "vite-plugin-solid-svg/types-component-solid", - "vite-plugin-pwa/solid", "bun-types" ], "isolatedModules": true,