创建工程

This commit is contained in:
2026-06-08 17:56:41 +08:00
commit f66e9959e5
14 changed files with 2253 additions and 0 deletions
+406
View File
@@ -0,0 +1,406 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { spawn } from 'child_process';
import { getConfig } from './extension';
import { FmdDiagnostics } from './diagnostics';
import { FmdProjectInfo, FmdProjectManager } from './projectManager';
export interface FmdOutputArtifacts {
projectDir: string;
projectName: string;
outputDir: string;
hexFile: string;
binFile: string;
}
export interface FmdBuildResult {
success: boolean;
exitCode: number;
artifacts?: FmdOutputArtifacts;
}
export class FmdCompiler {
private outputChannel: vscode.OutputChannel;
private diagnostics: FmdDiagnostics;
private projectManager: FmdProjectManager;
private building = false;
constructor(
outputChannel: vscode.OutputChannel,
diagnostics: FmdDiagnostics,
projectManager: FmdProjectManager
) {
this.outputChannel = outputChannel;
this.diagnostics = diagnostics;
this.projectManager = projectManager;
}
reloadConfig() {
this.outputChannel.appendLine('[FMD] 配置已更新');
}
/**
* 编译整个工程(所有 .c/.C 文件 + 链接)
*/
async buildProject(): Promise<void> {
const result = await this.buildProjectWithResult();
if (!result.success && result.exitCode !== -1) {
vscode.window.showErrorMessage(`FMD: 编译失败,退出码 ${result.exitCode},请查看输出面板`);
}
}
async buildProjectWithResult(): Promise<FmdBuildResult> {
if (this.building) {
vscode.window.showWarningMessage('FMD: 编译正在进行中...');
return { success: false, exitCode: -1 };
}
const cfg = getConfig();
// 自动保存
if (cfg.autoSaveBeforeBuild) {
await vscode.workspace.saveAll(false);
}
const projectInfo = await this.getProjectInfo();
const projectDir = projectInfo?.projectDir || await this.getProjectDir();
if (!projectDir) {
return { success: false, exitCode: -1 };
}
const projectName = projectInfo?.projectName || path.basename(projectDir);
const outputDir = cfg.outputDir || projectDir;
const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir);
this.building = true;
this.diagnostics.clear();
if (cfg.showOutputOnBuild) {
this.outputChannel.show(true);
}
this.outputChannel.appendLine('');
this.outputChannel.appendLine(`========== FMD 开始编译: ${projectName} ==========`);
this.outputChannel.appendLine(`工程目录: ${projectDir}`);
this.outputChannel.appendLine(`芯片: ${cfg.chip}`);
if (projectInfo?.device && projectInfo.device !== cfg.chip) {
this.outputChannel.appendLine(`[警告] 配置芯片 ${cfg.chip} 与工程文件 Device ${projectInfo.device} 不一致,本次编译使用配置芯片 ${cfg.chip}`);
}
this.outputChannel.appendLine(new Date().toLocaleString());
this.outputChannel.appendLine('');
try {
const cFiles = this.getSourceFiles(projectDir, projectInfo);
if (cFiles.length === 0) {
vscode.window.showErrorMessage('工程目录中没有找到 .c/.C 文件');
return { success: false, exitCode: -1, artifacts };
}
this.outputChannel.appendLine(`源文件 (${cFiles.length} 个):`);
cFiles.forEach(f => this.outputChannel.appendLine(` ${path.basename(f)}`));
this.outputChannel.appendLine('');
// 构建编译命令
// c.exe 是 XC8-like 驱动,可以接受多文件一次编译+链接
const args = this.buildCompileArgs(cFiles, projectDir, outputDir, projectName, cfg);
this.outputChannel.appendLine('编译命令:');
this.outputChannel.appendLine(` ${cfg.compilerPath} ${args.join(' ')}`);
this.outputChannel.appendLine('');
const exitCode = await this.runCompiler(cfg.compilerPath, args, projectDir);
if (exitCode === 0) {
this.outputChannel.appendLine('');
this.outputChannel.appendLine('========== 编译成功 ==========');
this.logArtifact(artifacts.hexFile);
this.logArtifact(artifacts.binFile);
vscode.window.showInformationMessage('FMD: 编译成功 ✓');
return { success: true, exitCode, artifacts };
}
this.outputChannel.appendLine('');
this.outputChannel.appendLine(`========== 编译失败 (退出码: ${exitCode}) ==========`);
return { success: false, exitCode, artifacts };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.outputChannel.appendLine(`[错误] ${msg}`);
vscode.window.showErrorMessage(`FMD 编译异常: ${msg}`);
return { success: false, exitCode: -1, artifacts };
} finally {
this.building = false;
}
}
/**
* 仅编译单个 .c 文件(生成 .obj,不链接)
*/
async buildFile(filePath: string): Promise<void> {
if (this.building) {
vscode.window.showWarningMessage('FMD: 编译正在进行中...');
return;
}
const cfg = getConfig();
if (cfg.autoSaveBeforeBuild) {
await vscode.workspace.saveAll(false);
}
const projectDir = path.dirname(filePath);
const fileName = path.basename(filePath);
this.building = true;
this.diagnostics.clear();
if (cfg.showOutputOnBuild) {
this.outputChannel.show(true);
}
this.outputChannel.appendLine('');
this.outputChannel.appendLine(`========== FMD 编译文件: ${fileName} ==========`);
try {
// 单文件编译:只预处理+编译到 .obj,不链接
const args = [
`--chip=${cfg.chip}`,
'-Q', // 静默链接器
'-P', // 仅编译,不链接(生成 .p1 和 .obj)
`-I${path.join(path.dirname(cfg.compilerPath), '..', 'include')}`,
filePath,
];
if (cfg.extraArgs) {
args.push(...cfg.extraArgs.split(' ').filter(a => a));
}
this.outputChannel.appendLine(`命令: ${cfg.compilerPath} ${args.join(' ')}`);
const exitCode = await this.runCompiler(cfg.compilerPath, args, projectDir);
if (exitCode === 0) {
this.outputChannel.appendLine('单文件编译成功');
vscode.window.showInformationMessage(`FMD: ${fileName} 编译成功 ✓`);
} else {
vscode.window.showErrorMessage(`FMD: ${fileName} 编译失败`);
}
} finally {
this.building = false;
}
}
/**
* 清理编译产物
*/
async cleanProject(): Promise<void> {
const projectDir = await this.getProjectDir();
if (!projectDir) {
return;
}
const cleanExts = ['.obj', '.p1', '.pre', '.d', '.lpp', '.cmf', '.sym', '.map', '.rlf', '.sdb', '.asm'];
let count = 0;
try {
const files = fs.readdirSync(projectDir);
for (const f of files) {
const ext = path.extname(f).toLowerCase();
if (cleanExts.includes(ext)) {
fs.unlinkSync(path.join(projectDir, f));
count++;
}
}
this.outputChannel.appendLine(`[FMD] 清理完成,删除 ${count} 个中间文件`);
vscode.window.showInformationMessage(`FMD: 清理完成,删除 ${count} 个中间文件`);
} catch (err) {
vscode.window.showErrorMessage(`FMD 清理失败: ${err}`);
}
}
async resolveOutputArtifacts(): Promise<FmdOutputArtifacts | undefined> {
const cfg = getConfig();
const projectInfo = await this.getProjectInfo();
const projectDir = projectInfo?.projectDir || await this.getProjectDir();
if (!projectDir) {
return undefined;
}
const projectName = projectInfo?.projectName || path.basename(projectDir);
const outputDir = cfg.outputDir || projectDir;
return this.getOutputArtifacts(projectDir, projectName, outputDir);
}
getOutputArtifacts(projectDir: string, projectName: string, outputDir: string): FmdOutputArtifacts {
return {
projectDir,
projectName,
outputDir,
hexFile: path.join(outputDir, projectName + '.hex'),
binFile: path.join(outputDir, projectName + '.bin'),
};
}
/**
* 构造编译参数
* 基于对 .map 文件的分析,c.exe 是 XC8-style 驱动器
* 标准调用:c.exe --chip=CHIP [options] file1.c file2.c ...
*/
private buildCompileArgs(
cFiles: string[],
projectDir: string,
outputDir: string,
projectName: string,
cfg: ReturnType<typeof getConfig>
): string[] {
const compilerBinDir = path.dirname(cfg.compilerPath);
const compilerDataDir = path.dirname(compilerBinDir); // data/
const includeDir = path.join(compilerDataDir, 'include');
const args: string[] = [
`--chip=${cfg.chip}`,
`-I${includeDir}`,
`-o${path.join(outputDir, projectName + '.hex')}`,
];
// 额外参数
if (cfg.extraArgs) {
args.push(...cfg.extraArgs.split(' ').filter(a => a));
}
// 源文件
args.push(...cFiles);
return args;
}
/**
* 执行编译器,实时输出日志
*/
private runCompiler(
compilerPath: string,
args: string[],
cwd: string
): Promise<number> {
return new Promise((resolve, reject) => {
// 检查编译器是否存在
if (!fs.existsSync(compilerPath)) {
reject(new Error(`编译器不存在: ${compilerPath}\n请检查 fmdCompiler.compilerPath 配置`));
return;
}
const proc = spawn(compilerPath, args, {
cwd,
shell: false,
env: { ...process.env },
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data: Buffer) => {
const text = data.toString();
stdout += text;
// 实时输出,逐行过滤空行
text.split('\n').forEach(line => {
if (line.trim()) {
this.outputChannel.appendLine(line.replace(/\r$/, ''));
}
});
});
proc.stderr.on('data', (data: Buffer) => {
const text = data.toString();
stderr += text;
text.split('\n').forEach(line => {
if (line.trim()) {
this.outputChannel.appendLine(line.replace(/\r$/, ''));
}
});
});
proc.on('close', (code: number | null) => {
const allOutput = stdout + stderr;
// 解析错误/警告,推送到诊断
this.diagnostics.parse(allOutput);
resolve(code ?? 1);
});
proc.on('error', (err: Error) => {
reject(err);
});
});
}
/**
* 获取工程目录
*/
private async getProjectDir(): Promise<string | undefined> {
const cfg = getConfig();
// 优先用配置指定的 .prj 文件目录
if (cfg.projectFile && fs.existsSync(cfg.projectFile)) {
return path.dirname(cfg.projectFile);
}
// 其次用 ProjectManager 自动检测到的
const detected = this.projectManager.getProjectDir();
if (detected) {
return detected;
}
// 最后用当前打开的文件目录
const editor = vscode.window.activeTextEditor;
if (editor) {
return path.dirname(editor.document.fileName);
}
vscode.window.showErrorMessage('FMD: 未找到工程目录,请打开 .c 文件或配置 fmdCompiler.projectFile');
return undefined;
}
private async getProjectInfo(): Promise<FmdProjectInfo | undefined> {
const cfg = getConfig();
try {
if (cfg.projectFile && fs.existsSync(cfg.projectFile)) {
return this.projectManager.getProjectInfo(cfg.projectFile);
}
return this.projectManager.getProjectInfo();
} catch (err) {
this.outputChannel.appendLine(`[警告] 读取工程文件失败: ${err}`);
return undefined;
}
}
private getSourceFiles(projectDir: string, projectInfo?: FmdProjectInfo): string[] {
const projectSources = projectInfo?.sourceFiles
.filter(f => /\.[cC]$/.test(f) && fs.existsSync(f));
if (projectSources && projectSources.length > 0) {
return this.sortSourceFiles(projectSources);
}
// 收集所有 .c/.C 文件
return this.sortSourceFiles(fs.readdirSync(projectDir)
.filter(f => /\.[cC]$/.test(f))
.map(f => path.join(projectDir, f)));
}
private sortSourceFiles(files: string[]): string[] {
// 这个 8 位 MCU 编译器对多源文件顺序较敏感。历史 IDE 输出中源文件按文件名稳定排序,
// 例如 1028.c 在 2288_test.C 前;保持该顺序可避免部分工程触发工具链内部错误。
return [...files].sort((a, b) => path.basename(a).localeCompare(path.basename(b), undefined, {
numeric: false,
sensitivity: 'base',
}));
}
private logArtifact(filePath: string): void {
if (fs.existsSync(filePath)) {
const stat = fs.statSync(filePath);
this.outputChannel.appendLine(`输出: ${filePath} (${stat.size} 字节)`);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
import * as vscode from 'vscode';
import * as path from 'path';
interface DiagEntry {
file: string;
line: number;
col: number;
severity: vscode.DiagnosticSeverity;
message: string;
code?: string;
}
export class FmdDiagnostics {
private collection: vscode.DiagnosticCollection;
constructor(collection: vscode.DiagnosticCollection) {
this.collection = collection;
}
clear() {
this.collection.clear();
}
/**
* 解析编译器输出,提取错误和警告
*
* 支持的格式:
* 1. GCC 风格: file.c:12: error: xxx
* 2. XC8 风格: file.c:12:5: error: xxx
* 3. Linker 风格:(1234) Error [L1234] xxx
*/
parse(output: string) {
const entries: DiagEntry[] = [];
// GCC/XC8 风格错误
// 匹配:文件路径:行号: severity: 消息
// 或: 文件路径:行号:列号: severity: 消息
const gccPattern = /^((?:[A-Za-z]:)?[^:]+\.[cChH]):(\d+)(?::(\d+))?:\s*(error|warning|note):\s*(.+)$/gm;
let m: RegExpExecArray | null;
while ((m = gccPattern.exec(output)) !== null) {
const [, file, lineStr, colStr, sev, msg] = m;
entries.push({
file: this.normalizePath(file),
line: parseInt(lineStr, 10) - 1,
col: colStr ? parseInt(colStr, 10) - 1 : 0,
severity: this.mapSeverity(sev),
message: msg.trim(),
});
}
// XC8 linker 错误:如 :error: (1234) xxx
const linkerPattern = /^.*?(error|warning)\s*\[([A-Z]\d+)\]\s*(.+)$/gim;
while ((m = linkerPattern.exec(output)) !== null) {
const [, sev, code, msg] = m;
// 链接器错误没有文件位置,用一个特殊处理
entries.push({
file: '',
line: 0,
col: 0,
severity: this.mapSeverity(sev),
message: `[${code}] ${msg.trim()}`,
code,
});
}
// 按文件分组,推送到 VSCode
const fileMap = new Map<string, vscode.Diagnostic[]>();
for (const entry of entries) {
if (!entry.file) {
continue;
}
const diag = new vscode.Diagnostic(
new vscode.Range(entry.line, entry.col, entry.line, entry.col + 999),
entry.message,
entry.severity
);
if (entry.code) {
diag.code = entry.code;
}
const key = entry.file;
if (!fileMap.has(key)) {
fileMap.set(key, []);
}
fileMap.get(key)!.push(diag);
}
for (const [filePath, diags] of fileMap) {
const uri = vscode.Uri.file(filePath);
this.collection.set(uri, diags);
}
}
private normalizePath(filePath: string): string {
// 确保路径分隔符统一
return filePath.replace(/\//g, path.sep);
}
private mapSeverity(sev: string): vscode.DiagnosticSeverity {
switch (sev.toLowerCase()) {
case 'error': return vscode.DiagnosticSeverity.Error;
case 'warning': return vscode.DiagnosticSeverity.Warning;
case 'note': return vscode.DiagnosticSeverity.Information;
default: return vscode.DiagnosticSeverity.Error;
}
}
}
+397
View File
@@ -0,0 +1,397 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { getConfig } from './extension';
import { FmdCompiler } from './compiler';
import { FmdProjectInfo, FmdProjectManager } from './projectManager';
import { expandArgs, expandTokens, TokenContext, ToolRunner } from './toolRunner';
import { extractRange, writeIntelHex } from './intelHex';
interface EepromGeometry {
baseAddress: number;
start: number;
size: number;
fill: number;
}
export class FmdEepromManager {
private panel: vscode.WebviewPanel | undefined;
private data: Uint8Array | undefined;
private eepromFile: string | undefined;
private geometry: EepromGeometry | undefined;
private runner = new ToolRunner();
constructor(
private outputChannel: vscode.OutputChannel,
private projectManager: FmdProjectManager,
private compiler: FmdCompiler
) {}
async openEditor(): Promise<void> {
const state = await this.loadState();
if (!state) {
return;
}
this.data = state.data;
this.eepromFile = state.eepromFile;
this.geometry = state.geometry;
if (!this.panel) {
this.panel = vscode.window.createWebviewPanel(
'fmdEeprom',
'FMD EEPROM',
vscode.ViewColumn.One,
{ enableScripts: true }
);
this.panel.onDidDispose(() => this.panel = undefined);
this.panel.webview.onDidReceiveMessage(async message => {
if (message.command === 'save') {
await this.saveFromWebview(message.values || {});
} else if (message.command === 'reload') {
await this.openEditor();
} else if (message.command === 'export') {
await this.exportEepromHex();
}
});
}
this.panel.webview.html = this.renderWebview(state.data, state.geometry, state.labels, state.eepromFile);
this.panel.reveal();
}
async exportEepromHex(): Promise<void> {
if (!this.data || !this.geometry) {
const state = await this.loadState();
if (!state) {
return;
}
this.data = state.data;
this.geometry = state.geometry;
this.eepromFile = state.eepromFile;
}
const target = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(this.eepromFile || 'eeprom.eep.hex'),
filters: {
'Intel HEX': ['hex', 'eep', 'eep.hex'],
'所有文件': ['*'],
},
});
if (!target) {
return;
}
fs.writeFileSync(target.fsPath, writeIntelHex(this.data, this.geometry.baseAddress));
this.eepromFile = target.fsPath;
vscode.window.showInformationMessage(`FMD: EEPROM 已导出: ${target.fsPath}`);
}
async readEeprom(): Promise<void> {
const cfg = getConfig();
const state = await this.loadState();
if (!state) {
return;
}
if (!cfg.programmerPath || cfg.eepromReadArgs.length === 0) {
vscode.window.showWarningMessage('FMD: 未配置 EEPROM 读取命令,请设置 fmdCompiler.programmerPath 和 fmdCompiler.eepromReadArgs');
return;
}
const artifacts = await this.compiler.resolveOutputArtifacts();
if (!artifacts) {
return;
}
const context = this.createTokenContext(artifacts, state.eepromFile, state.geometry);
const result = await this.runner.run({
executable: cfg.programmerPath,
args: expandArgs(cfg.eepromReadArgs, context),
cwd: expandTokens(cfg.programmerCwd || '${projectDir}', context),
shell: cfg.programmerUseShell,
outputChannel: this.outputChannel,
label: 'FMD 读取 EEPROM',
successExitCodes: cfg.programmerSuccessExitCodes,
});
if (result.success) {
vscode.window.showInformationMessage('FMD: EEPROM 读取完成 ✓');
await this.openEditor();
} else {
vscode.window.showErrorMessage(`FMD: EEPROM 读取失败,退出码 ${result.exitCode}`);
}
}
async writeEeprom(): Promise<void> {
if (this.data && this.geometry && this.eepromFile) {
fs.writeFileSync(this.eepromFile, writeIntelHex(this.data, this.geometry.baseAddress));
}
const cfg = getConfig();
const state = await this.loadState();
if (!state) {
return;
}
if (!cfg.programmerPath || cfg.eepromWriteArgs.length === 0) {
vscode.window.showWarningMessage('FMD: 未配置 EEPROM 写入命令,请设置 fmdCompiler.programmerPath 和 fmdCompiler.eepromWriteArgs');
return;
}
const artifacts = await this.compiler.resolveOutputArtifacts();
if (!artifacts) {
return;
}
const context = this.createTokenContext(artifacts, state.eepromFile, state.geometry);
const result = await this.runner.run({
executable: cfg.programmerPath,
args: expandArgs(cfg.eepromWriteArgs, context),
cwd: expandTokens(cfg.programmerCwd || '${projectDir}', context),
shell: cfg.programmerUseShell,
outputChannel: this.outputChannel,
label: 'FMD 写入 EEPROM',
successExitCodes: cfg.programmerSuccessExitCodes,
});
if (result.success) {
vscode.window.showInformationMessage('FMD: EEPROM 写入完成 ✓');
} else {
vscode.window.showErrorMessage(`FMD: EEPROM 写入失败,退出码 ${result.exitCode}`);
}
}
private async saveFromWebview(values: Record<string, string>): Promise<void> {
if (!this.data || !this.geometry || !this.eepromFile) {
return;
}
for (const [key, value] of Object.entries(values)) {
const index = parseInt(key, 10);
const byte = parseInt(value, 16);
if (!Number.isNaN(index) && !Number.isNaN(byte) && index >= 0 && index < this.data.length) {
this.data[index] = byte & 0xff;
}
}
fs.writeFileSync(this.eepromFile, writeIntelHex(this.data, this.geometry.baseAddress));
vscode.window.showInformationMessage(`FMD: EEPROM 已保存: ${this.eepromFile}`);
}
private async loadState(): Promise<{ data: Uint8Array; geometry: EepromGeometry; labels: Map<number, string[]>; eepromFile: string } | undefined> {
const projectInfo = this.getProjectInfo();
const artifacts = await this.compiler.resolveOutputArtifacts();
const projectDir = projectInfo?.projectDir || artifacts?.projectDir;
const projectName = projectInfo?.projectName || artifacts?.projectName;
if (!projectDir || !projectName) {
vscode.window.showErrorMessage('FMD: 未找到工程,无法打开 EEPROM');
return undefined;
}
const geometry = this.resolveGeometry(projectDir, projectName);
const eepromFile = this.resolveEepromFile(projectDir, projectName, projectInfo);
const data = this.loadEepromData(eepromFile, artifacts?.hexFile, geometry);
const labels = this.loadLabels(projectInfo, projectDir);
return { data, geometry, labels, eepromFile };
}
private resolveGeometry(projectDir: string, projectName: string): EepromGeometry {
const cfg = getConfig();
const mapFile = path.join(projectDir, projectName + '.map');
const configured = {
baseAddress: parseNumber(cfg.eepromBaseAddress, 0x2100),
start: parseNumber(cfg.eepromStart, 0),
size: cfg.eepromSize,
fill: parseNumber(cfg.eepromFill, 0xff),
};
if (!fs.existsSync(mapFile)) {
return configured;
}
const text = fs.readFileSync(mapFile, 'utf8');
const m = /-AEEDATA=([0-9A-Fa-f]+)h-([0-9A-Fa-f]+)h\/([0-9A-Fa-f]+)h/.exec(text);
if (!m) {
return configured;
}
const start = parseInt(m[1], 16);
const end = parseInt(m[2], 16);
const baseAddress = parseInt(m[3], 16);
return {
baseAddress,
start,
size: end - start + 1,
fill: configured.fill,
};
}
private resolveEepromFile(projectDir: string, projectName: string, projectInfo?: FmdProjectInfo): string {
const cfg = getConfig();
if (cfg.eepromImageFile) {
return path.isAbsolute(cfg.eepromImageFile) ? cfg.eepromImageFile : path.join(projectDir, cfg.eepromImageFile);
}
if (projectInfo?.eeFile) {
return projectInfo.eeFile;
}
return path.join(projectDir, `${projectName}.eep.hex`);
}
private loadEepromData(eepromFile: string, hexFile: string | undefined, geometry: EepromGeometry): Uint8Array {
if (fs.existsSync(eepromFile)) {
return extractRange(fs.readFileSync(eepromFile, 'utf8'), geometry.baseAddress, geometry.size, geometry.fill);
}
if (hexFile && fs.existsSync(hexFile)) {
return extractRange(fs.readFileSync(hexFile, 'utf8'), geometry.baseAddress, geometry.size, geometry.fill);
}
const data = new Uint8Array(geometry.size);
data.fill(geometry.fill & 0xff);
return data;
}
private loadLabels(projectInfo: FmdProjectInfo | undefined, projectDir: string): Map<number, string[]> {
const labels = new Map<number, string[]>();
const files = new Set<string>(projectInfo ? [...projectInfo.sourceFiles, ...projectInfo.headerFiles] : []);
if (files.size === 0) {
for (const file of fs.readdirSync(projectDir)) {
if (/\.[cChH]$/.test(file)) {
files.add(path.join(projectDir, file));
}
}
}
for (const file of files) {
if (!fs.existsSync(file)) {
continue;
}
const text = fs.readFileSync(file, 'utf8');
const pattern = /^\s*#\s*define\s+(eeprom_[A-Za-z0-9_]+)\s+(0x[0-9A-Fa-f]+|\d+)/gm;
let m: RegExpExecArray | null;
while ((m = pattern.exec(text)) !== null) {
const address = parseNumber(m[2], -1);
if (address >= 0) {
const arr = labels.get(address) || [];
arr.push(m[1]);
labels.set(address, arr);
}
}
}
return labels;
}
private renderWebview(data: Uint8Array, geometry: EepromGeometry, labels: Map<number, string[]>, eepromFile: string): string {
const rows = Array.from(data).map((value, index) => {
const logical = geometry.start + index;
const absolute = geometry.baseAddress + index;
const label = labels.get(logical)?.join(', ') || '';
const ascii = value >= 32 && value <= 126 ? String.fromCharCode(value) : '.';
return `<tr>
<td>0x${logical.toString(16).toUpperCase().padStart(2, '0')}</td>
<td>0x${absolute.toString(16).toUpperCase().padStart(4, '0')}</td>
<td><input data-index="${index}" value="${value.toString(16).toUpperCase().padStart(2, '0')}" maxlength="2" /></td>
<td>${escapeHtml(ascii)}</td>
<td>${escapeHtml(label)}</td>
</tr>`;
}).join('');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
body { font-family: var(--vscode-font-family); padding: 12px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid var(--vscode-panel-border); padding: 4px 8px; text-align: left; }
input { width: 3em; font-family: monospace; }
.actions { margin: 12px 0; display: flex; gap: 8px; }
.meta { color: var(--vscode-descriptionForeground); }
</style>
</head>
<body>
<h2>FMD EEPROM 编辑器</h2>
<div class="meta">文件: ${escapeHtml(eepromFile)}</div>
<div class="meta">Base: 0x${geometry.baseAddress.toString(16).toUpperCase()}Size: ${geometry.size} bytes</div>
<div class="actions">
<button id="save">保存</button>
<button id="export">导出 HEX</button>
<button id="reload">重新加载</button>
</div>
<table>
<thead><tr><th>逻辑地址</th><th>HEX 地址</th><th>值</th><th>ASCII</th><th>标签</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<script>
const vscode = acquireVsCodeApi();
document.getElementById('save').addEventListener('click', () => {
const values = {};
document.querySelectorAll('input[data-index]').forEach(input => values[input.dataset.index] = input.value);
vscode.postMessage({ command: 'save', values });
});
document.getElementById('export').addEventListener('click', () => vscode.postMessage({ command: 'export' }));
document.getElementById('reload').addEventListener('click', () => vscode.postMessage({ command: 'reload' }));
document.querySelectorAll('input[data-index]').forEach(input => {
input.addEventListener('input', () => input.value = input.value.replace(/[^0-9a-fA-F]/g, '').toUpperCase());
});
</script>
</body>
</html>`;
}
private createTokenContext(artifacts: { projectDir: string; projectName: string; hexFile: string; binFile: string }, eepromFile: string, geometry: EepromGeometry): TokenContext {
const cfg = getConfig();
return {
chip: cfg.chip,
projectFile: this.projectManager.getProjectFile() || cfg.projectFile,
projectDir: artifacts.projectDir,
projectName: artifacts.projectName,
compilerPath: cfg.compilerPath,
hexFile: artifacts.hexFile,
binFile: artifacts.binFile,
downloadFile: cfg.downloadFileType === 'bin' ? artifacts.binFile : artifacts.hexFile,
eepromFile,
eepromBaseAddress: `0x${geometry.baseAddress.toString(16).toUpperCase()}`,
eepromSize: String(geometry.size),
workspaceFolder: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '',
};
}
private getProjectInfo(): FmdProjectInfo | undefined {
const cfg = getConfig();
try {
if (cfg.projectFile && fs.existsSync(cfg.projectFile)) {
return this.projectManager.getProjectInfo(cfg.projectFile);
}
return this.projectManager.getProjectInfo();
} catch {
return undefined;
}
}
}
function parseNumber(value: string, fallback: number): number {
const trimmed = value.trim();
if (/^0x/i.test(trimmed)) {
return parseInt(trimmed.slice(2), 16);
}
if (/^[0-9A-Fa-f]+h$/i.test(trimmed)) {
return parseInt(trimmed.slice(0, -1), 16);
}
const parsed = parseInt(trimmed, 10);
return Number.isNaN(parsed) ? fallback : parsed;
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
+247
View File
@@ -0,0 +1,247 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { FmdCompiler } from './compiler';
import { FmdDiagnostics } from './diagnostics';
import { FmdProjectManager } from './projectManager';
import { FmdProgrammer } from './programmer';
import { FmdEepromManager } from './eepromManager';
let outputChannel: vscode.OutputChannel;
let diagnosticsCollection: vscode.DiagnosticCollection;
let compiler: FmdCompiler;
let diagnostics: FmdDiagnostics;
let projectManager: FmdProjectManager;
let programmer: FmdProgrammer;
let eepromManager: FmdEepromManager;
export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('FMD Compiler');
diagnosticsCollection = vscode.languages.createDiagnosticCollection('fmd');
diagnostics = new FmdDiagnostics(diagnosticsCollection);
projectManager = new FmdProjectManager();
compiler = new FmdCompiler(outputChannel, diagnostics, projectManager);
programmer = new FmdProgrammer(outputChannel, projectManager, compiler);
eepromManager = new FmdEepromManager(outputChannel, projectManager, compiler);
// 注册命令
context.subscriptions.push(
vscode.commands.registerCommand('fmdCompiler.build', () => compiler.buildProject()),
vscode.commands.registerCommand('fmdCompiler.buildFile', (uri?: vscode.Uri) => {
const filePath = uri?.fsPath || vscode.window.activeTextEditor?.document.fileName;
if (filePath) {
compiler.buildFile(filePath);
} else {
vscode.window.showWarningMessage('没有可编译的文件');
}
}),
vscode.commands.registerCommand('fmdCompiler.clean', () => compiler.cleanProject()),
vscode.commands.registerCommand('fmdCompiler.selectProject', (uri?: vscode.Uri) => {
if (uri) {
projectManager.setProjectFile(uri.fsPath);
vscode.window.showInformationMessage(`已选择工程: ${path.basename(uri.fsPath)}`);
} else {
projectManager.pickProjectFile();
}
updateStatusBars();
}),
vscode.commands.registerCommand('fmdCompiler.openOutput', () => {
outputChannel.show();
}),
vscode.commands.registerCommand('fmdCompiler.setCompilerPath', () => setCompilerPath()),
vscode.commands.registerCommand('fmdCompiler.detectCompilerPath', () => detectCompilerPath()),
vscode.commands.registerCommand('fmdCompiler.selectChip', () => selectChip()),
vscode.commands.registerCommand('fmdCompiler.syncChipFromProject', () => syncChipFromProject()),
vscode.commands.registerCommand('fmdCompiler.configureProgrammer', () => programmer.configureProgrammer()),
vscode.commands.registerCommand('fmdCompiler.download', () => programmer.download()),
vscode.commands.registerCommand('fmdCompiler.buildAndDownload', () => programmer.buildAndDownload()),
vscode.commands.registerCommand('fmdCompiler.openEeprom', () => eepromManager.openEditor()),
vscode.commands.registerCommand('fmdCompiler.readEeprom', () => eepromManager.readEeprom()),
vscode.commands.registerCommand('fmdCompiler.writeEeprom', () => eepromManager.writeEeprom()),
vscode.commands.registerCommand('fmdCompiler.exportEepromHex', () => eepromManager.exportEepromHex()),
diagnosticsCollection
);
// 状态栏
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
statusBar.command = 'fmdCompiler.build';
const chipStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 99);
chipStatusBar.command = 'fmdCompiler.selectChip';
const downloadStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 98);
downloadStatusBar.command = 'fmdCompiler.download';
const update = () => {
const cfg = getConfig();
const projectFile = projectManager.getProjectFile() || cfg.projectFile;
statusBar.text = '$(play) FMD Build';
statusBar.tooltip = projectFile ? `编译 FMD 工程: ${projectFile}` : '编译 FMD 工程 (F7)';
chipStatusBar.text = `$(circuit-board) ${cfg.chip}`;
chipStatusBar.tooltip = '切换 FMD 目标芯片';
downloadStatusBar.text = '$(cloud-upload) FMD Download';
downloadStatusBar.tooltip = cfg.programmerPath ? `下载程序: ${cfg.programmerPath}` : '未配置烧录工具,点击配置后可下载';
statusBar.show();
chipStatusBar.show();
downloadStatusBar.show();
};
updateStatusBars = update;
updateStatusBars();
context.subscriptions.push(statusBar, chipStatusBar, downloadStatusBar);
// 监听配置变化
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('fmdCompiler')) {
compiler.reloadConfig();
updateStatusBars();
}
})
);
// 尝试自动找工程文件
projectManager.autoDetectProject();
updateStatusBars();
outputChannel.appendLine('[FMD] 插件已激活');
outputChannel.appendLine(`[FMD] 编译器: ${getConfig().compilerPath}`);
}
export function deactivate() {
diagnosticsCollection?.dispose();
outputChannel?.dispose();
}
let updateStatusBars = () => {};
async function setCompilerPath(): Promise<void> {
const cfg = getConfig();
const files = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: '选择 FMD 编译器 c.exe',
defaultUri: cfg.compilerPath ? vscode.Uri.file(path.dirname(cfg.compilerPath)) : undefined,
filters: {
'Compiler': ['exe'],
'所有文件': ['*'],
},
});
if (!files || files.length === 0) {
return;
}
await vscode.workspace.getConfiguration('fmdCompiler').update('compilerPath', files[0].fsPath, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`FMD: 编译器路径已设置: ${files[0].fsPath}`);
}
async function detectCompilerPath(): Promise<void> {
const cfg = getConfig();
const candidates = Array.from(new Set([
...cfg.compilerSearchPaths,
'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe',
'C:\\Program Files\\CCompiler\\Compiler\\data\\bin\\c.exe',
]));
const found = candidates.find(file => fs.existsSync(file));
if (!found) {
vscode.window.showWarningMessage('FMD: 未自动找到编译器,请手动选择');
await setCompilerPath();
return;
}
const answer = await vscode.window.showInformationMessage(`找到 FMD 编译器: ${found}`, '使用此路径', '取消');
if (answer === '使用此路径') {
await vscode.workspace.getConfiguration('fmdCompiler').update('compilerPath', found, vscode.ConfigurationTarget.Workspace);
}
}
async function selectChip(): Promise<void> {
const cfg = getConfig();
const chips = collectChipCandidates();
const selected = await vscode.window.showQuickPick(chips, {
title: '选择 FMD 目标芯片',
placeHolder: cfg.chip,
});
if (!selected) {
return;
}
await vscode.workspace.getConfiguration('fmdCompiler').update('chip', selected, vscode.ConfigurationTarget.Workspace);
outputChannel.appendLine(`[FMD] 已切换芯片: ${selected}`);
const projectFile = projectManager.getProjectFile() || cfg.projectFile;
if (projectFile && fs.existsSync(projectFile)) {
const answer = await vscode.window.showInformationMessage(`是否同步修改工程文件 Device 为 ${selected}?`, '同步', '仅修改 VS Code 设置');
if (answer === '同步') {
await projectManager.updateProjectDevice(projectFile, selected);
vscode.window.showInformationMessage(`FMD: 已更新工程芯片: ${selected}`);
}
}
updateStatusBars();
}
async function syncChipFromProject(): Promise<void> {
const projectChip = projectManager.getCurrentChip(getConfig().projectFile);
if (!projectChip) {
vscode.window.showWarningMessage('FMD: 当前工程文件中没有找到 Device 字段');
return;
}
await vscode.workspace.getConfiguration('fmdCompiler').update('chip', projectChip, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`FMD: 已从工程同步芯片: ${projectChip}`);
}
function collectChipCandidates(): string[] {
const cfg = getConfig();
const chips = new Set<string>(['FT61FC6X', cfg.chip]);
const projectChip = projectManager.getCurrentChip(cfg.projectFile);
if (projectChip) {
chips.add(projectChip);
}
const includeDir = path.join(path.dirname(cfg.compilerPath), '..', 'include');
try {
for (const file of fs.readdirSync(includeDir)) {
const m = /([A-Z]{2}\d+[A-Z0-9]+)/i.exec(file);
if (m) {
chips.add(m[1].toUpperCase());
}
}
} catch {
// 没有 include 目录时忽略
}
return Array.from(chips).filter(Boolean).sort();
}
export function getConfig() {
const cfg = vscode.workspace.getConfiguration('fmdCompiler');
return {
compilerPath: cfg.get<string>('compilerPath', 'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe'),
compilerSearchPaths: cfg.get<string[]>('compilerSearchPaths', [
'C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe',
'C:\\Program Files\\CCompiler\\Compiler\\data\\bin\\c.exe',
]),
projectFile: cfg.get<string>('projectFile', ''),
chip: cfg.get<string>('chip', 'FT61FC6X'),
outputDir: cfg.get<string>('outputDir', ''),
extraArgs: cfg.get<string>('extraArgs', ''),
autoSaveBeforeBuild: cfg.get<boolean>('autoSaveBeforeBuild', true),
showOutputOnBuild: cfg.get<boolean>('showOutputOnBuild', true),
programmerPath: cfg.get<string>('programmerPath', ''),
programmerArgs: cfg.get<string[]>('programmerArgs', []),
programmerCwd: cfg.get<string>('programmerCwd', '${projectDir}'),
programmerUseShell: cfg.get<boolean>('programmerUseShell', false),
programmerSuccessExitCodes: cfg.get<number[]>('programmerSuccessExitCodes', [0]),
downloadFileType: cfg.get<'hex' | 'bin'>('downloadFileType', 'hex'),
autoBuildBeforeDownload: cfg.get<boolean>('autoBuildBeforeDownload', false),
showOutputOnDownload: cfg.get<boolean>('showOutputOnDownload', true),
eepromBaseAddress: cfg.get<string>('eepromBaseAddress', '0x2100'),
eepromStart: cfg.get<string>('eepromStart', '0x00'),
eepromSize: cfg.get<number>('eepromSize', 112),
eepromFill: cfg.get<string>('eepromFill', '0xFF'),
eepromImageFile: cfg.get<string>('eepromImageFile', ''),
eepromReadArgs: cfg.get<string[]>('eepromReadArgs', []),
eepromWriteArgs: cfg.get<string[]>('eepromWriteArgs', []),
};
}
+101
View File
@@ -0,0 +1,101 @@
export interface HexRecord {
address: number;
type: number;
data: number[];
}
export function parseIntelHex(text: string): HexRecord[] {
const records: HexRecord[] = [];
let upperAddress = 0;
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) {
continue;
}
if (!line.startsWith(':')) {
throw new Error(`无效 Intel HEX 行: ${line}`);
}
const bytes: number[] = [];
for (let i = 1; i < line.length; i += 2) {
bytes.push(parseInt(line.slice(i, i + 2), 16));
}
const count = bytes[0];
const address = (bytes[1] << 8) | bytes[2];
const type = bytes[3];
const data = bytes.slice(4, 4 + count);
const checksum = bytes[4 + count];
const sum = bytes.slice(0, 4 + count).reduce((a, b) => a + b, 0);
if (((sum + checksum) & 0xff) !== 0) {
throw new Error(`Intel HEX 校验失败: ${line}`);
}
if (type === 0x00) {
records.push({ address: upperAddress + address, type, data });
} else if (type === 0x01) {
break;
} else if (type === 0x04) {
upperAddress = (((data[0] << 8) | data[1]) << 16) >>> 0;
records.push({ address, type, data });
} else {
records.push({ address: upperAddress + address, type, data });
}
}
return records;
}
export function extractRange(text: string, baseAddress: number, size: number, fill: number): Uint8Array {
const data = new Uint8Array(size);
data.fill(fill & 0xff);
for (const record of parseIntelHex(text)) {
if (record.type !== 0x00) {
continue;
}
record.data.forEach((value, index) => {
const address = record.address + index;
if (address >= baseAddress && address < baseAddress + size) {
data[address - baseAddress] = value;
}
});
}
return data;
}
export function writeIntelHex(data: Uint8Array, baseAddress: number, bytesPerLine = 16): string {
const lines: string[] = [];
let currentUpper = -1;
for (let offset = 0; offset < data.length; offset += bytesPerLine) {
const absolute = baseAddress + offset;
const upper = absolute >>> 16;
if (upper !== currentUpper) {
currentUpper = upper;
lines.push(makeRecord(0, 0x04, [(upper >> 8) & 0xff, upper & 0xff]));
}
const chunk = Array.from(data.slice(offset, offset + bytesPerLine));
lines.push(makeRecord(absolute & 0xffff, 0x00, chunk));
}
lines.push(':00000001FF');
return lines.join('\r\n') + '\r\n';
}
function makeRecord(address: number, type: number, data: number[]): string {
const bytes = [
data.length & 0xff,
(address >> 8) & 0xff,
address & 0xff,
type & 0xff,
...data.map(v => v & 0xff),
];
const sum = bytes.reduce((a, b) => a + b, 0);
const checksum = ((~sum + 1) & 0xff);
return ':' + [...bytes, checksum].map(b => b.toString(16).toUpperCase().padStart(2, '0')).join('');
}
+132
View File
@@ -0,0 +1,132 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { getConfig } from './extension';
import { FmdCompiler, FmdOutputArtifacts } from './compiler';
import { FmdProjectManager } from './projectManager';
import { expandArgs, expandTokens, parseCommandLine, TokenContext, ToolRunner } from './toolRunner';
export class FmdProgrammer {
private runner = new ToolRunner();
constructor(
private outputChannel: vscode.OutputChannel,
private projectManager: FmdProjectManager,
private compiler: FmdCompiler
) {}
async configureProgrammer(): Promise<void> {
const cfg = getConfig();
const files = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: '选择烧录/下载工具',
defaultUri: cfg.programmerPath ? vscode.Uri.file(path.dirname(cfg.programmerPath)) : undefined,
filters: {
'可执行文件': ['exe', 'bat', 'cmd'],
'所有文件': ['*'],
},
});
if (!files || files.length === 0) {
return;
}
const programmerPath = files[0].fsPath;
await vscode.workspace.getConfiguration('fmdCompiler').update('programmerPath', programmerPath, vscode.ConfigurationTarget.Workspace);
const argText = await vscode.window.showInputBox({
title: '烧录工具参数',
prompt: '可使用 ${chip}、${hexFile}、${binFile}、${downloadFile} 等变量',
value: cfg.programmerArgs.join(' '),
placeHolder: '--chip ${chip} --file ${hexFile} --program',
});
if (argText !== undefined) {
await vscode.workspace.getConfiguration('fmdCompiler').update('programmerArgs', parseCommandLine(argText), vscode.ConfigurationTarget.Workspace);
}
vscode.window.showInformationMessage('FMD: 烧录工具配置已保存');
}
async buildAndDownload(): Promise<void> {
const result = await this.compiler.buildProjectWithResult();
if (!result.success) {
vscode.window.showErrorMessage('FMD: 编译失败,已取消下载');
return;
}
await this.download(result.artifacts);
}
async download(artifacts?: FmdOutputArtifacts): Promise<void> {
const cfg = getConfig();
const resolvedArtifacts = artifacts || await this.compiler.resolveOutputArtifacts();
if (!resolvedArtifacts) {
return;
}
if (!cfg.programmerPath) {
vscode.window.showWarningMessage('FMD: 未配置烧录工具路径,请先运行 “FMD: Configure Programmer”');
return;
}
if (cfg.programmerArgs.length === 0) {
vscode.window.showWarningMessage('FMD: 未配置烧录工具参数,请先运行 “FMD: Configure Programmer”');
return;
}
const downloadFile = cfg.downloadFileType === 'bin' ? resolvedArtifacts.binFile : resolvedArtifacts.hexFile;
if (!fs.existsSync(downloadFile)) {
vscode.window.showErrorMessage(`FMD: 未找到下载文件: ${downloadFile},请先编译工程`);
return;
}
if (cfg.showOutputOnDownload) {
this.outputChannel.show(true);
}
const context = this.createTokenContext(resolvedArtifacts, downloadFile);
const cwd = expandTokens(cfg.programmerCwd || '${projectDir}', context);
const args = expandArgs(cfg.programmerArgs, context);
try {
const result = await this.runner.run({
executable: cfg.programmerPath,
args,
cwd,
shell: cfg.programmerUseShell,
outputChannel: this.outputChannel,
label: 'FMD 下载程序',
successExitCodes: cfg.programmerSuccessExitCodes,
});
if (result.success) {
vscode.window.showInformationMessage('FMD: 下载完成 ✓');
} else {
vscode.window.showErrorMessage(`FMD: 下载失败,退出码 ${result.exitCode}`);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.outputChannel.appendLine(`[错误] ${msg}`);
vscode.window.showErrorMessage(`FMD 下载异常: ${msg}`);
}
}
createTokenContext(artifacts: FmdOutputArtifacts, downloadFile?: string): TokenContext {
const cfg = getConfig();
const projectFile = this.projectManager.getProjectFile() || cfg.projectFile;
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
return {
chip: cfg.chip,
projectFile,
projectDir: artifacts.projectDir,
projectName: artifacts.projectName,
compilerPath: cfg.compilerPath,
hexFile: artifacts.hexFile,
binFile: artifacts.binFile,
downloadFile: downloadFile || (cfg.downloadFileType === 'bin' ? artifacts.binFile : artifacts.hexFile),
workspaceFolder,
};
}
}
+215
View File
@@ -0,0 +1,215 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
export interface FmdProjectInfo {
projectFile: string;
projectDir: string;
projectName: string;
device?: string;
sourceFiles: string[];
headerFiles: string[];
eeFile?: string;
encoding: BufferEncoding;
}
export class FmdProjectManager {
private projectDir: string | undefined;
private projectFile: string | undefined;
/**
* 自动在工作区中搜索 .prj 文件
*/
autoDetectProject(): void {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
return;
}
for (const folder of workspaceFolders) {
const prjFiles = this.findPrjFiles(folder.uri.fsPath);
if (prjFiles.length === 1) {
this.projectFile = prjFiles[0];
this.projectDir = path.dirname(prjFiles[0]);
console.log(`[FMD] 自动检测到工程: ${this.projectFile}`);
return;
} else if (prjFiles.length > 1) {
// 多个工程文件,不自动选择
console.log(`[FMD] 发现多个工程文件,请手动选择`);
return;
}
}
}
/**
* 在目录下搜索 .prj 文件(最多2层深度)
*/
private findPrjFiles(dir: string, depth = 0): string[] {
if (depth > 2) {
return [];
}
const result: string[] = [];
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.toLowerCase().endsWith('.prj')) {
result.push(path.join(dir, entry.name));
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
result.push(...this.findPrjFiles(path.join(dir, entry.name), depth + 1));
}
}
} catch {
// 忽略权限错误
}
return result;
}
setProjectFile(filePath: string): void {
this.projectFile = filePath;
this.projectDir = path.dirname(filePath);
}
getProjectDir(): string | undefined {
// 优先用已设置的工程目录
if (this.projectDir) {
return this.projectDir;
}
// 其次用当前活动文件所在目录
const editor = vscode.window.activeTextEditor;
if (editor && /\.[cChH]$/.test(editor.document.fileName)) {
return path.dirname(editor.document.fileName);
}
return undefined;
}
getProjectFile(): string | undefined {
if (this.projectFile && fs.existsSync(this.projectFile)) {
return this.projectFile;
}
const dir = this.getProjectDir();
if (!dir) {
return undefined;
}
const files = this.findPrjFiles(dir, 2);
return files.length === 1 ? files[0] : undefined;
}
getProjectInfo(projectFile?: string): FmdProjectInfo | undefined {
const file = projectFile || this.getProjectFile();
if (!file || !fs.existsSync(file)) {
return undefined;
}
return this.readProjectInfo(file);
}
readProjectInfo(projectFile: string): FmdProjectInfo {
const { text, encoding } = this.readProjectText(projectFile);
const projectDir = path.dirname(projectFile);
const rawName = this.matchValue(text, /^\s*Projece\s+Name\s*=\s*(.+)$/im);
const projectName = path.basename(rawName || projectFile, path.extname(rawName || projectFile));
const device = this.matchValue(text, /^\s*Device\s*=\s*(.+)$/im);
const sourceLine = this.matchValue(text, /^\s*Source\s+File\s*=\s*(.*)$/im) || '';
const eeLine = this.matchValue(text, /^\s*EE\s+File\s*=\s*(.*)$/im) || '';
const headerFiles: string[] = [];
const headerPattern = /^\s*Head\s+File\s+\d+\s*=\s*(.+)$/gim;
let m: RegExpExecArray | null;
while ((m = headerPattern.exec(text)) !== null) {
const value = m[1].trim();
if (value) {
headerFiles.push(this.resolveProjectPath(projectDir, value));
}
}
const sourceFiles = sourceLine
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0)
.map(s => this.resolveProjectPath(projectDir, s));
return {
projectFile,
projectDir,
projectName,
device,
sourceFiles,
headerFiles,
eeFile: eeLine.trim() ? this.resolveProjectPath(projectDir, eeLine.trim()) : undefined,
encoding,
};
}
async updateProjectDevice(projectFile: string, chip: string): Promise<void> {
const { text, encoding } = this.readProjectText(projectFile);
const pattern = /^(\s*Device\s*=\s*).+$/im;
const nextText = pattern.test(text)
? text.replace(pattern, `$1${chip}`)
: `${text.replace(/\s*$/, '')}\r\nDevice = ${chip}\r\n`;
fs.writeFileSync(projectFile, Buffer.from(nextText, encoding));
}
getCurrentChip(projectFile?: string): string | undefined {
return this.getProjectInfo(projectFile)?.device;
}
/**
* 弹出文件选择框让用户选择 .prj 文件
*/
async pickProjectFile(): Promise<void> {
const files = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: '选择 FMD 工程文件',
filters: {
'FMD Project': ['prj'],
'所有文件': ['*'],
},
});
if (files && files.length > 0) {
this.setProjectFile(files[0].fsPath);
vscode.window.showInformationMessage(`已选择工程: ${path.basename(files[0].fsPath)}`);
}
}
private readProjectText(filePath: string): { text: string; encoding: BufferEncoding } {
const buf = fs.readFileSync(filePath);
const encoding = this.detectEncoding(buf);
let text = buf.toString(encoding);
if (text.charCodeAt(0) === 0xfeff) {
text = text.slice(1);
}
return { text, encoding };
}
private detectEncoding(buf: Buffer): BufferEncoding {
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
return 'utf16le';
}
const sampleLength = Math.min(buf.length, 200);
let nulCount = 0;
for (let i = 0; i < sampleLength; i++) {
if (buf[i] === 0) {
nulCount++;
}
}
return nulCount > sampleLength / 4 ? 'utf16le' : 'utf8';
}
private matchValue(text: string, pattern: RegExp): string | undefined {
const m = pattern.exec(text);
return m?.[1]?.trim();
}
private resolveProjectPath(projectDir: string, value: string): string {
return path.isAbsolute(value) ? value : path.join(projectDir, value);
}
}
+140
View File
@@ -0,0 +1,140 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import { spawn } from 'child_process';
export interface ToolRunOptions {
executable: string;
args: string[];
cwd: string;
shell?: boolean;
outputChannel: vscode.OutputChannel;
label: string;
successExitCodes?: number[];
}
export interface ToolRunResult {
exitCode: number;
stdout: string;
stderr: string;
success: boolean;
}
export type TokenContext = Record<string, string | undefined>;
export function expandTokens(input: string, context: TokenContext): string {
return input.replace(/\$\{([A-Za-z0-9_]+)\}/g, (match, key) => context[key] ?? match);
}
export function expandArgs(args: string[], context: TokenContext): string[] {
return args.map(arg => expandTokens(arg, context));
}
export function parseCommandLine(input: string): string[] {
const args: string[] = [];
let current = '';
let quote: string | undefined;
let escaped = false;
for (const ch of input) {
if (escaped) {
current += ch;
escaped = false;
continue;
}
if (ch === '\\') {
escaped = true;
continue;
}
if (quote) {
if (ch === quote) {
quote = undefined;
} else {
current += ch;
}
continue;
}
if (ch === '"' || ch === "'") {
quote = ch;
continue;
}
if (/\s/.test(ch)) {
if (current.length > 0) {
args.push(current);
current = '';
}
continue;
}
current += ch;
}
if (escaped) {
current += '\\';
}
if (current.length > 0) {
args.push(current);
}
return args;
}
export class ToolRunner {
async run(options: ToolRunOptions): Promise<ToolRunResult> {
const successExitCodes = options.successExitCodes || [0];
if (!options.shell && !fs.existsSync(options.executable)) {
throw new Error(`工具不存在: ${options.executable}`);
}
options.outputChannel.appendLine('');
options.outputChannel.appendLine(`========== ${options.label} ==========`);
options.outputChannel.appendLine(`工作目录: ${options.cwd}`);
options.outputChannel.appendLine(`命令: ${options.executable} ${options.args.join(' ')}`);
options.outputChannel.appendLine('');
return new Promise((resolve, reject) => {
const proc = spawn(options.executable, options.args, {
cwd: options.cwd,
shell: options.shell || false,
env: { ...process.env },
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data: Buffer) => {
const text = data.toString();
stdout += text;
this.writeLines(options.outputChannel, text);
});
proc.stderr.on('data', (data: Buffer) => {
const text = data.toString();
stderr += text;
this.writeLines(options.outputChannel, text);
});
proc.on('close', (code: number | null) => {
const exitCode = code ?? 1;
const success = successExitCodes.includes(exitCode);
options.outputChannel.appendLine('');
options.outputChannel.appendLine(`========== ${options.label}${success ? '成功' : '失败'} (退出码: ${exitCode}) ==========`);
resolve({ exitCode, stdout, stderr, success });
});
proc.on('error', (err: Error) => reject(err));
});
}
private writeLines(outputChannel: vscode.OutputChannel, text: string): void {
text.split('\n').forEach(line => {
if (line.trim()) {
outputChannel.appendLine(line.replace(/\r$/, ''));
}
});
}
}