Skip to content

Commit

Permalink
Websocket communication (#1151)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
renkun-ken authored Apr 24, 2023
1 parent dee867f commit e4b56ce
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 27 deletions.
143 changes: 140 additions & 3 deletions R/session/vsc.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ request_lock_file <- file.path(dir_watcher, "request.lock")
settings_file <- file.path(dir_watcher, "settings.json")
user_options <- names(options())

logger <- if (getOption("vsc.debug", FALSE)) {
function(...) cat(..., "\n", sep = "")
} else {
function(...) invisible()
}

load_settings <- function() {
if (!file.exists(settings_file)) {
return(FALSE)
Expand All @@ -20,6 +26,7 @@ load_settings <- function() {
}

mapping <- quote(list(
vsc.use_webserver = session$useWebServer,
vsc.use_httpgd = plot$useHttpgd,
vsc.show_object_size = workspaceViewer$showObjectSize,
vsc.rstudioapi = session$emulateRStudioAPI,
Expand Down Expand Up @@ -59,8 +66,133 @@ if (is.null(getOption("help_type"))) {
options(help_type = "html")
}

use_webserver <- isTRUE(getOption("vsc.use_webserver", FALSE))
if (use_webserver) {
if (requireNamespace("httpuv", quietly = TRUE)) {
request_handlers <- list(
hover = function(expr, ...) {
tryCatch({
expr <- parse(text = expr, keep.source = FALSE)[[1]]
obj <- eval(expr, .GlobalEnv)
list(str = capture_str(obj))
}, error = function(e) NULL)
},

complete = function(expr, trigger, ...) {
obj <- tryCatch({
expr <- parse(text = expr, keep.source = FALSE)[[1]]
eval(expr, .GlobalEnv)
}, error = function(e) NULL)

if (is.null(obj)) {
return(NULL)
}

if (trigger == "$") {
names <- if (is.object(obj)) {
.DollarNames(obj, pattern = "")
} else if (is.recursive(obj)) {
names(obj)
} else {
NULL
}

result <- lapply(names, function(name) {
item <- obj[[name]]
list(
name = name,
type = typeof(item),
str = try_capture_str(item)
)
})
return(result)
}

if (trigger == "@" && isS4(obj)) {
names <- slotNames(obj)
result <- lapply(names, function(name) {
item <- slot(obj, name)
list(
name = name,
type = typeof(item),
str = try_capture_str(item)
)
})
return(result)
}
}
)

server <- getOption("vsc.server")
if (!is.null(server) && server$isRunning()) {
host <- server$getHost()
port <- server$getPort()
token <- attr(server, "token")
} else {
host <- "127.0.0.1"
port <- httpuv::randomPort()
token <- sprintf("%d:%d:%.6f", pid, port, Sys.time())
server <- httpuv::startServer(host, port,
list(
onHeaders = function(req) {
logger("http request ",
req[["REMOTE_ADDR"]], ":",
req[["REMOTE_PORT"]], " ",
req[["REQUEST_METHOD"]], " ",
req[["HTTP_USER_AGENT"]]
)

if (!nzchar(req[["REMOTE_ADDR"]]) || identical(req[["REMOTE_PORT"]], "0")) {
return(NULL)
}

if (!identical(req[["HTTP_AUTHORIZATION"]], token)) {
return(list(
status = 401L,
headers = list(
"Content-Type" = "text/plain"
),
body = "Unauthorized"
))
}

if (!identical(req[["HTTP_CONTENT_TYPE"]], "application/json")) {
return(list(
status = 400L,
headers = list(
"Content-Type" = "text/plain"
),
body = "Bad request"
))
}
},
call = function(req) {
content <- req$rook.input$read_lines()
request <- jsonlite::fromJSON(content, simplifyVector = FALSE)
handler <- request_handlers[[request$type]]
response <- if (is.function(handler)) do.call(handler, request)

list(
status = 200L,
headers = list(
"Content-Type" = "application/json"
),
body = jsonlite::toJSON(response, auto_unbox = TRUE, force = TRUE)
)
}
)
)
attr(server, "token") <- token
options(vsc.server = server)
}
} else {
message("{httpuv} is required to use WebServer from the session watcher.")
use_webserver <- FALSE
}
}

get_timestamp <- function() {
format.default(Sys.time(), nsmall = 6, scientific = FALSE)
sprintf("%.6f", Sys.time())
}

scalar <- function(x) {
Expand Down Expand Up @@ -512,7 +644,12 @@ attach <- function() {
version = R.version.string,
start_time = format(file.info(tempdir)$ctime)
),
plot_url = if (identical(names(dev.cur()), "httpgd")) httpgd::hgd_url()
plot_url = if (identical(names(dev.cur()), "httpgd")) httpgd::hgd_url(),
server = if (use_webserver) list(
host = host,
port = port,
token = token
) else NULL
)
}

Expand Down Expand Up @@ -792,4 +929,4 @@ print.hsearch <- function(x, ...) {
invisible(NULL)
}

reg.finalizer(globalenv(), function(e) .vsc$request("detach"), onexit = TRUE)
reg.finalizer(.GlobalEnv, function(e) .vsc$request("detach"), onexit = TRUE)
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,11 @@
"default": true,
"description": "Enable R session watcher. Required for workspace viewer and most features to work with an R session. Restart required to take effect."
},
"r.session.useWebServer": {
"type": "boolean",
"default": false,
"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."
},
"r.session.watchGlobalEnvironment": {
"type": "boolean",
"default": true,
Expand Down
105 changes: 81 additions & 24 deletions src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { cleanLine } from './lineCache';
import { globalRHelp } from './extension';
import { config } from './util';
import { getChunks } from './rmarkdown';
import { CompletionItemKind } from 'vscode-languageclient';


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


export class HoverProvider implements vscode.HoverProvider {
provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover | null {
async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise<vscode.Hover | null> {
if(!session.workspaceData?.globalenv){
return null;
}
Expand All @@ -43,15 +44,36 @@ export class HoverProvider implements vscode.HoverProvider {
}
}

const wordRange = document.getWordRangeAtPosition(position);
const text = document.getText(wordRange);
// use juggling check here for both
// null and undefined
// eslint-disable-next-line eqeqeq
if (session.workspaceData.globalenv[text]?.str == null) {
return null;
let hoverRange = document.getWordRangeAtPosition(position);
let hoverText = null;

if (session.server) {
const exprRegex = /([a-zA-Z0-9._$@ ])+(?<![@$])/;
hoverRange = document.getWordRangeAtPosition(position, exprRegex)?.with({ end: hoverRange?.end });
const expr = document.getText(hoverRange);
const response = await session.sessionRequest(session.server, {
type: 'hover',
expr: expr
});

if (response) {
hoverText = response.str;
}

} else {
const symbol = document.getText(hoverRange);
const str = session.workspaceData.globalenv[symbol]?.str;

if (str) {
hoverText = str;
}
}
return new vscode.Hover(`\`\`\`\n${session.workspaceData.globalenv[text]?.str}\n\`\`\``);

if (hoverText) {
return new vscode.Hover(`\`\`\`\n${hoverText}\n\`\`\``, hoverRange);
}

return null;
}
}

Expand Down Expand Up @@ -108,12 +130,12 @@ export class StaticCompletionItemProvider implements vscode.CompletionItemProvid


export class LiveCompletionItemProvider implements vscode.CompletionItemProvider {
provideCompletionItems(
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
completionContext: vscode.CompletionContext
): vscode.CompletionItem[] {
): Promise<vscode.CompletionItem[]> {
const items: vscode.CompletionItem[] = [];
if (token.isCancellationRequested || !session.workspaceData?.globalenv) {
return items;
Expand Down Expand Up @@ -144,22 +166,38 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider
});
} else if(trigger === '$' || trigger === '@') {
const symbolPosition = new vscode.Position(position.line, position.character - 1);
const symbolRange = document.getWordRangeAtPosition(symbolPosition);
const symbol = document.getText(symbolRange);
const doc = new vscode.MarkdownString('Element of `' + symbol + '`');
const obj = session.workspaceData.globalenv[symbol];
let names: string[] | undefined;
if (obj !== undefined) {
if (completionContext.triggerCharacter === '$') {
names = obj.names;
} else if (completionContext.triggerCharacter === '@') {
names = obj.slots;
if (session.server) {
const re = /([a-zA-Z0-9._$@ ])+(?<![@$])/;
const exprRange = document.getWordRangeAtPosition(symbolPosition, re)?.with({ end: symbolPosition });
const expr = document.getText(exprRange);
const response: RObjectElement[] = await session.sessionRequest(session.server, {
type: 'complete',
expr: expr,
trigger: completionContext.triggerCharacter
});

if (response) {
items.push(...getCompletionItemsFromElements(response, '[session]'));
}
} else {
const symbolRange = document.getWordRangeAtPosition(symbolPosition);
const symbol = document.getText(symbolRange);
const doc = new vscode.MarkdownString('Element of `' + symbol + '`');
const obj = session.workspaceData.globalenv[symbol];
let names: string[] | undefined;
if (obj !== undefined) {
if (completionContext.triggerCharacter === '$') {
names = obj.names;
} else if (completionContext.triggerCharacter === '@') {
names = obj.slots;
}
}
}

if (names) {
items.push(...getCompletionItems(names, vscode.CompletionItemKind.Variable, '[session]', doc));
if (names) {
items.push(...getCompletionItems(names, vscode.CompletionItemKind.Variable, '[session]', doc));
}
}

}

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

interface RObjectElement {
name: string;
type: string;
str: string;
}

function getCompletionItemsFromElements(elements: RObjectElement[], detail: string): vscode.CompletionItem[] {
const len = elements.length.toString().length;
let index = 0;
return elements.map((e) => {
const item = new vscode.CompletionItem(e.name, (e.type === 'closure' || e.type === 'builtin') ? CompletionItemKind.Function : vscode.CompletionItemKind.Variable);
item.detail = detail;
item.documentation = new vscode.MarkdownString(`\`\`\`r\n${e.str}\n\`\`\``);
item.sortText = `0-${index.toString().padStart(len, '0')}`;
index++;
return item;
});
}

function getCompletionItems(names: string[], kind: vscode.CompletionItemKind, detail: string, documentation: vscode.MarkdownString): vscode.CompletionItem[] {
const len = names.length.toString().length;
let index = 0;
Expand Down
Loading

0 comments on commit e4b56ce

Please sign in to comment.