diff --git a/app/gui2/src/components/lexical/formatting.ts b/app/gui2/src/components/lexical/formatting.ts index f22d526b268b..9e01a8c3bb12 100644 --- a/app/gui2/src/components/lexical/formatting.ts +++ b/app/gui2/src/components/lexical/formatting.ts @@ -1,3 +1,4 @@ +import { useBufferedWritable } from '@/util/reactivity' import { $createCodeNode } from '@lexical/code' import { $isListNode, @@ -30,7 +31,7 @@ import { FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical' -import { computed, ref } from 'vue' +import { ref } from 'vue' export function useFormatting(editor: LexicalEditor) { const selectionReaders = new Array<(selection: RangeSelection) => void>() @@ -77,11 +78,9 @@ function useFormatProperty( onReadSelection((selection) => (state.value = selection.hasFormat(property))) - return computed({ + return useBufferedWritable({ get: () => state.value, - set: (value) => { - if (value !== state.value) editor.dispatchCommand(FORMAT_TEXT_COMMAND, property) - }, + set: (_value) => editor.dispatchCommand(FORMAT_TEXT_COMMAND, property), }) } @@ -227,10 +226,8 @@ function useBlockType( h3: () => $setBlocksType($getSelection(), () => $createHeadingNode('h3')), } - return computed({ + return useBufferedWritable({ get: () => state.value, - set: (value) => { - if (value !== state.value) editor.update($setBlockType[value]) - }, + set: (value) => editor.update($setBlockType[value]), }) } diff --git a/app/gui2/src/components/lexical/index.ts b/app/gui2/src/components/lexical/index.ts index 7b01ef933718..8112fefa5ef9 100644 --- a/app/gui2/src/components/lexical/index.ts +++ b/app/gui2/src/components/lexical/index.ts @@ -1,13 +1,13 @@ -import { blockTypes, normalizeHeadingLevel } from '@/components/lexical/formatting' +import { normalizeHeadingLevel } from '@/components/lexical/formatting' import { unrefElement, type MaybeElement } from '@vueuse/core' -import { - createEditor, - type EditorThemeClasses, - type KlassConstructor, - type LexicalEditor, - type LexicalNode, - type LexicalNodeReplacement, +import type { + EditorThemeClasses, + KlassConstructor, + LexicalEditor, + LexicalNode, + LexicalNodeReplacement, } from 'lexical' +import { createEditor } from 'lexical' import { assertDefined } from 'shared/util/assert' import { markRaw, onMounted, type Ref } from 'vue' diff --git a/app/gui2/src/components/lexical/sync.ts b/app/gui2/src/components/lexical/sync.ts index 67d6a8137d76..545894f4c6e8 100644 --- a/app/gui2/src/components/lexical/sync.ts +++ b/app/gui2/src/components/lexical/sync.ts @@ -1,4 +1,4 @@ -import type { ToValue } from '@/util/reactivity' +import { useBufferedWritable, type ToValue } from '@/util/reactivity' import type { LexicalEditor } from 'lexical' import { $createParagraphNode, $createTextNode, $getRoot, $setSelection } from 'lexical' import { computed, shallowRef, toValue } from 'vue' @@ -37,7 +37,7 @@ export function useLexicalSync( }) return { - content: computed({ + content: useBufferedWritable({ get: () => getContent.value, set: (content) => { editor.update(() => $write(content, getContent), { diff --git a/app/gui2/src/composables/astDocumentation.ts b/app/gui2/src/composables/astDocumentation.ts index 0abb4af006ce..cb7bfc687e16 100644 --- a/app/gui2/src/composables/astDocumentation.ts +++ b/app/gui2/src/composables/astDocumentation.ts @@ -1,11 +1,11 @@ import { type GraphStore } from '@/stores/graph' -import type { ToValue } from '@/util/reactivity' +import { useBufferedWritable, type ToValue } from '@/util/reactivity' import type { Ast } from 'shared/ast' -import { computed, toValue } from 'vue' +import { toValue } from 'vue' export function useAstDocumentation(graphStore: GraphStore, ast: ToValue) { return { - documentation: computed({ + documentation: useBufferedWritable({ get: () => toValue(ast)?.documentingAncestor()?.documentation() ?? '', set: (value) => { const astValue = toValue(ast) diff --git a/app/gui2/src/util/reactivity.ts b/app/gui2/src/util/reactivity.ts index bd2c2cef268d..a3f52c315ca9 100644 --- a/app/gui2/src/util/reactivity.ts +++ b/app/gui2/src/util/reactivity.ts @@ -178,3 +178,29 @@ export function computedFallback( set: (val: T) => (base.value = val), }) } + +/** Given a "raw" getter and setter, returns a writable-computed that buffers `set` operations. + * + * When the setter of the returned ref is invoked, the raw setter will be called during the next callback flush if and + * only if the most recently set value does not compare strictly-equal to the current value (read from the raw getter). + * + * The getter of the returned ref immediately reflects the value of any pending write. + */ +export function useBufferedWritable(raw: { + get: () => T + set: (value: T) => void +}): WritableComputedRef { + const pendingWrite = shallowRef<{ pending: T }>() + watch(pendingWrite, () => { + if (pendingWrite.value) { + if (pendingWrite.value.pending !== raw.get()) { + raw.set(pendingWrite.value.pending) + } + pendingWrite.value = undefined + } + }) + return computed({ + get: () => (pendingWrite.value ? pendingWrite.value.pending : raw.get()), + set: (value: T) => (pendingWrite.value = { pending: value }), + }) +}