diff --git a/html/httpgd/index.ejs b/html/httpgd/index.ejs
index d9afdd079..99036c41c 100644
--- a/html/httpgd/index.ejs
+++ b/html/httpgd/index.ejs
@@ -8,7 +8,7 @@
>
- <%- largePlot?.svg %>
+ <%- largePlot?.data %>
diff --git a/html/httpgd/smallPlot.ejs b/html/httpgd/smallPlot.ejs
index 1c24490cc..7d8337661 100644
--- a/html/httpgd/smallPlot.ejs
+++ b/html/httpgd/smallPlot.ejs
@@ -4,7 +4,7 @@
class="focusPlot <%- plot.id === activePlot ? 'active' : '' %> "
plotId="<%- plot.id %>"
>
- <%- plot.svg %>
+ <%- plot.data %>
{
- const res = await fetch(this.remove_index(index).href, {
- headers: this.httpHeaders
- });
- return res;
- }
-
- private remove_id(id: PlotId): URL {
- const url = new URL(this.httpRemove);
- url.searchParams.append('id', id);
- return url;
- }
-
- public async get_remove_id(id: PlotId): Promise {
- const res = await fetch(this.remove_id(id).href, {
- headers: this.httpHeaders
- });
- return res;
- }
-
- public async get_plots(): Promise {
- const res = await fetch(this.httpPlots, {
- headers: this.httpHeaders
- });
- return await (res.json() as Promise);
- }
-
- public async get_plot_contents_all(): Promise {
- const plotIds = await this.get_plots();
- const plots = plotIds.plots.map(async idRes => {
- return await this.get_plot_contents(idRes.id);
- });
- return await Promise.all(plots);
- }
-
- public async get_plot_contents(id: PlotId, width?: number, height?: number, c?: string): Promise {
- const url = this.svg_id(id, width, height, c).toString();
- const plot = fetch(url).then(res => res.text()).then(res => {
- return {
- url: url,
- host: this.host,
- id: id,
- svg: res,
- height: height,
- width: width,
- };
- });
- return plot;
- }
-
-
- public async get_clear(): Promise {
- const res = await fetch(this.httpClear, {
- headers: this.httpHeaders
- });
- return res;
- }
-
- public async get_state(): Promise {
- const res = await fetch(this.httpState, {
- headers: this.httpHeaders
- });
- return await (res.json() as Promise);
- }
-
- public new_websocket(): WebSocket {
- return new WebSocket(this.ws);
- }
-}
-
-const enum HttpgdConnectionMode {
- NONE,
- POLL,
- SLOWPOLL,
- WEBSOCKET
-}
-
-/**
- * Handles HTTP polling / WebSocket connection.
- * This handles falling back to HTTP polling when WebSockets are not available,
- * and automatically reconnects if the server is temporarily unavailable.
- */
-class HttpgdConnection {
- private static readonly INTERVAL_POLL: number = 500;
- private static readonly INTERVAL_POLL_SLOW: number = 5000;
-
- public api: HttpgdApi;
-
- private mode: HttpgdConnectionMode = HttpgdConnectionMode.NONE;
- private allowWebsockets: boolean;
-
- private socket?: WebSocket;
- private pollHandle?: ReturnType;
-
- private pausePoll: boolean = false;
- private disconnected: boolean = true;
-
- private lastState?: HttpgdState;
-
- public remoteStateChanged?: (newState: HttpgdState) => void;
- public connectionChanged?: (disconnected: boolean) => void;
-
- public constructor(host: string, token?: string, allowWebsockets?: boolean) {
- this.api = new HttpgdApi(host, token);
- this.allowWebsockets = allowWebsockets ? allowWebsockets : false;
- }
-
- public open(): void {
- if (this.mode !== HttpgdConnectionMode.NONE) {return;}
- this.start(HttpgdConnectionMode.WEBSOCKET);
- }
-
- public close(): void {
- if (this.mode === HttpgdConnectionMode.NONE) {return;}
- this.start(HttpgdConnectionMode.NONE);
- }
-
- private start(targetMode: HttpgdConnectionMode): void {
- if (this.mode === targetMode) {return;}
-
- switch (targetMode) {
- case HttpgdConnectionMode.POLL:
- console.log('Start POLL');
- this.clearWebsocket();
- this.clearPoll();
- this.pollHandle = setInterval(() => this.poll(), HttpgdConnection.INTERVAL_POLL);
- this.mode = targetMode;
- break;
- case HttpgdConnectionMode.SLOWPOLL:
- console.log('Start SLOWPOLL');
- this.clearWebsocket();
- this.clearPoll();
- this.pollHandle = setInterval(() => this.poll(), HttpgdConnection.INTERVAL_POLL_SLOW);
- this.mode = targetMode;
- break;
- case HttpgdConnectionMode.WEBSOCKET:
- if (!this.allowWebsockets) {
- this.start(HttpgdConnectionMode.POLL);
- break;
- }
- console.log('Start WEBSOCKET');
- this.clearPoll();
- this.clearWebsocket();
-
- this.socket = this.api.new_websocket();
- this.socket.onmessage = (ev) => this.onWsMessage(ev.data.toString());
- this.socket.onopen = () => this.onWsOpen();
- this.socket.onclose = () => this.onWsClose();
- this.socket.onerror = () => console.log('Websocket error');
- this.mode = targetMode;
- this.poll(); // get initial state
- break;
- case HttpgdConnectionMode.NONE:
- this.clearWebsocket();
- this.clearPoll();
- this.mode = targetMode;
- break;
- default:
- break;
- }
-
- }
-
- private clearPoll() {
- if (this.pollHandle) {
- clearInterval(this.pollHandle);
- }
- }
-
- private clearWebsocket() {
- if (this.socket) {
- this.socket.onclose = () => { /* ignore? */ };
- this.socket.close();
- }
- }
-
- private poll(): void {
- if (this.pausePoll) {return;}
- this.api.get_state().then((remoteState: HttpgdState) => {
- this.setDisconnected(false);
- if (this.mode === HttpgdConnectionMode.SLOWPOLL) {this.start(HttpgdConnectionMode.WEBSOCKET);} // reconnect
- if (this.pausePoll) {return;}
- this.checkState(remoteState);
- }).catch((e) => {
- console.warn(e);
- this.setDisconnected(true);
- });
- }
-
- private onWsMessage(message: string): void {
- if (message.startsWith('{')) {
- const remoteState = JSON.parse(message) as HttpgdState;
- this.checkState(remoteState);
- } else {
- console.log('Unknown WS message: ' + message);
- }
- }
- private onWsClose(): void {
- console.log('Websocket closed');
- this.setDisconnected(true);
- }
- private onWsOpen(): void {
- console.log('Websocket opened');
- this.setDisconnected(false);
- }
-
- private setDisconnected(disconnected: boolean): void {
- if (this.disconnected !== disconnected) {
- this.disconnected = disconnected;
- if (this.disconnected) {
- this.start(HttpgdConnectionMode.SLOWPOLL);
- } else {
- this.start(HttpgdConnectionMode.WEBSOCKET);
- }
- this.connectionChanged?.(disconnected);
- }
- }
-
- private checkState(remoteState: HttpgdState): void {
- if (
- (!this.lastState) ||
- (this.lastState.active !== remoteState.active) ||
- (this.lastState.hsize !== remoteState.hsize) ||
- (this.lastState.upid !== remoteState.upid)
- ) {
- this.lastState = remoteState;
- this.remoteStateChanged?.(remoteState);
- }
- }
-}
-
-/**
- * Public API for communicating with a httpgd server.
- */
-export class Httpgd implements IHttpgdViewerApi {
-
- private connection: HttpgdConnection;
-
- // Constructor is called by the viewer:
- public constructor(host: string, token?: string)
- {
- this.connection = new HttpgdConnection(host, token, true);
- }
-
- // Opens the connection to the server
- public start(): void {
- this.connection.open();
- }
-
- // api calls:
- // general state info:
- public getState(): Promise {
- return this.connection.api.get_state();
- }
- // get list of plot Ids:
- public getPlotIds(): Promise {
- return this.connection.api.get_plots().then(res => res.plots.map(r => r.id));
- }
- // get content of a single plot. Use sensible defaults if no height/width given:
- public getPlotContent(id: PlotId, height?: number, width?: number, c?: string): Promise {
- return this.connection.api.get_plot_contents(id, width, height, c);
- }
- // get content of multiple plots:
- // Use sensible defaults if no height/width given.
- // Return all plots if no ids given.
- public getPlotContents(ids?: PlotId[], height?: number, width?: number): Promise {
- if (!ids) {
- return this.connection.api.get_plot_contents_all();
- }
- const plots = ids.map(async id => {
- return await this.connection.api.get_plot_contents(id, width, height);
- });
- return Promise.all(plots);
- }
-
- // close/remove plot
- public async closePlot(id: PlotId): Promise {
- await this.connection.api.get_remove_id(id);
- }
-
- // Listen to connection changes of the httpgd server
- // Todo: Expand to fill observer pattern with multiple listeners (?)
- public onConnectionChange(listener: (disconnected: boolean) => void): void {
- this.connection.connectionChanged = listener;
- }
-
- // Listen to plot changes of the httpgd server
- // Todo: Expand to fill observer pattern with multiple listeners (?)
- public onPlotsChange(listener: () => void): void {
- this.connection.remoteStateChanged = listener;
- }
-
- // Dispose-function to clean up when vscode closes
- public dispose(): void {
- this.connection.close();
- }
-}
diff --git a/src/plotViewer/httpgdTypes.d.ts b/src/plotViewer/httpgdTypes.d.ts
index 98820865e..23dc7c8ea 100644
--- a/src/plotViewer/httpgdTypes.d.ts
+++ b/src/plotViewer/httpgdTypes.d.ts
@@ -1,81 +1,25 @@
+import { Httpgd } from 'httpgd';
+import { HttpgdPlotId } from 'httpgd/lib/types';
import * as vscode from 'vscode';
import { HttpgdManager } from '.';
import { PreviewPlotLayout } from './webviewMessages';
export type MaybePromise = T | Promise;
-// type to indicate where a plotId is required
-export type PlotId = string;
-
-// supported file types for image export
-export type ExportFormat = 'png' | 'jpg' | 'bmp' | 'svg';
-
-
-export interface HttpgdPlot {
- // url of the connection this plot was retrieved from
- url: string;
- host: string;
+export interface HttpgdPlot {
// unique ID for this plot (w.r.t. this connection/device)
- id: PlotId;
+ id: HttpgdPlotId;
- // svg of the plot
- svg: string;
+ // data of the plot
+ data: T;
- // Size when computed:
- // (displayed size might vary, if % values are used)
+ // Size
height: number;
width: number;
-}
-
-export interface HttpgdState {
- // What do these mean?
- upid: number;
- hsize: number;
- active: boolean;
- // /?
-
- // Include which plots have changed?
- changedPlots?: PlotId[];
-
- // Indicate that R wants to focus a specific plot?
- focusPlot?: PlotId;
-}
-
-// Roughly combines the functionality of HttpgdApi and HttpgdConnection
-export declare class IHttpgdViewerApi {
- // Constructor is called by the viewer:
- public constructor(host: string, token?: string);
-
- // api calls:
- // general state info:
- public getState(): MaybePromise;
- // get list of plot Ids:
- public getPlotIds(): MaybePromise;
- // get content of a single plot. Use sensible defaults if no height/width given:
- public getPlotContent(id: PlotId, height?: number, width?: number): MaybePromise;
- // get content of multiple plots:
- // Use sensible defaults if no height/width given.
- // Return all plots if no ids given.
- public getPlotContents(ids?: PlotId[], height?: number, width?: number): MaybePromise;
-
- // Export functionality could maybe also be implemented inside vscode-R?
- // Not sure which libraries produce better results...
- // User querying for format and filename is done by vscode
- public exportPlot?(id: PlotId, format: ExportFormat, outFile: string): MaybePromise;
-
- // Method to supply listeners
- // The listener should be called when there is a change to the device
- // Further info (new state, plots etc.) can then be queried by the viewer
- public onConnectionChange(listener: (disconnected: boolean) => void): void;
- public onPlotsChange(listener: () => void): void;
-
- // Dispose-function to clean up when vscode closes
- // E.g. to close connections etc., notify R, ...
- // Not sure if sensible here
- public dispose?(): MaybePromise;
+ zoom: number;
}
// Example for possible viewer creation options:
@@ -99,13 +43,13 @@ export class IHttpgdViewer {
webviewPanel?: vscode.WebviewPanel;
// Api that provides plot contents etc.
- api?: IHttpgdViewerApi;
+ api: Httpgd;
// active plots
- plots: HttpgdPlot[];
+ plots: HttpgdPlot[];
// Id of the currently viewed plot
- activePlot?: PlotId;
+ activePlot?: HttpgdPlotId;
// Size of the view area:
viewHeight: number;
@@ -123,7 +67,7 @@ export class IHttpgdViewer {
show(preserveFocus?: boolean): void;
// focus a specific plot id
- focusPlot(id: PlotId): void;
+ focusPlot(id: HttpgdPlotId): void;
// navigate through plots (supply `true` to go to end/beginning of list)
nextPlot(last?: boolean): void;
@@ -135,7 +79,7 @@ export class IHttpgdViewer {
// export plot
// if no format supplied, show a quickpick menu etc.
// if no filename supplied, show selector window
- exportPlot(id: PlotId, format?: ExportFormat, outFile?: string): void;
+ exportPlot(id: HttpgdPlotId, format?: string, outFile?: string): void;
// Dispose-function to clean up when vscode closes
// E.g. to close connections etc., notify R, ...
diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts
index d9d5e7146..aa68f0c26 100644
--- a/src/plotViewer/index.ts
+++ b/src/plotViewer/index.ts
@@ -3,8 +3,8 @@
import * as vscode from 'vscode';
-import { Httpgd } from './httpgd';
-import { HttpgdPlot, IHttpgdViewer, HttpgdViewerOptions, PlotId, ExportFormat, HttpgdState } from './httpgdTypes';
+import { Httpgd } from 'httpgd';
+import { HttpgdPlot, IHttpgdViewer, HttpgdViewerOptions } from './httpgdTypes';
import * as path from 'path';
import * as fs from 'fs';
import * as ejs from 'ejs';
@@ -14,8 +14,8 @@ import { config, setContext, UriIcon } from '../util';
import { extensionContext } from '../extension';
import { FocusPlotMessage, InMessage, OutMessage, ToggleStyleMessage, UpdatePlotMessage, HidePlotMessage, AddPlotMessage, PreviewPlotLayout, PreviewPlotLayoutMessage, ToggleFullWindowMessage } from './webviewMessages';
-
-import { isHost, rHostService, shareBrowser } from '../liveShare';
+import { HttpgdIdResponse, HttpgdPlotId, HttpgdRendererId } from 'httpgd/lib/types';
+import { Response } from 'node-fetch';
const commands = [
'showViewers',
@@ -41,7 +41,7 @@ type CommandName = typeof commands[number];
export function initializeHttpgd(): HttpgdManager {
const httpgdManager = new HttpgdManager();
- for(const cmd of commands){
+ for (const cmd of commands) {
const fullCommand = `r.plot.${cmd}`;
const cb = httpgdManager.getCommandHandler(cmd);
vscode.commands.registerCommand(fullCommand, cb);
@@ -56,7 +56,7 @@ export class HttpgdManager {
recentlyActiveViewers: HttpgdViewer[] = [];
- constructor(){
+ constructor() {
const htmlRoot = extensionContext.asAbsolutePath('html/httpgd');
this.viewerOptions = {
parent: this,
@@ -67,17 +67,16 @@ export class HttpgdManager {
public showViewer(urlString: string): void {
const url = new URL(urlString);
-
const host = url.host;
const token = url.searchParams.get('token') || undefined;
const ind = this.viewers.findIndex(
(viewer) => viewer.host === host
);
- if(ind >= 0){
+ if (ind >= 0) {
const viewer = this.viewers.splice(ind, 1)[0];
this.viewers.unshift(viewer);
viewer.show();
- } else{
+ } else {
const conf = config();
const colorTheme = conf.get('plot.defaults.colorTheme', 'vscode');
this.viewerOptions.stripStyles = (colorTheme === 'vscode');
@@ -93,7 +92,7 @@ export class HttpgdManager {
public registerActiveViewer(viewer: HttpgdViewer): void {
const ind = this.recentlyActiveViewers.indexOf(viewer);
- if(ind){
+ if (ind) {
this.recentlyActiveViewers.splice(ind, 1);
}
this.recentlyActiveViewers.unshift(viewer);
@@ -121,7 +120,7 @@ export class HttpgdManager {
prompt: 'Please enter the httpgd url'
};
const urlString = await vscode.window.showInputBox(options);
- if(urlString){
+ if (urlString) {
this.showViewer(urlString);
}
}
@@ -135,22 +134,22 @@ export class HttpgdManager {
// below is an attempt to handle these different combinations efficiently and (somewhat) robustly
//
- if(command === 'showViewers'){
+ if (command === 'showViewers') {
this.viewers.forEach(viewer => {
viewer.show(true);
});
return;
- } else if(command === 'openUrl'){
+ } else if (command === 'openUrl') {
void this.openUrl();
return;
}
// Identify the correct viewer
let viewer: HttpgdViewer | undefined;
- if(typeof hostOrWebviewUri === 'string'){
+ if (typeof hostOrWebviewUri === 'string') {
const host = hostOrWebviewUri;
viewer = this.viewers.find((viewer) => viewer.host === host);
- } else if(hostOrWebviewUri instanceof vscode.Uri){
+ } else if (hostOrWebviewUri instanceof vscode.Uri) {
const uri = hostOrWebviewUri;
viewer = this.viewers.find((viewer) => viewer.getPanelPath() === uri.path);
}
@@ -159,7 +158,7 @@ export class HttpgdManager {
viewer ||= this.getRecentViewer();
// Abort if no viewer identified
- if(!viewer){
+ if (!viewer) {
return;
}
@@ -168,7 +167,7 @@ export class HttpgdManager {
const boolArg = findItemOfType(args, 'boolean');
// Call corresponding method, possibly with an argument:
- switch(command) {
+ switch (command) {
case 'showIndex': {
void viewer.focusPlot(stringArg);
break;
@@ -225,9 +224,9 @@ export class HttpgdManager {
interface EjsData {
overwriteStyles: boolean;
previewPlotLayout: PreviewPlotLayout;
- activePlot?: PlotId;
- plots: HttpgdPlot[];
- largePlot: HttpgdPlot;
+ activePlot?: HttpgdPlotId;
+ plots: HttpgdPlot[];
+ largePlot: HttpgdPlot;
host: string;
asLocalPath: (relPath: string) => string;
asWebViewPath: (localPath: string) => string;
@@ -235,7 +234,7 @@ interface EjsData {
overwriteCssPath: string;
// only used to render an individual smallPlot div:
- plot?: HttpgdPlot;
+ plot?: HttpgdPlot;
}
interface ShowOptions {
@@ -255,17 +254,16 @@ export class HttpgdViewer implements IHttpgdViewer {
webviewPanel?: vscode.WebviewPanel;
// Api that provides plot contents etc.
- api?: Httpgd;
+ readonly api: Httpgd;
// active plots
- plots: HttpgdPlot[] = [];
- state?: HttpgdState;
+ plots: HttpgdPlot[] = [];
// Id of the currently viewed plot
- activePlot?: PlotId;
+ activePlot?: HttpgdPlotId;
// Ids of plots that are not shown, but not closed inside httpgd
- hiddenPlots: PlotId[] = [];
+ hiddenPlots: HttpgdPlotId[] = [];
readonly defaultStripStyles: boolean = true;
stripStyles: boolean;
@@ -287,14 +285,16 @@ export class HttpgdViewer implements IHttpgdViewer {
plotHeight: number;
plotWidth: number;
- readonly scale0: number = 1;
- scale: number = this.scale0;
+ readonly zoom0: number = 1;
+ zoom: number = this.zoom0;
resizeTimeout?: NodeJS.Timeout;
readonly resizeTimeoutLength: number = 1300;
refreshTimeout?: NodeJS.Timeout;
readonly refreshTimeoutLength: number = 10;
+
+ private lastExportUri?: vscode.Uri;
readonly htmlTemplate: string;
readonly smallPlotTemplate: string;
@@ -307,50 +307,44 @@ export class HttpgdViewer implements IHttpgdViewer {
// Get/set active plot by index instead of id:
protected get activeIndex(): number {
+ if(!this.activePlot){
+ return -1;
+ }
return this.getIndex(this.activePlot);
}
protected set activeIndex(ind: number) {
- if(this.plots.length === 0){
+ if (this.plots.length === 0) {
this.activePlot = undefined;
- } else{
+ } else {
ind = Math.max(ind, 0);
ind = Math.min(ind, this.plots.length - 1);
this.activePlot = this.plots[ind].id;
}
}
- // Get scaled view size:
- protected get scaledViewHeight(): number {
- return this.viewHeight * this.scale;
- }
- protected get scaledViewWidth(): number {
- return this.viewWidth * this.scale;
- }
-
// constructor called by the session watcher if a corresponding function was called in R
// creates a new api instance itself
constructor(host: string, options: HttpgdViewerOptions) {
this.host = host;
this.token = options.token;
this.parent = options.parent;
- this.api = new Httpgd(this.host, this.token);
- this.api.onPlotsChange(() => {
- this.checkStateDelayed();
+
+ this.api = new Httpgd(this.host, this.token, true);
+ this.api.onPlotsChanged((newState) => {
+ void this.refreshPlots(newState.plots);
});
- this.api.onConnectionChange((disconnected: boolean) => {
- if(disconnected){
- this.api?.dispose();
- this.api = undefined;
- } else{
- this.checkStateDelayed();
- }
+ this.api.onConnectionChanged(() => {
+ // todo
+ });
+ this.api.onDeviceActiveChanged(() => {
+ // todo
});
const conf = config();
this.customOverwriteCssPath = conf.get('plot.customStyleOverwrites', '');
const localResourceRoots = (
this.customOverwriteCssPath ?
- [extensionContext.extensionUri, vscode.Uri.file(path.dirname(this.customOverwriteCssPath))] :
- undefined
+ [extensionContext.extensionUri, vscode.Uri.file(path.dirname(this.customOverwriteCssPath))] :
+ undefined
);
this.htmlRoot = options.htmlRoot;
this.htmlTemplate = fs.readFileSync(path.join(this.htmlRoot, 'index.ejs'), 'utf-8');
@@ -373,8 +367,8 @@ export class HttpgdViewer implements IHttpgdViewer {
this.fullWindow = this.defaultFullWindow;
this.resizeTimeoutLength = options.refreshTimeoutLength ?? this.resizeTimeoutLength;
this.refreshTimeoutLength = options.refreshTimeoutLength ?? this.refreshTimeoutLength;
- this.api.start();
- void this.checkState();
+ void this.api.connect();
+ //void this.checkState();
}
@@ -384,14 +378,14 @@ export class HttpgdViewer implements IHttpgdViewer {
// Called to create a new webview if the user closed the old one:
public show(preserveFocus?: boolean): void {
preserveFocus ??= this.showOptions.preserveFocus;
- if(!this.webviewPanel){
+ if (!this.webviewPanel) {
const showOptions = {
...this.showOptions,
preserveFocus: preserveFocus
};
this.webviewPanel = this.makeNewWebview(showOptions);
this.refreshHtml();
- } else{
+ } else {
this.webviewPanel.reveal(undefined, preserveFocus);
}
this.parent.registerActiveViewer(this);
@@ -399,7 +393,7 @@ export class HttpgdViewer implements IHttpgdViewer {
public openExternal(): void {
let urlString = `http://${this.host}/live`;
- if(this.token){
+ if (this.token) {
urlString += `?token=${this.token}`;
}
const uri = vscode.Uri.parse(urlString);
@@ -407,19 +401,20 @@ export class HttpgdViewer implements IHttpgdViewer {
}
// focus a specific plot id
- public async focusPlot(id?: PlotId): Promise {
- if(id){
- this.activePlot = id;
- }
+ public async focusPlot(id?: HttpgdPlotId): Promise {
+ this.activePlot = id || this.activePlot;
const plt = this.plots[this.activeIndex];
- if(plt.height !== this.viewHeight * this.scale || plt.width !== this.viewHeight * this.scale){
- await this.refreshPlots();
- } else{
+ if (plt.height !== this.viewHeight || plt.width !== this.viewHeight || plt.zoom !== this.zoom) {
+ await this.refreshPlots(this.api.getPlots());
+ } else {
this._focusPlot();
}
}
- protected _focusPlot(plotId?: PlotId): void {
+ protected _focusPlot(plotId?: HttpgdPlotId): void {
plotId ??= this.activePlot;
+ if(!plotId){
+ return;
+ }
const msg: FocusPlotMessage = {
message: 'focusPlot',
plotId: plotId
@@ -430,34 +425,34 @@ export class HttpgdViewer implements IHttpgdViewer {
// navigate through plots (supply `true` to go to end/beginning of list)
public async nextPlot(last?: boolean): Promise {
- this.activeIndex = last ? this.plots.length - 1 : this.activeIndex+1;
+ this.activeIndex = last ? this.plots.length - 1 : this.activeIndex + 1;
await this.focusPlot();
}
public async prevPlot(first?: boolean): Promise {
- this.activeIndex = first ? 0 : this.activeIndex-1;
+ this.activeIndex = first ? 0 : this.activeIndex - 1;
await this.focusPlot();
}
// restore closed plots, reset zoom, redraw html
public resetPlots(): void {
this.hiddenPlots = [];
- this.scale = this.scale0;
- void this.refreshPlots(true, true);
+ this.zoom = this.zoom0;
+ void this.refreshPlots(this.api.getPlots(), true, true);
}
- public hidePlot(id?: PlotId): void {
+ public hidePlot(id?: HttpgdPlotId): void {
id ??= this.activePlot;
- if(!id){ return; }
+ if (!id) { return; }
const tmpIndex = this.activeIndex;
this.hiddenPlots.push(id);
this.plots = this.plots.filter((plt) => !this.hiddenPlots.includes(plt.id));
- if(id === this.activePlot){
+ if (id === this.activePlot) {
this.activeIndex = tmpIndex;
this._focusPlot();
}
this._hidePlot(id);
}
- protected _hidePlot(id: PlotId): void {
+ protected _hidePlot(id: HttpgdPlotId): void {
const msg: HidePlotMessage = {
message: 'hidePlot',
plotId: id
@@ -465,15 +460,15 @@ export class HttpgdViewer implements IHttpgdViewer {
this.postWebviewMessage(msg);
}
- public async closePlot(id?: PlotId): Promise {
+ public async closePlot(id?: HttpgdPlotId): Promise {
id ??= this.activePlot;
- if(id){
+ if (id) {
this.hidePlot(id);
- await this.api?.closePlot(id);
+ await this.api.removePlot({ id: id });
}
}
- public toggleStyle(force?: boolean): void{
+ public toggleStyle(force?: boolean): void {
this.stripStyles = force ?? !this.stripStyles;
const msg: ToggleStyleMessage = {
message: 'toggleStyle',
@@ -491,14 +486,14 @@ export class HttpgdViewer implements IHttpgdViewer {
this.postWebviewMessage(msg);
}
- public togglePreviewPlots(force?: PreviewPlotLayout): void{
- if(force){
+ public togglePreviewPlots(force?: PreviewPlotLayout): void {
+ if (force) {
this.previewPlotLayout = force;
- } else if(this.previewPlotLayout === 'multirow'){
+ } else if (this.previewPlotLayout === 'multirow') {
this.previewPlotLayout = 'scroll';
- } else if(this.previewPlotLayout === 'scroll'){
+ } else if (this.previewPlotLayout === 'scroll') {
this.previewPlotLayout = 'hidden';
- } else if(this.previewPlotLayout === 'hidden'){
+ } else if (this.previewPlotLayout === 'hidden') {
this.previewPlotLayout = 'multirow';
}
const msg: PreviewPlotLayoutMessage = {
@@ -508,87 +503,54 @@ export class HttpgdViewer implements IHttpgdViewer {
this.postWebviewMessage(msg);
}
- public zoomIn(): void {
- if(this.scale > 0){
- this.scale -= 0.1;
+ public zoomOut(): void {
+ if (this.zoom > 0) {
+ this.zoom -= 0.1;
void this.resizePlot();
}
}
- public zoomOut(): void {
- this.scale += 0.1;
+ public zoomIn(): void {
+ this.zoom += 0.1;
void this.resizePlot();
}
- public async setContextValues(mightBeInBackground: boolean = false): Promise {
- if(this.webviewPanel?.active){
+ public async setContextValues(mightBeInBackground: boolean = false): Promise {
+ if (this.webviewPanel?.active) {
this.parent.registerActiveViewer(this);
await setContext('r.plot.active', true);
await setContext('r.plot.canGoBack', this.activeIndex > 0);
await setContext('r.plot.canGoForward', this.activeIndex < this.plots.length - 1);
- } else if (!mightBeInBackground){
+ } else if (!mightBeInBackground) {
await setContext('r.plot.active', false);
}
- }
+ }
public getPanelPath(): string | undefined {
- if(!this.webviewPanel) {
+ if (!this.webviewPanel) {
return undefined;
}
const dummyUri = this.webviewPanel.webview.asWebviewUri(vscode.Uri.file(''));
const m = /^[^.]*/.exec(dummyUri.authority);
- const webviewId = m[0] || '';
+ const webviewId = m?.[0] || '';
return `webview-panel/webview-${webviewId}`;
}
- // internal functions
- //
-
- // use a delay to avoid refreshing while a plot is incrementally drawn
- protected checkStateDelayed(): void {
- clearTimeout(this.refreshTimeout);
- if(this.refreshTimeoutLength <= 0){
- void this.checkState();
- this.refreshTimeout = undefined;
- } else {
- this.refreshTimeout = setTimeout(() => {
- void this.checkState();
- }, this.refreshTimeoutLength);
- }
- }
- protected async checkState(): Promise {
- if(!this.api){
- return;
- }
- const oldUpid = this.state?.upid;
- this.state = await this.api.getState();
- if(this.state.upid !== oldUpid){
- await this.refreshPlots();
- if (isHost()) {
- const urlString = `http://${this.host}/live?token=${this.token}`;
- void shareBrowser(
- urlString,
- '[VSC-R] R Plot',
- true
- );
- void rHostService.notifyGuestPlotManager(urlString);
- }
- }
- }
-
- protected getIndex(id: PlotId): number {
- return this.plots.findIndex((plt: HttpgdPlot) => plt.id === id);
+ protected getIndex(id: HttpgdPlotId): number {
+ return this.plots.findIndex((plt: HttpgdPlot) => plt.id === id);
}
protected handleResize(height: number, width: number, userTriggered: boolean = false): void {
this.viewHeight = height;
this.viewWidth = width;
- if(userTriggered || this.resizeTimeoutLength === 0){
- clearTimeout(this.resizeTimeout);
+ if (userTriggered || this.resizeTimeoutLength === 0) {
+ if(this.resizeTimeout){
+ clearTimeout(this.resizeTimeout);
+ }
this.resizeTimeout = undefined;
void this.resizePlot();
- } else if(!this.resizeTimeout){
+ } else if (!this.resizeTimeout) {
this.resizeTimeout = setTimeout(() => {
void this.resizePlot().then(() =>
this.resizeTimeout = undefined
@@ -597,46 +559,40 @@ export class HttpgdViewer implements IHttpgdViewer {
}
}
- protected async resizePlot(id?: PlotId): Promise {
+ protected async resizePlot(id?: HttpgdPlotId): Promise {
id ??= this.activePlot;
- if(!id){ return; }
- const height = this.scaledViewHeight;
- const width = this.scaledViewWidth;
- const plt = await this.getPlotContent(id, height, width);
- if(plt){
- this.plotWidth = plt.width;
- this.plotHeight = plt.height;
- this.updatePlot(plt);
- }
+ if (!id) { return; }
+ const plt = await this.getPlotContent(id, this.viewWidth, this.viewHeight, this.zoom);
+ this.plotWidth = plt.width;
+ this.plotHeight = plt.height;
+ this.updatePlot(plt);
}
- protected async refreshPlots(redraw: boolean = false, force: boolean = false): Promise {
- if(!this.api){
- return;
- }
+ protected async refreshPlots(plotsIdResponse: HttpgdIdResponse[], redraw: boolean = false, force: boolean = false): Promise {
const nPlots = this.plots.length;
- const oldPlotIds = this.plots.map(plt => plt.id);
- let plotIds = await this.api.getPlotIds();
+ let plotIds = plotsIdResponse.map((x) => x.id);
plotIds = plotIds.filter((id) => !this.hiddenPlots.includes(id));
- const newPlots = plotIds.map(async (id) => {
+ const newPlotPromises = plotIds.map(async (id) => {
const plot = this.plots.find((plt) => plt.id === id);
- if(force || !plot || id === this.activePlot){
- return await this.getPlotContent(id, this.scaledViewHeight, this.scaledViewWidth);
- } else{
+ if (force || !plot || id === this.activePlot) {
+ return await this.getPlotContent(id, this.viewWidth, this.viewHeight, this.zoom);
+ } else {
return plot;
}
});
- this.plots = await Promise.all(newPlots);
- if(this.plots.length !== nPlots){
+ const newPlots = await Promise.all(newPlotPromises);
+ const oldPlotIds = this.plots.map(plt => plt.id);
+ this.plots = newPlots;
+ if (this.plots.length !== nPlots) {
this.activePlot = this.plots[this.plots.length - 1]?.id;
}
- if(redraw || !this.webviewPanel){
+ if (redraw || !this.webviewPanel) {
this.refreshHtml();
- } else{
- for(const plt of this.plots){
- if(oldPlotIds.includes(plt.id)){
+ } else {
+ for (const plt of this.plots) {
+ if (oldPlotIds.includes(plt.id)) {
this.updatePlot(plt);
- } else{
+ } else {
this.addPlot(plt);
}
}
@@ -644,16 +600,16 @@ export class HttpgdViewer implements IHttpgdViewer {
}
}
- protected updatePlot(plt: HttpgdPlot): void {
+ protected updatePlot(plt: HttpgdPlot): void {
const msg: UpdatePlotMessage = {
message: 'updatePlot',
plotId: plt.id,
- svg: plt.svg
+ svg: plt.data
};
this.postWebviewMessage(msg);
}
- protected addPlot(plt: HttpgdPlot): void {
+ protected addPlot(plt: HttpgdPlot): void {
const ejsData = this.makeEjsData();
ejsData.plot = plt;
const html = ejs.render(this.smallPlotTemplate, ejsData);
@@ -662,18 +618,32 @@ export class HttpgdViewer implements IHttpgdViewer {
html: html
};
this.postWebviewMessage(msg);
+ void this.focusPlot(plt.id);
void this.setContextValues();
}
- protected async getPlotContent(id: PlotId, height?: number, width?: number): Promise {
- if(!this.api){
- return undefined;
- }
- height ||= this.scaledViewHeight;
- width ||= this.scaledViewWidth;
- const plt = await this.api.getPlotContent(id, height, width);
- stripSize(plt);
- makeIdsUnique(plt, this.state?.upid || 0);
+ // get content of a single plot
+ protected async getPlotContent(id: HttpgdPlotId, width: number, height: number, zoom: number): Promise> {
+
+ const args = {
+ id: id,
+ height: height,
+ width: width,
+ zoom: zoom,
+ renderer: 'svgp'
+ };
+
+ const plotContent = await this.api.getPlot(args);
+ const svg = await plotContent?.text() || '';
+
+ const plt: HttpgdPlot = {
+ id: id,
+ data: svg,
+ height: height,
+ width: width,
+ zoom: zoom,
+ };
+
this.viewHeight ??= plt.height;
this.viewWidth ??= plt.width;
return plt;
@@ -699,14 +669,14 @@ export class HttpgdViewer implements IHttpgdViewer {
protected makeEjsData(): EjsData {
const asLocalPath = (relPath: string) => {
- if(!this.webviewPanel){
+ if (!this.webviewPanel) {
return relPath;
}
const localUri = vscode.Uri.file(path.join(this.htmlRoot, relPath));
return localUri.fsPath;
};
const asWebViewPath = (localPath: string) => {
- if(!this.webviewPanel){
+ if (!this.webviewPanel) {
return localPath;
}
const localUri = vscode.Uri.file(path.join(this.htmlRoot, localPath));
@@ -718,10 +688,10 @@ export class HttpgdViewer implements IHttpgdViewer {
return `command:${command}?${argString}`;
};
let overwriteCssPath = '';
- if(this.customOverwriteCssPath){
+ if (this.customOverwriteCssPath) {
const uri = vscode.Uri.file(this.customOverwriteCssPath);
- overwriteCssPath = this.webviewPanel.webview.asWebviewUri(uri).toString();
- } else{
+ overwriteCssPath = this.webviewPanel?.webview.asWebviewUri(uri).toString() || '';
+ } else {
overwriteCssPath = asWebViewPath('styleOverwrites.css');
}
const ejsData: EjsData = {
@@ -758,9 +728,9 @@ export class HttpgdViewer implements IHttpgdViewer {
}
protected handleWebviewMessage(msg: OutMessage): void {
- if(msg.message === 'log'){
+ if (msg.message === 'log') {
console.log(msg.body);
- } else if(msg.message === 'resize'){
+ } else if (msg.message === 'resize') {
const height = msg.height;
const width = msg.width;
const userTriggered = msg.userTriggered;
@@ -776,50 +746,91 @@ export class HttpgdViewer implements IHttpgdViewer {
// export plot
// if no format supplied, show a quickpick menu etc.
// if no filename supplied, show selector window
- public async exportPlot(id?: PlotId, format?: ExportFormat, outFile?: string): Promise {
+ public async exportPlot(id?: HttpgdPlotId, rendererId?: HttpgdRendererId, outFile?: string): Promise {
// make sure id is valid or return:
- id ||= this.activePlot || this.plots[this.plots.length-1]?.id;
+ id ||= this.activePlot || this.plots[this.plots.length - 1]?.id;
const plot = this.plots.find((plt) => plt.id === id);
- if(!plot){
+ if (!plot) {
void vscode.window.showWarningMessage('No plot available for export.');
return;
}
// make sure format is valid or return:
- if(!format){
- const formats: ExportFormat[] = ['svg'];
+ if (!rendererId) {
+ const renderers = this.api.getRenderers();
+ const qpItems = renderers.map(renderer => ({
+ label: renderer.name,
+ detail: renderer.descr,
+ id: renderer.id
+ }));
const options: vscode.QuickPickOptions = {
placeHolder: 'Please choose a file format'
};
- format = await vscode.window.showQuickPick(formats, options) as ExportFormat | undefined;
- if(!format){
+ // format = await vscode.window.showQuickPick(formats, options);
+ const qpPick = await vscode.window.showQuickPick(qpItems, options);
+ rendererId = qpPick?.id;
+ if(!rendererId){
return;
}
}
// make sure outFile is valid or return:
- if(!outFile){
+ if (!outFile) {
const options: vscode.SaveDialogOptions = {};
+
+ // Suggest a file extension:
+ const renderer = this.api.getRenderers().find(r => r.id === rendererId);
+ const ext = renderer?.ext.replace(/^\./, '');
+
+ // try to set default URI:
+ if(this.lastExportUri){
+ const noExtPath = this.lastExportUri.fsPath.replace(/\.[^.]*$/, '');
+ const defaultPath = noExtPath + (ext ? `.${ext}` : '');
+ options.defaultUri = vscode.Uri.file(defaultPath);
+ } else {
+ // construct default Uri
+ const defaultFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
+ if(defaultFolder){
+ const defaultName = 'plot' + (ext ? `.${ext}` : '');
+ options.defaultUri = vscode.Uri.file(path.join(defaultFolder, defaultName));
+ }
+ }
+ // set file extension filter
+ if(ext && renderer?.name){
+ options.filters = {
+ [renderer.name]: [ext],
+ ['All']: ['*'],
+ };
+ }
+
const outUri = await vscode.window.showSaveDialog(options);
- outFile = outUri?.fsPath;
- if(!outFile){
+ if(outUri){
+ this.lastExportUri = outUri;
+ outFile = outUri.fsPath;
+ } else {
return;
}
}
- // actually export plot:
- if(format === 'svg'){
- // do export
- fs.writeFileSync(outFile, plot.svg);
- // const uri = vscode.Uri.file(outFile);
- // await vscode.workspace.openTextDocument(uri);
- // void vscode.window.showTextDocument(uri);
- } else{
- void vscode.window.showWarningMessage('Format not implemented');
- }
+ // get plot:
+ const plt = await this.api.getPlot({
+ id: this.activePlot,
+ renderer: rendererId
+ }) as unknown as Response; // I am not sure why eslint thinks this is the
+ // browser Response object and not the node-fetch one.
+ // cross-fetch problem or config problem in vscode-r?
+
+ const dest = fs.createWriteStream(outFile);
+ dest.on('error', (err) => void vscode.window.showErrorMessage(
+ `Export failed: ${err.message}`
+ ));
+ dest.on('close', () => void vscode.window.showInformationMessage(
+ `Export done: ${outFile}`
+ ));
+ void plt.body.pipe(dest);
}
// Dispose-function to clean up when vscode closes
// E.g. to close connections etc., notify R, ...
public dispose(): void {
- this.api?.dispose();
+ this.api.disconnect();
}
}
@@ -831,40 +842,3 @@ function findItemOfType(arr: any[], type: string): T {
const item = arr.find((elm) => typeof elm === type) as T;
return item;
}
-
-
-function stripSize(plt: HttpgdPlot): void {
- const re = /<(svg.*)width="([^"]*)" height="([^"]*)"(.*)>/;
- const m = re.exec(plt.svg);
- if(!plt.width || isNaN(plt.width)){
- plt.width = Number(m[2]);
- }
- if(!plt.height || isNaN(plt.height)){
- plt.height = Number(m[3]);
- }
- plt.svg = plt.svg.replace(re, '<$1 preserveAspectRatio="none" $4>');
-}
-
-function makeIdsUnique(plt: HttpgdPlot, upid: number): void {
- const re = //g;
- const ids: string[] = [];
- let svg = plt.svg;
- let m: RegExpExecArray;
- do {
- m = re.exec(svg);
- if(m){
- ids.push(m[1]);
- }
- } while(m);
- for(const id of ids){
- const newId = `$${upid}_${plt.id}_${plt.height}_${plt.width}_${id}`;
- const re1 = new RegExp(``);
- const replacement1 = ``;
- const re2 = new RegExp(`clip-path='url\\(#${id}\\)'`, 'g');
- const replacement2 = `clip-path='url(#${newId})'`;
- svg = svg.replace(re1, replacement1);
- svg = svg.replace(re2, replacement2);
- }
- plt.svg = svg;
- return;
-}
diff --git a/yarn.lock b/yarn.lock
index fc55ecaa9..8e9baca73 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -270,6 +270,13 @@
dependencies:
"@types/node" "*"
+"@types/ws@^8.2.0":
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
+ integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
+ dependencies:
+ "@types/node" "*"
+
"@typescript-eslint/eslint-plugin@4.25.0":
version "4.25.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.25.0.tgz#d82657b6ab4caa4c3f888ff923175fadc2f31f2a"
@@ -902,6 +909,13 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+cross-fetch@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
+ integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
+ dependencies:
+ node-fetch "2.6.1"
+
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1530,6 +1544,16 @@ http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
+httpgd@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/httpgd/-/httpgd-0.1.6.tgz#bce29a769aa2662128b8be553eb3230cf5fe6da3"
+ integrity sha512-HyozzYjOq+rGi3P+YZtLnvBPAWvdn2tiCfUuB4tSUradRtOoKAvwcZ+yvOYxusMzaZIGkf02s/BTkcDzj+XS/w==
+ dependencies:
+ "@types/ws" "^8.2.0"
+ cross-fetch "^3.1.4"
+ isomorphic-ws "^4.0.1"
+ ws "^8.2.3"
+
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
@@ -1675,6 +1699,11 @@ isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+isomorphic-ws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
+ integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
+
jake@^10.6.1:
version "10.8.2"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
@@ -1979,7 +2008,7 @@ nerdbank-streams@2.5.60:
caught "^0.1.3"
msgpack-lite "^0.1.26"
-node-fetch@^2.6.1:
+node-fetch@2.6.1, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -2848,6 +2877,11 @@ ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+ws@^8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"