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",
|
"request": "launch",
|
||||||
"name": "Start dev",
|
"name": "Start dev",
|
||||||
// The path to a JavaScript or TypeScript file to run.
|
// 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.
|
// The arguments to pass to the program, if any.
|
||||||
"args": [],
|
"args": [],
|
||||||
// The working directory of the program.
|
// The working directory of the program.
|
||||||
|
@ -15,40 +15,9 @@
|
||||||
"env": {},
|
"env": {},
|
||||||
// If the environment variables should not be inherited from the parent process.
|
// If the environment variables should not be inherited from the parent process.
|
||||||
"strictEnv": false,
|
"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,
|
"watchMode": false,
|
||||||
// If the debugger should stop on the first line of the program.
|
// 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.
|
|
||||||
"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,
|
|
||||||
// If the debugger should be disabled. (for example, breakpoints will not be hit)
|
// If the debugger should be disabled. (for example, breakpoints will not be hit)
|
||||||
"noDebug": false,
|
"noDebug": false,
|
||||||
// The path to the `bun` executable, defaults to your `PATH` environment variable.
|
// 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.
|
// The arguments to pass to the `bun` executable, if any.
|
||||||
// Unlike `args`, these are passed to the executable itself, not the program.
|
// Unlike `args`, these are passed to the executable itself, not the program.
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"run",
|
"--bun",
|
||||||
"test"
|
"--inspect",
|
||||||
|
"dev"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "bun",
|
"type": "bun",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Attach to Bun",
|
"name": "Attach Bun",
|
||||||
// The URL of the WebSocket inspector to attach to.
|
|
||||||
// This value can be retreived by using `bun --inspect`.
|
|
||||||
"url": "ws://localhost:6499/",
|
"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.
|
// The path to the `bun` executable.
|
||||||
"bun.runtime": "/path/to/bun",
|
"bun.runtime": "/path/to/bun",
|
||||||
"bun.debugTerminal": {
|
"bun.debugTerminal.enabled": true,
|
||||||
// If support for Bun should be added to the default "JavaScript Debug Terminal".
|
"bun.debugTerminal.stopOnEntry": true
|
||||||
"enabled": true,
|
|
||||||
// If the debugger should stop on the first line of the program.
|
|
||||||
"stopOnEntry": false,
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { createEventListenerMap, DocumentEventListener, WindowEventListener } from "@solid-primitives/event-listener";
|
import { createEventListenerMap, DocumentEventListener, WindowEventListener } from "@solid-primitives/event-listener";
|
||||||
import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js";
|
import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount, Setter } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore, produce } from "solid-js/store";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
import { createMap } from './map';
|
import { createMap } from './map';
|
||||||
import { unified } from "unified";
|
import { unified } from "unified";
|
||||||
|
@ -10,6 +10,7 @@ export type SelectFunction = (range: Range) => void;
|
||||||
type Editor = { select: SelectFunction, readonly selection: Accessor<Range | undefined> };
|
type Editor = { select: SelectFunction, readonly selection: Accessor<Range | undefined> };
|
||||||
|
|
||||||
interface EditorStoreType {
|
interface EditorStoreType {
|
||||||
|
text: string;
|
||||||
isComposing: boolean;
|
isComposing: boolean;
|
||||||
selection: Range | undefined;
|
selection: Range | undefined;
|
||||||
characterBounds: DOMRect[];
|
characterBounds: DOMRect[];
|
||||||
|
@ -17,7 +18,7 @@ interface EditorStoreType {
|
||||||
selectionBounds: DOMRect;
|
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) {
|
if (isServer) {
|
||||||
return {
|
return {
|
||||||
select() { },
|
select() { },
|
||||||
|
@ -29,14 +30,8 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
||||||
throw new Error('`EditContext` is not implemented');
|
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>({
|
const [store, setStore] = createStore<EditorStoreType>({
|
||||||
|
text: value(),
|
||||||
isComposing: false,
|
isComposing: false,
|
||||||
selection: undefined,
|
selection: undefined,
|
||||||
|
|
||||||
|
@ -46,20 +41,54 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
||||||
selectionBounds: new DOMRect(),
|
selectionBounds: new DOMRect(),
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(on(mutations, () => {
|
const context = new EditContext({
|
||||||
const selection = store.selection;
|
text: store.text,
|
||||||
|
});
|
||||||
|
|
||||||
if (selection === undefined) {
|
const mutations = observe(ref);
|
||||||
return
|
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(() => {
|
queueMicrotask(() => {
|
||||||
console.log(selection);
|
console.log('post rerender?');
|
||||||
|
console.log(indices);
|
||||||
updateSelection(selection);
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
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, {
|
createEventListenerMap<any>(context, {
|
||||||
textupdate(e: TextUpdateEvent) {
|
textupdate(e: TextUpdateEvent) {
|
||||||
const selection = store.selection;
|
const selection = store.selection;
|
||||||
|
@ -68,6 +97,7 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selection.extractContents();
|
||||||
selection.insertNode(document.createTextNode(e.text));
|
selection.insertNode(document.createTextNode(e.text));
|
||||||
selection.collapse();
|
selection.collapse();
|
||||||
},
|
},
|
||||||
|
@ -100,8 +130,6 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
||||||
function updateSelection(range: Range) {
|
function updateSelection(range: Range) {
|
||||||
const [start, end] = indexMap.query(range);
|
const [start, end] = indexMap.query(range);
|
||||||
|
|
||||||
console.log(start, end, range);
|
|
||||||
|
|
||||||
if (!start || !end) {
|
if (!start || !end) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createContextProvider } from "@solid-primitives/context";
|
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 { createEditor, SelectFunction } from "./context";
|
||||||
import { createSource, Source } from "../source";
|
import { createSource, Source } from "../source";
|
||||||
import { getTextNodes } from "@solid-primitives/selection";
|
import { getTextNodes } from "@solid-primitives/selection";
|
||||||
|
@ -19,7 +19,7 @@ interface EditorContextProps extends Record<string, unknown> {
|
||||||
|
|
||||||
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((props) => {
|
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((props) => {
|
||||||
const source = createSource(() => props.value);
|
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(() => {
|
createEffect(() => {
|
||||||
props.oninput?.(source.in);
|
props.oninput?.(source.in);
|
||||||
|
@ -38,7 +38,7 @@ const [EditorProvider, useEditor] = createContextProvider<EditorContextType, Edi
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: createMemo(() => source.out),
|
text: () => source.out,
|
||||||
select,
|
select,
|
||||||
source,
|
source,
|
||||||
selection,
|
selection,
|
||||||
|
@ -65,11 +65,7 @@ export function Editor(props: ParentProps<{ value: string, oninput?: (value: str
|
||||||
function Content(props: { ref: Setter<Element | undefined> }) {
|
function Content(props: { ref: Setter<Element | undefined> }) {
|
||||||
const { text } = useEditor();
|
const { text } = useEditor();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(on(text, () => console.error('rerendering')));
|
||||||
text();
|
|
||||||
|
|
||||||
console.error('rerendering');
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div ref={props.ref} innerHTML={text()} />;
|
return <div ref={props.ref} innerHTML={text()} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,21 @@ export type IndexMap = IndexNode[];
|
||||||
export type IndexRange = [IndexNode, IndexNode] | [undefined, undefined];
|
export type IndexRange = [IndexNode, IndexNode] | [undefined, undefined];
|
||||||
|
|
||||||
export function createMap(root: Accessor<Element | undefined>, ast: Accessor<Root>) {
|
export function createMap(root: Accessor<Element | undefined>, ast: Accessor<Root>) {
|
||||||
const mapping = createMemo(() => {
|
const [mapping, setMapping] = createSignal(new WeakMap());
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
const node = root();
|
const node = root();
|
||||||
const tree = ast();
|
const tree = ast();
|
||||||
|
|
||||||
if (node === undefined) {
|
if (node === undefined) {
|
||||||
return new WeakMap();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn('recalculating map');
|
// 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
|
||||||
return createMapping(node, tree);
|
queueMicrotask(() => {
|
||||||
|
setMapping(createMapping(node, tree));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -31,15 +31,15 @@ this is *a string* that contains italicized text
|
||||||
export default function Formatter(props: {}) {
|
export default function Formatter(props: {}) {
|
||||||
const [value, setValue] = createSignal(tempVal);
|
const [value, setValue] = createSignal(tempVal);
|
||||||
|
|
||||||
const onInput = debounce((e: InputEvent) => {
|
const onInput = (e: InputEvent) => {
|
||||||
setValue((e.target! as HTMLTextAreaElement).value);
|
setValue((e.target! as HTMLTextAreaElement).value);
|
||||||
}, 300);
|
};
|
||||||
|
|
||||||
return <div class={css.root}>
|
return <div class={css.root}>
|
||||||
<textarea oninput={onInput} title="markdown">{value()}</textarea>
|
<textarea oninput={onInput} title="markdown">{value()}</textarea>
|
||||||
|
|
||||||
<div class={css.editor}>
|
<div class={css.editor}>
|
||||||
<Editor value={untrack(value)} oninput={setValue}>
|
<Editor value={value()} oninput={setValue}>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<SearchAndReplace />
|
<SearchAndReplace />
|
||||||
</Editor>
|
</Editor>
|
||||||
|
@ -48,10 +48,12 @@ export default function Formatter(props: {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Toolbar() {
|
function Toolbar() {
|
||||||
|
const { selection } = useEditor();
|
||||||
|
|
||||||
const bold = () => {
|
const bold = () => {
|
||||||
const range = window.getSelection()!.getRangeAt(0);
|
const range = untrack(selection)!;
|
||||||
// const { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } = range;
|
|
||||||
// console.log(startContainer, startOffset, endContainer, endOffset, commonAncestorContainer);
|
console.log(range);
|
||||||
|
|
||||||
if (range.startContainer.nodeType !== Node.TEXT_NODE) {
|
if (range.startContainer.nodeType !== Node.TEXT_NODE) {
|
||||||
return;
|
return;
|
||||||
|
@ -61,6 +63,13 @@ function Toolbar() {
|
||||||
return;
|
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();
|
const fragment = range.extractContents();
|
||||||
|
|
||||||
if (range.startContainer === range.commonAncestorContainer && range.endContainer === range.commonAncestorContainer && range.commonAncestorContainer.parentElement?.tagName === 'STRONG') {
|
if (range.startContainer === range.commonAncestorContainer && range.endContainer === range.commonAncestorContainer && range.commonAncestorContainer.parentElement?.tagName === 'STRONG') {
|
||||||
|
@ -72,6 +81,7 @@ function Toolbar() {
|
||||||
strong.append(fragment);
|
strong.append(fragment);
|
||||||
|
|
||||||
range.insertNode(strong);
|
range.insertNode(strong);
|
||||||
|
range.selectNode(strong);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,7 +97,7 @@ function Toolbar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchAndReplace() {
|
function SearchAndReplace() {
|
||||||
const { mutate, source } = useEditor();
|
const { source } = useEditor();
|
||||||
const [replacement, setReplacement] = createSignal('');
|
const [replacement, setReplacement] = createSignal('');
|
||||||
const [term, setTerm] = createSignal('');
|
const [term, setTerm] = createSignal('');
|
||||||
const [caseInsensitive, setCaseInsensitive] = createSignal(true);
|
const [caseInsensitive, setCaseInsensitive] = createSignal(true);
|
||||||
|
@ -104,7 +114,9 @@ function SearchAndReplace() {
|
||||||
const form = e.target as HTMLFormElement;
|
const form = e.target as HTMLFormElement;
|
||||||
form.reset();
|
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">
|
return <form on:submit={replace} class={css.search} popover="manual">
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
"@vitest/browser/providers/playwright",
|
"@vitest/browser/providers/playwright",
|
||||||
"vinxi/types/client",
|
"vinxi/types/client",
|
||||||
"vite-plugin-solid-svg/types-component-solid",
|
"vite-plugin-solid-svg/types-component-solid",
|
||||||
"vite-plugin-pwa/solid",
|
|
||||||
"bun-types"
|
"bun-types"
|
||||||
],
|
],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue