Repository navigation
Feature/update status bar PID for session-aware data/list viewers from different sessions #1828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e3cdf20
255f8fc
a8cd3ec
9a1c05e
e295a18
a63ed53
b2b1ce4
386eafd
34e47a6
b7da006
227e89b
a1600c6
e708fbc
d1eb898
80a04e8
a2e0342
958657c
bde05c7
16a8c06
1c2850e
466929c
f71cc84
8f2dfa0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,9 +96,6 @@ export let requestFile: string; | |
| export let requestLockFile: string; | ||
| export let sessionDir: string; | ||
| export let workingDir: string; | ||
| let rVer: string; | ||
| let pid: string; | ||
| let info: SessionInfo; | ||
| export let globalPipePath: string | undefined; | ||
| export let workspaceFile: string; | ||
|
|
||
|
|
@@ -112,6 +109,7 @@ const terminalRegistry = new TerminalSessionRegistry<Session, vscode.Terminal>( | |
| terminal => !terminal.exitStatus && window.terminals.includes(terminal), | ||
| ); | ||
| const documentSessions = new Map<string, Session>(); | ||
| let focusedDataViewSessionId: string | null = null; | ||
| const sessionDocumentBound = new vscode.EventEmitter<Uri>(); | ||
| export const onDidBindSessionDocument = sessionDocumentBound.event; | ||
|
|
||
|
|
@@ -135,8 +133,14 @@ export function unbindSessionDocument(uri: Uri): void { documentSessions.delete( | |
| export function unregisterSessionTransport(target: Session): void { | ||
| for (const [uri, owner] of documentSessions) { if (owner === target) { documentSessions.delete(uri); } } | ||
| terminalRegistry.releaseSession(target); | ||
| if (sessions.get(target.sessionId) === target) { sessions.delete(target.sessionId); } | ||
| if (activeSession === target) { void clearActiveSession(); } | ||
| const current = sessions.get(target.sessionId) === target; | ||
| const focused = current && focusedDataViewPanel && focusedDataViewSessionId === target.sessionId; | ||
| if (current) { sessions.delete(target.sessionId); } | ||
| if (activeSession === target) { | ||
| void clearActiveSession(); | ||
| } else if (focused) { | ||
| resetStatusBar(); | ||
| } | ||
| } | ||
|
|
||
| function clearActiveSession(): Promise<void> { | ||
|
|
@@ -146,7 +150,15 @@ function clearActiveSession(): Promise<void> { | |
| activeSession = undefined; | ||
| workspaceData = { search: [], loaded_namespaces: [], globalenv: {} }; | ||
| workingDir = ''; | ||
| resetStatusBar(); | ||
|
|
||
| const focusedSession = sessions.get(focusedDataViewSessionId ?? ''); | ||
|
|
||
| if (focusedDataViewPanel && focusedSession) { | ||
| updateSessionStatusBar(focusedSession); | ||
| } else { | ||
| resetStatusBar(); | ||
| } | ||
|
|
||
| rWorkspace?.refresh(); | ||
| return setContext('rSessionActive', false); | ||
| } | ||
|
|
@@ -181,7 +193,7 @@ export async function executeSessionCode(target: Session, code: string): Promise | |
| export function updateSessionWorkspace(target: Session, data: WorkspaceData): void { | ||
| target.workspaceData = data; | ||
| if (activeSession === target) { | ||
| void refreshActiveSession(target); | ||
| void refreshActiveSession(target, false); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -201,6 +213,9 @@ export function registerSessionTransport(id: string, host: string, directory: st | |
| sessions.set(id, target); | ||
| return target; | ||
| } | ||
| // An unattached terminal has no session to restore after a viewer loses focus. | ||
| let selectedTerminalDetached = false; | ||
|
|
||
| /** Wait for the same connected owner used by execution and terminal selection. */ | ||
| export function waitForTerminalReady(terminal: vscode.Terminal, timeout = 30000): Promise<boolean> { | ||
| return new Promise(resolve => { | ||
|
|
@@ -269,6 +284,7 @@ interface DataViewRequestMessage { | |
| const dynamicDataViewPanels = new Map<string, vscode.WebviewPanel>(); | ||
| const dynamicDataViewStateGenerations = new WeakMap<vscode.WebviewPanel, number>(); | ||
| const documentGenerations = new WeakMap<Webview, number>(); | ||
| let focusedDataViewPanel: vscode.WebviewPanel | undefined; | ||
| let documentGenerationRevision = 0; | ||
|
|
||
| function escapeHtml(text: string): string { | ||
|
|
@@ -282,6 +298,35 @@ function escapeHtml(text: string): string { | |
| return text.replace(/[&<>"']/g, c => map[c]); | ||
| } | ||
|
|
||
| function focusDataViewPanel(panel: vscode.WebviewPanel, sessionId: string | null): void { | ||
| focusedDataViewPanel = panel; | ||
| focusedDataViewSessionId = sessionId; | ||
|
|
||
| const session = sessions.get(sessionId ?? ''); | ||
| if (session) { | ||
| updateSessionStatusBar(session); | ||
| } else { | ||
| resetStatusBar(); | ||
| } | ||
| } | ||
|
|
||
| function blurDataViewPanel(panel: vscode.WebviewPanel): void { | ||
| if (focusedDataViewPanel !== panel) { | ||
| return; | ||
| } | ||
|
|
||
| focusedDataViewPanel = undefined; | ||
| focusedDataViewSessionId = null; | ||
|
|
||
| if (selectedTerminalDetached) { | ||
| resetStatusBar(); | ||
| } else if (activeSession) { | ||
| updateSessionStatusBar(activeSession); | ||
| } else { | ||
| resetStatusBar(); | ||
| } | ||
| } | ||
|
|
||
| function registerDataViewPanel( | ||
| panel: vscode.WebviewPanel, key: string, viewId: string, sessionId: string | null, | ||
| stateGeneration?: number, | ||
|
|
@@ -292,7 +337,16 @@ function registerDataViewPanel( | |
| if (stateGeneration !== undefined) { | ||
| dynamicDataViewStateGenerations.set(panel, stateGeneration); | ||
| } | ||
| panel.onDidChangeViewState(({ webviewPanel }) => { | ||
| if (!webviewPanel.active) { | ||
| blurDataViewPanel(panel); | ||
| return; | ||
|
Comment on lines
+340
to
+343
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Restore the active session's PID when leaving a viewer This listener writes A's PID when A's viewer gains focus, but returns immediately when it loses focus. With B still the active session, switching from A's viewer back to an ordinary, unbound |
||
| } | ||
|
|
||
| focusDataViewPanel(panel, sessionId); | ||
| }); | ||
| panel.onDidDispose(() => { | ||
| blurDataViewPanel(panel); | ||
| documentGenerations.delete(webview); | ||
| const currentStateGeneration = dynamicDataViewStateGenerations.get(panel); | ||
| dynamicDataViewStateGenerations.delete(panel); | ||
|
|
@@ -328,6 +382,20 @@ function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, | |
| }; | ||
|
|
||
| webview.onDidReceiveMessage(async (raw: unknown) => { | ||
| const focusMessage = raw as { message?: string; documentGeneration?: number }; | ||
| if (focusMessage.message === 'dataview/focus' || focusMessage.message === 'dataview/blur') { | ||
| if (focusMessage.documentGeneration === documentGenerations.get(webview)) { | ||
| if (focusMessage.message === 'dataview/focus') { | ||
| if (panel.active) { | ||
| focusDataViewPanel(panel, sessionId); | ||
| } | ||
| } else { | ||
| blurDataViewPanel(panel); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const msg = raw as Partial<DataViewRequestMessage>; | ||
| if (msg.message !== 'dataview/request' || typeof msg.requestId !== 'number' || | ||
| typeof msg.documentGeneration !== 'number' || | ||
|
|
@@ -887,6 +955,7 @@ export async function shutdownSessionWatcher(): Promise<void> { | |
| activeConnections.clear(); | ||
| pendingSessionAttachments.clear(); | ||
| pipeClient = undefined; | ||
| sessionSelectionRevision++; | ||
|
|
||
| if (globalSessionServer) { | ||
| await new Promise<void>((resolve) => { | ||
|
|
@@ -1170,6 +1239,16 @@ export async function showDataView( | |
| if (message.documentGeneration !== documentGenerations.get(webview)) { | ||
| return; | ||
| } | ||
| if (message.message === 'dataview/focus') { | ||
| if (panel.active) { | ||
| focusDataViewPanel(panel, sessionId); | ||
| } | ||
| return; | ||
| } | ||
| if (message.message === 'dataview/blur') { | ||
| blurDataViewPanel(panel); | ||
| return; | ||
| } | ||
| if (!Array.isArray(message.path) || !message.path.every(index => Number.isSafeInteger(index) && index > 0)) { | ||
| return; | ||
| } | ||
|
|
@@ -1409,6 +1488,12 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t | |
| <script> | ||
| const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : { postMessage: () => {} }; | ||
| const documentGeneration = ${documentGeneration}; | ||
| window.addEventListener('focus', () => { | ||
| vscode.postMessage({ message: 'dataview/focus', documentGeneration }); | ||
| }); | ||
| window.addEventListener('blur', () => { | ||
| vscode.postMessage({ message: 'dataview/blur', documentGeneration }); | ||
| }); | ||
| let requestIdSeq = 1; | ||
| const pending = new Map(); | ||
| let gridApi; | ||
|
|
@@ -1977,10 +2062,16 @@ export function getListHtml( | |
|
|
||
| import * as rstudioapi from './rstudioapi'; | ||
|
|
||
| export async function activateSession(session: Session): Promise<void> { | ||
| export async function activateSession(session: Session, updateStatus = true): Promise<void> { | ||
| sessionSelectionRevision++; | ||
| selectedTerminalDetached = false; | ||
| if (updateStatus) { | ||
| focusedDataViewPanel = undefined; | ||
| focusedDataViewSessionId = null; | ||
| } | ||
|
|
||
| activeSession = session; | ||
| const refreshed = refreshActiveSession(session); | ||
| const refreshed = refreshActiveSession(session, updateStatus); | ||
| if (!session.workspaceUnavailable) { scheduleWorkspaceRefresh(); } | ||
| await refreshed; | ||
| } | ||
|
|
@@ -1992,7 +2083,7 @@ function activateAttachedSession( | |
| ): Promise<void> { | ||
| // Replacing the selected connection preserves the user's logical session choice, | ||
| // even if that same session was reselected while discovery was pending. | ||
| if (previous && activeSession === previous) { return activateSession(session); } | ||
| if (previous && activeSession === previous) { return activateSession(session, !focusedDataViewPanel); } | ||
|
|
||
| // Automatic selection may not override a newer choice of a different session. | ||
| if (selectionRevision !== sessionSelectionRevision || selectedTerminal !== window.activeTerminal) { | ||
|
|
@@ -2002,24 +2093,18 @@ function activateAttachedSession( | |
| const hasNativeTerminal = terminalRegistry.forSession(session)?.kind === 'native'; | ||
| const shouldPreserveSelectedTerminal = hasNativeTerminal || selected?.kind === 'explicit'; | ||
| const preferred = shouldPreserveSelectedTerminal ? selected?.session : undefined; | ||
| return activateSession(preferred ?? session); | ||
| return activateSession(preferred ?? session, !focusedDataViewPanel); | ||
| } | ||
|
|
||
| async function refreshActiveSession(session: Session): Promise<void> { | ||
| async function refreshActiveSession(session: Session, updateStatus = true): Promise<void> { | ||
| pipeClient = session.socket; | ||
| if (!session.requester) { globalPipePath = session.pipePath; } | ||
| pid = session.pid; | ||
| rVer = session.rVer; | ||
| info = session.info; | ||
| sessionDir = session.sessionDir; | ||
| workingDir = session.workingDir; | ||
| workspaceData = session.workspaceData; | ||
|
|
||
| if (sessionStatusBarItem) { | ||
| const version = rVer.replace(/^R (?:version )?/, '').replace(/\s+\(.*/, ''); | ||
| sessionStatusBarItem.text = `R ${version}: ${pid}`; | ||
| sessionStatusBarItem.tooltip = `${info.version || rVer}\nProcess ID: ${pid}\nCommand: ${info.command}\nStart time: ${info.start_time}\nClick to attach to active terminal.`; | ||
| sessionStatusBarItem.show(); | ||
| if (updateStatus) { | ||
| updateSessionStatusBar(session); | ||
| } | ||
| rWorkspace?.refresh(); | ||
| await setContext('rSessionActive', !session.workspaceUnavailable); | ||
|
|
@@ -2041,6 +2126,15 @@ export async function activateSessionById(sessionId: string, options?: RSessionA | |
| return true; | ||
| } | ||
|
|
||
| function updateSessionStatusBar(session: Session): void { | ||
| if (sessionStatusBarItem) { | ||
| const version = session.rVer.replace(/^R (?:version )?/, '').replace(/\s+\(.*/, ''); | ||
| sessionStatusBarItem.text = `R ${version}: ${session.pid}`; | ||
| sessionStatusBarItem.tooltip = `${session.info.version || session.rVer}\nProcess ID: ${session.pid}\nCommand: ${session.info.command}\nStart time: ${session.info.start_time}\nClick to attach to active terminal.`; | ||
| sessionStatusBarItem.show(); | ||
| } | ||
| } | ||
|
|
||
| export function resetStatusBar(): void { | ||
| if (sessionStatusBarItem) { | ||
| sessionStatusBarItem.text = 'R: (not attached)'; | ||
|
|
@@ -2085,9 +2179,39 @@ async function findLocalTerminal(rPid: string): Promise<NativeTerminalCandidate | |
|
|
||
| export async function switchSessionByTerminal(terminal: vscode.Terminal | undefined): Promise<void> { | ||
| sessionSelectionRevision++; | ||
| const owner = terminalRegistry.ownerOf(terminal); | ||
| if (owner) { await activateSession(owner); } | ||
| else { resetStatusBar(); } | ||
|
|
||
| if (!terminal) { | ||
| selectedTerminalDetached = false; | ||
| if (focusedDataViewPanel) { | ||
| return; | ||
| } | ||
| if (activeSession) { | ||
| updateSessionStatusBar(activeSession); | ||
| } else { | ||
| resetStatusBar(); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| focusedDataViewPanel = undefined; | ||
| focusedDataViewSessionId = null; | ||
|
|
||
| const target = terminalRegistry.ownerOf(terminal); | ||
|
|
||
| if(!target) { | ||
| selectedTerminalDetached = true; | ||
| if (!focusedDataViewPanel) { resetStatusBar(); } | ||
| return; | ||
| } | ||
| selectedTerminalDetached = false; | ||
| const activated = activateSession(target, false); | ||
| const revision = sessionSelectionRevision; | ||
| await activated; | ||
|
|
||
| if (revision !== sessionSelectionRevision || activeSession !== target || !isConnectedSession(target)) { | ||
| return; | ||
| } | ||
| if (!focusedDataViewPanel) { updateSessionStatusBar(target); } | ||
| } | ||
|
|
||
| function sendToSocket(socket: IpcSocket, data: Record<string, unknown>): void { | ||
|
|
@@ -2398,6 +2522,8 @@ export async function cleanupSession(sessionId: string, closingSocket?: IpcSocke | |
| } | ||
| if (activeSession === session) { | ||
| await clearActiveSession(); | ||
| } else if (focusedDataViewPanel && focusedDataViewSessionId === sessionId) { | ||
| resetStatusBar(); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Restore the selected terminal's display when refocusing it
This fallback always displays
activeSession, which can differ from the still-selected terminal after choosing an Interactive session. VS Code does not emitonDidChangeActiveTerminalwhen keyboard focus returns to the same terminal, soswitchSessionByTerminal()cannot correct the display. Reproduction on macOS arm64 / VS Code 1.140.0: (1) select attached terminal B, (2) open/focus Interactive session A without changing the terminal selection, (3) open and focus A's expanded table or list viewer, (4) focus terminal B again, without selecting another terminal. The actualdataview/blurarrives withpanel.active === true; the terminal-selection event count stays unchanged; this branch leaves A's PID displayed in B's terminal. The live Interactive table check recorded actualR 4.6.1: 35567versus terminal B's expectedR 4.6.0: 35770, with one active-terminal event before and after refocus. Both native viewer checks reproduce the same result. Please restore B's display for a real terminal-focus transition while preserving the sidebar fix and leavingactiveSessionunchanged. This needs focus information beyondpanel.active, since both sidebar and terminal focus leave that flag true.To adapt the real-panel reproduction patch I posted earlier, keep the setup that selects B then explicitly activates A, and replace the
workbench.view.explorertransition with:Do not call
switchSessionByTerminal()after refocusing B in this check: doing so supplies the missing event and masks the display regression.