Files
sim/apps/sim/hooks/use-code-viewer.ts
Waleed 05bbf34265 improvement(canvas): add multi-block select, add batch handle, enabled, and edge operations (#2738)
* improvement(canvas): add multi-block select, add batch handle, enabled, and edge operations

* feat(i18n): update translations (#2732)

Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>

* don't allow flip handles for subflows

* ack PR comments

* more

* fix missing handler

* remove dead subflow-specific ops

* remove unused code

* fixed subflow ops

* keep edges on subflow actions intact

* fix subflow resizing

* fix remove from subflow bulk

* improvement(canvas): add multi-block select, add batch handle, enabled, and edge operations

* don't allow flip handles for subflows

* ack PR comments

* more

* fix missing handler

* remove dead subflow-specific ops

* remove unused code

* fixed subflow ops

* fix subflow resizing

* keep edges on subflow actions intact

* fixed copy from inside subflow

* types improvement, preview fixes

* fetch varible data in deploy modal

* moved remove from subflow one position to the right

* fix subflow issues

* address greptile comment

* fix test

* improvement(preview): ui/ux

* fix(preview): subflows

* added batch add edges

* removed recovery

* use consolidated consts for sockets operations

* more

---------

Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
2026-01-09 14:48:23 -08:00

156 lines
4.6 KiB
TypeScript

'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
interface UseCodeViewerFeaturesOptions {
/** Reference to the content container for scroll-to-match functionality */
contentRef?: React.RefObject<HTMLDivElement | null>
/** Initial wrap text state (ignored if externalWrapText is provided) */
initialWrapText?: boolean
/** External wrap text state (e.g., from Zustand store) */
externalWrapText?: boolean
/** External setter for wrap text (required if externalWrapText is provided) */
onWrapTextChange?: (wrap: boolean) => void
/** Callback when escape is pressed (optional, for custom handling) */
onEscape?: () => void
}
interface UseCodeViewerFeaturesReturn {
wrapText: boolean
setWrapText: (wrap: boolean) => void
toggleWrapText: () => void
isSearchActive: boolean
searchQuery: string
setSearchQuery: (query: string) => void
matchCount: number
currentMatchIndex: number
activateSearch: () => void
closeSearch: () => void
goToNextMatch: () => void
goToPreviousMatch: () => void
handleMatchCountChange: (count: number) => void
searchInputRef: React.RefObject<HTMLInputElement | null>
}
/**
* Reusable hook for Code.Viewer features: search and wrap text functionality.
* Supports both internal state and external state (e.g., from Zustand) for wrapText.
*/
export function useCodeViewerFeatures(
options: UseCodeViewerFeaturesOptions = {}
): UseCodeViewerFeaturesReturn {
const {
contentRef,
initialWrapText = true,
externalWrapText,
onWrapTextChange,
onEscape,
} = options
// Use external state if provided, otherwise use internal state
const [internalWrapText, setInternalWrapText] = useState(initialWrapText)
const wrapText = externalWrapText !== undefined ? externalWrapText : internalWrapText
const setWrapText = onWrapTextChange ?? setInternalWrapText
const [isSearchActive, setIsSearchActive] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [matchCount, setMatchCount] = useState(0)
const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
const searchInputRef = useRef<HTMLInputElement>(null)
const toggleWrapText = useCallback(() => {
setWrapText(!wrapText)
}, [wrapText, setWrapText])
const activateSearch = useCallback(() => {
setIsSearchActive(true)
setTimeout(() => {
searchInputRef.current?.focus()
}, 0)
}, [])
const closeSearch = useCallback(() => {
setIsSearchActive(false)
setSearchQuery('')
setMatchCount(0)
setCurrentMatchIndex(0)
}, [])
const goToNextMatch = useCallback(() => {
if (matchCount === 0) return
setCurrentMatchIndex((prev) => (prev + 1) % matchCount)
}, [matchCount])
const goToPreviousMatch = useCallback(() => {
if (matchCount === 0) return
setCurrentMatchIndex((prev) => (prev - 1 + matchCount) % matchCount)
}, [matchCount])
const handleMatchCountChange = useCallback((count: number) => {
setMatchCount(count)
setCurrentMatchIndex(0)
}, [])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isSearchActive) {
e.preventDefault()
closeSearch()
onEscape?.()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isSearchActive, closeSearch, onEscape])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isSearchActive) return
const isSearchInputFocused = document.activeElement === searchInputRef.current
if (e.key === 'Enter' && isSearchInputFocused && matchCount > 0) {
e.preventDefault()
if (e.shiftKey) {
goToPreviousMatch()
} else {
goToNextMatch()
}
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isSearchActive, matchCount, goToNextMatch, goToPreviousMatch])
useEffect(() => {
if (!isSearchActive || matchCount === 0 || !contentRef?.current) return
const matchElements = contentRef.current.querySelectorAll('[data-search-match]')
const currentElement = matchElements[currentMatchIndex]
if (currentElement) {
currentElement.scrollIntoView({ block: 'center' })
}
}, [currentMatchIndex, isSearchActive, matchCount, contentRef])
return {
wrapText,
setWrapText,
toggleWrapText,
isSearchActive,
searchQuery,
setSearchQuery,
matchCount,
currentMatchIndex,
activateSearch,
closeSearch,
goToNextMatch,
goToPreviousMatch,
handleMatchCountChange,
searchInputRef,
}
}