Skip to content

Commit b3835a2

Browse files
committed
Fix Live Share issues: improve plot sharing support, fix file reading for guests, and resolve circular dependency
1 parent 978e8f6 commit b3835a2

7 files changed

Lines changed: 123 additions & 78 deletions

File tree

‎src/liveShare/index.ts‎

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import * as vsls from 'vsls';
99
import * as fs from 'fs-extra';
1010

1111
import { enableSessionWatcher, extensionContext } from '../extension';
12+
import { docScheme, docProvider } from './virtualDocs';
13+
import * as path from 'path';
1214
import { activateRSessionGuest, browserDisposables, initGuest } from './shareSession';
1315
import { initTreeView, rLiveShareProvider, shareWorkspace, ToggleNode } from './shareTree';
1416
import { Commands, Callback, liveShareOnRequest, liveShareRequest } from './shareCommands';
@@ -79,6 +81,10 @@ export async function initLiveShare(context: vscode.ExtensionContext): Promise<v
7981
void vscode.commands.executeCommand('setContext', 'r.liveShare:isGuest', isGuestSession);
8082

8183
// push commands
84+
context.subscriptions.push(
85+
vscode.commands.registerCommand('r.activateRSessionGuest', () => activateRSessionGuest())
86+
);
87+
8288
if (!isGuestSession) {
8389
context.subscriptions.push(
8490
vscode.commands.registerCommand(
@@ -91,10 +97,6 @@ export async function initLiveShare(context: vscode.ExtensionContext): Promise<v
9197
}
9298
)
9399
);
94-
} else {
95-
context.subscriptions.push(
96-
vscode.commands.registerCommand('r.activateRSessionGuest', () => activateRSessionGuest())
97-
);
98100
}
99101
}
100102
}
@@ -144,6 +146,7 @@ export async function LiveSessionListener(): Promise<void> {
144146
break;
145147
case vsls.Role.Guest:
146148
console.log('[LiveSessionListener] guest event');
149+
initGuest(extensionContext);
147150
await rGuestService?.startService();
148151
break;
149152
case vsls.Role.Host:
@@ -165,6 +168,7 @@ export async function LiveSessionListener(): Promise<void> {
165168
break;
166169
case vsls.Role.Guest:
167170
console.log('[LiveSessionListener] guest event');
171+
initGuest(extensionContext);
168172
await rGuestService.startService();
169173
break;
170174
default:
@@ -200,7 +204,19 @@ export class HostService {
200204
// Provides core liveshare functionality
201205
// The shared service is used as a RPC service
202206
// to pass messages between the host and guests
203-
service = await liveSession.shareService(ShareProviderName);
207+
console.info(`[HostService] Attempting to share service: ${ShareProviderName}`);
208+
try {
209+
await liveSession.unshareService(ShareProviderName);
210+
} catch (e) {
211+
// ignore error if it wasn't shared
212+
}
213+
try {
214+
service = await liveSession.shareService(ShareProviderName);
215+
console.info(`[HostService] shareService returned: ${!!service}`);
216+
} catch (e) {
217+
console.error(`[HostService] shareService threw error: ${e}`);
218+
service = null;
219+
}
204220
if (service) {
205221
this._isStarted = true;
206222
for (const command in Commands.host) {
@@ -233,9 +249,9 @@ export class HostService {
233249
void this.notifyWorkspace(workspaceData);
234250
}
235251
}
236-
public notifyPlot(file: string): void {
252+
public notifyPlot(data: string, format: string): void {
237253
if (this._isStarted && shareWorkspace) {
238-
void liveShareRequest(Callback.NotifyPlotUpdate, file);
254+
void liveShareRequest(Callback.NotifyPlotUpdate, data, format);
239255
}
240256
}
241257
public notifyGuestPlotManager(url: string): void {
@@ -256,7 +272,9 @@ export class GuestService {
256272
return this._isStarted;
257273
}
258274
public async startService(): Promise<void> {
275+
console.info(`[GuestService] Attempting to get shared service: ${ShareProviderName}`);
259276
service = await liveSession.getSharedService(ShareProviderName);
277+
console.info(`[GuestService] getSharedService returned: ${!!service}`);
260278
if (service) {
261279
this._isStarted = true;
262280
this.requestAttach();
@@ -352,3 +370,45 @@ async function sessionCleanup(): Promise<void> {
352370
rLiveShareProvider.refresh();
353371
}
354372
}
373+
374+
export let guestResDir: string;
375+
376+
export function initGuest(context: vscode.ExtensionContext): void {
377+
if (_sessionStatusBarItem) {
378+
console.info('initGuest: sessionStatusBarItem already exists');
379+
return;
380+
}
381+
console.info('Create guestSessionStatusBarItem');
382+
const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
383+
sessionStatusBarItem.command = 'r.activateRSessionGuest';
384+
sessionStatusBarItem.text = 'Guest R: (not attached)';
385+
sessionStatusBarItem.tooltip = 'Click to activate host R session';
386+
sessionStatusBarItem.show();
387+
context.subscriptions.push(
388+
sessionStatusBarItem,
389+
vscode.workspace.registerTextDocumentContentProvider(docScheme, docProvider)
390+
);
391+
_sessionStatusBarItem = sessionStatusBarItem;
392+
rGuestService?.setStatusBarItem(sessionStatusBarItem);
393+
guestResDir = path.join(context.extensionPath, 'dist', 'resources');
394+
}
395+
396+
export async function activateRSessionGuest(): Promise<void> {
397+
if (!isGuest()) {
398+
void vscode.window.showInformationMessage('This command is only available for guests.');
399+
return;
400+
}
401+
if (config().get<boolean>('sessionWatcher', true)) {
402+
console.info('[activateRSessionGuest]');
403+
if (!rGuestService?.isStarted()) {
404+
await rGuestService?.startService();
405+
}
406+
if (rGuestService?.isStarted()) {
407+
void rGuestService.requestAttach();
408+
} else {
409+
void vscode.window.showWarningMessage('Failed to connect to host R service. Please ensure the host has enabled sharing.');
410+
}
411+
} else {
412+
void vscode.window.showInformationMessage('This command requires that r.sessionWatcher be enabled.');
413+
}
414+
}

‎src/liveShare/shareCommands.ts‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ export const Commands: ICommands = {
9595
[Callback.NotifyWorkspaceUpdate]: (args: [hostWorkspace: WorkspaceData]): void => {
9696
void updateGuestWorkspace(args[0]);
9797
},
98-
[Callback.NotifyPlotUpdate]: (args: [file: string]): void => {
99-
void updateGuestPlot(args[0]);
98+
[Callback.NotifyPlotUpdate]: (args: [data: string, format: string]): void => {
99+
void updateGuestPlot(args[0], args[1]);
100100
},
101101
[Callback.NotifyGuestPlotManager]: (args: [url: string]): void => {
102102
void globalPlotManager?.showHttpgdPlot(args[0]);
@@ -146,16 +146,8 @@ export function liveShareOnRequest(name: string, command: unknown, service: vsls
146146

147147
export function liveShareRequest(name: string, ...rest: unknown[]): unknown {
148148
if (isGuest()) {
149-
if (rest !== undefined) {
150-
return (service as vsls.SharedServiceProxy).request(name, rest);
151-
} else {
152-
return (service as vsls.SharedServiceProxy).request(name, []);
153-
}
149+
return (service as vsls.SharedServiceProxy).request(name, rest);
154150
} else {
155-
if (rest !== undefined) {
156-
return (service as vsls.SharedService).notify(name, { ...rest });
157-
} else {
158-
return (service as vsls.SharedService).notify(name, {});
159-
}
151+
return (service as vsls.SharedService).notify(name, rest);
160152
}
161153
}

‎src/liveShare/shareSession.ts‎

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import { extensionContext, globalPlotManager, globalRHelp, rWorkspace } from '..
55
import { asViewColumn, config, readContent } from '../util';
66
import { showBrowser, showDataView, WorkspaceData } from '../session';
77
import { showWebView } from '../webViewer';
8-
import { liveSession, UUID, rGuestService, _sessionStatusBarItem as sessionStatusBarItem } from '.';
8+
import { liveSession, UUID, rGuestService, _sessionStatusBarItem as sessionStatusBarItem, isGuest, guestResDir } from '.';
99
import { autoShareBrowser } from './shareTree';
1010
import { docProvider, docScheme } from './virtualDocs';
1111

1212
// Workspace Vars
1313
let guestPid: string;
1414
export let guestWorkspace: WorkspaceData | undefined;
15-
export let guestResDir: string;
1615
let rVer: string;
1716
let info: IRequest['info'];
1817

@@ -46,21 +45,7 @@ export interface IRequest {
4645
};
4746
}
4847

49-
export function initGuest(context: vscode.ExtensionContext): void {
50-
// create status bar item that contains info about the *guest* session watcher
51-
console.info('Create guestSessionStatusBarItem');
52-
const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
53-
sessionStatusBarItem.command = 'r.activateRSessionGuest';
54-
sessionStatusBarItem.text = 'Guest R: (not attached)';
55-
sessionStatusBarItem.tooltip = 'Click to activate host R session';
56-
sessionStatusBarItem.show();
57-
context.subscriptions.push(
58-
sessionStatusBarItem,
59-
vscode.workspace.registerTextDocumentContentProvider(docScheme, docProvider)
60-
);
61-
rGuestService?.setStatusBarItem(sessionStatusBarItem);
62-
guestResDir = path.join(context.extensionPath, 'dist', 'resources');
63-
}
48+
6449

6550
export function detachGuest(): void {
6651
console.info('[Guest Service] detach guest from workspace');
@@ -70,20 +55,13 @@ export function detachGuest(): void {
7055
rWorkspace?.refresh();
7156
}
7257

73-
export function activateRSessionGuest(): void {
74-
if (config().get<boolean>('sessionWatcher', true)) {
75-
console.info('[activateRSessionGuest]');
76-
void rGuestService?.requestAttach();
77-
} else {
78-
void vscode.window.showInformationMessage('This command requires that r.sessionWatcher be enabled.');
79-
}
80-
}
58+
8159

8260
// Guest version of session.ts updateRequest(), no need to check for changes in files
8361
// as this is handled by the session.ts variant
8462
// the force parameter is used for ensuring that the 'attach' case is appropriately called on guest join
8563
export async function updateGuestRequest(file: string, force: boolean = false): Promise<void> {
86-
const requestContent: string | undefined = await readContent(file, 'utf8');
64+
const requestContent: string | undefined = await rGuestService?.requestFileContent(file, 'utf8');
8765
if (!requestContent) {
8866
return;
8967
}
@@ -197,14 +175,11 @@ export function updateGuestWorkspace(hostWorkspace: WorkspaceData): void {
197175
// Instead of creating a file, we pass the base64 of the plot image
198176
// to the guest, and read that into an html page
199177
let panel: vscode.WebviewPanel | undefined = undefined;
200-
export async function updateGuestPlot(file: string): Promise<void> {
201-
const plotContent = await readContent(file, 'base64');
202-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
203-
178+
export async function updateGuestPlot(data: string, format: string): Promise<void> {
204179
const guestPlotView: vscode.ViewColumn = asViewColumn(config().get<string>('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two);
205-
if (plotContent) {
180+
if (data) {
206181
if (panel) {
207-
panel.webview.html = getGuestImageHtml(plotContent);
182+
panel.webview.html = getGuestImageHtml(data, format);
208183
panel.reveal(guestPlotView, true);
209184
} else {
210185
panel = vscode.window.createWebviewPanel('dataview', 'R Guest Plot',
@@ -218,7 +193,7 @@ export async function updateGuestPlot(file: string): Promise<void> {
218193
retainContextWhenHidden: true,
219194
localResourceRoots: [vscode.Uri.file(guestResDir)],
220195
});
221-
const content = getGuestImageHtml(plotContent);
196+
const content = getGuestImageHtml(data, format);
222197
panel.webview.html = content;
223198
panel.onDidDispose(
224199
() => {
@@ -234,30 +209,41 @@ export async function updateGuestPlot(file: string): Promise<void> {
234209

235210
// Purely used in order to decode a base64 string into
236211
// an image format, bypassing saving a file onto the guest's system
237-
function getGuestImageHtml(content: string) {
212+
function getGuestImageHtml(content: string, format: string) {
213+
let imageSrc = '';
214+
if (format === 'svglite' || format === 'svg') {
215+
imageSrc = `data:image/svg+xml;base64,${String(content)}`;
216+
} else {
217+
imageSrc = `data:image/png;base64,${String(content)}`;
218+
}
219+
238220
return `
239221
<!doctype HTML>
240222
<html>
241223
<head>
242224
<meta charset="utf-8" />
243225
<meta name="viewport" content="width=device-width, initial-scale=1">
244226
<style type="text/css">
245-
body {
246-
color: black;
227+
body, html {
228+
margin: 0;
229+
padding: 0;
230+
width: 100%;
231+
height: 100%;
232+
overflow: hidden;
247233
background-color: var(--vscode-editor-background);
234+
display: flex;
235+
justify-content: center;
236+
align-items: center;
248237
}
249238
img {
250-
position: absolute;
251-
top:0;
252-
bottom: 0;
253-
left: 0;
254-
right: 0;
255-
margin: auto;
239+
max-width: 100%;
240+
max-height: 100%;
241+
object-fit: contain;
256242
}
257243
</style>
258244
</head>
259245
<body>
260-
<img src = "data:image/png;base64, ${String(content)}">
246+
<img src = "${imageSrc}">
261247
</body>
262248
</html>
263249
`;

‎src/plotViewer/httpgdViewer.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class HttpgdManager {
4848
this.viewerOptions.fullWindow = conf.get('plot.defaults.fullWindowMode', false);
4949
this.viewerOptions.token = token;
5050
const viewer = new HttpgdViewer(host, this.viewerOptions);
51-
if (isHost() && autoShareBrowser) {
51+
if (isHost()) {
5252
const disposable = await shareServer(url, 'httpgd');
5353
viewer.webviewPanel?.onDidDispose(() => void disposable.dispose());
5454
}

‎src/plotViewer/standardViewer.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
33
import { asViewColumn, config, UriIcon } from '../util';
44
import { sessionRequest, server } from '../session';
55
import { PlotViewer } from './types';
6+
import { rHostService } from '../liveShare';
67

78
interface PlotResponse {
89
data: string;
@@ -99,6 +100,7 @@ export class StandardPlotViewer implements PlotViewer {
99100
data: this.plotData,
100101
format: this.plotFormat
101102
});
103+
rHostService?.notifyPlot(this.plotData, this.plotFormat);
102104
}
103105
}
104106

‎src/test/suite/sessInstall.test.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ suite('Sess Install Test Suite', () => {
2828
const getVersionStub = sandbox.stub(util, 'getRPackageVersion').resolves(undefined);
2929
const showMessageStub = sandbox.stub(vscode.window, 'showErrorMessage').resolves(undefined);
3030

31-
await util.promptToInstallSessPackage();
31+
await util.promptToInstallSessPackage(undefined, undefined, getVersionStub);
3232

3333
assert.strictEqual(getVersionStub.called, false);
3434
assert.strictEqual(showMessageStub.called, false);
3535
});
3636

3737
test('promptToInstallSessPackage prompts to install if not installed', async () => {
3838
await vscode.workspace.getConfiguration('r').update('sessionWatcher', true, vscode.ConfigurationTarget.Global);
39-
sandbox.stub(util, 'getRPackageVersion').resolves(undefined);
39+
const getVersionStub = sandbox.stub(util, 'getRPackageVersion').resolves(undefined);
4040

4141
// Mock reading DESCRIPTION file
42-
sandbox.stub(util, 'readFileSyncSafe').returns('Package: sess\nVersion: 0.1.0\n');
42+
const readFileStub = sandbox.stub(util, 'readFileSyncSafe').returns('Package: sess\nVersion: 0.1.0\n');
4343

4444
const showMessageStub = sandbox.stub(vscode.window, 'showErrorMessage').resolves(undefined);
4545

46-
await util.promptToInstallSessPackage();
46+
await util.promptToInstallSessPackage(undefined, undefined, getVersionStub, readFileStub);
4747

4848
assert.strictEqual(showMessageStub.calledOnce, true);
4949
const args = showMessageStub.getCall(0).args;
@@ -52,14 +52,14 @@ suite('Sess Install Test Suite', () => {
5252

5353
test('promptToInstallSessPackage prompts to update if installed version is older', async () => {
5454
await vscode.workspace.getConfiguration('r').update('sessionWatcher', true, vscode.ConfigurationTarget.Global);
55-
sandbox.stub(util, 'getRPackageVersion').resolves('0.0.9');
55+
const getVersionStub = sandbox.stub(util, 'getRPackageVersion').resolves('0.0.9');
5656

5757
// Mock reading DESCRIPTION file with newer version
58-
sandbox.stub(util, 'readFileSyncSafe').returns('Package: sess\nVersion: 0.1.0\n');
58+
const readFileStub = sandbox.stub(util, 'readFileSyncSafe').returns('Package: sess\nVersion: 0.1.0\n');
5959

6060
const showMessageStub = sandbox.stub(vscode.window, 'showErrorMessage').resolves(undefined);
6161

62-
await util.promptToInstallSessPackage();
62+
await util.promptToInstallSessPackage(undefined, undefined, getVersionStub, readFileStub);
6363

6464
assert.strictEqual(showMessageStub.calledOnce, true);
6565
const args = showMessageStub.getCall(0).args;
@@ -68,13 +68,13 @@ suite('Sess Install Test Suite', () => {
6868

6969
test('promptToInstallSessPackage does not prompt if installed version is newer or equal', async () => {
7070
await vscode.workspace.getConfiguration('r').update('sessionWatcher', true, vscode.ConfigurationTarget.Global);
71-
sandbox.stub(util, 'getRPackageVersion').resolves('0.1.0');
71+
const getVersionStub = sandbox.stub(util, 'getRPackageVersion').resolves('0.1.0');
7272

73-
sandbox.stub(util, 'readFileSyncSafe').returns('Package: sess\nVersion: 0.1.0\n');
73+
const readFileStub = sandbox.stub(util, 'readFileSyncSafe').returns('Package: sess\nVersion: 0.1.0\n');
7474

7575
const showMessageStub = sandbox.stub(vscode.window, 'showErrorMessage').resolves(undefined);
7676

77-
await util.promptToInstallSessPackage();
77+
await util.promptToInstallSessPackage(undefined, undefined, getVersionStub, readFileStub);
7878

7979
assert.strictEqual(showMessageStub.called, false);
8080
});

0 commit comments

Comments
 (0)