You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vscode-sshfs/src/webview.ts

157 lines
6.4 KiB

import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { deleteConfig, loadConfigsRaw, updateConfig } from './config';
import { getLocations } from './fileSystemConfig';
import { DEBUG, Logging as _Logging, LOGGING_NO_STACKTRACE } from './logging';
import { toPromise } from './utils';
import type { Message, Navigation } from './webviewMessages';
const Logging = _Logging.scope('WebView');
let webviewPanel: vscode.WebviewPanel | undefined;
let pendingNavigation: Navigation | undefined;
function getExtensionPath(): string | undefined {
const ext = vscode.extensions.getExtension('Kelvin.vscode-sshfs');
return ext && ext.extensionPath;
}
async function getDebugContent(): Promise<string | false> {
if (!DEBUG) return false;
const URL = `http://localhost:3000/`;
const http = await import('http');
return toPromise<string>(cb => http.get(URL, async (message) => {
if (message.statusCode !== 200) return cb(new Error(`Error code ${message.statusCode} (${message.statusMessage}) connecting to React dev server}`));
let body = '';
message.on('data', chunk => body += chunk);
await toPromise(cb => message.on('end', cb));
body = body.toString().replace(/\/static\/js\/bundle\.js/, `${URL}/static/js/bundle.js`);
// Make sure the CSP meta tag also includes the React dev server (including connect-src for the socket, which uses both http:// and ws://)
body = body.replace(/\$WEBVIEW_CSPSOURCE/g, `$WEBVIEW_CSPSOURCE ${URL}`);
body = body.replace(/\$WEBVIEW_CSPEXTRA/g, `connect-src ${URL} ${URL.replace('http://', 'ws://')};`);
body = body.replace(/src="\/static\//g, `src="${URL}/static/`);
cb(null, body);
}).on('error', err => {
Logging.warning(`Error connecting to React dev server: ${err}`);
cb(new Error('Could not connect to React dev server. Not running?'));
}));
}
export async function open() {
if (!webviewPanel) {
const extensionPath = getExtensionPath();
webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true });
webviewPanel.onDidDispose(() => webviewPanel = undefined);
if (extensionPath) webviewPanel.iconPath = vscode.Uri.file(path.join(extensionPath, 'resources/icon.svg'));
const { webview } = webviewPanel;
webview.onDidReceiveMessage(handleMessage);
let content = await getDebugContent().catch((e: Error) => (vscode.window.showErrorMessage(e.message), null));
if (!content) {
if (!extensionPath) throw new Error('Could not get extensionPath');
// If we got here, we're either not in debug mode, or something went wrong (and an error message is shown)
content = fs.readFileSync(path.resolve(extensionPath, 'webview/build/index.html')).toString();
// Built index.html has e.g. `href="/static/js/stuff.js"`, need to make it use vscode-resource: and point to the built static directory
// Scrap that last part, vscode-resource: is deprecated and we need to use Webview.asWebviewUri
//content = content.replace(/\/static\//g, vscode.Uri.file(path.join(extensionPath, 'webview/build/static/')).with({ scheme: 'vscode-resource' }).toString());
content = content.replace(/\/static\//g, webview.asWebviewUri(vscode.Uri.file(path.join(extensionPath, 'webview/build/static/'))).toString());
}
// Make sure the CSP meta tag has the right cspSource
content = content.replace(/\$WEBVIEW_CSPSOURCE/g, webview.cspSource);
// The EXTRA tag is used in debug mode to define connect-src. By default we can (and should) just delete it
content = content.replace(/\$WEBVIEW_CSPEXTRA/g, '');
webview.html = content;
}
webviewPanel.reveal();
}
export async function navigate(navigation: Navigation) {
Logging.debug(`Navigation requested: ${JSON.stringify(navigation, null, 4)}`);
pendingNavigation = navigation;
postMessage({ navigation, type: 'navigate' });
return open();
}
function postMessage<T extends Message>(message: T) {
webviewPanel?.webview.postMessage(message);
}
async function handleMessage(message: Message): Promise<any> {
if (!webviewPanel) return Logging.warning(`Got message without webviewPanel: ${JSON.stringify(message, null, 4)}`);
Logging.debug(`Got message: ${JSON.stringify(message, null, 4)}`);
if (pendingNavigation) {
postMessage({
type: 'navigate',
navigation: pendingNavigation,
});
pendingNavigation = undefined;
}
switch (message.type) {
case 'requestData': {
const configs = await loadConfigsRaw();
const locations = getLocations(configs);
return postMessage({
configs, locations,
type: 'responseData',
});
}
case 'saveConfig': {
const { uniqueId, config, name, remove } = message;
let error: string | undefined;
try {
if (remove) {
await deleteConfig(config);
} else {
await updateConfig(config, name);
}
} catch (e) {
Logging.error('Error handling saveConfig message for settings UI:', LOGGING_NO_STACKTRACE);
Logging.error(JSON.stringify(message), LOGGING_NO_STACKTRACE);
Logging.error(e);
error = e.message;
}
return postMessage({
uniqueId, config, error,
type: 'saveConfigResult',
});
}
case 'promptPath': {
const { uniqueId } = message;
let uri: vscode.Uri | undefined;
let error: string | undefined;
try {
const uris = await vscode.window.showOpenDialog({});
if (uris) [uri] = uris;
} catch (e) {
Logging.error('Error handling promptPath message for settings UI:', LOGGING_NO_STACKTRACE);
Logging.error(JSON.stringify(message), LOGGING_NO_STACKTRACE);
Logging.error(e);
error = e.message;
}
return postMessage({
uniqueId,
path: uri && uri.fsPath,
type: 'promptPathResult',
});
}
case 'navigated': {
const { view } = message;
type View = 'startscreen' | 'newconfig' | 'configeditor' | 'configlocator';
let title: string | undefined;
switch (view as View) {
case 'configeditor':
title = 'SSH FS - Edit config';
break;
case 'configlocator':
title = 'SSH FS - Locate config';
break;
case 'newconfig':
title = 'SSH FS - New config';
break;
}
webviewPanel.title = title || 'SSH FS';
}
}
}