/ src / hooks / useInputBuffer.ts
useInputBuffer.ts
  1  import { useCallback, useRef, useState } from 'react'
  2  import type { PastedContent } from '../utils/config.js'
  3  
  4  export type BufferEntry = {
  5    text: string
  6    cursorOffset: number
  7    pastedContents: Record<number, PastedContent>
  8    timestamp: number
  9  }
 10  
 11  export type UseInputBufferProps = {
 12    maxBufferSize: number
 13    debounceMs: number
 14  }
 15  
 16  export type UseInputBufferResult = {
 17    pushToBuffer: (
 18      text: string,
 19      cursorOffset: number,
 20      pastedContents?: Record<number, PastedContent>,
 21    ) => void
 22    undo: () => BufferEntry | undefined
 23    canUndo: boolean
 24    clearBuffer: () => void
 25  }
 26  
 27  export function useInputBuffer({
 28    maxBufferSize,
 29    debounceMs,
 30  }: UseInputBufferProps): UseInputBufferResult {
 31    const [buffer, setBuffer] = useState<BufferEntry[]>([])
 32    const [currentIndex, setCurrentIndex] = useState(-1)
 33    const lastPushTime = useRef<number>(0)
 34    const pendingPush = useRef<ReturnType<typeof setTimeout> | null>(null)
 35  
 36    const pushToBuffer = useCallback(
 37      (
 38        text: string,
 39        cursorOffset: number,
 40        pastedContents: Record<number, PastedContent> = {},
 41      ) => {
 42        const now = Date.now()
 43  
 44        // Clear any pending push
 45        if (pendingPush.current) {
 46          clearTimeout(pendingPush.current)
 47          pendingPush.current = null
 48        }
 49  
 50        // Debounce rapid changes
 51        if (now - lastPushTime.current < debounceMs) {
 52          pendingPush.current = setTimeout(
 53            pushToBuffer,
 54            debounceMs,
 55            text,
 56            cursorOffset,
 57            pastedContents,
 58          )
 59          return
 60        }
 61  
 62        lastPushTime.current = now
 63  
 64        setBuffer(prevBuffer => {
 65          // If we're not at the end of the buffer, truncate everything after current position
 66          const newBuffer =
 67            currentIndex >= 0 ? prevBuffer.slice(0, currentIndex + 1) : prevBuffer
 68  
 69          // Don't add if it's the same as the last entry
 70          const lastEntry = newBuffer[newBuffer.length - 1]
 71          if (lastEntry && lastEntry.text === text) {
 72            return newBuffer
 73          }
 74  
 75          // Add new entry
 76          const updatedBuffer = [
 77            ...newBuffer,
 78            { text, cursorOffset, pastedContents, timestamp: now },
 79          ]
 80  
 81          // Limit buffer size
 82          if (updatedBuffer.length > maxBufferSize) {
 83            return updatedBuffer.slice(-maxBufferSize)
 84          }
 85  
 86          return updatedBuffer
 87        })
 88  
 89        // Update current index to point to the new entry
 90        setCurrentIndex(prev => {
 91          const newIndex = prev >= 0 ? prev + 1 : buffer.length
 92          return Math.min(newIndex, maxBufferSize - 1)
 93        })
 94      },
 95      [debounceMs, maxBufferSize, currentIndex, buffer.length],
 96    )
 97  
 98    const undo = useCallback((): BufferEntry | undefined => {
 99      if (currentIndex < 0 || buffer.length === 0) {
100        return undefined
101      }
102  
103      const targetIndex = Math.max(0, currentIndex - 1)
104      const entry = buffer[targetIndex]
105  
106      if (entry) {
107        setCurrentIndex(targetIndex)
108        return entry
109      }
110  
111      return undefined
112    }, [buffer, currentIndex])
113  
114    const clearBuffer = useCallback(() => {
115      setBuffer([])
116      setCurrentIndex(-1)
117      lastPushTime.current = 0
118      if (pendingPush.current) {
119        clearTimeout(pendingPush.current)
120        pendingPush.current = null
121      }
122    }, [lastPushTime, pendingPush])
123  
124    const canUndo = currentIndex > 0 && buffer.length > 1
125  
126    return {
127      pushToBuffer,
128      undo,
129      canUndo,
130      clearBuffer,
131    }
132  }