From 5d1760398d119522d8f97e1146e7a09e4fa43303 Mon Sep 17 00:00:00 2001 From: Fred Wu Date: Thu, 24 Apr 2025 13:01:02 +1000 Subject: [PATCH 01/15] Data viewer update --- R/session/vsc.R | 25 +++++++++-- package.json | 8 +++- src/liveShare/shareSession.ts | 3 +- src/session.ts | 79 +++++++++++++++++++++++++++++++---- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/R/session/vsc.R b/R/session/vsc.R index 5ab461394..9c3c08898 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -456,6 +456,9 @@ if (use_httpgd && "httpgd" %in% .packages(all.available = TRUE)) { show_view <- !identical(getOption("vsc.view", "Two"), FALSE) if (show_view) { + # Create registry to track dataview UUIDs by title + dataview_registry <- new.env(parent = emptyenv()) + get_column_def <- function(name, field, value) { filter <- TRUE tooltip <- sprintf( @@ -535,10 +538,24 @@ if (show_view) { return(.data) } + # Generate a dataview_uuid (separate from the LiveShare uuid) + dataview_uuid <- NULL if (missing(title)) { sub <- substitute(x) title <- deparse(sub, nlines = 1) } + + # Generate a unique ID for this dataview based on the title + title_key <- title + if (exists(title_key, envir = dataview_registry, inherits = FALSE)) { + dataview_uuid <- get(title_key, envir = dataview_registry, inherits = FALSE) + logger("Reusing existing dataview UUID for title:", title, "UUID:", dataview_uuid) + } else { + dataview_uuid <- paste0("dataview-", format(Sys.time(), "%Y%m%d%H%M%S"), "-", sample(1000:9999, 1)) + assign(title_key, dataview_uuid, envir = dataview_registry) + logger("Created new dataview UUID for title:", title, "UUID:", dataview_uuid) + } + if (inherits(x, "ArrowTabular")) { x <- as_truncated_data(x) x <- as.data.frame(x) @@ -602,21 +619,21 @@ if (show_view) { file <- tempfile(tmpdir = tempdir, fileext = ".json") jsonlite::write_json(data, file, na = "string", null = "null", auto_unbox = TRUE, force = TRUE) request("dataview", source = "table", type = "json", - title = title, file = file, viewer = viewer, uuid = uuid + title = title, file = file, viewer = viewer, uuid = uuid, dataview_uuid = dataview_uuid ) } else if (is.list(x)) { tryCatch({ file <- tempfile(tmpdir = tempdir, fileext = ".json") jsonlite::write_json(x, file, na = "string", null = "null", auto_unbox = TRUE, force = TRUE) request("dataview", source = "list", type = "json", - title = title, file = file, viewer = viewer, uuid = uuid + title = title, file = file, viewer = viewer, uuid = uuid, dataview_uuid = dataview_uuid ) }, error = function(e) { file <- file.path(tempdir, paste0(make.names(title), ".txt")) text <- utils::capture.output(print(x)) writeLines(text, file) request("dataview", source = "object", type = "txt", - title = title, file = file, viewer = viewer, uuid = uuid + title = title, file = file, viewer = viewer, uuid = uuid, dataview_uuid = dataview_uuid ) }) } else { @@ -628,7 +645,7 @@ if (show_view) { } writeLines(code, file) request("dataview", source = "object", type = "R", - title = title, file = file, viewer = viewer, uuid = uuid + title = title, file = file, viewer = viewer, uuid = uuid, dataview_uuid = dataview_uuid ) } } diff --git a/package.json b/package.json index 981687150..a9cf9523e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "activationEvents": [ "workspaceContains:**/*.{rproj,Rproj,r,R,rd,Rd,rmd,Rmd}", "onCommand:r.runSelectionInActiveTerm", - "onWebviewPanel:rhelp" + "onWebviewPanel:rhelp", + "onWebviewPanel:rdataviewer" ], "main": "./dist/extension", "contributes": { @@ -1872,6 +1873,11 @@ }, "additionalProperties": false }, + "r.dataViewer.reuseWindow": { + "type": "boolean", + "default": true, + "markdownDescription": "Reuse the existing data viewer window when `View()` is called multiple times. When enabled, only one data viewer window will be shown and refreshed with new data." + }, "r.rtermSendDelay": { "type": "integer", "default": 8, diff --git a/src/liveShare/shareSession.ts b/src/liveShare/shareSession.ts index d11b930e0..6882202b1 100644 --- a/src/liveShare/shareSession.ts +++ b/src/liveShare/shareSession.ts @@ -36,6 +36,7 @@ export interface IRequest { url?: string; requestPath?: string; uuid?: number; + dataview_uuid?: string; // Add this property tempdir?: string; version?: string; info?: { @@ -149,7 +150,7 @@ export async function updateGuestRequest(file: string, force: boolean = false): if (request.source && request.type && request.title && request.file && request.viewer !== undefined) { await showDataView(request.source, - request.type, request.title, request.file, request.viewer); + request.type, request.title, request.file, request.viewer, request.dataview_uuid); } break; } diff --git a/src/session.ts b/src/session.ts index b5959019a..595d88a33 100644 --- a/src/session.ts +++ b/src/session.ts @@ -67,6 +67,9 @@ let activeBrowserPanel: WebviewPanel | undefined; let activeBrowserUri: Uri | undefined; let activeBrowserExternalUri: Uri | undefined; +// Add a map to track dataview panels by UUID +const dataviewPanels = new Map(); + export function deploySessionWatcher(extensionPath: string): void { console.info(`[deploySessionWatcher] extensionPath: ${extensionPath}`); resDir = path.join(extensionPath, 'dist', 'resources'); @@ -334,15 +337,59 @@ export async function showWebView(file: string, title: string, viewer: string | console.info('[showWebView] Done'); } -export async function showDataView(source: string, type: string, title: string, file: string, viewer: string): Promise { - console.info(`[showDataView] source: ${source}, type: ${type}, title: ${title}, file: ${file}, viewer: ${viewer}`); +export async function showDataView(source: string, type: string, title: string, file: string, viewer: string, dataview_uuid?: string): Promise { + console.info(`[showDataView] source: ${source}, type: ${type}, title: ${title}, file: ${file}, viewer: ${viewer}, dataview_uuid: ${dataview_uuid}`); if (isGuestSession) { resDir = guestResDir; } + // Check if we have an existing panel with this UUID + let panel: WebviewPanel | undefined; + if (dataview_uuid && dataviewPanels.has(dataview_uuid)) { + panel = dataviewPanels.get(dataview_uuid); + // Panel might have been closed, check if it's still valid + if (panel) { + try { + panel.title = title; // Update title + panel.reveal(ViewColumn[viewer as keyof typeof ViewColumn]); + + if (source === 'table') { + // Update existing panel content + const content = await getTableHtml(panel.webview, file); + panel.webview.html = content; + } else if (source === 'list') { + const content = await getListHtml(panel.webview, file); + panel.webview.html = content; + } else { + // For other types, we'll still reopen, but use the existing panel + if (isGuestSession) { + const fileContent = await rGuestService?.requestFileContent(file, 'utf8'); + if (fileContent) { + await openVirtualDoc(file, fileContent, true, true, ViewColumn[viewer as keyof typeof ViewColumn]); + } + } else { + await commands.executeCommand('vscode.open', Uri.file(file), { + preserveFocus: true, + preview: true, + viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], + }); + } + } + console.info('[showDataView] Updated existing panel'); + return; + } catch (e) { + console.log(`Panel was disposed, creating new one: ${e}`); + // Panel was disposed, remove from registry + dataviewPanels.delete(dataview_uuid); + panel = undefined; + } + } + } + + // Create a new panel if needed if (source === 'table') { - const panel = window.createWebviewPanel('dataview', title, + panel = window.createWebviewPanel('dataview', title, { preserveFocus: true, viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], @@ -356,8 +403,16 @@ export async function showDataView(source: string, type: string, title: string, const content = await getTableHtml(panel.webview, file); panel.iconPath = new UriIcon('open-preview'); panel.webview.html = content; + + // Register panel if we have a UUID + if (dataview_uuid) { + dataviewPanels.set(dataview_uuid, panel); + panel.onDidDispose(() => { + dataviewPanels.delete(dataview_uuid!); + }); + } } else if (source === 'list') { - const panel = window.createWebviewPanel('dataview', title, + panel = window.createWebviewPanel('dataview', title, { preserveFocus: true, viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], @@ -371,6 +426,14 @@ export async function showDataView(source: string, type: string, title: string, const content = await getListHtml(panel.webview, file); panel.iconPath = new UriIcon('open-preview'); panel.webview.html = content; + + // Register panel if we have a UUID + if (dataview_uuid) { + dataviewPanels.set(dataview_uuid, panel); + panel.onDidDispose(() => { + dataviewPanels.delete(dataview_uuid!); + }); + } } else { if (isGuestSession) { const fileContent = await rGuestService?.requestFileContent(file, 'utf8'); @@ -738,7 +801,8 @@ export async function writeSuccessResponse(responseSessionDir: string): Promise< type ISessionRequest = { plot_url?: string, - server?: SessionServer + server?: SessionServer, + dataview_uuid?: string // Add this property to match the R code } & IRequest; async function updateRequest(sessionStatusBarItem: StatusBarItem) { @@ -753,7 +817,7 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { console.info(`[updateRequest] request: ${requestContent}`); const request = JSON.parse(requestContent) as ISessionRequest; if (request.wd && isFromWorkspace(request.wd)) { - if (request.uuid === null || request.uuid === undefined || request.uuid === UUID) { + if (request.uuid === null || request.uuid === undefined || String(request.uuid) === String(UUID)) { switch (request.command) { case 'help': { if (globalRHelp && request.requestPath) { @@ -818,8 +882,9 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { } case 'dataview': { if (request.source && request.type && request.file && request.title && request.viewer !== undefined) { + // Use dataview_uuid for panel tracking, preserve uuid for LiveShare await showDataView(request.source, - request.type, request.title, request.file, request.viewer); + request.type, request.title, request.file, request.viewer, request.dataview_uuid); } break; } From 2e1076d6174f9a2b1383e32a3aa7325b627b422e Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:13:13 +1000 Subject: [PATCH 02/15] infinite row model --- R/session/init.R | 2 +- R/session/vsc.R | 215 ++++++++++++++++++++++++--------------- src/session.ts | 257 +++++++++++++++++++++++++++++------------------ 3 files changed, 294 insertions(+), 180 deletions(-) diff --git a/R/session/init.R b/R/session/init.R index 410263c2e..ae6467716 100644 --- a/R/session/init.R +++ b/R/session/init.R @@ -17,7 +17,7 @@ init_first <- function() { } # check required packages - required_packages <- c("jsonlite", "rlang") + required_packages <- c("jsonlite", "rlang", "data.table") missing_packages <- required_packages[ !vapply(required_packages, requireNamespace, logical(1L), quietly = TRUE diff --git a/R/session/vsc.R b/R/session/vsc.R index 9c3c08898..9ab4be9d8 100644 --- a/R/session/vsc.R +++ b/R/session/vsc.R @@ -67,6 +67,130 @@ if (is.null(getOption("help_type"))) { } use_webserver <- isTRUE(getOption("vsc.use_webserver", FALSE)) + +get_column_def <- function(name, field, value) { + filter <- TRUE + tooltip <- sprintf( + "%s, class: [%s], type: %s", + name, + toString(class(value)), + typeof(value) + ) + if (is.numeric(value)) { + type <- "numericColumn" + if (is.null(attr(value, "class"))) { + filter <- "agNumberColumnFilter" + } + } else if (inherits(value, "Date")) { + type <- "dateColumn" + filter <- "agDateColumnFilter" + } else { + type <- "textColumn" + filter <- "agTextColumnFilter" + } + list( + headerName = name, + headerTooltip = tooltip, + field = field, + type = type, + filter = filter + ) +} + +dataview_table <- function(data, start = 0, end = NULL, sortModel = NULL) { + + if (is.matrix(data)) { + data <- as.data.frame.matrix(data) + } + if (!is.data.frame(data)) { + stop("data must be a data.frame or a matrix") + } + + data.table::setDT(data) + + # number of rows & original column names + .nrow <- nrow(data) + .colnames <- colnames(data) + if (is.null(.colnames)) { + .colnames <- sprintf("V%d", seq_len(ncol(data))) + } else { + .colnames <- trimws(.colnames) + } + + # capture or generate rownames + if (.row_names_info(data) > 0L) { + rownames_ <- rownames(data) + rownames(data) <- NULL + } else { + rownames_ <- seq_len(.nrow) + } + + .colnames <- c("(row)", .colnames) + fields <- sprintf("x%d", seq_along(.colnames)) + + # map x1→"(row)", x2→first real col, … + field_map <- setNames(.colnames, fields) + + # ── SORT data before slicing ── + if (!is.null(sortModel) && length(sortModel) > 0) { + + sort <- sortModel[[1]] + col_f <- sort$colId + dir <- sort$sort + real <- field_map[[col_f]] + + # only sort if it's one of your real data columns + if (!is.null(real) && real %in% .colnames[-1]) { + + # attach rownames_ as a helper column + data[, "__rownames__" := rownames_] + + # sort in place: order=1L for asc, -1L for desc + data.table::setorderv( + data, + cols = real, + order = if (dir == "asc") 1L else -1L + ) + + # pull the helper back out as rownames_ + rownames_ <- data[["__rownames__"]] + data[, "__rownames__" := NULL] + } + } + + if (is.null(end)) end <- .nrow + s <- as.integer(start) + 1 + e <- min(.nrow, as.integer(end)) + + if (s > .nrow || e < 1 || s > e) { + rows <- data[0, , drop = FALSE] + rownums <- integer(0) + } else { + rows <- data[s:e, , drop = FALSE] + rownums <- rownames_[s:e] + } + + rows <- c(list(" " = rownums), .subset(rows)) + names(rows) <- fields + class(rows) <- "data.frame" + attr(rows, "row.names") <- .set_row_names(length(rownums)) + + rows <- jsonlite::fromJSON( + jsonlite::toJSON(rows, dataframe = "rows", na = "null", auto_unbox = TRUE) + ) + columns <- .mapply( + get_column_def, + list(.colnames, fields, rows), + NULL + ) + + list( + columns = columns, + rows = rows, + totalRows = .nrow + ) +} + if (use_webserver) { if (requireNamespace("httpuv", quietly = TRUE)) { request_handlers <- list( @@ -120,6 +244,12 @@ if (use_webserver) { }) return(result) } + }, + dataview_fetch_rows = function(varname, start, end, sortModel, ...) { + obj <- get(varname, envir = .GlobalEnv) + out <- dataview_table(obj, start, end, sortModel) + out$columns <- NULL + return(out) } ) @@ -456,76 +586,8 @@ if (use_httpgd && "httpgd" %in% .packages(all.available = TRUE)) { show_view <- !identical(getOption("vsc.view", "Two"), FALSE) if (show_view) { - # Create registry to track dataview UUIDs by title dataview_registry <- new.env(parent = emptyenv()) - get_column_def <- function(name, field, value) { - filter <- TRUE - tooltip <- sprintf( - "%s, class: [%s], type: %s", - name, - toString(class(value)), - typeof(value) - ) - if (is.numeric(value)) { - type <- "numericColumn" - if (is.null(attr(value, "class"))) { - filter <- "agNumberColumnFilter" - } - } else if (inherits(value, "Date")) { - type <- "dateColumn" - filter <- "agDateColumnFilter" - } else { - type <- "textColumn" - filter <- "agTextColumnFilter" - } - list( - headerName = name, - headerTooltip = tooltip, - field = field, - type = type, - filter = filter - ) - } - - dataview_table <- function(data) { - if (is.matrix(data)) { - data <- as.data.frame.matrix(data) - } - - if (is.data.frame(data)) { - .nrow <- nrow(data) - .colnames <- colnames(data) - if (is.null(.colnames)) { - .colnames <- sprintf("V%d", seq_len(ncol(data))) - } else { - .colnames <- trimws(.colnames) - } - if (.row_names_info(data) > 0L) { - rownames <- rownames(data) - rownames(data) <- NULL - } else { - rownames <- seq_len(.nrow) - } - .colnames <- c("(row)", .colnames) - fields <- sprintf("x%d", seq_along(.colnames)) - data <- c(list(" " = rownames), .subset(data)) - names(data) <- fields - class(data) <- "data.frame" - attr(data, "row.names") <- .set_row_names(.nrow) - columns <- .mapply(get_column_def, - list(.colnames, fields, data), - NULL - ) - list( - columns = columns, - data = data - ) - } else { - stop("data must be a data.frame or a matrix") - } - } - show_dataview <- function(x, title, uuid = NULL, viewer = getOption("vsc.view", "Two"), row_limit = abs(getOption("vsc.row_limit", 0))) { @@ -615,9 +677,11 @@ if (show_view) { } if (is.data.frame(x) || is.matrix(x)) { x <- as_truncated_data(x) - data <- dataview_table(x) + # Get initial chunk of data (first 100 rows) + meta <- dataview_table(x, start = 0, end = 1) + meta$rows <- list() file <- tempfile(tmpdir = tempdir, fileext = ".json") - jsonlite::write_json(data, file, na = "string", null = "null", auto_unbox = TRUE, force = TRUE) + jsonlite::write_json(meta, file, na = "string", null = "null", auto_unbox = TRUE, force = TRUE) request("dataview", source = "table", type = "json", title = title, file = file, viewer = viewer, uuid = uuid, dataview_uuid = dataview_uuid ) @@ -690,8 +754,6 @@ path_to_uri <- function(path) { } request_browser <- function(url, title, ..., viewer) { - # Printing URL with specific port triggers - # auto port-forwarding under remote development message("Browsing ", url) request("browser", url = url, title = title, ..., viewer = viewer) } @@ -820,7 +882,6 @@ options( page_viewer = show_page_viewer ) -# rstudioapi rstudioapi_enabled <- function() { isTRUE(getOption("vsc.rstudioapi", TRUE)) } @@ -832,12 +893,11 @@ if (rstudioapi_enabled()) { file.create(response_lock_file, showWarnings = FALSE) file.create(response_file, showWarnings = FALSE) addin_registry <- file.path(dir_session, "addins.json") - # This is created in attach() get_response_timestamp <- function() { readLines(response_lock_file) } - # initialise the reponse timestamp to empty string + response_time_stamp <- "" get_response_lock <- function() { @@ -876,10 +936,6 @@ if (rstudioapi_enabled()) { } ) if ("rstudioapi" %in% loadedNamespaces()) { - # if the rstudioapi is already loaded, for example via a call to - # library(tidyverse) in the user's profile, we need to shim it now. - # There's no harm in having also registered the hook in this case. It can - # work in the event that the namespace is unloaded and reloaded. rstudioapi_util_env$rstudioapi_patch_hook(rstudioapi_env) } @@ -945,7 +1001,6 @@ print.hsearch <- function(x, ...) { invisible(x) } -# a copy of .S3method(), since this function is new in R 4.0 .S3method <- function(generic, class, method) { if (missing(method)) { method <- paste(generic, class, sep = ".") diff --git a/src/session.ts b/src/session.ts index 595d88a33..8bcc3ba36 100644 --- a/src/session.ts +++ b/src/session.ts @@ -9,13 +9,14 @@ import { commands, StatusBarItem, Uri, ViewColumn, Webview, window, workspace, e import { runTextInTerm } from './rTerminal'; import { FSWatcher } from 'fs-extra'; -import { config, readContent, setContext, UriIcon } from './util'; +import { config, readContent, setContext, UriIcon} from './util'; import { purgeAddinPickerItems, dispatchRStudioAPICall } from './rstudioapi'; import { IRequest } from './liveShare/shareSession'; import { homeExtDir, rWorkspace, globalRHelp, globalHttpgdManager, extensionContext, sessionStatusBarItem } from './extension'; import { UUID, rHostService, rGuestService, isLiveShare, isHost, isGuestSession, closeBrowser, guestResDir, shareBrowser, openVirtualDoc, shareWorkspace } from './liveShare'; + export interface GlobalEnv { [key: string]: { class: string[]; @@ -41,6 +42,11 @@ export interface SessionServer { token: string; } +interface WebviewMessage { + command: string; + start?: number; + end?: number; +} export let workspaceData: WorkspaceData; let resDir: string; export let requestFile: string; @@ -338,124 +344,141 @@ export async function showWebView(file: string, title: string, viewer: string | } export async function showDataView(source: string, type: string, title: string, file: string, viewer: string, dataview_uuid?: string): Promise { - console.info(`[showDataView] source: ${source}, type: ${type}, title: ${title}, file: ${file}, viewer: ${viewer}, dataview_uuid: ${dataview_uuid}`); + console.info(`[showDataView] source: ${source}, type: ${type}, title: ${title}, file: ${file}, viewer: ${viewer}, dataview_uuid: ${String(dataview_uuid)}`); if (isGuestSession) { resDir = guestResDir; } - // Check if we have an existing panel with this UUID + // Check if we have an existing panel with this UUID let panel: WebviewPanel | undefined; if (dataview_uuid && dataviewPanels.has(dataview_uuid)) { panel = dataviewPanels.get(dataview_uuid); // Panel might have been closed, check if it's still valid if (panel) { try { - panel.title = title; // Update title + panel.title = title; panel.reveal(ViewColumn[viewer as keyof typeof ViewColumn]); - - if (source === 'table') { - // Update existing panel content - const content = await getTableHtml(panel.webview, file); - panel.webview.html = content; - } else if (source === 'list') { - const content = await getListHtml(panel.webview, file); - panel.webview.html = content; - } else { - // For other types, we'll still reopen, but use the existing panel - if (isGuestSession) { - const fileContent = await rGuestService?.requestFileContent(file, 'utf8'); - if (fileContent) { - await openVirtualDoc(file, fileContent, true, true, ViewColumn[viewer as keyof typeof ViewColumn]); - } - } else { - await commands.executeCommand('vscode.open', Uri.file(file), { - preserveFocus: true, - preview: true, - viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], - }); - } - } - console.info('[showDataView] Updated existing panel'); - return; } catch (e) { - console.log(`Panel was disposed, creating new one: ${e}`); - // Panel was disposed, remove from registry + console.log(`Panel was disposed, creating new one: ${String(e)}`); dataviewPanels.delete(dataview_uuid); panel = undefined; } } } - // Create a new panel if needed - if (source === 'table') { - panel = window.createWebviewPanel('dataview', title, - { - preserveFocus: true, - viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], - }, - { - enableScripts: true, - enableFindWidget: true, - retainContextWhenHidden: true, - localResourceRoots: [Uri.file(resDir)], - }); - const content = await getTableHtml(panel.webview, file); - panel.iconPath = new UriIcon('open-preview'); - panel.webview.html = content; - - // Register panel if we have a UUID - if (dataview_uuid) { - dataviewPanels.set(dataview_uuid, panel); - panel.onDidDispose(() => { - dataviewPanels.delete(dataview_uuid!); - }); - } - } else if (source === 'list') { - panel = window.createWebviewPanel('dataview', title, - { - preserveFocus: true, - viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], - }, - { - enableScripts: true, - enableFindWidget: true, - retainContextWhenHidden: true, - localResourceRoots: [Uri.file(resDir)], - }); - const content = await getListHtml(panel.webview, file); - panel.iconPath = new UriIcon('open-preview'); - panel.webview.html = content; - - // Register panel if we have a UUID - if (dataview_uuid) { - dataviewPanels.set(dataview_uuid, panel); - panel.onDidDispose(() => { - dataviewPanels.delete(dataview_uuid!); - }); - } - } else { - if (isGuestSession) { - const fileContent = await rGuestService?.requestFileContent(file, 'utf8'); - if (fileContent) { - await openVirtualDoc(file, fileContent, true, true, ViewColumn[viewer as keyof typeof ViewColumn]); + if (!panel) { + if (source === 'table' || source === 'list') { + panel = window.createWebviewPanel('dataview', title, + { + preserveFocus: true, + viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], + }, + { + enableScripts: true, + enableFindWidget: true, + retainContextWhenHidden: true, + localResourceRoots: [Uri.file(resDir)], + }); + + panel.iconPath = new UriIcon('open-preview'); + + if (dataview_uuid) { + dataviewPanels.set(dataview_uuid, panel); + panel.onDidDispose(() => { + dataviewPanels.delete(dataview_uuid); + }); } } else { - await commands.executeCommand('vscode.open', Uri.file(file), { - preserveFocus: true, - preview: true, - viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], - }); + if (isGuestSession) { + const fileContent = await rGuestService?.requestFileContent(file, 'utf8'); + if (fileContent) { + await openVirtualDoc(file, fileContent, true, true, ViewColumn[viewer as keyof typeof ViewColumn]); + } + } else { + await commands.executeCommand('vscode.open', Uri.file(file), { + preserveFocus: true, + preview: true, + viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], + }); + } } } + + // Register the message handler after panel is created or retrieved, but only once per panel + if (panel) { + panel.webview.onDidReceiveMessage(async (message: WebviewMessage & { + requestId?: string; + sortModel?: Array<{ colId: string; sort: 'asc' | 'desc' }>; + }) => { + if (message.command === 'fetchRows') { + try { + const { start, end, sortModel, requestId } = message; + if (!server) { + throw new Error('R server not available'); + } + const response: unknown = await sessionRequest(server, { + type: 'dataview_fetch_rows', + varname: title, + start, + end, + sortModel + }); + if (typeof response !== 'object' || response === null || !('rows' in response) || !('totalRows' in response)) { + throw new Error('Invalid response from R server'); + } + const rows: unknown = (response as {rows: object[]}).rows; + const totalRows: unknown = (response as {totalRows: number}).totalRows; + if (!Array.isArray(rows) || typeof totalRows !== 'number') { + throw new Error('Fetched rows or totalRows invalid'); + } + await panel?.webview.postMessage({ + command: 'fetchedRows', + start, + end, + rows: rows as object[], + totalRows: totalRows, + requestId + }); + } catch (error) { + console.error('[fetchRows] Error:', error); + await panel?.webview.postMessage({ + command: 'fetchError', + error: String(error), + requestId: message.requestId + }); + } + } + }); + } + + if (panel) { + if (source === 'table') { + // Persistent ag-Grid request map for matching responses + await panel.webview.postMessage({ command: 'initAgGridRequestMap' }); + + const content = await getTableHtml(panel.webview, file); + panel.webview.html = content; + } else if (source === 'list') { + const content = await getListHtml(panel.webview, file); + panel.webview.html = content; + } + } + console.info('[showDataView] Done'); } export async function getTableHtml(webview: Webview, file: string): Promise { - resDir = isGuestSession ? guestResDir : resDir; - const pageSize = config().get('session.data.pageSize', 500); - const content = await readContent(file, 'utf8'); - return ` + try { + resDir = isGuestSession ? guestResDir : resDir; + const pageSize = config().get('session.data.pageSize', 500); + const content = await readContent(file, 'utf8'); + if (!content) { + console.error('[getTableHtml] Empty content'); + throw new Error('Empty content in getTableHtml'); + } + const data = JSON.parse(content); + return ` @@ -557,6 +580,7 @@ export async function getTableHtml(webview: Webview, file: string): Promise