stabalized the index map, now the selection is lost on rerenders again :/

This commit is contained in:
Chris Kruining 2025-03-17 16:31:11 +01:00
parent 5a813627ea
commit 41a1ef0dbb
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
8 changed files with 91 additions and 86 deletions

46
.vscode/launch.json vendored
View file

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

View file

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

View file

@ -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<Range | undefined> };
interface EditorStoreType {
text: string;
isComposing: boolean;
selection: Range | undefined;
characterBounds: DOMRect[];
@ -17,7 +18,7 @@ interface EditorStoreType {
selectionBounds: DOMRect;
}
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>): Editor {
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>, setValue: (next: string) => any): Editor {
if (isServer) {
return {
select() { },
@ -29,14 +30,8 @@ export function createEditor(ref: Accessor<Element | undefined>, 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<EditorStoreType>({
text: value(),
isComposing: false,
selection: undefined,
@ -46,20 +41,54 @@ export function createEditor(ref: Accessor<Element | undefined>, 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<any>(context, {
textupdate(e: TextUpdateEvent) {
const selection = store.selection;
@ -68,6 +97,7 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
return;
}
selection.extractContents();
selection.insertNode(document.createTextNode(e.text));
selection.collapse();
},
@ -100,8 +130,6 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
function updateSelection(range: Range) {
const [start, end] = indexMap.query(range);
console.log(start, end, range);
if (!start || !end) {
return;
}

View file

@ -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<string, unknown> {
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((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<EditorContextType, Edi
}));
return {
text: createMemo(() => 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<Element | undefined> }) {
const { text } = useEditor();
createEffect(() => {
text();
console.error('rerendering');
});
createEffect(on(text, () => console.error('rerendering')));
return <div ref={props.ref} innerHTML={text()} />;
}

View file

@ -8,17 +8,21 @@ export type IndexMap = IndexNode[];
export type IndexRange = [IndexNode, IndexNode] | [undefined, undefined];
export function createMap(root: Accessor<Element | undefined>, ast: Accessor<Root>) {
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 {

View file

@ -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 <div class={css.root}>
<textarea oninput={onInput} title="markdown">{value()}</textarea>
<div class={css.editor}>
<Editor value={untrack(value)} oninput={setValue}>
<Editor value={value()} oninput={setValue}>
<Toolbar />
<SearchAndReplace />
</Editor>
@ -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 <form on:submit={replace} class={css.search} popover="manual">

View file

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