From 21125bcacc8606937dff6459ca6f7f7f1fbb1395 Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Tue, 12 May 2026 21:57:07 +1000 Subject: [PATCH 1/2] feat(data viewer): Allow reusing data viewer instead of opening a new one --- sess/R/handlers.R | 19 ++++++++++++++++--- sess/R/hooks.R | 20 +++++++++++++++++++- sess/R/server.R | 3 +++ src/session.ts | 38 +++++++++++++++++++++++++++----------- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/sess/R/handlers.R b/sess/R/handlers.R index f08b2ff9..6de0e8b3 100644 --- a/sess/R/handlers.R +++ b/sess/R/handlers.R @@ -261,12 +261,25 @@ dataview_columns <- function(state) { }, list(state$headers, state$types, seq_along(state$headers), state$columns), NULL) } -dataview_register <- function(data) { +dataview_new_id <- function() { + repeat { + ts <- gsub("[^0-9]", "", format(Sys.time(), "%Y%m%d%H%M%OS6"), perl = TRUE) + view_id <- sprintf("dv_%s_%06d", ts, sample.int(999999L, 1L)) + if (is.null(.sess_env$dataviews) || is.null(.sess_env$dataviews[[view_id]])) { + return(view_id) + } + } +} + +dataview_register <- function(data, view_id = NULL) { if (is.null(.sess_env$dataviews)) { .sess_env$dataviews <- list() } - ts <- gsub("[^0-9]", "", format(Sys.time(), "%Y%m%d%H%M%OS6"), perl = TRUE) - view_id <- sprintf("dv_%s_%06d", ts, sample.int(999999L, 1L)) + + if (is.null(view_id)) { + view_id <- dataview_new_id() + } + state <- dataview_to_state(data) .sess_env$dataviews[[view_id]] <- state list( diff --git a/sess/R/hooks.R b/sess/R/hooks.R index 320d2210..48dd7858 100644 --- a/sess/R/hooks.R +++ b/sess/R/hooks.R @@ -5,6 +5,10 @@ #' @export register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE) { # 1. Override View() to serve table data via paged RPC. + if (is.null(.sess_env$dataview_registry)) { + .sess_env$dataview_registry <- new.env(parent = emptyenv()) + } + show_dataview <- function(x, title = deparse(substitute(x))) { # make sure title is computed. force(title) @@ -14,7 +18,21 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE) { } if (is.data.frame(x) || is.matrix(x)) { - registration <- dataview_register(x) + title_key <- paste(as.character(title), collapse = "\n") + dataview_registry <- .sess_env$dataview_registry + has_view_id <- nzchar(title_key) && + exists(title_key, envir = dataview_registry, inherits = FALSE) + view_id <- if (has_view_id) { + get(title_key, envir = dataview_registry, inherits = FALSE) + } else { + id <- dataview_new_id() + if (nzchar(title_key)) { + assign(title_key, id, envir = dataview_registry) + } + id + } + + registration <- dataview_register(x, view_id = view_id) notify_client("dataview", list( title = title, diff --git a/sess/R/server.R b/sess/R/server.R index 5ecf0ec0..26fc67e7 100644 --- a/sess/R/server.R +++ b/sess/R/server.R @@ -11,6 +11,9 @@ connect <- function(pipe_path = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE) .sess_env$pending_responses <- list() .sess_env$read_buffer <- "" .sess_env$dataviews <- list() + if (is.null(.sess_env$dataview_registry)) { + .sess_env$dataview_registry <- new.env(parent = emptyenv()) + } .sess_env$tempdir <- file.path(tempdir(), "sess") dir.create(.sess_env$tempdir, showWarnings = FALSE, recursive = TRUE) diff --git a/src/session.ts b/src/session.ts index 470f3254..8ddbca16 100644 --- a/src/session.ts +++ b/src/session.ts @@ -114,7 +114,8 @@ interface DataViewRequestMessage { filterModel?: Record; } -const dynamicDataViewPanels = new WeakSet(); +const dynamicDataViewPanels = new Map(); +let dynamicDataViewReloadRevision = 0; function escapeHtml(text: string): string { const map: Record = { @@ -132,8 +133,6 @@ function formatDataViewPanelTitle(baseTitle: string, totalRows: number): string } function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, baseTitle: string): void { - dynamicDataViewPanels.add(panel); - const postResponse = (requestId: number, ok: boolean, result?: unknown, error?: string) => { void panel.webview.postMessage({ message: 'dataview/response', @@ -192,8 +191,9 @@ function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, method: 'dataview_dispose', params: { view_id: viewId }, }); - // Remove from panels set to prevent duplicate disposal on panel close - dynamicDataViewPanels.delete(panel); + if (dynamicDataViewPanels.get(viewId) === panel) { + dynamicDataViewPanels.delete(viewId); + } postResponse(msg.requestId, true, true); return; } @@ -205,10 +205,10 @@ function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, }); panel.onDidDispose(() => { - if (!dynamicDataViewPanels.has(panel)) { + if (dynamicDataViewPanels.get(viewId) !== panel) { return; } - dynamicDataViewPanels.delete(panel); + dynamicDataViewPanels.delete(viewId); void sessionRequest({ method: 'dataview_dispose', params: { view_id: viewId }, @@ -637,11 +637,25 @@ export function openExternalBrowser(): void { } } -export async function showDataView(source: string, type: string, title: string, file: string, viewer: string, viewId?: string): Promise { +export async function showDataView(source: string, type: string, title: string, file: string, viewer: string, viewId?: string, totalRows?: number): Promise { console.info(`[showDataView] source: ${source}, type: ${type}, title: ${title}, file: ${file}, viewer: ${viewer}, viewId: ${String(viewId ?? '')}`); + const panelTitle = totalRows !== undefined && Number.isFinite(totalRows) + ? formatDataViewPanelTitle(title, totalRows) + : title; if (source === 'table') { - const panel = window.createWebviewPanel('dataview', title, + if (viewId) { + const existing = dynamicDataViewPanels.get(viewId); + if (existing) { + existing.title = panelTitle; + existing.reveal(ViewColumn[viewer as keyof typeof ViewColumn], true); + const content = await getTableHtml(existing.webview, undefined, title); + existing.webview.html = `${content}\n`; + return; + } + } + + const panel = window.createWebviewPanel('dataview', panelTitle, { preserveFocus: true, viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], @@ -652,12 +666,13 @@ export async function showDataView(source: string, type: string, title: string, retainContextWhenHidden: true, localResourceRoots: [Uri.file(resDir)], }); - const content = await getTableHtml(panel.webview, file || undefined, title); panel.iconPath = new UriIcon('open-preview'); - panel.webview.html = content; if (viewId) { + dynamicDataViewPanels.set(viewId, panel); attachDynamicDataViewBridge(panel, viewId, title); } + const content = await getTableHtml(panel.webview, file || undefined, title); + panel.webview.html = content; } else if (source === 'list') { const panel = window.createWebviewPanel('dataview', title, { @@ -1559,6 +1574,7 @@ async function handleNotification(message: Record, socket: IpcS String(params.file ?? ''), viewer, params.view_id ? String(params.view_id) : undefined, + typeof params.total_rows === 'number' ? params.total_rows : undefined, ); } } From d3cc86300eedd41e2f02832a9ebad8fb57ffdb26 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Tue, 12 May 2026 23:21:48 +0800 Subject: [PATCH 2/2] Remove rows in data viewer panel title --- sess/R/hooks.R | 3 +-- src/session.ts | 22 +++++----------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/sess/R/hooks.R b/sess/R/hooks.R index 48dd7858..624aecc6 100644 --- a/sess/R/hooks.R +++ b/sess/R/hooks.R @@ -38,8 +38,7 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE) { title = title, source = "table", type = "json", - view_id = registration$view_id, - total_rows = registration$total_rows + view_id = registration$view_id )) } else if (is.list(x)) { file_path <- tempfile(tmpdir = .sess_env$tempdir, fileext = ".json") diff --git a/src/session.ts b/src/session.ts index 8ddbca16..5ec62cab 100644 --- a/src/session.ts +++ b/src/session.ts @@ -128,10 +128,6 @@ function escapeHtml(text: string): string { return text.replace(/[&<>"']/g, c => map[c]); } -function formatDataViewPanelTitle(baseTitle: string, totalRows: number): string { - return `${baseTitle} (rows: ${totalRows.toLocaleString()})`; -} - function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, baseTitle: string): void { const postResponse = (requestId: number, ok: boolean, result?: unknown, error?: string) => { void panel.webview.postMessage({ @@ -158,9 +154,7 @@ function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, if (!result || typeof result.totalRows !== 'number') { throw new Error('Invalid dataview_init response: missing or invalid totalRows'); } - if (Number.isFinite(result.totalRows)) { - panel.title = formatDataViewPanelTitle(baseTitle, result.totalRows); - } + panel.title = baseTitle; postResponse(msg.requestId, true, result); return; } @@ -179,9 +173,7 @@ function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string, if (!result || typeof result.totalRows !== 'number') { throw new Error('Invalid dataview_page response: missing or invalid totalRows'); } - if (Number.isFinite(result.totalRows)) { - panel.title = formatDataViewPanelTitle(baseTitle, result.totalRows); - } + panel.title = baseTitle; postResponse(msg.requestId, true, result); return; } @@ -637,17 +629,14 @@ export function openExternalBrowser(): void { } } -export async function showDataView(source: string, type: string, title: string, file: string, viewer: string, viewId?: string, totalRows?: number): Promise { +export async function showDataView(source: string, type: string, title: string, file: string, viewer: string, viewId?: string): Promise { console.info(`[showDataView] source: ${source}, type: ${type}, title: ${title}, file: ${file}, viewer: ${viewer}, viewId: ${String(viewId ?? '')}`); - const panelTitle = totalRows !== undefined && Number.isFinite(totalRows) - ? formatDataViewPanelTitle(title, totalRows) - : title; if (source === 'table') { if (viewId) { const existing = dynamicDataViewPanels.get(viewId); if (existing) { - existing.title = panelTitle; + existing.title = title; existing.reveal(ViewColumn[viewer as keyof typeof ViewColumn], true); const content = await getTableHtml(existing.webview, undefined, title); existing.webview.html = `${content}\n`; @@ -655,7 +644,7 @@ export async function showDataView(source: string, type: string, title: string, } } - const panel = window.createWebviewPanel('dataview', panelTitle, + const panel = window.createWebviewPanel('dataview', title, { preserveFocus: true, viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], @@ -1574,7 +1563,6 @@ async function handleNotification(message: Record, socket: IpcS String(params.file ?? ''), viewer, params.view_id ? String(params.view_id) : undefined, - typeof params.total_rows === 'number' ? params.total_rows : undefined, ); } }