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
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