perf: skip hidden terminal sync fits
Skip sidebar sync-fit work for hidden display:none terminal panes while preserving visible and measurable startup panes.
This commit is contained in:
parent
09392af48b
commit
c7974783d1
|
|
@ -1047,6 +1047,9 @@ export default function TerminalPane({
|
|||
cwd,
|
||||
isActive,
|
||||
isVisible,
|
||||
// Why: hidden startup probes are opacity-hidden but measurable; ordinary
|
||||
// hidden tabs are display:none and refit on visibility resume instead.
|
||||
isSyncFitEnabled: isVisible || shouldMeasureHiddenStartup,
|
||||
paneCount,
|
||||
managerRef,
|
||||
containerRef,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import { fitPanes } from './pane-helpers'
|
|||
|
||||
type UseTerminalContainerFitSyncArgs = {
|
||||
isVisible: boolean
|
||||
isSyncFitEnabled: boolean
|
||||
managerRef: React.RefObject<PaneManager | null>
|
||||
containerRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
export function useTerminalContainerFitSync({
|
||||
isVisible,
|
||||
isSyncFitEnabled,
|
||||
managerRef,
|
||||
containerRef
|
||||
}: UseTerminalContainerFitSyncArgs): void {
|
||||
|
|
@ -21,10 +23,12 @@ export function useTerminalContainerFitSync({
|
|||
// ResizeObserver rAF would otherwise produce. The subsequent per-pane
|
||||
// ResizeObserver rAF and the 150ms debounced global fit become no-ops
|
||||
// because proposeDimensions() will match current cols/rows (early-return
|
||||
// branch in safeFit). Listener is global (not gated on isVisible/isActive)
|
||||
// so background tabs also fit, keeping their scroll position intact for
|
||||
// when the user switches back.
|
||||
// branch in safeFit). Hidden display:none panes cannot be measured
|
||||
// accurately, so they skip this global path and refit on visibility resume.
|
||||
useEffect(() => {
|
||||
if (!isSyncFitEnabled) {
|
||||
return
|
||||
}
|
||||
const onSyncFit = (): void => {
|
||||
managerRef.current?.fitAllPanes()
|
||||
}
|
||||
|
|
@ -32,7 +36,7 @@ export function useTerminalContainerFitSync({
|
|||
return () => {
|
||||
window.removeEventListener(SYNC_FIT_PANES_EVENT, onSyncFit)
|
||||
}
|
||||
}, [managerRef])
|
||||
}, [isSyncFitEnabled, managerRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/* eslint-disable max-lines -- Why: these hook tests share a mocked React lifecycle harness with global event cases. */
|
||||
import type * as ReactModule from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SYNC_FIT_PANES_EVENT } from '@/constants/terminal'
|
||||
import { useTerminalPaneGlobalEffects } from './use-terminal-pane-global-effects'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
|
|
@ -82,6 +84,7 @@ function useMountForFileDrop(
|
|||
cwd?: string
|
||||
isActive?: boolean
|
||||
isVisible?: boolean
|
||||
isSyncFitEnabled?: boolean
|
||||
paneCount?: number
|
||||
} = {}
|
||||
): {
|
||||
|
|
@ -116,6 +119,7 @@ function useMountForFileDrop(
|
|||
cwd: options.cwd,
|
||||
isActive: options.isActive ?? true,
|
||||
isVisible: options.isVisible ?? true,
|
||||
isSyncFitEnabled: options.isSyncFitEnabled ?? options.isVisible ?? true,
|
||||
paneCount: options.paneCount ?? 0,
|
||||
managerRef: { current: manager as never },
|
||||
containerRef: { current: null },
|
||||
|
|
@ -187,6 +191,7 @@ describe('useTerminalPaneGlobalEffects', () => {
|
|||
worktreeId: 'wt-1',
|
||||
isActive: true,
|
||||
isVisible: true,
|
||||
isSyncFitEnabled: true,
|
||||
paneCount: 2,
|
||||
managerRef: { current: manager as never },
|
||||
containerRef: { current: null },
|
||||
|
|
@ -244,6 +249,7 @@ describe('useTerminalPaneGlobalEffects', () => {
|
|||
isActiveRef: { current: false },
|
||||
isVisibleRef: { current: false },
|
||||
paneCount: 1,
|
||||
isSyncFitEnabled: true,
|
||||
toggleExpandPane: vi.fn()
|
||||
}
|
||||
|
||||
|
|
@ -330,4 +336,74 @@ describe('useTerminalPaneGlobalEffects', () => {
|
|||
|
||||
expect(mocks.handleTerminalFileDrop).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips global sync-fit registration for hidden non-measurable terminal panes', () => {
|
||||
const manager = {
|
||||
getPanes: vi.fn(() => []),
|
||||
resumeRendering: vi.fn(),
|
||||
suspendRendering: vi.fn(),
|
||||
fitAllPanes: vi.fn(),
|
||||
getActivePane: vi.fn(() => null)
|
||||
}
|
||||
|
||||
beginHookRender()
|
||||
useTerminalPaneGlobalEffects({
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
isActive: false,
|
||||
isVisible: false,
|
||||
isSyncFitEnabled: false,
|
||||
paneCount: 0,
|
||||
managerRef: { current: manager as never },
|
||||
containerRef: { current: null },
|
||||
paneTransportsRef: { current: new Map() },
|
||||
isActiveRef: { current: false },
|
||||
isVisibleRef: { current: false },
|
||||
toggleExpandPane: vi.fn()
|
||||
})
|
||||
|
||||
const syncFitListener = vi
|
||||
.mocked(window.addEventListener)
|
||||
.mock.calls.find(([eventName]) => eventName === SYNC_FIT_PANES_EVENT)
|
||||
|
||||
expect(syncFitListener).toBeUndefined()
|
||||
})
|
||||
|
||||
it('registers global sync-fit for measurable hidden startup panes', () => {
|
||||
const manager = {
|
||||
getPanes: vi.fn(() => []),
|
||||
resumeRendering: vi.fn(),
|
||||
suspendRendering: vi.fn(),
|
||||
fitAllPanes: vi.fn(),
|
||||
getActivePane: vi.fn(() => null)
|
||||
}
|
||||
|
||||
beginHookRender()
|
||||
useTerminalPaneGlobalEffects({
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
isActive: false,
|
||||
isVisible: false,
|
||||
isSyncFitEnabled: true,
|
||||
paneCount: 0,
|
||||
managerRef: { current: manager as never },
|
||||
containerRef: { current: null },
|
||||
paneTransportsRef: { current: new Map() },
|
||||
isActiveRef: { current: false },
|
||||
isVisibleRef: { current: false },
|
||||
toggleExpandPane: vi.fn()
|
||||
})
|
||||
|
||||
const syncFitListener = vi
|
||||
.mocked(window.addEventListener)
|
||||
.mock.calls.find(([eventName]) => eventName === SYNC_FIT_PANES_EVENT)
|
||||
|
||||
expect(syncFitListener).toBeDefined()
|
||||
const listener = syncFitListener?.[1]
|
||||
if (typeof listener !== 'function') {
|
||||
throw new Error('expected sync-fit listener')
|
||||
}
|
||||
listener(new Event(SYNC_FIT_PANES_EVENT))
|
||||
expect(manager.fitAllPanes).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type UseTerminalPaneGlobalEffectsArgs = {
|
|||
cwd?: string
|
||||
isActive: boolean
|
||||
isVisible: boolean
|
||||
isSyncFitEnabled: boolean
|
||||
paneCount: number
|
||||
managerRef: React.RefObject<PaneManager | null>
|
||||
containerRef: React.RefObject<HTMLDivElement | null>
|
||||
|
|
@ -45,6 +46,7 @@ export function useTerminalPaneGlobalEffects({
|
|||
cwd,
|
||||
isActive,
|
||||
isVisible,
|
||||
isSyncFitEnabled,
|
||||
paneCount,
|
||||
managerRef,
|
||||
containerRef,
|
||||
|
|
@ -73,7 +75,7 @@ export function useTerminalPaneGlobalEffects({
|
|||
visibleResumeCompleteRef: wasVisibleRef,
|
||||
paneCount
|
||||
})
|
||||
useTerminalContainerFitSync({ isVisible, managerRef, containerRef })
|
||||
useTerminalContainerFitSync({ isVisible, isSyncFitEnabled, managerRef, containerRef })
|
||||
|
||||
useEffect(() => {
|
||||
const manager = managerRef.current
|
||||
|
|
|
|||
Loading…
Reference in New Issue