进入VS Code扩展开发的高级阶段,我们将探索最前沿的扩展开发技术。本阶段重点学习AI集成、复杂WebView应用、Notebook扩展开发和自定义编辑器构建,这些技能将使你能够开发出具有创新性和专业水准的扩展。


🎯 学习目标

完成本阶段学习后,你将能够:

  • 🤖 AI扩展开发:集成Language Model、创建Chat Participant、开发AI工具
  • 🌐 WebView进阶应用:React/Vue集成、复杂数据可视化、实时通信
  • 📓 Notebook扩展:自定义渲染器、内核集成、交互式文档
  • ✏️ 自定义编辑器:可视化编辑器、二进制编辑器、专用数据格式
  • 🔧 虚拟文档系统:虚拟文件系统、动态内容生成

预计学习时间:3-4周(每天1-2小时)


🤖 第一部分:AI集成开发

1.1 Language Model 集成基础

配置Language Model API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// src/aiProvider.ts
import * as vscode from 'vscode';

export class AIProvider {
private model: vscode.LanguageModelChat | undefined;

constructor() {
this.initializeModel();
}

private async initializeModel() {
try {
// 获取可用的语言模型
const models = await vscode.lm.selectChatModels({
vendor: 'copilot',
family: 'gpt-4'
});

if (models.length > 0) {
this.model = models[0];
console.log('AI模型初始化成功:', this.model.name);
} else {
vscode.window.showWarningMessage('未找到可用的AI模型');
}
} catch (error) {
console.error('AI模型初始化失败:', error);
vscode.window.showErrorMessage('AI功能初始化失败');
}
}

async generateCode(prompt: string, context?: string): Promise<string> {
if (!this.model) {
throw new Error('AI模型未初始化');
}

const messages = [
vscode.LanguageModelChatMessage.User(`请根据以下要求生成代码:
${prompt}

${context ? `上下文信息:${context}` : ''}

请只返回代码,不要包含解释。`)
];

const request = await this.model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);

let response = '';
for await (const fragment of request.text) {
response += fragment;
}

return response.trim();
}

async explainCode(code: string): Promise<string> {
if (!this.model) {
throw new Error('AI模型未初始化');
}

const messages = [
vscode.LanguageModelChatMessage.User(`请解释以下代码的功能和工作原理:

\`\`\`
${code}
\`\`\`

请用简洁明了的语言进行解释。`)
];

const request = await this.model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);

let response = '';
for await (const fragment of request.text) {
response += fragment;
}

return response.trim();
}

async optimizeCode(code: string): Promise<{ optimizedCode: string; suggestions: string[] }> {
if (!this.model) {
throw new Error('AI模型未初始化');
}

const messages = [
vscode.LanguageModelChatMessage.User(`请优化以下代码并提供改进建议:

\`\`\`
${code}
\`\`\`

请返回JSON格式:
{
"optimizedCode": "优化后的代码",
"suggestions": ["建议1", "建议2", ...]
}`)
];

const request = await this.model.sendRequest(messages, {}, new vscode.CancellationTokenSource().token);

let response = '';
for await (const fragment of request.text) {
response += fragment;
}

try {
return JSON.parse(response);
} catch (error) {
return {
optimizedCode: code,
suggestions: ['AI响应格式错误,无法解析优化建议']
};
}
}
}

1.2 Chat Participant 开发

创建AI聊天参与者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// src/chatParticipant.ts
import * as vscode from 'vscode';
import { AIProvider } from './aiProvider';

export class CodeAssistantChatParticipant {
private aiProvider: AIProvider;

constructor() {
this.aiProvider = new AIProvider();
}

register(context: vscode.ExtensionContext) {
// 注册聊天参与者
const participant = vscode.chat.createChatParticipant('codeAssistant', this.handleChatRequest.bind(this));

participant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'images', 'assistant-icon.png');
participant.followupProvider = {
provideFollowups: this.provideFollowups.bind(this)
};

context.subscriptions.push(participant);
}

private async handleChatRequest(
request: vscode.ChatRequest,
context: vscode.ChatContext,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<void> {

try {
const prompt = request.prompt;

// 分析请求类型
if (prompt.includes('生成') || prompt.includes('创建')) {
await this.handleCodeGeneration(prompt, stream, token);
} else if (prompt.includes('解释') || prompt.includes('说明')) {
await this.handleCodeExplanation(prompt, stream, token);
} else if (prompt.includes('优化') || prompt.includes('改进')) {
await this.handleCodeOptimization(prompt, stream, token);
} else {
await this.handleGeneralQuestion(prompt, stream, token);
}

} catch (error) {
stream.markdown(`❌ 处理请求时发生错误: ${error}`);
}
}

private async handleCodeGeneration(
prompt: string,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<void> {

stream.progress('正在生成代码...');

// 获取当前编辑器上下文
const editor = vscode.window.activeTextEditor;
const context = editor ? editor.document.getText() : '';

try {
const generatedCode = await this.aiProvider.generateCode(prompt, context);

stream.markdown('### 生成的代码\n\n');
stream.markdown(`\`\`\`typescript\n${generatedCode}\n\`\`\``);

// 提供插入代码的按钮
stream.button({
command: 'codeAssistant.insertCode',
arguments: [generatedCode],
title: '插入到编辑器'
});

} catch (error) {
stream.markdown(`生成代码失败: ${error}`);
}
}

private async handleCodeExplanation(
prompt: string,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<void> {

const editor = vscode.window.activeTextEditor;

if (!editor || editor.selection.isEmpty) {
stream.markdown('请先选中要解释的代码。');
return;
}

stream.progress('正在分析代码...');

const selectedCode = editor.document.getText(editor.selection);

try {
const explanation = await this.aiProvider.explainCode(selectedCode);

stream.markdown('### 代码解释\n\n');
stream.markdown(explanation);

} catch (error) {
stream.markdown(`代码解释失败: ${error}`);
}
}

private async handleCodeOptimization(
prompt: string,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<void> {

const editor = vscode.window.activeTextEditor;

if (!editor || editor.selection.isEmpty) {
stream.markdown('请先选中要优化的代码。');
return;
}

stream.progress('正在优化代码...');

const selectedCode = editor.document.getText(editor.selection);

try {
const result = await this.aiProvider.optimizeCode(selectedCode);

stream.markdown('### 优化建议\n\n');

result.suggestions.forEach((suggestion, index) => {
stream.markdown(`${index + 1}. ${suggestion}\n`);
});

stream.markdown('\n### 优化后的代码\n\n');
stream.markdown(`\`\`\`typescript\n${result.optimizedCode}\n\`\`\``);

// 提供替换代码的按钮
stream.button({
command: 'codeAssistant.replaceCode',
arguments: [result.optimizedCode],
title: '替换选中代码'
});

} catch (error) {
stream.markdown(`代码优化失败: ${error}`);
}
}

private async handleGeneralQuestion(
prompt: string,
stream: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<void> {

stream.progress('正在思考...');

// 这里可以调用通用的AI模型进行对话
stream.markdown('我是代码助手,专门帮助您处理代码相关的问题。\n\n');
stream.markdown('我可以帮您:\n');
stream.markdown('- 🔨 生成代码\n');
stream.markdown('- 📖 解释代码\n');
stream.markdown('- ⚡ 优化代码\n');
stream.markdown('- 🐛 调试代码\n\n');
stream.markdown('请告诉我您需要什么帮助!');
}

private provideFollowups(
result: vscode.ChatResult,
context: vscode.ChatContext,
token: vscode.CancellationToken
): vscode.ProviderResult<vscode.ChatFollowup[]> {

return [
{
prompt: '生成一个函数',
label: '💡 生成函数',
command: 'codeAssistant.generateFunction'
},
{
prompt: '解释选中的代码',
label: '📖 解释代码',
command: 'codeAssistant.explainSelection'
},
{
prompt: '优化这段代码',
label: '⚡ 优化代码',
command: 'codeAssistant.optimizeSelection'
}
];
}
}

1.3 Language Model Tool 开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// src/languageModelTool.ts
import * as vscode from 'vscode';

export class LanguageModelTool {

register(context: vscode.ExtensionContext) {
// 注册语言模型工具
const tool = vscode.lm.registerTool('codeAnalyzer', {
displayName: 'Code Analyzer',
description: '分析代码复杂度和质量',
parametersSchema: {
type: 'object',
properties: {
code: {
type: 'string',
description: '要分析的代码'
},
language: {
type: 'string',
description: '编程语言'
}
},
required: ['code']
}
}, async (options, token) => {
return this.analyzeCode(options.parameters, token);
});

context.subscriptions.push(tool);
}

private async analyzeCode(
parameters: { code: string; language?: string },
token: vscode.CancellationToken
): Promise<vscode.LanguageModelToolResult> {

const { code, language = 'unknown' } = parameters;

// 简单的代码分析逻辑
const analysis = {
lineCount: code.split('\n').length,
characterCount: code.length,
wordCount: code.split(/\s+/).length,
complexity: this.calculateComplexity(code),
suggestions: this.generateSuggestions(code, language)
};

const result = `代码分析报告:

📊 **基础统计**
- 行数: ${analysis.lineCount}
- 字符数: ${analysis.characterCount}
- 单词数: ${analysis.wordCount}

📈 **复杂度分析**
- 圈复杂度: ${analysis.complexity.cyclomatic}
- 认知复杂度: ${analysis.complexity.cognitive}

💡 **改进建议**
${analysis.suggestions.map(s => `- ${s}`).join('\n')}`;

return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(result)
]);
}

private calculateComplexity(code: string): { cyclomatic: number; cognitive: number } {
// 简化的复杂度计算
const cyclomaticPatterns = [
/if\s*\(/g,
/else\s+if\s*\(/g,
/while\s*\(/g,
/for\s*\(/g,
/switch\s*\(/g,
/case\s+/g,
/catch\s*\(/g
];

let cyclomaticComplexity = 1; // 基础复杂度

cyclomaticPatterns.forEach(pattern => {
const matches = code.match(pattern);
if (matches) {
cyclomaticComplexity += matches.length;
}
});

// 认知复杂度(简化版)
const cognitiveComplexity = Math.min(cyclomaticComplexity * 1.2, 100);

return {
cyclomatic: cyclomaticComplexity,
cognitive: Math.round(cognitiveComplexity)
};
}

private generateSuggestions(code: string, language: string): string[] {
const suggestions: string[] = [];

// 基于代码内容生成建议
if (code.includes('var ')) {
suggestions.push('建议使用 let 或 const 替代 var');
}

if (code.split('\n').length > 50) {
suggestions.push('函数过长,建议拆分为更小的函数');
}

if (!/^\s*\/\/|^\s*\/\*|\*/.test(code)) {
suggestions.push('建议添加注释来提高代码可读性');
}

if (code.includes('console.log')) {
suggestions.push('移除或使用适当的日志库替代 console.log');
}

return suggestions.length > 0 ? suggestions : ['代码质量良好,没有明显问题'];
}
}

🌐 第二部分:WebView进阶开发

2.1 React集成开发

项目结构设置

1
2
3
4
5
6
7
8
9
10
11
12
13
src/
├── webview/
│ ├── components/
│ │ ├── App.tsx
│ │ ├── CodeEditor.tsx
│ │ └── FileExplorer.tsx
│ ├── utils/
│ │ └── vscodeApi.ts
│ ├── styles/
│ │ └── global.css
│ └── index.tsx
├── extension.ts
└── webviewProvider.ts

WebView Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// src/webviewProvider.ts
import * as vscode from 'vscode';
import * as path from 'path';

export class ReactWebviewProvider implements vscode.WebviewViewProvider {

public static readonly viewType = 'reactWebview';

private _view?: vscode.WebviewView;

constructor(
private readonly _extensionUri: vscode.Uri,
) { }

public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;

webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
this._extensionUri
]
};

webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);

// 监听来自React应用的消息
webviewView.webview.onDidReceiveMessage(data => {
switch (data.type) {
case 'openFile':
this.openFile(data.path);
break;
case 'saveFile':
this.saveFile(data.path, data.content);
break;
case 'getWorkspaceFiles':
this.getWorkspaceFiles();
break;
}
});
}

private _getHtmlForWebview(webview: vscode.Webview) {
// 在开发模式下,可以指向本地开发服务器
const isDevelopment = process.env.NODE_ENV === 'development';

if (isDevelopment) {
return this._getHtmlForDevelopment();
} else {
return this._getHtmlForProduction(webview);
}
}

private _getHtmlForDevelopment() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React WebView</title>
</head>
<body>
<div id="root"></div>
<script src="http://localhost:3000/static/js/bundle.js"></script>
</body>
</html>`;
}

private _getHtmlForProduction(webview: vscode.Webview) {
// 获取打包后的React应用文件
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'out', 'webview', 'bundle.js'));
const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'out', 'webview', 'bundle.css'));

return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React WebView</title>
<link href="${styleUri}" rel="stylesheet">
<style>
body {
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
const vscode = acquireVsCodeApi();
window.vscode = vscode;
</script>
<script src="${scriptUri}"></script>
</body>
</html>`;
}

private async openFile(filePath: string) {
try {
const document = await vscode.workspace.openTextDocument(filePath);
await vscode.window.showTextDocument(document);
} catch (error) {
vscode.window.showErrorMessage(`无法打开文件: ${error}`);
}
}

private async saveFile(filePath: string, content: string) {
try {
const uri = vscode.Uri.file(filePath);
await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8'));
vscode.window.showInformationMessage('文件保存成功');
} catch (error) {
vscode.window.showErrorMessage(`保存文件失败: ${error}`);
}
}

private async getWorkspaceFiles() {
try {
const files = await vscode.workspace.findFiles('**/*', '**/node_modules/**', 100);
const fileList = files.map(file => ({
path: file.fsPath,
name: path.basename(file.fsPath)
}));

this._view?.webview.postMessage({
type: 'workspaceFiles',
files: fileList
});
} catch (error) {
vscode.window.showErrorMessage(`获取文件列表失败: ${error}`);
}
}

public sendMessage(message: any) {
this._view?.webview.postMessage(message);
}
}

React组件开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// src/webview/components/App.tsx
import React, { useState, useEffect } from 'react';
import FileExplorer from './FileExplorer';
import CodeEditor from './CodeEditor';
import { VSCodeAPI } from '../utils/vscodeApi';

interface File {
path: string;
name: string;
}

const App: React.FC = () => {
const [files, setFiles] = useState<File[]>([]);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [fileContent, setFileContent] = useState<string>('');

useEffect(() => {
// 请求工作区文件列表
VSCodeAPI.postMessage({
type: 'getWorkspaceFiles'
});

// 监听来自VS Code的消息
const messageListener = (event: MessageEvent) => {
const message = event.data;

switch (message.type) {
case 'workspaceFiles':
setFiles(message.files);
break;
case 'fileContent':
setFileContent(message.content);
break;
}
};

window.addEventListener('message', messageListener);

return () => {
window.removeEventListener('message', messageListener);
};
}, []);

const handleFileSelect = (file: File) => {
setSelectedFile(file);
// 请求文件内容
VSCodeAPI.postMessage({
type: 'openFile',
path: file.path
});
};

const handleContentChange = (content: string) => {
setFileContent(content);
};

const handleSave = () => {
if (selectedFile) {
VSCodeAPI.postMessage({
type: 'saveFile',
path: selectedFile.path,
content: fileContent
});
}
};

return (
<div className="app">
<div className="header">
<h2>文件管理器</h2>
{selectedFile && (
<button onClick={handleSave} className="save-button">
保存文件
</button>
)}
</div>

<div className="content">
<div className="sidebar">
<FileExplorer
files={files}
selectedFile={selectedFile}
onFileSelect={handleFileSelect}
/>
</div>

<div className="main">
{selectedFile ? (
<CodeEditor
file={selectedFile}
content={fileContent}
onChange={handleContentChange}
/>
) : (
<div className="no-file-selected">
选择一个文件开始编辑
</div>
)}
</div>
</div>
</div>
);
};

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/webview/components/FileExplorer.tsx
import React from 'react';

interface File {
path: string;
name: string;
}

interface FileExplorerProps {
files: File[];
selectedFile: File | null;
onFileSelect: (file: File) => void;
}

const FileExplorer: React.FC<FileExplorerProps> = ({
files,
selectedFile,
onFileSelect
}) => {
return (
<div className="file-explorer">
<h3>文件列表</h3>
<div className="file-list">
{files.map((file, index) => (
<div
key={index}
className={`file-item ${selectedFile?.path === file.path ? 'selected' : ''}`}
onClick={() => onFileSelect(file)}
>
<span className="file-icon">📄</span>
<span className="file-name">{file.name}</span>
</div>
))}
</div>
</div>
);
};

export default FileExplorer;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/webview/components/CodeEditor.tsx
import React from 'react';

interface File {
path: string;
name: string;
}

interface CodeEditorProps {
file: File;
content: string;
onChange: (content: string) => void;
}

const CodeEditor: React.FC<CodeEditorProps> = ({
file,
content,
onChange
}) => {
return (
<div className="code-editor">
<div className="editor-header">
<h3>{file.name}</h3>
<span className="file-path">{file.path}</span>
</div>

<textarea
className="editor-content"
value={content}
onChange={(e) => onChange(e.target.value)}
placeholder="文件内容..."
rows={20}
cols={80}
/>
</div>
);
};

export default CodeEditor;

2.2 数据可视化应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// src/dataVisualization.ts
import * as vscode from 'vscode';

export class DataVisualizationProvider {

static createVisualizationPanel(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
'dataVisualization',
'数据可视化',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);

panel.webview.html = this.getWebviewContent();

// 处理消息
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'generateChart':
this.generateChartData(panel.webview, message.type);
break;
case 'exportChart':
this.exportChart(message.data);
break;
}
},
undefined,
context.subscriptions
);

return panel;
}

private static getWebviewContent(): string {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据可视化</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
padding: 20px;
}

.controls {
margin-bottom: 20px;
padding: 15px;
border: 1px solid var(--vscode-panel-border);
border-radius: 5px;
}

.chart-container {
position: relative;
height: 400px;
margin: 20px 0;
}

button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 8px 16px;
margin: 5px;
border-radius: 3px;
cursor: pointer;
}

button:hover {
background-color: var(--vscode-button-hoverBackground);
}

select {
background-color: var(--vscode-dropdown-background);
color: var(--vscode-dropdown-foreground);
border: 1px solid var(--vscode-dropdown-border);
padding: 5px;
margin: 5px;
}
</style>
</head>
<body>
<div class="controls">
<h3>图表控制</h3>
<label>图表类型:</label>
<select id="chartType">
<option value="line">折线图</option>
<option value="bar">柱状图</option>
<option value="pie">饼图</option>
<option value="doughnut">环形图</option>
</select>

<button onclick="generateChart()">生成图表</button>
<button onclick="exportChart()">导出图表</button>
</div>

<div class="chart-container">
<canvas id="chartCanvas"></canvas>
</div>

<script>
const vscode = acquireVsCodeApi();
let currentChart = null;

function generateChart() {
const chartType = document.getElementById('chartType').value;
vscode.postMessage({
command: 'generateChart',
type: chartType
});
}

function exportChart() {
if (currentChart) {
const imageData = currentChart.toBase64Image();
vscode.postMessage({
command: 'exportChart',
data: imageData
});
}
}

function createChart(type, data) {
const ctx = document.getElementById('chartCanvas').getContext('2d');

if (currentChart) {
currentChart.destroy();
}

currentChart = new Chart(ctx, {
type: type,
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: 'var(--vscode-foreground)'
}
}
},
scales: type !== 'pie' && type !== 'doughnut' ? {
x: {
ticks: {
color: 'var(--vscode-foreground)'
},
grid: {
color: 'var(--vscode-panel-border)'
}
},
y: {
ticks: {
color: 'var(--vscode-foreground)'
},
grid: {
color: 'var(--vscode-panel-border)'
}
}
} : {}
}
});
}

// 监听来自扩展的消息
window.addEventListener('message', event => {
const message = event.data;

switch (message.type) {
case 'chartData':
createChart(message.chartType, message.data);
break;
}
});
</script>
</body>
</html>`;
}

private static generateChartData(webview: vscode.Webview, chartType: string) {
// 生成示例数据
const labels = ['一月', '二月', '三月', '四月', '五月', '六月'];
const data1 = [12, 19, 3, 5, 2, 3];
const data2 = [2, 3, 20, 5, 1, 4];

let chartData;

switch (chartType) {
case 'line':
chartData = {
labels: labels,
datasets: [{
label: '数据集1',
data: data1,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1
}, {
label: '数据集2',
data: data2,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.1
}]
};
break;

case 'bar':
chartData = {
labels: labels,
datasets: [{
label: '销售额',
data: data1,
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 205, 86, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)'
]
}]
};
break;

case 'pie':
case 'doughnut':
chartData = {
labels: ['红色', '蓝色', '黄色', '绿色', '紫色', '橙色'],
datasets: [{
data: data1,
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
};
break;

default:
chartData = { labels: [], datasets: [] };
}

webview.postMessage({
type: 'chartData',
chartType: chartType,
data: chartData
});
}

private static async exportChart(imageData: string) {
try {
// 移除data:image/png;base64,前缀
const base64Data = imageData.replace(/^data:image\/png;base64,/, '');
const buffer = Buffer.from(base64Data, 'base64');

// 保存文件
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file('chart.png'),
filters: {
'PNG图片': ['png']
}
});

if (uri) {
await vscode.workspace.fs.writeFile(uri, buffer);
vscode.window.showInformationMessage(`图表已保存到:${uri.fsPath}`);
}
} catch (error) {
vscode.window.showErrorMessage(`导出图表失败:${error}`);
}
}
}

📓 第三部分:Notebook扩展开发

3.1 自定义Notebook渲染器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/notebookRenderer.ts
import * as vscode from 'vscode';

export class CustomNotebookRenderer {

static register(context: vscode.ExtensionContext) {
// 注册notebook渲染器
const renderer = vscode.notebooks.createRendererMessaging('customRenderer');

context.subscriptions.push(renderer);

// 处理来自渲染器的消息
renderer.onDidReceiveMessage(message => {
switch (message.type) {
case 'openFile':
this.openFile(message.path);
break;
case 'runCode':
this.runCode(message.code, message.language);
break;
}
});

return renderer;
}

private static async openFile(path: string) {
try {
const uri = vscode.Uri.file(path);
const document = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(document);
} catch (error) {
vscode.window.showErrorMessage(`无法打开文件:${error}`);
}
}

private static async runCode(code: string, language: string) {
// 简单的代码执行示例
if (language === 'javascript') {
try {
// 注意:这里只是示例,实际使用中需要安全的执行环境
const result = eval(code);
vscode.window.showInformationMessage(`执行结果:${result}`);
} catch (error) {
vscode.window.showErrorMessage(`执行错误:${error}`);
}
} else {
vscode.window.showInformationMessage(`暂不支持 ${language} 语言的执行`);
}
}
}

渲染器前端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
<!-- src/notebookRenderer/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义Notebook渲染器</title>
<style>
body {
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
margin: 0;
padding: 10px;
}

.output-container {
border: 1px solid var(--vscode-panel-border);
border-radius: 5px;
padding: 15px;
margin: 10px 0;
}

.code-block {
background-color: var(--vscode-textCodeBlock-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 3px;
padding: 10px;
font-family: var(--vscode-editor-font-family);
white-space: pre-wrap;
overflow-x: auto;
}

.result-block {
background-color: var(--vscode-editor-background);
border-left: 4px solid var(--vscode-textLink-foreground);
padding: 10px;
margin-top: 10px;
}

.error-block {
background-color: rgba(255, 0, 0, 0.1);
border-left: 4px solid red;
padding: 10px;
margin-top: 10px;
color: red;
}

.interactive-element {
margin: 10px 0;
}

button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 6px 12px;
border-radius: 3px;
cursor: pointer;
margin: 2px;
}

button:hover {
background-color: var(--vscode-button-hoverBackground);
}

.chart-container {
width: 100%;
height: 300px;
margin: 10px 0;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<script type="module">
// 获取VS Code API
const vscode = acquireVsCodeApi();

// 渲染输出内容
function renderOutput(outputItem) {
const container = document.createElement('div');
container.className = 'output-container';

const mimeType = outputItem.mime;
const data = outputItem.data;

switch (mimeType) {
case 'application/vnd.custom+json':
renderCustomJson(container, JSON.parse(data));
break;

case 'text/html':
container.innerHTML = data;
break;

case 'text/plain':
const textElement = document.createElement('div');
textElement.className = 'result-block';
textElement.textContent = data;
container.appendChild(textElement);
break;

case 'image/png':
const img = document.createElement('img');
img.src = `data:image/png;base64,${data}`;
img.style.maxWidth = '100%';
container.appendChild(img);
break;

default:
const defaultElement = document.createElement('div');
defaultElement.textContent = `不支持的MIME类型: ${mimeType}`;
container.appendChild(defaultElement);
}

return container;
}

function renderCustomJson(container, data) {
if (data.type === 'code_execution') {
// 渲染代码执行结果
const codeBlock = document.createElement('div');
codeBlock.className = 'code-block';
codeBlock.textContent = data.code;
container.appendChild(codeBlock);

const resultBlock = document.createElement('div');
resultBlock.className = data.success ? 'result-block' : 'error-block';
resultBlock.textContent = data.result;
container.appendChild(resultBlock);

// 添加重新执行按钮
const runButton = document.createElement('button');
runButton.textContent = '重新执行';
runButton.onclick = () => {
vscode.postMessage({
type: 'runCode',
code: data.code,
language: data.language
});
};
container.appendChild(runButton);

} else if (data.type === 'interactive_chart') {
// 渲染交互式图表
renderInteractiveChart(container, data);

} else if (data.type === 'file_reference') {
// 渲染文件引用
const fileLink = document.createElement('button');
fileLink.textContent = `打开文件: ${data.filename}`;
fileLink.onclick = () => {
vscode.postMessage({
type: 'openFile',
path: data.path
});
};
container.appendChild(fileLink);
}
}

function renderInteractiveChart(container, data) {
const chartContainer = document.createElement('div');
chartContainer.className = 'chart-container';

const canvas = document.createElement('canvas');
chartContainer.appendChild(canvas);
container.appendChild(chartContainer);

// 创建Chart.js图表
new Chart(canvas, {
type: data.chartType || 'line',
data: data.chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: 'var(--vscode-foreground)'
}
}
},
scales: {
x: {
ticks: { color: 'var(--vscode-foreground)' },
grid: { color: 'var(--vscode-panel-border)' }
},
y: {
ticks: { color: 'var(--vscode-foreground)' },
grid: { color: 'var(--vscode-panel-border)' }
}
}
}
});

// 添加交互按钮
const controls = document.createElement('div');
controls.className = 'interactive-element';

const updateButton = document.createElement('button');
updateButton.textContent = '更新数据';
updateButton.onclick = () => updateChartData();

const exportButton = document.createElement('button');
exportButton.textContent = '导出图表';
exportButton.onclick = () => exportChart();

controls.appendChild(updateButton);
controls.appendChild(exportButton);
container.appendChild(controls);
}

// 处理渲染请求
const renderOutputItem = (outputItem, element) => {
const rendered = renderOutput(outputItem);
element.appendChild(rendered);
};

// 暴露渲染函数给VS Code
self.createOutputItem = renderOutputItem;
</script>
</body>
</html>

3.2 Notebook内核集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// src/notebookKernel.ts
import * as vscode from 'vscode';

export class CustomNotebookKernel {
private readonly _id = 'customKernel';
private readonly _label = '自定义内核';
private readonly _supportedLanguages = ['javascript', 'typescript', 'markdown'];

private _controller: vscode.NotebookController;

constructor() {
this._controller = vscode.notebooks.createNotebookController(
this._id,
'custom-notebook',
this._label
);

this._controller.supportedLanguages = this._supportedLanguages;
this._controller.supportsExecutionOrder = true;
this._controller.executeHandler = this._executeHandler.bind(this);
this._controller.interruptHandler = this._interruptHandler.bind(this);
}

private async _executeHandler(
cells: vscode.NotebookCell[],
notebook: vscode.NotebookDocument,
controller: vscode.NotebookController
): Promise<void> {

for (const cell of cells) {
await this._executeCell(cell);
}
}

private async _executeCell(cell: vscode.NotebookCell): Promise<void> {
const execution = this._controller.createNotebookCellExecution(cell);
execution.executionOrder = Date.now();
execution.start(Date.now());

try {
const code = cell.document.getText();
const language = cell.document.languageId;

let result: any;

switch (language) {
case 'javascript':
result = await this._executeJavaScript(code);
break;

case 'typescript':
result = await this._executeTypeScript(code);
break;

case 'markdown':
result = await this._renderMarkdown(code);
break;

default:
throw new Error(`不支持的语言: ${language}`);
}

// 清除之前的输出
execution.clearOutput();

// 添加新的输出
if (result.type === 'html') {
execution.replaceOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.text(result.content, 'text/html')
])
]);
} else if (result.type === 'json') {
execution.replaceOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.json(result.content)
])
]);
} else {
execution.replaceOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.text(String(result.content), 'text/plain')
])
]);
}

execution.end(true, Date.now());

} catch (error) {
// 输出错误信息
execution.replaceOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.error(error as Error)
])
]);

execution.end(false, Date.now());
}
}

private async _executeJavaScript(code: string): Promise<any> {
// 简单的JavaScript执行(实际应用中需要使用安全的沙箱环境)
try {
// 创建一个新的函数来执行代码
const func = new Function(`
const console = {
log: (...args) => window.logOutput?.push(args.join(' ')) || console.log(...args)
};

${code}
`);

const result = func();

return {
type: 'text',
content: result !== undefined ? String(result) : '执行成功'
};
} catch (error) {
throw new Error(`JavaScript执行错误: ${error}`);
}
}

private async _executeTypeScript(code: string): Promise<any> {
// TypeScript执行需要编译步骤
// 这里简化处理,实际应用中可以集成TypeScript编译器
vscode.window.showInformationMessage('TypeScript执行需要编译步骤,暂时以JavaScript模式执行');
return this._executeJavaScript(code);
}

private async _renderMarkdown(code: string): Promise<any> {
// 简单的Markdown渲染
let html = code
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
.replace(/`(.*?)`/gim, '<code>$1</code>')
.replace(/\n/gim, '<br>');

return {
type: 'html',
content: html
};
}

private _interruptHandler(notebook: vscode.NotebookDocument): void {
// 中断执行的处理逻辑
vscode.window.showInformationMessage('执行已中断');
}

dispose(): void {
this._controller.dispose();
}
}

✏️ 第四部分:自定义编辑器开发

4.1 可视化配置编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
// src/configEditor.ts
import * as vscode from 'vscode';

export class ConfigEditorProvider implements vscode.CustomTextEditorProvider {

public static register(context: vscode.ExtensionContext): vscode.Disposable {
const provider = new ConfigEditorProvider(context);
const providerRegistration = vscode.window.registerCustomEditorProvider(
'configEditor.jsonEditor',
provider,
{
webviewOptions: {
retainContextWhenHidden: true,
},
supportsMultipleEditorsPerDocument: false,
}
);
return providerRegistration;
}

constructor(
private readonly context: vscode.ExtensionContext
) { }

public async resolveCustomTextEditor(
document: vscode.TextDocument,
webviewPanel: vscode.WebviewPanel,
_token: vscode.CancellationToken
): Promise<void> {

// 设置webview选项
webviewPanel.webview.options = {
enableScripts: true,
};

webviewPanel.webview.html = this.getHtmlForWebview(webviewPanel.webview);

// 监听文档变化
const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.uri.toString() === document.uri.toString()) {
this.updateWebview(webviewPanel.webview, document);
}
});

// 监听webview消息
webviewPanel.webview.onDidReceiveMessage(e => {
switch (e.type) {
case 'update':
this.updateDocument(document, e.content);
return;

case 'validate':
this.validateConfig(e.content);
return;
}
});

// 清理资源
webviewPanel.onDidDispose(() => {
changeDocumentSubscription.dispose();
});

// 初始化webview内容
this.updateWebview(webviewPanel.webview, document);
}

private getHtmlForWebview(webview: vscode.Webview): string {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置编辑器</title>
<style>
body {
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
margin: 0;
padding: 20px;
}

.form-group {
margin: 15px 0;
}

label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}

input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid var(--vscode-input-border);
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border-radius: 3px;
box-sizing: border-box;
}

button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 8px 16px;
margin: 5px 0;
border-radius: 3px;
cursor: pointer;
}

button:hover {
background-color: var(--vscode-button-hoverBackground);
}

.section {
border: 1px solid var(--vscode-panel-border);
border-radius: 5px;
padding: 15px;
margin: 15px 0;
}

.section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: var(--vscode-textLink-foreground);
}

.array-item {
border: 1px solid var(--vscode-panel-border);
border-radius: 3px;
padding: 10px;
margin: 5px 0;
background-color: var(--vscode-sideBar-background);
}

.remove-button {
background-color: var(--vscode-errorBadge-background);
color: var(--vscode-errorBadge-foreground);
float: right;
padding: 4px 8px;
font-size: 12px;
}

.add-button {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}

.validation-error {
color: var(--vscode-errorForeground);
background-color: rgba(255, 0, 0, 0.1);
padding: 5px;
border-radius: 3px;
margin-top: 5px;
}
</style>
</head>
<body>
<div id="configEditor">
<h1>配置编辑器</h1>

<div class="section">
<div class="section-title">基础配置</div>

<div class="form-group">
<label for="name">项目名称:</label>
<input type="text" id="name" onchange="updateConfig()">
</div>

<div class="form-group">
<label for="version">版本号:</label>
<input type="text" id="version" onchange="updateConfig()">
</div>

<div class="form-group">
<label for="description">描述:</label>
<textarea id="description" rows="3" onchange="updateConfig()"></textarea>
</div>
</div>

<div class="section">
<div class="section-title">环境配置</div>

<div class="form-group">
<label for="environment">环境:</label>
<select id="environment" onchange="updateConfig()">
<option value="development">开发环境</option>
<option value="testing">测试环境</option>
<option value="production">生产环境</option>
</select>
</div>

<div class="form-group">
<label for="debugMode">调试模式:</label>
<input type="checkbox" id="debugMode" onchange="updateConfig()">
</div>
</div>

<div class="section">
<div class="section-title">数据库配置</div>
<div id="databaseConfig">
<!-- 动态生成的数据库配置 -->
</div>
<button class="add-button" onclick="addDatabase()">添加数据库</button>
</div>

<div class="section">
<div class="section-title">API端点</div>
<div id="apiEndpoints">
<!-- 动态生成的API端点配置 -->
</div>
<button class="add-button" onclick="addEndpoint()">添加端点</button>
</div>

<div class="form-group">
<button onclick="validateConfig()">验证配置</button>
<button onclick="saveConfig()">保存配置</button>
</div>

<div id="validationResult"></div>
</div>

<script>
const vscode = acquireVsCodeApi();
let currentConfig = {};

function updateConfig() {
currentConfig = {
name: document.getElementById('name').value,
version: document.getElementById('version').value,
description: document.getElementById('description').value,
environment: document.getElementById('environment').value,
debugMode: document.getElementById('debugMode').checked,
databases: getDatabasesConfig(),
apiEndpoints: getEndpointsConfig()
};

vscode.postMessage({
type: 'update',
content: JSON.stringify(currentConfig, null, 2)
});
}

function getDatabasesConfig() {
const databases = [];
const dbElements = document.querySelectorAll('.database-item');

dbElements.forEach(element => {
databases.push({
name: element.querySelector('.db-name').value,
host: element.querySelector('.db-host').value,
port: parseInt(element.querySelector('.db-port').value) || 3306,
username: element.querySelector('.db-username').value,
password: element.querySelector('.db-password').value
});
});

return databases;
}

function getEndpointsConfig() {
const endpoints = [];
const endpointElements = document.querySelectorAll('.endpoint-item');

endpointElements.forEach(element => {
endpoints.push({
name: element.querySelector('.endpoint-name').value,
url: element.querySelector('.endpoint-url').value,
method: element.querySelector('.endpoint-method').value
});
});

return endpoints;
}

function addDatabase() {
const container = document.getElementById('databaseConfig');
const dbItem = document.createElement('div');
dbItem.className = 'array-item database-item';

dbItem.innerHTML = \`
<button class="remove-button" onclick="removeElement(this.parentElement)">删除</button>
<div class="form-group">
<label>数据库名称:</label>
<input type="text" class="db-name" onchange="updateConfig()">
</div>
<div class="form-group">
<label>主机:</label>
<input type="text" class="db-host" onchange="updateConfig()">
</div>
<div class="form-group">
<label>端口:</label>
<input type="number" class="db-port" value="3306" onchange="updateConfig()">
</div>
<div class="form-group">
<label>用户名:</label>
<input type="text" class="db-username" onchange="updateConfig()">
</div>
<div class="form-group">
<label>密码:</label>
<input type="password" class="db-password" onchange="updateConfig()">
</div>
\`;

container.appendChild(dbItem);
}

function addEndpoint() {
const container = document.getElementById('apiEndpoints');
const endpointItem = document.createElement('div');
endpointItem.className = 'array-item endpoint-item';

endpointItem.innerHTML = \`
<button class="remove-button" onclick="removeElement(this.parentElement)">删除</button>
<div class="form-group">
<label>端点名称:</label>
<input type="text" class="endpoint-name" onchange="updateConfig()">
</div>
<div class="form-group">
<label>URL:</label>
<input type="text" class="endpoint-url" onchange="updateConfig()">
</div>
<div class="form-group">
<label>方法:</label>
<select class="endpoint-method" onchange="updateConfig()">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
\`;

container.appendChild(endpointItem);
}

function removeElement(element) {
element.remove();
updateConfig();
}

function validateConfig() {
vscode.postMessage({
type: 'validate',
content: JSON.stringify(currentConfig, null, 2)
});
}

function saveConfig() {
updateConfig();
}

function loadConfig(configData) {
try {
const config = JSON.parse(configData);
currentConfig = config;

// 填充基础字段
document.getElementById('name').value = config.name || '';
document.getElementById('version').value = config.version || '';
document.getElementById('description').value = config.description || '';
document.getElementById('environment').value = config.environment || 'development';
document.getElementById('debugMode').checked = config.debugMode || false;

// 清空并重新加载数据库配置
document.getElementById('databaseConfig').innerHTML = '';
if (config.databases) {
config.databases.forEach(db => {
addDatabase();
const dbItems = document.querySelectorAll('.database-item');
const lastItem = dbItems[dbItems.length - 1];
lastItem.querySelector('.db-name').value = db.name || '';
lastItem.querySelector('.db-host').value = db.host || '';
lastItem.querySelector('.db-port').value = db.port || 3306;
lastItem.querySelector('.db-username').value = db.username || '';
lastItem.querySelector('.db-password').value = db.password || '';
});
}

// 清空并重新加载API端点配置
document.getElementById('apiEndpoints').innerHTML = '';
if (config.apiEndpoints) {
config.apiEndpoints.forEach(endpoint => {
addEndpoint();
const endpointItems = document.querySelectorAll('.endpoint-item');
const lastItem = endpointItems[endpointItems.length - 1];
lastItem.querySelector('.endpoint-name').value = endpoint.name || '';
lastItem.querySelector('.endpoint-url').value = endpoint.url || '';
lastItem.querySelector('.endpoint-method').value = endpoint.method || 'GET';
});
}

} catch (error) {
console.error('加载配置失败:', error);
}
}

// 监听来自扩展的消息
window.addEventListener('message', event => {
const message = event.data;

switch (message.type) {
case 'load':
loadConfig(message.content);
break;

case 'validationResult':
showValidationResult(message.errors);
break;
}
});

function showValidationResult(errors) {
const resultDiv = document.getElementById('validationResult');

if (errors && errors.length > 0) {
resultDiv.innerHTML = \`
<div class="validation-error">
<strong>配置验证失败:</strong>
<ul>
\${errors.map(error => \`<li>\${error}</li>\`).join('')}
</ul>
</div>
\`;
} else {
resultDiv.innerHTML = \`
<div style="color: var(--vscode-gitDecoration-addedResourceForeground); background-color: rgba(0, 255, 0, 0.1); padding: 5px; border-radius: 3px;">
✅ 配置验证通过
</div>
\`;
}
}
</script>
</body>
</html>`;
}

private updateWebview(webview: vscode.Webview, document: vscode.TextDocument) {
webview.postMessage({
type: 'load',
content: document.getText()
});
}

private updateDocument(document: vscode.TextDocument, content: string) {
const edit = new vscode.WorkspaceEdit();

// 替换整个文档内容
edit.replace(
document.uri,
new vscode.Range(0, 0, document.lineCount, 0),
content
);

return vscode.workspace.applyEdit(edit);
}

private validateConfig(content: string) {
const errors: string[] = [];

try {
const config = JSON.parse(content);

// 验证必需字段
if (!config.name || config.name.trim() === '') {
errors.push('项目名称不能为空');
}

if (!config.version || config.version.trim() === '') {
errors.push('版本号不能为空');
}

// 验证版本号格式
if (config.version && !/^\d+\.\d+\.\d+$/.test(config.version)) {
errors.push('版本号格式应为 x.y.z');
}

// 验证数据库配置
if (config.databases && Array.isArray(config.databases)) {
config.databases.forEach((db: any, index: number) => {
if (!db.name) {
errors.push(`数据库 ${index + 1} 缺少名称`);
}
if (!db.host) {
errors.push(`数据库 ${index + 1} 缺少主机地址`);
}
if (db.port && (db.port < 1 || db.port > 65535)) {
errors.push(`数据库 ${index + 1} 端口号无效`);
}
});
}

// 验证API端点
if (config.apiEndpoints && Array.isArray(config.apiEndpoints)) {
config.apiEndpoints.forEach((endpoint: any, index: number) => {
if (!endpoint.name) {
errors.push(`API端点 ${index + 1} 缺少名称`);
}
if (!endpoint.url) {
errors.push(`API端点 ${index + 1} 缺少URL`);
}
if (endpoint.url && !endpoint.url.startsWith('http')) {
errors.push(`API端点 ${index + 1} URL格式无效`);
}
});
}

} catch (parseError) {
errors.push('JSON格式错误');
}

// 发送验证结果
vscode.window.activeTextEditor?.document.uri.toString();
// 这里需要找到对应的webview来发送消息
// 实际实现中可能需要维护webview的引用
}
}

📚 本阶段总结

通过本阶段学习,你已经掌握了:

AI扩展开发:Language Model集成、Chat Participant、AI工具开发
WebView进阶技术:React集成、数据可视化、复杂交互
Notebook扩展:自定义渲染器、内核集成、交互式文档
自定义编辑器:可视化配置编辑器、文档格式处理
虚拟文档系统:动态内容生成、文件系统抽象

核心技术回顾

  • vscode.lm.selectChatModels() - 选择语言模型
  • vscode.chat.createChatParticipant() - 创建聊天参与者
  • vscode.notebooks.createRendererMessaging() - 创建Notebook渲染器
  • vscode.window.registerCustomEditorProvider() - 注册自定义编辑器
  • webview双向通信 - 扩展与WebView的数据交换

🚀 下一步学习方向

在下一阶段(用户体验设计),我们将学习:

  • 📐 UX设计原则:界面设计指南、交互模式
  • 🎛️ 用户界面最佳实践:响应式设计、无障碍支持
  • 🔔 通知系统设计:消息层次、用户体验优化
  • 📱 跨平台适配:多操作系统支持、性能优化

继续深入学习之前,建议充分练习本阶段的AI集成和WebView开发技术。


相关资源

恭喜你完成了VS Code扩展开发的高级技术阶段!现在你已经具备了开发创新性、专业级扩展的能力。