Compare commits
17 Commits
acf3f9ff37
...
merge/2501
| Author | SHA1 | Date | |
|---|---|---|---|
| 251289a340 | |||
| c22081c5e9 | |||
| cca82c7885 | |||
| e4ff49bade | |||
| ada4806493 | |||
| 3831de2849 | |||
| 0df529c4fd | |||
| 5c53d7f0e9 | |||
| ef2a0dc16e | |||
| 5ce420295b | |||
| 1d7f3d7626 | |||
| 9b0d2d5e01 | |||
| 27e3351b55 | |||
| de3e84aa4e | |||
| e48e822d07 | |||
| 8dc34ee435 | |||
| d8cd86361e |
11
.gitignore
vendored
11
.gitignore
vendored
@ -3,3 +3,14 @@ dist
|
|||||||
node_modules
|
node_modules
|
||||||
.vscode-test/
|
.vscode-test/
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
# waveform_trace 打包产物(exe 太大,通过 Release 发布)
|
||||||
|
tools/waveform_trace/bin/
|
||||||
|
tools/waveform_trace/src/build/
|
||||||
|
tools/waveform_trace/src/dist/
|
||||||
|
tools/waveform_trace/src/*.spec
|
||||||
|
|
||||||
|
# Python 缓存
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|||||||
29
.vscodeignore
Normal file
29
.vscodeignore
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 排除开发文件
|
||||||
|
.vscode/**
|
||||||
|
.git/**
|
||||||
|
.gitignore
|
||||||
|
node_modules/**
|
||||||
|
src/**
|
||||||
|
**/*.ts
|
||||||
|
**/*.map
|
||||||
|
|
||||||
|
# 排除测试文件
|
||||||
|
test/**
|
||||||
|
**/*.test.js
|
||||||
|
|
||||||
|
# 排除文档
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# 排除 waveform_trace Python 源码(只保留 exe)
|
||||||
|
tools/waveform_trace/src/**
|
||||||
|
tools/waveform_trace/build/**
|
||||||
|
tools/waveform_trace/dist/**
|
||||||
|
tools/waveform_trace/build.bat
|
||||||
|
tools/waveform_trace/build.sh
|
||||||
|
|
||||||
|
# 排除打包临时文件
|
||||||
|
**/__pycache__/**
|
||||||
|
**/*.pyc
|
||||||
|
**/*.pyo
|
||||||
|
**/*.spec
|
||||||
@ -101,7 +101,8 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"media",
|
"media",
|
||||||
"tools"
|
"tools",
|
||||||
|
"src/assets"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wavedrom/doppler": "^1.14.0",
|
"@wavedrom/doppler": "^1.14.0",
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import * as vscode from "vscode";
|
|||||||
type Environment = "dev" | "test" | "prod";
|
type Environment = "dev" | "test" | "prod";
|
||||||
|
|
||||||
/** 当前环境 - 修改这里切换环境 */
|
/** 当前环境 - 修改这里切换环境 */
|
||||||
const CURRENT_ENV: Environment = "test";
|
const CURRENT_ENV: Environment = "dev";
|
||||||
|
|
||||||
/** 配置项接口 */
|
/** 配置项接口 */
|
||||||
export interface IccoderConfig {
|
export interface IccoderConfig {
|
||||||
@ -25,7 +25,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
|||||||
/** 本地开发环境 */
|
/** 本地开发环境 */
|
||||||
dev: {
|
dev: {
|
||||||
backendUrl: "http://localhost:2233",
|
backendUrl: "http://localhost:2233",
|
||||||
timeout: 60000,
|
timeout: 300000, // 5分钟,与子智能体超时一致
|
||||||
userId: "default-user",
|
userId: "default-user",
|
||||||
},
|
},
|
||||||
/** 测试服务器环境 */
|
/** 测试服务器环境 */
|
||||||
|
|||||||
@ -82,6 +82,11 @@ export const agentIconSvg = `
|
|||||||
*/
|
*/
|
||||||
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
|
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask 模式图标 SVG
|
||||||
|
*/
|
||||||
|
export const askIconSvg = `<svg t="1767143500000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#8a8a8a"/><path d="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420.4c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8 0-25.6 10.1-49.4 28.4-67.2 18.7-18.2 43.4-28.2 71.6-28.2s52.9 10 71.6 28.2c18.3 17.8 28.4 41.6 28.4 67.2 0 29.5-12.2 55.3-36.2 76.6-23.2 20.6-61.1 45.9-82.2 60.6-17.8 12.4-28.6 32.7-28.6 54.2V640c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-35.8c0-4.1 2.6-7.8 6.5-9.2 31.3-11.6 84.8-40.6 113.8-64.8 42.6-35.6 66.2-83.5 66.2-134.8 0-39.7-17.2-76.4-48.4-103.3z" fill="#8a8a8a"/><path d="M512 716m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#8a8a8a"/></svg>`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存知识库图标 SVG
|
* 保存知识库图标 SVG
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -141,6 +141,9 @@ export async function showICHelperPanel(
|
|||||||
// 切换到当前面板的任务上下文
|
// 切换到当前面板的任务上下文
|
||||||
historyManager.switchToPanelTask(panelId);
|
historyManager.switchToPanelTask(panelId);
|
||||||
|
|
||||||
|
// 显示进度条
|
||||||
|
panel.webview.postMessage({ type: 'showProgress' });
|
||||||
|
|
||||||
handleUserMessage(
|
handleUserMessage(
|
||||||
panel,
|
panel,
|
||||||
message.text,
|
message.text,
|
||||||
@ -278,6 +281,109 @@ export async function showICHelperPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
// 添加文件上下文 - 显示工作区文件列表
|
||||||
|
case "addContextFile":
|
||||||
|
{
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作区所有文件
|
||||||
|
const files = await vscode.workspace.findFiles(
|
||||||
|
"**/*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showWorkspaceFileList",
|
||||||
|
files: files.map((uri) => ({
|
||||||
|
path: uri.fsPath,
|
||||||
|
relativePath: vscode.workspace.asRelativePath(uri),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// 添加文件夹上下文 - 显示工作区文件夹列表
|
||||||
|
case "addContextFolder":
|
||||||
|
{
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (!workspaceFolder) {
|
||||||
|
vscode.window.showWarningMessage("请先打开一个工作区");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作区所有文件夹
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const folders: Array<{ path: string; relativePath: string }> = [];
|
||||||
|
|
||||||
|
function scanFolders(dir: string, baseDir: string) {
|
||||||
|
try {
|
||||||
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
|
||||||
|
const fullPath = path.join(dir, item.name);
|
||||||
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
|
folders.push({ path: fullPath, relativePath });
|
||||||
|
scanFolders(fullPath, baseDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("扫描文件夹失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
|
||||||
|
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "showWorkspaceFolderList",
|
||||||
|
folders: folders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// 添加图片上下文
|
||||||
|
case "addContextImage":
|
||||||
|
{
|
||||||
|
const imageUris = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectMany: true,
|
||||||
|
openLabel: "选择图片",
|
||||||
|
filters: {
|
||||||
|
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (imageUris && imageUris.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "contextImagesSelected",
|
||||||
|
images: imageUris.map((uri) => uri.fsPath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// 添加文档库上下文
|
||||||
|
case "addContextDocument":
|
||||||
|
{
|
||||||
|
const docUris = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectMany: true,
|
||||||
|
openLabel: "选择文档",
|
||||||
|
filters: {
|
||||||
|
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
|
||||||
|
"所有文件": ["*"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (docUris && docUris.length > 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
command: "contextDocumentsSelected",
|
||||||
|
documents: docUris.map((uri) => uri.fsPath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
// 新增:检查工作区状态
|
// 新增:检查工作区状态
|
||||||
case "checkWorkspace":
|
case "checkWorkspace":
|
||||||
const hasWorkspace = !!(
|
const hasWorkspace = !!(
|
||||||
|
|||||||
@ -331,6 +331,11 @@ function dispatchEvent(
|
|||||||
case 'context_usage':
|
case 'context_usage':
|
||||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||||
break;
|
break;
|
||||||
|
case 'heartbeat':
|
||||||
|
// 心跳事件:仅用于保持连接,不需要特殊处理
|
||||||
|
// Node.js req.setTimeout 会在收到数据时自动重置计时器
|
||||||
|
console.log('[SSE] 收到心跳');
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import * as fs from 'fs';
|
|||||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||||
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
|
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
|
||||||
|
import { analyzeVcdFile } from '../utils/vcdParser';
|
||||||
|
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
|
||||||
import {
|
import {
|
||||||
submitToolResult,
|
submitToolResult,
|
||||||
createSuccessResult,
|
createSuccessResult,
|
||||||
@ -79,6 +81,9 @@ export async function executeToolCall(
|
|||||||
case 'waveform_summary':
|
case 'waveform_summary':
|
||||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||||
break;
|
break;
|
||||||
|
case 'waveform_trace':
|
||||||
|
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
|
||||||
|
break;
|
||||||
case 'knowledge_save':
|
case 'knowledge_save':
|
||||||
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
|
||||||
break;
|
break;
|
||||||
@ -300,12 +305,36 @@ async function executeSimulation(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 waveform_summary 工具
|
* 执行 waveform_summary 工具
|
||||||
* TODO: 实现 VCD 波形分析
|
* 解析 VCD 文件并返回波形摘要
|
||||||
*/
|
*/
|
||||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
||||||
// TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件
|
const { vcdPath, signals, checkpoints } = args;
|
||||||
// 目前返回一个占位响应
|
|
||||||
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
|
// 获取工作区路径
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error('请先打开一个工作区');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
|
|
||||||
|
// 解析 VCD 文件路径(支持相对路径)
|
||||||
|
const absolutePath = path.isAbsolute(vcdPath)
|
||||||
|
? vcdPath
|
||||||
|
: path.join(workspacePath, vcdPath);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(absolutePath)) {
|
||||||
|
throw new Error(`VCD 文件不存在: ${vcdPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析检查点时间
|
||||||
|
const checkpoint = checkpoints ? parseInt(checkpoints, 10) : undefined;
|
||||||
|
|
||||||
|
// 调用 VCD 解析器
|
||||||
|
const result = analyzeVcdFile(absolutePath, signals, checkpoint);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -309,6 +309,7 @@ export type ToolName =
|
|||||||
| 'syntax_check'
|
| 'syntax_check'
|
||||||
| 'simulation'
|
| 'simulation'
|
||||||
| 'waveform_summary'
|
| 'waveform_summary'
|
||||||
|
| 'waveform_trace'
|
||||||
| 'knowledge_save'
|
| 'knowledge_save'
|
||||||
| 'knowledge_load';
|
| 'knowledge_load';
|
||||||
|
|
||||||
@ -354,6 +355,18 @@ export interface WaveformSummaryArgs {
|
|||||||
checkpoints?: string;
|
checkpoints?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** waveform_trace 工具参数 */
|
||||||
|
export interface WaveformTraceArgs {
|
||||||
|
/** Verilog 源文件路径(相对于项目根目录) */
|
||||||
|
verilogPath: string;
|
||||||
|
/** VCD 波形文件路径(相对于项目根目录) */
|
||||||
|
vcdPath: string;
|
||||||
|
/** 仿真工具的输出字符串(包含 mismatch 信息) */
|
||||||
|
simOutput: string;
|
||||||
|
/** BFS 回溯层数,默认 2 */
|
||||||
|
traceLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** knowledge_save 工具参数 */
|
/** knowledge_save 工具参数 */
|
||||||
export interface KnowledgeSaveArgs {
|
export interface KnowledgeSaveArgs {
|
||||||
/** 知识图谱 JSON 数据 */
|
/** 知识图谱 JSON 数据 */
|
||||||
@ -374,5 +387,6 @@ export type ToolArgs =
|
|||||||
| SyntaxCheckArgs
|
| SyntaxCheckArgs
|
||||||
| SimulationArgs
|
| SimulationArgs
|
||||||
| WaveformSummaryArgs
|
| WaveformSummaryArgs
|
||||||
|
| WaveformTraceArgs
|
||||||
| KnowledgeSaveArgs
|
| KnowledgeSaveArgs
|
||||||
| KnowledgeLoadArgs;
|
| KnowledgeLoadArgs;
|
||||||
|
|||||||
@ -127,18 +127,20 @@ async function handleUserMessageWithBackend(
|
|||||||
mode?: RunMode,
|
mode?: RunMode,
|
||||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const historyManager = ChatHistoryManager.getInstance();
|
||||||
|
|
||||||
|
// 获取 historyManager 中的 taskId(由 ICHelperPanel 创建)
|
||||||
|
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||||
|
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||||
|
|
||||||
// 创建或复用会话
|
// 创建或复用会话
|
||||||
if (!currentSession || !currentSession.active) {
|
if (!currentSession || !currentSession.active) {
|
||||||
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
||||||
// 保存 taskId 用于后续操作(如压缩)
|
// 保存 taskId 用于后续操作(如压缩)
|
||||||
lastTaskId = currentSession.getTaskId();
|
lastTaskId = currentSession.getTaskId();
|
||||||
if (reuseTaskId) {
|
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
||||||
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyManager = ChatHistoryManager.getInstance();
|
|
||||||
|
|
||||||
// 显示状态栏
|
// 显示状态栏
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: "updateStatus",
|
command: "updateStatus",
|
||||||
@ -196,10 +198,6 @@ async function handleUserMessageWithBackend(
|
|||||||
|
|
||||||
// 最后一次发送完整的段落
|
// 最后一次发送完整的段落
|
||||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||||
console.log(
|
|
||||||
"[MessageHandler] segments 内容:",
|
|
||||||
JSON.stringify(segments)
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await panel.webview.postMessage({
|
const result = await panel.webview.postMessage({
|
||||||
command: "updateSegments",
|
command: "updateSegments",
|
||||||
|
|||||||
467
src/utils/vcdParser.ts
Normal file
467
src/utils/vcdParser.ts
Normal file
@ -0,0 +1,467 @@
|
|||||||
|
/**
|
||||||
|
* VCD (Value Change Dump) 解析器
|
||||||
|
* 纯 TypeScript 实现,参照 VerilogCoder 项目格式
|
||||||
|
*
|
||||||
|
* @deprecated 当前未使用,保留备用
|
||||||
|
* 目前使用 waveformTracer.ts 调用 Python 打包的 waveform_trace.exe
|
||||||
|
* 未来可能用此文件替换 Python 实现
|
||||||
|
*/
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 信号定义 */
|
||||||
|
export interface VcdSignal {
|
||||||
|
name: string; // 完整路径名,如 "tb.top_module.data"
|
||||||
|
shortName: string; // 短名,如 "data"
|
||||||
|
symbolId: string; // VCD 符号 ID,如 "!", "#"
|
||||||
|
width: number; // 位宽
|
||||||
|
varType: string; // 变量类型:wire, reg
|
||||||
|
module: string; // 所属模块
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间-值对 */
|
||||||
|
export interface TimeValue {
|
||||||
|
time: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 信号波形数据 */
|
||||||
|
export interface SignalWaveform {
|
||||||
|
signal: VcdSignal;
|
||||||
|
changes: TimeValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** VCD 解析结果 */
|
||||||
|
export interface VcdData {
|
||||||
|
date?: string;
|
||||||
|
version?: string;
|
||||||
|
timescale: string;
|
||||||
|
endTime: number;
|
||||||
|
signals: Map<string, VcdSignal>; // symbolId -> signal
|
||||||
|
waveforms: Map<string, TimeValue[]>; // symbolId -> changes
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mismatch 信息 */
|
||||||
|
export interface MismatchInfo {
|
||||||
|
time: number;
|
||||||
|
signal: string;
|
||||||
|
dutValue: string;
|
||||||
|
refValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== VCD 解析器 ====================
|
||||||
|
|
||||||
|
export class VcdParser {
|
||||||
|
private signals: Map<string, VcdSignal> = new Map();
|
||||||
|
private waveforms: Map<string, TimeValue[]> = new Map();
|
||||||
|
private scopeStack: string[] = [];
|
||||||
|
private timescale: string = '1ns';
|
||||||
|
private currentTime: number = 0;
|
||||||
|
private endTime: number = 0;
|
||||||
|
private date?: string;
|
||||||
|
private version?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 VCD 文件
|
||||||
|
*/
|
||||||
|
parse(filePath: string): VcdData {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
return this.parseContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 VCD 内容
|
||||||
|
*/
|
||||||
|
parseContent(content: string): VcdData {
|
||||||
|
// 预处理:将多行指令合并成单行
|
||||||
|
const normalizedContent = this.normalizeVcdContent(content);
|
||||||
|
const lines = normalizedContent.split('\n');
|
||||||
|
let inDefinitions = true;
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
if (inDefinitions) {
|
||||||
|
// 解析定义区
|
||||||
|
if (line.startsWith('$enddefinitions')) {
|
||||||
|
inDefinitions = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.parseDefinition(line);
|
||||||
|
} else {
|
||||||
|
// 解析数据区
|
||||||
|
this.parseValueChange(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: this.date,
|
||||||
|
version: this.version,
|
||||||
|
timescale: this.timescale,
|
||||||
|
endTime: this.endTime,
|
||||||
|
signals: this.signals,
|
||||||
|
waveforms: this.waveforms
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDefinition(line: string): void {
|
||||||
|
if (line.startsWith('$date')) {
|
||||||
|
this.date = this.extractValue(line);
|
||||||
|
} else if (line.startsWith('$version')) {
|
||||||
|
this.version = this.extractValue(line);
|
||||||
|
} else if (line.startsWith('$timescale')) {
|
||||||
|
this.timescale = this.extractValue(line) || '1ns';
|
||||||
|
} else if (line.startsWith('$scope')) {
|
||||||
|
const match = line.match(/\$scope\s+\w+\s+(\S+)/);
|
||||||
|
if (match) {
|
||||||
|
this.scopeStack.push(match[1]);
|
||||||
|
}
|
||||||
|
} else if (line.startsWith('$upscope')) {
|
||||||
|
this.scopeStack.pop();
|
||||||
|
} else if (line.startsWith('$var')) {
|
||||||
|
this.parseVariable(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseVariable(line: string): void {
|
||||||
|
// $var wire 8 # data [7:0] $end
|
||||||
|
// $var reg 1 ! clk $end
|
||||||
|
const match = line.match(/\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+(\S+)/);
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
const [, varType, widthStr, symbolId, name] = match;
|
||||||
|
const width = parseInt(widthStr, 10);
|
||||||
|
const module = this.scopeStack.join('.');
|
||||||
|
const fullName = module ? `${module}.${name}` : name;
|
||||||
|
|
||||||
|
const signal: VcdSignal = {
|
||||||
|
name: fullName,
|
||||||
|
shortName: name.replace(/\[\d+:\d+\]/, ''), // 移除位宽标注
|
||||||
|
symbolId,
|
||||||
|
width,
|
||||||
|
varType,
|
||||||
|
module
|
||||||
|
};
|
||||||
|
|
||||||
|
this.signals.set(symbolId, signal);
|
||||||
|
this.waveforms.set(symbolId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseValueChange(line: string): void {
|
||||||
|
if (line.startsWith('#')) {
|
||||||
|
// 时间戳: #100
|
||||||
|
this.currentTime = parseInt(line.substring(1), 10);
|
||||||
|
this.endTime = Math.max(this.endTime, this.currentTime);
|
||||||
|
} else if (line.startsWith('b') || line.startsWith('B')) {
|
||||||
|
// 多位值: b10101010 #
|
||||||
|
const spaceIdx = line.indexOf(' ');
|
||||||
|
if (spaceIdx > 0) {
|
||||||
|
const value = line.substring(1, spaceIdx);
|
||||||
|
const symbolId = line.substring(spaceIdx + 1).trim();
|
||||||
|
this.addChange(symbolId, value);
|
||||||
|
}
|
||||||
|
} else if (line.length >= 2 && !line.startsWith('$')) {
|
||||||
|
// 单位值: 0! 或 1# 或 x$
|
||||||
|
const value = line[0];
|
||||||
|
const symbolId = line.substring(1).trim();
|
||||||
|
if (symbolId && this.signals.has(symbolId)) {
|
||||||
|
this.addChange(symbolId, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addChange(symbolId: string, value: string): void {
|
||||||
|
const changes = this.waveforms.get(symbolId);
|
||||||
|
if (changes) {
|
||||||
|
changes.push({ time: this.currentTime, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractValue(line: string): string {
|
||||||
|
// 提取 $xxx value $end 中的 value
|
||||||
|
const match = line.match(/\$\w+\s+(.+?)\s*\$end/);
|
||||||
|
return match ? match[1].trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预处理 VCD 内容,将多行指令合并成单行
|
||||||
|
*/
|
||||||
|
private normalizeVcdContent(content: string): string {
|
||||||
|
// 将多行 $xxx ... $end 合并成单行
|
||||||
|
return content.replace(/(\$\w+)\s*\n\s*([^\$]+?)\s*\n\s*(\$end)/g, '$1 $2 $3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 波形分析工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二进制字符串转十六进制
|
||||||
|
*/
|
||||||
|
export function binaryToHex(binary: string): string {
|
||||||
|
if (binary === 'x' || binary === 'X' || binary.includes('x')) {
|
||||||
|
return 'xx';
|
||||||
|
}
|
||||||
|
if (binary === 'z' || binary === 'Z' || binary.includes('z')) {
|
||||||
|
return 'zz';
|
||||||
|
}
|
||||||
|
if (binary.length <= 1) {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
// 补齐到 4 的倍数
|
||||||
|
const padded = binary.padStart(Math.ceil(binary.length / 4) * 4, '0');
|
||||||
|
let hex = '';
|
||||||
|
for (let i = 0; i < padded.length; i += 4) {
|
||||||
|
hex += parseInt(padded.substring(i, i + 4), 2).toString(16);
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取信号在指定时间的值
|
||||||
|
*/
|
||||||
|
export function getValueAtTime(
|
||||||
|
changes: TimeValue[],
|
||||||
|
time: number
|
||||||
|
): string {
|
||||||
|
let value = 'x';
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.time <= time) {
|
||||||
|
value = change.value;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找 DUT 和 REF 信号的第一个 mismatch
|
||||||
|
*/
|
||||||
|
export function findFirstMismatch(
|
||||||
|
vcdData: VcdData,
|
||||||
|
dutSignals: string[],
|
||||||
|
refSignals: string[]
|
||||||
|
): MismatchInfo | null {
|
||||||
|
// 收集所有时间点
|
||||||
|
const allTimes = new Set<number>();
|
||||||
|
for (const changes of vcdData.waveforms.values()) {
|
||||||
|
for (const c of changes) {
|
||||||
|
allTimes.add(c.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sortedTimes = Array.from(allTimes).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// 按信号名匹配 DUT 和 REF
|
||||||
|
for (const time of sortedTimes) {
|
||||||
|
for (let i = 0; i < dutSignals.length; i++) {
|
||||||
|
const dutSig = findSignalByName(vcdData, dutSignals[i]);
|
||||||
|
const refSig = findSignalByName(vcdData, refSignals[i]);
|
||||||
|
|
||||||
|
if (!dutSig || !refSig) continue;
|
||||||
|
|
||||||
|
const dutChanges = vcdData.waveforms.get(dutSig.symbolId) || [];
|
||||||
|
const refChanges = vcdData.waveforms.get(refSig.symbolId) || [];
|
||||||
|
|
||||||
|
const dutVal = getValueAtTime(dutChanges, time);
|
||||||
|
const refVal = getValueAtTime(refChanges, time);
|
||||||
|
|
||||||
|
// 跳过未知值
|
||||||
|
if (dutVal.includes('x') || refVal.includes('x')) continue;
|
||||||
|
|
||||||
|
if (dutVal !== refVal) {
|
||||||
|
return {
|
||||||
|
time,
|
||||||
|
signal: dutSig.shortName,
|
||||||
|
dutValue: binaryToHex(dutVal),
|
||||||
|
refValue: binaryToHex(refVal)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按名称查找信号
|
||||||
|
*/
|
||||||
|
function findSignalByName(vcdData: VcdData, name: string): VcdSignal | null {
|
||||||
|
for (const signal of vcdData.signals.values()) {
|
||||||
|
if (signal.name.endsWith(name) || signal.shortName === name) {
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成波形表格(参照 VerilogCoder 格式)
|
||||||
|
*/
|
||||||
|
export function generateWaveformTable(
|
||||||
|
vcdData: VcdData,
|
||||||
|
signalNames: string[],
|
||||||
|
startTime: number = 0,
|
||||||
|
endTime?: number,
|
||||||
|
windowSize: number = 20
|
||||||
|
): string {
|
||||||
|
const actualEndTime = endTime ?? vcdData.endTime;
|
||||||
|
|
||||||
|
// 查找信号
|
||||||
|
const signals: VcdSignal[] = [];
|
||||||
|
for (const name of signalNames) {
|
||||||
|
const sig = findSignalByName(vcdData, name);
|
||||||
|
if (sig) signals.push(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signals.length === 0) {
|
||||||
|
return '未找到指定信号';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集时间点
|
||||||
|
const times = new Set<number>();
|
||||||
|
for (const sig of signals) {
|
||||||
|
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||||
|
for (const c of changes) {
|
||||||
|
if (c.time >= startTime && c.time <= actualEndTime) {
|
||||||
|
times.add(c.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortedTimes = Array.from(times).sort((a, b) => a - b);
|
||||||
|
if (sortedTimes.length > windowSize) {
|
||||||
|
sortedTimes = sortedTimes.slice(0, windowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成表头
|
||||||
|
const headers = ['time(ns)', ...signals.map(s => s.shortName)];
|
||||||
|
const colWidths = headers.map(h => Math.max(h.length, 8));
|
||||||
|
|
||||||
|
let table = '### Waveform Trace ###\n';
|
||||||
|
table += headers.map((h, i) => h.padEnd(colWidths[i])).join(' ') + '\n';
|
||||||
|
table += colWidths.map(w => '─'.repeat(w)).join('──') + '\n';
|
||||||
|
|
||||||
|
// 生成数据行
|
||||||
|
for (const time of sortedTimes) {
|
||||||
|
const row = [time.toString()];
|
||||||
|
for (const sig of signals) {
|
||||||
|
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||||
|
const val = getValueAtTime(changes, time);
|
||||||
|
row.push(binaryToHex(val));
|
||||||
|
}
|
||||||
|
table += row.map((v, i) => v.padEnd(colWidths[i])).join(' ') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
table += '### Waveform Trace End ###\n';
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取信号显示名称(模块.信号名[位宽])
|
||||||
|
*/
|
||||||
|
function getSignalDisplayName(sig: VcdSignal): string {
|
||||||
|
const moduleParts = sig.module.split('.');
|
||||||
|
const moduleShort = moduleParts[moduleParts.length - 1] || '';
|
||||||
|
const bitInfo = sig.width > 1 ? `[${sig.width - 1}:0]` : '';
|
||||||
|
return moduleShort ? `${moduleShort}.${sig.shortName}${bitInfo}` : `${sig.shortName}${bitInfo}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成变化日志格式(只记录信号变化)
|
||||||
|
*/
|
||||||
|
export function generateChangeLog(vcdData: VcdData): string {
|
||||||
|
// 筛选信号(排除 parameter)
|
||||||
|
const signals: VcdSignal[] = [];
|
||||||
|
for (const sig of vcdData.signals.values()) {
|
||||||
|
if (sig.varType !== 'parameter') {
|
||||||
|
signals.push(sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有时间点
|
||||||
|
const times = new Set<number>();
|
||||||
|
for (const sig of signals) {
|
||||||
|
const changes = vcdData.waveforms.get(sig.symbolId) || [];
|
||||||
|
for (const c of changes) {
|
||||||
|
times.add(c.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sortedTimes = Array.from(times).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// 记录每个信号的上一个值
|
||||||
|
const lastValues = new Map<string, string | null>();
|
||||||
|
for (const sig of signals) {
|
||||||
|
lastValues.set(sig.symbolId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = '';
|
||||||
|
|
||||||
|
for (const time of sortedTimes) {
|
||||||
|
const changes: string[] = [];
|
||||||
|
|
||||||
|
for (const sig of signals) {
|
||||||
|
const waveform = vcdData.waveforms.get(sig.symbolId) || [];
|
||||||
|
const currentVal = binaryToHex(getValueAtTime(waveform, time));
|
||||||
|
const lastVal = lastValues.get(sig.symbolId);
|
||||||
|
|
||||||
|
if (lastVal === null) {
|
||||||
|
changes.push(`${getSignalDisplayName(sig)}=${currentVal}`);
|
||||||
|
} else if (currentVal !== lastVal) {
|
||||||
|
changes.push(`${getSignalDisplayName(sig)} ${lastVal}→${currentVal}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValues.set(sig.symbolId, currentVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.length > 0) {
|
||||||
|
log += `#${time}: ${changes.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析 VCD 文件(主入口)
|
||||||
|
*/
|
||||||
|
export function analyzeVcdFile(
|
||||||
|
filePath: string,
|
||||||
|
signalFilter?: string,
|
||||||
|
checkpoint?: number
|
||||||
|
): string {
|
||||||
|
// 解析 VCD
|
||||||
|
const parser = new VcdParser();
|
||||||
|
const vcdData = parser.parse(filePath);
|
||||||
|
|
||||||
|
// 解析信号过滤器
|
||||||
|
const signalNames = signalFilter
|
||||||
|
? signalFilter.split(',').map(s => s.trim())
|
||||||
|
: Array.from(vcdData.signals.values()).map(s => s.shortName);
|
||||||
|
|
||||||
|
// 生成摘要
|
||||||
|
let result = `=== VCD 波形分析 ===\n`;
|
||||||
|
result += `文件: ${path.basename(filePath)}\n`;
|
||||||
|
result += `时间单位: ${vcdData.timescale}\n`;
|
||||||
|
result += `仿真时长: 0 - ${vcdData.endTime}${vcdData.timescale}\n\n`;
|
||||||
|
|
||||||
|
// 信号列表
|
||||||
|
result += `--- 信号列表 (${vcdData.signals.size} 个) ---\n`;
|
||||||
|
let idx = 1;
|
||||||
|
for (const sig of vcdData.signals.values()) {
|
||||||
|
if (idx <= 10) {
|
||||||
|
result += `${idx}. ${sig.shortName} (${sig.width}-bit, ${sig.varType})\n`;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (vcdData.signals.size > 10) {
|
||||||
|
result += `... 还有 ${vcdData.signals.size - 10} 个信号\n`;
|
||||||
|
}
|
||||||
|
result += '\n';
|
||||||
|
|
||||||
|
// 变化日志
|
||||||
|
result += generateChangeLog(vcdData);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
145
src/utils/waveformTracer.ts
Normal file
145
src/utils/waveformTracer.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* 波形追踪工具
|
||||||
|
* 调用 PyInstaller 打包的 waveform_trace 可执行文件
|
||||||
|
*/
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 波形追踪参数
|
||||||
|
*/
|
||||||
|
export interface WaveformTraceArgs {
|
||||||
|
/** Verilog 源文件路径(相对于项目根目录) */
|
||||||
|
verilogPath: string;
|
||||||
|
/** VCD 波形文件路径(相对于项目根目录) */
|
||||||
|
vcdPath: string;
|
||||||
|
/** 仿真工具的输出字符串(包含 mismatch 信息) */
|
||||||
|
simOutput: string;
|
||||||
|
/** BFS 回溯层数,默认 2 */
|
||||||
|
traceLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行波形追踪
|
||||||
|
* @param args 追踪参数
|
||||||
|
* @param context 执行上下文
|
||||||
|
* @returns 追踪结果字符串
|
||||||
|
*/
|
||||||
|
export async function executeWaveformTrace(
|
||||||
|
args: WaveformTraceArgs,
|
||||||
|
context: { extensionPath: string }
|
||||||
|
): Promise<string> {
|
||||||
|
// 获取可执行文件路径
|
||||||
|
const tracerPath = getWaveformTracerPath(context.extensionPath);
|
||||||
|
|
||||||
|
// 检查可执行文件是否存在
|
||||||
|
if (!fs.existsSync(tracerPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`waveform_trace 工具未安装: ${tracerPath}\n` +
|
||||||
|
'请确保插件包含 tools/waveform_trace/bin/ 目录'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作区路径
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
|
throw new Error('请先打开一个工作区');
|
||||||
|
}
|
||||||
|
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||||
|
|
||||||
|
// 解析路径(支持相对路径)
|
||||||
|
const verilogAbsPath = path.isAbsolute(args.verilogPath)
|
||||||
|
? args.verilogPath
|
||||||
|
: path.join(workspacePath, args.verilogPath);
|
||||||
|
const vcdAbsPath = path.isAbsolute(args.vcdPath)
|
||||||
|
? args.vcdPath
|
||||||
|
: path.join(workspacePath, args.vcdPath);
|
||||||
|
|
||||||
|
// 验证文件存在
|
||||||
|
if (!fs.existsSync(verilogAbsPath)) {
|
||||||
|
throw new Error(`Verilog 文件不存在: ${args.verilogPath}`);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(vcdAbsPath)) {
|
||||||
|
throw new Error(`VCD 文件不存在: ${args.vcdPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用可执行文件
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(tracerPath, [
|
||||||
|
'--verilog', verilogAbsPath,
|
||||||
|
'--vcd', vcdAbsPath,
|
||||||
|
'--sim-output', args.simOutput,
|
||||||
|
'--trace-level', String(args.traceLevel || 2),
|
||||||
|
'--output-format', 'text'
|
||||||
|
], {
|
||||||
|
windowsHide: true,
|
||||||
|
cwd: workspacePath,
|
||||||
|
shell: false
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout.on('data', (data: Buffer) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (data: Buffer) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code: number | null) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(stdout);
|
||||||
|
} else {
|
||||||
|
reject(new Error(
|
||||||
|
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error: Error) => {
|
||||||
|
reject(new Error(`waveform_trace 启动失败: ${error.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 waveform_trace 可执行文件路径
|
||||||
|
*/
|
||||||
|
function getWaveformTracerPath(extensionPath: string): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
let binName = 'waveform_trace';
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
binName = 'waveform_trace.exe';
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(extensionPath, 'tools', 'waveform_trace', 'bin', binName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 waveform_trace 工具是否可用
|
||||||
|
*/
|
||||||
|
export function checkWaveformTraceAvailable(extensionPath: string): {
|
||||||
|
available: boolean;
|
||||||
|
message: string;
|
||||||
|
path?: string;
|
||||||
|
} {
|
||||||
|
const tracerPath = getWaveformTracerPath(extensionPath);
|
||||||
|
|
||||||
|
if (fs.existsSync(tracerPath)) {
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
message: 'waveform_trace 工具可用',
|
||||||
|
path: tracerPath
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
message: `waveform_trace 工具未找到: ${tracerPath}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -160,13 +160,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 处理侧边栏的消息
|
// 处理侧边栏的消息
|
||||||
webviewView.webview.onDidReceiveMessage((message) => {
|
webviewView.webview.onDidReceiveMessage(
|
||||||
if (message.command === "openChat") {
|
(message) => {
|
||||||
vscode.commands.executeCommand("ic-coder.openChat");
|
if (message.command === "openChat") {
|
||||||
} else if (message.command === "login") {
|
vscode.commands.executeCommand("ic-coder.openChat");
|
||||||
vscode.commands.executeCommand("ic-coder.login");
|
} else if (message.command === "login") {
|
||||||
}
|
vscode.commands.executeCommand("ic-coder.login");
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
this.context.subscriptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWebviewContent(
|
private getWebviewContent(
|
||||||
|
|||||||
@ -96,6 +96,27 @@ export function getAgentCardStyles(): string {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
/* 低调显示的工具调用样式 */
|
||||||
|
.agent-step.low-profile {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: transparent;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.agent-step.low-profile .step-icon {
|
||||||
|
opacity: 0.4;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.agent-step.low-profile .step-name {
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.agent-step.low-profile .step-result {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,14 +140,18 @@ export function getAgentCardScript(): string {
|
|||||||
'queryKnowledgeSummary': '查询知识摘要',
|
'queryKnowledgeSummary': '查询知识摘要',
|
||||||
'queryRules': '查询规则',
|
'queryRules': '查询规则',
|
||||||
'setModule': '设置模块',
|
'setModule': '设置模块',
|
||||||
'addSignal': '添加信号',
|
'addSignal': '正在分析信号定义',
|
||||||
'addSignalExample': '添加信号示例',
|
'addSignalExample': '正在处理信号示例',
|
||||||
'validateKnowledgeGraph': '验证知识图谱',
|
'validateKnowledgeGraph': '验证知识图谱',
|
||||||
'querySignals': '查询信号',
|
'querySignals': '查询信号',
|
||||||
'addPlan': '添加计划',
|
'addPlan': '添加计划',
|
||||||
'addEdge': '添加边',
|
'addEdge': '添加边',
|
||||||
'showPlan': '显示计划',
|
'showPlan': '显示计划',
|
||||||
'spawnExplorer': '代码探索'
|
'spawnExplorer': '代码探索',
|
||||||
|
'spawnDebugger': '波形调试',
|
||||||
|
'queryByBFS': 'BFS查询',
|
||||||
|
'queryStateTransitions': '查询状态转移',
|
||||||
|
'addStateTransition': '添加状态转移'
|
||||||
};
|
};
|
||||||
return toolNameMap[toolName] || toolName;
|
return toolNameMap[toolName] || toolName;
|
||||||
}
|
}
|
||||||
@ -147,7 +172,16 @@ export function getAgentCardScript(): string {
|
|||||||
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||||
const displayName = getAgentToolDisplayName(step.toolName);
|
const displayName = getAgentToolDisplayName(step.toolName);
|
||||||
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
||||||
return \`<div class="agent-step"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
// 为技术性工具调用添加低调样式(用户看不懂的)
|
||||||
|
const lowProfileTools = [
|
||||||
|
'knowledge_save', 'knowledge_load',
|
||||||
|
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||||
|
'setModule', 'addSignal', 'addSignalExample',
|
||||||
|
'validateKnowledgeGraph', 'addPlan', 'addEdge',
|
||||||
|
'showPlan', 'spawnExplorer'
|
||||||
|
];
|
||||||
|
const stepClass = lowProfileTools.includes(step.toolName) ? 'agent-step low-profile' : 'agent-step';
|
||||||
|
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
|
|||||||
@ -8,6 +8,12 @@
|
|||||||
* - Agent: 智能体自主,自动执行大部分操作
|
* - Agent: 智能体自主,自动执行大部分操作
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
plannerIconSvg,
|
||||||
|
askIconSvg,
|
||||||
|
agentIconSvg,
|
||||||
|
} from "../constants/toolIcons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取模式选择器的 HTML 内容
|
* 获取模式选择器的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -23,16 +29,25 @@ export function getModeSelectorContent(): string {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mode-dropdown" id="modeDropdown">
|
<div class="mode-dropdown" id="modeDropdown">
|
||||||
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
||||||
<span class="mode-option-label">Plan</span>
|
<div class="mode-option-header">
|
||||||
<span class="mode-option-desc">只读模式</span>
|
<span class="mode-option-icon">${plannerIconSvg}</span>
|
||||||
|
<span class="mode-option-label">Plan</span>
|
||||||
|
</div>
|
||||||
|
<span class="mode-option-desc">仅根据需求生成设计文档,之后由用户决定下一步,可以提高工程质量</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
|
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
|
||||||
<span class="mode-option-label">Ask</span>
|
<div class="mode-option-header">
|
||||||
<span class="mode-option-desc">逐个确认</span>
|
<span class="mode-option-icon">${askIconSvg}</span>
|
||||||
|
<span class="mode-option-label">Ask</span>
|
||||||
|
</div>
|
||||||
|
<span class="mode-option-desc">仅给与智能体读权限,用于依据项目回答用户问题,或者与用户进行探讨</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
|
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
|
||||||
<span class="mode-option-label">Agent</span>
|
<div class="mode-option-header">
|
||||||
<span class="mode-option-desc">智能体自主</span>
|
<span class="mode-option-icon">${agentIconSvg}</span>
|
||||||
|
<span class="mode-option-label">Agent</span>
|
||||||
|
</div>
|
||||||
|
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -83,7 +98,8 @@ export function getModeSelectorStyles(): string {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 2px);
|
bottom: calc(100% + 2px);
|
||||||
left: 0;
|
left: 0;
|
||||||
min-width: 140px;
|
min-width: 200px;
|
||||||
|
max-width: 300px;
|
||||||
background: var(--vscode-dropdown-background);
|
background: var(--vscode-dropdown-background);
|
||||||
border: 1px solid var(--vscode-dropdown-border);
|
border: 1px solid var(--vscode-dropdown-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -98,13 +114,12 @@ export function getModeSelectorStyles(): string {
|
|||||||
/* 模式选择器的选项样式 */
|
/* 模式选择器的选项样式 */
|
||||||
.mode-option {
|
.mode-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.mode-option:hover {
|
.mode-option:hover {
|
||||||
background: rgba(128, 128, 128, 0.3);
|
background: rgba(128, 128, 128, 0.3);
|
||||||
@ -112,13 +127,31 @@ export function getModeSelectorStyles(): string {
|
|||||||
.mode-option.selected {
|
.mode-option.selected {
|
||||||
background: rgba(64, 158, 255, 0.2);
|
background: rgba(64, 158, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
.mode-option-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.mode-option-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mode-option-icon svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.mode-option-label {
|
.mode-option-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.mode-option-desc {
|
.mode-option-desc {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
margin-left: 12px;
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -157,9 +190,9 @@ export function getModeSelectorScript(): string {
|
|||||||
// 更新 tooltip
|
// 更新 tooltip
|
||||||
if (modeTooltip) {
|
if (modeTooltip) {
|
||||||
const tooltipMap = {
|
const tooltipMap = {
|
||||||
'plan': '只读模式 - 只能查询分析',
|
'plan': 'plan模式',
|
||||||
'ask': '逐个确认 - 每个写操作需确认',
|
'ask': 'ask模式',
|
||||||
'agent': '智能体自主模式','
|
'agent': 'agent模式'
|
||||||
};
|
};
|
||||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,14 +7,78 @@
|
|||||||
*/
|
*/
|
||||||
export function getContextButtonContent(): string {
|
export function getContextButtonContent(): string {
|
||||||
return `
|
return `
|
||||||
<div class="tooltip">
|
<div class="context-selector-wrapper">
|
||||||
<button class="add-context-button" onclick="handleAddContext()">
|
<div class="tooltip">
|
||||||
<svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200">
|
<button class="add-context-button" onclick="toggleContextMenu()">
|
||||||
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
<svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200">
|
||||||
</svg>
|
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
|
||||||
<span class="add-context-label">添加上下文</span>
|
</svg>
|
||||||
</button>
|
<span class="add-context-label">添加上下文</span>
|
||||||
<span class="tooltiptext">添加文件或代码片段作为上下文</span>
|
<svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上拉菜单 -->
|
||||||
|
<div class="context-menu" id="contextMenu">
|
||||||
|
<!-- 主菜单 -->
|
||||||
|
<div class="context-menu-main" id="contextMenuMain">
|
||||||
|
<div class="context-menu-item" onclick="showFileList()">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span>文件</span>
|
||||||
|
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" onclick="showFolderList()">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span>文件夹</span>
|
||||||
|
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" onclick="handleAddImage()">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span>图片</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" onclick="handleAddDocument()">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span>文档库</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件/文件夹列表视图 -->
|
||||||
|
<div class="context-menu-list" id="contextMenuList" style="display: none;">
|
||||||
|
<div class="context-menu-list-header">
|
||||||
|
<button class="context-menu-back" onclick="backToMainMenu()">
|
||||||
|
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8c-16.4 12.8-16.4 37.5 0 50.3l450.8 352.1c5.3 4.1 12.9 0.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span id="contextMenuListTitle">选择文件</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-list-body" id="contextMenuListBody">
|
||||||
|
<!-- 动态加载列表 -->
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-list-footer">
|
||||||
|
<input type="text" id="contextMenuSearch" placeholder="搜索..." />
|
||||||
|
<div class="context-menu-list-actions">
|
||||||
|
<span id="contextMenuListCount">已选择 0 项</span>
|
||||||
|
<button class="primary" onclick="confirmSelection()">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -24,6 +88,12 @@ export function getContextButtonContent(): string {
|
|||||||
*/
|
*/
|
||||||
export function getContextButtonStyles(): string {
|
export function getContextButtonStyles(): string {
|
||||||
return `
|
return `
|
||||||
|
/* 上下文选择器容器 */
|
||||||
|
.context-selector-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
/* 添加上下文按钮样式 */
|
/* 添加上下文按钮样式 */
|
||||||
.add-context-button {
|
.add-context-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -45,15 +115,218 @@ export function getContextButtonStyles(): string {
|
|||||||
border-color: var(--vscode-focusBorder);
|
border-color: var(--vscode-focusBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-context-button svg {
|
.add-context-button svg.icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-context-button .dropdown-arrow {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-context-button.active .dropdown-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.add-context-label {
|
.add-context-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 上拉菜单样式 */
|
||||||
|
.context-menu {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
background: var(--vscode-dropdown-background);
|
||||||
|
border: 1px solid var(--vscode-dropdown-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
min-width: 180px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu.show {
|
||||||
|
display: block;
|
||||||
|
animation: slideUp 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item span {
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item .arrow-right {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表视图样式 */
|
||||||
|
.context-menu-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-back {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-back:hover {
|
||||||
|
background: var(--vscode-toolbar-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-back svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-header span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-item.selected {
|
||||||
|
background: var(--vscode-list-activeSelectionBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-item input[type="checkbox"] {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-item label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-footer {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-footer input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--vscode-input-foreground);
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-footer span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-footer button {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-list-footer button:hover {
|
||||||
|
background: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,10 +335,174 @@ export function getContextButtonStyles(): string {
|
|||||||
*/
|
*/
|
||||||
export function getContextButtonScript(): string {
|
export function getContextButtonScript(): string {
|
||||||
return `
|
return `
|
||||||
// 添加上下文处理函数
|
// 上下文菜单状态
|
||||||
function handleAddContext() {
|
let currentListData = [];
|
||||||
// 发送添加上下文请求到扩展
|
let currentListType = '';
|
||||||
vscode.postMessage({ command: 'addContext' });
|
let selectedItems = new Set();
|
||||||
|
|
||||||
|
// 切换上下文菜单显示/隐藏
|
||||||
|
function toggleContextMenu() {
|
||||||
|
const menu = document.getElementById('contextMenu');
|
||||||
|
const button = document.querySelector('.add-context-button');
|
||||||
|
|
||||||
|
if (menu && button) {
|
||||||
|
const isShown = menu.classList.contains('show');
|
||||||
|
|
||||||
|
if (isShown) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
button.classList.remove('active');
|
||||||
|
backToMainMenu(); // 关闭时回到主菜单
|
||||||
|
} else {
|
||||||
|
menu.classList.add('show');
|
||||||
|
button.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭菜单
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const wrapper = document.querySelector('.context-selector-wrapper');
|
||||||
|
const menu = document.getElementById('contextMenu');
|
||||||
|
const button = document.querySelector('.add-context-button');
|
||||||
|
|
||||||
|
if (wrapper && menu && button && !wrapper.contains(event.target)) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
button.classList.remove('active');
|
||||||
|
backToMainMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示文件列表
|
||||||
|
function showFileList() {
|
||||||
|
vscode.postMessage({ command: 'addContextFile' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示文件夹列表
|
||||||
|
function showFolderList() {
|
||||||
|
vscode.postMessage({ command: 'addContextFolder' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回主菜单
|
||||||
|
function backToMainMenu() {
|
||||||
|
const mainMenu = document.getElementById('contextMenuMain');
|
||||||
|
const listView = document.getElementById('contextMenuList');
|
||||||
|
|
||||||
|
if (mainMenu && listView) {
|
||||||
|
mainMenu.style.display = 'block';
|
||||||
|
listView.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedItems.clear();
|
||||||
|
currentListData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到列表视图
|
||||||
|
function switchToListView(title, type, data) {
|
||||||
|
const mainMenu = document.getElementById('contextMenuMain');
|
||||||
|
const listView = document.getElementById('contextMenuList');
|
||||||
|
const titleEl = document.getElementById('contextMenuListTitle');
|
||||||
|
|
||||||
|
if (mainMenu && listView && titleEl) {
|
||||||
|
mainMenu.style.display = 'none';
|
||||||
|
listView.style.display = 'flex';
|
||||||
|
titleEl.textContent = title;
|
||||||
|
|
||||||
|
currentListType = type;
|
||||||
|
currentListData = data;
|
||||||
|
selectedItems.clear();
|
||||||
|
|
||||||
|
renderList(data);
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染列表
|
||||||
|
function renderList(data) {
|
||||||
|
const body = document.getElementById('contextMenuListBody');
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
body.innerHTML = data.map((item, index) => \`
|
||||||
|
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
|
||||||
|
<input type="checkbox" id="item-\${index}" />
|
||||||
|
<label for="item-\${index}">\${item.relativePath}</label>
|
||||||
|
</div>
|
||||||
|
\`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换项选择
|
||||||
|
function toggleItemSelection(index) {
|
||||||
|
const checkbox = document.getElementById('item-' + index);
|
||||||
|
const item = document.querySelectorAll('.context-menu-list-item')[index];
|
||||||
|
|
||||||
|
if (checkbox && item) {
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
selectedItems.add(index);
|
||||||
|
item.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
selectedItems.delete(index);
|
||||||
|
item.classList.remove('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新选中数量
|
||||||
|
function updateSelectedCount() {
|
||||||
|
const countEl = document.getElementById('contextMenuListCount');
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = '已选择 ' + selectedItems.size + ' 项';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
function confirmSelection() {
|
||||||
|
const selected = Array.from(selectedItems).map(index => currentListData[index]);
|
||||||
|
|
||||||
|
if (selected.length > 0) {
|
||||||
|
selected.forEach(item => {
|
||||||
|
addContextItem(currentListType, item.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
|
function handleAddImage() {
|
||||||
|
vscode.postMessage({ command: 'addContextImage' });
|
||||||
|
toggleContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文档
|
||||||
|
function handleAddDocument() {
|
||||||
|
vscode.postMessage({ command: 'addContextDocument' });
|
||||||
|
toggleContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
const searchInput = document.getElementById('contextMenuSearch');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', function(e) {
|
||||||
|
const keyword = e.target.value.toLowerCase();
|
||||||
|
const filtered = currentListData.filter(item =>
|
||||||
|
item.relativePath.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
renderList(filtered);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理后端消息
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
const message = event.data;
|
||||||
|
|
||||||
|
if (message.command === 'showWorkspaceFileList') {
|
||||||
|
switchToListView('选择文件', 'file', message.files);
|
||||||
|
} else if (message.command === 'showWorkspaceFolderList') {
|
||||||
|
switchToListView('选择文件夹', 'folder', message.folders);
|
||||||
|
}
|
||||||
|
});
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
225
src/views/contextDisplay.ts
Normal file
225
src/views/contextDisplay.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* 上下文显示组件
|
||||||
|
* 用于显示已选择的文件、文件夹、图片和文档
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上下文显示区域的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getContextDisplayContent(): string {
|
||||||
|
return `
|
||||||
|
<div class="context-display-area" id="contextDisplayArea" style="display: none;">
|
||||||
|
<div class="context-items-container" id="contextItemsContainer">
|
||||||
|
<!-- 动态添加的上下文项将显示在这里 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上下文显示区域的样式
|
||||||
|
*/
|
||||||
|
export function getContextDisplayStyles(): string {
|
||||||
|
return `
|
||||||
|
/* 上下文显示区域 */
|
||||||
|
.context-display-area {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(128, 128, 128, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-items-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上下文项样式 */
|
||||||
|
.context-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
max-width: 300px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item-remove {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片预览样式 */
|
||||||
|
.context-item.image-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item-preview {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--vscode-input-border);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上下文显示区域的脚本
|
||||||
|
*/
|
||||||
|
export function getContextDisplayScript(): string {
|
||||||
|
return `
|
||||||
|
// 存储上下文项
|
||||||
|
let contextItems = [];
|
||||||
|
|
||||||
|
// 获取文件图标 SVG
|
||||||
|
function getFileIcon() {
|
||||||
|
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7z" fill="currentColor"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件夹图标 SVG
|
||||||
|
function getFolderIcon() {
|
||||||
|
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片图标 SVG
|
||||||
|
function getImageIcon() {
|
||||||
|
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文档图标 SVG
|
||||||
|
function getDocumentIcon() {
|
||||||
|
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取删除图标 SVG
|
||||||
|
function getRemoveIcon() {
|
||||||
|
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取文件名
|
||||||
|
function getFileName(path) {
|
||||||
|
return path.split(/[\\\\/]/).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加上下文项
|
||||||
|
function addContextItem(type, path) {
|
||||||
|
const id = Date.now() + Math.random();
|
||||||
|
contextItems.push({ id, type, path });
|
||||||
|
renderContextItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除上下文项
|
||||||
|
function removeContextItem(id) {
|
||||||
|
contextItems = contextItems.filter(item => item.id !== id);
|
||||||
|
renderContextItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染上下文项
|
||||||
|
function renderContextItems() {
|
||||||
|
const container = document.getElementById('contextItemsContainer');
|
||||||
|
const displayArea = document.getElementById('contextDisplayArea');
|
||||||
|
|
||||||
|
if (!container || !displayArea) return;
|
||||||
|
|
||||||
|
if (contextItems.length === 0) {
|
||||||
|
displayArea.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayArea.style.display = 'block';
|
||||||
|
container.innerHTML = contextItems.map(item => {
|
||||||
|
let icon = '';
|
||||||
|
switch(item.type) {
|
||||||
|
case 'file': icon = getFileIcon(); break;
|
||||||
|
case 'folder': icon = getFolderIcon(); break;
|
||||||
|
case 'image': icon = getImageIcon(); break;
|
||||||
|
case 'document': icon = getDocumentIcon(); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \`
|
||||||
|
<div class="context-item" title="\${item.path}">
|
||||||
|
\${icon}
|
||||||
|
<span class="context-item-name">\${getFileName(item.path)}</span>
|
||||||
|
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
|
||||||
|
\${getRemoveIcon()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理后端返回的文件选择结果
|
||||||
|
window.addEventListener('message', event => {
|
||||||
|
const message = event.data;
|
||||||
|
|
||||||
|
switch(message.command) {
|
||||||
|
case 'contextFilesSelected':
|
||||||
|
if (message.files && message.files.length > 0) {
|
||||||
|
message.files.forEach(file => addContextItem('file', file));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'contextFoldersSelected':
|
||||||
|
if (message.folders && message.folders.length > 0) {
|
||||||
|
message.folders.forEach(folder => addContextItem('folder', folder));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'contextImagesSelected':
|
||||||
|
if (message.images && message.images.length > 0) {
|
||||||
|
message.images.forEach(image => addContextItem('image', image));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'contextDocumentsSelected':
|
||||||
|
if (message.documents && message.documents.length > 0) {
|
||||||
|
message.documents.forEach(doc => addContextItem('document', doc));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取所有上下文项(供发送消息时使用)
|
||||||
|
window.getContextItems = function() {
|
||||||
|
return contextItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空上下文项(供清空对话时使用)
|
||||||
|
window.clearContextItems = function() {
|
||||||
|
contextItems = [];
|
||||||
|
renderContextItems();
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -14,6 +14,11 @@ import {
|
|||||||
getContextButtonStyles,
|
getContextButtonStyles,
|
||||||
getContextButtonScript,
|
getContextButtonScript,
|
||||||
} from "./contextButton";
|
} from "./contextButton";
|
||||||
|
import {
|
||||||
|
getContextDisplayContent,
|
||||||
|
getContextDisplayStyles,
|
||||||
|
getContextDisplayScript,
|
||||||
|
} from "./contextDisplay";
|
||||||
import {
|
import {
|
||||||
getContextCompressContent,
|
getContextCompressContent,
|
||||||
getContextCompressStyles,
|
getContextCompressStyles,
|
||||||
@ -36,13 +41,15 @@ export function getInputAreaContent(
|
|||||||
maxIcon: string = ''
|
maxIcon: string = ''
|
||||||
): string {
|
): string {
|
||||||
return `
|
return `
|
||||||
<div class="input-area">
|
<div class="input-area centered" id="inputArea">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<!-- 顶部工具栏 -->
|
<!-- 顶部工具栏 -->
|
||||||
<div class="input-top-toolbar">
|
<div class="input-top-toolbar">
|
||||||
${getContextButtonContent()}
|
${getContextButtonContent()}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 上下文显示区域 -->
|
||||||
|
${getContextDisplayContent()}
|
||||||
<textarea
|
<textarea
|
||||||
id="messageInput"
|
id="messageInput"
|
||||||
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||||||
@ -76,12 +83,30 @@ export function getInputAreaStyles(): string {
|
|||||||
${getModeSelectorStyles()}
|
${getModeSelectorStyles()}
|
||||||
${getModelSelectorStyles()}
|
${getModelSelectorStyles()}
|
||||||
${getContextButtonStyles()}
|
${getContextButtonStyles()}
|
||||||
|
${getContextDisplayStyles()}
|
||||||
${getContextCompressStyles()}
|
${getContextCompressStyles()}
|
||||||
${getOptimizeButtonStyles()}
|
${getOptimizeButtonStyles()}
|
||||||
.input-area {
|
.input-area {
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
/* 居中模式:未发起对话时 */
|
||||||
|
.input-area.centered {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-width: 800px;
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
/* 底部模式:发起对话后 */
|
||||||
|
.input-area.bottom {
|
||||||
|
position: relative;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
.input-group {
|
.input-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -264,16 +289,34 @@ export function getInputAreaScript(): string {
|
|||||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||||
${getModelSelectorScript()}
|
${getModelSelectorScript()}
|
||||||
${getContextButtonScript()}
|
${getContextButtonScript()}
|
||||||
|
${getContextDisplayScript()}
|
||||||
${getContextCompressScript()}
|
${getContextCompressScript()}
|
||||||
${getOptimizeButtonScript()}
|
${getOptimizeButtonScript()}
|
||||||
|
|
||||||
// 对话状态管理
|
// 对话状态管理
|
||||||
let isConversationActive = false;
|
let isConversationActive = false;
|
||||||
|
let hasMessages = false; // 是否已有消息
|
||||||
|
|
||||||
// 工作区检测状态
|
// 工作区检测状态
|
||||||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||||
let hasWorkspace = true; // 工作区状态
|
let hasWorkspace = true; // 工作区状态
|
||||||
|
|
||||||
|
// 切换输入框布局模式
|
||||||
|
function updateInputAreaLayout() {
|
||||||
|
const inputArea = document.getElementById('inputArea');
|
||||||
|
if (!inputArea) return;
|
||||||
|
|
||||||
|
if (hasMessages) {
|
||||||
|
// 有消息时,移到底部
|
||||||
|
inputArea.classList.remove('centered');
|
||||||
|
inputArea.classList.add('bottom');
|
||||||
|
} else {
|
||||||
|
// 无消息时,居中显示
|
||||||
|
inputArea.classList.add('centered');
|
||||||
|
inputArea.classList.remove('bottom');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 自动调整 textarea 高度
|
// 自动调整 textarea 高度
|
||||||
function autoResizeTextarea() {
|
function autoResizeTextarea() {
|
||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
@ -357,12 +400,26 @@ export function getInputAreaScript(): string {
|
|||||||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||||
const planMode = document.getElementById('planToggle')?.checked || false;
|
const planMode = document.getElementById('planToggle')?.checked || false;
|
||||||
|
|
||||||
|
// 获取上下文项
|
||||||
|
const contextItems = window.getContextItems ? window.getContextItems() : [];
|
||||||
|
|
||||||
addMessage(text, 'user');
|
addMessage(text, 'user');
|
||||||
|
|
||||||
|
// 标记已有消息,切换布局到底部
|
||||||
|
hasMessages = true;
|
||||||
|
updateInputAreaLayout();
|
||||||
|
|
||||||
// 切换按钮为暂停状态
|
// 切换按钮为暂停状态
|
||||||
setSendButtonState(true);
|
setSendButtonState(true);
|
||||||
|
|
||||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode });
|
vscode.postMessage({
|
||||||
|
command: 'sendMessage',
|
||||||
|
text: text,
|
||||||
|
mode: mode,
|
||||||
|
model: model,
|
||||||
|
planMode: planMode,
|
||||||
|
contextItems: contextItems
|
||||||
|
});
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
autoResizeTextarea(); // 重置输入框高度
|
autoResizeTextarea(); // 重置输入框高度
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
@ -370,5 +427,28 @@ export function getInputAreaScript(): string {
|
|||||||
// 重置优化状态
|
// 重置优化状态
|
||||||
resetOptimizeButton();
|
resetOptimizeButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全局函数:重置输入框布局(用于清空对话时)
|
||||||
|
window.resetInputAreaLayout = function() {
|
||||||
|
hasMessages = false;
|
||||||
|
updateInputAreaLayout();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全局函数:检查是否有消息(用于页面加载时)
|
||||||
|
window.checkMessagesAndUpdateLayout = function() {
|
||||||
|
const messagesContainer = document.getElementById('messages');
|
||||||
|
if (messagesContainer) {
|
||||||
|
const messageElements = messagesContainer.querySelectorAll('.message');
|
||||||
|
hasMessages = messageElements.length > 0;
|
||||||
|
updateInputAreaLayout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载时检查消息状态
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.checkMessagesAndUpdateLayout) {
|
||||||
|
window.checkMessagesAndUpdateLayout();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -373,6 +373,12 @@ export function getMessageAreaStyles(): string {
|
|||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
/* 低调显示的工具调用 - 移除边距和背景 */
|
||||||
|
.segment-tool.low-profile {
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
.tool-segment-header {
|
.tool-segment-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -532,6 +538,23 @@ export function getMessageAreaStyles(): string {
|
|||||||
.tool-segment-content.collapsed {
|
.tool-segment-content.collapsed {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
}
|
}
|
||||||
|
/* 低调显示的工具调用样式 */
|
||||||
|
.segment-tool.low-profile .tool-segment-header {
|
||||||
|
opacity: 0.65;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.segment-tool.low-profile .tool-segment-icon {
|
||||||
|
opacity: 0.55;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.segment-tool.low-profile .tool-segment-name {
|
||||||
|
font-weight: 300;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.segment-tool.low-profile .tool-segment-result {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
.segment-question {
|
.segment-question {
|
||||||
background: var(--vscode-textBlockQuote-background);
|
background: var(--vscode-textBlockQuote-background);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@ -689,8 +712,8 @@ export function getMessageAreaScript(): string {
|
|||||||
'queryKnowledgeSummary': '已查询知识摘要',
|
'queryKnowledgeSummary': '已查询知识摘要',
|
||||||
'queryRules': '已查询规则',
|
'queryRules': '已查询规则',
|
||||||
'setModule': '已设置模块',
|
'setModule': '已设置模块',
|
||||||
'addSignal': '已添加信号',
|
'addSignal': '信号分析完成',
|
||||||
'addSignalExample': '已添加信号示例',
|
'addSignalExample': '信号示例处理完成',
|
||||||
'validateKnowledgeGraph': '已验证知识图谱',
|
'validateKnowledgeGraph': '已验证知识图谱',
|
||||||
'querySignals': '已查询信号',
|
'querySignals': '已查询信号',
|
||||||
'addPlan': '已添加计划',
|
'addPlan': '已添加计划',
|
||||||
@ -936,8 +959,30 @@ export function getMessageAreaScript(): string {
|
|||||||
// 清空容器并重新渲染所有段落
|
// 清空容器并重新渲染所有段落
|
||||||
currentSegmentedMessage.innerHTML = '';
|
currentSegmentedMessage.innerHTML = '';
|
||||||
|
|
||||||
|
// 合并连续相同的工具调用
|
||||||
|
const mergedSegments = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < segments.length) {
|
||||||
|
const segment = segments[i];
|
||||||
|
if (segment.type === 'tool') {
|
||||||
|
// 统计连续相同的工具调用
|
||||||
|
let count = 1;
|
||||||
|
while (i + count < segments.length &&
|
||||||
|
segments[i + count].type === 'tool' &&
|
||||||
|
segments[i + count].toolName === segment.toolName) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
// 添加合并后的段落(带计数)
|
||||||
|
mergedSegments.push({ ...segment, toolCount: count });
|
||||||
|
i += count;
|
||||||
|
} else {
|
||||||
|
mergedSegments.push(segment);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let toolIndex = 0; // 用于跟踪工具段落的索引
|
let toolIndex = 0; // 用于跟踪工具段落的索引
|
||||||
segments.forEach((segment, index) => {
|
mergedSegments.forEach((segment, index) => {
|
||||||
const segmentDiv = document.createElement('div');
|
const segmentDiv = document.createElement('div');
|
||||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||||
|
|
||||||
@ -949,8 +994,23 @@ export function getMessageAreaScript(): string {
|
|||||||
if (segment.toolName === 'spawnExplorer') {
|
if (segment.toolName === 'spawnExplorer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为技术性工具调用添加低调样式
|
||||||
|
const lowProfileTools = [
|
||||||
|
'knowledge_save', 'knowledge_load',
|
||||||
|
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||||
|
'setModule', 'addSignal', 'addSignalExample',
|
||||||
|
'validateKnowledgeGraph', 'addPlan', 'addEdge',
|
||||||
|
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
|
||||||
|
];
|
||||||
|
if (lowProfileTools.includes(segment.toolName)) {
|
||||||
|
segmentDiv.className += ' low-profile';
|
||||||
|
}
|
||||||
|
|
||||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||||
const toolResult = segment.toolResult || '';
|
const toolResult = segment.toolResult || '';
|
||||||
|
const toolCount = segment.toolCount || 1;
|
||||||
|
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||||
|
|
||||||
// 检查工具结果是否过长(超过一行显示不下)
|
// 检查工具结果是否过长(超过一行显示不下)
|
||||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||||
@ -964,7 +1024,7 @@ export function getMessageAreaScript(): string {
|
|||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||||
</div>
|
</div>
|
||||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||||
@ -1162,7 +1222,29 @@ export function getMessageAreaScript(): string {
|
|||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'message bot-message segmented-message';
|
container.className = 'message bot-message segmented-message';
|
||||||
|
|
||||||
segments.forEach((segment, index) => {
|
// 合并连续相同的工具调用
|
||||||
|
const mergedSegments = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < segments.length) {
|
||||||
|
const segment = segments[i];
|
||||||
|
if (segment.type === 'tool') {
|
||||||
|
// 统计连续相同的工具调用
|
||||||
|
let count = 1;
|
||||||
|
while (i + count < segments.length &&
|
||||||
|
segments[i + count].type === 'tool' &&
|
||||||
|
segments[i + count].toolName === segment.toolName) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
// 添加合并后的段落(带计数)
|
||||||
|
mergedSegments.push({ ...segment, toolCount: count });
|
||||||
|
i += count;
|
||||||
|
} else {
|
||||||
|
mergedSegments.push(segment);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedSegments.forEach((segment, index) => {
|
||||||
const segmentDiv = document.createElement('div');
|
const segmentDiv = document.createElement('div');
|
||||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||||
|
|
||||||
@ -1174,8 +1256,23 @@ export function getMessageAreaScript(): string {
|
|||||||
if (segment.toolName === 'spawnExplorer') {
|
if (segment.toolName === 'spawnExplorer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为技术性工具调用添加低调样式
|
||||||
|
const lowProfileTools = [
|
||||||
|
'knowledge_save', 'knowledge_load',
|
||||||
|
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||||
|
'setModule', 'addSignal', 'addSignalExample',
|
||||||
|
'validateKnowledgeGraph', 'addPlan', 'addEdge',
|
||||||
|
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
|
||||||
|
];
|
||||||
|
if (lowProfileTools.includes(segment.toolName)) {
|
||||||
|
segmentDiv.className += ' low-profile';
|
||||||
|
}
|
||||||
|
|
||||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||||
const toolResult = segment.toolResult || '';
|
const toolResult = segment.toolResult || '';
|
||||||
|
const toolCount = segment.toolCount || 1;
|
||||||
|
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||||
|
|
||||||
// 检查工具结果是否过长(超过一行显示不下)
|
// 检查工具结果是否过长(超过一行显示不下)
|
||||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||||
@ -1183,7 +1280,7 @@ export function getMessageAreaScript(): string {
|
|||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||||||
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
|
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||||
</div>
|
</div>
|
||||||
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
* 获取模型选择器的 HTML 内容
|
* 获取模型选择器的 HTML 内容
|
||||||
*/
|
*/
|
||||||
export function getModelSelectorContent(
|
export function getModelSelectorContent(
|
||||||
autoIcon: string = '',
|
autoIcon: string = "",
|
||||||
liteIcon: string = '',
|
liteIcon: string = "",
|
||||||
syIcon: string = '',
|
syIcon: string = "",
|
||||||
maxIcon: string = ''
|
maxIcon: string = ""
|
||||||
): string {
|
): string {
|
||||||
return `
|
return `
|
||||||
<!-- 模型选择 -->
|
<!-- 模型选择 -->
|
||||||
@ -22,25 +22,51 @@ export function getModelSelectorContent(
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-dropdown" id="modelDropdown">
|
<div class="select-dropdown" id="modelDropdown">
|
||||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">
|
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
|
||||||
${autoIcon ? `<img src="${autoIcon}" class="model-icon" alt="Auto">` : ''}
|
${
|
||||||
<span class="option-label">Auto</span>
|
autoIcon
|
||||||
|
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="option-content">
|
||||||
|
<span class="option-label">Auto</span>
|
||||||
|
<span class="option-desc">智能匹配最优模型</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">
|
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
|
||||||
${liteIcon ? `<img src="${liteIcon}" class="model-icon" alt="Lite">` : ''}
|
${
|
||||||
<span class="option-label">Lite</span>
|
liteIcon
|
||||||
|
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="option-content">
|
||||||
|
<span class="option-label">Lite</span>
|
||||||
|
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">
|
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||||
${syIcon ? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">` : ''}
|
${
|
||||||
<span class="option-label">Syntaxic</span>
|
syIcon
|
||||||
|
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="option-content">
|
||||||
|
<span class="option-label">Syntaxic</span>
|
||||||
|
<span class="option-desc">均衡成本和性能,节省credits同时保持可靠输出</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">
|
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
|
||||||
${maxIcon ? `<img src="${maxIcon}" class="model-icon" alt="Max">` : ''}
|
${
|
||||||
<span class="option-label">Max</span>
|
maxIcon
|
||||||
|
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="option-content">
|
||||||
|
<span class="option-label">Max</span>
|
||||||
|
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 模型选择器的 tooltip 容器 -->
|
|
||||||
<div id="modelTooltip" class="model-tooltip"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="tooltiptext">选择模型</span>
|
<span class="tooltiptext">选择模型</span>
|
||||||
</div>
|
</div>
|
||||||
@ -104,13 +130,13 @@ export function getModelSelectorStyles(): string {
|
|||||||
/* 模型选择器的选项样式 */
|
/* 模型选择器的选项样式 */
|
||||||
#modelDropdown .select-option {
|
#modelDropdown .select-option {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 6px 12px;
|
padding: 8px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
#modelDropdown .select-option:hover {
|
#modelDropdown .select-option:hover {
|
||||||
background: rgba(128, 128, 128, 0.3);
|
background: rgba(128, 128, 128, 0.3);
|
||||||
@ -125,54 +151,22 @@ export function getModelSelectorStyles(): string {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
.option-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
.option-label {
|
.option-label {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: var(--vscode-foreground);
|
color: var(--vscode-foreground);
|
||||||
|
font-weight: 500;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
/* 模型选择器的 tooltip 样式 */
|
.option-desc {
|
||||||
.model-tooltip {
|
font-size: 11px;
|
||||||
position: fixed;
|
color: var(--vscode-descriptionForeground);
|
||||||
background: #1e1e1e;
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10000;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
|
||||||
}
|
|
||||||
.model-tooltip.show {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
/* tooltip 箭头 */
|
|
||||||
.model-tooltip::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
border-width: 7px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
.model-tooltip::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
border-width: 6px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent #1e1e1e transparent transparent;
|
|
||||||
margin-right: 1px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -235,46 +229,5 @@ export function getModelSelectorScript(): string {
|
|||||||
function getCurrentModel() {
|
function getCurrentModel() {
|
||||||
return currentModel;
|
return currentModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模型选择器 tooltip 功能
|
|
||||||
(function initModelTooltip() {
|
|
||||||
const modelDropdown = document.getElementById('modelDropdown');
|
|
||||||
const modelTooltip = document.getElementById('modelTooltip');
|
|
||||||
|
|
||||||
if (!modelDropdown || !modelTooltip) return;
|
|
||||||
|
|
||||||
// 为每个选项添加鼠标事件
|
|
||||||
const options = modelDropdown.querySelectorAll('.select-option');
|
|
||||||
|
|
||||||
options.forEach(option => {
|
|
||||||
option.addEventListener('mouseenter', function(e) {
|
|
||||||
const tooltipText = this.getAttribute('data-tooltip');
|
|
||||||
if (!tooltipText) return;
|
|
||||||
|
|
||||||
// 设置 tooltip 内容
|
|
||||||
modelTooltip.textContent = tooltipText;
|
|
||||||
|
|
||||||
// 获取选项的位置
|
|
||||||
const rect = this.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 计算 tooltip 位置(在选项右侧)
|
|
||||||
const tooltipRect = modelTooltip.getBoundingClientRect();
|
|
||||||
const left = rect.right + 12;
|
|
||||||
const top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
|
|
||||||
|
|
||||||
// 设置位置
|
|
||||||
modelTooltip.style.left = left + 'px';
|
|
||||||
modelTooltip.style.top = top + 'px';
|
|
||||||
|
|
||||||
// 显示 tooltip
|
|
||||||
modelTooltip.classList.add('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
option.addEventListener('mouseleave', function() {
|
|
||||||
// 隐藏 tooltip
|
|
||||||
modelTooltip.classList.remove('show');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,10 +61,30 @@ export function getPlanCardStyles(): string {
|
|||||||
.plan-step:last-child {
|
.plan-step:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.step-num {
|
.step-checkbox {
|
||||||
color: var(--vscode-textLink-foreground);
|
display: inline-flex;
|
||||||
font-weight: 500;
|
align-items: center;
|
||||||
margin-right: 6px;
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: 2px solid var(--vscode-textLink-foreground);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.step-checkbox.completed {
|
||||||
|
background: var(--vscode-textLink-foreground);
|
||||||
|
border-color: var(--vscode-textLink-foreground);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.step-checkbox.completed::after {
|
||||||
|
content: '✓';
|
||||||
|
color: var(--vscode-editor-background);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.plan-actions {
|
.plan-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -151,7 +171,7 @@ export function getPlanCardScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
// 选项按钮
|
// 选项按钮
|
||||||
@ -231,7 +251,7 @@ export function getPlanCardScript(): string {
|
|||||||
function renderPlanCardStatic(segment, segmentDiv) {
|
function renderPlanCardStatic(segment, segmentDiv) {
|
||||||
segmentDiv.className += ' segment-plan';
|
segmentDiv.className += ' segment-plan';
|
||||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
\`<div class="plan-step"><span class="step-checkbox"></span> \${step}</div>\`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
|
|||||||
411
src/views/progressBar.ts
Normal file
411
src/views/progressBar.ts
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
/**
|
||||||
|
* 进度条模块
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 显示开发流程进度: Spec -> Design代码编写 -> 仿真检查 -> AST -> Done
|
||||||
|
* - 支持动态更新当前进度状态
|
||||||
|
* - 提供视觉反馈显示已完成和进行中的步骤
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度条的 HTML 内容
|
||||||
|
*/
|
||||||
|
export function getProgressBarContent(): string {
|
||||||
|
return `
|
||||||
|
<div class="progress-bar-container" style="display: none;">
|
||||||
|
<div class="progress-bar-header">
|
||||||
|
<span class="progress-bar-title">开发流程</span>
|
||||||
|
<button class="progress-bar-toggle" title="收起/展开">
|
||||||
|
<span class="toggle-icon">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="progress-steps">
|
||||||
|
<div class="progress-step" data-step="spec">
|
||||||
|
<div class="step-circle">
|
||||||
|
<span class="step-number">1</span>
|
||||||
|
<span class="step-check">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Spec设计文档</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-line"></div>
|
||||||
|
|
||||||
|
<div class="progress-step" data-step="design">
|
||||||
|
<div class="step-circle">
|
||||||
|
<span class="step-number">2</span>
|
||||||
|
<span class="step-check">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Design代码编写</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-line"></div>
|
||||||
|
|
||||||
|
<div class="progress-step" data-step="simulation">
|
||||||
|
<div class="step-circle">
|
||||||
|
<span class="step-number">3</span>
|
||||||
|
<span class="step-check">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Sim仿真检查</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-line"></div>
|
||||||
|
|
||||||
|
<div class="progress-step" data-step="done">
|
||||||
|
<div class="step-circle">
|
||||||
|
<span class="step-number">4</span>
|
||||||
|
<span class="step-check">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">Done完成</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度条的样式
|
||||||
|
*/
|
||||||
|
export function getProgressBarStyles(): string {
|
||||||
|
return `
|
||||||
|
.progress-bar-container {
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container.collapsed .toggle-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-steps {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px 10px 20px;
|
||||||
|
max-height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container.collapsed .progress-steps {
|
||||||
|
max-height: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-circle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
border: 2px solid var(--vscode-input-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-check {
|
||||||
|
display: none;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--vscode-input-border);
|
||||||
|
margin: 0 6px;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已完成状态 */
|
||||||
|
.progress-step.completed .step-circle {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
border-color: var(--vscode-button-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed .step-number {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed .step-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed .step-label {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.completed + .progress-line {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进行中状态 */
|
||||||
|
.progress-step.active .step-circle {
|
||||||
|
background: var(--vscode-button-background);
|
||||||
|
border-color: var(--vscode-button-background);
|
||||||
|
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.active .step-number {
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.active .step-label {
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.progress-steps {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-circle {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度条的脚本
|
||||||
|
*/
|
||||||
|
export function getProgressBarScript(): string {
|
||||||
|
return `
|
||||||
|
// 进度条管理
|
||||||
|
const ProgressBar = {
|
||||||
|
steps: ['spec', 'design', 'simulation', 'done'],
|
||||||
|
currentStep: 'spec',
|
||||||
|
isCollapsed: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化进度条
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.updateProgress('spec');
|
||||||
|
this.initToggle();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化收起/展开功能
|
||||||
|
*/
|
||||||
|
initToggle() {
|
||||||
|
const container = document.querySelector('.progress-bar-container');
|
||||||
|
const header = document.querySelector('.progress-bar-header');
|
||||||
|
const toggle = document.querySelector('.progress-bar-toggle');
|
||||||
|
|
||||||
|
if (!container || !header || !toggle) return;
|
||||||
|
|
||||||
|
// 点击头部或按钮都可以切换
|
||||||
|
const handleToggle = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.isCollapsed = !this.isCollapsed;
|
||||||
|
|
||||||
|
if (this.isCollapsed) {
|
||||||
|
container.classList.add('collapsed');
|
||||||
|
} else {
|
||||||
|
container.classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
header.addEventListener('click', handleToggle);
|
||||||
|
toggle.addEventListener('click', handleToggle);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示进度条
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
const container = document.querySelector('.progress-bar-container');
|
||||||
|
if (container) {
|
||||||
|
container.style.display = 'block';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏进度条
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
const container = document.querySelector('.progress-bar-container');
|
||||||
|
if (container) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新进度到指定步骤
|
||||||
|
* @param {string} stepName - 步骤名称
|
||||||
|
*/
|
||||||
|
updateProgress(stepName) {
|
||||||
|
if (!this.steps.includes(stepName)) {
|
||||||
|
console.warn('Invalid step name:', stepName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStep = stepName;
|
||||||
|
const currentIndex = this.steps.indexOf(stepName);
|
||||||
|
|
||||||
|
// 更新所有步骤的状态
|
||||||
|
document.querySelectorAll('.progress-step').forEach((step, index) => {
|
||||||
|
step.classList.remove('completed', 'active');
|
||||||
|
|
||||||
|
if (index < currentIndex) {
|
||||||
|
step.classList.add('completed');
|
||||||
|
} else if (index === currentIndex) {
|
||||||
|
step.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新连接线
|
||||||
|
document.querySelectorAll('.progress-line').forEach((line, index) => {
|
||||||
|
if (index < currentIndex) {
|
||||||
|
line.style.background = 'var(--vscode-button-background)';
|
||||||
|
} else {
|
||||||
|
line.style.background = 'var(--vscode-input-border)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前进到下一步
|
||||||
|
*/
|
||||||
|
nextStep() {
|
||||||
|
const currentIndex = this.steps.indexOf(this.currentStep);
|
||||||
|
if (currentIndex < this.steps.length - 1) {
|
||||||
|
this.updateProgress(this.steps[currentIndex + 1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置进度条
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.updateProgress('spec');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成所有步骤
|
||||||
|
*/
|
||||||
|
complete() {
|
||||||
|
this.updateProgress('done');
|
||||||
|
// 将最后一步也标记为完成
|
||||||
|
const lastStep = document.querySelector('.progress-step[data-step="done"]');
|
||||||
|
if (lastStep) {
|
||||||
|
lastStep.classList.remove('active');
|
||||||
|
lastStep.classList.add('completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化进度条
|
||||||
|
ProgressBar.init();
|
||||||
|
|
||||||
|
// 监听来自扩展的消息以更新进度
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
const message = event.data;
|
||||||
|
if (message.type === 'updateProgress') {
|
||||||
|
ProgressBar.updateProgress(message.step);
|
||||||
|
} else if (message.type === 'resetProgress') {
|
||||||
|
ProgressBar.reset();
|
||||||
|
} else if (message.type === 'completeProgress') {
|
||||||
|
ProgressBar.complete();
|
||||||
|
} else if (message.type === 'showProgress') {
|
||||||
|
ProgressBar.show();
|
||||||
|
} else if (message.type === 'hideProgress') {
|
||||||
|
ProgressBar.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
178
src/views/thinkingProcess.ts
Normal file
178
src/views/thinkingProcess.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* 思考过程组件
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 显示 AI 的思考过程
|
||||||
|
* - 支持展开/折叠功能
|
||||||
|
* - 提供打字机效果的流式显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取思考过程组件的 HTML 内容
|
||||||
|
* @param thinking - 思考内容
|
||||||
|
* @param isExpanded - 是否默认展开
|
||||||
|
*/
|
||||||
|
export function getThinkingProcessContent(
|
||||||
|
thinking: string = "",
|
||||||
|
isExpanded: boolean = false
|
||||||
|
): string {
|
||||||
|
return `
|
||||||
|
<div class="thinking-process-container ${isExpanded ? "expanded" : ""}">
|
||||||
|
<div class="thinking-header">
|
||||||
|
<div class="thinking-icon-wrapper">
|
||||||
|
<svg class="thinking-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="thinking-title">思考过程</span>
|
||||||
|
</div>
|
||||||
|
<div class="thinking-content">
|
||||||
|
<div class="thinking-text">${thinking || "正在思考中..."}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取思考过程组件的样式
|
||||||
|
*/
|
||||||
|
export function getThinkingProcessStyles(): string {
|
||||||
|
return `
|
||||||
|
.thinking-process-container {
|
||||||
|
margin: 12px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
background: var(--vscode-input-background);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
width: 85px;
|
||||||
|
border-radius: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-process-container.typing .thinking-header::before {
|
||||||
|
animation: shine 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
50%, 100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-header:hover {
|
||||||
|
background: var(--vscode-list-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-icon-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-process-container.expanded .thinking-icon-wrapper {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-icon {
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-process-container.expanded .thinking-content {
|
||||||
|
max-height: 500px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-text {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 2px solid var(--vscode-textBlockQuote-border);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打字机效果 */
|
||||||
|
.thinking-text.typing::after {
|
||||||
|
content: '▋';
|
||||||
|
animation: blink 1s step-end infinite;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
51%, 100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.thinking-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--vscode-scrollbarSlider-background);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--vscode-scrollbarSlider-hoverBackground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -18,6 +18,12 @@ import {
|
|||||||
getMessageAreaScript,
|
getMessageAreaScript,
|
||||||
} from "./messageArea";
|
} from "./messageArea";
|
||||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||||
|
import {
|
||||||
|
getProgressBarContent,
|
||||||
|
getProgressBarStyles,
|
||||||
|
getProgressBarScript,
|
||||||
|
} from "./progressBar";
|
||||||
|
import { getCurrentEnv } from "../config/settings";
|
||||||
/**
|
/**
|
||||||
* 获取 WebView 面板的 HTML 内容
|
* 获取 WebView 面板的 HTML 内容
|
||||||
*/
|
*/
|
||||||
@ -28,6 +34,10 @@ export function getWebviewContent(
|
|||||||
syIconUri?: string,
|
syIconUri?: string,
|
||||||
maxIconUri?: string
|
maxIconUri?: string
|
||||||
): string {
|
): string {
|
||||||
|
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
|
||||||
|
const currentEnv = getCurrentEnv();
|
||||||
|
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
@ -75,6 +85,7 @@ export function getWebviewContent(
|
|||||||
${getAgentCardStyles()}
|
${getAgentCardStyles()}
|
||||||
${getWaveformPreviewContent()}
|
${getWaveformPreviewContent()}
|
||||||
${getConversationHistoryBarStyles()}
|
${getConversationHistoryBarStyles()}
|
||||||
|
${getProgressBarStyles()}
|
||||||
${getInputAreaStyles()}
|
${getInputAreaStyles()}
|
||||||
|
|
||||||
.file-editor-section {
|
.file-editor-section {
|
||||||
@ -380,6 +391,7 @@ export function getWebviewContent(
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${getConversationHistoryBarContent()}
|
${getConversationHistoryBarContent()}
|
||||||
|
${getProgressBarContent()}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||||
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
|
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
|
||||||
@ -397,12 +409,16 @@ export function getWebviewContent(
|
|||||||
<span id="statusText">思考中...</span>
|
<span id="statusText">思考中...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="quick-actions">
|
${
|
||||||
|
showQuickActions
|
||||||
|
? `<div class="quick-actions">
|
||||||
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
||||||
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
||||||
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
||||||
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
||||||
</div> -->
|
</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
||||||
</div>
|
</div>
|
||||||
@ -443,10 +459,9 @@ export function getWebviewContent(
|
|||||||
}
|
}
|
||||||
if (modeTooltip) {
|
if (modeTooltip) {
|
||||||
const tooltipMap = {
|
const tooltipMap = {
|
||||||
'plan': '只读模式 - 只能查询分析',
|
'plan': 'plan模式',
|
||||||
'ask': '逐个确认 - 每个写操作需确认',
|
'ask': 'ask模式',
|
||||||
'agent': '智能体自主模式',
|
'agent': 'agent模式'
|
||||||
'auto': '完全自动 - 所有操作自动执行'
|
|
||||||
};
|
};
|
||||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||||
}
|
}
|
||||||
@ -650,6 +665,10 @@ export function getWebviewContent(
|
|||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
// 重置输入框布局到居中
|
||||||
|
if (typeof window.resetInputAreaLayout === 'function') {
|
||||||
|
window.resetInputAreaLayout();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'addUserMessage':
|
case 'addUserMessage':
|
||||||
@ -657,6 +676,10 @@ export function getWebviewContent(
|
|||||||
if (message.text) {
|
if (message.text) {
|
||||||
addMessage(message.text, 'user');
|
addMessage(message.text, 'user');
|
||||||
}
|
}
|
||||||
|
// 检查并更新输入框布局
|
||||||
|
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
|
||||||
|
window.checkMessagesAndUpdateLayout();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'addAiMessage':
|
case 'addAiMessage':
|
||||||
@ -664,6 +687,10 @@ export function getWebviewContent(
|
|||||||
if (message.text) {
|
if (message.text) {
|
||||||
addMessage(message.text, 'bot');
|
addMessage(message.text, 'bot');
|
||||||
}
|
}
|
||||||
|
// 检查并更新输入框布局
|
||||||
|
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
|
||||||
|
window.checkMessagesAndUpdateLayout();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'switchMode':
|
case 'switchMode':
|
||||||
@ -696,6 +723,7 @@ export function getWebviewContent(
|
|||||||
${getAgentCardScript()}
|
${getAgentCardScript()}
|
||||||
${getWaveformPreviewScript()}
|
${getWaveformPreviewScript()}
|
||||||
${getConversationHistoryBarScript()}
|
${getConversationHistoryBarScript()}
|
||||||
|
${getProgressBarScript()}
|
||||||
${getInputAreaScript()}
|
${getInputAreaScript()}
|
||||||
</script></body>
|
</script></body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|||||||
42
tools/waveform_trace/build.bat
Normal file
42
tools/waveform_trace/build.bat
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@echo off
|
||||||
|
REM waveform_trace 打包脚本 (Windows)
|
||||||
|
REM 用法: build.bat
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo waveform_trace 打包脚本
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
cd /d "%~dp0src"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [1/3] 安装依赖...
|
||||||
|
pip install -r requirements.txt
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 依赖安装失败
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [2/3] 清理旧文件...
|
||||||
|
if exist build rmdir /s /q build
|
||||||
|
if exist dist rmdir /s /q dist
|
||||||
|
if exist waveform_trace.spec del waveform_trace.spec
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/3] PyInstaller 打包...
|
||||||
|
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo 错误: 打包失败
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [4/4] 复制到 bin 目录...
|
||||||
|
if not exist "..\bin" mkdir "..\bin"
|
||||||
|
copy /y "dist\waveform_trace.exe" "..\bin\"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo 打包完成!
|
||||||
|
echo 输出: tools/waveform_trace/bin/waveform_trace.exe
|
||||||
|
echo ========================================
|
||||||
35
tools/waveform_trace/build.sh
Normal file
35
tools/waveform_trace/build.sh
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# waveform_trace 打包脚本 (Linux/macOS)
|
||||||
|
# 用法: ./build.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " waveform_trace 打包脚本"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR/src"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[1/4] 安装依赖..."
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[2/4] 清理旧文件..."
|
||||||
|
rm -rf build dist *.spec
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[3/4] PyInstaller 打包..."
|
||||||
|
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[4/4] 复制到 bin 目录..."
|
||||||
|
mkdir -p ../bin
|
||||||
|
cp dist/waveform_trace ../bin/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " 打包完成!"
|
||||||
|
echo " 输出: tools/waveform_trace/bin/waveform_trace"
|
||||||
|
echo "========================================"
|
||||||
115
tools/waveform_trace/src/README.md
Normal file
115
tools/waveform_trace/src/README.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# AST 波形调试核心代码
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
| 文件 | 作用 | 核心函数 | TS重写需要 |
|
||||||
|
|------|------|----------|------------|
|
||||||
|
| `ast_node.py` | AST节点定义,遍历建图 | `toplogic_tree_traverse()` | ✅ 已完成 |
|
||||||
|
| `graph_builder.py` | 入口函数,调用解析器 | `generate_top_logic_graph()` | ✅ 已完成 |
|
||||||
|
| `debug_graph_analyzer.py` | BFS回溯控制信号 | `get_k_control_signals()` | ⚠️ 需重写 |
|
||||||
|
| `vcd_waveform_analyzer.py` | VCD波形文件解析 | `parse_mismatch()`, `get_tabular()` | ⚠️ 需重写 |
|
||||||
|
| `waveform_trace_tool.py` | 完整追踪工具封装 | `waveform_trace_tool()` | ⚠️ 需重写 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调用流程
|
||||||
|
|
||||||
|
```
|
||||||
|
Verilog代码文件
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ graph_builder.py │
|
||||||
|
│ generate_top_logic_graph(filelist) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ PyVerilog.parse() → AST │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ast.toplogic_tree_traverse() │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ NetworkX 有向图(信号依赖图) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ debug_graph_analyzer.py │
|
||||||
|
│ DebugGraph.get_k_control_signals() │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ BFS回溯K层,找到控制信号链 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ vcd_waveform_analyzer.py │
|
||||||
|
│ parse_mismatch() + get_tabular() │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 提取相关信号的波形表 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心代码位置
|
||||||
|
|
||||||
|
### 1. AST遍历建图 (ast_node.py:32-137)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def toplogic_tree_traverse(self, network_G, rvalue=False, lvalue=False, offset=0):
|
||||||
|
"""
|
||||||
|
递归遍历AST,提取信号依赖关系,填充到NetworkX图中
|
||||||
|
|
||||||
|
关键逻辑:
|
||||||
|
1. 识别 Rvalue(右值)和 Lvalue(左值)
|
||||||
|
2. 递归收集子节点的信号
|
||||||
|
3. 建立边:右值信号 → 左值信号(控制关系)
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 图构建入口 (graph_builder.py:89-99)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generate_top_logic_graph(filelist: list[str]):
|
||||||
|
# 1. PyVerilog解析Verilog代码
|
||||||
|
ast, directives = parse(filelist, preprocess_include=[], preprocess_define=[])
|
||||||
|
# 2. 遍历AST,构建信号依赖图
|
||||||
|
return create_graph_from_ast(ast, display=False, display_signal_only=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. BFS回溯 (debug_graph_analyzer.py:20-66)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_k_control_signals(self, target_signals: list[str], k: int, signal_only: bool = False):
|
||||||
|
"""
|
||||||
|
从出错信号出发,BFS回溯K层,找到所有控制信号
|
||||||
|
|
||||||
|
输入:target_signals = ['out'] # 出错的信号
|
||||||
|
输出:control_signals = {'out': (10,10), 'state': (5,8), 'clk': (1,1)}
|
||||||
|
signal_level_tracer = [['clk->state', 'reset->state'], ['state->out']]
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依赖库
|
||||||
|
|
||||||
|
```
|
||||||
|
pyverilog # Verilog解析,生成AST
|
||||||
|
networkx # 图数据结构
|
||||||
|
pandas # 波形数据处理(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 如果要用JavaScript重写
|
||||||
|
|
||||||
|
需要重写的核心逻辑:
|
||||||
|
|
||||||
|
1. **Verilog解析器** → 用 ANTLR4 + Verilog.g4 或 tree-sitter-verilog
|
||||||
|
2. **AST遍历建图** → 约100行,参考 ast_node.py:32-137
|
||||||
|
3. **BFS回溯** → 约70行,参考 debug_graph_analyzer.py
|
||||||
|
|
||||||
|
总计约 **200行核心逻辑**(不含解析器)
|
||||||
455
tools/waveform_trace/src/TS_REWRITE_SPEC.md
Normal file
455
tools/waveform_trace/src/TS_REWRITE_SPEC.md
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
# AST波形调试工具 - TypeScript重写规范
|
||||||
|
|
||||||
|
## 一、项目背景
|
||||||
|
|
||||||
|
将Python实现的Verilog AST波形调试工具重写为TypeScript,用于VSCode插件。
|
||||||
|
|
||||||
|
**已完成部分**:
|
||||||
|
- ✅ Verilog AST解析(生成JSON格式的信号依赖图)
|
||||||
|
- ✅ 图结构定义
|
||||||
|
|
||||||
|
**待重写部分**:
|
||||||
|
- ⚠️ BFS信号回溯
|
||||||
|
- ⚠️ VCD波形解析
|
||||||
|
- ⚠️ 仿真输出解析
|
||||||
|
- ⚠️ 工具整合封装
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、数据结构定义
|
||||||
|
|
||||||
|
### 2.1 AST图结构(已完成)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ASTNode {
|
||||||
|
id: string;
|
||||||
|
attributes: {
|
||||||
|
lines: [number, number]; // [起始行, 结束行]
|
||||||
|
type: string; // Input/Output/Reg/Wire/Always/Assign等
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ASTEdge {
|
||||||
|
from: string; // 控制信号
|
||||||
|
to: string; // 被控制信号
|
||||||
|
attributes: {
|
||||||
|
lines: [number, number];
|
||||||
|
type: string; // Always/Assign/IfStatement等
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ASTGraph {
|
||||||
|
metadata: {
|
||||||
|
moduleName: string;
|
||||||
|
nodeCount: number;
|
||||||
|
edgeCount: number;
|
||||||
|
generatedAt: string;
|
||||||
|
};
|
||||||
|
nodes: ASTNode[];
|
||||||
|
edges: ASTEdge[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 追踪结果结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TraceResult {
|
||||||
|
controlSignals: Map<string, [number, number]>; // 信号名 -> 代码行号
|
||||||
|
signalLevelTracer: string[][]; // 每层的控制关系链
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 波形数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WaveformData {
|
||||||
|
time: number; // 时间点(ns)
|
||||||
|
signals: {
|
||||||
|
[signalName: string]: string; // 信号名 -> 值(十六进制)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MismatchInfo {
|
||||||
|
signals: string[]; // 出错的信号列表
|
||||||
|
firstMismatchTime: number; // 第一次出错的时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、需要重写的模块
|
||||||
|
|
||||||
|
### 3.1 BFS信号回溯模块
|
||||||
|
|
||||||
|
**源文件**: `debug_graph_analyzer.py`
|
||||||
|
**代码行数**: ~70行
|
||||||
|
**第三方依赖**: 无
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
从出错信号出发,BFS反向遍历图,找到所有控制该信号的上游信号。
|
||||||
|
|
||||||
|
#### 输入输出
|
||||||
|
```typescript
|
||||||
|
// 输入
|
||||||
|
graph: ASTGraph // AST图(JSON格式)
|
||||||
|
targetSignals: string[] // 出错的信号列表,如 ['count', 'overflow']
|
||||||
|
k: number // 回溯层数
|
||||||
|
signalOnly: boolean // 是否只返回信号节点(过滤Always/Assign等)
|
||||||
|
|
||||||
|
// 输出
|
||||||
|
TraceResult {
|
||||||
|
controlSignals: Map<string, [number, number]>,
|
||||||
|
signalLevelTracer: string[][]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心算法(伪代码)
|
||||||
|
```
|
||||||
|
1. 构建前驱映射(反向边)
|
||||||
|
for each edge in graph.edges:
|
||||||
|
predecessorMap[edge.to].push(edge.from)
|
||||||
|
|
||||||
|
2. 初始化BFS队列
|
||||||
|
for each signal in targetSignals:
|
||||||
|
queue.push([signal, signal])
|
||||||
|
controlSignals.set(signal, node.lines)
|
||||||
|
|
||||||
|
3. BFS遍历K层
|
||||||
|
for level = 0 to k:
|
||||||
|
while queue not empty:
|
||||||
|
[curSignal, controlledSignal] = queue.pop()
|
||||||
|
记录关系: curSignal -> controlledSignal
|
||||||
|
|
||||||
|
for each predecessor of curSignal:
|
||||||
|
if not visited and not filtered:
|
||||||
|
queue.push([predecessor, curSignal])
|
||||||
|
|
||||||
|
记录本层关系到 signalLevelTracer
|
||||||
|
|
||||||
|
4. 返回结果
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 过滤规则
|
||||||
|
```typescript
|
||||||
|
// 需要过滤的节点类型
|
||||||
|
const FILTERED_TYPES = ['Parameter', 'Localparam'];
|
||||||
|
|
||||||
|
// signalOnly=true时,还需要过滤以下前缀
|
||||||
|
const FILTERED_PREFIXES = ['Always', 'Assign', 'Module', 'IntConst'];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 仿真输出解析模块
|
||||||
|
|
||||||
|
**源文件**: `vcd_waveform_analyzer.py` 中的 `parse_mismatch()`
|
||||||
|
**代码行数**: ~20行
|
||||||
|
**第三方依赖**: 无
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
解析仿真工具的输出文本,提取出错信号名和出错时间。
|
||||||
|
|
||||||
|
#### 输入输出
|
||||||
|
```typescript
|
||||||
|
// 输入
|
||||||
|
testOutput: string // 仿真工具的输出文本
|
||||||
|
|
||||||
|
// 输出
|
||||||
|
MismatchInfo {
|
||||||
|
signals: string[], // 出错信号列表
|
||||||
|
firstMismatchTime: number // 第一次出错时间(ns)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 解析规则
|
||||||
|
```typescript
|
||||||
|
// 需要匹配的格式
|
||||||
|
// "First mismatch occurred at time 100. Output 'count' ..."
|
||||||
|
|
||||||
|
const pattern = /First mismatch occurred at time (\d+).*Output '(\w+)'/g;
|
||||||
|
|
||||||
|
// 提取所有匹配
|
||||||
|
// 返回信号列表和最小时间戳
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
```
|
||||||
|
输入:
|
||||||
|
"First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
|
||||||
|
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0"
|
||||||
|
|
||||||
|
输出:
|
||||||
|
{
|
||||||
|
signals: ['count', 'overflow'],
|
||||||
|
firstMismatchTime: 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 VCD波形解析模块
|
||||||
|
|
||||||
|
**源文件**: `vcd_waveform_analyzer.py` 中的 `get_tabular()` 和 `tabular_via_dataframe()`
|
||||||
|
**代码行数**: ~150行
|
||||||
|
**第三方依赖**: Python版用了 `vcdvcd`, `pandas`, `numpy`
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
读取VCD(Value Change Dump)波形文件,提取指定信号的波形值,生成表格。
|
||||||
|
|
||||||
|
#### VCD文件格式简介
|
||||||
|
```vcd
|
||||||
|
$timescale 1ns $end
|
||||||
|
$scope module tb $end
|
||||||
|
$var wire 1 ! clk $end
|
||||||
|
$var wire 8 " count [7:0] $end
|
||||||
|
$upscope $end
|
||||||
|
$enddefinitions $end
|
||||||
|
#0
|
||||||
|
b0 "
|
||||||
|
1!
|
||||||
|
#5
|
||||||
|
0!
|
||||||
|
#10
|
||||||
|
1!
|
||||||
|
b00000001 "
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 输入输出
|
||||||
|
```typescript
|
||||||
|
// 输入
|
||||||
|
vcdPath: string // VCD文件路径
|
||||||
|
signalsToTrace: string[] // 需要提取的信号列表
|
||||||
|
offset: number // 时间偏移(从哪个时间点开始)
|
||||||
|
windowSize: number // 窗口大小(提取多少个时间点)
|
||||||
|
|
||||||
|
// 输出
|
||||||
|
string // 格式化的波形表格字符串
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 输出格式示例
|
||||||
|
```
|
||||||
|
### First mismatched signals time(ns) Trace ###
|
||||||
|
time(ns) clk reset count_ref count_dut
|
||||||
|
0 1 1 00 00
|
||||||
|
5 0 1 00 00
|
||||||
|
10 1 0 00 00
|
||||||
|
15 0 0 00 00
|
||||||
|
20 1 0 01 00 <- mismatch
|
||||||
|
### First mismatched signals time(ns) End ###
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TS实现建议
|
||||||
|
1. **方案A**: 找现有的JS VCD解析库
|
||||||
|
- 搜索: `npm vcd parser`, `vcd-stream`, `wavedrom`
|
||||||
|
|
||||||
|
2. **方案B**: 自己实现简单的VCD解析器
|
||||||
|
- VCD格式相对简单,核心是解析变量定义和时间变化
|
||||||
|
- 约100-150行代码
|
||||||
|
|
||||||
|
#### VCD解析核心逻辑
|
||||||
|
```typescript
|
||||||
|
class VCDParser {
|
||||||
|
signals: Map<string, Signal>; // 信号定义
|
||||||
|
timeValues: Map<number, Map<string, string>>; // 时间 -> 信号值
|
||||||
|
|
||||||
|
parse(vcdContent: string): void {
|
||||||
|
// 1. 解析头部($var定义)
|
||||||
|
// 2. 解析数据部分(#时间 和 值变化)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignalValues(signalName: string, startTime: number, endTime: number): WaveformData[] {
|
||||||
|
// 提取指定信号在时间范围内的值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 工具整合封装模块
|
||||||
|
|
||||||
|
**源文件**: `waveform_trace_tool.py`
|
||||||
|
**代码行数**: ~150行
|
||||||
|
**第三方依赖**: 依赖上面所有模块
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
整合所有模块,提供统一的调试接口。
|
||||||
|
|
||||||
|
#### 输入输出
|
||||||
|
```typescript
|
||||||
|
// 输入
|
||||||
|
verilogFilePath: string // Verilog文件路径
|
||||||
|
vcdFilePath: string // VCD波形文件路径
|
||||||
|
simulationOutput: string // 仿真输出文本
|
||||||
|
traceLevel: number // 回溯层数
|
||||||
|
|
||||||
|
// 输出
|
||||||
|
string // 完整的调试报告
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 调试报告格式
|
||||||
|
```
|
||||||
|
[Signal Traces] Backtrace control signal relations.
|
||||||
|
clk->count
|
||||||
|
reset->count
|
||||||
|
-count->state
|
||||||
|
--state->out (*last output port level)
|
||||||
|
|
||||||
|
[Signal Waveform]:
|
||||||
|
<signal>_ref 是期望值(golden)
|
||||||
|
<signal>_dut 是实际输出
|
||||||
|
[Traced Signals]: out, state, count, clk, reset
|
||||||
|
|
||||||
|
[Table Waveform in hexadecimal format]
|
||||||
|
time(ns) clk reset count_ref count_dut
|
||||||
|
...
|
||||||
|
|
||||||
|
[Verilog of DUT]:
|
||||||
|
```verilog
|
||||||
|
module counter(...);
|
||||||
|
...
|
||||||
|
endmodule
|
||||||
|
```
|
||||||
|
|
||||||
|
[Hint] ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、调用流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ waveform_trace_tool() │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. 检查文件是否存在 │
|
||||||
|
│ ├── verilogFilePath │
|
||||||
|
│ └── vcdFilePath │
|
||||||
|
│ │
|
||||||
|
│ 2. 加载AST图(已有JSON) │
|
||||||
|
│ └── graph = loadASTGraph(verilogFilePath) │
|
||||||
|
│ │
|
||||||
|
│ 3. 解析仿真输出,获取出错信号 │
|
||||||
|
│ └── mismatchInfo = parseMismatch(simulationOutput) │
|
||||||
|
│ ├── signals: ['count', 'overflow'] │
|
||||||
|
│ └── firstMismatchTime: 100 │
|
||||||
|
│ │
|
||||||
|
│ 4. BFS回溯,找到控制信号链 │
|
||||||
|
│ └── traceResult = getKControlSignals(graph, signals, k) │
|
||||||
|
│ ├── controlSignals: Map<信号名, 行号> │
|
||||||
|
│ └── signalLevelTracer: [['clk->count'], ...] │
|
||||||
|
│ │
|
||||||
|
│ 5. 读取VCD波形,提取相关信号的值 │
|
||||||
|
│ └── waveformTable = getTabular(vcdPath, signals, offset) │
|
||||||
|
│ │
|
||||||
|
│ 6. 读取Verilog源码 │
|
||||||
|
│ └── verilogCode = readFile(verilogFilePath) │
|
||||||
|
│ │
|
||||||
|
│ 7. 组装调试报告 │
|
||||||
|
│ └── return formatReport(traceResult, waveformTable, code) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、参考实现
|
||||||
|
|
||||||
|
### 5.1 Python源文件位置
|
||||||
|
|
||||||
|
```
|
||||||
|
ast_debug_core/
|
||||||
|
├── ast_node.py # AST节点定义(参考32-137行)
|
||||||
|
├── graph_builder.py # 图构建入口
|
||||||
|
├── debug_graph_analyzer.py # BFS回溯(完整文件,约70行)
|
||||||
|
├── vcd_waveform_analyzer.py # VCD解析(参考89-285行)
|
||||||
|
└── waveform_trace_tool.py # 工具封装(完整文件,约180行)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 关键函数对照表
|
||||||
|
|
||||||
|
| Python函数 | 位置 | TS函数名建议 |
|
||||||
|
|------------|------|--------------|
|
||||||
|
| `get_k_control_signals()` | debug_graph_analyzer.py:20 | `getKControlSignals()` |
|
||||||
|
| `parse_mismatch()` | vcd_waveform_analyzer.py:244 | `parseMismatch()` |
|
||||||
|
| `get_tabular()` | vcd_waveform_analyzer.py:264 | `getTabular()` |
|
||||||
|
| `tabular_via_dataframe()` | vcd_waveform_analyzer.py:95 | `generateWaveformTable()` |
|
||||||
|
| `waveform_trace_tool()` | waveform_trace_tool.py:63 | `waveformTraceTool()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、测试用例
|
||||||
|
|
||||||
|
### 6.1 BFS回溯测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 输入
|
||||||
|
const graph: ASTGraph = /* 加载 counter_ast_graph.json */;
|
||||||
|
const targetSignals = ['count'];
|
||||||
|
const k = 2;
|
||||||
|
|
||||||
|
// 期望输出
|
||||||
|
const expected = {
|
||||||
|
controlSignals: new Map([
|
||||||
|
['count', [6, 6]],
|
||||||
|
['next_count', [10, 10]],
|
||||||
|
['reset', [4, 4]],
|
||||||
|
['clk', [3, 3]],
|
||||||
|
['enable', [5, 5]]
|
||||||
|
]),
|
||||||
|
signalLevelTracer: [
|
||||||
|
['count->count'],
|
||||||
|
['next_count->count', 'reset->count', 'clk->count'],
|
||||||
|
['enable->next_count', 'count->next_count']
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 仿真输出解析测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 输入
|
||||||
|
const testOutput = `
|
||||||
|
Mismatches: 2
|
||||||
|
First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
|
||||||
|
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 期望输出
|
||||||
|
const expected = {
|
||||||
|
signals: ['count', 'overflow'],
|
||||||
|
firstMismatchTime: 100
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、注意事项
|
||||||
|
|
||||||
|
1. **无第三方依赖要求**
|
||||||
|
- BFS回溯和仿真解析完全可以用原生TS实现
|
||||||
|
- VCD解析可以自己实现或找现有库
|
||||||
|
|
||||||
|
2. **性能考虑**
|
||||||
|
- 图遍历使用Map而非Object,提高查找效率
|
||||||
|
- VCD文件可能很大,考虑流式解析
|
||||||
|
|
||||||
|
3. **错误处理**
|
||||||
|
- 文件不存在时返回友好错误信息
|
||||||
|
- 信号不在图中时跳过而非报错
|
||||||
|
|
||||||
|
4. **兼容性**
|
||||||
|
- 信号名可能包含方括号,如 `count[7:0]`
|
||||||
|
- 时间单位统一为ns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、交付物
|
||||||
|
|
||||||
|
1. `debugGraphAnalyzer.ts` - BFS回溯模块
|
||||||
|
2. `simulationParser.ts` - 仿真输出解析模块
|
||||||
|
3. `vcdParser.ts` - VCD波形解析模块
|
||||||
|
4. `waveformTraceTool.ts` - 工具整合封装
|
||||||
|
5. `types.ts` - 类型定义
|
||||||
|
6. 单元测试文件
|
||||||
1403
tools/waveform_trace/src/ast_node.py
Normal file
1403
tools/waveform_trace/src/ast_node.py
Normal file
File diff suppressed because it is too large
Load Diff
70
tools/waveform_trace/src/debug_graph_analyzer.py
Normal file
70
tools/waveform_trace/src/debug_graph_analyzer.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# Author : Chia-Tung (Mark) Ho, NVIDIA
|
||||||
|
#
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
from collections import deque
|
||||||
|
from graph_builder import generate_top_logic_graph
|
||||||
|
|
||||||
|
# use class
|
||||||
|
class DebugGraph:
|
||||||
|
|
||||||
|
def __init__(self, verilog_filelist: list[str]):
|
||||||
|
self.filelist = verilog_filelist
|
||||||
|
self.graph = generate_top_logic_graph(verilog_filelist)
|
||||||
|
# print(list(self.graph.nodes(data=True)))
|
||||||
|
|
||||||
|
def get_k_control_signals(self, target_signals: list[str], k:int, signal_only: bool=False) -> list[str]:
|
||||||
|
|
||||||
|
control_signals = {}
|
||||||
|
signal_level_tracer = []
|
||||||
|
# queue
|
||||||
|
q = deque()
|
||||||
|
tmp_q = deque()
|
||||||
|
|
||||||
|
for signal in target_signals:
|
||||||
|
# store (predecessors, controlled signal)
|
||||||
|
q.append((signal, signal))
|
||||||
|
control_signals[signal] = self.graph.nodes[signal]['lines']
|
||||||
|
|
||||||
|
# BFS
|
||||||
|
for l in range (k + 1):
|
||||||
|
# traverse l layers
|
||||||
|
tmp_q.clear()
|
||||||
|
level_signal_control_rels = []
|
||||||
|
while len(q) > 0:
|
||||||
|
cur_signal = q.popleft()
|
||||||
|
level_signal_control_rels.append(cur_signal[0] + "->" + cur_signal[1])
|
||||||
|
if cur_signal[0] not in control_signals:
|
||||||
|
if self.graph.has_edge(cur_signal[0], cur_signal[1]):
|
||||||
|
# must be the control signals through the edge
|
||||||
|
control_signals[cur_signal[0]] = self.graph[cur_signal[0]][cur_signal[1]]['lines']
|
||||||
|
else:
|
||||||
|
print("[Error] Edge not found! - ", cur_signal)
|
||||||
|
# find the predecessors
|
||||||
|
controls = self.graph.predecessors(cur_signal[0])
|
||||||
|
for c in controls:
|
||||||
|
if c in control_signals:
|
||||||
|
continue
|
||||||
|
# exclude the parameter
|
||||||
|
if 'type' in self.graph.nodes[c] and self.graph.nodes[c]['type'] in ["Parameter", "Localparam"]:
|
||||||
|
continue
|
||||||
|
if signal_only and (re.match('^Always', c) or re.match('^Assign', c) or re.match('^Module', c) or re.match('^IntConst', c)):
|
||||||
|
continue
|
||||||
|
# store (predecessors, controlled signal)
|
||||||
|
tmp_q.append((c, cur_signal[0]))
|
||||||
|
# swap the q
|
||||||
|
assert(len(q) == 0)
|
||||||
|
print(tmp_q)
|
||||||
|
q = copy.deepcopy(tmp_q)
|
||||||
|
# record the signal relations
|
||||||
|
signal_level_tracer.append(level_signal_control_rels)
|
||||||
|
|
||||||
|
return control_signals, signal_level_tracer
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
debug_graph_tracer = DebugGraph(["/home/scratch.chiatungh_nvresearch/hardware-agent-marco/hardware_agent/examples/verilog_testcases/fsm_serialdata.v"])
|
||||||
|
print(debug_graph_tracer.get_k_control_signals(['out_byte', 'done'], k=3, signal_only=True))
|
||||||
144
tools/waveform_trace/src/graph_builder.py
Normal file
144
tools/waveform_trace/src/graph_builder.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# Author : Chia-Tung (Mark) Ho, NVIDIA
|
||||||
|
#
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
# 优先使用本地修改过的 pyverilog(包含 toplogic_tree_traverse 方法)
|
||||||
|
_local_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, _local_path)
|
||||||
|
|
||||||
|
from pyverilog.vparser.parser import parse
|
||||||
|
from io import StringIO
|
||||||
|
import networkx as nx
|
||||||
|
# importing matplotlib.pyplot
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import re
|
||||||
|
|
||||||
|
# create graph from ast str
|
||||||
|
# directed graph from networkX
|
||||||
|
def create_graph_from_ast(ast, display=False, display_signal_only=False):
|
||||||
|
graph = nx.DiGraph()
|
||||||
|
ast.toplogic_tree_traverse(network_G=graph, rvalue=False, lvalue=False)
|
||||||
|
if not display and not display_signal_only:
|
||||||
|
return graph
|
||||||
|
# Print out nodes with attributes
|
||||||
|
nodes_to_display = []
|
||||||
|
edges_to_display = []
|
||||||
|
print("Nodes:")
|
||||||
|
for node, attrs in graph.nodes(data=True):
|
||||||
|
if display_signal_only and (not re.match("^Assign", node) and not re.match("^Always", node) and not re.match("^Module", node)):
|
||||||
|
nodes_to_display.append(node)
|
||||||
|
print(f"Node {node}: {attrs}")
|
||||||
|
|
||||||
|
# Print out edges with attributes
|
||||||
|
print("\nEdges:")
|
||||||
|
for src, dst, attrs in graph.edges(data=True):
|
||||||
|
if display_signal_only and src in nodes_to_display and dst in nodes_to_display:
|
||||||
|
edges_to_display.append((src, dst))
|
||||||
|
print(f"Edge {src} to {dst}: {attrs}")
|
||||||
|
|
||||||
|
# displaying graphs
|
||||||
|
plt.figure(figsize=(18, 16)) # Set the figure size
|
||||||
|
pos = nx.spring_layout(graph, k=1.0)
|
||||||
|
if display_signal_only:
|
||||||
|
subgraph = graph.subgraph(nodes_to_display)
|
||||||
|
# subgraph.add_edges_from(edges_to_display)
|
||||||
|
else:
|
||||||
|
subgraph = graph
|
||||||
|
|
||||||
|
nx.draw_networkx(subgraph, pos, with_labels=True) # Draw the graph without labels
|
||||||
|
|
||||||
|
# Add node labels
|
||||||
|
# node_labels = nx.get_node_attributes(graph, 'label')
|
||||||
|
# nx.draw_networkx_labels(graph, pos, labels=node_labels)
|
||||||
|
|
||||||
|
# edge labels
|
||||||
|
edge_labels = nx.get_edge_attributes(subgraph, 'lines')
|
||||||
|
nx.draw_networkx_edge_labels(
|
||||||
|
subgraph, pos,
|
||||||
|
edge_labels=edge_labels,
|
||||||
|
font_color='blue'
|
||||||
|
)
|
||||||
|
# plt.axis('off')
|
||||||
|
plt.show()
|
||||||
|
return graph
|
||||||
|
|
||||||
|
def get_ast_structure_str(ast):
|
||||||
|
normal_stdout = sys.stdout
|
||||||
|
# put the string output to a string buffer
|
||||||
|
result = StringIO()
|
||||||
|
sys.stdout = result
|
||||||
|
|
||||||
|
# traverse the ast
|
||||||
|
ast.show(buf=sys.stdout)
|
||||||
|
|
||||||
|
# Redirect std output to the normal mode
|
||||||
|
sys.stdout = normal_stdout
|
||||||
|
|
||||||
|
# Get the result out
|
||||||
|
ast_str = result.getvalue()
|
||||||
|
# print('ast str = ', ast_str, '\n ast end')
|
||||||
|
return ast_str
|
||||||
|
|
||||||
|
def generate_top_logic_graph(filelist: list[str]):
|
||||||
|
for f in filelist:
|
||||||
|
if not os.path.exists(f):
|
||||||
|
raise IOError("file not found: " + f)
|
||||||
|
|
||||||
|
ast, directives = parse(filelist,
|
||||||
|
preprocess_include=[],
|
||||||
|
preprocess_define=[])
|
||||||
|
|
||||||
|
# ast_str = get_ast_structure_str(ast)
|
||||||
|
return create_graph_from_ast(ast, display=False, display_signal_only=False)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
INFO = "Verilog code parser"
|
||||||
|
VERSION = pyverilog.__version__
|
||||||
|
USAGE = "Usage: python example_parser.py file ..."
|
||||||
|
|
||||||
|
def showVersion():
|
||||||
|
print(INFO)
|
||||||
|
print(VERSION)
|
||||||
|
print(USAGE)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
optparser = OptionParser()
|
||||||
|
optparser.add_option("-v", "--version", action="store_true", dest="showversion",
|
||||||
|
default=False, help="Show the version")
|
||||||
|
optparser.add_option("-I", "--include", dest="include", action="append",
|
||||||
|
default=[], help="Include path")
|
||||||
|
optparser.add_option("-D", dest="define", action="append",
|
||||||
|
default=[], help="Macro Definition")
|
||||||
|
(options, args) = optparser.parse_args()
|
||||||
|
|
||||||
|
filelist = args
|
||||||
|
# print(filelist)
|
||||||
|
if options.showversion:
|
||||||
|
showVersion()
|
||||||
|
|
||||||
|
for f in filelist:
|
||||||
|
if not os.path.exists(f):
|
||||||
|
raise IOError("file not found: " + f)
|
||||||
|
|
||||||
|
if len(filelist) == 0:
|
||||||
|
showVersion()
|
||||||
|
|
||||||
|
ast, directives = parse(filelist,
|
||||||
|
preprocess_include=options.include,
|
||||||
|
preprocess_define=options.define)
|
||||||
|
|
||||||
|
# ast_str = get_ast_structure_str(ast)
|
||||||
|
create_graph_from_ast(ast, display_signal_only=True, display=True)
|
||||||
|
ast.show(attrnames=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
39241
tools/waveform_trace/src/parser.out
Normal file
39241
tools/waveform_trace/src/parser.out
Normal file
File diff suppressed because it is too large
Load Diff
443
tools/waveform_trace/src/parsetab.py
Normal file
443
tools/waveform_trace/src/parsetab.py
Normal file
File diff suppressed because one or more lines are too long
8
tools/waveform_trace/src/pyverilog/Makefile
Normal file
8
tools/waveform_trace/src/pyverilog/Makefile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
make clean -C ./utils
|
||||||
|
make clean -C ./vparser
|
||||||
|
make clean -C ./dataflow
|
||||||
|
make clean -C ./controlflow
|
||||||
|
make clean -C ./ast_code_generator
|
||||||
|
rm -rf *.pyc __pycache__ *.out parsetab.py *.html
|
||||||
1
tools/waveform_trace/src/pyverilog/VERSION
Normal file
1
tools/waveform_trace/src/pyverilog/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
1.3.0
|
||||||
7
tools/waveform_trace/src/pyverilog/__init__.py
Normal file
7
tools/waveform_trace/src/pyverilog/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), "VERSION")) as f:
|
||||||
|
__version__ = f.read().splitlines()[0]
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf *.pyc __pycache__ parsetab.py *.out
|
||||||
1030
tools/waveform_trace/src/pyverilog/ast_code_generator/codegen.py
Normal file
1030
tools/waveform_trace/src/pyverilog/ast_code_generator/codegen.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,104 @@
|
|||||||
|
Source
|
||||||
|
Description
|
||||||
|
ModuleDef
|
||||||
|
Paramlist
|
||||||
|
Portlist
|
||||||
|
Port
|
||||||
|
Width
|
||||||
|
Length
|
||||||
|
Dimensions
|
||||||
|
Identifier
|
||||||
|
Value
|
||||||
|
Constant
|
||||||
|
IntConst
|
||||||
|
FloatConst
|
||||||
|
StringConst
|
||||||
|
Variable
|
||||||
|
Input
|
||||||
|
Output
|
||||||
|
Inout
|
||||||
|
Tri
|
||||||
|
Wire
|
||||||
|
Reg
|
||||||
|
Integer
|
||||||
|
Real
|
||||||
|
Genvar
|
||||||
|
Ioport
|
||||||
|
Parameter
|
||||||
|
Localparam
|
||||||
|
Decl
|
||||||
|
Concat
|
||||||
|
LConcat
|
||||||
|
Repeat
|
||||||
|
Partselect
|
||||||
|
Pointer
|
||||||
|
Lvalue
|
||||||
|
Rvalue
|
||||||
|
Operator
|
||||||
|
UnaryOperator
|
||||||
|
Uminus
|
||||||
|
Ulnot
|
||||||
|
Unot
|
||||||
|
Uand
|
||||||
|
Unand
|
||||||
|
Uor
|
||||||
|
Unor
|
||||||
|
Uxor
|
||||||
|
Uxnor
|
||||||
|
Power
|
||||||
|
Times
|
||||||
|
Divide
|
||||||
|
Mod
|
||||||
|
Plus
|
||||||
|
Minus
|
||||||
|
Sll
|
||||||
|
Srl
|
||||||
|
Sra
|
||||||
|
LessThan
|
||||||
|
GreaterThan
|
||||||
|
LessEq
|
||||||
|
GreaterEq
|
||||||
|
Eq
|
||||||
|
NotEq
|
||||||
|
Eql
|
||||||
|
NotEql
|
||||||
|
And
|
||||||
|
Xor
|
||||||
|
Xnor
|
||||||
|
Or
|
||||||
|
Land
|
||||||
|
Lor
|
||||||
|
Cond
|
||||||
|
Assign
|
||||||
|
Always
|
||||||
|
SensList
|
||||||
|
Sens
|
||||||
|
Substitution
|
||||||
|
BlockingSubstitution
|
||||||
|
NonblockingSubstitution
|
||||||
|
IfStatement
|
||||||
|
ForStatement
|
||||||
|
WhileStatement
|
||||||
|
CaseStatement
|
||||||
|
Case
|
||||||
|
Block
|
||||||
|
Initial
|
||||||
|
WaitStatement
|
||||||
|
ForeverStatement
|
||||||
|
DelayStatement
|
||||||
|
InstanceList
|
||||||
|
Instance
|
||||||
|
ParamArg
|
||||||
|
PortArg
|
||||||
|
Function
|
||||||
|
FunctionCall
|
||||||
|
Task
|
||||||
|
GenerateStatement
|
||||||
|
SystemCall
|
||||||
|
IdentifierScopeLabel
|
||||||
|
IdentifierScope
|
||||||
|
Pragma
|
||||||
|
PragmaEntry
|
||||||
|
Disable
|
||||||
|
ParallelBlock
|
||||||
|
SingleStatement
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
always @({{ sens_list }}) {{ statement }}
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
assign {{ left }} = {{ right }};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
begin{% if scope != '' %} : {{ scope }}{% endif %}
|
||||||
|
{%- for statement in statements %}
|
||||||
|
{{ statement }}
|
||||||
|
{%- endfor %}
|
||||||
|
end
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{% if ldelay != '' %}{{ ldelay }} {% endif %}{{ left }} = {% if rdelay != '' %}{{ rdelay }} {% endif %}{{ right }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ cond }}: {{ statement }}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
case({{ comp }})
|
||||||
|
{%- for case in caselist %}
|
||||||
|
{{ case }}
|
||||||
|
{%- endfor %}
|
||||||
|
endcase
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
casex({{ comp }})
|
||||||
|
{%- for case in caselist %}
|
||||||
|
{{ case }}
|
||||||
|
{%- endfor %}
|
||||||
|
endcase
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ {% for item in items %}{{ item }}{% if loop.index < len_items %}, {% endif %}{% endfor %} }
|
||||||
@ -0,0 +1 @@
|
|||||||
|
(({{ cond }})? {{ true_value }} : {{ false_value }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ value }}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
{%- for item in items %}{{ item }}
|
||||||
|
{%- endfor %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
#{{ delay }}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
{% for definition in definitions %}
|
||||||
|
{{ definition }}
|
||||||
|
{% endfor %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
diable {{ name }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
@({{ senslist }});
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ value }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
forever {{ statement }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
for({{ pre }} {{ cond }}; {{ post }}) {{ statement }}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
function {{ retwidth }} {{ name }};
|
||||||
|
{%- for s in statement %}
|
||||||
|
{{ s }}
|
||||||
|
{%- endfor %}
|
||||||
|
endfunction
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ name }}({% for arg in args %}{{ arg }}{% if loop.index < len_args %}, {% endif %}{% endfor %})
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
generate {% for item in items %}{{ item }}{% endfor %}
|
||||||
|
endgenerate
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
genvar {{ name }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ scope }}{{ name }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{% for scope in scopes %}{{ scope }}{% endfor %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ name }}{%- if loop != '' %}[{{ loop }}]{%- endif %}.
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
if({{ cond }}) {{ true_statement }}
|
||||||
|
{%- if true_statement[-1] != ' ' and true_statement[-1] != '\n' %} {% endif -%}
|
||||||
|
{%- if true_statement.count('\n') == 0 and false_statement != '' %}
|
||||||
|
{% endif -%}
|
||||||
|
{%- if false_statement != '' %}else {{ false_statement }}{% endif -%}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
initial {{ statement }}
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
inout {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
input {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{{ name }}{{ array }}
|
||||||
|
({% for port in portlist %}
|
||||||
|
{{ port }}{%- if loop.index < len_portlist -%}, {%- endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
)
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
{{ module }}
|
||||||
|
{%- if len_parameterlist > 0 %}
|
||||||
|
#({% for param in parameterlist %}
|
||||||
|
{{ param }}{%- if loop.index < len_parameterlist -%},
|
||||||
|
{%- endif -%}{% endfor %}
|
||||||
|
)
|
||||||
|
{%- endif %}
|
||||||
|
{%- for instance in instances %}
|
||||||
|
{{ instance }}{%- if loop.index < len_instances -%},
|
||||||
|
{%- endif -%}{%- endfor -%};
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ value }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
integer {{ name }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ first }} {% if second != '' %}{{ second }} {% endif %}{% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ {% for item in items %}{{ item }}{% if loop.index < len_items %}, {% endif %}{% endfor %} }
|
||||||
@ -0,0 +1 @@
|
|||||||
|
[{{ msb }}:{{ lsb }}]
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
localparam {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }} = {{ value }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{{ var }}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
module {{ modulename }}{% if paramlist != '' %} #
|
||||||
|
(
|
||||||
|
{{ paramlist }}
|
||||||
|
)
|
||||||
|
{%- endif %}
|
||||||
|
(
|
||||||
|
{{ portlist }}
|
||||||
|
);
|
||||||
|
|
||||||
|
{% for item in items %}{{ item }}
|
||||||
|
{% endfor %}
|
||||||
|
endmodule
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{% if ldelay != '' %}{{ ldelay }} {% endif %}{{ left }} <= {% if rdelay != '' %}{{ rdelay }} {% endif %}{{ right }};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
({{ left }} {{ op }} {{ right }})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
output {% if signed %}signed {% endif %}{% if width != '' %}{{ width }} {% endif %}{{ name }}{% if dimensions != '' %} {{ dimensions }}{% endif %};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
fork{% if scope != '' %} : {{ scope }}{% endif %}
|
||||||
|
{%- for statement in statements %}
|
||||||
|
{{ statement }}
|
||||||
|
{%- endfor %}
|
||||||
|
join
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{%- if paramname != '' -%}.{{ paramname }}({{ argname }}){%- else -%}{{ argname }}{%- endif -%}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user