Skip to content

Commit e736b8e

Browse files
authored
LiveShare Functionality (#626)
* Liveshare Functionality Implements session watcher features for LiveShare guests * Update package-lock.json * Review changes (1) - isLiveShare now relies on liveSession instead of a new vsls.getApi() call -- Many functions are now synchronous as a result - Various tooltip changes to clarify 'forward guest commands' - Clean up helper bool functions * Move files to liveshare folder As per #641, liveshare files are moved to a /liveshare/ folder, and the "r" prefix has been removed from their name * Review changes (2) - Liveshare API handling -- Abort on timeout -- Retry button - Guest & host workspace always in sync - Guests no longer create files, instead relying on webview + virtual docs - Fix bug with webviews returning object instead of string * Resolve upstream changes (#2) * Share httpgd session with guest - Httpgd sessions are shared with guests through vsls shareBrowser functionality - Resolve some conflicts from upstream merge * Fix merge error + refresh tree - Remove duplicate function declaration - Refresh liveshare tree on retry api * Review changes (3) - Use liveShare.defaults as per #620 - Change overuse of brackets * Better integrate httpgd - The shared httpgd is now opened/reopened even if closed - Shared httpgd is now shared even if started before liveshare - Minor bug fixes * *Better* integrate httpgd - Instead of doing a workaround browser solution, use the native httpgd webview * Minor changes - Change timeout default: 3000 => 10000 - Remove leftover information message - Fix accidental merge overwrite * Fix guest httpgd eagerness - notifyGuestPlotManager is now called less often, preventing it from taking focus unexpectedly * Minor changes - .UUID -> uuid - Shorten commandNode label * Review changes (4) - request() to liveShareRequest() - onRequest() to liveShareOnRequest() - use index design pattern - remove global shareurl - remove badge from README * Lint + misc change - Appease the linter gods
1 parent 309cbe6 commit e736b8e

File tree

17 files changed

+1422
-248
lines changed

17 files changed

+1422
-248
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

R/vsc.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ if (show_view) {
323323
list(columns = columns, data = data)
324324
}
325325

326-
show_dataview <- function(x, title,
326+
show_dataview <- function(x, title, uuid = NULL,
327327
viewer = getOption("vsc.view", "Two")) {
328328
if (missing(title)) {
329329
sub <- substitute(x)
@@ -387,19 +387,19 @@ if (show_view) {
387387
file <- tempfile(tmpdir = tempdir, fileext = ".json")
388388
jsonlite::write_json(data, file, matrix = "rowmajor")
389389
request("dataview", source = "table", type = "json",
390-
title = title, file = file, viewer = viewer)
390+
title = title, file = file, viewer = viewer, uuid = uuid)
391391
} else if (is.list(x)) {
392392
tryCatch({
393393
file <- tempfile(tmpdir = tempdir, fileext = ".json")
394394
jsonlite::write_json(x, file, auto_unbox = TRUE)
395395
request("dataview", source = "list", type = "json",
396-
title = title, file = file, viewer = viewer)
396+
title = title, file = file, viewer = viewer, uuid = uuid)
397397
}, error = function(e) {
398398
file <- file.path(tempdir, paste0(make.names(title), ".txt"))
399399
text <- utils::capture.output(print(x))
400400
writeLines(text, file)
401401
request("dataview", source = "object", type = "txt",
402-
title = title, file = file, viewer = viewer)
402+
title = title, file = file, viewer = viewer, uuid = uuid)
403403
})
404404
} else {
405405
file <- file.path(tempdir, paste0(make.names(title), ".R"))
@@ -410,7 +410,7 @@ if (show_view) {
410410
}
411411
writeLines(code, file)
412412
request("dataview", source = "object", type = "R",
413-
title = title, file = file, viewer = viewer)
413+
title = title, file = file, viewer = viewer, uuid = uuid)
414414
}
415415
}
416416

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# R support for Visual Studio Code
22

3+
[![Badge](https://aka.ms/vsls-badge)](https://aka.ms/vsls)
4+
35
Requires [R](https://www.r-project.org/).
46

57
We recommend using this extension with [radian](https://github.com/randy3k/radian), an alternative R console with multiline editing and rich syntax highlighting.
@@ -255,6 +257,18 @@ Alternatively, individual addin functions can be bound to keys using `r.runRComm
255257

256258
See the wiki for [lists of supported `{rstudioapi}` commands, and verified compatible addin packages](https://github.com/Ikuyadeu/vscode-R/wiki/RStudio-addin-support).
257259

260+
### Live Share support
261+
262+
The session watcher further enhances LiveShare collaboration.
263+
The workspace viewer, data view, plots, and browsers are available to guests through the host session.
264+
To enable this feature, *both* the host and guest must have this extension and session watcher enabled.
265+
266+
Hosts can control the level of access guests have through the provided Live Share Control view. This provides the following controls:
267+
268+
* Whether guests can access the current R session and its workspace
269+
* Whether R commands should be forwarded from the guest to the host terminal (bypasses terminal permissions)
270+
* Whether opened R browsers should be shared with guests
271+
258272
### How to disable it
259273

260274
For the case of basic usage, turning off `r.sessionWatcher` in VSCode settings is sufficient

package.json

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@
8989
"icon": "./images/Rlogo.svg",
9090
"contextualTitle": "R",
9191
"when": "r.helpViewer.show"
92+
},
93+
{
94+
"id": "rLiveShare",
95+
"name": "Live Share Controls",
96+
"icon": "./images/Rlogo.svg",
97+
"contextualTitle": "R",
98+
"when": "r.WorkspaceViewer:show && !r.liveShare:isGuest",
99+
"visibility": "collapsed"
92100
}
93101
]
94102
},
@@ -100,6 +108,16 @@
100108
{
101109
"view": "rHelpPages",
102110
"contents": "R Help Pages"
111+
},
112+
{
113+
"view": "rLiveShare",
114+
"contents": "R Live Share not active.",
115+
"when": "!r.liveShare:aborted"
116+
},
117+
{
118+
"view": "rLiveShare",
119+
"contents": "Could not connect to Live Share service.",
120+
"when": "r.liveShare:aborted"
103121
}
104122
],
105123
"languages": [
@@ -615,6 +633,15 @@
615633
"category": "R",
616634
"command": "r.helpPanel.openForPath"
617635
},
636+
{
637+
"command": "r.liveShare.toggle",
638+
"title": "Toggle"
639+
},
640+
{
641+
"command": "r.liveShare.retry",
642+
"title": "Retry connection to Live Share service",
643+
"icon": "$(refresh)"
644+
},
618645
{
619646
"title": "Toggle Style",
620647
"category": "R Plot",
@@ -976,18 +1003,22 @@
9761003
"command": "r.helpPanel.summarizeTopics",
9771004
"group": "inline@8",
9781005
"when": "view == rHelpPages && viewItem =~ /_summarizeTopics_/"
1006+
},
1007+
{
1008+
"command": "r.liveShare.toggle",
1009+
"when": "view == rLiveShare"
9791010
}
9801011
],
9811012
"view/title": [
9821013
{
9831014
"command": "r.workspaceViewer.load",
9841015
"group": "navigation@0",
985-
"when": "view == workspaceViewer"
1016+
"when": "view == workspaceViewer && !r.liveShare:isGuest"
9861017
},
9871018
{
9881019
"command": "r.workspaceViewer.save",
9891020
"group": "navigation@1",
990-
"when": "view == workspaceViewer"
1021+
"when": "view == workspaceViewer && !r.liveShare:isGuest"
9911022
},
9921023
{
9931024
"command": "r.workspaceViewer.clear",
@@ -1003,6 +1034,11 @@
10031034
"command": "r.helpPanel.showQuickPick",
10041035
"group": "navigation",
10051036
"when": "view == rHelpPages"
1037+
},
1038+
{
1039+
"command": "r.liveShare.retry",
1040+
"group": "navigation@3",
1041+
"when": "view == rLiveShare && r.liveShare:aborted"
10061042
}
10071043
]
10081044
},
@@ -1144,6 +1180,26 @@
11441180
"default": false,
11451181
"description": "Remove hidden items when clearing workspace."
11461182
},
1183+
"r.liveShare.timeout": {
1184+
"type": "integer",
1185+
"default": 10000,
1186+
"description": "Time in milliseconds before aborting attempt to connect to Live Share API"
1187+
},
1188+
"r.liveShare.defaults.commandForward": {
1189+
"type": "boolean",
1190+
"default": false,
1191+
"description": "Default boolean value for guest command forwarding."
1192+
},
1193+
"r.liveShare.defaults.shareWorkspace": {
1194+
"type": "boolean",
1195+
"default": true,
1196+
"description": "Default boolean value for sharing the R workspace with guests."
1197+
},
1198+
"r.liveShare.defaults.shareBrowser": {
1199+
"type": "boolean",
1200+
"default": false,
1201+
"description": "Default boolean value for automatically sharing R browser ports with guests."
1202+
},
11471203
"r.plot.defaults.colorTheme": {
11481204
"type": "string",
11491205
"default": "original",
@@ -1250,6 +1306,7 @@
12501306
"popper.js": "^1.16.1",
12511307
"showdown": "^1.9.1",
12521308
"tree-kill": "^1.2.2",
1309+
"vsls": "^1.0.3015",
12531310
"winreg": "^1.2.4",
12541311
"ws": "^7.4.6"
12551312
}

src/extension.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import * as workspaceViewer from './workspaceViewer';
1616
import * as apiImplementation from './apiImplementation';
1717
import * as rHelp from './helpViewer';
1818
import * as completions from './completions';
19+
import * as rShare from './liveshare';
1920
import * as httpgdViewer from './plotViewer';
2021

2122

2223
// global objects used in other files
2324
export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undefined;
2425
export let globalRHelp: rHelp.RHelp | undefined = undefined;
2526
export let extensionContext: vscode.ExtensionContext;
27+
export let enableSessionWatcher: boolean = undefined;
2628
export let globalHttpgdManager: httpgdViewer.HttpgdManager | undefined = undefined;
2729

2830

@@ -36,6 +38,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
3638
// assign extension context to global variable
3739
extensionContext = context;
3840

41+
// assign session watcher setting to global variable
42+
enableSessionWatcher = util.config().get<boolean>('sessionWatcher', false);
43+
3944
// register commands specified in package.json
4045
const commands = {
4146
// create R terminal
@@ -91,15 +96,15 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
9196

9297
// workspace viewer
9398
'r.workspaceViewer.refreshEntry': () => rWorkspace?.refresh(),
94-
'r.workspaceViewer.view': (node: workspaceViewer.WorkspaceItem) => rTerminal.runTextInTerm(`View(${node.label})`),
95-
'r.workspaceViewer.remove': (node: workspaceViewer.WorkspaceItem) => rTerminal.runTextInTerm(`rm(${node.label})`),
99+
'r.workspaceViewer.view': (node: workspaceViewer.WorkspaceItem) => workspaceViewer.viewItem(node.label),
100+
'r.workspaceViewer.remove': (node: workspaceViewer.WorkspaceItem) => workspaceViewer.removeItem(node.label),
96101
'r.workspaceViewer.clear': workspaceViewer.clearWorkspace,
97102
'r.workspaceViewer.load': workspaceViewer.loadWorkspace,
98103
'r.workspaceViewer.save': workspaceViewer.saveWorkspace,
99104

100105
// browser controls
101106
'r.browser.refresh': session.refreshBrowser,
102-
'r.browser.openExternal': session.openExternalBrowser
107+
'r.browser.openExternal': session.openExternalBrowser,
103108

104109
// (help related commands are registered in rHelp.initializeHelp)
105110
};
@@ -142,6 +147,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
142147
vscode.languages.registerHoverProvider('r', new completions.HelpLinkHoverProvider());
143148
vscode.languages.registerCompletionItemProvider('r', new completions.StaticCompletionItemProvider(), '@');
144149

150+
// deploy liveshare listener
151+
await rShare.initLiveShare(context);
145152

146153
// register task provider
147154
const type = 'R';
@@ -165,20 +172,21 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
165172

166173

167174
// deploy session watcher (if configured by user)
168-
const enableSessionWatcher = util.config().get<boolean>('sessionWatcher', false);
169175
if (enableSessionWatcher) {
170-
console.info('Initialize session watcher');
171-
session.deploySessionWatcher(context.extensionPath);
172-
173-
// create status bar item that contains info about the session watcher
174-
console.info('Create sessionStatusBarItem');
175-
const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
176-
sessionStatusBarItem.command = 'r.attachActive';
177-
sessionStatusBarItem.text = 'R: (not attached)';
178-
sessionStatusBarItem.tooltip = 'Attach Active Terminal';
179-
sessionStatusBarItem.show();
180-
context.subscriptions.push(sessionStatusBarItem);
181-
session.startRequestWatcher(sessionStatusBarItem);
176+
if (!rShare.isGuestSession) {
177+
console.info('Initialize session watcher');
178+
void session.deploySessionWatcher(context.extensionPath);
179+
180+
// create status bar item that contains info about the session watcher
181+
console.info('Create sessionStatusBarItem');
182+
const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
183+
sessionStatusBarItem.command = 'r.attachActive';
184+
sessionStatusBarItem.text = 'R: (not attached)';
185+
sessionStatusBarItem.tooltip = 'Attach Active Terminal';
186+
sessionStatusBarItem.show();
187+
context.subscriptions.push(sessionStatusBarItem);
188+
void session.startRequestWatcher(sessionStatusBarItem);
189+
}
182190

183191
// track active text editor
184192
rstudioapi.trackLastActiveTextEditor(vscode.window.activeTextEditor);
@@ -199,5 +207,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
199207
vscode.languages.registerCompletionItemProvider('r', new completions.LiveCompletionItemProvider(), ...liveTriggerCharacters);
200208
}
201209

210+
202211
return rExtension;
203212
}

src/helpViewer/index.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { HelpPanel } from './panel';
1111
import { HelpProvider, AliasProvider } from './helpProvider';
1212
import { HelpTreeWrapper } from './treeView';
1313
import { PackageManager } from './packages';
14-
14+
import { isGuestSession, rGuestService } from '../liveshare';
1515

1616
// Initialization function that is called once when activating the extension
1717
export async function initializeHelp(context: vscode.ExtensionContext, rExtension: api.RExtension): Promise<RHelp|undefined> {
@@ -74,7 +74,7 @@ export async function initializeHelp(context: vscode.ExtensionContext, rExtensio
7474
}
7575
})
7676
);
77-
77+
7878
vscode.window.registerWebviewPanelSerializer('rhelp', rHelp);
7979
}
8080

@@ -180,7 +180,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
180180
this.treeViewWrapper = new HelpTreeWrapper(this);
181181
this.helpPanelOptions = options;
182182
}
183-
183+
184184

185185
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, path: string): Promise<void>{
186186
const panel = this.makeNewHelpPanel(webviewPanel);
@@ -295,7 +295,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
295295
}
296296
return false;
297297
}
298-
298+
299299
// quickly open help for selection
300300
public async openHelpForSelection(preserveFocus: boolean = false): Promise<boolean> {
301301
// only use if we failed to show help page:
@@ -358,7 +358,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
358358
}
359359
return false;
360360
}
361-
361+
362362
public async getMatchingAliases(token: string): Promise<Alias[]> {
363363
const aliases = await this.getAllAliases();
364364
if(!aliases){
@@ -370,7 +370,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
370370
|| token === `${alias.package}::${alias.name}`
371371
|| token === `${alias.package}:::${alias.name}`
372372
));
373-
373+
374374
return matchingAliases;
375375
}
376376

@@ -440,7 +440,9 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
440440
public async getHelpFileForPath(requestPath: string, modify: boolean = true): Promise<HelpFile | null> {
441441
// get helpFile from helpProvider if not cached
442442
if(!this.cachedHelpFiles.has(requestPath)){
443-
const helpFile = await this.helpProvider.getHelpFileFromRequestPath(requestPath);
443+
const helpFile = (!isGuestSession ?
444+
await this.helpProvider.getHelpFileFromRequestPath(requestPath) :
445+
await rGuestService.requestHelpContent(requestPath));
444446
this.cachedHelpFiles.set(requestPath, helpFile);
445447
}
446448

0 commit comments

Comments
 (0)