Skip to content

Commit

Permalink
LiveShare Functionality (#626)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ElianHugh authored Jun 11, 2021
1 parent 309cbe6 commit e736b8e
Show file tree
Hide file tree
Showing 17 changed files with 1,422 additions and 248 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
10 changes: 5 additions & 5 deletions R/vsc.R
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ if (show_view) {
list(columns = columns, data = data)
}

show_dataview <- function(x, title,
show_dataview <- function(x, title, uuid = NULL,
viewer = getOption("vsc.view", "Two")) {
if (missing(title)) {
sub <- substitute(x)
Expand Down Expand Up @@ -387,19 +387,19 @@ if (show_view) {
file <- tempfile(tmpdir = tempdir, fileext = ".json")
jsonlite::write_json(data, file, matrix = "rowmajor")
request("dataview", source = "table", type = "json",
title = title, file = file, viewer = viewer)
title = title, file = file, viewer = viewer, uuid = uuid)
} else if (is.list(x)) {
tryCatch({
file <- tempfile(tmpdir = tempdir, fileext = ".json")
jsonlite::write_json(x, file, auto_unbox = TRUE)
request("dataview", source = "list", type = "json",
title = title, file = file, viewer = viewer)
title = title, file = file, viewer = viewer, uuid = uuid)
}, error = function(e) {
file <- file.path(tempdir, paste0(make.names(title), ".txt"))
text <- utils::capture.output(print(x))
writeLines(text, file)
request("dataview", source = "object", type = "txt",
title = title, file = file, viewer = viewer)
title = title, file = file, viewer = viewer, uuid = uuid)
})
} else {
file <- file.path(tempdir, paste0(make.names(title), ".R"))
Expand All @@ -410,7 +410,7 @@ if (show_view) {
}
writeLines(code, file)
request("dataview", source = "object", type = "R",
title = title, file = file, viewer = viewer)
title = title, file = file, viewer = viewer, uuid = uuid)
}
}

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# R support for Visual Studio Code

[![Badge](https://aka.ms/vsls-badge)](https://aka.ms/vsls)

Requires [R](https://www.r-project.org/).

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

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

### Live Share support

The session watcher further enhances LiveShare collaboration.
The workspace viewer, data view, plots, and browsers are available to guests through the host session.
To enable this feature, *both* the host and guest must have this extension and session watcher enabled.

Hosts can control the level of access guests have through the provided Live Share Control view. This provides the following controls:

* Whether guests can access the current R session and its workspace
* Whether R commands should be forwarded from the guest to the host terminal (bypasses terminal permissions)
* Whether opened R browsers should be shared with guests

### How to disable it

For the case of basic usage, turning off `r.sessionWatcher` in VSCode settings is sufficient
Expand Down
61 changes: 59 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@
"icon": "./images/Rlogo.svg",
"contextualTitle": "R",
"when": "r.helpViewer.show"
},
{
"id": "rLiveShare",
"name": "Live Share Controls",
"icon": "./images/Rlogo.svg",
"contextualTitle": "R",
"when": "r.WorkspaceViewer:show && !r.liveShare:isGuest",
"visibility": "collapsed"
}
]
},
Expand All @@ -100,6 +108,16 @@
{
"view": "rHelpPages",
"contents": "R Help Pages"
},
{
"view": "rLiveShare",
"contents": "R Live Share not active.",
"when": "!r.liveShare:aborted"
},
{
"view": "rLiveShare",
"contents": "Could not connect to Live Share service.",
"when": "r.liveShare:aborted"
}
],
"languages": [
Expand Down Expand Up @@ -615,6 +633,15 @@
"category": "R",
"command": "r.helpPanel.openForPath"
},
{
"command": "r.liveShare.toggle",
"title": "Toggle"
},
{
"command": "r.liveShare.retry",
"title": "Retry connection to Live Share service",
"icon": "$(refresh)"
},
{
"title": "Toggle Style",
"category": "R Plot",
Expand Down Expand Up @@ -976,18 +1003,22 @@
"command": "r.helpPanel.summarizeTopics",
"group": "inline@8",
"when": "view == rHelpPages && viewItem =~ /_summarizeTopics_/"
},
{
"command": "r.liveShare.toggle",
"when": "view == rLiveShare"
}
],
"view/title": [
{
"command": "r.workspaceViewer.load",
"group": "navigation@0",
"when": "view == workspaceViewer"
"when": "view == workspaceViewer && !r.liveShare:isGuest"
},
{
"command": "r.workspaceViewer.save",
"group": "navigation@1",
"when": "view == workspaceViewer"
"when": "view == workspaceViewer && !r.liveShare:isGuest"
},
{
"command": "r.workspaceViewer.clear",
Expand All @@ -1003,6 +1034,11 @@
"command": "r.helpPanel.showQuickPick",
"group": "navigation",
"when": "view == rHelpPages"
},
{
"command": "r.liveShare.retry",
"group": "navigation@3",
"when": "view == rLiveShare && r.liveShare:aborted"
}
]
},
Expand Down Expand Up @@ -1144,6 +1180,26 @@
"default": false,
"description": "Remove hidden items when clearing workspace."
},
"r.liveShare.timeout": {
"type": "integer",
"default": 10000,
"description": "Time in milliseconds before aborting attempt to connect to Live Share API"
},
"r.liveShare.defaults.commandForward": {
"type": "boolean",
"default": false,
"description": "Default boolean value for guest command forwarding."
},
"r.liveShare.defaults.shareWorkspace": {
"type": "boolean",
"default": true,
"description": "Default boolean value for sharing the R workspace with guests."
},
"r.liveShare.defaults.shareBrowser": {
"type": "boolean",
"default": false,
"description": "Default boolean value for automatically sharing R browser ports with guests."
},
"r.plot.defaults.colorTheme": {
"type": "string",
"default": "original",
Expand Down Expand Up @@ -1250,6 +1306,7 @@
"popper.js": "^1.16.1",
"showdown": "^1.9.1",
"tree-kill": "^1.2.2",
"vsls": "^1.0.3015",
"winreg": "^1.2.4",
"ws": "^7.4.6"
}
Expand Down
41 changes: 25 additions & 16 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import * as workspaceViewer from './workspaceViewer';
import * as apiImplementation from './apiImplementation';
import * as rHelp from './helpViewer';
import * as completions from './completions';
import * as rShare from './liveshare';
import * as httpgdViewer from './plotViewer';


// global objects used in other files
export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undefined;
export let globalRHelp: rHelp.RHelp | undefined = undefined;
export let extensionContext: vscode.ExtensionContext;
export let enableSessionWatcher: boolean = undefined;
export let globalHttpgdManager: httpgdViewer.HttpgdManager | undefined = undefined;


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

// assign session watcher setting to global variable
enableSessionWatcher = util.config().get<boolean>('sessionWatcher', false);

// register commands specified in package.json
const commands = {
// create R terminal
Expand Down Expand Up @@ -91,15 +96,15 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp

// workspace viewer
'r.workspaceViewer.refreshEntry': () => rWorkspace?.refresh(),
'r.workspaceViewer.view': (node: workspaceViewer.WorkspaceItem) => rTerminal.runTextInTerm(`View(${node.label})`),
'r.workspaceViewer.remove': (node: workspaceViewer.WorkspaceItem) => rTerminal.runTextInTerm(`rm(${node.label})`),
'r.workspaceViewer.view': (node: workspaceViewer.WorkspaceItem) => workspaceViewer.viewItem(node.label),
'r.workspaceViewer.remove': (node: workspaceViewer.WorkspaceItem) => workspaceViewer.removeItem(node.label),
'r.workspaceViewer.clear': workspaceViewer.clearWorkspace,
'r.workspaceViewer.load': workspaceViewer.loadWorkspace,
'r.workspaceViewer.save': workspaceViewer.saveWorkspace,

// browser controls
'r.browser.refresh': session.refreshBrowser,
'r.browser.openExternal': session.openExternalBrowser
'r.browser.openExternal': session.openExternalBrowser,

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

// deploy liveshare listener
await rShare.initLiveShare(context);

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


// deploy session watcher (if configured by user)
const enableSessionWatcher = util.config().get<boolean>('sessionWatcher', false);
if (enableSessionWatcher) {
console.info('Initialize session watcher');
session.deploySessionWatcher(context.extensionPath);

// create status bar item that contains info about the session watcher
console.info('Create sessionStatusBarItem');
const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
sessionStatusBarItem.command = 'r.attachActive';
sessionStatusBarItem.text = 'R: (not attached)';
sessionStatusBarItem.tooltip = 'Attach Active Terminal';
sessionStatusBarItem.show();
context.subscriptions.push(sessionStatusBarItem);
session.startRequestWatcher(sessionStatusBarItem);
if (!rShare.isGuestSession) {
console.info('Initialize session watcher');
void session.deploySessionWatcher(context.extensionPath);

// create status bar item that contains info about the session watcher
console.info('Create sessionStatusBarItem');
const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
sessionStatusBarItem.command = 'r.attachActive';
sessionStatusBarItem.text = 'R: (not attached)';
sessionStatusBarItem.tooltip = 'Attach Active Terminal';
sessionStatusBarItem.show();
context.subscriptions.push(sessionStatusBarItem);
void session.startRequestWatcher(sessionStatusBarItem);
}

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


return rExtension;
}
16 changes: 9 additions & 7 deletions src/helpViewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { HelpPanel } from './panel';
import { HelpProvider, AliasProvider } from './helpProvider';
import { HelpTreeWrapper } from './treeView';
import { PackageManager } from './packages';

import { isGuestSession, rGuestService } from '../liveshare';

// Initialization function that is called once when activating the extension
export async function initializeHelp(context: vscode.ExtensionContext, rExtension: api.RExtension): Promise<RHelp|undefined> {
Expand Down Expand Up @@ -74,7 +74,7 @@ export async function initializeHelp(context: vscode.ExtensionContext, rExtensio
}
})
);

vscode.window.registerWebviewPanelSerializer('rhelp', rHelp);
}

Expand Down Expand Up @@ -180,7 +180,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
this.treeViewWrapper = new HelpTreeWrapper(this);
this.helpPanelOptions = options;
}


async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, path: string): Promise<void>{
const panel = this.makeNewHelpPanel(webviewPanel);
Expand Down Expand Up @@ -295,7 +295,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
}
return false;
}

// quickly open help for selection
public async openHelpForSelection(preserveFocus: boolean = false): Promise<boolean> {
// only use if we failed to show help page:
Expand Down Expand Up @@ -358,7 +358,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
}
return false;
}

public async getMatchingAliases(token: string): Promise<Alias[]> {
const aliases = await this.getAllAliases();
if(!aliases){
Expand All @@ -370,7 +370,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer<strin
|| token === `${alias.package}::${alias.name}`
|| token === `${alias.package}:::${alias.name}`
));

return matchingAliases;
}

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

Expand Down
Loading

0 comments on commit e736b8e

Please sign in to comment.