Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/dataViewer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

import { viewerSessionStyle } from './viewerSession';

export function getDataViewerStyle(): string {
return `
body {
Expand All @@ -8,11 +10,15 @@ export function getDataViewerStyle(): string {
}
#gridContainer { position: relative; flex: 1; min-height: 0; }
#viewerToolbar {
display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
display: flex; align-items: flex-start; gap: 6px;
padding: 6px 8px; border-bottom: 1px solid var(--vscode-panel-border);
background: var(--vscode-editor-background); color: var(--vscode-foreground);
font: var(--vscode-font-size, 13px) var(--vscode-font-family, sans-serif);
}
#viewerControls {
display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
flex: 1; min-width: 0;
}
#viewerToolbar button {
padding: 4px 8px; border: 1px solid transparent; border-radius: 3px;
background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground);
Expand All @@ -24,21 +30,31 @@ export function getDataViewerStyle(): string {
#viewerToolbar button:focus-visible, #columnPanel :focus-visible {
outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px;
}
#viewerRowCount { margin-left: auto; font-size: 12px; color: var(--vscode-descriptionForeground); }
#viewerInfo {
display: flex; align-items: center; gap: 12px; padding-top: 5px;
flex-shrink: 0; white-space: nowrap;
font-size: 12px; color: var(--vscode-descriptionForeground);
}
${viewerSessionStyle}
.dataview-na { color: var(--vscode-descriptionForeground); font-style: italic; opacity: 0.75; }
`;
}

export function getDataViewerToolbarHtml(): string {
export function getDataViewerToolbarHtml(sessionHtml = ''): string {
return `
<div id="viewerToolbar" role="group" aria-label="Data viewer controls">
<div id="viewerControls">
<button id="columnPanelToggle" type="button" aria-controls="columnPanel" aria-expanded="false">Columns</button>
<button id="viewerFilters" type="button" aria-pressed="false" title="Show filters below the headers. Press Enter to apply text and number filters.">Filters</button>
<button id="viewerClearFilters" type="button" disabled>Clear filters</button>
<button id="viewerAutoSize" type="button" aria-pressed="false" title="Size columns to their loaded contents">Size to content</button>
<button id="viewerFit" type="button" aria-pressed="true" title="Keep columns fitted to the available width">Fit width</button>
<button id="viewerReset" type="button" title="Reset column layout, sorting, filters and page size">Reset view</button>
<span id="viewerRowCount" role="status" aria-live="polite"></span>
</div>
<div id="viewerInfo">
<span id="viewerRowCount" role="status" aria-live="polite"></span>
${sessionHtml}
</div>
</div>
`;
}
Expand Down
5 changes: 5 additions & 0 deletions src/interactive/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,7 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
const target = session.registerSessionTransport(`${manifest.id}:${manifest.generation}`, manifest.host, manifest.directory, data =>
client.request('inspect', { method: data.method, params: data.params ?? {} }, data.method === 'hover' || data.method === 'completion' ? 250 : 6000));
target.pid = String(manifest.rPid ?? ''); target.rVer = manifest.rVersion ?? '';
target.processExited = manifest.status === 'exited';
target.rPath = manifest.rPath; target.libraryPaths = manifest.libraryPaths;
target.label = manifest.label;
target.execute = async code => {
Expand Down Expand Up @@ -829,6 +830,8 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
view.executions.clear();
for (const cell of view.notebook.getCells()) { session.unbindSessionDocument(cell.document.uri); }
view.client.manifest = snapshot.manifest;
view.target.pid = String(snapshot.manifest.rPid ?? ''); view.target.rVer = snapshot.manifest.rVersion ?? '';
view.target.processExited = snapshot.manifest.status === 'exited';
view.target.rPath = snapshot.manifest.rPath; view.target.libraryPaths = snapshot.manifest.libraryPaths;
view.base = snapshot.manifest.assetBase ? (await vscode.env.asExternalUri(vscode.Uri.parse(snapshot.manifest.assetBase))).toString(true) : '';
view.model.restore(snapshot);
Expand Down Expand Up @@ -927,6 +930,7 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
this.updateStatus();
if (event.type === 'state') {
view.client.manifest.status = event.data.status as SessionManifest['status'];
view.target.processExited = event.data.status === 'exited';
view.client.manifest.rPid = typeof event.data.rPid === 'number' ? event.data.rPid : undefined;
view.client.manifest.rVersion = typeof event.data.rVersion === 'string' ? event.data.rVersion : undefined;
view.client.manifest.ended = typeof event.data.ended === 'number' ? event.data.ended : undefined;
Expand Down Expand Up @@ -1481,6 +1485,7 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
const snapshot = await view.client.snapshot();
view.client.manifest = snapshot.manifest;
target.pid = String(snapshot.manifest.rPid ?? ''); target.rVer = snapshot.manifest.rVersion ?? '';
target.processExited = snapshot.manifest.status === 'exited';
target.rPath = snapshot.manifest.rPath; target.libraryPaths = snapshot.manifest.libraryPaths;
view.prompts.clear();
if (snapshot.workspace) { target.workspaceData = snapshot.workspace as unknown as session.WorkspaceData; }
Expand Down
4 changes: 4 additions & 0 deletions src/listViewer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference lib="dom" />

import { viewerSessionStyle } from './viewerSession';

export interface ListViewNavigation {
title: string;
path: number[];
Expand Down Expand Up @@ -249,6 +251,8 @@ export const listViewerStyle = `
.r-list-viewer .back { gap: 4px; padding: 4px 6px; flex-shrink: 0; border-radius: 3px; }
.r-list-viewer .back:disabled { opacity: 0.4; cursor: default; background: transparent; }
.r-list-viewer .reset { gap: 4px; padding: 4px 6px; flex-shrink: 0; border-radius: 3px; }
${viewerSessionStyle}
.r-list-viewer .viewer-session { margin-left: auto; }
.r-list-viewer .codicon { flex-shrink: 0; }
.r-list-viewer .breadcrumbs {
display: flex;
Expand Down
73 changes: 62 additions & 11 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { config, readContent, setContext, UriIcon } from './util';
import * as rTerminal from './rTerminal';
import { getProcessAncestors } from './processTree';
import { TerminalSessionRegistry } from './terminalSessionRegistry';
import { SessionProcessMonitor } from './sessionProcessMonitor';
import { purgeAddinPickerItems, RSEditOperation, RSRange } from './rstudioapi';

import { extensionContext, rWorkspace, globalRHelp, globalPlotManager, sessionStatusBarItem, enableSessionWatcher } from './extension';
Expand All @@ -23,6 +24,7 @@ import { showWebView } from './webViewer';
import { getListViewerScript, listViewerStyle, ListViewNavigation } from './listViewer';
import { getDataViewerScript, getDataViewerStyle, getDataViewerToolbarHtml } from './dataViewer';
import { getDataViewerColumnPanelHtml, getDataViewerColumnPanelScript, getDataViewerColumnPanelStyle } from './dataViewerColumnPanel';
import { formatSessionLabel, getViewerSessionScript } from './viewerSession';

export interface SessionInfo {
version: string;
Expand Down Expand Up @@ -57,6 +59,12 @@ interface IpcSocket extends net.Socket {
}

export class Session {
private exited = false;
public get processExited(): boolean { return this.exited; }
public set processExited(value: boolean) {
this.exited = value;
if (value) { sessionProcessMonitor.markExited(this); }
}
public label?: string;
public workspaceUnavailable?: string;
public execute?: (code: string) => Promise<void>;
Expand Down Expand Up @@ -105,6 +113,7 @@ export let workspaceFile: string;
const SESS_PROTOCOL_VERSION = 2;

const sessions = new Map<string, Session>();
const sessionProcessMonitor = new SessionProcessMonitor<Session>(isLocalHost);
// Only the newest handshake for a stable ID may commit after terminal discovery.
const pendingSessionAttachments = new Map<string, IpcSocket>();
const terminalRegistry = new TerminalSessionRegistry<Session, vscode.Terminal>(
Expand Down Expand Up @@ -1109,7 +1118,7 @@ export async function showDataView(
}
existing.title = title;
existing.reveal(existing.viewColumn, true);
existing.webview.html = await getTableHtml(existing.webview, undefined, title);
existing.webview.html = await getTableHtml(existing.webview, undefined, title, sessionId);
return;
}
}
Expand All @@ -1126,11 +1135,12 @@ export async function showDataView(
localResourceRoots: [Uri.file(resDir)],
});
panel.iconPath = new UriIcon('open-preview');
attachViewerSessionBridge(panel, sessionId);
if (viewId) {
registerDataViewPanel(panel, panelKey, viewId, sessionId, stateGeneration);
attachDynamicDataViewBridge(panel, viewId, sessionId);
}
const content = await getTableHtml(panel.webview, file || undefined, title);
const content = await getTableHtml(panel.webview, file || undefined, title, sessionId);
panel.webview.html = content;
} else if (source === 'list') {
if (viewId) {
Expand All @@ -1142,7 +1152,7 @@ export async function showDataView(
existing.title = title;
existing.reveal(existing.viewColumn, true);
existing.webview.html = getListHtml(
existing.webview, title, navigation
existing.webview, title, navigation, sessionId
);
return;
}
Expand All @@ -1160,6 +1170,7 @@ export async function showDataView(
localResourceRoots: [Uri.file(resDir)],
});
panel.iconPath = new UriIcon('preview');
attachViewerSessionBridge(panel, sessionId);
if (viewId) {
registerDataViewPanel(panel, panelKey, viewId, sessionId, stateGeneration);
const webview = panel.webview;
Expand Down Expand Up @@ -1214,7 +1225,7 @@ export async function showDataView(
});
}
panel.webview.html = getListHtml(
panel.webview, title, navigation
panel.webview, title, navigation, sessionId
);
} else {
await commands.executeCommand('vscode.open', Uri.file(file), {
Expand All @@ -1226,7 +1237,41 @@ export async function showDataView(
console.info('[showDataView] Done');
}

export async function getTableHtml(webview: Webview, file: string | undefined, title: string): Promise<string> {
function getViewerSessionHtml(sessionId: string | null): string {
const owner = sessions.get(sessionId ?? '');
if (!owner) { return ''; }
const exited = sessionProcessMonitor.hasExited(owner);
if (!exited && (!owner.pid || !owner.rVer)) { return ''; }
const info = escapeHtml(exited ? 'R: (not attached)' : formatSessionLabel(owner.rVer, owner.pid));
return `<span class="viewer-session" role="img" tabindex="0" aria-label="${info}" aria-describedby="viewer-session-tooltip"><span class="codicon codicon-info" aria-hidden="true"></span><span id="viewer-session-tooltip" class="viewer-session-tooltip" role="tooltip">${info}</span></span>`;
}

function attachViewerSessionBridge(panel: vscode.WebviewPanel, sessionId: string | null): void {
const owner = sessions.get(sessionId ?? '');
if (!owner || (!sessionProcessMonitor.hasExited(owner) && (!owner.pid || !owner.rVer))) { return; }
// Keep the original process identity after detaching, reconnecting or restarting.
const sourcePid = owner.pid;
const attachedLabel = formatSessionLabel(owner.rVer, sourcePid);
const refresh = (force = false) => {
const text = source.exited ? 'R: (not attached)' : attachedLabel;
if (force || text !== lastLabel) {
lastLabel = text;
void panel.webview.postMessage({ message: 'viewer-session/update', text });
}
};
const source = sessionProcessMonitor.observe(owner, refresh);
let lastLabel = source.exited ? 'R: (not attached)' : attachedLabel;
const received = panel.webview.onDidReceiveMessage((message: { message?: string }) => {
if (message?.message === 'viewer-session/ready') { refresh(true); }
});
panel.onDidDispose(() => { source.dispose(); received?.dispose(); });
}

export async function getTableHtml(
webview: Webview, file: string | undefined, title: string, sessionId: string | null = null,
): Promise<string> {
const sessionHtml = getViewerSessionHtml(sessionId);
const codicons = webview.asWebviewUri(Uri.file(path.join(resDir, 'codicon.css'))).toString();
const pageSize = config().get<number>('session.data.pageSize', 500);
if (!file) {
const documentGeneration = ++documentGenerationRevision;
Expand All @@ -1238,6 +1283,7 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(title)}</title>
<link rel="stylesheet" href="${codicons}">
<style media="only screen">
html, body {
height: 100%;
Expand Down Expand Up @@ -1647,7 +1693,7 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
const info = document.createElement('span');
info.textContent = 'Full data';
info.title = 'This view retains the full object without a copy. Reference edits may appear here. Reopen from the cell to refresh after edits. Sorting and filtering scan the full data.';
document.querySelector('#viewerToolbar').append(info);
document.querySelector('#viewerControls').append(info);
}
const bigintFields = prepareViewerColumns(columns);
updateViewerRowCount(filteredRows, totalRows);
Expand Down Expand Up @@ -1744,7 +1790,7 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
</script>
</head>
<body>
${getDataViewerToolbarHtml()}
${getDataViewerToolbarHtml(sessionHtml)}
<div id="gridContainer">
<div id="myGrid" style="height: 100%;"></div>
${getDataViewerColumnPanelHtml()}
Expand All @@ -1754,6 +1800,7 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
<button id="fetchRetryBtn" type="button">Retry</button>
</div>
</div>
<script>${getViewerSessionScript()}</script>
</body>
</html>
`;
Expand All @@ -1767,6 +1814,7 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(title)}</title>
<link rel="stylesheet" href="${codicons}">
<style media="only screen">
html, body {
height: 100%;
Expand Down Expand Up @@ -1913,11 +1961,12 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
</script>
</head>
<body onload='onload()'>
${getDataViewerToolbarHtml()}
${getDataViewerToolbarHtml(sessionHtml)}
<div id="gridContainer">
<div id="myGrid" style="height: 100%;"></div>
${getDataViewerColumnPanelHtml()}
</div>
<script>${getViewerSessionScript()}</script>
</body>
</html>
`;
Expand All @@ -1926,7 +1975,8 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
export function getListHtml(
webview: Webview,
title: string,
navigation?: ListViewNavigation
navigation?: ListViewNavigation,
sessionId: string | null = null,
): string {
const documentGeneration = ++documentGenerationRevision;
documentGenerations.set(webview, documentGeneration);
Expand Down Expand Up @@ -1962,13 +2012,15 @@ export function getListHtml(
<button id="back" class="back" title="Back" aria-label="Back" disabled><span class="codicon codicon-arrow-left" aria-hidden="true"></span>Back</button>
<nav id="breadcrumbs" class="breadcrumbs" aria-label="Object path"></nav>
<button id="reset" class="reset" title="Reset to initial view" aria-label="Reset to initial view"><span class="codicon codicon-discard" aria-hidden="true"></span>Reset</button>
${getViewerSessionHtml(sessionId)}
</div>
<div id="navigation-status" class="navigation-status" role="status"></div>
<div id="list" class="list"></div>
<script>
${getListViewerScript(documentGeneration, navigation ?? {
title, path: [], breadcrumbs: [{ label: title, path: [] }],
})}
${getViewerSessionScript()}
</script>
</body>
</html>
Expand Down Expand Up @@ -2016,8 +2068,7 @@ async function refreshActiveSession(session: Session): Promise<void> {
workspaceData = session.workspaceData;

if (sessionStatusBarItem) {
const version = rVer.replace(/^R (?:version )?/, '').replace(/\s+\(.*/, '');
sessionStatusBarItem.text = `R ${version}: ${pid}`;
sessionStatusBarItem.text = formatSessionLabel(rVer, 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();
}
Expand Down
Loading
Loading