stabalized the index map, now the selection is lost on rerenders again :/
This commit is contained in:
parent
5a813627ea
commit
41a1ef0dbb
8 changed files with 91 additions and 86 deletions
46
.vscode/launch.json
vendored
46
.vscode/launch.json
vendored
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()} />;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue