feat(workspace): preserve terminal sessions across view changes
This commit is contained in:
@@ -2081,10 +2081,12 @@ function LocalPcWorkspaceMockup({
|
||||
fitTerminal,
|
||||
copyTerminalSelection,
|
||||
pasteTerminalClipboard,
|
||||
startTerminalResize
|
||||
startTerminalResize,
|
||||
stopAllTerminalSessions
|
||||
} = useWorkspaceTerminal({
|
||||
moduleName: selectedModule?.name,
|
||||
moduleFolderName: selectedModule?.folderName,
|
||||
openModuleNames: moduleTabs.map((module) => module.name),
|
||||
clonePathPreview,
|
||||
remoteWorkspace,
|
||||
getRemoteWorkspaceTarget,
|
||||
@@ -2950,6 +2952,7 @@ function LocalPcWorkspaceMockup({
|
||||
setPendingEditorCloseRequest({ kind: 'workspace' })
|
||||
return
|
||||
}
|
||||
stopAllTerminalSessions()
|
||||
onClose()
|
||||
}
|
||||
const performPendingEditorCloseRequest = (): void => {
|
||||
@@ -2995,6 +2998,7 @@ function LocalPcWorkspaceMockup({
|
||||
}
|
||||
|
||||
discardDirtyFiles(Object.keys(dirtyEditorFiles).filter((file) => dirtyEditorFiles[file]))
|
||||
stopAllTerminalSessions()
|
||||
onClose()
|
||||
}
|
||||
const revealSearchMatch = (lineNumber: number, query: string): boolean => {
|
||||
|
||||
@@ -10,9 +10,19 @@ type TerminalContextMenu = {
|
||||
y: number
|
||||
}
|
||||
|
||||
type ManagedTerminalSession = {
|
||||
moduleName: string
|
||||
remoteWorkspace: boolean
|
||||
attachedTerminal: XTerm | null
|
||||
suppressedData: string[]
|
||||
removeDataListener: () => void
|
||||
removeSuppressListener: () => void
|
||||
}
|
||||
|
||||
type UseWorkspaceTerminalOptions = {
|
||||
moduleName?: string | null
|
||||
moduleFolderName?: string | null
|
||||
openModuleNames: string[]
|
||||
clonePathPreview: string
|
||||
remoteWorkspace: boolean
|
||||
getRemoteWorkspaceTarget: () => RemoteWorkspaceTarget
|
||||
@@ -32,11 +42,13 @@ type UseWorkspaceTerminalResult = {
|
||||
copyTerminalSelection: () => void
|
||||
pasteTerminalClipboard: () => void
|
||||
startTerminalResize: (event: React.MouseEvent<HTMLDivElement>) => void
|
||||
stopAllTerminalSessions: () => void
|
||||
}
|
||||
|
||||
export function useWorkspaceTerminal({
|
||||
moduleName,
|
||||
moduleFolderName,
|
||||
openModuleNames,
|
||||
clonePathPreview,
|
||||
remoteWorkspace,
|
||||
getRemoteWorkspaceTarget,
|
||||
@@ -52,12 +64,31 @@ export function useWorkspaceTerminal({
|
||||
const terminalFitRef = useRef<FitAddon | null>(null)
|
||||
const terminalSessionIdRef = useRef<string | null>(null)
|
||||
const remoteTargetResolverRef = useRef(getRemoteWorkspaceTarget)
|
||||
const terminalSuppressQueueRef = useRef<string[]>([])
|
||||
const terminalOutputBufferRef = useRef<Record<string, string>>({})
|
||||
const terminalSessionsRef = useRef<Map<string, ManagedTerminalSession>>(new Map())
|
||||
const resetKeyRef = useRef(resetKey)
|
||||
const [terminalPanelHeight, setTerminalPanelHeight] = useState(235)
|
||||
|
||||
remoteTargetResolverRef.current = getRemoteWorkspaceTarget
|
||||
|
||||
const stopTerminalSession = (sessionId: string): void => {
|
||||
const session = terminalSessionsRef.current.get(sessionId)
|
||||
if (!session) return
|
||||
|
||||
session.attachedTerminal = null
|
||||
session.removeDataListener()
|
||||
session.removeSuppressListener()
|
||||
terminalSessionsRef.current.delete(sessionId)
|
||||
delete terminalOutputBufferRef.current[sessionId]
|
||||
|
||||
if (session.remoteWorkspace) window.remoteWorkspace.terminal.stop(sessionId)
|
||||
else window.localWorkspace.terminal.stop(sessionId)
|
||||
}
|
||||
|
||||
const stopAllTerminalSessions = (): void => {
|
||||
Array.from(terminalSessionsRef.current.keys()).forEach(stopTerminalSession)
|
||||
}
|
||||
|
||||
const fitTerminal = (): void => {
|
||||
terminalFitRef.current?.fit()
|
||||
}
|
||||
@@ -115,6 +146,21 @@ export function useWorkspaceTerminal({
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
const openModuleNamesKey = openModuleNames.join('\u0000')
|
||||
|
||||
useEffect(() => {
|
||||
const openModuleNameSet = new Set(openModuleNames)
|
||||
Array.from(terminalSessionsRef.current.entries()).forEach(([sessionId, session]) => {
|
||||
if (!openModuleNameSet.has(session.moduleName)) stopTerminalSession(sessionId)
|
||||
})
|
||||
}, [openModuleNamesKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (resetKeyRef.current === resetKey) return
|
||||
resetKeyRef.current = resetKey
|
||||
stopAllTerminalSessions()
|
||||
}, [resetKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (!moduleName || !terminalActive || !terminalHostRef.current || terminalRef.current) return
|
||||
const terminalSessionId = getModuleTerminalSessionId(moduleName)
|
||||
@@ -164,22 +210,36 @@ export function useWorkspaceTerminal({
|
||||
terminalFitRef.current = fitAddon
|
||||
|
||||
const terminalApi = remoteWorkspace ? window.remoteWorkspace.terminal : window.localWorkspace.terminal
|
||||
const removeDataListener = terminalApi.onData((data) => {
|
||||
const suppressQueue = terminalSuppressQueueRef.current
|
||||
if (suppressQueue.length && suppressQueue[0] === data) {
|
||||
suppressQueue.shift()
|
||||
return
|
||||
}
|
||||
let terminalSession = terminalSessionsRef.current.get(terminalSessionId)
|
||||
if (!terminalSession || terminalSession.remoteWorkspace !== remoteWorkspace) {
|
||||
if (terminalSession) stopTerminalSession(terminalSessionId)
|
||||
|
||||
const nextData = stripInternalTerminalCommands(data)
|
||||
if (nextData) {
|
||||
const suppressedData: string[] = []
|
||||
const removeDataListener = terminalApi.onData((data) => {
|
||||
if (suppressedData.length && suppressedData[0] === data) {
|
||||
suppressedData.shift()
|
||||
return
|
||||
}
|
||||
|
||||
const nextData = stripInternalTerminalCommands(data)
|
||||
if (!nextData) return
|
||||
terminalOutputBufferRef.current[terminalSessionId] = `${terminalOutputBufferRef.current[terminalSessionId] ?? ''}${nextData}`.slice(-60000)
|
||||
term.write(nextData)
|
||||
terminalSessionsRef.current.get(terminalSessionId)?.attachedTerminal?.write(nextData)
|
||||
}, terminalSessionId)
|
||||
const removeSuppressListener = terminalApi.onSuppressData((data) => {
|
||||
suppressedData.push(data)
|
||||
}, terminalSessionId)
|
||||
terminalSession = {
|
||||
moduleName,
|
||||
remoteWorkspace,
|
||||
attachedTerminal: null,
|
||||
suppressedData,
|
||||
removeDataListener,
|
||||
removeSuppressListener
|
||||
}
|
||||
}, terminalSessionId)
|
||||
const removeSuppressListener = terminalApi.onSuppressData((data) => {
|
||||
terminalSuppressQueueRef.current.push(data)
|
||||
}, terminalSessionId)
|
||||
terminalSessionsRef.current.set(terminalSessionId, terminalSession)
|
||||
}
|
||||
terminalSession.attachedTerminal = term
|
||||
const inputDisposable = term.onData((data) => terminalApi.write(data, terminalSessionId))
|
||||
|
||||
let sizeSyncTimer: number | null = null
|
||||
@@ -215,9 +275,10 @@ export function useWorkspaceTerminal({
|
||||
})
|
||||
})
|
||||
void startTerminal.then((started) => {
|
||||
if (disposed || !started) return
|
||||
fitAddon.fit()
|
||||
if (!started) return
|
||||
sizeSyncTimer = window.setTimeout(() => {
|
||||
if (disposed) return
|
||||
fitAddon.fit()
|
||||
if (remoteWorkspace) {
|
||||
void window.remoteWorkspace.terminal.resize(remoteTargetResolverRef.current(), term.cols, term.rows, terminalSessionId)
|
||||
@@ -237,19 +298,14 @@ export function useWorkspaceTerminal({
|
||||
window.removeEventListener('resize', onResize)
|
||||
terminalElement.removeEventListener('contextmenu', handleTerminalContextMenu)
|
||||
terminalElement.removeEventListener('mousedown', focusTerminal)
|
||||
removeDataListener()
|
||||
removeSuppressListener()
|
||||
const currentSession = terminalSessionsRef.current.get(terminalSessionId)
|
||||
if (currentSession?.attachedTerminal === term) currentSession.attachedTerminal = null
|
||||
inputDisposable.dispose()
|
||||
if (sizeSyncTimer !== null) window.clearTimeout(sizeSyncTimer)
|
||||
// Full-screen programs such as vi cannot be restored from a partial raw output buffer.
|
||||
delete terminalOutputBufferRef.current[terminalSessionId]
|
||||
if (remoteWorkspace) window.remoteWorkspace.terminal.stop(terminalSessionId)
|
||||
else window.localWorkspace.terminal.stop(terminalSessionId)
|
||||
term.dispose()
|
||||
terminalRef.current = null
|
||||
terminalFitRef.current = null
|
||||
terminalSessionIdRef.current = null
|
||||
terminalSuppressQueueRef.current = []
|
||||
}
|
||||
}, [clonePathPreview, dark, moduleFolderName, moduleName, remoteWorkspace, resetKey, terminalActive])
|
||||
|
||||
@@ -262,6 +318,7 @@ export function useWorkspaceTerminal({
|
||||
fitTerminal,
|
||||
copyTerminalSelection,
|
||||
pasteTerminalClipboard,
|
||||
startTerminalResize
|
||||
startTerminalResize,
|
||||
stopAllTerminalSessions
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user