Skip to content

Commit ed8bb3e

Browse files
committed
- Fixed console submission lag when the workspace contains many or large objects by delaying workspace inspection until after command submission.
- Fixed nested expansion in the workspace viewer for lists, environments, pairlists, S4 objects, and data frames. - Improved workspace viewer performance by loading object items on demand. Expanding an object shows the first 500 items directly, and selecting the ellipsis row loads the next 500.
1 parent 20f32fe commit ed8bb3e

6 files changed

Lines changed: 415 additions & 55 deletions

File tree

sess/R/handlers.R

Lines changed: 179 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,88 @@
11
# Handlers for the client Pull Requests (HTTP GET/POST)
22

3+
capture_str <- function(object, max_level = 0L) {
4+
paste0(utils::capture.output(
5+
utils::str(object,
6+
max.level = max_level,
7+
give.attr = FALSE,
8+
vec.len = 1L
9+
)
10+
), collapse = "\n")
11+
}
12+
13+
try_capture_str <- function(object, max_level = 0L) {
14+
tryCatch(
15+
capture_str(object, max_level),
16+
error = function(e) paste0(class(object), collapse = ", ")
17+
)
18+
}
19+
20+
workspace_child_count <- function(object) {
21+
if (is.environment(object)) {
22+
length(object)
23+
} else if (isS4(object)) {
24+
length(methods::slotNames(object))
25+
} else if (typeof(object) %in% c("list", "pairlist")) {
26+
length(object)
27+
} else {
28+
0L
29+
}
30+
}
31+
332
get_workspace_data <- function() {
433
env <- .GlobalEnv
5-
all_names <- ls(env)
34+
all_names <- ls(env, sorted = FALSE)
635

736
objs <- lapply(all_names, function(name) {
37+
if (bindingIsActive(name, env)) {
38+
return(list(
39+
class = "active_binding",
40+
type = "active_binding",
41+
length = 0L,
42+
str = "(active-binding)",
43+
has_children = FALSE
44+
))
45+
}
46+
847
obj <- env[[name]]
9-
list(
48+
obj_class <- class(obj)
49+
obj_type <- typeof(obj)
50+
obj_length <- length(obj)
51+
obj_dim <- dim(obj)
52+
first_class <- if (length(obj_class)) obj_class[[1]] else obj_type
53+
info <- list(
1054
class = class(obj),
11-
type = typeof(obj),
12-
length = length(obj),
13-
# Create a concise string representation
14-
str = paste0(
15-
utils::capture.output(utils::str(obj, max.level = 0, give.attr = FALSE)),
16-
collapse = "\n"
17-
)
55+
type = obj_type,
56+
length = obj_length,
57+
str = if (!is.null(obj_dim)) {
58+
paste0(first_class, ": ", paste(obj_dim, collapse = " x "))
59+
} else if (obj_type == "environment") {
60+
"<environment>"
61+
} else if (obj_type %in% c("closure", "builtin")) {
62+
trimws(try_capture_str(obj))
63+
} else {
64+
paste0(first_class, ", length ", obj_length)
65+
},
66+
has_children = workspace_child_count(obj) > 0L
1867
)
68+
69+
obj_names <- if (is.object(obj)) {
70+
utils::.DollarNames(obj, pattern = "")
71+
} else if (is.recursive(obj)) {
72+
names(obj)
73+
} else {
74+
NULL
75+
}
76+
if (length(obj_names)) {
77+
info$names <- obj_names
78+
}
79+
if (isS4(obj)) {
80+
info$slots <- methods::slotNames(obj)
81+
}
82+
if (!is.null(obj_dim)) {
83+
info$dim <- obj_dim
84+
}
85+
info
1986
})
2087
names(objs) <- all_names
2188

@@ -26,16 +93,113 @@ get_workspace_data <- function() {
2693
)
2794
}
2895

96+
workspace_object <- function(name, path = list()) {
97+
object <- get(name, envir = .GlobalEnv, inherits = FALSE)
98+
for (selector in path) {
99+
object <- switch(selector$kind,
100+
index = object[[as.integer(selector$value)]],
101+
name = get(selector$value, envir = object, inherits = FALSE),
102+
slot = methods::slot(object, selector$value),
103+
stop("Unknown workspace selector")
104+
)
105+
}
106+
object
107+
}
108+
109+
workspace_child_page_size <- 500L
110+
111+
workspace_child_item <- function(object, str, selector) {
112+
list(
113+
str = str,
114+
class = paste(class(object), collapse = ", "),
115+
type = typeof(object),
116+
has_children = workspace_child_count(object) > 0L,
117+
selector = selector
118+
)
119+
}
120+
121+
workspace_child_label <- function(name, index) {
122+
if (!is.null(name) && !is.na(name) && nzchar(name)) {
123+
paste0("$ ", name)
124+
} else {
125+
paste0("[[", index, "]]")
126+
}
127+
}
128+
129+
get_workspace_children <- function(name, path = list(), start = 1L) {
130+
tryCatch({
131+
object <- workspace_object(name, path)
132+
child_count <- workspace_child_count(object)
133+
if (child_count == 0L) {
134+
return(list(children = I(list()), next_start = NULL))
135+
}
136+
137+
start <- max(1L, as.integer(start))
138+
end <- min(child_count, start + workspace_child_page_size - 1L)
139+
if (start > end) {
140+
return(list(children = I(list()), next_start = NULL))
141+
}
142+
143+
children <- if (is.environment(object)) {
144+
child_names <- ls(object, sorted = FALSE)[seq.int(start, end)]
145+
lapply(child_names, function(child_name) {
146+
if (bindingIsActive(child_name, object)) {
147+
list(
148+
str = paste0("$ ", child_name, ": (active-binding)"),
149+
class = "active_binding",
150+
type = "active_binding",
151+
has_children = FALSE
152+
)
153+
} else {
154+
child <- get(child_name, envir = object, inherits = FALSE)
155+
workspace_child_item(
156+
child,
157+
paste0("$ ", child_name, ": ", trimws(try_capture_str(child))),
158+
list(kind = "name", value = child_name)
159+
)
160+
}
161+
})
162+
} else if (isS4(object)) {
163+
child_names <- methods::slotNames(object)[seq.int(start, end)]
164+
lapply(child_names, function(child_name) {
165+
child <- methods::slot(object, child_name)
166+
workspace_child_item(
167+
child,
168+
paste0("@ ", child_name, ": ", trimws(try_capture_str(child))),
169+
list(kind = "slot", value = child_name)
170+
)
171+
})
172+
} else {
173+
indices <- seq.int(start, end)
174+
child_names <- names(object)
175+
lapply(indices, function(index) {
176+
child <- object[[index]]
177+
child_name <- if (is.null(child_names)) NULL else child_names[[index]]
178+
workspace_child_item(
179+
child,
180+
paste0(
181+
workspace_child_label(child_name, index),
182+
": ",
183+
trimws(try_capture_str(child))
184+
),
185+
list(kind = "index", value = index)
186+
)
187+
})
188+
}
189+
190+
list(
191+
children = I(children),
192+
next_start = if (end < child_count) end + 1L else NULL
193+
)
194+
}, error = function(e) list(children = I(list()), next_start = NULL))
195+
}
196+
29197
handle_hover <- function(expr_str) {
30198
tryCatch(
31199
{
32200
expr <- parse(text = expr_str, keep.source = FALSE)[[1]]
33201
obj <- eval(expr, .GlobalEnv)
34-
str_preview <- paste0(
35-
utils::capture.output(utils::str(obj, max.level = 0, give.attr = FALSE)),
36-
collapse = "\n"
37-
)
38-
list(str = str_preview)
202+
list(str = capture_str(obj))
39203
},
40204
error = function(e) NULL
41205
)
@@ -77,7 +241,7 @@ handle_complete <- function(expr_str, trigger = NULL) {
77241
}))
78242
}
79243

80-
if (trigger == "@" && methods::isS4(obj)) {
244+
if (trigger == "@" && isS4(obj)) {
81245
nms <- methods::slotNames(obj)
82246
return(lapply(nms, function(n) {
83247
item <- methods::slot(obj, n)

sess/R/server.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ dispatch_message <- function(line) {
172172
# Request from vscode → R must reply
173173
handlers <- list(
174174
"workspace" = function(p) get_workspace_data(),
175+
"workspace_children" = function(p) get_workspace_children(p$name, p$path, p$start),
175176
"hover" = function(p) handle_hover(p$expr),
176177
"completion" = function(p) handle_complete(p$expr, p$trigger),
177178
"plot_latest" = function(p) handle_plot_latest(p),

src/rTerminal.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { extensionContext } from './extension';
99
import * as util from './util';
1010
import * as selection from './selection';
1111
import { getSelection } from './selection';
12-
import { cleanupSession } from './session';
12+
import { cleanupSession, deferWorkspaceRefresh } from './session';
1313
import { config, delay, getRterm, getCurrentWorkspaceFolder } from './util';
1414
import * as fs from 'fs';
1515
import * as yaml from 'js-yaml';
@@ -342,6 +342,7 @@ export async function runChunksInTerm(chunks: vscode.Range[]): Promise<void> {
342342
}
343343

344344
export async function runTextInTerm(text: string, execute: boolean = true): Promise<void> {
345+
deferWorkspaceRefresh();
345346
const term = await chooseTerminal();
346347
if (term === undefined) {
347348
return;

src/session.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ export interface SessionInfo {
2525

2626
export interface GlobalEnv {
2727
[key: string]: {
28-
class: string[];
28+
class: string[] | string;
2929
type: string;
3030
length: number;
3131
str: string;
3232
size?: number;
3333
dim?: number[],
3434
names?: string[],
35-
slots?: string[]
35+
slots?: string[],
36+
has_children?: boolean
3637
}
3738
}
3839

@@ -85,6 +86,9 @@ export let workspaceFile: string;
8586
const sessions = new Map<string, Session>();
8687
export let activeSession: Session | undefined;
8788
let activeBrowserUri: Uri | undefined;
89+
let workspaceRefreshTimer: NodeJS.Timeout | undefined;
90+
let workspaceRefreshInProgress = false;
91+
let workspaceRefreshPending = false;
8892

8993
interface DataViewColumnDef {
9094
headerName: string;
@@ -580,15 +584,48 @@ async function updatePlot() {
580584
await globalPlotManager?.showStandardPlot();
581585
}
582586

587+
export function deferWorkspaceRefresh(): void {
588+
if (workspaceRefreshTimer) {
589+
clearTimeout(workspaceRefreshTimer);
590+
workspaceRefreshTimer = undefined;
591+
}
592+
}
593+
594+
function scheduleWorkspaceRefresh(delayMs: number = 500): void {
595+
workspaceRefreshPending = true;
596+
if (workspaceRefreshTimer) {
597+
clearTimeout(workspaceRefreshTimer);
598+
}
599+
workspaceRefreshTimer = setTimeout(() => {
600+
workspaceRefreshTimer = undefined;
601+
void runWorkspaceRefresh();
602+
}, delayMs);
603+
}
604+
605+
async function runWorkspaceRefresh(): Promise<void> {
606+
if (workspaceRefreshInProgress || !workspaceRefreshPending) {
607+
return;
608+
}
609+
workspaceRefreshPending = false;
610+
workspaceRefreshInProgress = true;
611+
try {
612+
await updateWorkspace();
613+
} finally {
614+
workspaceRefreshInProgress = false;
615+
if (workspaceRefreshPending) {
616+
scheduleWorkspaceRefresh();
617+
}
618+
}
619+
}
620+
583621
export async function updateWorkspace() {
584-
if (!globalPipePath) {return;}
622+
const requestedSession = activeSession;
623+
if (!globalPipePath || !requestedSession) {return;}
585624
try {
586625
const response = await sessionRequest({ method: 'workspace' });
587-
if (response) {
626+
if (response && activeSession === requestedSession) {
588627
workspaceData = response as WorkspaceData;
589-
if (activeSession) {
590-
activeSession.workspaceData = workspaceData;
591-
}
628+
requestedSession.workspaceData = workspaceData;
592629
void rWorkspace?.refresh();
593630
console.info('[updateWorkspace] Done');
594631
}
@@ -1500,13 +1537,15 @@ async function handleNotification(message: Record<string, unknown>, socket: IpcS
15001537
if (params.plot_url) {
15011538
await globalPlotManager?.showHttpgdPlot(String(params.plot_url));
15021539
}
1503-
void updateWorkspace();
1540+
scheduleWorkspaceRefresh(0);
15041541
void watchProcess(rPid).then((v: string) => { void cleanupSession(v); });
15051542
break;
15061543
}
15071544

15081545
case 'workspace_updated': {
1509-
void updateWorkspace();
1546+
if (socket === activeSession?.socket) {
1547+
scheduleWorkspaceRefresh();
1548+
}
15101549
break;
15111550
}
15121551
case 'help': {
@@ -1674,6 +1713,8 @@ export async function cleanupSession(pidArg: string): Promise<void> {
16741713
session.socket.destroy();
16751714
}
16761715
if (activeSession === session || pid === pidArg) {
1716+
deferWorkspaceRefresh();
1717+
workspaceRefreshPending = false;
16771718
resetStatusBar();
16781719
globalPipePath = undefined;
16791720
activeSession = undefined;

src/test/suite/session.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ suite('Session Communication', () => {
9797
assert.ok(listData, 'my_list should be in workspaceData.globalenv');
9898
const className = Array.isArray(listData.class) ? listData.class[0] : listData.class;
9999
assert.strictEqual(className, 'list', 'my_list should be a list');
100+
assert.strictEqual(listData.has_children, true, 'my_list should be expandable');
101+
102+
const childrenResult = await session.sessionRequest({
103+
method: 'workspace_children',
104+
params: { name: 'my_list', path: [], start: 1 }
105+
}) as { children: Record<string, unknown>[], next_start?: number };
106+
107+
assert.ok(Array.isArray(childrenResult.children), 'workspace children should be an array');
108+
assert.strictEqual(childrenResult.children.length, 1, 'my_list should have one workspace child');
109+
assert.match(String(childrenResult.children[0].str), /hello_vscode/);
100110

101111
const completionRequestParams = {
102112
expr: 'my_list',

0 commit comments

Comments
 (0)