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
13 changes: 11 additions & 2 deletions sess/R/handlers.R
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,17 @@ listview_summary <- function(object) {
} else if (size > 1L) {
paste0(" [1:", size, "]")
}
paste0(type, shape, " ",
listview_format_values(object[1L]), if (size > 1L) " ...")
numeric_preview <- is.numeric(object) && !is.object(object)
preview_size <- if (numeric_preview) min(size, 5L) else 1L
# Subset before formatting so even very large vectors have a bounded preview.
values <- object[seq_len(preview_size)]
formatted <- if (numeric_preview) {
vapply(values, format, "", digits = 3L, trim = TRUE, justify = "none")
} else {
listview_format_values(values)
}
paste0(type, shape, " ", paste(formatted, collapse = " "),
if (size > preview_size) " ...")
} else {
trimws(try_capture_str(object))
}
Expand Down
11 changes: 11 additions & 0 deletions sess/R/interactive.R
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,17 @@ interactive_execute <- function(id, code, source = NULL) {
printed))
return(TRUE)
}
if (is.list(value) && !is.object(value)) {
view_id <- dataview_new_id()
# Nested panels reuse the root's owner; separate cell results must stay independent.
root <- listview_state(value, "List", paste0("interactive:", view_id))
dataview_set_state(view_id, root)
preview <- get_workspace_children(view_id = view_id)
.interactive_event("display", c(list(kind = "list", viewId = view_id,
navigation = listview_navigation(listview_location(root))),
preview, .interactive_table_text(value)))
return(TRUE)
}
if (inherits(value, "htmlwidget") && requireNamespace("htmlwidgets", quietly = TRUE)) {
directory <- tempfile("widget-")
dir.create(directory)
Expand Down
30 changes: 26 additions & 4 deletions sess/inst/tinytest/test-listview-format.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# List rows use the same class formatting and numeric precision as table cells.
# Individual vector rows retain table precision; numeric summaries are compact.
local({
runtime <- sess:::.sess_env
previous <- runtime$dataviews
Expand All @@ -19,17 +19,39 @@ local({
"0", "NA", "NaN", "Inf", "-Inf")
expect_equal(text(page(numbers)), expected)
expect_equal(text(page(list(nested = numbers), list(1L))), expected)
expect_equal(text(page(as.list(numbers))), paste("num", expected))
compact <- c("1.23", "1.54e-100", "-6.65e-13", "1.23e+14",
"0", "NA", "NaN", "Inf", "-Inf")
expect_equal(text(page(as.list(numbers))), paste("num", compact))
for (index in seq_along(numbers)) {
expect_true(sess:::listview_supported(numbers[index]))
expect_true(sess:::listview_is_vector(numbers[index]))
expect_equal(text(page(numbers[index])), expected[index])
expect_false(page(numbers[index])$children[[1L]]$has_children)
expect_false(page(numbers[index])$children[[1L]]$viewable)
}
expect_true(grepl(expected[[1L]], text(page(list(numbers)))[[1L]], fixed = TRUE))
expect_equal(text(page(list(numbers))),
"num [1:9] 1.23 1.54e-100 -6.65e-13 1.23e+14 0 ...")
expect_equal(text(page(list(matrix(rep(numbers[[1L]], 4L), nrow = 2L)))),
paste0("num [1:2, 1:2] ", expected[[1L]], " ..."))
"num [1:2, 1:2] 1.23 1.23 1.23 1.23")
values <- c(-1.33067613368562, -0.0827652250416069, 0.836484327361507,
1.23456789, -2.3456789)
root <- list(a = 1, b = rep(values, 2L), c = list(x = values, y = list(z = values[1:3])),
mtcars = datasets::mtcars)
expect_equal(text(page(root))[1:3],
c("num 1", "num [1:10] -1.33 -0.0828 0.836 1.23 -2.35 ...", "List of 2"))
expect_equal(text(page(root, list(3L)))[[1L]],
"num [1:5] -1.33 -0.0828 0.836 1.23 -2.35")
expect_equal(text(page(root, list(3L, 2L))), "num [1:3] -1.33 -0.0828 0.836")
expect_equal(text(page(list(integer(), numeric(), 1:6))),
c("int(0)", "num(0)", "int [1:6] 1 2 3 4 5 ..."))
expect_equal(text(page(list(c(0, NA_real_, NaN, Inf, -Inf)))),
"num [1:5] 0 NA NaN Inf -Inf")
# A summary reads only its prefix, including for compact ALTREP sequences.
expect_equal(text(page(list(seq_len(1000000000L)))),
"int [1:1000000000] 1 2 3 4 5 ...")
options(digits = 22L)
expect_equal(text(page(list(values))), "num [1:5] -1.33 -0.0828 0.836 1.23 -2.35")
options(digits = 3L)
expect_identical(numbers, c(
1.23456789012345, 1.54e-100, -6.65e-13, 1.23456789012345e14, 0, NA_real_, NaN, Inf, -Inf
))
Expand Down
36 changes: 31 additions & 5 deletions src/interactive/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
private closed = false;
private clientId: string;
private savedConnections: Map<string, SavedConnection>;
private outputViews: Map<string, { tableView?: string; selectedPlot?: string }>;
private outputViews: Map<string, { tableView?: string; listView?: string; selectedPlot?: string }>;
private status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 20);

constructor(private context: vscode.ExtensionContext) {
Expand Down Expand Up @@ -1064,14 +1064,15 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
archived: cell.generation !== view.model.generation, connected: view.client.connected,
running: !['exited', 'stopping'].includes(view.client.manifest.status) };
const preference = this.outputViews.get(`${view.client.manifest.id}:${cell.generation}:${String(data.displayId)}`);
if (data.kind === 'list') { display.listView = preference?.listView ?? 'list'; }
if (data.kind === 'table') { display.tableView = preference?.tableView ?? util.config().get('interactive.tableView', 'table'); }
if (data.svg || data.asset) { display.url = view.base + String(data.svg ?? data.asset).split('/').map(encodeURIComponent).join('/'); }
else if (typeof data.url === 'string' && /^https?:\/\/(localhost|127\.0\.0\.1)(:|\/)/.test(data.url)) {
display.url = (await vscode.env.asExternalUri(vscode.Uri.parse(data.url))).toString(true);
}
if (data.kind === 'mime' && data.mime === 'text/html') { display.kind = 'htmlText'; }
items = [vscode.NotebookCellOutputItem.json(display, DISPLAY_MIME),
vscode.NotebookCellOutputItem.text(data.kind === 'table' ? (typeof data.printedText === 'string' ? data.printedText : `${String(data.totalRows)} rows\n${JSON.stringify(data.rows, null, 2)}`) : `R ${String(data.kind)} output`)];
vscode.NotebookCellOutputItem.text(typeof data.printedText === 'string' ? data.printedText : data.kind === 'table' ? `${String(data.totalRows)} rows\n${JSON.stringify(data.rows, null, 2)}` : data.kind === 'list' ? JSON.stringify(data.children, null, 2) : `R ${String(data.kind)} output`)];
if (typeof data.svg === 'string') {
try {
const svg = portable ? readAsset(path.join(this.root, view.client.manifest.id, 'assets'), data.svg).toString('base64')
Expand Down Expand Up @@ -1185,19 +1186,20 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
const data = output.data;
try {
if ((message.action === 'tableView' && data.kind === 'table' && ['table', 'text'].includes(String(message.mode))) ||
(message.action === 'listView' && data.kind === 'list' && ['list', 'text'].includes(String(message.mode))) ||
(message.action === 'plotPage' && ['plot', 'image'].includes(String(data.kind)) && owner?.outputs.some(item => item.type === 'display' &&
['plot', 'image'].includes(String(item.data.kind)) && item.data.displayId === message.selectedPlot))) {
const key = `${view.client.manifest.id}:${owner.generation}:${String(data.displayId)}`;
const preference = message.action === 'tableView' ? { tableView: String(message.mode) } : { selectedPlot: String(message.selectedPlot) };
const preference = message.action === 'tableView' ? { tableView: String(message.mode) } : message.action === 'listView' ? { listView: String(message.mode) } : { selectedPlot: String(message.selectedPlot) };
this.outputViews.delete(key); this.outputViews.set(key, preference);
while (this.outputViews.size > 1000) { this.outputViews.delete(this.outputViews.keys().next().value as string); }
await this.context.workspaceState.update('r.interactive.outputViews', [...this.outputViews]);
return;
}
if (['table', 'page', 'resize'].includes(String(message.action)) && ['exited', 'stopping'].includes(view.client.manifest.status)) {
if (['table', 'page', 'list', 'listPage', 'listNavigate', 'listItem', 'resize'].includes(String(message.action)) && ['exited', 'stopping'].includes(view.client.manifest.status)) {
throw new Error('R has stopped. Start a new session and run the code again to use live controls.');
}
if (['table', 'page', 'resize'].includes(String(message.action)) && (view.restarting || owner?.generation !== view.model.generation)) {
if (['table', 'page', 'list', 'listPage', 'listNavigate', 'listItem', 'resize'].includes(String(message.action)) && (view.restarting || owner?.generation !== view.model.generation)) {
throw new Error('This output belongs to a previous R process. Run its code again to use live controls.');
}
let result: unknown;
Expand All @@ -1209,6 +1211,28 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
if (data.kind !== 'table') { return; }
result = await queryTablePage(data, message, request => view.client.request('inspect', request)); break;
}
case 'list': case 'listPage': case 'listNavigate': case 'listItem': {
if (data.kind !== 'list') { return; }
const listPath = message.path ?? [];
if (!Array.isArray(listPath) || !listPath.every(index => Number.isSafeInteger(index) && index > 0)) { throw new Error('Invalid list path'); }
const params = { view_id: data.viewId, path: listPath };
if (message.action === 'listPage') {
if (!Number.isSafeInteger(message.start) || Number(message.start) < 1) { throw new Error('Invalid list page'); }
result = await view.client.request('inspect', { method: 'workspace_children', params: { ...params, start: message.start } });
} else {
if (message.action === 'listItem' && (!Number.isSafeInteger(message.index) || Number(message.index) < 1)) { throw new Error('Invalid list index'); }
const navigation = await view.client.request<ListViewNavigation | boolean>('inspect', {
method: message.action === 'listItem' ? 'listview_view' : 'listview_navigate', params: { ...params, index: message.index },
});
if (!navigation) { throw new Error('Unable to open this item. Check the R session and try again.'); }
if (typeof navigation === 'object' && Array.isArray(navigation.breadcrumbs)) {
if (message.action === 'list') {
await session.showDataView('list', 'json', `${view.client.manifest.label}: ${navigation.title}`, '', 'Beside', String(data.viewId), navigation, view.target.sessionId);
} else { result = { navigation }; }
}
}
break;
}
case 'resize':
if (data.kind !== 'plot') { return; }
await view.client.request('resize', { device: data.device, plot: data.plot, width: message.width, height: message.height }); break;
Expand Down Expand Up @@ -1534,6 +1558,8 @@ export class InteractiveManager implements vscode.Disposable, vscode.TreeDataPro
const columns = data.columns as { field: string; headerName: string; type?: unknown }[];
const rows = data.rows as Record<string, unknown>[];
sections.push(`<table><tr>${columns.map(column => `<th style="text-align:${tableColumnAlignment(data, column)}">${escapeXml(column.headerName)}</th>`).join('')}</tr>${rows.map((row, rowIndex) => `<tr>${columns.map(column => `<td style="text-align:${tableColumnAlignment(data, column)}">${escapeXml(tableDisplayValue(data, row, column.field, rowIndex))}</td>`).join('')}</tr>`).join('')}</table><p>${tableSnapshotSummary(data) || `${String(data.totalRows)} rows (preview)`}</p>`);
} else if (data.kind === 'list') {
sections.push(`<pre>${escapeXml(data.printedText ?? JSON.stringify(data.children, null, 2))}</pre>`);
} else if (data.kind === 'mime' && data.mime === 'text/html') {
sections.push(`<iframe sandbox="allow-scripts" srcdoc="${escapeXml(String(data.text))}" style="width:100%;height:500px;border:0"></iframe>`);
} else { sections.push(`<pre>${escapeXml(data.text ?? data.message ?? JSON.stringify(data, null, 2))}</pre>`); }
Expand Down
Loading
Loading