fix: stop duplicate native quick prompt paste

This commit is contained in:
lucaronin
2026-04-19 00:18:42 +02:00
parent 72b5b52140
commit cac7f12c6d
4 changed files with 106 additions and 61 deletions

View File

@@ -1,5 +1,7 @@
import {
useMemo,
useRef,
useState,
type ReactNode,
} from 'react'
import type { VaultEntry } from '../types'
@@ -14,6 +16,12 @@ import {
extractInlineWikilinkReferences,
findActiveWikilinkQuery,
} from './inlineWikilinkText'
import { serializeInlineNode } from './inlineWikilinkDom'
import {
buildPendingPasteState,
type PendingPasteState,
shouldRecoverPendingPaste,
} from './inlineWikilinkPasteRecovery'
import {
InlineWikilinkEditorField,
InlineWikilinkPaletteLayout,
@@ -48,7 +56,6 @@ function collapseSelectionRange(nextSelectionIndex: number) {
end: nextSelectionIndex,
}
}
function submitInlineValue({
onSubmit,
submitOnEmpty,
@@ -66,51 +73,6 @@ function submitInlineValue({
onSubmit(normalizedValue, references)
}
function renderInlineEditorField({
value,
placeholder,
disabled,
inputRef,
dataTestId,
editorClassName,
onInput,
onKeyDown,
onPaste,
onSelectionChange,
segments,
typeEntryMap,
}: {
value: string
placeholder?: string
disabled: boolean
inputRef: React.Ref<HTMLDivElement>
dataTestId: string
editorClassName?: string
onInput: () => void
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
onPaste: (event: React.ClipboardEvent<HTMLDivElement>) => void
onSelectionChange: () => void
segments: ReturnType<typeof buildInlineWikilinkSegments>
typeEntryMap: Record<string, VaultEntry>
}) {
return (
<InlineWikilinkEditorField
value={value}
placeholder={placeholder}
disabled={disabled}
inputRef={inputRef}
dataTestId={dataTestId}
editorClassName={editorClassName}
onInput={onInput}
onKeyDown={onKeyDown}
onPaste={onPaste}
onSelectionChange={onSelectionChange}
segments={segments}
typeEntryMap={typeEntryMap}
/>
)
}
function renderInlineSuggestionList({
suggestions,
selectedSuggestionIndex,
@@ -160,12 +122,14 @@ export function InlineWikilinkInput({
paletteEmptyState,
paletteFooter,
}: InlineWikilinkInputProps) {
const [, forceRender] = useState(0)
const segments = useMemo(
() => buildInlineWikilinkSegments(value, entries),
[entries, value],
)
const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries])
const {
editorRef,
selectionRange,
selectionIndex,
setSelectionRange,
@@ -178,6 +142,7 @@ export function InlineWikilinkInput({
onChange,
inputRef,
})
const pendingPasteRef = useRef<PendingPasteState | null>(null)
const activeQuery = useMemo(
() => selectionRange.start === selectionRange.end
? findActiveWikilinkQuery(value, selectionIndex)
@@ -218,11 +183,30 @@ export function InlineWikilinkInput({
const pastedText = normalizeInlineWikilinkValue(event.clipboardData.getData('text/plain'))
if (!pastedText) return
event.preventDefault()
const nextState = replaceInlineSelection(value, selectionRange, pastedText)
pendingPasteRef.current = buildPendingPasteState(value, selectionRange, pastedText)
event.preventDefault()
onChange(nextState.value)
setSelectionRange(nextState.selection)
}
const handleInput = () => {
const editor = editorRef.current
const pendingPaste = pendingPasteRef.current
if (editor && pendingPaste) {
const nextValue = normalizeInlineWikilinkValue(serializeInlineNode(editor))
pendingPasteRef.current = null
if (shouldRecoverPendingPaste(nextValue, pendingPaste)) {
onChange(pendingPaste.expectedValue)
forceRender((current) => current + 1)
setSelectionRange({ ...pendingPaste.expectedSelection })
return
}
}
commitValueFromEditor()
}
const submitValue = () =>
submitInlineValue({ onSubmit, submitOnEmpty, value, references })
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) =>
@@ -237,20 +221,22 @@ export function InlineWikilinkInput({
canSubmit: onSubmit !== undefined,
onSubmit: submitValue,
})
const editor = renderInlineEditorField({
value,
placeholder,
disabled,
inputRef: setCombinedRef,
dataTestId,
editorClassName,
onInput: commitValueFromEditor,
onKeyDown: handleKeyDown,
onPaste: handlePaste,
onSelectionChange: syncSelectionRange,
segments,
typeEntryMap,
})
const editor = (
<InlineWikilinkEditorField
value={value}
placeholder={placeholder}
disabled={disabled}
inputRef={setCombinedRef}
dataTestId={dataTestId}
editorClassName={editorClassName}
onInput={handleInput}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
onSelectionChange={syncSelectionRange}
segments={segments}
typeEntryMap={typeEntryMap}
/>
)
const suggestionList = renderInlineSuggestionList({
suggestions,
selectedSuggestionIndex,

View File

@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import {
buildPendingPasteState,
shouldRecoverPendingPaste,
} from './inlineWikilinkPasteRecovery'
describe('inlineWikilinkPasteRecovery', () => {
it('recognizes the expected post-paste value as recoverable', () => {
const pendingPaste = buildPendingPasteState(' ', { start: 1, end: 1 }, 'hello world')
expect(pendingPaste).toEqual({
duplicatedValue: ' hello worldhello world',
expectedValue: ' hello world',
expectedSelection: { start: 12, end: 12 },
})
expect(shouldRecoverPendingPaste(' hello world', pendingPaste)).toBe(true)
})
it('recognizes the duplicate native replay as recoverable when pasting into the middle of text', () => {
const pendingPaste = buildPendingPasteState('prefix suffix', { start: 7, end: 7 }, 'hello ')
expect(pendingPaste.duplicatedValue).toBe('prefix hello hello suffix')
expect(shouldRecoverPendingPaste('prefix hello hello suffix', pendingPaste)).toBe(true)
expect(shouldRecoverPendingPaste('prefix hello suffix!', pendingPaste)).toBe(false)
})
})

View File

@@ -0,0 +1,32 @@
import type { InlineSelectionRange } from './inlineWikilinkDom'
import { replaceInlineSelection } from './inlineWikilinkEdits'
export interface PendingPasteState {
duplicatedValue: string
expectedValue: string
expectedSelection: InlineSelectionRange
}
export function buildPendingPasteState(
value: string,
selectionRange: InlineSelectionRange,
pastedText: string,
): PendingPasteState {
const nextState = replaceInlineSelection(value, selectionRange, pastedText)
return {
duplicatedValue: replaceInlineSelection(nextState.value, nextState.selection, pastedText).value,
expectedValue: nextState.value,
expectedSelection: nextState.selection,
}
}
export function shouldRecoverPendingPaste(
nextValue: string,
pendingPaste: PendingPasteState,
): boolean {
return (
nextValue === pendingPaste.expectedValue ||
nextValue === pendingPaste.duplicatedValue
)
}

View File

@@ -69,6 +69,7 @@ export function useInlineWikilinkSelection({
}, [selectionRange, value])
return {
editorRef,
selectionRange,
selectionIndex: selectionRange.end,
setSelectionRange,