diff --git a/package-lock.json b/package-lock.json index 5970c2a..5cd55bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fmd-c-compiler", - "version": "0.1.0", + "version": "0.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fmd-c-compiler", - "version": "0.1.0", + "version": "0.2.8", "devDependencies": { "@types/node": "^20.0.0", "@types/vscode": "^1.85.0", diff --git a/package.json b/package.json index 168fc60..1c2a16e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fmd-c-compiler", "displayName": "FMD C Compiler", "description": "FMD/FT61FC6X 系列 MCU 编译器支持(C.exe 工具链)", - "version": "0.2.0", + "version": "0.2.8", "license": "MIT", "icon": "resources/icon.png", "engines": { @@ -78,6 +78,11 @@ { "command": "fmdCompiler.exportEepromHex", "title": "FMD: Export EEPROM HEX" + }, + { + "command": "fmdCompiler.regenerateConfig", + "title": "FMD: Regenerate VS Code Config", + "icon": "$(gear)" } ], "keybindings": [ @@ -98,6 +103,11 @@ "command": "fmdCompiler.download", "when": "resourceExtname =~ /\\.[cChH]$/", "group": "navigation" + }, + { + "command": "fmdCompiler.regenerateConfig", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "navigation" } ], "editor/context": [ @@ -125,6 +135,11 @@ "command": "fmdCompiler.openEeprom", "when": "resourceExtname =~ /\\.[cChH]$/", "group": "fmd@5" + }, + { + "command": "fmdCompiler.regenerateConfig", + "when": "resourceExtname =~ /\\.[cChH]$/", + "group": "fmd@6" } ], "explorer/context": [ @@ -152,6 +167,11 @@ "command": "fmdCompiler.openEeprom", "when": "resourceExtname == '.prj' || resourceExtname == '.hex'", "group": "fmd@5" + }, + { + "command": "fmdCompiler.regenerateConfig", + "when": "resourceExtname == '.prj' || resourceExtname =~ /\\.[cChH]$/", + "group": "fmd@6" } ] }, @@ -186,8 +206,8 @@ }, "fmdCompiler.outputDir": { "type": "string", - "default": "", - "description": "输出目录(留空则与工程同目录)" + "default": "build", + "description": "输出目录。默认 build 会输出到工程目录下的 build 文件夹;留空则与工程同目录;也可填写绝对路径。" }, "fmdCompiler.extraArgs": { "type": "string", diff --git a/src/compiler.ts b/src/compiler.ts index df346c7..e40a714 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -70,7 +70,8 @@ export class FmdCompiler { } const projectName = projectInfo?.projectName || path.basename(projectDir); - const outputDir = cfg.outputDir || projectDir; + const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir); + fs.mkdirSync(outputDir, { recursive: true }); const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir); this.building = true; @@ -229,7 +230,7 @@ export class FmdCompiler { } const projectName = projectInfo?.projectName || path.basename(projectDir); - const outputDir = cfg.outputDir || projectDir; + const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir); return this.getOutputArtifacts(projectDir, projectName, outputDir); } @@ -243,6 +244,13 @@ export class FmdCompiler { }; } + private resolveOutputDir(projectDir: string, outputDir: string): string { + if (!outputDir) { + return projectDir; + } + return path.isAbsolute(outputDir) ? outputDir : path.join(projectDir, outputDir); + } + /** * 构造编译参数 * 基于对 .map 文件的分析,c.exe 是 XC8-style 驱动器 diff --git a/src/extension.ts b/src/extension.ts index 2989643..1f4cc83 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -36,13 +36,16 @@ export function activate(context: vscode.ExtensionContext) { } }), vscode.commands.registerCommand('fmdCompiler.clean', () => compiler.cleanProject()), - vscode.commands.registerCommand('fmdCompiler.selectProject', (uri?: vscode.Uri) => { + vscode.commands.registerCommand('fmdCompiler.selectProject', async (uri?: vscode.Uri) => { if (uri) { projectManager.setProjectFile(uri.fsPath); vscode.window.showInformationMessage(`已选择工程: ${path.basename(uri.fsPath)}`); } else { - projectManager.pickProjectFile(); + await projectManager.pickProjectFile(); } + await ensureWorkspaceSettings(); + ensureCppProperties(); + ensureGitignore(); updateStatusBars(); }), vscode.commands.registerCommand('fmdCompiler.openOutput', () => { @@ -59,6 +62,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('fmdCompiler.readEeprom', () => eepromManager.readEeprom()), vscode.commands.registerCommand('fmdCompiler.writeEeprom', () => eepromManager.writeEeprom()), vscode.commands.registerCommand('fmdCompiler.exportEepromHex', () => eepromManager.exportEepromHex()), + vscode.commands.registerCommand('fmdCompiler.regenerateConfig', () => regenerateConfig()), diagnosticsCollection ); @@ -69,6 +73,8 @@ export function activate(context: vscode.ExtensionContext) { chipStatusBar.command = 'fmdCompiler.selectChip'; const downloadStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 98); downloadStatusBar.command = 'fmdCompiler.download'; + const configStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 97); + configStatusBar.command = 'fmdCompiler.regenerateConfig'; const update = () => { const cfg = getConfig(); @@ -79,13 +85,16 @@ export function activate(context: vscode.ExtensionContext) { chipStatusBar.tooltip = '切换 FMD 目标芯片'; downloadStatusBar.text = '$(cloud-upload) FMD Download'; downloadStatusBar.tooltip = cfg.programmerPath ? `下载程序: ${cfg.programmerPath}` : '未配置烧录工具,点击配置后可下载'; + configStatusBar.text = '$(gear) FMD Config'; + configStatusBar.tooltip = '一键重新生成 .gitignore 和 .vscode 配置'; statusBar.show(); chipStatusBar.show(); downloadStatusBar.show(); + configStatusBar.show(); }; updateStatusBars = update; updateStatusBars(); - context.subscriptions.push(statusBar, chipStatusBar, downloadStatusBar); + context.subscriptions.push(statusBar, chipStatusBar, downloadStatusBar, configStatusBar); // 监听配置变化 context.subscriptions.push( @@ -99,6 +108,9 @@ export function activate(context: vscode.ExtensionContext) { // 尝试自动找工程文件 projectManager.autoDetectProject(); + ensureWorkspaceSettings(); + ensureCppProperties(); + ensureGitignore(); updateStatusBars(); outputChannel.appendLine('[FMD] 插件已激活'); @@ -112,6 +124,18 @@ export function deactivate() { let updateStatusBars = () => {}; +async function regenerateConfig(): Promise { + outputChannel.show(true); + outputChannel.appendLine(''); + outputChannel.appendLine('========== FMD 重新生成 VS Code 配置 =========='); + await ensureWorkspaceSettings(); + ensureCppProperties(); + ensureGitignore(); + updateStatusBars(); + outputChannel.appendLine('========== FMD 配置生成完成 =========='); + vscode.window.showInformationMessage('FMD: 已重新生成 .gitignore 和 .vscode 配置'); +} + async function setCompilerPath(): Promise { const cfg = getConfig(); const files = await vscode.window.showOpenDialog({ @@ -191,6 +215,344 @@ async function syncChipFromProject(): Promise { vscode.window.showInformationMessage(`FMD: 已从工程同步芯片: ${projectChip}`); } +async function ensureWorkspaceSettings(): Promise { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + return; + } + + for (const folder of folders) { + const settingsDir = path.join(folder.uri.fsPath, '.vscode'); + const settingsFile = path.join(settingsDir, 'settings.json'); + let settings: Record = {}; + + try { + if (fs.existsSync(settingsFile)) { + settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')) as Record; + } + } catch (err) { + outputChannel.appendLine(`[警告] 无法解析工作区设置,跳过自动写入: ${settingsFile}: ${err}`); + continue; + } + + let changed = false; + const cfg = getConfig(); + const projectFile = projectManager.getProjectFile() || findSinglePrjFile(folder.uri.fsPath) || cfg.projectFile; + const projectChip = projectFile ? projectManager.getCurrentChip(projectFile) : undefined; + + if (settings['fmdCompiler.outputDir'] === undefined) { + settings['fmdCompiler.outputDir'] = 'build'; + changed = true; + } + if (settings['fmdCompiler.compilerPath'] === undefined) { + settings['fmdCompiler.compilerPath'] = cfg.compilerPath; + changed = true; + } + if (settings['fmdCompiler.chip'] === undefined) { + settings['fmdCompiler.chip'] = projectChip || cfg.chip || 'FT61FC6X'; + changed = true; + } + if (projectFile && settings['fmdCompiler.projectFile'] === undefined) { + settings['fmdCompiler.projectFile'] = projectFile; + changed = true; + } + if (settings['fmdCompiler.autoSaveBeforeBuild'] === undefined) { + settings['fmdCompiler.autoSaveBeforeBuild'] = true; + changed = true; + } + if (settings['fmdCompiler.showOutputOnBuild'] === undefined) { + settings['fmdCompiler.showOutputOnBuild'] = true; + changed = true; + } + + if (changed) { + fs.mkdirSync(settingsDir, { recursive: true }); + fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n'); + outputChannel.appendLine(`[FMD] 已自动生成/更新工作区设置: ${settingsFile}`); + } + } +} + +function ensureCppProperties(): void { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + return; + } + + const cfg = getConfig(); + const compilerInclude = path.join(path.dirname(cfg.compilerPath), '..', 'include'); + + for (const folder of folders) { + const vscodeDir = path.join(folder.uri.fsPath, '.vscode'); + const propertiesFile = path.join(vscodeDir, 'c_cpp_properties.json'); + const projectFile = projectManager.getProjectFile() || findSinglePrjFile(folder.uri.fsPath) || cfg.projectFile; + const projectDir = projectFile ? path.dirname(projectFile) : folder.uri.fsPath; + const chip = (projectFile ? projectManager.getCurrentChip(projectFile) : undefined) || cfg.chip; + const intellisenseHeader = ensureFmdIntellisenseHeader(vscodeDir, compilerInclude, chip); + const includePath = [ + '${workspaceFolder}/**', + normalizeForCppProperties(projectDir), + normalizeForCppProperties(path.join(projectDir, '**')), + normalizeForCppProperties(compilerInclude), + ]; + const defines = [ + `_${chip}`, + '__GCC8PRO__', + '_CHIP_SELECT_H_', + ]; + const forcedInclude = [ + normalizeForCppProperties(intellisenseHeader), + ]; + let properties: { + configurations?: Array>; + version?: number; + [key: string]: unknown; + } = {}; + + try { + if (fs.existsSync(propertiesFile)) { + properties = JSON.parse(fs.readFileSync(propertiesFile, 'utf8')); + } + } catch (err) { + outputChannel.appendLine(`[警告] 无法解析 C/C++ 配置,跳过自动写入: ${propertiesFile}: ${err}`); + continue; + } + + if (!Array.isArray(properties.configurations) || properties.configurations.length === 0) { + properties.configurations = [{ + name: 'FMD', + includePath, + defines, + forcedInclude, + compilerPath: cfg.compilerPath, + cStandard: 'c99', + intelliSenseMode: 'windows-gcc-x86', + }]; + } else { + const configuration = properties.configurations[0]; + const currentIncludePath = Array.isArray(configuration.includePath) ? configuration.includePath as string[] : []; + const currentDefines = Array.isArray(configuration.defines) ? configuration.defines as string[] : []; + const currentForcedInclude = Array.isArray(configuration.forcedInclude) ? configuration.forcedInclude as string[] : []; + configuration.includePath = mergeUnique(currentIncludePath, includePath); + configuration.defines = mergeUnique(currentDefines, defines); + configuration.forcedInclude = mergeUnique(currentForcedInclude, forcedInclude); + if (!configuration.compilerPath) { + configuration.compilerPath = cfg.compilerPath; + } + if (!configuration.cStandard) { + configuration.cStandard = 'c99'; + } + if (!configuration.intelliSenseMode) { + configuration.intelliSenseMode = 'windows-gcc-x86'; + } + } + + if (!properties.version) { + properties.version = 4; + } + + fs.mkdirSync(vscodeDir, { recursive: true }); + fs.writeFileSync(propertiesFile, JSON.stringify(properties, null, 2) + '\n'); + outputChannel.appendLine(`[FMD] 已自动生成/更新 C/C++ 头文件路径: ${propertiesFile}`); + } +} + +function ensureFmdIntellisenseHeader(vscodeDir: string, compilerInclude: string, chip: string): string { + fs.mkdirSync(vscodeDir, { recursive: true }); + const target = path.join(vscodeDir, 'fmd_intellisense.h'); + const chipHeader = findChipHeader(compilerInclude, chip); + const names = chipHeader ? extractChipSymbols(chipHeader) : []; + const lines = [ + '/* Auto-generated by FMD C Compiler extension. */', + '/* This file is only for VS Code IntelliSense and is not used by c.exe. */', + '#ifndef FMD_INTELLISENSE_H', + '#define FMD_INTELLISENSE_H', + '', + '#ifndef __FMD_INTELLISENSE__', + '#define __FMD_INTELLISENSE__ 1', + '#endif', + '', + '#ifndef bit', + 'typedef unsigned char bit;', + '#endif', + '', + '#ifndef asm', + '#define asm(...)', + '#endif', + '', + '#ifndef interrupt', + '#define interrupt', + '#endif', + '', + `#ifndef _${chip}`, + `#define _${chip}`, + '#endif', + '', + ...names.map(name => `extern volatile unsigned char ${name};`), + '', + '#endif', + '', + ]; + + fs.writeFileSync(target, lines.join('\n')); + return target; +} + +function findChipHeader(compilerInclude: string, chip: string): string | undefined { + const candidates = [ + path.join(compilerInclude, `${chip}.h`), + path.join(compilerInclude, `${chip}.H`), + ]; + return candidates.find(file => fs.existsSync(file)); +} + +function extractChipSymbols(chipHeader: string): string[] { + const text = fs.readFileSync(chipHeader, 'utf8'); + const names = new Set(); + const patterns = [ + /volatile\s+(?:unsigned\s+char|bit)\s+([A-Za-z_][A-Za-z0-9_]*)\s*@/g, + /volatile\s+union\s*\{[\s\S]*?\}\s*([A-Za-z_][A-Za-z0-9_]*)\s*@/g, + ]; + + for (const pattern of patterns) { + let m: RegExpExecArray | null; + while ((m = pattern.exec(text)) !== null) { + names.add(m[1]); + } + } + + return Array.from(names).sort(); +} + +function normalizeForCppProperties(filePath: string): string { + return filePath.replace(/\\/g, '/'); +} + +function mergeUnique(first: string[], second: string[]): string[] { + const result: string[] = []; + for (const value of [...first, ...second]) { + if (value && !result.includes(value)) { + result.push(value); + } + } + return result; +} + +function ensureGitignore(): void { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + return; + } + + const patterns = [ + '.vscode', + '**/*.as', + '**/*.asm', + '**/*.bin', + '**/*.cmf', + '**/*.cof', + '**/*.d', + '**/*.hex', + '**/*.lpp', + '**/*.map', + '**/*.obj', + '**/*.p1', + '**/*.pre', + '**/*.rlf', + '**/*.sdb', + '**/*.sym', + '**/*.hxl', + '**/*.ini', + '**/*.rar', + '**/*.o', + '**/*.crf', + '**/*.htm', + '**/*.dep', + '**/*.bak', + '**/*.lnp', + '**/*.lst', + '**/*.iex', + '**/*.sct', + '**/*.scvd', + '**/*.uvguix', + '**/*.dbg*', + '**/*.uvguix.*', + '**/.mxproject', + '**/*.uvopt', + '**/*.uvgui.*', + '**/Listings', + '**/output', + '**/*.zip', + ]; + + const blockStart = '# FMD generated ignores'; + const blockEnd = '# End FMD generated ignores'; + const block = [blockStart, ...patterns, blockEnd].join('\n'); + + for (const folder of folders) { + const gitignoreFile = path.join(folder.uri.fsPath, '.gitignore'); + let text = ''; + + if (fs.existsSync(gitignoreFile)) { + text = fs.readFileSync(gitignoreFile, 'utf8'); + if (text.includes(blockStart) && text.includes(blockEnd)) { + const pattern = new RegExp(`${escapeRegExp(blockStart)}[\\s\\S]*?${escapeRegExp(blockEnd)}`); + const nextText = text.replace(pattern, block); + if (nextText !== text) { + fs.writeFileSync(gitignoreFile, ensureTrailingNewline(nextText)); + outputChannel.appendLine(`[FMD] 已更新 .gitignore: ${gitignoreFile}`); + } + continue; + } + } + + const missing = patterns.filter(p => !hasGitignorePattern(text, p)); + if (missing.length === 0) { + continue; + } + + const prefix = text.trim().length > 0 ? ensureTrailingNewline(text).replace(/\s*$/, '\n\n') : ''; + fs.writeFileSync(gitignoreFile, `${prefix}${block}\n`); + outputChannel.appendLine(`[FMD] 已自动生成/更新 .gitignore: ${gitignoreFile}`); + } +} + +function hasGitignorePattern(text: string, pattern: string): boolean { + return text.split(/\r?\n/).some(line => line.trim() === pattern); +} + +function ensureTrailingNewline(text: string): string { + return text.endsWith('\n') ? text : text + '\n'; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function findSinglePrjFile(folderPath: string): string | undefined { + const result: string[] = []; + const walk = (dir: string, depth: number) => { + if (depth > 2 || result.length > 1) { + return; + } + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isFile() && entry.name.toLowerCase().endsWith('.prj')) { + result.push(fullPath); + } else if (entry.isDirectory() && !entry.name.startsWith('.')) { + walk(fullPath, depth + 1); + } + } + } catch { + // 忽略权限错误 + } + }; + + walk(folderPath, 0); + return result.length === 1 ? result[0] : undefined; +} + function collectChipCandidates(): string[] { const cfg = getConfig(); const chips = new Set(['FT61FC6X', cfg.chip]); @@ -224,7 +586,7 @@ export function getConfig() { ]), projectFile: cfg.get('projectFile', ''), chip: cfg.get('chip', 'FT61FC6X'), - outputDir: cfg.get('outputDir', ''), + outputDir: cfg.get('outputDir', 'build'), extraArgs: cfg.get('extraArgs', ''), autoSaveBeforeBuild: cfg.get('autoSaveBeforeBuild', true), showOutputOnBuild: cfg.get('showOutputOnBuild', true),