Compare commits
1 Commits
feat/codeT
...
c9e6b383c6
| Author | SHA1 | Date | |
|---|---|---|---|
| c9e6b383c6 |
804
docs/VSCode-Extension-API-Guide.md
Normal file
804
docs/VSCode-Extension-API-Guide.md
Normal file
@ -0,0 +1,804 @@
|
||||
# VS Code Extension API 核心知识点
|
||||
|
||||
## 目录
|
||||
- [1. Extension 生命周期](#1-extension-生命周期) ⭐⭐⭐
|
||||
- [2. 激活事件 (Activation Events)](#2-激活事件-activation-events) ⭐⭐
|
||||
- [3. 命令系统 (Commands)](#3-命令系统-commands) ⭐⭐
|
||||
- [4. Webview API](#4-webview-api) ⭐⭐⭐⭐⭐ **面试重点**
|
||||
- [5. TreeView 和自定义视图](#5-treeview-和自定义视图) ⭐⭐
|
||||
- [6. 文件系统操作](#6-文件系统操作) ⭐⭐⭐
|
||||
- [7. 配置和存储](#7-配置和存储) ⭐⭐⭐⭐ **面试重点**
|
||||
- [8. 消息通知](#8-消息通知) ⭐
|
||||
- [9. 语言特性支持](#9-语言特性支持) ⭐
|
||||
- [10. 调试和诊断](#10-调试和诊断) ⭐
|
||||
|
||||
---
|
||||
|
||||
## 1. Extension 生命周期 ⭐⭐⭐
|
||||
|
||||
### 1.1 核心函数 🔥必考
|
||||
|
||||
```typescript
|
||||
// extension.ts
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// 插件激活时调用(只调用一次)
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Extension is now active!');
|
||||
|
||||
// 注册命令、视图、事件监听等
|
||||
// 使用 context.subscriptions 管理资源
|
||||
}
|
||||
|
||||
// 插件停用时调用(清理资源)
|
||||
export function deactivate() {
|
||||
console.log('Extension is deactivated');
|
||||
// 清理资源、关闭连接等
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 ExtensionContext 重要属性 🔥必考
|
||||
|
||||
```typescript
|
||||
interface ExtensionContext {
|
||||
// 插件订阅管理(自动清理)
|
||||
subscriptions: { dispose(): any }[];
|
||||
|
||||
// 工作区存储路径
|
||||
storageUri: vscode.Uri | undefined;
|
||||
globalStorageUri: vscode.Uri;
|
||||
|
||||
// 插件路径
|
||||
extensionUri: vscode.Uri;
|
||||
extensionPath: string;
|
||||
|
||||
// 状态存储
|
||||
workspaceState: Memento; // 工作区级别
|
||||
globalState: Memento; // 全局级别
|
||||
secrets: SecretStorage; // 敏感信息存储
|
||||
|
||||
// 环境变量
|
||||
environmentVariableCollection: EnvironmentVariableCollection;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 资源管理最佳实践 🔥必考
|
||||
|
||||
```typescript
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
// ✅ 推荐:使用 context.subscriptions 自动管理
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('extension.command', () => {})
|
||||
);
|
||||
|
||||
// ❌ 不推荐:手动管理容易忘记清理
|
||||
const disposable = vscode.commands.registerCommand('extension.command', () => {});
|
||||
// 需要在 deactivate 中手动调用 disposable.dispose()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 激活事件 (Activation Events) ⭐⭐
|
||||
|
||||
### 2.1 常用激活事件 📌重要
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"activationEvents": [
|
||||
// 启动时激活
|
||||
"onStartupFinished",
|
||||
|
||||
// 执行命令时激活
|
||||
"onCommand:extension.helloWorld",
|
||||
|
||||
// 打开特定语言文件时激活
|
||||
"onLanguage:javascript",
|
||||
"onLanguage:verilog",
|
||||
|
||||
// 打开特定文件类型时激活
|
||||
"onFileSystem:sftp",
|
||||
|
||||
// 打开特定视图时激活
|
||||
"onView:myCustomView",
|
||||
|
||||
// 调试时激活
|
||||
"onDebug",
|
||||
|
||||
// 打开特定 URI 时激活
|
||||
"onUri",
|
||||
|
||||
// Webview 恢复时激活
|
||||
"onWebviewPanel:myWebview",
|
||||
|
||||
// 任务执行时激活
|
||||
"onTaskType:npm"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 延迟激活策略 🔥必考
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用 onStartupFinished 延迟激活
|
||||
"activationEvents": ["onStartupFinished"]
|
||||
|
||||
// ❌ 不推荐:使用 * 会拖慢启动速度
|
||||
"activationEvents": ["*"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 命令系统 (Commands)
|
||||
|
||||
### 3.1 注册命令
|
||||
|
||||
```typescript
|
||||
// 注册简单命令
|
||||
const disposable = vscode.commands.registerCommand(
|
||||
'extension.helloWorld',
|
||||
() => {
|
||||
vscode.window.showInformationMessage('Hello World!');
|
||||
}
|
||||
);
|
||||
context.subscriptions.push(disposable);
|
||||
|
||||
// 注册带参数的命令
|
||||
vscode.commands.registerCommand(
|
||||
'extension.openFile',
|
||||
(filePath: string) => {
|
||||
vscode.workspace.openTextDocument(filePath).then(doc => {
|
||||
vscode.window.showTextDocument(doc);
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 执行命令
|
||||
|
||||
```typescript
|
||||
// 执行内置命令
|
||||
await vscode.commands.executeCommand('workbench.action.files.save');
|
||||
|
||||
// 执行自定义命令
|
||||
await vscode.commands.executeCommand('extension.openFile', '/path/to/file');
|
||||
|
||||
// 获取所有可用命令
|
||||
const commands = await vscode.commands.getCommands();
|
||||
```
|
||||
|
||||
### 3.3 常用内置命令
|
||||
|
||||
```typescript
|
||||
// 文件操作
|
||||
'workbench.action.files.save'
|
||||
'workbench.action.files.saveAll'
|
||||
'workbench.action.closeActiveEditor'
|
||||
|
||||
// 编辑器操作
|
||||
'editor.action.formatDocument'
|
||||
'editor.action.commentLine'
|
||||
'editor.action.selectAll'
|
||||
|
||||
// 窗口操作
|
||||
'workbench.action.toggleSidebarVisibility'
|
||||
'workbench.action.terminal.new'
|
||||
'workbench.action.quickOpen'
|
||||
|
||||
// Git 操作
|
||||
'git.commit'
|
||||
'git.push'
|
||||
'git.pull'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Webview API ⭐⭐⭐⭐⭐ **面试重点**
|
||||
|
||||
### 4.1 创建 Webview Panel 🔥必考
|
||||
|
||||
```typescript
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'myWebview', // viewType(唯一标识)
|
||||
'My Webview', // 标题
|
||||
vscode.ViewColumn.One, // 显示位置
|
||||
{
|
||||
enableScripts: true, // 启用 JavaScript
|
||||
retainContextWhenHidden: true, // 隐藏时保留状态
|
||||
localResourceRoots: [ // 允许访问的本地资源路径
|
||||
vscode.Uri.joinPath(context.extensionUri, 'media')
|
||||
]
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 4.2 设置 Webview 内容
|
||||
|
||||
```typescript
|
||||
panel.webview.html = getWebviewContent();
|
||||
|
||||
function getWebviewContent() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My Webview</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Webview!</h1>
|
||||
<button onclick="sendMessage()">Send Message</button>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
function sendMessage() {
|
||||
vscode.postMessage({
|
||||
command: 'alert',
|
||||
text: 'Hello from Webview!'
|
||||
});
|
||||
}
|
||||
|
||||
// 接收来自 Extension 的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
console.log('Received:', message);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Webview 消息通信 🔥必考(项目核心)
|
||||
|
||||
```typescript
|
||||
// Extension → Webview
|
||||
panel.webview.postMessage({
|
||||
command: 'update',
|
||||
data: 'some data'
|
||||
});
|
||||
|
||||
// Webview → Extension
|
||||
panel.webview.onDidReceiveMessage(
|
||||
message => {
|
||||
switch (message.command) {
|
||||
case 'alert':
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case 'getData':
|
||||
// 处理数据请求
|
||||
panel.webview.postMessage({
|
||||
command: 'dataResponse',
|
||||
data: fetchData()
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
```
|
||||
|
||||
### 4.4 Webview 生命周期管理 📌重要
|
||||
|
||||
```typescript
|
||||
// 监听 Webview 关闭事件
|
||||
panel.onDidDispose(
|
||||
() => {
|
||||
// 清理资源
|
||||
console.log('Webview disposed');
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
);
|
||||
|
||||
// 监听 Webview 可见性变化
|
||||
panel.onDidChangeViewState(
|
||||
e => {
|
||||
if (e.webviewPanel.visible) {
|
||||
console.log('Webview is now visible');
|
||||
}
|
||||
},
|
||||
null,
|
||||
context.subscriptions
|
||||
);
|
||||
```
|
||||
|
||||
### 4.5 加载本地资源 📌重要
|
||||
|
||||
```typescript
|
||||
// 获取本地资源 URI
|
||||
const scriptUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'media', 'script.js')
|
||||
);
|
||||
|
||||
const styleUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, 'media', 'style.css')
|
||||
);
|
||||
|
||||
// 在 HTML 中使用
|
||||
const html = `
|
||||
<link href="${styleUri}" rel="stylesheet">
|
||||
<script src="${scriptUri}"></script>
|
||||
`;
|
||||
```
|
||||
|
||||
### 4.6 Webview 状态持久化 📌重要
|
||||
|
||||
```typescript
|
||||
// Webview 中保存状态
|
||||
const vscode = acquireVsCodeApi();
|
||||
const state = vscode.getState() || { count: 0 };
|
||||
|
||||
// 更新状态
|
||||
state.count++;
|
||||
vscode.setState(state);
|
||||
|
||||
// Extension 中序列化状态
|
||||
panel.webview.options = {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true
|
||||
};
|
||||
|
||||
// 恢复 Webview
|
||||
vscode.window.registerWebviewPanelSerializer('myWebview', {
|
||||
async deserializeWebviewPanel(webviewPanel, state) {
|
||||
webviewPanel.webview.html = getWebviewContent();
|
||||
// 恢复状态
|
||||
webviewPanel.webview.postMessage({ command: 'restore', state });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. TreeView 和自定义视图
|
||||
|
||||
### 5.1 创建 TreeView Provider
|
||||
|
||||
```typescript
|
||||
class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
getTreeItem(element: TreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getChildren(element?: TreeItem): Thenable<TreeItem[]> {
|
||||
if (!element) {
|
||||
// 返回根节点
|
||||
return Promise.resolve([
|
||||
new TreeItem('Item 1', vscode.TreeItemCollapsibleState.None),
|
||||
new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed)
|
||||
]);
|
||||
}
|
||||
// 返回子节点
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly collapsibleState: vscode.TreeItemCollapsibleState
|
||||
) {
|
||||
super(label, collapsibleState);
|
||||
this.tooltip = `Tooltip for ${label}`;
|
||||
this.command = {
|
||||
command: 'extension.itemClicked',
|
||||
title: 'Click Item',
|
||||
arguments: [this]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 注册 TreeView
|
||||
|
||||
```typescript
|
||||
const treeDataProvider = new MyTreeDataProvider();
|
||||
const treeView = vscode.window.createTreeView('myTreeView', {
|
||||
treeDataProvider,
|
||||
showCollapseAll: true
|
||||
});
|
||||
|
||||
context.subscriptions.push(treeView);
|
||||
|
||||
// 刷新视图
|
||||
treeDataProvider.refresh();
|
||||
```
|
||||
|
||||
### 5.3 WebviewView Provider(侧边栏 Webview)
|
||||
|
||||
```typescript
|
||||
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
context: vscode.WebviewViewResolveContext,
|
||||
token: vscode.CancellationToken
|
||||
) {
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true
|
||||
};
|
||||
|
||||
webviewView.webview.html = getWebviewContent();
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(message => {
|
||||
// 处理消息
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 注册
|
||||
vscode.window.registerWebviewViewProvider(
|
||||
'myWebviewView',
|
||||
new MyWebviewViewProvider()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 文件系统操作 ⭐⭐⭐
|
||||
|
||||
### 6.1 读取文件 📌重要
|
||||
|
||||
```typescript
|
||||
// 读取文本文件
|
||||
const uri = vscode.Uri.file('/path/to/file.txt');
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
const text = Buffer.from(content).toString('utf8');
|
||||
|
||||
// 使用 TextDocument API
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
const text = document.getText();
|
||||
```
|
||||
|
||||
### 6.2 写入文件
|
||||
|
||||
```typescript
|
||||
// 写入文件
|
||||
const uri = vscode.Uri.file('/path/to/file.txt');
|
||||
const content = Buffer.from('Hello World', 'utf8');
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
|
||||
// 使用 WorkspaceEdit
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
edit.createFile(uri, { overwrite: true });
|
||||
edit.insert(uri, new vscode.Position(0, 0), 'Hello World');
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
```
|
||||
|
||||
### 6.3 文件监听
|
||||
|
||||
```typescript
|
||||
// 监听文件变化
|
||||
const watcher = vscode.workspace.createFileSystemWatcher('**/*.js');
|
||||
|
||||
watcher.onDidCreate(uri => {
|
||||
console.log('File created:', uri.fsPath);
|
||||
});
|
||||
|
||||
watcher.onDidChange(uri => {
|
||||
console.log('File changed:', uri.fsPath);
|
||||
});
|
||||
|
||||
watcher.onDidDelete(uri => {
|
||||
console.log('File deleted:', uri.fsPath);
|
||||
});
|
||||
|
||||
context.subscriptions.push(watcher);
|
||||
```
|
||||
|
||||
### 6.4 工作区操作
|
||||
|
||||
```typescript
|
||||
// 获取工作区文件夹
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders) {
|
||||
const rootPath = workspaceFolders[0].uri.fsPath;
|
||||
}
|
||||
|
||||
// 查找文件
|
||||
const files = await vscode.workspace.findFiles(
|
||||
'**/*.ts', // include pattern
|
||||
'**/node_modules/**' // exclude pattern
|
||||
);
|
||||
|
||||
// 打开文件
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(document);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 配置和存储 ⭐⭐⭐⭐ **面试重点**
|
||||
|
||||
### 7.1 读取配置 📌重要
|
||||
|
||||
```typescript
|
||||
// 读取配置
|
||||
const config = vscode.workspace.getConfiguration('myExtension');
|
||||
const value = config.get<string>('settingName', 'defaultValue');
|
||||
|
||||
// 监听配置变化
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('myExtension.settingName')) {
|
||||
console.log('Configuration changed');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 更新配置
|
||||
|
||||
```typescript
|
||||
const config = vscode.workspace.getConfiguration('myExtension');
|
||||
|
||||
// 更新用户配置(全局)
|
||||
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Global);
|
||||
|
||||
// 更新工作区配置
|
||||
await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Workspace);
|
||||
```
|
||||
|
||||
### 7.3 状态存储 🔥必考
|
||||
|
||||
```typescript
|
||||
// 工作区状态(仅当前工作区)
|
||||
await context.workspaceState.update('key', 'value');
|
||||
const value = context.workspaceState.get('key');
|
||||
|
||||
// 全局状态(跨工作区)
|
||||
await context.globalState.update('key', 'value');
|
||||
const value = context.globalState.get('key');
|
||||
|
||||
// 存储对象
|
||||
await context.globalState.update('userData', { name: 'John', age: 30 });
|
||||
```
|
||||
|
||||
### 7.4 敏感信息存储 🔥必考(Token 管理)
|
||||
|
||||
```typescript
|
||||
// 存储密码、Token 等敏感信息
|
||||
await context.secrets.store('apiToken', 'secret-token-value');
|
||||
|
||||
// 读取
|
||||
const token = await context.secrets.get('apiToken');
|
||||
|
||||
// 删除
|
||||
await context.secrets.delete('apiToken');
|
||||
|
||||
// 监听变化
|
||||
context.secrets.onDidChange(e => {
|
||||
console.log('Secret changed:', e.key);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 消息通知
|
||||
|
||||
### 8.1 信息提示
|
||||
|
||||
```typescript
|
||||
// 普通信息
|
||||
vscode.window.showInformationMessage('Operation completed!');
|
||||
|
||||
// 警告
|
||||
vscode.window.showWarningMessage('This action may cause issues');
|
||||
|
||||
// 错误
|
||||
vscode.window.showErrorMessage('Operation failed!');
|
||||
```
|
||||
|
||||
### 8.2 带按钮的提示
|
||||
|
||||
```typescript
|
||||
const result = await vscode.window.showInformationMessage(
|
||||
'Do you want to continue?',
|
||||
'Yes',
|
||||
'No',
|
||||
'Cancel'
|
||||
);
|
||||
|
||||
if (result === 'Yes') {
|
||||
// 用户点击了 Yes
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 输入框
|
||||
|
||||
```typescript
|
||||
// 简单输入
|
||||
const input = await vscode.window.showInputBox({
|
||||
prompt: 'Enter your name',
|
||||
placeHolder: 'John Doe',
|
||||
validateInput: (value) => {
|
||||
return value.length < 3 ? 'Name too short' : null;
|
||||
}
|
||||
});
|
||||
|
||||
// 快速选择
|
||||
const selected = await vscode.window.showQuickPick(
|
||||
['Option 1', 'Option 2', 'Option 3'],
|
||||
{
|
||||
placeHolder: 'Select an option',
|
||||
canPickMany: false
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 8.4 进度提示
|
||||
|
||||
```typescript
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: 'Processing...',
|
||||
cancellable: true
|
||||
},
|
||||
async (progress, token) => {
|
||||
token.onCancellationRequested(() => {
|
||||
console.log('User canceled');
|
||||
});
|
||||
|
||||
progress.report({ increment: 0, message: 'Starting...' });
|
||||
await doWork1();
|
||||
|
||||
progress.report({ increment: 50, message: 'Half done...' });
|
||||
await doWork2();
|
||||
|
||||
progress.report({ increment: 100, message: 'Complete!' });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 语言特性支持
|
||||
|
||||
### 9.1 代码补全
|
||||
|
||||
```typescript
|
||||
const provider = vscode.languages.registerCompletionItemProvider(
|
||||
'javascript',
|
||||
{
|
||||
provideCompletionItems(document, position) {
|
||||
const item = new vscode.CompletionItem('myFunction');
|
||||
item.kind = vscode.CompletionItemKind.Function;
|
||||
item.detail = 'My custom function';
|
||||
item.documentation = 'This is a custom function';
|
||||
item.insertText = new vscode.SnippetString('myFunction($1)$0');
|
||||
|
||||
return [item];
|
||||
}
|
||||
},
|
||||
'.' // 触发字符
|
||||
);
|
||||
|
||||
context.subscriptions.push(provider);
|
||||
```
|
||||
|
||||
### 9.2 悬停提示
|
||||
|
||||
```typescript
|
||||
const provider = vscode.languages.registerHoverProvider('javascript', {
|
||||
provideHover(document, position) {
|
||||
const range = document.getWordRangeAtPosition(position);
|
||||
const word = document.getText(range);
|
||||
|
||||
return new vscode.Hover([
|
||||
`**${word}**`,
|
||||
'This is a hover tooltip'
|
||||
]);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 9.3 诊断(错误提示)
|
||||
|
||||
```typescript
|
||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension');
|
||||
context.subscriptions.push(diagnosticCollection);
|
||||
|
||||
function updateDiagnostics(document: vscode.TextDocument) {
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
|
||||
const text = document.getText();
|
||||
const regex = /TODO/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text))) {
|
||||
const range = new vscode.Range(
|
||||
document.positionAt(match.index),
|
||||
document.positionAt(match.index + match[0].length)
|
||||
);
|
||||
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
range,
|
||||
'TODO found',
|
||||
vscode.DiagnosticSeverity.Warning
|
||||
);
|
||||
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
diagnosticCollection.set(document.uri, diagnostics);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 调试和诊断
|
||||
|
||||
### 10.1 输出通道
|
||||
|
||||
```typescript
|
||||
const outputChannel = vscode.window.createOutputChannel('My Extension');
|
||||
context.subscriptions.push(outputChannel);
|
||||
|
||||
outputChannel.appendLine('Extension activated');
|
||||
outputChannel.show(); // 显示输出面板
|
||||
```
|
||||
|
||||
### 10.2 日志记录
|
||||
|
||||
```typescript
|
||||
// 使用 LogOutputChannel(带时间戳)
|
||||
const logger = vscode.window.createOutputChannel('My Extension', { log: true });
|
||||
|
||||
logger.trace('Trace message');
|
||||
logger.debug('Debug message');
|
||||
logger.info('Info message');
|
||||
logger.warn('Warning message');
|
||||
logger.error('Error message');
|
||||
```
|
||||
|
||||
### 10.3 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
vscode.window.showErrorMessage(`Error: ${error.message}`);
|
||||
logger.error(error.stack || error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践总结
|
||||
|
||||
### ✅ 推荐做法
|
||||
|
||||
1. **资源管理**:所有 disposable 对象都放入 `context.subscriptions`
|
||||
2. **延迟激活**:使用 `onStartupFinished` 而不是 `*`
|
||||
3. **异步操作**:使用 `async/await` 处理异步操作
|
||||
4. **错误处理**:捕获异常并给用户友好提示
|
||||
5. **类型安全**:充分利用 TypeScript 类型系统
|
||||
6. **状态持久化**:使用 `globalState`/`workspaceState` 保存状态
|
||||
7. **敏感信息**:使用 `secrets` API 存储 Token、密码等
|
||||
|
||||
### ❌ 避免做法
|
||||
|
||||
1. 不要在 `activate` 中执行耗时操作
|
||||
2. 不要忘记清理资源(监听器、Webview 等)
|
||||
3. 不要在 Webview 中直接访问文件系统
|
||||
4. 不要在配置中存储敏感信息
|
||||
5. 不要阻塞主线程(使用 Worker 或异步操作)
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [VS Code Extension API 官方文档](https://code.visualstudio.com/api)
|
||||
- [Extension Samples](https://github.com/microsoft/vscode-extension-samples)
|
||||
- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
|
||||
@ -43,8 +43,7 @@ export interface MessageSegment {
|
||||
toolResult?: string;
|
||||
toolDescription?: string;
|
||||
askId?: string;
|
||||
question?: string;
|
||||
options?: string[];
|
||||
questions?: import("../types/api").QuestionItem[];
|
||||
// 智能体相关字段
|
||||
agentId?: string;
|
||||
agentName?: string;
|
||||
@ -647,8 +646,11 @@ export class DialogSession {
|
||||
this.segments.push({
|
||||
type: "question",
|
||||
askId: askId,
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
questions: [{
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
multiSelect: false
|
||||
}],
|
||||
});
|
||||
|
||||
// 实时发送段落更新
|
||||
@ -666,9 +668,12 @@ export class DialogSession {
|
||||
await userInteractionManager.handleAskUser(
|
||||
{
|
||||
askId: askId,
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
} as AskUserEvent,
|
||||
questions: [{
|
||||
question: question,
|
||||
options: ["确认执行", "取消"],
|
||||
multiSelect: false
|
||||
}]
|
||||
},
|
||||
this.taskId
|
||||
);
|
||||
|
||||
@ -714,12 +719,15 @@ export class DialogSession {
|
||||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||
const planEvent = {
|
||||
askId: askId,
|
||||
question: `请确认执行计划:${data.title}`,
|
||||
options: ["确认执行", "修改计划", "取消"],
|
||||
questions: [{
|
||||
question: `请确认执行计划:${data.title}`,
|
||||
options: ["确认执行", "修改计划", "取消"],
|
||||
multiSelect: false
|
||||
}]
|
||||
};
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(
|
||||
planEvent as AskUserEvent,
|
||||
planEvent,
|
||||
this.taskId
|
||||
);
|
||||
} catch (error) {
|
||||
@ -856,13 +864,9 @@ export class DialogSession {
|
||||
this.segments.push({
|
||||
type: "question",
|
||||
askId: data.askId,
|
||||
question: data.question,
|
||||
options: data.options,
|
||||
questions: data.questions,
|
||||
});
|
||||
// 实时发送段落更新(包含问题)
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
// 同时调用 onQuestion 用于更新状态栏等
|
||||
callbacks.onQuestion?.(data.askId, data.question, data.options);
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(data, this.taskId);
|
||||
} catch (error) {
|
||||
@ -1109,14 +1113,13 @@ export class DialogSession {
|
||||
*/
|
||||
async submitAnswer(
|
||||
askId: string,
|
||||
answers?: { [key: string]: string[] },
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
): Promise<void> {
|
||||
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||
// 如果 pendingQuestions 中有问题,走正常流程
|
||||
// 如果没有,receiveAnswer 会使用 fallbackTaskId 直接发送到后端
|
||||
await userInteractionManager.receiveAnswer(
|
||||
askId,
|
||||
answers,
|
||||
selected,
|
||||
customInput,
|
||||
this.taskId
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api';
|
||||
|
||||
/**
|
||||
* 待处理的用户问题
|
||||
@ -12,8 +12,7 @@ import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
interface PendingQuestion {
|
||||
askId: string;
|
||||
taskId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
questions: QuestionItem[];
|
||||
resolve: (answer: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
@ -45,20 +44,15 @@ export class UserInteractionManager {
|
||||
* @param taskId 当前任务ID
|
||||
*/
|
||||
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
||||
const { askId, question, options } = event;
|
||||
const { askId, questions } = event;
|
||||
|
||||
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
|
||||
console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`);
|
||||
|
||||
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
|
||||
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
||||
|
||||
// 创建 Promise 等待用户回答
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingQuestions.set(askId, {
|
||||
askId,
|
||||
taskId,
|
||||
question,
|
||||
options,
|
||||
questions,
|
||||
resolve: (answer: string) => {
|
||||
this.submitUserAnswer(askId, taskId, answer)
|
||||
.then(() => resolve())
|
||||
@ -67,7 +61,6 @@ export class UserInteractionManager {
|
||||
reject
|
||||
});
|
||||
|
||||
// 设置超时(2小时)
|
||||
setTimeout(() => {
|
||||
if (this.pendingQuestions.has(askId)) {
|
||||
this.pendingQuestions.delete(askId);
|
||||
@ -80,24 +73,35 @@ export class UserInteractionManager {
|
||||
/**
|
||||
* 处理用户提交的回答(从 WebView 调用)
|
||||
* @param askId 问题ID
|
||||
* @param selected 选中的选项
|
||||
* @param customInput 自定义输入
|
||||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||
* @param answers 新格式:按问题索引的答案
|
||||
* @param selected 旧格式:选中的选项
|
||||
* @param customInput 旧格式:自定义输入
|
||||
* @param fallbackTaskId 当问题不存在时使用的 taskId
|
||||
*/
|
||||
async receiveAnswer(
|
||||
askId: string,
|
||||
answers?: { [key: string]: string[] },
|
||||
selected?: string[],
|
||||
customInput?: string,
|
||||
fallbackTaskId?: string
|
||||
): Promise<void> {
|
||||
const pending = this.pendingQuestions.get(askId);
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
// 构建答案字符串
|
||||
let answer = '';
|
||||
if (answers) {
|
||||
answer = Object.entries(answers)
|
||||
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
||||
.map(([_, vals]) => vals.join('; '))
|
||||
.join(' | ');
|
||||
} else {
|
||||
answer = customInput || selected?.join(', ') || '';
|
||||
}
|
||||
|
||||
if (!pending) {
|
||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||
if (fallbackTaskId) {
|
||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}`);
|
||||
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
|
||||
} else {
|
||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||
}
|
||||
@ -105,11 +109,7 @@ export class UserInteractionManager {
|
||||
}
|
||||
|
||||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||||
|
||||
// 移除待处理问题
|
||||
this.pendingQuestions.delete(askId);
|
||||
|
||||
// 触发 resolve
|
||||
pending.resolve(answer);
|
||||
}
|
||||
|
||||
@ -119,7 +119,8 @@ export class UserInteractionManager {
|
||||
private async submitUserAnswer(
|
||||
askId: string,
|
||||
taskId: string,
|
||||
answer: string
|
||||
answer: string,
|
||||
answers?: { [key: string]: string[] }
|
||||
): Promise<void> {
|
||||
// 检查是否是工具确认类型的问题
|
||||
if (askId.startsWith('tool_confirm_')) {
|
||||
@ -148,7 +149,8 @@ export class UserInteractionManager {
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
customInput: answer
|
||||
answers: answers,
|
||||
customInput: answers ? undefined : answer
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** 单个问题项 */
|
||||
export interface QuestionItem {
|
||||
question: string;
|
||||
options: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
questions: QuestionItem[];
|
||||
}
|
||||
|
||||
/** complete 事件数据 */
|
||||
@ -351,10 +357,12 @@ export interface AnswerRequest {
|
||||
askId: string;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 选中的选项列表 */
|
||||
/** 选中的选项列表(旧格式,兼容) */
|
||||
selected?: string[];
|
||||
/** 自定义输入内容 */
|
||||
/** 自定义输入内容(旧格式,兼容) */
|
||||
customInput?: string;
|
||||
/** 新格式:按问题索引的答案,key 为问题下标 "0","1",value 为选中项列表 */
|
||||
answers?: { [key: string]: string[] };
|
||||
}
|
||||
|
||||
/** 用户回答响应 */
|
||||
|
||||
@ -468,11 +468,12 @@ async function handleUserMessageWithBackend(
|
||||
*/
|
||||
export async function handleUserAnswer(
|
||||
askId: string,
|
||||
answers?: { [key: string]: string[] },
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
): Promise<void> {
|
||||
if (currentSession) {
|
||||
await currentSession.submitAnswer(askId, selected, customInput);
|
||||
await currentSession.submitAnswer(askId, answers, selected, customInput);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -128,6 +128,15 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
message.askId,
|
||||
message.answers,
|
||||
message.selected,
|
||||
message.customInput
|
||||
);
|
||||
break;
|
||||
case "userAnswer":
|
||||
handleUserAnswer(
|
||||
message.askId,
|
||||
message.answers,
|
||||
message.selected,
|
||||
message.customInput
|
||||
);
|
||||
|
||||
@ -1188,7 +1188,6 @@ export function getMessageAreaScript(): string {
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.className += ' segment-question';
|
||||
|
||||
// 检查是否已回答
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||
|
||||
@ -1196,56 +1195,49 @@ export function getMessageAreaScript(): string {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
// 检查是否有选项
|
||||
const hasOptions = segment.options && segment.options.length > 0;
|
||||
const questions = segment.questions || [];
|
||||
const questionsHtml = questions.map((q, qIdx) => {
|
||||
const inputType = q.multiSelect ? 'checkbox' : 'radio';
|
||||
const inputName = 'q_' + segment.askId + '_' + qIdx;
|
||||
const optionsHtml = q.options.map((opt, optIdx) => {
|
||||
const escapedOpt = opt.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
return '<label class="question-option">' +
|
||||
'<input type="' + inputType + '" name="' + inputName + '" value="' + escapedOpt + '" data-q-idx="' + qIdx + '" data-opt-idx="' + optIdx + '" ' + (isAnswered ? 'disabled' : '') + ' />' +
|
||||
'<span>' + opt + '</span>' +
|
||||
'</label>';
|
||||
}).join('');
|
||||
return '<div class="question-item">' +
|
||||
'<div class="question-text">' + formatText(q.question) + '</div>' +
|
||||
'<div class="question-options">' + optionsHtml + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
const optionsHtml = hasOptions
|
||||
? (segment.options || []).map(opt => {
|
||||
const isSelected = isAnswered && opt === selectedAnswer;
|
||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||
}).join('')
|
||||
: '';
|
||||
segmentDiv.innerHTML = questionsHtml +
|
||||
'<div class="custom-input-container" style="display: ' + (isAnswered ? 'none' : 'flex') + ';">' +
|
||||
'<button class="custom-submit">提交答案</button>' +
|
||||
'</div>';
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
|
||||
<button class="custom-submit">提交</button>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
if (hasOptions) {
|
||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||
optionButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const option = this.getAttribute('data-option');
|
||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
const customInput = segmentDiv.querySelector('.custom-input');
|
||||
if (submitBtn && customInput) {
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
// 支持回车提交
|
||||
customInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
}
|
||||
}
|
||||
const answers = {};
|
||||
questions.forEach((q, qIdx) => {
|
||||
const inputs = segmentDiv.querySelectorAll('input[data-q-idx="' + qIdx + '"]:checked');
|
||||
answers[qIdx] = Array.from(inputs).map(inp => (inp as HTMLInputElement).value);
|
||||
});
|
||||
vscode.postMessage({
|
||||
command: 'userAnswer',
|
||||
askId: segment.askId,
|
||||
answers: answers,
|
||||
taskId: currentTaskId
|
||||
});
|
||||
answeredQuestions.set(segment.askId, JSON.stringify(answers));
|
||||
segmentDiv.classList.add('answered');
|
||||
segmentDiv.querySelectorAll('input').forEach(inp => (inp as HTMLInputElement).disabled = true);
|
||||
const container = segmentDiv.querySelector('.custom-input-container') as HTMLElement;
|
||||
if (container) container.style.display = 'none';
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
@ -1446,14 +1438,15 @@ export function getMessageAreaScript(): string {
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-segment">
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
<div class="question-options">
|
||||
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
const questions = segment.questions || [];
|
||||
const questionsHtml = questions.map(q => {
|
||||
const optionsHtml = q.options.map(opt => '<span class="question-opt">' + opt + '</span>').join('');
|
||||
return '<div class="question-item">' +
|
||||
'<div class="question-text">' + formatText(q.question) + '</div>' +
|
||||
'<div class="question-options">' + optionsHtml + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
segmentDiv.innerHTML = '<div class="question-segment">' + questionsHtml + '</div>';
|
||||
} else if (segment.type === 'agent') {
|
||||
// 智能体卡片渲染
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
|
||||
Reference in New Issue
Block a user