|
|
|
@ -1,132 +1,251 @@
|
|
|
|
|
|
|
|
|
|
// 导入 common/fileSystemConfig 模块中的 getLocations 函数
|
|
|
|
|
import { getLocations } from 'common/fileSystemConfig';
|
|
|
|
|
// 导入 common/webviewMessages 模块中的 Message 和 Navigation 类型
|
|
|
|
|
import type { Message, Navigation } from 'common/webviewMessages';
|
|
|
|
|
// 导入 fs 模块,用于文件系统操作
|
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
// 导入 path 模块,用于路径操作
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
// 导入 vscode 模块,用于 VS Code 扩展 API
|
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
// 导入 config 模块中的 deleteConfig、getConfigs、loadConfigs、updateConfig 函数
|
|
|
|
|
import { deleteConfig, getConfigs, loadConfigs, updateConfig } from './config';
|
|
|
|
|
// 导入 logging 模块中的 DEBUG、LOGGING_NO_STACKTRACE、Logging 类型
|
|
|
|
|
import { DEBUG, LOGGING_NO_STACKTRACE, Logging as _Logging } from './logging';
|
|
|
|
|
// 导入 utils 模块中的 toPromise 函数
|
|
|
|
|
import { toPromise } from './utils';
|
|
|
|
|
|
|
|
|
|
// 使用 _Logging 模块创建一个名为 'WebView' 的日志记录器
|
|
|
|
|
const Logging = _Logging.scope('WebView');
|
|
|
|
|
|
|
|
|
|
// 存储当前激活的 WebView 面板实例,用于在扩展中与 WebView 进行交互
|
|
|
|
|
let webviewPanel: vscode.WebviewPanel | undefined;
|
|
|
|
|
// 存储待处理的导航信息,当用户在 WebView 中点击链接或按钮时,可能会触发导航事件
|
|
|
|
|
let pendingNavigation: Navigation | undefined;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取 VS Code 扩展的路径
|
|
|
|
|
* @returns 扩展的路径,如果扩展未找到则返回 undefined
|
|
|
|
|
*/
|
|
|
|
|
function getExtensionPath(): string | undefined {
|
|
|
|
|
// 使用 vscode.extensions.getExtension 方法获取名为 'Kelvin.vscode-sshfs' 的扩展
|
|
|
|
|
const ext = vscode.extensions.getExtension('Kelvin.vscode-sshfs');
|
|
|
|
|
// 如果找到了扩展,则返回扩展的路径,否则返回 undefined
|
|
|
|
|
return ext && ext.extensionPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取调试内容
|
|
|
|
|
* @returns 调试内容字符串,如果未启用调试则返回 false
|
|
|
|
|
* @description
|
|
|
|
|
* 这个函数用于获取调试内容。它首先检查是否启用了调试模式(DEBUG 标志),如果未启用,则直接返回 false。
|
|
|
|
|
* 如果启用了调试模式,它会构建一个 URL,指向本地开发服务器的根路径(默认为 http://localhost:3000/)。
|
|
|
|
|
* 然后,它使用 Node.js 的 http 模块向该 URL 发送 GET 请求,并等待响应。
|
|
|
|
|
* 当接收到响应时,它会检查响应的状态码是否为 200。如果不是,它会返回一个错误。
|
|
|
|
|
* 如果状态码为 200,它会读取响应的 body,并将其转换为字符串。
|
|
|
|
|
* 接着,它会对字符串进行一些替换操作,以确保其中的资源路径和 CSP 元数据正确地指向开发服务器。
|
|
|
|
|
* 最后,它将处理后的字符串作为结果返回。
|
|
|
|
|
*/
|
|
|
|
|
async function getDebugContent(): Promise<string | false> {
|
|
|
|
|
// 如果未启用 DEBUG 模式,则直接返回 false
|
|
|
|
|
if (!DEBUG) return false;
|
|
|
|
|
// 构建 React 开发服务器的 URL
|
|
|
|
|
const URL = `http://localhost:3000/`;
|
|
|
|
|
// 异步导入 http 模块
|
|
|
|
|
const http = await import('http');
|
|
|
|
|
// 发送 GET 请求到 React 开发服务器,并处理响应
|
|
|
|
|
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}`));
|
|
|
|
|
// 如果响应状态码不是 200,则返回错误
|
|
|
|
|
if (message.statusCode!== 200) return cb(new Error(`Error code ${message.statusCode} (${message.statusMessage}) connecting to React dev server`));
|
|
|
|
|
// 初始化响应 body 字符串
|
|
|
|
|
let body = '';
|
|
|
|
|
// 当接收到数据块时,将其添加到 body 字符串中
|
|
|
|
|
message.on('data', chunk => body += chunk);
|
|
|
|
|
// 等待数据接收完毕
|
|
|
|
|
await toPromise(cb => message.on('end', cb));
|
|
|
|
|
// 替换 body 字符串中的资源路径
|
|
|
|
|
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://)
|
|
|
|
|
// 确保 CSP 元数据包含 React 开发服务器的 URL
|
|
|
|
|
body = body.replace(/\$WEBVIEW_CSPSOURCE/g, `$WEBVIEW_CSPSOURCE ${URL}`);
|
|
|
|
|
// 添加额外的 CSP 规则,允许连接到 React 开发服务器的 socket
|
|
|
|
|
body = body.replace(/\$WEBVIEW_CSPEXTRA/g, `connect-src ${URL} ${URL.replace('http://', 'ws://')};`);
|
|
|
|
|
// 替换 body 字符串中的资源路径前缀
|
|
|
|
|
body = body.replace(/src="\/static\//g, `src="${URL}/static/`);
|
|
|
|
|
// 回调函数,返回处理后的 body 字符串
|
|
|
|
|
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?'));
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 打开一个 WebView 面板,显示 SSH-FS 的设置界面
|
|
|
|
|
* @description
|
|
|
|
|
* 这个函数用于创建并显示一个 WebView 面板,该面板用于展示 SSH-FS 的设置界面。
|
|
|
|
|
* 它首先检查是否已经存在一个 WebView 面板实例,如果不存在,则创建一个新的实例。
|
|
|
|
|
* 然后,它设置了 WebView 面板的一些基本属性,如标题、位置和图标。
|
|
|
|
|
* 接着,它尝试获取调试内容,如果获取成功,则将其设置为 WebView 面板的内容;如果获取失败,则读取本地文件系统中的静态 HTML 文件,并将其设置为 WebView 面板的内容。
|
|
|
|
|
* 在设置内容之前,它会替换 HTML 中的一些特定字符串,以确保资源路径和 CSP 元数据正确。
|
|
|
|
|
* 最后,它显示 WebView 面板,并在必要时处理错误。
|
|
|
|
|
*/
|
|
|
|
|
export async function open() {
|
|
|
|
|
// 检查是否已经存在 WebView 面板实例
|
|
|
|
|
if (!webviewPanel) {
|
|
|
|
|
// 获取扩展的路径
|
|
|
|
|
const extensionPath = getExtensionPath();
|
|
|
|
|
// 创建一个新的 WebView 面板实例
|
|
|
|
|
webviewPanel = vscode.window.createWebviewPanel('sshfs-settings', 'SSH-FS', vscode.ViewColumn.One, { enableFindWidget: true, enableScripts: true });
|
|
|
|
|
// 当 WebView 面板被关闭时,将 webviewPanel 变量设置为 undefined
|
|
|
|
|
webviewPanel.onDidDispose(() => webviewPanel = undefined);
|
|
|
|
|
// 如果扩展路径存在,则设置 WebView 面板的图标
|
|
|
|
|
if (extensionPath) webviewPanel.iconPath = vscode.Uri.file(path.join(extensionPath, 'resources/icon.svg'));
|
|
|
|
|
// 获取 WebView 实例
|
|
|
|
|
const { webview } = webviewPanel;
|
|
|
|
|
// 监听 WebView 接收到的消息
|
|
|
|
|
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)
|
|
|
|
|
// 读取本地文件系统中的静态 HTML 文件
|
|
|
|
|
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());
|
|
|
|
|
// 替换 HTML 中的资源路径,使其指向 WebView 的资源
|
|
|
|
|
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
|
|
|
|
|
// 替换 HTML 中的 CSP 元数据
|
|
|
|
|
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
|
|
|
|
|
// 移除 HTML 中的额外 CSP 规则
|
|
|
|
|
content = content.replace(/\$WEBVIEW_CSPEXTRA/g, '');
|
|
|
|
|
// 设置 WebView 面板的内容
|
|
|
|
|
webview.html = content;
|
|
|
|
|
}
|
|
|
|
|
// 显示 WebView 面板
|
|
|
|
|
webviewPanel.reveal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理导航请求
|
|
|
|
|
* @param navigation - 导航请求对象,包含导航的目标和其他相关信息
|
|
|
|
|
* @returns 一个 Promise,当导航请求处理完成时解决
|
|
|
|
|
* @description
|
|
|
|
|
* 这个函数用于处理来自 WebView 的导航请求。当用户在 WebView 中点击链接或触发导航事件时,会调用此函数。
|
|
|
|
|
* 它首先记录一条调试信息,表明接收到了导航请求,并将请求的详细信息记录下来。
|
|
|
|
|
* 然后,它将导航请求存储在 `pendingNavigation` 变量中,以便后续处理。
|
|
|
|
|
* 接着,它向 WebView 发送一个消息,通知 WebView 有导航请求正在等待处理,并提供导航请求的详细信息。
|
|
|
|
|
* 最后,它调用 `open` 函数来打开 WebView 面板,显示 SSH-FS 的设置界面。
|
|
|
|
|
* 请注意,这个函数返回一个 Promise,当导航请求处理完成时解决。这意味着调用者可以等待导航请求处理完成后再执行其他操作。
|
|
|
|
|
*/
|
|
|
|
|
export async function navigate(navigation: Navigation) {
|
|
|
|
|
// 记录接收到导航请求的调试信息
|
|
|
|
|
Logging.debug`Navigation requested: ${navigation}`;
|
|
|
|
|
// 将导航请求存储在 pendingNavigation 变量中
|
|
|
|
|
pendingNavigation = navigation;
|
|
|
|
|
// 向 WebView 发送导航请求消息
|
|
|
|
|
postMessage({ navigation, type: 'navigate' });
|
|
|
|
|
// 打开 WebView 面板
|
|
|
|
|
return open();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 向 WebView 面板发送消息
|
|
|
|
|
* @param message - 要发送的消息对象,必须继承自 Message 类型
|
|
|
|
|
* @description
|
|
|
|
|
* 这个函数用于向 WebView 面板发送消息。它首先检查 `webviewPanel` 是否存在,如果存在,则使用 `webviewPanel.webview.postMessage` 方法发送消息。
|
|
|
|
|
* 请注意,`message` 参数必须是 `Message` 类型或其子类型。
|
|
|
|
|
*/
|
|
|
|
|
function postMessage<T extends Message>(message: T) {
|
|
|
|
|
// 检查 webviewPanel 是否存在,如果存在则发送消息
|
|
|
|
|
webviewPanel?.webview.postMessage(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理来自 WebView 面板的消息
|
|
|
|
|
* @param message - 从 WebView 面板接收到的消息对象
|
|
|
|
|
* @returns 一个 Promise,当消息处理完成时解决
|
|
|
|
|
* @description
|
|
|
|
|
* 这个函数用于处理来自 WebView 面板的消息。它首先检查 `webviewPanel` 是否存在,如果不存在,则记录一条警告信息。
|
|
|
|
|
* 然后,它记录接收到的消息的调试信息。如果存在待处理的导航请求,它会发送一个导航消息,并将 `pendingNavigation` 变量重置为 `undefined`。
|
|
|
|
|
* 接着,它根据消息的类型进行相应的处理。目前支持的消息类型包括 `requestData`、`saveConfig`、`promptPath` 和 `navigated`。
|
|
|
|
|
* - `requestData`:请求配置数据。函数会加载配置数据并返回给 WebView 面板。
|
|
|
|
|
* - `saveConfig`:保存配置数据。函数会根据消息中的指示更新或删除配置数据,并返回操作结果。
|
|
|
|
|
* - `promptPath`:提示用户选择路径。函数会显示一个文件选择对话框,并将用户选择的路径返回给 WebView 面板。
|
|
|
|
|
* - `navigated`:导航事件。函数会根据导航的目标更新 WebView 面板的标题。
|
|
|
|
|
* 请注意,这个函数返回一个 Promise,当消息处理完成时解决。这意味着调用者可以等待消息处理完成后再执行其他操作。
|
|
|
|
|
*/
|
|
|
|
|
async function handleMessage(message: Message): Promise<any> {
|
|
|
|
|
// 如果 webviewPanel 不存在,则记录警告信息
|
|
|
|
|
if (!webviewPanel) return Logging.warning`Got message without webviewPanel: ${message}`;
|
|
|
|
|
// 记录接收到的消息的调试信息
|
|
|
|
|
Logging.debug`Got message: ${message}`;
|
|
|
|
|
// 如果存在待处理的导航请求
|
|
|
|
|
if (pendingNavigation) {
|
|
|
|
|
// 发送导航消息
|
|
|
|
|
postMessage({
|
|
|
|
|
type: 'navigate',
|
|
|
|
|
navigation: pendingNavigation,
|
|
|
|
|
});
|
|
|
|
|
// 重置 pendingNavigation 变量
|
|
|
|
|
pendingNavigation = undefined;
|
|
|
|
|
}
|
|
|
|
|
// 根据消息的类型进行相应的处理
|
|
|
|
|
switch (message.type) {
|
|
|
|
|
case 'requestData': {
|
|
|
|
|
// 请求配置数据
|
|
|
|
|
const configs = await (message.reload ? loadConfigs : getConfigs)();
|
|
|
|
|
// 获取配置数据的位置信息
|
|
|
|
|
const locations = getLocations(configs);
|
|
|
|
|
// 返回配置数据和位置信息
|
|
|
|
|
return postMessage({
|
|
|
|
|
configs, locations,
|
|
|
|
|
type: 'responseData',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
case 'saveConfig': {
|
|
|
|
|
// 保存配置数据
|
|
|
|
|
const { uniqueId, config, name, remove } = message;
|
|
|
|
|
let error: string | undefined;
|
|
|
|
|
try {
|
|
|
|
|
// 根据 remove 标志决定是删除还是更新配置数据
|
|
|
|
|
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:\n${message}\n${e}`;
|
|
|
|
|
error = e.message;
|
|
|
|
|
}
|
|
|
|
|
// 返回用户选择的路径
|
|
|
|
|
return postMessage({
|
|
|
|
|
uniqueId,
|
|
|
|
|
path: uri && uri.fsPath,
|
|
|
|
@ -134,9 +253,11 @@ async function handleMessage(message: Message): Promise<any> {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
case 'navigated': {
|
|
|
|
|
// 导航事件
|
|
|
|
|
const { view } = message;
|
|
|
|
|
type View = 'startscreen' | 'newconfig' | 'configeditor' | 'configlocator';
|
|
|
|
|
let title: string | undefined;
|
|
|
|
|
// 根据导航目标更新 WebView 面板的标题
|
|
|
|
|
switch (view as View) {
|
|
|
|
|
case 'configeditor':
|
|
|
|
|
title = 'SSH FS - Edit config';
|
|
|
|
@ -148,6 +269,7 @@ async function handleMessage(message: Message): Promise<any> {
|
|
|
|
|
title = 'SSH FS - New config';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// 设置 WebView 面板的标题
|
|
|
|
|
webviewPanel.title = title || 'SSH FS';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|