Skip to content

Commit e4b56ce

Browse files
authored
Websocket communication (#1151)
* Initial use of web socket * Update websocket * Update hover * Update completions * Update session socket * Update complete * Remove unused import * Update code * Update session * Update request handlers * Support exprRange in completion * Update vsc * Update support expr in hover * Update settings * Update hover * Update hover * Use http server * Reuse running server on re-attach * Update vsc.R * Rename setting * Update httpAgent * Update webserver call * Use onHeaders to reject invalid requests * Update message
1 parent dee867f commit e4b56ce

4 files changed

Lines changed: 275 additions & 27 deletions

File tree

R/session/vsc.R

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ request_lock_file <- file.path(dir_watcher, "request.lock")
1010
settings_file <- file.path(dir_watcher, "settings.json")
1111
user_options <- names(options())
1212

13+
logger <- if (getOption("vsc.debug", FALSE)) {
14+
function(...) cat(..., "\n", sep = "")
15+
} else {
16+
function(...) invisible()
17+
}
18+
1319
load_settings <- function() {
1420
if (!file.exists(settings_file)) {
1521
return(FALSE)
@@ -20,6 +26,7 @@ load_settings <- function() {
2026
}
2127

2228
mapping <- quote(list(
29+
vsc.use_webserver = session$useWebServer,
2330
vsc.use_httpgd = plot$useHttpgd,
2431
vsc.show_object_size = workspaceViewer$showObjectSize,
2532
vsc.rstudioapi = session$emulateRStudioAPI,
@@ -59,8 +66,133 @@ if (is.null(getOption("help_type"))) {
5966
options(help_type = "html")
6067
}
6168

69+
use_webserver <- isTRUE(getOption("vsc.use_webserver", FALSE))
70+
if (use_webserver) {
71+
if (requireNamespace("httpuv", quietly = TRUE)) {
72+
request_handlers <- list(
73+
hover = function(expr, ...) {
74+
tryCatch({
75+
expr <- parse(text = expr, keep.source = FALSE)[[1]]
76+
obj <- eval(expr, .GlobalEnv)
77+
list(str = capture_str(obj))
78+
}, error = function(e) NULL)
79+
},
80+
81+
complete = function(expr, trigger, ...) {
82+
obj <- tryCatch({
83+
expr <- parse(text = expr, keep.source = FALSE)[[1]]
84+
eval(expr, .GlobalEnv)
85+
}, error = function(e) NULL)
86+
87+
if (is.null(obj)) {
88+
return(NULL)
89+
}
90+
91+
if (trigger == "$") {
92+
names <- if (is.object(obj)) {
93+
.DollarNames(obj, pattern = "")
94+
} else if (is.recursive(obj)) {
95+
names(obj)
96+
} else {
97+
NULL
98+
}
99+
100+
result <- lapply(names, function(name) {
101+
item <- obj[[name]]
102+
list(
103+
name = name,
104+
type = typeof(item),
105+
str = try_capture_str(item)
106+
)
107+
})
108+
return(result)
109+
}
110+
111+
if (trigger == "@" && isS4(obj)) {
112+
names <- slotNames(obj)
113+
result <- lapply(names, function(name) {
114+
item <- slot(obj, name)
115+
list(
116+
name = name,
117+
type = typeof(item),
118+
str = try_capture_str(item)
119+
)
120+
})
121+
return(result)
122+
}
123+
}
124+
)
125+
126+
server <- getOption("vsc.server")
127+
if (!is.null(server) && server$isRunning()) {
128+
host <- server$getHost()
129+
port <- server$getPort()
130+
token <- attr(server, "token")
131+
} else {
132+
host <- "127.0.0.1"
133+
port <- httpuv::randomPort()
134+
token <- sprintf("%d:%d:%.6f", pid, port, Sys.time())
135+
server <- httpuv::startServer(host, port,
136+
list(
137+
onHeaders = function(req) {
138+
logger("http request ",
139+
req[["REMOTE_ADDR"]], ":",
140+
req[["REMOTE_PORT"]], " ",
141+
req[["REQUEST_METHOD"]], " ",
142+
req[["HTTP_USER_AGENT"]]
143+
)
144+
145+
if (!nzchar(req[["REMOTE_ADDR"]]) || identical(req[["REMOTE_PORT"]], "0")) {
146+
return(NULL)
147+
}
148+
149+
if (!identical(req[["HTTP_AUTHORIZATION"]], token)) {
150+
return(list(
151+
status = 401L,
152+
headers = list(
153+
"Content-Type" = "text/plain"
154+
),
155+
body = "Unauthorized"
156+
))
157+
}
158+
159+
if (!identical(req[["HTTP_CONTENT_TYPE"]], "application/json")) {
160+
return(list(
161+
status = 400L,
162+
headers = list(
163+
"Content-Type" = "text/plain"
164+
),
165+
body = "Bad request"
166+
))
167+
}
168+
},
169+
call = function(req) {
170+
content <- req$rook.input$read_lines()
171+
request <- jsonlite::fromJSON(content, simplifyVector = FALSE)
172+
handler <- request_handlers[[request$type]]
173+
response <- if (is.function(handler)) do.call(handler, request)
174+
175+
list(
176+
status = 200L,
177+
headers = list(
178+
"Content-Type" = "application/json"
179+
),
180+
body = jsonlite::toJSON(response, auto_unbox = TRUE, force = TRUE)
181+
)
182+
}
183+
)
184+
)
185+
attr(server, "token") <- token
186+
options(vsc.server = server)
187+
}
188+
} else {
189+
message("{httpuv} is required to use WebServer from the session watcher.")
190+
use_webserver <- FALSE
191+
}
192+
}
193+
62194
get_timestamp <- function() {
63-
format.default(Sys.time(), nsmall = 6, scientific = FALSE)
195+
sprintf("%.6f", Sys.time())
64196
}
65197

66198
scalar <- function(x) {
@@ -512,7 +644,12 @@ attach <- function() {
512644
version = R.version.string,
513645
start_time = format(file.info(tempdir)$ctime)
514646
),
515-
plot_url = if (identical(names(dev.cur()), "httpgd")) httpgd::hgd_url()
647+
plot_url = if (identical(names(dev.cur()), "httpgd")) httpgd::hgd_url(),
648+
server = if (use_webserver) list(
649+
host = host,
650+
port = port,
651+
token = token
652+
) else NULL
516653
)
517654
}
518655

@@ -792,4 +929,4 @@ print.hsearch <- function(x, ...) {
792929
invisible(NULL)
793930
}
794931

795-
reg.finalizer(globalenv(), function(e) .vsc$request("detach"), onexit = TRUE)
932+
reg.finalizer(.GlobalEnv, function(e) .vsc$request("detach"), onexit = TRUE)

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,6 +1720,11 @@
17201720
"default": true,
17211721
"description": "Enable R session watcher. Required for workspace viewer and most features to work with an R session. Restart required to take effect."
17221722
},
1723+
"r.session.useWebServer": {
1724+
"type": "boolean",
1725+
"default": false,
1726+
"markdownDescription": "Enable experimental use of web server in the R session to handle session requests from the extension. Changes the option `vsc.use_webserver` in R. Requires `#r.sessionWatcher#` to be set to `true`. Requires the `httpuv` R package."
1727+
},
17231728
"r.session.watchGlobalEnvironment": {
17241729
"type": "boolean",
17251730
"default": true,

src/completions.ts

Lines changed: 81 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { cleanLine } from './lineCache';
1313
import { globalRHelp } from './extension';
1414
import { config } from './util';
1515
import { getChunks } from './rmarkdown';
16+
import { CompletionItemKind } from 'vscode-languageclient';
1617

1718

1819
// Get with names(roxygen2:::default_tags())
@@ -30,7 +31,7 @@ const roxygenTagCompletionItems = [
3031

3132

3233
export class HoverProvider implements vscode.HoverProvider {
33-
provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover | null {
34+
async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise<vscode.Hover | null> {
3435
if(!session.workspaceData?.globalenv){
3536
return null;
3637
}
@@ -43,15 +44,36 @@ export class HoverProvider implements vscode.HoverProvider {
4344
}
4445
}
4546

46-
const wordRange = document.getWordRangeAtPosition(position);
47-
const text = document.getText(wordRange);
48-
// use juggling check here for both
49-
// null and undefined
50-
// eslint-disable-next-line eqeqeq
51-
if (session.workspaceData.globalenv[text]?.str == null) {
52-
return null;
47+
let hoverRange = document.getWordRangeAtPosition(position);
48+
let hoverText = null;
49+
50+
if (session.server) {
51+
const exprRegex = /([a-zA-Z0-9._$@ ])+(?<![@$])/;
52+
hoverRange = document.getWordRangeAtPosition(position, exprRegex)?.with({ end: hoverRange?.end });
53+
const expr = document.getText(hoverRange);
54+
const response = await session.sessionRequest(session.server, {
55+
type: 'hover',
56+
expr: expr
57+
});
58+
59+
if (response) {
60+
hoverText = response.str;
61+
}
62+
63+
} else {
64+
const symbol = document.getText(hoverRange);
65+
const str = session.workspaceData.globalenv[symbol]?.str;
66+
67+
if (str) {
68+
hoverText = str;
69+
}
5370
}
54-
return new vscode.Hover(`\`\`\`\n${session.workspaceData.globalenv[text]?.str}\n\`\`\``);
71+
72+
if (hoverText) {
73+
return new vscode.Hover(`\`\`\`\n${hoverText}\n\`\`\``, hoverRange);
74+
}
75+
76+
return null;
5577
}
5678
}
5779

@@ -108,12 +130,12 @@ export class StaticCompletionItemProvider implements vscode.CompletionItemProvid
108130

109131

110132
export class LiveCompletionItemProvider implements vscode.CompletionItemProvider {
111-
provideCompletionItems(
133+
async provideCompletionItems(
112134
document: vscode.TextDocument,
113135
position: vscode.Position,
114136
token: vscode.CancellationToken,
115137
completionContext: vscode.CompletionContext
116-
): vscode.CompletionItem[] {
138+
): Promise<vscode.CompletionItem[]> {
117139
const items: vscode.CompletionItem[] = [];
118140
if (token.isCancellationRequested || !session.workspaceData?.globalenv) {
119141
return items;
@@ -144,22 +166,38 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider
144166
});
145167
} else if(trigger === '$' || trigger === '@') {
146168
const symbolPosition = new vscode.Position(position.line, position.character - 1);
147-
const symbolRange = document.getWordRangeAtPosition(symbolPosition);
148-
const symbol = document.getText(symbolRange);
149-
const doc = new vscode.MarkdownString('Element of `' + symbol + '`');
150-
const obj = session.workspaceData.globalenv[symbol];
151-
let names: string[] | undefined;
152-
if (obj !== undefined) {
153-
if (completionContext.triggerCharacter === '$') {
154-
names = obj.names;
155-
} else if (completionContext.triggerCharacter === '@') {
156-
names = obj.slots;
169+
if (session.server) {
170+
const re = /([a-zA-Z0-9._$@ ])+(?<![@$])/;
171+
const exprRange = document.getWordRangeAtPosition(symbolPosition, re)?.with({ end: symbolPosition });
172+
const expr = document.getText(exprRange);
173+
const response: RObjectElement[] = await session.sessionRequest(session.server, {
174+
type: 'complete',
175+
expr: expr,
176+
trigger: completionContext.triggerCharacter
177+
});
178+
179+
if (response) {
180+
items.push(...getCompletionItemsFromElements(response, '[session]'));
181+
}
182+
} else {
183+
const symbolRange = document.getWordRangeAtPosition(symbolPosition);
184+
const symbol = document.getText(symbolRange);
185+
const doc = new vscode.MarkdownString('Element of `' + symbol + '`');
186+
const obj = session.workspaceData.globalenv[symbol];
187+
let names: string[] | undefined;
188+
if (obj !== undefined) {
189+
if (completionContext.triggerCharacter === '$') {
190+
names = obj.names;
191+
} else if (completionContext.triggerCharacter === '@') {
192+
names = obj.slots;
193+
}
157194
}
158-
}
159195

160-
if (names) {
161-
items.push(...getCompletionItems(names, vscode.CompletionItemKind.Variable, '[session]', doc));
196+
if (names) {
197+
items.push(...getCompletionItems(names, vscode.CompletionItemKind.Variable, '[session]', doc));
198+
}
162199
}
200+
163201
}
164202

165203
if (trigger === undefined || trigger === '[' || trigger === ',' || trigger === '"' || trigger === '\'') {
@@ -174,6 +212,25 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider
174212
}
175213
}
176214

215+
interface RObjectElement {
216+
name: string;
217+
type: string;
218+
str: string;
219+
}
220+
221+
function getCompletionItemsFromElements(elements: RObjectElement[], detail: string): vscode.CompletionItem[] {
222+
const len = elements.length.toString().length;
223+
let index = 0;
224+
return elements.map((e) => {
225+
const item = new vscode.CompletionItem(e.name, (e.type === 'closure' || e.type === 'builtin') ? CompletionItemKind.Function : vscode.CompletionItemKind.Variable);
226+
item.detail = detail;
227+
item.documentation = new vscode.MarkdownString(`\`\`\`r\n${e.str}\n\`\`\``);
228+
item.sortText = `0-${index.toString().padStart(len, '0')}`;
229+
index++;
230+
return item;
231+
});
232+
}
233+
177234
function getCompletionItems(names: string[], kind: vscode.CompletionItemKind, detail: string, documentation: vscode.MarkdownString): vscode.CompletionItem[] {
178235
const len = names.length.toString().length;
179236
let index = 0;

0 commit comments

Comments
 (0)